From de230d6da1c2579039a16ed9869bdfbb5b2dc852 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Mon, 9 Oct 2023 17:08:00 -0400 Subject: [PATCH 01/11] Update serializers.py --- breathecode/events/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/breathecode/events/serializers.py b/breathecode/events/serializers.py index 19580f4f5..f883d6621 100644 --- a/breathecode/events/serializers.py +++ b/breathecode/events/serializers.py @@ -614,7 +614,7 @@ def validate(self, data): def create(self, validated_data): event_checkin = super().create(validated_data) - tasks_activity.add_activity.delay(event_checkin.attendee, + tasks_activity.add_activity.delay(event_checkin.attendee.id, 'event_checkin_created', related_type='events.EventCheckin', related_id=event_checkin.id) From a36c92b694c9e08392ebd3863bde4e3c9894c075 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Mon, 9 Oct 2023 22:39:35 +0000 Subject: [PATCH 02/11] now saving utm information from incoming events --- breathecode/assignments/serializers.py | 3 ++- breathecode/events/models.py | 5 +++++ .../tests/urls/tests_eventbrite_webhook_id.py | 20 +++++++++++++++++++ .../tests/urls/tests_me_event_id_join.py | 4 ++++ .../eventbrite/actions/order_placed.py | 1 + 5 files changed, 32 insertions(+), 1 deletion(-) diff --git a/breathecode/assignments/serializers.py b/breathecode/assignments/serializers.py index 7671a51cc..f3b592a16 100644 --- a/breathecode/assignments/serializers.py +++ b/breathecode/assignments/serializers.py @@ -215,7 +215,8 @@ def validate(self, data): return data def update(self, instance, validated_data): - if 'opened_at' in validated_data and validated_data['opened_at'] > instance.opened_at: + if 'opened_at' in validated_data and (instance.opened_at is None + or validated_data['opened_at'] > instance.opened_at): tasks_activity.add_activity.delay(self.context['request'].user.id, 'read_assignment', related_type='assignments.Task', diff --git a/breathecode/events/models.py b/breathecode/events/models.py index ab135c703..4e5ab8d51 100644 --- a/breathecode/events/models.py +++ b/breathecode/events/models.py @@ -300,6 +300,11 @@ def __init__(self, *args, **kwargs): event = models.ForeignKey(Event, on_delete=models.CASCADE) status = models.CharField(max_length=9, choices=CHECKIN_STATUS, default=PENDING) + utm_medium = models.CharField(max_length=70, blank=True, null=True, default=None) + utm_campaign = models.CharField(max_length=70, blank=True, null=True, default=None) + utm_source = models.CharField(max_length=70, blank=True, null=True, default=None) + utm_url = models.CharField(max_length=2000, null=True, default=None, blank=True) + created_at = models.DateTimeField(auto_now_add=True, editable=False) updated_at = models.DateTimeField(auto_now=True, editable=False) attended_at = models.DateTimeField(null=True, default=None, blank=True) diff --git a/breathecode/events/tests/urls/tests_eventbrite_webhook_id.py b/breathecode/events/tests/urls/tests_eventbrite_webhook_id.py index c373bebcb..b34a52074 100644 --- a/breathecode/events/tests/urls/tests_eventbrite_webhook_id.py +++ b/breathecode/events/tests/urls/tests_eventbrite_webhook_id.py @@ -219,6 +219,10 @@ def test_eventbrite_webhook_without_active_campaign_academy(self): 'email': 'john.smith@example.com', 'event_id': 1, 'id': 1, + 'utm_campaign': None, + 'utm_medium': None, + 'utm_source': None, + 'utm_url': None, 'status': 'PENDING', 'attended_at': None }]) @@ -263,6 +267,10 @@ def test_eventbrite_webhook_without_automation(self): self.assertEqual(self.all_event_checkin_dict(), [{ 'attendee_id': None, 'email': 'john.smith@example.com', + 'utm_campaign': None, + 'utm_medium': None, + 'utm_source': None, + 'utm_url': None, 'event_id': 1, 'id': 1, 'status': 'PENDING', @@ -321,6 +329,10 @@ def test_eventbrite_webhook_without_lang(self): self.assertEqual(self.all_event_checkin_dict(), [{ 'attendee_id': 1, + 'utm_campaign': None, + 'utm_medium': None, + 'utm_source': None, + 'utm_url': None, 'email': 'john.smith@example.com', 'event_id': 1, 'id': 1, @@ -417,6 +429,10 @@ def test_eventbrite_webhook_without_lang__active_campaign_belong_to_other_academ self.assertEqual(self.all_event_checkin_dict(), [{ 'attendee_id': 1, + 'utm_campaign': None, + 'utm_medium': None, + 'utm_source': None, + 'utm_url': None, 'email': 'john.smith@example.com', 'event_id': 1, 'id': 1, @@ -514,6 +530,10 @@ def test_eventbrite_webhook(self): self.assertEqual(self.all_event_checkin_dict(), [{ 'attendee_id': 1, + 'utm_campaign': None, + 'utm_medium': None, + 'utm_source': None, + 'utm_url': None, 'email': 'john.smith@example.com', 'event_id': 1, 'id': 1, diff --git a/breathecode/events/tests/urls/tests_me_event_id_join.py b/breathecode/events/tests/urls/tests_me_event_id_join.py index 97d2b4422..f77c48e06 100644 --- a/breathecode/events/tests/urls/tests_me_event_id_join.py +++ b/breathecode/events/tests/urls/tests_me_event_id_join.py @@ -45,6 +45,10 @@ def event_checkin_serializer(id, event, user): return { 'attended_at': UTC_NOW, 'attendee_id': user.id, + 'utm_campaign': None, + 'utm_medium': None, + 'utm_source': None, + 'utm_url': None, 'email': user.email, 'event_id': event.id, 'id': id, diff --git a/breathecode/services/eventbrite/actions/order_placed.py b/breathecode/services/eventbrite/actions/order_placed.py index fed9fa2b2..2a7fd21c5 100644 --- a/breathecode/services/eventbrite/actions/order_placed.py +++ b/breathecode/services/eventbrite/actions/order_placed.py @@ -50,6 +50,7 @@ def order_placed(self, webhook, payload: dict): elif not EventCheckin.objects.filter(email=email, event=local_event, attendee=local_attendee).count(): event_checkin = EventCheckin.objects.filter(email=email, event=local_event).first() event_checkin.attendee = local_attendee + event_checkin.utm_source = 'eventbrite' event_checkin.save() contact = { From 90754f090a6766fe4d9c040ac957539dc13340d9 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Mon, 9 Oct 2023 22:41:49 +0000 Subject: [PATCH 03/11] now saving utm information from incoming events --- .../migrations/0055_auto_20231009_2240.py | 33 +++++++++++++++++++ breathecode/events/serializers.py | 4 +++ 2 files changed, 37 insertions(+) create mode 100644 breathecode/events/migrations/0055_auto_20231009_2240.py diff --git a/breathecode/events/migrations/0055_auto_20231009_2240.py b/breathecode/events/migrations/0055_auto_20231009_2240.py new file mode 100644 index 000000000..1b50ae008 --- /dev/null +++ b/breathecode/events/migrations/0055_auto_20231009_2240.py @@ -0,0 +1,33 @@ +# Generated by Django 3.2.21 on 2023-10-09 22:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('events', '0054_alter_event_uuid'), + ] + + operations = [ + migrations.AddField( + model_name='eventcheckin', + name='utm_campaign', + field=models.CharField(blank=True, default=None, max_length=70, null=True), + ), + migrations.AddField( + model_name='eventcheckin', + name='utm_medium', + field=models.CharField(blank=True, default=None, max_length=70, null=True), + ), + migrations.AddField( + model_name='eventcheckin', + name='utm_source', + field=models.CharField(blank=True, default=None, max_length=70, null=True), + ), + migrations.AddField( + model_name='eventcheckin', + name='utm_url', + field=models.CharField(blank=True, default=None, max_length=2000, null=True), + ), + ] diff --git a/breathecode/events/serializers.py b/breathecode/events/serializers.py index f883d6621..c152b5ab0 100644 --- a/breathecode/events/serializers.py +++ b/breathecode/events/serializers.py @@ -416,6 +416,10 @@ class EventHookCheckinSerializer(serpy.Serializer): id = serpy.Field() email = serpy.Field() status = serpy.Field() + utm_url = serpy.Field() + utm_source = serpy.Field() + utm_campaign = serpy.Field() + utm_medium = serpy.Field() created_at = serpy.Field() attended_at = serpy.Field() attendee = UserSerializer(required=False) From b5b5fc1e929480217c36df2b4cb92f2f2581d364 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Mon, 9 Oct 2023 19:11:35 -0400 Subject: [PATCH 04/11] Update admin.py --- breathecode/events/admin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/breathecode/events/admin.py b/breathecode/events/admin.py index 2bd3e0c6e..e150e6efc 100644 --- a/breathecode/events/admin.py +++ b/breathecode/events/admin.py @@ -104,8 +104,8 @@ class EventTypeAdmin(admin.ModelAdmin): # Register your models here. @admin.register(EventCheckin) class EventCheckinAdmin(admin.ModelAdmin): - list_display = ('id', 'email', 'attendee', 'event', 'status', 'created_at', 'attended_at') - list_filter = ['status'] + list_display = ('id', 'email', 'attendee', 'event', 'status', 'created_at', 'attended_at', 'utm_source') + list_filter = ['status', 'utm_source', 'utm_medium'] search_fields = ['email', 'event__title', 'event__slug'] raw_id_fields = ['event', 'attendee'] From 99cf7e5b0d8ca16e9594a79d63cc7d9586a960bd Mon Sep 17 00:00:00 2001 From: jefer94 Date: Mon, 9 Oct 2023 18:55:01 -0500 Subject: [PATCH 05/11] accept an invite using an api rest --- breathecode/authenticate/serializers.py | 2 +- .../tests/urls/tests_member_invite_token.py | 108 +++++ .../tests/urls/tests_subscribe.py | 406 +++++++++--------- breathecode/authenticate/views.py | 244 ++++++----- 4 files changed, 449 insertions(+), 311 deletions(-) diff --git a/breathecode/authenticate/serializers.py b/breathecode/authenticate/serializers.py index a0b845ff3..1234905c7 100644 --- a/breathecode/authenticate/serializers.py +++ b/breathecode/authenticate/serializers.py @@ -1291,7 +1291,7 @@ def validate(self, data: dict[str, str]): now = str(timezone.now()) if not self.instance: - data['token'] = hashlib.sha1((now + data['email']).encode('UTF-8')).hexdigest() + data['token'] = hashlib.sha512((data['email']).encode('UTF-8') + os.urandom(64)).hexdigest() return data diff --git a/breathecode/authenticate/tests/urls/tests_member_invite_token.py b/breathecode/authenticate/tests/urls/tests_member_invite_token.py index babe78f1e..00018586b 100644 --- a/breathecode/authenticate/tests/urls/tests_member_invite_token.py +++ b/breathecode/authenticate/tests/urls/tests_member_invite_token.py @@ -6,6 +6,7 @@ from unittest.mock import MagicMock, patch from django.template import loader from django.urls.base import reverse_lazy +import pytest from rest_framework import status from django.http.request import HttpRequest from random import randint @@ -139,6 +140,28 @@ def salt(self): return 'salt' +def post_serializer(data={}): + return { + 'created_at': ..., + 'email': None, + 'id': 0, + 'sent_at': None, + 'status': 'PENDING', + **data, + } + + +created_at = None + + +@pytest.fixture(autouse=True) +def setup(monkeypatch, utc_now): + global created_at + created_at = utc_now + monkeypatch.setattr('django.utils.timezone.now', MagicMock(return_value=utc_now)) + yield + + class AuthenticateTestSuite(AuthTestCase): """Authentication test suite""" """ @@ -1020,3 +1043,88 @@ def test_member_invite_token__post__with_cohort__without_role(self): ]) self.assertEqual(self.bc.database.list_of('admissions.CohortUser'), []) + + """ + 🔽🔽🔽 POST JSON password is empty, UserInvite with email + """ + + @patch('django.template.loader.render_to_string', MagicMock(side_effect=render_to_string_mock)) + @patch('django.contrib.auth.hashers.get_hasher', MagicMock(side_effect=GetHasherMock)) + @patch('django.db.models.signals.pre_delete.send', MagicMock(return_value=None)) + @patch('breathecode.admissions.signals.student_edu_status_updated.send', MagicMock(return_value=None)) + def test__post__json__password_is_empty(self): + user_invite = {'email': 'user@dotdotdotdot.dot'} + model = self.bc.database.create(user_invite=user_invite) + url = reverse_lazy('authenticate:member_invite_token', kwargs={'token': model.user_invite.token}) + + data = {'first_name': 'abc', 'last_name': 'xyz'} + response = self.client.post(url, data, format='json') + + json = response.json() + expected = {'detail': 'Password is empty', 'status_code': 400} + + self.assertEqual(json, expected) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(self.bc.database.list_of('authenticate.UserInvite'), [ + self.bc.format.to_dict(model.user_invite), + ]) + + self.assertEqual(self.bc.database.list_of('auth.User'), []) + self.assertEqual(self.bc.database.list_of('authenticate.ProfileAcademy'), []) + self.assertEqual(self.bc.database.list_of('admissions.CohortUser'), []) + + """ + 🔽🔽🔽 POST JSON with first name, last name and passwords, UserInvite with email + """ + + @patch('django.template.loader.render_to_string', MagicMock(side_effect=render_to_string_mock)) + @patch('django.contrib.auth.hashers.get_hasher', MagicMock(side_effect=GetHasherMock)) + @patch('django.db.models.signals.pre_delete.send', MagicMock(return_value=None)) + @patch('breathecode.admissions.signals.student_edu_status_updated.send', MagicMock(return_value=None)) + def test__post__json__with_first_name_last_name_and_passwords(self): + user_invite = {'email': 'user@dotdotdotdot.dot'} + model = self.bc.database.create(user_invite=user_invite) + url = reverse_lazy('authenticate:member_invite_token', kwargs={'token': model.user_invite.token}) + data = { + 'first_name': 'abc', + 'last_name': 'xyz', + 'password': '^3^3uUppppp', + 'repeat_password': '^3^3uUppppp', + } + response = self.client.post(url, data, format='json') + + json = response.json() + expected = post_serializer({ + 'id': 1, + 'created_at': self.bc.datetime.to_iso_string(created_at), + 'status': 'ACCEPTED', + 'email': 'user@dotdotdotdot.dot', + }) + + self.assertEqual(json, expected) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(self.bc.database.list_of('authenticate.UserInvite'), + [{ + **self.bc.format.to_dict(model.user_invite), + 'status': 'ACCEPTED', + 'is_email_validated': True, + }]) + + user_db = [ + x for x in self.bc.database.list_of('auth.User') if x['date_joined'] and x.pop('date_joined') + ] + self.assertEqual(user_db, [{ + 'email': 'user@dotdotdotdot.dot', + 'first_name': 'abc', + 'id': 1, + 'is_active': True, + 'is_staff': False, + 'is_superuser': False, + 'last_login': None, + 'last_name': 'xyz', + 'password': CSRF_TOKEN, + 'username': 'user@dotdotdotdot.dot' + }]) + + self.assertEqual(self.bc.database.list_of('authenticate.ProfileAcademy'), []) + self.assertEqual(self.bc.database.list_of('admissions.CohortUser'), []) diff --git a/breathecode/authenticate/tests/urls/tests_subscribe.py b/breathecode/authenticate/tests/urls/tests_subscribe.py index 54dc7751b..8d30df0f4 100644 --- a/breathecode/authenticate/tests/urls/tests_subscribe.py +++ b/breathecode/authenticate/tests/urls/tests_subscribe.py @@ -3,11 +3,13 @@ """ import hashlib from datetime import datetime +import os import random from unittest.mock import MagicMock, call, patch from django.urls.base import reverse_lazy from django.utils import timezone +import pytest from rest_framework import status from breathecode.notify import actions as notify_actions @@ -122,6 +124,15 @@ def put_serializer(user_invite, cohort=None, syllabus=None, plans=[], data={}): } +b = os.urandom(64) + + +@pytest.fixture(autouse=True) +def setup(monkeypatch): + monkeypatch.setattr('os.urandom', lambda _: b) + yield + + class SubscribeTestSuite(AuthTestCase): """Test /v1/auth/subscribe""" """ @@ -180,8 +191,7 @@ def test_task__post__without_user_invite(self): 'role_id': None, 'sent_at': None, 'status': 'ACCEPTED', - 'token': hashlib.sha1( - (str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest(), + 'token': hashlib.sha512('pokemon@potato.io'.encode('UTF-8') + b).hexdigest(), 'process_message': '', 'process_status': 'DONE', 'syllabus_id': None, @@ -422,7 +432,7 @@ def test_task__post__with_user_invite(self): 'role_id': None, 'sent_at': None, 'status': 'ACCEPTED', - 'token': hashlib.sha1((str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest(), + 'token': hashlib.sha512(('pokemon@potato.io').encode('UTF-8') + b).hexdigest(), 'process_message': '', 'process_status': 'DONE', 'syllabus_id': None, @@ -456,8 +466,8 @@ def test_task__post__with_user_invite(self): 'verify_email', 'pokemon@potato.io', { 'SUBJECT': '4Geeks - Validate account', - 'LINK': ('http://localhost:8000/v1/auth/password/' + hashlib.sha1( - (str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest()) + 'LINK': ('http://localhost:8000/v1/auth/password/' + + hashlib.sha512('pokemon@potato.io'.encode('UTF-8') + b).hexdigest()) }) ]) @@ -518,7 +528,7 @@ def test_task__post__does_not_get_in_waiting_list_using_a_plan(self): 'status': 'WAITING_LIST', 'process_message': '', 'process_status': 'PENDING', - 'token': hashlib.sha1((str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest(), + 'token': hashlib.sha512(('pokemon@potato.io').encode('UTF-8') + b).hexdigest(), 'syllabus_id': None, **data, } @@ -582,7 +592,7 @@ def test_task__post__get_in_waiting_list_using_a_plan(self): 'role_id': None, 'sent_at': None, 'status': 'ACCEPTED', - 'token': hashlib.sha1((str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest(), + 'token': hashlib.sha512(('pokemon@potato.io').encode('UTF-8') + b).hexdigest(), 'process_message': '', 'process_status': 'DONE', 'syllabus_id': None, @@ -594,7 +604,7 @@ def test_task__post__get_in_waiting_list_using_a_plan(self): self.assertEqual(self.bc.database.list_of('payments.Plan'), [plan_db_item(model.plan, data={})]) self.bc.check.queryset_with_pks(model.plan.invites.all(), [2]) - token = hashlib.sha1((str(now) + data['email']).encode('UTF-8')).hexdigest() + token = hashlib.sha512('pokemon@potato.io'.encode('UTF-8') + b).hexdigest() self.bc.check.calls(notify_actions.send_email_message.call_args_list, [ call('verify_email', 'pokemon@potato.io', { @@ -704,7 +714,7 @@ def test__post__course_without_syllabus(self): self.assertEqual(self.bc.database.list_of('authenticate.UserInvite'), [ user_invite_db_item( data={ - 'token': hashlib.sha1((str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest(), + 'token': hashlib.sha512(('pokemon@potato.io').encode('UTF-8') + b).hexdigest(), 'process_status': 'DONE', 'status': 'ACCEPTED', 'academy_id': 1, @@ -730,7 +740,7 @@ def test__post__course_without_syllabus(self): self.bc.check.queryset_with_pks(model.course.invites.all(), [1]) self.assertEqual(self.bc.database.list_of('payments.Plan'), []) - token = hashlib.sha1((str(now) + data['email']).encode('UTF-8')).hexdigest() + token = hashlib.sha512('pokemon@potato.io'.encode('UTF-8') + b).hexdigest() self.bc.check.calls(notify_actions.send_email_message.call_args_list, [ call('verify_email', 'pokemon@potato.io', { @@ -783,7 +793,7 @@ def test__post__course_and_syllabus(self): self.assertEqual(self.bc.database.list_of('authenticate.UserInvite'), [ user_invite_db_item( data={ - 'token': hashlib.sha1((str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest(), + 'token': hashlib.sha512(('pokemon@potato.io').encode('UTF-8') + b).hexdigest(), 'process_status': 'DONE', 'status': 'ACCEPTED', 'academy_id': 1, @@ -810,7 +820,7 @@ def test__post__course_and_syllabus(self): self.bc.check.queryset_with_pks(model.course.invites.all(), [1]) self.assertEqual(self.bc.database.list_of('payments.Plan'), []) - token = hashlib.sha1((str(now) + data['email']).encode('UTF-8')).hexdigest() + token = hashlib.sha512('pokemon@potato.io'.encode('UTF-8') + b).hexdigest() self.bc.check.calls(notify_actions.send_email_message.call_args_list, [ call('verify_email', 'pokemon@potato.io', { @@ -913,7 +923,7 @@ def test__post__course_and_syllabus__waiting_list(self): self.assertEqual(self.bc.database.list_of('authenticate.UserInvite'), [ user_invite_db_item( data={ - 'token': hashlib.sha1((str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest(), + 'token': hashlib.sha512(('pokemon@potato.io').encode('UTF-8') + b).hexdigest(), 'process_status': 'PENDING', 'status': 'WAITING_LIST', 'academy_id': 1, @@ -976,7 +986,7 @@ def test__post__with_other_invite__course_and_syllabus__waiting_list(self): self.bc.format.to_dict(model.user_invite), user_invite_db_item( data={ - 'token': hashlib.sha1((str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest(), + 'token': hashlib.sha512(('pokemon@potato.io').encode('UTF-8') + b).hexdigest(), 'process_status': 'PENDING', 'status': 'WAITING_LIST', 'academy_id': 1, @@ -1039,7 +1049,7 @@ def test__post__with_other_invite__plan__waiting_list(self): self.bc.format.to_dict(model.user_invite), user_invite_db_item( data={ - 'token': hashlib.sha1((str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest(), + 'token': hashlib.sha512(('pokemon@potato.io').encode('UTF-8') + b).hexdigest(), 'process_status': 'PENDING', 'status': 'WAITING_LIST', 'academy_id': None, @@ -1102,7 +1112,7 @@ def test__post__with_other_invite__cohort__waiting_list(self): self.assertEqual(json, expected) self.assertEqual(response.status_code, status.HTTP_201_CREATED) - token = hashlib.sha1((str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest() + token = hashlib.sha512(('pokemon@potato.io').encode('UTF-8') + b).hexdigest() self.assertEqual(self.bc.database.list_of('authenticate.UserInvite'), [ self.bc.format.to_dict(model.user_invite), user_invite_db_item( @@ -1196,7 +1206,7 @@ def test__post__with_other_invite__syllabus__waiting_list(self): self.assertEqual(json, expected) self.assertEqual(response.status_code, status.HTTP_201_CREATED) - token = hashlib.sha1((str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest() + token = hashlib.sha512(('pokemon@potato.io').encode('UTF-8') + b).hexdigest() self.assertEqual(self.bc.database.list_of('authenticate.UserInvite'), [ self.bc.format.to_dict(model.user_invite), user_invite_db_item( @@ -1314,25 +1324,25 @@ def test_task__put__with_user_invite__cohort_as_none(self): self.assertEqual(json, expected) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(self.bc.database.list_of('authenticate.UserInvite'), - [{ - 'user_id': 1, - 'academy_id': None, - 'author_id': None, - 'cohort_id': None, - 'id': 1, - 'is_email_validated': False, - 'role_id': None, - 'sent_at': None, - 'status': 'ACCEPTED', - 'token': hashlib.sha1( - (str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest(), - 'process_message': '', - 'process_status': 'DONE', - 'token': token, - 'syllabus_id': None, - **data, - }]) + self.assertEqual( + self.bc.database.list_of('authenticate.UserInvite'), + [{ + 'user_id': 1, + 'academy_id': None, + 'author_id': None, + 'cohort_id': None, + 'id': 1, + 'is_email_validated': False, + 'role_id': None, + 'sent_at': None, + 'status': 'ACCEPTED', + 'token': hashlib.sha512((str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest(), + 'process_message': '', + 'process_status': 'DONE', + 'token': token, + 'syllabus_id': None, + **data, + }]) user_db = self.bc.database.list_of('auth.User') for item in user_db: @@ -1400,28 +1410,28 @@ def test_task__put__with_user_invite__cohort_not_found(self): self.assertEqual(json, expected) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(self.bc.database.list_of('authenticate.UserInvite'), - [{ - 'user_id': None, - 'academy_id': None, - 'author_id': None, - 'cohort_id': None, - 'id': 1, - 'role_id': None, - 'sent_at': None, - 'is_email_validated': False, - 'status': 'WAITING_LIST', - 'token': hashlib.sha1( - (str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest(), - 'process_message': '', - 'process_status': 'PENDING', - 'token': token, - 'email': 'pokemon@potato.io', - 'first_name': None, - 'last_name': None, - 'phone': '', - 'syllabus_id': None, - }]) + self.assertEqual( + self.bc.database.list_of('authenticate.UserInvite'), + [{ + 'user_id': None, + 'academy_id': None, + 'author_id': None, + 'cohort_id': None, + 'id': 1, + 'role_id': None, + 'sent_at': None, + 'is_email_validated': False, + 'status': 'WAITING_LIST', + 'token': hashlib.sha512((str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest(), + 'process_message': '', + 'process_status': 'PENDING', + 'token': token, + 'email': 'pokemon@potato.io', + 'first_name': None, + 'last_name': None, + 'phone': '', + 'syllabus_id': None, + }]) self.assertEqual(self.bc.database.list_of('marketing.Course'), []) self.assertEqual(self.bc.database.list_of('payments.Plan'), []) @@ -1472,25 +1482,25 @@ def test_task__put__with_user_invite__cohort_found(self): self.assertEqual(response.status_code, status.HTTP_200_OK) del data['cohort'] - self.assertEqual(self.bc.database.list_of('authenticate.UserInvite'), - [{ - 'user_id': 1, - 'academy_id': 1, - 'author_id': None, - 'cohort_id': 1, - 'id': 1, - 'is_email_validated': False, - 'role_id': None, - 'sent_at': None, - 'status': 'ACCEPTED', - 'token': hashlib.sha1( - (str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest(), - 'process_message': '', - 'process_status': 'DONE', - 'token': token, - 'syllabus_id': None, - **data, - }]) + self.assertEqual( + self.bc.database.list_of('authenticate.UserInvite'), + [{ + 'user_id': 1, + 'academy_id': 1, + 'author_id': None, + 'cohort_id': 1, + 'id': 1, + 'is_email_validated': False, + 'role_id': None, + 'sent_at': None, + 'status': 'ACCEPTED', + 'token': hashlib.sha512((str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest(), + 'process_message': '', + 'process_status': 'DONE', + 'token': token, + 'syllabus_id': None, + **data, + }]) self.assertEqual(self.bc.database.list_of('marketing.Course'), []) self.assertEqual(self.bc.database.list_of('payments.Plan'), []) @@ -1567,25 +1577,25 @@ def test_task__put__with_user_invite__cohort_found__academy_available_as_saas__u self.assertEqual(response.status_code, status.HTTP_200_OK) del data['cohort'] - self.assertEqual(self.bc.database.list_of('authenticate.UserInvite'), - [{ - 'user_id': 1, - 'academy_id': 1, - 'author_id': None, - 'cohort_id': 1, - 'id': 1, - 'is_email_validated': False, - 'role_id': None, - 'sent_at': None, - 'status': 'ACCEPTED', - 'token': hashlib.sha1( - (str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest(), - 'process_message': '', - 'process_status': 'DONE', - 'token': token, - 'syllabus_id': None, - **data, - }]) + self.assertEqual( + self.bc.database.list_of('authenticate.UserInvite'), + [{ + 'user_id': 1, + 'academy_id': 1, + 'author_id': None, + 'cohort_id': 1, + 'id': 1, + 'is_email_validated': False, + 'role_id': None, + 'sent_at': None, + 'status': 'ACCEPTED', + 'token': hashlib.sha512((str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest(), + 'process_message': '', + 'process_status': 'DONE', + 'token': token, + 'syllabus_id': None, + **data, + }]) user_db = self.bc.database.list_of('auth.User') for item in user_db: @@ -1663,25 +1673,25 @@ def test_task__put__with_user_invite__cohort_found__academy_available_as_saas__u self.assertEqual(response.status_code, status.HTTP_200_OK) del data['cohort'] - self.assertEqual(self.bc.database.list_of('authenticate.UserInvite'), - [{ - 'user_id': 1, - 'academy_id': 1, - 'author_id': 1, - 'cohort_id': 1, - 'id': 1, - 'is_email_validated': False, - 'role_id': None, - 'sent_at': None, - 'status': 'ACCEPTED', - 'token': hashlib.sha1( - (str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest(), - 'process_message': '', - 'process_status': 'DONE', - 'token': token, - 'syllabus_id': None, - **data, - }]) + self.assertEqual( + self.bc.database.list_of('authenticate.UserInvite'), + [{ + 'user_id': 1, + 'academy_id': 1, + 'author_id': 1, + 'cohort_id': 1, + 'id': 1, + 'is_email_validated': False, + 'role_id': None, + 'sent_at': None, + 'status': 'ACCEPTED', + 'token': hashlib.sha512((str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest(), + 'process_message': '', + 'process_status': 'DONE', + 'token': token, + 'syllabus_id': None, + **data, + }]) self.assertEqual(self.bc.database.list_of('marketing.Course'), []) self.assertEqual(self.bc.database.list_of('payments.Plan'), []) @@ -1725,28 +1735,28 @@ def test_task__put__with_user_invite__syllabus_not_found(self): self.assertEqual(json, expected) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(self.bc.database.list_of('authenticate.UserInvite'), - [{ - 'user_id': None, - 'academy_id': None, - 'author_id': None, - 'cohort_id': None, - 'is_email_validated': False, - 'id': 1, - 'role_id': None, - 'sent_at': None, - 'status': 'WAITING_LIST', - 'token': hashlib.sha1( - (str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest(), - 'process_message': '', - 'process_status': 'PENDING', - 'token': token, - 'email': 'pokemon@potato.io', - 'first_name': None, - 'last_name': None, - 'phone': '', - 'syllabus_id': None, - }]) + self.assertEqual( + self.bc.database.list_of('authenticate.UserInvite'), + [{ + 'user_id': None, + 'academy_id': None, + 'author_id': None, + 'cohort_id': None, + 'is_email_validated': False, + 'id': 1, + 'role_id': None, + 'sent_at': None, + 'status': 'WAITING_LIST', + 'token': hashlib.sha512((str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest(), + 'process_message': '', + 'process_status': 'PENDING', + 'token': token, + 'email': 'pokemon@potato.io', + 'first_name': None, + 'last_name': None, + 'phone': '', + 'syllabus_id': None, + }]) self.assertEqual(self.bc.database.list_of('marketing.Course'), []) self.assertEqual(self.bc.database.list_of('payments.Plan'), []) @@ -1810,7 +1820,7 @@ def test_task__put__with_user_invite__syllabus_found(self): 'is_email_validated': False, 'sent_at': None, 'status': 'ACCEPTED', - 'token': hashlib.sha1((str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest(), + 'token': hashlib.sha512(('pokemon@potato.io').encode('UTF-8') + b).hexdigest(), 'process_message': '', 'process_status': 'DONE', 'token': token, @@ -1899,25 +1909,25 @@ def test_task__put__with_user_invite__syllabus_found__academy_available_as_saas_ self.assertEqual(response.status_code, status.HTTP_200_OK) del data['syllabus'] - self.assertEqual(self.bc.database.list_of('authenticate.UserInvite'), - [{ - 'user_id': 1, - 'academy_id': 1, - 'author_id': None, - 'cohort_id': None, - 'id': 1, - 'role_id': None, - 'is_email_validated': False, - 'sent_at': None, - 'status': 'ACCEPTED', - 'token': hashlib.sha1( - (str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest(), - 'process_message': '', - 'process_status': 'DONE', - 'token': token, - 'syllabus_id': 1, - **data, - }]) + self.assertEqual( + self.bc.database.list_of('authenticate.UserInvite'), + [{ + 'user_id': 1, + 'academy_id': 1, + 'author_id': None, + 'cohort_id': None, + 'id': 1, + 'role_id': None, + 'is_email_validated': False, + 'sent_at': None, + 'status': 'ACCEPTED', + 'token': hashlib.sha512((str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest(), + 'process_message': '', + 'process_status': 'DONE', + 'token': token, + 'syllabus_id': 1, + **data, + }]) user_db = self.bc.database.list_of('auth.User') for item in user_db: @@ -2001,25 +2011,25 @@ def test_task__put__with_user_invite__syllabus_found__academy_available_as_saas_ self.assertEqual(response.status_code, status.HTTP_200_OK) del data['syllabus'] - self.assertEqual(self.bc.database.list_of('authenticate.UserInvite'), - [{ - 'user_id': 1, - 'academy_id': 1, - 'author_id': 1, - 'cohort_id': None, - 'id': 1, - 'is_email_validated': False, - 'role_id': None, - 'sent_at': None, - 'status': 'ACCEPTED', - 'token': hashlib.sha1( - (str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest(), - 'process_message': '', - 'process_status': 'DONE', - 'token': token, - 'syllabus_id': 1, - **data, - }]) + self.assertEqual( + self.bc.database.list_of('authenticate.UserInvite'), + [{ + 'user_id': 1, + 'academy_id': 1, + 'author_id': 1, + 'cohort_id': None, + 'id': 1, + 'is_email_validated': False, + 'role_id': None, + 'sent_at': None, + 'status': 'ACCEPTED', + 'token': hashlib.sha512((str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest(), + 'process_message': '', + 'process_status': 'DONE', + 'token': token, + 'syllabus_id': 1, + **data, + }]) self.assertEqual(self.bc.database.list_of('marketing.Course'), []) self.assertEqual(self.bc.database.list_of('payments.Plan'), []) @@ -2069,28 +2079,28 @@ def test_task__put__plan_does_not_exist(self): self.assertEqual(json, expected) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) # del data['cohort'] - self.assertEqual(self.bc.database.list_of('authenticate.UserInvite'), - [{ - 'user_id': None, - 'academy_id': 1, - 'author_id': None, - 'cohort_id': 1, - 'id': 1, - 'is_email_validated': False, - 'role_id': None, - 'sent_at': None, - 'status': 'WAITING_LIST', - 'email': 'pokemon@potato.io', - 'first_name': None, - 'last_name': None, - 'phone': '', - 'token': hashlib.sha1( - (str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest(), - 'process_message': '', - 'process_status': 'PENDING', - 'token': token, - 'syllabus_id': None, - }]) + self.assertEqual( + self.bc.database.list_of('authenticate.UserInvite'), + [{ + 'user_id': None, + 'academy_id': 1, + 'author_id': None, + 'cohort_id': 1, + 'id': 1, + 'is_email_validated': False, + 'role_id': None, + 'sent_at': None, + 'status': 'WAITING_LIST', + 'email': 'pokemon@potato.io', + 'first_name': None, + 'last_name': None, + 'phone': '', + 'token': hashlib.sha512((str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest(), + 'process_message': '', + 'process_status': 'PENDING', + 'token': token, + 'syllabus_id': None, + }]) self.assertEqual(self.bc.database.list_of('marketing.Course'), []) self.assertEqual(self.bc.database.list_of('payments.Plan'), []) diff --git a/breathecode/authenticate/views.py b/breathecode/authenticate/views.py index 3fdb66cdd..95ed63fa1 100644 --- a/breathecode/authenticate/views.py +++ b/breathecode/authenticate/views.py @@ -1569,14 +1569,129 @@ def render_user_invite(request, token): }) +def accept_invite_action(data={}, token=None, lang='en'): + from breathecode.payments.models import Invoice, Bag, Plan + + password1 = data.get('password', None) + password2 = data.get('repeat_password', None) + + invite = UserInvite.objects.filter(token=str(token), status='PENDING', email__isnull=False).first() + if invite is None: + raise Exception( + translation(lang, + en='Invalid or expired invitation ' + str(token), + es='Invitación inválida o expirada ' + str(token))) + + first_name = data.get('first_name', None) + last_name = data.get('last_name', None) + if first_name is None or first_name == '' or last_name is None or last_name == '': + raise Exception(translation(lang, en='Invalid first or last name', es='Nombre o apellido inválido')) + + if password1 != password2: + raise Exception(translation(lang, en='Passwords don\'t match', es='Las contraseñas no coinciden')) + + if not password1: + raise Exception(translation(lang, en='Password is empty', es='La contraseña está vacía')) + + user = User.objects.filter(email=invite.email).first() + if user is None: + user = User(email=invite.email, first_name=first_name, last_name=last_name, username=invite.email) + user.save() + user.set_password(password1) + user.save() + + if invite.academy is not None: + profile = ProfileAcademy.objects.filter(email=invite.email, academy=invite.academy).first() + if profile is None: + role = invite.role + if not role: + role = Role.objects.filter(slug='student').first() + + if not role: + raise Exception( + translation(lang, + en='Unexpected error occurred with invite, please contact the ' + 'staff of 4geeks', + es='Ocurrió un error inesperado con la invitación, por favor ' + 'contacta al staff de 4geeks')) + + profile = ProfileAcademy(email=invite.email, + academy=invite.academy, + role=role, + first_name=first_name, + last_name=last_name) + + if invite.first_name is not None and invite.first_name != '': + profile.first_name = invite.first_name + if invite.last_name is not None and invite.last_name != '': + profile.last_name = invite.last_name + + profile.user = user + profile.status = 'ACTIVE' + profile.save() + + if invite.cohort is not None: + role = 'student' + if invite.role is not None and invite.role.slug != 'student': + role = invite.role.slug.upper() + + cu = CohortUser.objects.filter(user=user, cohort=invite.cohort).first() + if cu is None: + cu = CohortUser(user=user, cohort=invite.cohort, role=role.upper()) + cu.save() + + plan = Plan.objects.filter(cohort_set__cohorts=invite.cohort, invites=invite).first() + + if plan and invite.user and invite.cohort.academy.main_currency and ( + invite.cohort.available_as_saas == True or + (invite.cohort.available_as_saas == None and invite.cohort.academy.available_as_saas == True)): + utc_now = timezone.now() + + bag = Bag() + bag.chosen_period = 'NO_SET' + bag.status = 'PAID' + bag.type = 'INVITED' + bag.how_many_installments = 1 + bag.academy = invite.cohort.academy + bag.user = user + bag.is_recurrent = False + bag.was_delivered = False + bag.token = None + bag.currency = invite.cohort.academy.main_currency + bag.expires_at = None + + bag.save() + + bag.plans.add(plan) + bag.selected_cohorts.add(invite.cohort) + + invoice = Invoice(amount=0, + paid_at=utc_now, + user=invite.user, + bag=bag, + academy=bag.academy, + status='FULFILLED', + currency=bag.academy.main_currency) + invoice.save() + + payment_tasks.build_plan_financing.delay(bag.id, invoice.id, is_free=True) + + invite.status = 'ACCEPTED' + invite.is_email_validated = True + invite.save() + + return invite + + @api_view(['GET', 'POST']) @permission_classes([AllowAny]) def render_invite(request, token, member_id=None): - from breathecode.payments.models import Invoice, Bag, Plan _dict = request.POST.copy() _dict['token'] = token _dict['callback'] = request.GET.get('callback', '') + lang = get_user_language(request) + if request.method == 'GET': invite = UserInvite.objects.filter(token=token, status='PENDING').first() if invite is None: @@ -1603,124 +1718,29 @@ def render_invite(request, token, member_id=None): 'form': form, }) - if request.method == 'POST': - form = InviteForm(_dict) - password1 = request.POST.get('password', None) - password2 = request.POST.get('repeat_password', None) - - invite = UserInvite.objects.filter(token=str(token), status='PENDING', email__isnull=False).first() - if invite is None: - messages.error(request, 'Invalid or expired invitation ' + str(token)) - return render(request, 'form_invite.html', {'form': form}) + if request.method == 'POST' and request.META.get('CONTENT_TYPE') == 'application/json': + _dict = request.data + _dict['token'] = token + _dict['callback'] = request.GET.get('callback', '') - first_name = request.POST.get('first_name', None) - last_name = request.POST.get('last_name', None) - if first_name is None or first_name == '' or last_name is None or last_name == '': - messages.error(request, 'Invalid first or last name') - return render(request, 'form_invite.html', { - 'form': form, - }) + try: + invite = accept_invite_action(_dict, token, lang) + except Exception as e: + raise ValidationException(str(e)) - if password1 != password2: - messages.error(request, 'Passwords don\'t match') - return render(request, 'form_invite.html', { - 'form': form, - }) + serializer = UserInviteShortSerializer(invite, many=False) + return Response(serializer.data) - if not password1: - messages.error(request, 'Password is empty') + if request.method == 'POST': + try: + accept_invite_action(_dict, token, lang) + except Exception as e: + form = InviteForm(_dict) + messages.error(request, str(e)) return render(request, 'form_invite.html', { 'form': form, }) - user = User.objects.filter(email=invite.email).first() - if user is None: - user = User(email=invite.email, first_name=first_name, last_name=last_name, username=invite.email) - user.save() - user.set_password(password1) - user.save() - - if invite.academy is not None: - profile = ProfileAcademy.objects.filter(email=invite.email, academy=invite.academy).first() - if profile is None: - role = invite.role - if not role: - role = Role.objects.filter(slug='student').first() - - if not role: - messages.error( - request, 'Unexpected error occurred with invite, please contact the ' - 'staff of 4geeks') - return render(request, 'form_invite.html', { - 'form': form, - }) - - profile = ProfileAcademy(email=invite.email, - academy=invite.academy, - role=role, - first_name=first_name, - last_name=last_name) - - if invite.first_name is not None and invite.first_name != '': - profile.first_name = invite.first_name - if invite.last_name is not None and invite.last_name != '': - profile.last_name = invite.last_name - - profile.user = user - profile.status = 'ACTIVE' - profile.save() - - if invite.cohort is not None: - role = 'student' - if invite.role is not None and invite.role.slug != 'student': - role = invite.role.slug.upper() - - cu = CohortUser.objects.filter(user=user, cohort=invite.cohort).first() - if cu is None: - cu = CohortUser(user=user, cohort=invite.cohort, role=role.upper()) - cu.save() - - plan = Plan.objects.filter(cohort_set__cohorts=invite.cohort, invites=invite).first() - - if plan and invite.user and invite.cohort.academy.main_currency and ( - invite.cohort.available_as_saas == True or - (invite.cohort.available_as_saas == None - and invite.cohort.academy.available_as_saas == True)): - utc_now = timezone.now() - - bag = Bag() - bag.chosen_period = 'NO_SET' - bag.status = 'PAID' - bag.type = 'INVITED' - bag.how_many_installments = 1 - bag.academy = invite.cohort.academy - bag.user = user - bag.is_recurrent = False - bag.was_delivered = False - bag.token = None - bag.currency = invite.cohort.academy.main_currency - bag.expires_at = None - - bag.save() - - bag.plans.add(plan) - bag.selected_cohorts.add(invite.cohort) - - invoice = Invoice(amount=0, - paid_at=utc_now, - user=invite.user, - bag=bag, - academy=bag.academy, - status='FULFILLED', - currency=bag.academy.main_currency) - invoice.save() - - payment_tasks.build_plan_financing.delay(bag.id, invoice.id, is_free=True) - - invite.status = 'ACCEPTED' - invite.is_email_validated = True - invite.save() - callback = request.POST.get('callback', None) if callback: uri = callback[0] if isinstance(callback, list) else callback From a31ed6e18163a205262931b920b13dbc86e139e0 Mon Sep 17 00:00:00 2001 From: jefer94 Date: Mon, 9 Oct 2023 19:02:23 -0500 Subject: [PATCH 06/11] move a function to actions.py --- breathecode/authenticate/actions.py | 114 ++++++++++++++++++++++++++ breathecode/authenticate/views.py | 120 +--------------------------- 2 files changed, 117 insertions(+), 117 deletions(-) diff --git a/breathecode/authenticate/actions.py b/breathecode/authenticate/actions.py index 23f25a8b7..c476e621e 100644 --- a/breathecode/authenticate/actions.py +++ b/breathecode/authenticate/actions.py @@ -902,3 +902,117 @@ def get_app(pk: str | int) -> App: raise Exception('App not found') return app + + +def accept_invite_action(data={}, token=None, lang='en'): + from breathecode.payments.models import Invoice, Bag, Plan + + password1 = data.get('password', None) + password2 = data.get('repeat_password', None) + + invite = UserInvite.objects.filter(token=str(token), status='PENDING', email__isnull=False).first() + if invite is None: + raise Exception( + translation(lang, + en='Invalid or expired invitation ' + str(token), + es='Invitación inválida o expirada ' + str(token))) + + first_name = data.get('first_name', None) + last_name = data.get('last_name', None) + if first_name is None or first_name == '' or last_name is None or last_name == '': + raise Exception(translation(lang, en='Invalid first or last name', es='Nombre o apellido inválido')) + + if password1 != password2: + raise Exception(translation(lang, en='Passwords don\'t match', es='Las contraseñas no coinciden')) + + if not password1: + raise Exception(translation(lang, en='Password is empty', es='La contraseña está vacía')) + + user = User.objects.filter(email=invite.email).first() + if user is None: + user = User(email=invite.email, first_name=first_name, last_name=last_name, username=invite.email) + user.save() + user.set_password(password1) + user.save() + + if invite.academy is not None: + profile = ProfileAcademy.objects.filter(email=invite.email, academy=invite.academy).first() + if profile is None: + role = invite.role + if not role: + role = Role.objects.filter(slug='student').first() + + if not role: + raise Exception( + translation(lang, + en='Unexpected error occurred with invite, please contact the ' + 'staff of 4geeks', + es='Ocurrió un error inesperado con la invitación, por favor ' + 'contacta al staff de 4geeks')) + + profile = ProfileAcademy(email=invite.email, + academy=invite.academy, + role=role, + first_name=first_name, + last_name=last_name) + + if invite.first_name is not None and invite.first_name != '': + profile.first_name = invite.first_name + if invite.last_name is not None and invite.last_name != '': + profile.last_name = invite.last_name + + profile.user = user + profile.status = 'ACTIVE' + profile.save() + + if invite.cohort is not None: + role = 'student' + if invite.role is not None and invite.role.slug != 'student': + role = invite.role.slug.upper() + + cu = CohortUser.objects.filter(user=user, cohort=invite.cohort).first() + if cu is None: + cu = CohortUser(user=user, cohort=invite.cohort, role=role.upper()) + cu.save() + + plan = Plan.objects.filter(cohort_set__cohorts=invite.cohort, invites=invite).first() + + if plan and invite.user and invite.cohort.academy.main_currency and ( + invite.cohort.available_as_saas == True or + (invite.cohort.available_as_saas == None and invite.cohort.academy.available_as_saas == True)): + utc_now = timezone.now() + + bag = Bag() + bag.chosen_period = 'NO_SET' + bag.status = 'PAID' + bag.type = 'INVITED' + bag.how_many_installments = 1 + bag.academy = invite.cohort.academy + bag.user = user + bag.is_recurrent = False + bag.was_delivered = False + bag.token = None + bag.currency = invite.cohort.academy.main_currency + bag.expires_at = None + + bag.save() + + bag.plans.add(plan) + bag.selected_cohorts.add(invite.cohort) + + invoice = Invoice(amount=0, + paid_at=utc_now, + user=invite.user, + bag=bag, + academy=bag.academy, + status='FULFILLED', + currency=bag.academy.main_currency) + invoice.save() + + payment_tasks.build_plan_financing.delay(bag.id, invoice.id, is_free=True) + + invite.status = 'ACCEPTED' + invite.is_email_validated = True + invite.save() + + return invite diff --git a/breathecode/authenticate/views.py b/breathecode/authenticate/views.py index 95ed63fa1..5a0f98280 100644 --- a/breathecode/authenticate/views.py +++ b/breathecode/authenticate/views.py @@ -44,9 +44,9 @@ from breathecode.utils.shorteners import C from breathecode.utils.views import (private_view, render_message, set_query_parameter) -from .actions import (generate_academy_token, get_app, get_user_language, resend_invite, reset_password, - set_gitpod_user_expiration, update_gitpod_users, sync_organization_members, - get_github_scopes, accept_invite) +from .actions import (accept_invite_action, generate_academy_token, get_app, get_user_language, resend_invite, + reset_password, set_gitpod_user_expiration, update_gitpod_users, + sync_organization_members, get_github_scopes, accept_invite) from .authentication import ExpiringTokenAuthentication from .forms import (InviteForm, LoginForm, PasswordChangeCustomForm, PickPasswordForm, ResetPasswordForm, SyncGithubUsersForm) @@ -1569,120 +1569,6 @@ def render_user_invite(request, token): }) -def accept_invite_action(data={}, token=None, lang='en'): - from breathecode.payments.models import Invoice, Bag, Plan - - password1 = data.get('password', None) - password2 = data.get('repeat_password', None) - - invite = UserInvite.objects.filter(token=str(token), status='PENDING', email__isnull=False).first() - if invite is None: - raise Exception( - translation(lang, - en='Invalid or expired invitation ' + str(token), - es='Invitación inválida o expirada ' + str(token))) - - first_name = data.get('first_name', None) - last_name = data.get('last_name', None) - if first_name is None or first_name == '' or last_name is None or last_name == '': - raise Exception(translation(lang, en='Invalid first or last name', es='Nombre o apellido inválido')) - - if password1 != password2: - raise Exception(translation(lang, en='Passwords don\'t match', es='Las contraseñas no coinciden')) - - if not password1: - raise Exception(translation(lang, en='Password is empty', es='La contraseña está vacía')) - - user = User.objects.filter(email=invite.email).first() - if user is None: - user = User(email=invite.email, first_name=first_name, last_name=last_name, username=invite.email) - user.save() - user.set_password(password1) - user.save() - - if invite.academy is not None: - profile = ProfileAcademy.objects.filter(email=invite.email, academy=invite.academy).first() - if profile is None: - role = invite.role - if not role: - role = Role.objects.filter(slug='student').first() - - if not role: - raise Exception( - translation(lang, - en='Unexpected error occurred with invite, please contact the ' - 'staff of 4geeks', - es='Ocurrió un error inesperado con la invitación, por favor ' - 'contacta al staff de 4geeks')) - - profile = ProfileAcademy(email=invite.email, - academy=invite.academy, - role=role, - first_name=first_name, - last_name=last_name) - - if invite.first_name is not None and invite.first_name != '': - profile.first_name = invite.first_name - if invite.last_name is not None and invite.last_name != '': - profile.last_name = invite.last_name - - profile.user = user - profile.status = 'ACTIVE' - profile.save() - - if invite.cohort is not None: - role = 'student' - if invite.role is not None and invite.role.slug != 'student': - role = invite.role.slug.upper() - - cu = CohortUser.objects.filter(user=user, cohort=invite.cohort).first() - if cu is None: - cu = CohortUser(user=user, cohort=invite.cohort, role=role.upper()) - cu.save() - - plan = Plan.objects.filter(cohort_set__cohorts=invite.cohort, invites=invite).first() - - if plan and invite.user and invite.cohort.academy.main_currency and ( - invite.cohort.available_as_saas == True or - (invite.cohort.available_as_saas == None and invite.cohort.academy.available_as_saas == True)): - utc_now = timezone.now() - - bag = Bag() - bag.chosen_period = 'NO_SET' - bag.status = 'PAID' - bag.type = 'INVITED' - bag.how_many_installments = 1 - bag.academy = invite.cohort.academy - bag.user = user - bag.is_recurrent = False - bag.was_delivered = False - bag.token = None - bag.currency = invite.cohort.academy.main_currency - bag.expires_at = None - - bag.save() - - bag.plans.add(plan) - bag.selected_cohorts.add(invite.cohort) - - invoice = Invoice(amount=0, - paid_at=utc_now, - user=invite.user, - bag=bag, - academy=bag.academy, - status='FULFILLED', - currency=bag.academy.main_currency) - invoice.save() - - payment_tasks.build_plan_financing.delay(bag.id, invoice.id, is_free=True) - - invite.status = 'ACCEPTED' - invite.is_email_validated = True - invite.save() - - return invite - - @api_view(['GET', 'POST']) @permission_classes([AllowAny]) def render_invite(request, token, member_id=None): From e62973ea32d1292cd32c187f5c26e718bb34006b Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Tue, 10 Oct 2023 09:57:37 -0400 Subject: [PATCH 07/11] Update views.py --- breathecode/assignments/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/breathecode/assignments/views.py b/breathecode/assignments/views.py index 741632cc8..64e4dfdf8 100644 --- a/breathecode/assignments/views.py +++ b/breathecode/assignments/views.py @@ -644,7 +644,7 @@ def post(self, request, user_id=None): tasks_activity.add_activity.delay(request.user.id, 'open_syllabus_module', related_type='assignments.Task', - related_id=serializer.id) + related_id=serializer.data['id']) return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) From 5dac22060e8aeeab7a03ae470244da4305ba5432 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Tue, 10 Oct 2023 10:53:47 -0400 Subject: [PATCH 08/11] Update views.py --- breathecode/assignments/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/breathecode/assignments/views.py b/breathecode/assignments/views.py index 64e4dfdf8..5c8e6a448 100644 --- a/breathecode/assignments/views.py +++ b/breathecode/assignments/views.py @@ -639,12 +639,12 @@ def post(self, request, user_id=None): }, many=True) if serializer.is_valid(): - serializer.save() + task = serializer.save() # tasks.teacher_task_notification.delay(serializer.data['id']) tasks_activity.add_activity.delay(request.user.id, 'open_syllabus_module', related_type='assignments.Task', - related_id=serializer.data['id']) + related_id=task.id) return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) From c0645024955b493560e07ce273e23cc55204cd2d Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Tue, 10 Oct 2023 11:20:44 -0400 Subject: [PATCH 09/11] Update views.py --- breathecode/assignments/views.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/breathecode/assignments/views.py b/breathecode/assignments/views.py index 5c8e6a448..542c7801f 100644 --- a/breathecode/assignments/views.py +++ b/breathecode/assignments/views.py @@ -639,12 +639,14 @@ def post(self, request, user_id=None): }, many=True) if serializer.is_valid(): - task = serializer.save() + tasks = serializer.save() # tasks.teacher_task_notification.delay(serializer.data['id']) - tasks_activity.add_activity.delay(request.user.id, - 'open_syllabus_module', - related_type='assignments.Task', - related_id=task.id) + for t in tasks: + tasks_activity.add_activity.delay(request.user.id, + 'open_syllabus_module', + related_type='assignments.Task', + related_id=t.id) + return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) From e0cb6f813f45a9098e1e259fca22e853a1a2686b Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Tue, 10 Oct 2023 17:25:21 -0400 Subject: [PATCH 10/11] Update tasks.py --- breathecode/activity/tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/breathecode/activity/tasks.py b/breathecode/activity/tasks.py index 7adda8ffd..d076327c2 100644 --- a/breathecode/activity/tasks.py +++ b/breathecode/activity/tasks.py @@ -153,7 +153,7 @@ def add_activity(user_id: int, related_id: Optional[str | int] = None, related_slug: Optional[str] = None, **_): - logger.info('Executing add_activity') + logger.info(f'Executing add_activity related to {str(kind)}') if related_type and not (bool(related_id) ^ bool(related_slug)): raise AbortTask( From 577e77127e934d7e05faf84be8ada5e33afd9951 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Tue, 10 Oct 2023 17:27:31 -0400 Subject: [PATCH 11/11] Update views.py --- breathecode/payments/views.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/breathecode/payments/views.py b/breathecode/payments/views.py index aee7cb5f3..025ed26b8 100644 --- a/breathecode/payments/views.py +++ b/breathecode/payments/views.py @@ -1603,10 +1603,11 @@ def post(self, request): serializer = GetInvoiceSerializer(invoice, many=False) - tasks_activity.add_activity.delay(request.user.id, - 'checkout_completed', - related_type='payments.Invoice', - related_id=serializer.instance.id) + # TODO: Implement payments.Invoice in ALLOWED_TYPES before uncommenting this line + # tasks_activity.add_activity.delay(request.user.id, + # 'checkout_completed', + # related_type='payments.Invoice', + # related_id=serializer.instance.id) return Response(serializer.data, status=201)