From 25bdddac02c8e063112ea65dd5ec3ad59341a5ea Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Mon, 10 Jun 2024 14:56:43 -0400 Subject: [PATCH 01/35] Update book_session.html --- breathecode/mentorship/templates/book_session.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/breathecode/mentorship/templates/book_session.html b/breathecode/mentorship/templates/book_session.html index b7f70cf8e..2e70de90b 100644 --- a/breathecode/mentorship/templates/book_session.html +++ b/breathecode/mentorship/templates/book_session.html @@ -28,7 +28,7 @@

{{ SUBJECT }}

Hello {{mentee.first_name }}, in this page - {{mentor.service.name}} you are with: {{mentor.user.first_name}} + {{mentor.service.name}} you are scheduling with: {{mentor.user.first_name}} {{mentor.user.last_name}}

From 8664674c4ab654ca92175e5e14813c3b2ad3dc5c Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Thu, 13 Jun 2024 16:01:29 +0000 Subject: [PATCH 02/35] addded enpoint to get assessment threshold by id --- breathecode/assessment/urls.py | 1 + breathecode/assessment/views.py | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/breathecode/assessment/urls.py b/breathecode/assessment/urls.py index 0e49347c3..5b9add631 100644 --- a/breathecode/assessment/urls.py +++ b/breathecode/assessment/urls.py @@ -18,6 +18,7 @@ path('academy/layout', AcademyAssessmentLayoutView.as_view()), path('academy/layout/', AcademyAssessmentLayoutView.as_view()), path('/threshold', GetThresholdView.as_view()), + path('/threshold/', GetThresholdView.as_view()), path('/question/', AssessmentQuestionView.as_view()), path('/option/', AssessmentOptionView.as_view()), path('', GetAssessmentView.as_view()), diff --git a/breathecode/assessment/views.py b/breathecode/assessment/views.py index 309ab08ec..ddffdafa7 100644 --- a/breathecode/assessment/views.py +++ b/breathecode/assessment/views.py @@ -381,12 +381,20 @@ class GetThresholdView(APIView): """ permission_classes = [AllowAny] - def get(self, request, assessment_slug): + def get(self, request, assessment_slug, threshold_id=None): item = Assessment.objects.filter(slug=assessment_slug).first() if item is None: raise ValidationException('Assessment not found', 404) + if threshold_id is not None: + single = AssessmentThreshold.objects.filter(id=threshold_id, assessment__slug=assessment_slug).first() + if single is None: + raise ValidationException(f'Threshold {threshold_id} not found', 404, slug='threshold-not-found') + + serializer = GetAssessmentThresholdSerializer(single, many=False) + return handler.response(serializer.data) + # get original all assessments (assessments that have no parent) items = AssessmentThreshold.objects.filter(assessment__slug=assessment_slug) lookup = {} From 11b2f704634384806ba627a971f68036ccb24de6 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Thu, 13 Jun 2024 16:13:38 -0400 Subject: [PATCH 03/35] Update serializers.py --- breathecode/registry/serializers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/breathecode/registry/serializers.py b/breathecode/registry/serializers.py index fa3ef7c97..19d94afc9 100644 --- a/breathecode/registry/serializers.py +++ b/breathecode/registry/serializers.py @@ -345,6 +345,7 @@ class AssetBigSerializer(AssetMidSerializer): last_synch_at = serpy.Field() status_text = serpy.Field() published_at = serpy.Field() + updated_at = serpy.Field() enable_table_of_content = serpy.Field() delivery_instructions = serpy.Field() From c68d04dec7385b9a8a26c995ea935bfe52341fd0 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Thu, 13 Jun 2024 17:37:56 -0400 Subject: [PATCH 04/35] Update serializers.py --- breathecode/registry/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/breathecode/registry/serializers.py b/breathecode/registry/serializers.py index 19d94afc9..bada8a795 100644 --- a/breathecode/registry/serializers.py +++ b/breathecode/registry/serializers.py @@ -360,7 +360,7 @@ class AssetBigSerializer(AssetMidSerializer): superseded_by = AssetTinySerializer(required=False) def get_assets_related(self, obj): - _assets_related = [AssetSmallSerializer(asset).data for asset in obj.assets_related.all()] + _assets_related = [AssetSmallSerializer(asset).data for asset in obj.assets_related.filter(lang=obj.lang)] return _assets_related From 29dc7f2ae73b7b9d70e8a3674c32316ce746345f Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Thu, 13 Jun 2024 18:08:37 -0400 Subject: [PATCH 05/35] Update admin.py --- breathecode/registry/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/breathecode/registry/admin.py b/breathecode/registry/admin.py index f97a47d17..6a877b304 100644 --- a/breathecode/registry/admin.py +++ b/breathecode/registry/admin.py @@ -344,7 +344,7 @@ def queryset(self, request, queryset): class AssetAdmin(admin.ModelAdmin): form = AssetForm search_fields = ['title', 'slug', 'author__email', 'url'] - filter_horizontal = ('technologies', 'all_translations', 'seo_keywords') + filter_horizontal = ('technologies', 'all_translations', 'seo_keywords', 'assets_related') list_display = ('main', 'current_status', 'alias', 'techs', 'url_path') list_filter = [ 'asset_type', 'status', 'sync_status', 'test_status', 'lang', 'external', AssessmentFilter, WithKeywordFilter, From e59e4e830ef2ed25016c030694ff25d507dd9ae9 Mon Sep 17 00:00:00 2001 From: jefer94 Date: Thu, 13 Jun 2024 19:12:36 -0500 Subject: [PATCH 06/35] add warnings to bills --- breathecode/provisioning/actions.py | 109 ++++++++---------- breathecode/provisioning/models.py | 2 + breathecode/provisioning/tasks.py | 5 +- .../templates/provisioning_invoice.html | 31 +++-- .../tasks/tests_calculate_bill_amounts.py | 16 +-- .../provisioning/tests/tasks/tests_upload.py | 14 ++- .../tests/urls/tests_bill_html.py | 5 +- 7 files changed, 89 insertions(+), 93 deletions(-) diff --git a/breathecode/provisioning/actions.py b/breathecode/provisioning/actions.py index e7d78df2e..b8af02872 100644 --- a/breathecode/provisioning/actions.py +++ b/breathecode/provisioning/actions.py @@ -316,7 +316,7 @@ def add_codespaces_activity(context: ActivityContext, field: dict, position: int academies = random.choices(academies, k=1) errors = [] - ignores = [] + warnings = [] logs = {} provisioning_bills = {} provisioning_vendor = None @@ -354,28 +354,10 @@ def add_codespaces_activity(context: ActivityContext, field: dict, position: int provisioning_bills[academy.id] = provisioning_bill date = datetime.strptime(field['Date'], '%Y-%m-%d') - for academy_id in logs.keys(): - for log in logs[academy_id]: - if (log['storage_action'] == 'DELETE' and log['storage_status'] == 'SYNCHED' - and log['starting_at'] <= pytz.utc.localize(date) <= log['ending_at']): - provisioning_bills.pop(academy_id, None) - ignores.append(f'User {field["Username"]} was deleted from the academy during this event at {date}') - - if not provisioning_bills: - for academy_id in logs.keys(): - cohort_user = CohortUser.objects.filter( - Q(cohort__ending_date__lte=date) | Q(cohort__never_ends=True), - cohort__kickoff_date__gte=date, - cohort__academy__id=academy_id, - user__credentialsgithub__username=field['Username']).order_by('-created_at').first() - - if cohort_user: - errors.append('We found activity from this user while he was studying at one of your cohort ' - f'{cohort_user.cohort.slug}') if not_found: - errors.append(f'We could not find enough information about {field["Username"]}, mark this user user as ' - 'deleted if you don\'t recognize it') + warnings.append(f'We could not find enough information about {field["Username"]}, mark this user user as ' + 'deleted if you don\'t recognize it') if not (kind := context['provisioning_activity_kinds'].get((field['Product'], field['SKU']), None)): kind, _ = ProvisioningConsumptionKind.objects.get_or_create( @@ -415,19 +397,22 @@ def add_codespaces_activity(context: ActivityContext, field: dict, position: int csv_row=position, ) - if errors and not (len(errors) == 1 and not_found): + last_status_list = [x for x in pa.status_text.split(', ') if x] + if errors: pa.status = 'ERROR' - pa.status_text = pa.status_text + (', ' if pa.status_text else '') + ', '.join(errors + ignores) + pa.status_text = ', '.join(last_status_list + errors + warnings) + + elif warnings: + if pa.status != 'ERROR': + pa.status = 'WARNING' - elif pa.status != 'ERROR' and ignores and not provisioning_bills: - pa.status = 'IGNORED' - pa.status_text = pa.status_text + (', ' if pa.status_text else '') + ', '.join(ignores) + pa.status_text = ', '.join(last_status_list + warnings) else: pa.status = 'PERSISTED' - pa.status_text = pa.status_text + (', ' if pa.status_text else '') + ', '.join(errors + ignores) + pa.status_text = ', '.join(last_status_list + errors + warnings) - pa.status_text = ', '.join(sorted(set(pa.status_text.split(', ')))) + pa.status_text = ', '.join([x for x in sorted(set(pa.status_text.split(', '))) if x]) pa.status_text = pa.status_text[:255] pa.save() @@ -469,13 +454,14 @@ def add_gitpod_activity(context: ActivityContext, field: dict, position: int): academies = list(context['academies']) errors = [] + warnings = [] if not academies: - errors.append(f'We could not find enough information about {field["userName"]}, mark this user user as ' - 'deleted if you don\'t recognize it') + warnings.append(f'We could not find enough information about {field["userName"]}, mark this user user as ' + 'deleted if you don\'t recognize it') pattern = r'^https://github\.com/[^/]+/([^/]+)/?' if not (result := re.findall(pattern, field['contextURL'])): - errors.append(f'Invalid repository URL {field["contextURL"]}') + warnings.append(f'Invalid repository URL {field["contextURL"]}') slug = 'unknown' else: @@ -556,9 +542,22 @@ def add_gitpod_activity(context: ActivityContext, field: dict, position: int): if pa.status == 'PENDING': pa.status = 'PERSISTED' if not errors else 'ERROR' - pa.status_text = pa.status_text + (', ' if pa.status_text else '') + ', '.join(errors) + last_status_list = [x for x in pa.status_text.split(', ') if x] + if errors: + pa.status = 'ERROR' + pa.status_text = ', '.join(last_status_list + errors + warnings) + + elif warnings: + if pa.status != 'ERROR': + pa.status = 'WARNING' + + pa.status_text = ', '.join(last_status_list + warnings) - pa.status_text = ', '.join(sorted(set(pa.status_text.split(', ')))) + else: + pa.status = 'PERSISTED' + pa.status_text = ', '.join(last_status_list + errors + warnings) + + pa.status_text = ', '.join([x for x in sorted(set(pa.status_text.split(', '))) if x]) pa.status_text = pa.status_text[:255] pa.save() @@ -572,7 +571,7 @@ def add_gitpod_activity(context: ActivityContext, field: dict, position: int): def add_rigobot_activity(context: ActivityContext, field: dict, position: int) -> None: errors = [] - ignores = [] + warnings = [] if field['organization'] != '4Geeks': return @@ -657,31 +656,11 @@ def add_rigobot_activity(context: ActivityContext, field: dict, position: int) - context['provisioning_bills'][academy.id] = provisioning_bill provisioning_bills[academy.id] = provisioning_bill - for academy_id in logs.keys(): - for log in logs[academy_id]: - if (log['storage_action'] == 'DELETE' and log['storage_status'] == 'SYNCHED' - and log['starting_at'] <= pytz.utc.localize(date) <= log['ending_at']): - provisioning_bills.pop(academy_id, None) - ignores.append( - f'User {field["github_username"]} was deleted from the academy during this event at {date}') - - # disabled because rigobot doesn't have the organization configured yet. - # if not provisioning_bills: - # for academy_id in logs.keys(): - # cohort_user = CohortUser.objects.filter( - # Q(cohort__ending_date__lte=date) | Q(cohort__never_ends=True), - # cohort__kickoff_date__gte=date, - # cohort__academy__id=academy_id, - # user__credentialsgithub__username=field['github_username']).order_by('-created_at').first() - - # if cohort_user: - # errors.append('We found activity from this user while he was studying at one of your cohort ' - # f'{cohort_user.cohort.slug}') - # not implemented yet if not_found: - errors.append(f'We could not find enough information about {field["github_username"]}, mark this user user as ' - 'deleted if you don\'t recognize it') + warnings.append( + f'We could not find enough information about {field["github_username"]}, mark this user user as ' + 'deleted if you don\'t recognize it') s_slug = f'{field["purpose_slug"] or "no-provided"}--{field["pricing_type"].lower()}--{field["model"].lower()}' s_name = f'{field["purpose"]} (type: {field["pricing_type"]}, model: {field["model"]})' @@ -723,20 +702,22 @@ def add_rigobot_activity(context: ActivityContext, field: dict, position: int) - csv_row=position, ) - # if errors and not (len(errors) == 1 and not_found): + last_status_list = [x for x in pa.status_text.split(', ') if x] if errors: pa.status = 'ERROR' - pa.status_text = pa.status_text + (', ' if pa.status_text else '') + ', '.join(errors + ignores) + pa.status_text = ', '.join(last_status_list + errors + warnings) + + elif warnings: + if pa.status != 'ERROR': + pa.status = 'WARNING' - elif pa.status != 'ERROR' and ignores and not provisioning_bills: - pa.status = 'IGNORED' - pa.status_text = pa.status_text + (', ' if pa.status_text else '') + ', '.join(ignores) + pa.status_text = ', '.join(last_status_list + warnings) else: pa.status = 'PERSISTED' - pa.status_text = pa.status_text + (', ' if pa.status_text else '') + ', '.join(errors + ignores) + pa.status_text = ', '.join(last_status_list + errors + warnings) - pa.status_text = ', '.join(sorted(set(pa.status_text.split(', ')))) + pa.status_text = ', '.join([x for x in sorted(set(pa.status_text.split(', '))) if x]) pa.status_text = pa.status_text[:255] pa.save() diff --git a/breathecode/provisioning/models.py b/breathecode/provisioning/models.py index 3a2a8a17b..084c8d9c1 100644 --- a/breathecode/provisioning/models.py +++ b/breathecode/provisioning/models.py @@ -138,10 +138,12 @@ def __str__(self): PENDING = 'PENDING' PERSISTED = 'PERSISTED' +WARNING = 'WARNING' ACTIVITY_STATUS = ( (PENDING, 'Pending'), (PERSISTED, 'Persisted'), (IGNORED, 'Ignored'), + (WARNING, 'Warning'), (ERROR, 'Error'), ) diff --git a/breathecode/provisioning/tasks.py b/breathecode/provisioning/tasks.py index 46d1ac8d6..25c4d0d0c 100644 --- a/breathecode/provisioning/tasks.py +++ b/breathecode/provisioning/tasks.py @@ -99,7 +99,7 @@ def calculate_bill_amounts(hash: str, *, force: bool = False, **_: Any): for bill in bills: amount = 0 - for activity in ProvisioningUserConsumption.objects.filter(bills=bill, status='PERSISTED'): + for activity in ProvisioningUserConsumption.objects.filter(bills=bill, status__in=['PERSISTED', 'WARNING']): consumption_amount = 0 consumption_quantity = 0 for item in activity.events.all(): @@ -242,6 +242,9 @@ def upload(hash: str, *, page: int = 0, force: bool = False, task_manager_id: in elif not ProvisioningUserConsumption.objects.filter(hash=hash, status='ERROR').exists(): calculate_bill_amounts.delay(hash) + elif ProvisioningUserConsumption.objects.filter(hash=hash, status='ERROR').exists(): + ProvisioningBill.objects.filter(hash=hash).update(status='ERROR') + @task(priority=TaskPriority.BACKGROUND.value) def archive_provisioning_bill(bill_id: int, **_: Any): diff --git a/breathecode/provisioning/templates/provisioning_invoice.html b/breathecode/provisioning/templates/provisioning_invoice.html index 369fca1c6..773544d8d 100644 --- a/breathecode/provisioning/templates/provisioning_invoice.html +++ b/breathecode/provisioning/templates/provisioning_invoice.html @@ -2,9 +2,8 @@ {% load math %} {% block head %} - + {% endblock %} {% block content %} @@ -121,7 +120,6 @@ .alert td { width: 100%; } - @@ -160,7 +158,7 @@
Created At: {{ bill.created_at }}
{% if bill.stripe_url %} - Pay + Pay {% endif %}
@@ -189,10 +187,10 @@
{{ consumption.kind.product_name }} ({{ consumption.kind.sku }}) {% if consumption.status_text %} - - - - see error - + - + + show errors + {% endif %} @@ -200,9 +198,9 @@
{% if consumption.amount.is_integer %} - {{ consumption.amount|floatformat:0 }} + {{ consumption.amount|floatformat:0 }} {% else %} - {{ consumption.amount|floatformat:2 }} + {{ consumption.amount|floatformat:2 }} {% endif %} @@ -222,14 +220,15 @@
- {% if page < pages %} - - {% endif %} + + {% endif %} - + {% endblock %} diff --git a/breathecode/provisioning/tests/tasks/tests_calculate_bill_amounts.py b/breathecode/provisioning/tests/tasks/tests_calculate_bill_amounts.py index 996d41c6a..77d27aa6c 100644 --- a/breathecode/provisioning/tests/tasks/tests_calculate_bill_amounts.py +++ b/breathecode/provisioning/tests/tasks/tests_calculate_bill_amounts.py @@ -1,19 +1,21 @@ """ Test /answer/:id """ -from datetime import datetime, timedelta +import logging import math import os import random import re +from datetime import datetime, timedelta +from unittest.mock import MagicMock, PropertyMock, call, patch + +import pandas as pd from django.utils import timezone from faker import Faker -import pandas as pd from pytz import UTC -from breathecode.provisioning.tasks import calculate_bill_amounts -import logging -from unittest.mock import PropertyMock, patch, MagicMock, call + from breathecode.payments.services.stripe import Stripe +from breathecode.provisioning.tasks import calculate_bill_amounts from ..mixins import ProvisioningTestCase @@ -252,7 +254,7 @@ def test_bill_exists_and_activities__gitpod(self): } for n in range(2)] provisioning_user_consumptions = [{ - 'status': 'PERSISTED', + 'status': random.choice(['PERSISTED', 'WARNING']), } for _ in range(2)] amount = sum([ @@ -333,7 +335,7 @@ def test_bill_exists_and_activities__codespaces(self): } for n in range(2)] provisioning_user_consumptions = [{ - 'status': 'PERSISTED', + 'status': random.choice(['PERSISTED', 'WARNING']), } for _ in range(2)] amount = sum([ diff --git a/breathecode/provisioning/tests/tasks/tests_upload.py b/breathecode/provisioning/tests/tasks/tests_upload.py index af05b61ab..8ea0aa0b3 100644 --- a/breathecode/provisioning/tests/tasks/tests_upload.py +++ b/breathecode/provisioning/tests/tasks/tests_upload.py @@ -670,6 +670,7 @@ def test_users_not_found__case1(self): 'vendor_id': None, 'hash': slug, 'total_amount': 0.0, + 'status': 'ERROR', }) for n in range(20) ]) self.assertEqual(self.bc.database.list_of('provisioning.ProvisioningConsumptionKind'), [ @@ -820,6 +821,7 @@ def test_users_not_found__case2(self): 'vendor_id': None, 'hash': slug, 'total_amount': 0.0, + 'status': 'ERROR', }) for n in range(20) ]) self.assertEqual(self.bc.database.list_of('provisioning.ProvisioningConsumptionKind'), [ @@ -1344,7 +1346,7 @@ def test_from_github_credentials__generate_anything__case1(self): 'processed_at': UTC_NOW, 'status': - 'PERSISTED', + 'WARNING', 'status_text': (f"We could not find enough information about {csv['Username'][n]}, mark this user user " "as deleted if you don't recognize it"), }) for n in range(10) @@ -1501,7 +1503,10 @@ def test_from_github_credentials__vendor_not_found(self): self.assertEqual(self.bc.database.list_of('payments.Currency'), [currency_data()]) self.assertEqual(self.bc.database.list_of('provisioning.ProvisioningBill'), [ - provisioning_bill_data({'hash': slug}), + provisioning_bill_data({ + 'hash': slug, + 'status': 'ERROR', + }), ]) self.assertEqual(self.bc.database.list_of('provisioning.ProvisioningConsumptionKind'), [ provisioning_activity_kind_data({ @@ -2342,7 +2347,10 @@ def test_from_github_credentials__vendor_not_found(self): self.assertEqual(self.bc.database.list_of('payments.Currency'), [currency_data()]) self.assertEqual(self.bc.database.list_of('provisioning.ProvisioningBill'), [ - provisioning_bill_data({'hash': slug}), + provisioning_bill_data({ + 'hash': slug, + 'status': 'ERROR', + }), ]) self.assertEqual(self.bc.database.list_of('provisioning.ProvisioningConsumptionKind'), [ provisioning_activity_kind_data( diff --git a/breathecode/provisioning/tests/urls/tests_bill_html.py b/breathecode/provisioning/tests/urls/tests_bill_html.py index b12c99f2f..3562eddaf 100644 --- a/breathecode/provisioning/tests/urls/tests_bill_html.py +++ b/breathecode/provisioning/tests/urls/tests_bill_html.py @@ -2,11 +2,12 @@ Test cases for /academy/:id/member/:id """ import os -import urllib.parse + from django.template import loader from django.urls.base import reverse_lazy -from rest_framework import status from django.utils import timezone +from rest_framework import status + from ..mixins import ProvisioningTestCase UTC_NOW = timezone.now() From 29ecff5ede01a7687d0496f2bd008433d2368278 Mon Sep 17 00:00:00 2001 From: jefer94 Date: Thu, 13 Jun 2024 19:14:38 -0500 Subject: [PATCH 07/35] add migration --- ...lter_provisioninguserconsumption_status.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 breathecode/provisioning/migrations/0017_alter_provisioninguserconsumption_status.py diff --git a/breathecode/provisioning/migrations/0017_alter_provisioninguserconsumption_status.py b/breathecode/provisioning/migrations/0017_alter_provisioninguserconsumption_status.py new file mode 100644 index 000000000..7441534a4 --- /dev/null +++ b/breathecode/provisioning/migrations/0017_alter_provisioninguserconsumption_status.py @@ -0,0 +1,21 @@ +# Generated by Django 5.0.6 on 2024-06-14 00:14 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('provisioning', '0016_alter_provisioningconsumptionevent_repository_url_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='provisioninguserconsumption', + name='status', + field=models.CharField(choices=[('PENDING', 'Pending'), ('PERSISTED', 'Persisted'), ('IGNORED', 'Ignored'), + ('WARNING', 'Warning'), ('ERROR', 'Error')], + default='PENDING', + max_length=20), + ), + ] From 4634c90d6f5c2ef998a8237d587bd4e672297bc2 Mon Sep 17 00:00:00 2001 From: jefer94 Date: Thu, 13 Jun 2024 21:03:45 -0500 Subject: [PATCH 08/35] change in asgi.py --- breathecode/asgi.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/breathecode/asgi.py b/breathecode/asgi.py index 29c8ec47c..ea2837143 100644 --- a/breathecode/asgi.py +++ b/breathecode/asgi.py @@ -7,11 +7,6 @@ https://docs.djangoproject.com/en/3.0/howto/deployment/wsgi/ """ -# keeps this above -import newrelic.agent - -newrelic.agent.initialize() - # the rest of your ASGI file contents go here import os @@ -20,5 +15,3 @@ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'breathecode.settings') application = get_asgi_application() -if os.getenv('NOWRAP_APP') != '1': - application = newrelic.agent.ASGIApplicationWrapper(application) From ff32145a45789c332f688356a95d97a4e8bbbf0b Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Fri, 14 Jun 2024 09:56:50 -0400 Subject: [PATCH 09/35] Update actions.py --- breathecode/registry/actions.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/breathecode/registry/actions.py b/breathecode/registry/actions.py index 81f6c1f44..918e999b6 100644 --- a/breathecode/registry/actions.py +++ b/breathecode/registry/actions.py @@ -361,6 +361,22 @@ def pull_github_lesson(github, asset: Asset, override_meta=False): if 'title' in fm and fm['title'] != '': asset.title = fm['title'] + + def parse_boolean(value): + true_values = {'1', 'true'} + false_values = {'0', 'false'} + if isinstance(value, bool): + return value + if isinstance(value, (int, str)): + value_str = str(value).lower() # Convert value to string and lowercase + if value_str in true_values: + return True + elif value_str in false_values: + return False + + raise ValueError(f"Invalid value for boolean conversion: {value}") + if 'table_of_contents' in fm and fm['table_of_contents'] != '': + asset.enable_table_of_content = parse_boolean(fm['table_of_contents']) if 'video' in fm and fm['video'] != '': asset.intro_video_url = fm['video'] From 8fcb6c61c18900641479e47ec12f2783eb7f3cd3 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Fri, 14 Jun 2024 15:48:27 -0400 Subject: [PATCH 10/35] Update serializers.py --- breathecode/registry/serializers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/breathecode/registry/serializers.py b/breathecode/registry/serializers.py index bada8a795..d344d9276 100644 --- a/breathecode/registry/serializers.py +++ b/breathecode/registry/serializers.py @@ -330,6 +330,7 @@ class AssetMidSerializer(AssetSerializer): interactive = serpy.Field() with_solutions = serpy.Field() with_video = serpy.Field() + updated_at = serpy.Field() class AssetBigSerializer(AssetMidSerializer): @@ -345,7 +346,7 @@ class AssetBigSerializer(AssetMidSerializer): last_synch_at = serpy.Field() status_text = serpy.Field() published_at = serpy.Field() - updated_at = serpy.Field() + enable_table_of_content = serpy.Field() delivery_instructions = serpy.Field() From 9480c79e695ca226598eb117407a9c6cb82dc161 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Mon, 17 Jun 2024 21:12:10 +0000 Subject: [PATCH 11/35] added new fied for assessment title --- .../0009_assessmentthreshold_title.py | 22 +++++++++++++++++++ breathecode/assessment/models.py | 6 +++++ 2 files changed, 28 insertions(+) create mode 100644 breathecode/assessment/migrations/0009_assessmentthreshold_title.py diff --git a/breathecode/assessment/migrations/0009_assessmentthreshold_title.py b/breathecode/assessment/migrations/0009_assessmentthreshold_title.py new file mode 100644 index 000000000..89183db30 --- /dev/null +++ b/breathecode/assessment/migrations/0009_assessmentthreshold_title.py @@ -0,0 +1,22 @@ +# Generated by Django 5.0.3 on 2024-06-17 20:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assessment', '0008_alter_answer_option_alter_userassessment_status'), + ] + + operations = [ + migrations.AddField( + model_name='assessmentthreshold', + name='title', + field=models.CharField(blank=True, + default=None, + help_text='Title is good for internal use', + max_length=255, + null=True), + ), + ] diff --git a/breathecode/assessment/models.py b/breathecode/assessment/models.py index 9df4d5944..28b6706b9 100644 --- a/breathecode/assessment/models.py +++ b/breathecode/assessment/models.py @@ -131,6 +131,12 @@ class AssessmentThreshold(models.Model): blank=True, null=True) + title = models.CharField(max_length=255, + default=None, + blank=True, + null=True, + help_text='Title is good for internal use') + academy = models.ForeignKey( Academy, on_delete=models.CASCADE, From c25f9ec7d196473d29710b505db576c0709f25b7 Mon Sep 17 00:00:00 2001 From: jefer94 Date: Mon, 17 Jun 2024 17:08:16 -0500 Subject: [PATCH 12/35] add google meet provider --- breathecode/authenticate/urls.py | 17 +- breathecode/celery.py | 10 +- breathecode/mentorship/actions.py | 60 ++++- breathecode/mentorship/models.py | 162 ++++++++---- .../tests_get_pending_sessions_or_create.py | 182 ++++++++++++- .../tests/urls/tests_academy_service.py | 2 + .../tests/urls/tests_academy_service_id.py | 1 + .../tests_meet_slug_service_slug.py | 199 ++++++++++++-- breathecode/mentorship/views.py | 5 +- breathecode/services/__init__.py | 1 + breathecode/services/google_meet/__init__.py | 1 + .../services/google_meet/google_meet.py | 250 ++++++++++-------- breathecode/tests/mixins/datetime_mixin.py | 16 +- .../utils/create_models.py | 5 +- capyc/django/pytest/fixtures/database.py | 6 + 15 files changed, 687 insertions(+), 230 deletions(-) create mode 100644 breathecode/services/google_meet/__init__.py diff --git a/breathecode/authenticate/urls.py b/breathecode/authenticate/urls.py index 77d21d94d..267f79fb6 100644 --- a/breathecode/authenticate/urls.py +++ b/breathecode/authenticate/urls.py @@ -13,6 +13,8 @@ 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ +import os + from django.urls import path from linked_services.rest_framework.views import app_webhook, authorize_view @@ -67,6 +69,19 @@ sync_gitpod_users_view, ) +# avoiding issues on test environment due that the fixture are loaded after this app +ENV = os.getenv('ENV') +TEST_ENV = (ENV == 'test' or ENV not in ['development', 'staging', 'production']) +LOGIN_URL = '/v1/auth/view/login' +app_url = os.getenv('APP_URL') + +if TEST_ENV and not app_url: + import faker + + fake = faker.Faker() + + app_url = fake.url().replace('http://', 'https://') + app_name = 'authenticate' urlpatterns = [ path('confirmation/', ConfirmEmailView.as_view(), name='confirmation_token'), @@ -142,7 +157,7 @@ # authorize path('authorize/', - authorize_view(login_url='/v1/auth/view/login', get_language=get_user_language), + authorize_view(login_url=LOGIN_URL, app_url=app_url, get_language=get_user_language), name='authorize_slug'), # apps diff --git a/breathecode/celery.py b/breathecode/celery.py index 09302387a..6fc8a9910 100644 --- a/breathecode/celery.py +++ b/breathecode/celery.py @@ -7,7 +7,7 @@ # the rest of your Celery file contents go here import os -from datetime import datetime, timedelta +from datetime import UTC, datetime from typing import TypedDict from celery import Celery @@ -123,7 +123,7 @@ def __exit__(self, *args, **kwargs): if len(data[i]) >= 2: data[i].sort(key=lambda x: x['created_at']) - if datetime.utcnow() - data[i][-1]['created_at'] < delta: + if datetime.now(UTC) - data[i][-1]['created_at'] < delta: available[i] = False data[i] = data[i][-2:] else: @@ -131,7 +131,7 @@ def __exit__(self, *args, **kwargs): data[i] = data[i][-1:] elif len(data[i]) == 1: - if datetime.utcnow() - data[i][0]['created_at'] < delta: + if datetime.now(UTC) - data[i][0]['created_at'] < delta: available[i] = False else: available[i] = True @@ -142,7 +142,7 @@ def __exit__(self, *args, **kwargs): found = False for i in range(workers): if available[i]: - data[i].append({'pid': worker_id, 'created_at': datetime.utcnow()}) + data[i].append({'pid': worker_id, 'created_at': datetime.now(UTC)}) found = True break @@ -153,7 +153,7 @@ def __exit__(self, *args, **kwargs): if len(data[i]) < len(pointer): pointer = data[i] - pointer.append({'pid': worker_id, 'created_at': datetime.utcnow()}) + pointer.append({'pid': worker_id, 'created_at': datetime.now(UTC)}) cache.set('workers', data, timeout=None) break diff --git a/breathecode/mentorship/actions.py b/breathecode/mentorship/actions.py index aaaa87b65..204bcd54b 100644 --- a/breathecode/mentorship/actions.py +++ b/breathecode/mentorship/actions.py @@ -3,18 +3,21 @@ from datetime import timedelta import pytz +from asgiref.sync import sync_to_async from dateutil.relativedelta import relativedelta from django.db.models import Q, QuerySet from django.shortcuts import render from django.utils import timezone +from google.apps.meet_v2.types import Space, SpaceConfig import breathecode.activity.tasks as tasks_activity from breathecode.mentorship.exceptions import ExtendSessionException from breathecode.services.daily.client import DailyClient +from breathecode.services.google_meet.google_meet import GoogleMeet from breathecode.utils.datetime_integer import duration_to_str from capyc.rest_framework.exceptions import ValidationException -from .models import MentorProfile, MentorshipBill, MentorshipSession +from .models import MentorProfile, MentorshipBill, MentorshipService, MentorshipSession logger = logging.getLogger(__name__) @@ -91,12 +94,20 @@ def get_pending_sessions_or_create(token, mentor, service, mentee=None): is_online=True, service=service, ends_at=timezone.now() + duration) - daily = DailyClient() - room = daily.create_room(exp_in_seconds=service.duration.seconds) - session.online_meeting_url = room['url'] - session.name = room['name'] - session.mentee = mentee - session.save() + + if session.service.video_provider == MentorshipService.VideoProvider.GOOGLE_MEET: + create_room_on_google_meet(session) + + elif session.service.video_provider == MentorshipService.VideoProvider.DAILY: + daily = DailyClient() + room = daily.create_room(exp_in_seconds=service.duration.seconds) + session.online_meeting_url = room['url'] + session.name = room['name'] + session.mentee = mentee + session.save() + + else: + raise Exception('Invalid video provider') if mentee: tasks_activity.add_activity.delay(mentee.id, @@ -392,3 +403,38 @@ def mentor_is_ready(mentor: MentorProfile): raise Exception(f'Mentor {mentor.name} online meeting URL is failing.') return True + + +def create_room_on_google_meet(session: MentorshipSession) -> None: + """Create a room on google meet for a mentorship session.""" + + if isinstance(session, MentorshipSession) is False: + raise Exception('session argument must be a MentorshipSession') + + if session.service.video_provider != session.service.VideoProvider.GOOGLE_MEET: + raise Exception('Video provider must be Google Meet') + + if not session.service: + raise Exception('Mentorship session doesn\'t have a service associated with it') + + mentor = session.mentor + + meet = GoogleMeet() + if session.id is None: + session.save() + + title = (f'{session.service.name} {session.id} | ' + f'{mentor.user.first_name} {mentor.user.last_name}') + s = Space( + name=title, + config=SpaceConfig(access_type=SpaceConfig.AccessType.OPEN), + ) + space = meet.create_space(space=s) + session.online_meeting_url = space.meeting_uri + session.name = title + session.save() + + +@sync_to_async +def acreate_room_on_google_meet(session: MentorshipSession) -> None: + return create_room_on_google_meet(session) diff --git a/breathecode/mentorship/models.py b/breathecode/mentorship/models.py index 41c532011..385639717 100644 --- a/breathecode/mentorship/models.py +++ b/breathecode/mentorship/models.py @@ -12,73 +12,133 @@ from breathecode.notify.models import SlackChannel from breathecode.utils.validators.language import validate_language_code -# settings customizable for each academy -# class AcademySettings(models.Model): -# is_video_streaming_active = models.BooleanField(default=False) -# academy = models.OneToOneField(Academy, on_delete=models.CASCADE) -# @staticmethod -# def get(pk): -# settings = AcademySettings.objects.filter(academy__id=pk).first() -# # lets create the settings if they dont exist for this academy -# if settings is None: -# settings = AcademySettings.objects.create(academy=pk) -# return settings -# def warnings(self): -# # return a dictionary with a list of the fields and warning messages related to them -# # for example: { "is_video_streaming_active": "Please settup a video streaming" } -# return {} -# def errors(self): -# # return a dictionary with a list of the fields and errors messages related to them -# return {} - -DRAFT = 'DRAFT' -ACTIVE = 'ACTIVE' -UNLISTED = 'UNLISTED' -INNACTIVE = 'INNACTIVE' -MENTORSHIP_STATUS = ( - (DRAFT, 'Draft'), - (ACTIVE, 'Active'), - (UNLISTED, 'Unlisted'), - (INNACTIVE, 'Innactive'), -) + +class VideoProvider(models.TextChoices): + DAILY = ('DAILY', 'Daily') + GOOGLE_MEET = ('GOOGLE_MEET', 'Google Meet') + + +MENTORSHIP_SETTINGS = { + 'duration': timedelta(hours=1), + 'max_duration': timedelta(hours=2), + 'missed_meeting_duration': timedelta(minutes=10), + 'language': 'en', + 'allow_mentee_to_extend': True, + 'allow_mentors_to_extend': True, + 'video_provider': VideoProvider.GOOGLE_MEET, +} + + +class AcademyMentorshipSettings(models.Model): + VideoProvider = VideoProvider + + academy = models.OneToOneField(Academy, on_delete=models.CASCADE) + duration = models.DurationField(default=MENTORSHIP_SETTINGS['duration'], + help_text='Default duration for mentorship sessions of this service') + + max_duration = models.DurationField( + default=MENTORSHIP_SETTINGS['max_duration'], + help_text='Maximum allowed duration or extra time, make it 0 for unlimited meetings') + + missed_meeting_duration = models.DurationField( + default=MENTORSHIP_SETTINGS['missed_meeting_duration'], + help_text='Duration that will be paid when the mentee doesn\'t come to the session') + + language = models.CharField(max_length=5, + default=MENTORSHIP_SETTINGS['language'], + validators=[validate_language_code], + help_text='ISO 639-1 language code + ISO 3166-1 alpha-2 country code, e.g. en-US') + + allow_mentee_to_extend = models.BooleanField(default=MENTORSHIP_SETTINGS['allow_mentee_to_extend'], + help_text='If true, mentees will be able to extend mentorship session') + allow_mentors_to_extend = models.BooleanField( + default=MENTORSHIP_SETTINGS['allow_mentors_to_extend'], + help_text='If true, mentors will be able to extend mentorship session') + + video_provider = models.CharField(max_length=15, choices=VideoProvider, default=VideoProvider.GOOGLE_MEET) + + created_at = models.DateTimeField(auto_now_add=True, editable=False) + updated_at = models.DateTimeField(auto_now=True, editable=False) + + def __str__(self): + return self.academy.name + + def clean(self) -> None: + return super().clean() + + def save(self, **kwargs) -> None: + return super().save(**kwargs) class MentorshipService(models.Model): + VideoProvider = VideoProvider + + class Status(models.TextChoices): + DRAFT = ('DRAFT', 'Draft') + ACTIVE = ('ACTIVE', 'Active') + UNLISTED = ('UNLISTED', 'Unlisted') + INNACTIVE = ('INNACTIVE', 'Innactive') + slug = models.SlugField(max_length=150, unique=True) name = models.CharField(max_length=150) logo_url = models.CharField(max_length=150, default=None, blank=True, null=True) description = models.TextField(max_length=500, default=None, blank=True, null=True) - duration = models.DurationField(default=timedelta(hours=1), + duration = models.DurationField(default=None, + blank=True, help_text='Default duration for mentorship sessions of this service') max_duration = models.DurationField( - default=timedelta(hours=2), - help_text='Maximum allowed duration or extra time, make it 0 for unlimited meetings') + default=None, blank=True, help_text='Maximum allowed duration or extra time, make it 0 for unlimited meetings') missed_meeting_duration = models.DurationField( - default=timedelta(minutes=10), - help_text='Duration that will be paid when the mentee doesn\'t come to the session') + default=None, blank=True, help_text='Duration that will be paid when the mentee doesn\'t come to the session') - status = models.CharField(max_length=15, choices=MENTORSHIP_STATUS, default=DRAFT) + status = models.CharField(max_length=15, choices=Status, default=Status.DRAFT) language = models.CharField(max_length=5, - default='en', + default=None, + blank=True, validators=[validate_language_code], help_text='ISO 639-1 language code + ISO 3166-1 alpha-2 country code, e.g. en-US') - allow_mentee_to_extend = models.BooleanField(default=True, + allow_mentee_to_extend = models.BooleanField(blank=True, + default=None, help_text='If true, mentees will be able to extend mentorship session') allow_mentors_to_extend = models.BooleanField( - default=True, help_text='If true, mentors will be able to extend mentorship session') + default=None, blank=True, help_text='If true, mentors will be able to extend mentorship session') academy = models.ForeignKey(Academy, on_delete=models.CASCADE) + video_provider = models.CharField(max_length=15, default=None, choices=VideoProvider, blank=True) created_at = models.DateTimeField(auto_now_add=True, editable=False) updated_at = models.DateTimeField(auto_now=True, editable=False) def __str__(self): - return f'{self.name} ({self.id})' + return f'{self.name} ({self.slug})' + + def clean(self) -> None: + fetched = False + academy_settings = None + for field, value in MENTORSHIP_SETTINGS.items(): + current = getattr(self, field) + if current is None: + if fetched is False: + fetched = True + academy_settings = AcademyMentorshipSettings.objects.filter(academy=self.academy).first() + + if academy_settings: + academy_value = getattr(academy_settings, field) + setattr(self, field, academy_value) + + else: + setattr(self, field, value) + + return super().clean() + + def save(self, **kwargs) -> None: + self.full_clean() + return super().save(**kwargs) class SupportChannel(models.Model): @@ -91,13 +151,11 @@ class SupportChannel(models.Model): updated_at = models.DateTimeField(auto_now=True, editable=False) -INVITED = 'INVITED' -MENTOR_STATUS = ( - (INVITED, 'Invited'), - (ACTIVE, 'Active'), - (UNLISTED, 'Unlisted'), - (INNACTIVE, 'Innactive'), -) +class MentorStatus(models.TextChoices): + INVITED = ('INVITED', 'Invited') + ACTIVE = ('ACTIVE', 'Active') + UNLISTED = ('UNLISTED', 'Unlisted') + INNACTIVE = ('INNACTIVE', 'Innactive') class SupportAgent(models.Model): @@ -109,9 +167,9 @@ class SupportAgent(models.Model): unique=True, help_text='Used for inviting the user to become a support agent') status = models.CharField(max_length=15, - choices=MENTOR_STATUS, - default=INVITED, - help_text=f'Options are: {", ".join([key for key,label in MENTOR_STATUS])}') + choices=MentorStatus, + default=MentorStatus.INVITED, + help_text=f'Options are: {", ".join([key for key,label in MentorStatus.choices])}') email = models.CharField(blank=True, max_length=150, @@ -176,9 +234,9 @@ class MentorProfile(models.Model): help_text='What syllabis is this mentor going to be menting to?') status = models.CharField(max_length=15, - choices=MENTOR_STATUS, - default=INVITED, - help_text=f'Options are: {", ".join([key for key,label in MENTOR_STATUS])}') + choices=MentorStatus, + default=MentorStatus.INVITED, + help_text=f'Options are: {", ".join([key for key,label in MentorStatus.choices])}') email = models.CharField(blank=True, max_length=150, diff --git a/breathecode/mentorship/tests/actions/tests_get_pending_sessions_or_create.py b/breathecode/mentorship/tests/actions/tests_get_pending_sessions_or_create.py index 256070783..7dfb3b69e 100644 --- a/breathecode/mentorship/tests/actions/tests_get_pending_sessions_or_create.py +++ b/breathecode/mentorship/tests/actions/tests_get_pending_sessions_or_create.py @@ -53,19 +53,29 @@ def format_mentorship_session_attrs(attrs={}): } +class GoogleMeetMock: + + def __init__(self, meeting_uri='https://meet.google.com/fake'): + self.meeting_uri = meeting_uri + + +def get_title(pk, service, mentor) -> str: + return (f'{service.name} {pk} | {mentor.user.first_name} {mentor.user.last_name}') + + class GetOrCreateSessionTestSuite(MentorshipTestCase): @patch(REQUESTS_PATH['request'], apply_requests_request_mock([(200, daily_url, daily_payload)])) @patch('breathecode.mentorship.signals.mentorship_session_status.send', MagicMock()) @patch('django.utils.timezone.now', MagicMock(return_value=ENDS_AT)) @patch('breathecode.mentorship.actions.close_older_sessions', MagicMock()) - def test_create_session_mentor_first_no_previous_nothing(self): + def test_create_session_mentor_first_no_previous_nothing__daily(self): """ When the mentor gets into the room before the mentee if should create a room with status 'pending' """ - models = self.bc.database.create(mentor_profile=1, user=1, mentorship_service=1) + models = self.bc.database.create(mentor_profile=1, user=1, mentorship_service={'video_provider': 'DAILY'}) mentor = models.mentor_profile mentor_token, created = Token.get_or_create(mentor.user, token_type='permanent') @@ -91,6 +101,44 @@ def test_create_session_mentor_first_no_previous_nothing(self): self.assertEqual(actions.close_older_sessions.call_args_list, [call()]) + @patch.multiple('breathecode.services.google_meet.google_meet.GoogleMeet', + __init__=MagicMock(return_value=None), + create_space=MagicMock(return_value=GoogleMeetMock(meeting_uri='https://meet.google.com/fake'))) + @patch('breathecode.mentorship.signals.mentorship_session_status.send', MagicMock()) + @patch('django.utils.timezone.now', MagicMock(return_value=ENDS_AT)) + @patch('breathecode.mentorship.actions.close_older_sessions', MagicMock()) + def test_create_session_mentor_first_no_previous_nothing__google_meet(self): + """ + When the mentor gets into the room before the mentee + if should create a room with status 'pending' + """ + + models = self.bc.database.create(mentor_profile=1, user=1, mentorship_service={'video_provider': 'GOOGLE_MEET'}) + + mentor = models.mentor_profile + mentor_token, created = Token.get_or_create(mentor.user, token_type='permanent') + + pending_sessions = get_pending_sessions_or_create(mentor_token, mentor, models.mentorship_service, mentee=None) + + self.bc.check.queryset_of(pending_sessions, MentorshipSession) + self.bc.check.queryset_with_pks(pending_sessions, [1]) + + self.assertEqual(self.bc.database.list_of('mentorship.MentorshipSession'), [ + format_mentorship_session_attrs({ + 'id': 1, + 'status': 'PENDING', + 'mentor_id': 1, + 'mentee_id': None, + 'service_id': 1, + 'is_online': True, + 'name': get_title(1, models.mentorship_service, models.mentor_profile), + 'online_meeting_url': 'https://meet.google.com/fake', + 'ends_at': ENDS_AT + timedelta(seconds=3600), + }), + ]) + + self.assertEqual(actions.close_older_sessions.call_args_list, [call()]) + @patch(REQUESTS_PATH['request'], apply_requests_request_mock([(200, daily_url, daily_payload)])) @patch('breathecode.mentorship.signals.mentorship_session_status.send', MagicMock()) @patch('django.utils.timezone.now', MagicMock(return_value=ENDS_AT)) @@ -102,7 +150,9 @@ def test_create_session_mentor_first_previous_pending_without_mentee(self): """ mentorship_session = {'mentee_id': None} - models = self.bc.database.create(mentor_profile=1, mentorship_session=mentorship_session, mentorship_service=1) + models = self.bc.database.create(mentor_profile=1, + mentorship_session=mentorship_session, + mentorship_service={'video_provider': 'DAILY'}) mentor = models.mentor_profile mentor_token, created = Token.get_or_create(mentor.user, token_type='permanent') @@ -141,7 +191,7 @@ def test_create_session_mentor_first_previous_pending_with_mentee(self): models = self.bc.database.create(mentor_profile=1, user=1, mentorship_session=mentorship_session, - mentorship_service=1) + mentorship_service={'video_provider': 'DAILY'}) mentor = models.mentor_profile session = models.mentorship_session @@ -181,7 +231,7 @@ def test_create_session_mentor_first_started_without_mentee(self): models = self.bc.database.create(mentor_profile=1, user=1, mentorship_session=mentorship_session, - mentorship_service=1) + mentorship_service={'video_provider': 'DAILY'}) mentor = models.mentor_profile mentor_token, created = Token.get_or_create(mentor.user, token_type='permanent') @@ -209,13 +259,13 @@ def test_create_session_mentor_first_started_without_mentee(self): @patch('breathecode.mentorship.signals.mentorship_session_status.send', MagicMock()) @patch('django.utils.timezone.now', MagicMock(return_value=ENDS_AT)) @patch('breathecode.mentorship.actions.close_older_sessions', MagicMock()) - def test_create_session_mentee_first_no_previous_nothing(self): + def test_create_session_mentee_first_no_previous_nothing__daily(self): """ Mentee comes first, there is nothing previously created it should return a brand new sessions with started at already started """ - models = self.bc.database.create(mentor_profile=1, user=2, mentorship_service=1) + models = self.bc.database.create(mentor_profile=1, user=2, mentorship_service={'video_provider': 'DAILY'}) mentor = models.mentor_profile mentee = models.user[1] @@ -241,6 +291,44 @@ def test_create_session_mentee_first_no_previous_nothing(self): self.assertEqual(actions.close_older_sessions.call_args_list, [call()]) + @patch.multiple('breathecode.services.google_meet.google_meet.GoogleMeet', + __init__=MagicMock(return_value=None), + create_space=MagicMock(return_value=GoogleMeetMock(meeting_uri='https://meet.google.com/fake'))) + @patch('breathecode.mentorship.signals.mentorship_session_status.send', MagicMock()) + @patch('django.utils.timezone.now', MagicMock(return_value=ENDS_AT)) + @patch('breathecode.mentorship.actions.close_older_sessions', MagicMock()) + def test_create_session_mentee_first_no_previous_nothing__google_meet(self): + """ + Mentee comes first, there is nothing previously created + it should return a brand new sessions with started at already started + """ + + models = self.bc.database.create(mentor_profile=1, user=2, mentorship_service={'video_provider': 'GOOGLE_MEET'}) + mentor = models.mentor_profile + mentee = models.user[1] + + mentee_token, created = Token.get_or_create(mentee, token_type='permanent') + sessions = get_pending_sessions_or_create(mentee_token, mentor, models.mentorship_service, mentee) + + self.bc.check.queryset_of(sessions, MentorshipSession) + self.bc.check.queryset_with_pks(sessions, [1]) + + self.assertEqual(self.bc.database.list_of('mentorship.MentorshipSession'), [ + format_mentorship_session_attrs({ + 'id': 1, + 'status': 'PENDING', + 'mentor_id': 1, + 'mentee_id': 2, + 'service_id': 1, + 'is_online': True, + 'ends_at': ENDS_AT + timedelta(seconds=3600), + 'name': get_title(1, models.mentorship_service, models.mentor_profile), + 'online_meeting_url': 'https://meet.google.com/fake', + }), + ]) + + self.assertEqual(actions.close_older_sessions.call_args_list, [call()]) + @patch(REQUESTS_PATH['request'], apply_requests_request_mock([(200, daily_url, daily_payload)])) @patch('breathecode.mentorship.signals.mentorship_session_status.send', MagicMock()) @patch('django.utils.timezone.now', MagicMock(return_value=ENDS_AT)) @@ -255,7 +343,7 @@ def test_create_session_mentee_first_with_wihout_mentee(self): models = self.bc.database.create(mentor_profile=1, user=2, mentorship_session=mentorship_session, - mentorship_service=1) + mentorship_service={'video_provider': 'DAILY'}) new_mentee = models.user[1] mentee_token, created = Token.get_or_create(new_mentee, token_type='permanent') @@ -284,7 +372,7 @@ def test_create_session_mentee_first_with_wihout_mentee(self): @patch('breathecode.mentorship.signals.mentorship_session_status.send', MagicMock()) @patch('django.utils.timezone.now', MagicMock(return_value=ENDS_AT)) @patch('breathecode.mentorship.actions.close_older_sessions', MagicMock()) - def test_create_session_mentee_first_with_another_mentee(self): + def test_create_session_mentee_first_with_another_mentee__daily(self): """ Mentee comes first, there is a previous pending meeting with another mentee it should keep and ignore old one (untouched) and create and return new one for this mentee @@ -293,12 +381,15 @@ def test_create_session_mentee_first_with_another_mentee(self): # other random mentoring session precreated just for better testing mentorship_session = {'status': 'PENDING'} - self.bc.database.create(mentor_profile=1, user=1, mentorship_session=mentorship_session, mentorship_service=1) + self.bc.database.create(mentor_profile=1, + user=1, + mentorship_session=mentorship_session, + mentorship_service={'video_provider': 'DAILY'}) models = self.bc.database.create(mentor_profile=1, user=1, mentorship_session=mentorship_session, - mentorship_service=1) + mentorship_service={'video_provider': 'DAILY'}) new_mentee = self.bc.database.create(user=1).user mentee_token, created = Token.get_or_create(new_mentee, token_type='permanent') @@ -340,6 +431,71 @@ def test_create_session_mentee_first_with_another_mentee(self): self.assertEqual(actions.close_older_sessions.call_args_list, [call()]) + @patch.multiple('breathecode.services.google_meet.google_meet.GoogleMeet', + __init__=MagicMock(return_value=None), + create_space=MagicMock(return_value=GoogleMeetMock(meeting_uri='https://meet.google.com/fake'))) + @patch('breathecode.mentorship.signals.mentorship_session_status.send', MagicMock()) + @patch('django.utils.timezone.now', MagicMock(return_value=ENDS_AT)) + @patch('breathecode.mentorship.actions.close_older_sessions', MagicMock()) + def test_create_session_mentee_first_with_another_mentee__google_meet(self): + """ + Mentee comes first, there is a previous pending meeting with another mentee + it should keep and ignore old one (untouched) and create and return new one for this mentee + """ + + # other random mentoring session precreated just for better testing + + mentorship_session = {'status': 'PENDING'} + self.bc.database.create(mentor_profile=1, + user=1, + mentorship_session=mentorship_session, + mentorship_service={'video_provider': 'DAILY'}) + + models = self.bc.database.create(mentor_profile=1, + user=1, + mentorship_session=mentorship_session, + mentorship_service={'video_provider': 'GOOGLE_MEET'}) + new_mentee = self.bc.database.create(user=1).user + + mentee_token, created = Token.get_or_create(new_mentee, token_type='permanent') + sessions_to_render = get_pending_sessions_or_create(mentee_token, + models.mentor_profile, + models.mentorship_service, + mentee=new_mentee) + + self.bc.check.queryset_of(sessions_to_render, MentorshipSession) + self.bc.check.queryset_with_pks(sessions_to_render, [3]) + + self.assertEqual(self.bc.database.list_of('mentorship.MentorshipSession'), [ + format_mentorship_session_attrs({ + 'id': 1, + 'ends_at': None, + 'mentee_id': 1, + 'mentor_id': 1, + 'service_id': 1, + }), + format_mentorship_session_attrs({ + 'id': 2, + 'ends_at': None, + 'mentee_id': 2, + 'mentor_id': 2, + 'service_id': 2, + }), + format_mentorship_session_attrs({ + 'id': 3, + 'status': 'PENDING', + 'mentor_id': 2, + 'mentee_id': 3, + 'is_online': True, + 'ends_at': ENDS_AT + timedelta(seconds=3600), + 'name': get_title(3, models.mentorship_service, models.mentor_profile), + 'online_meeting_url': 'https://meet.google.com/fake', + 'service_id': 2, + }), + ]) + + self.assertEqual(actions.close_older_sessions.call_args_list, [call()]) + @patch(REQUESTS_PATH['request'], apply_requests_request_mock([(200, daily_url, daily_payload)])) @patch('breathecode.mentorship.signals.mentorship_session_status.send', MagicMock()) @patch('django.utils.timezone.now', MagicMock(return_value=ENDS_AT)) @@ -354,13 +510,13 @@ def test_create_session_mentee_first_with_another_same_mentee(self): self.bc.database.create(mentor_profile=1, user=1, mentorship_session={'status': 'PENDING'}, - mentorship_service=1) + mentorship_service={'video_provider': 'DAILY'}) # old meeting with SAME mentee, should be re-used models = self.bc.database.create(mentor_profile=1, user=1, mentorship_session={'status': 'PENDING'}, - mentorship_service=1) + mentorship_service={'video_provider': 'DAILY'}) same_mentee = models.user mentee_token, created = Token.get_or_create(same_mentee, token_type='permanent') diff --git a/breathecode/mentorship/tests/urls/tests_academy_service.py b/breathecode/mentorship/tests/urls/tests_academy_service.py index c95b0181c..e5502a12d 100644 --- a/breathecode/mentorship/tests/urls/tests_academy_service.py +++ b/breathecode/mentorship/tests/urls/tests_academy_service.py @@ -52,6 +52,7 @@ def post_serializer(data={}): 'name': '', 'slug': '', 'status': 'DRAFT', + 'video_provider': 'GOOGLE_MEET', **data, } @@ -71,6 +72,7 @@ def mentorship_service_columns(data={}): 'name': '', 'slug': '', 'status': 'DRAFT', + 'video_provider': 'GOOGLE_MEET', **data, } diff --git a/breathecode/mentorship/tests/urls/tests_academy_service_id.py b/breathecode/mentorship/tests/urls/tests_academy_service_id.py index 59939d353..ce081fc58 100644 --- a/breathecode/mentorship/tests/urls/tests_academy_service_id.py +++ b/breathecode/mentorship/tests/urls/tests_academy_service_id.py @@ -85,6 +85,7 @@ def mentorship_service_columns(mentorship_service, data={}): 'name': mentorship_service.name, 'slug': mentorship_service.slug, 'status': mentorship_service.status, + 'video_provider': mentorship_service.video_provider, **data, } diff --git a/breathecode/mentorship/tests/urls_shortner/tests_meet_slug_service_slug.py b/breathecode/mentorship/tests/urls_shortner/tests_meet_slug_service_slug.py index c3584a97d..cbd36d274 100644 --- a/breathecode/mentorship/tests/urls_shortner/tests_meet_slug_service_slug.py +++ b/breathecode/mentorship/tests/urls_shortner/tests_meet_slug_service_slug.py @@ -92,10 +92,19 @@ def get_env(key, value=None): return get_env -def render_message(message, data={}): +def render_message(message, data={}, academy=None): request = None context = {'MESSAGE': message, 'BUTTON': None, 'BUTTON_TARGET': '_blank', 'LINK': None, **data} + if academy: + context['COMPANY_INFO_EMAIL'] = academy.feedback_email + context['COMPANY_LEGAL_NAME'] = academy.legal_name or academy.name + context['COMPANY_LOGO'] = academy.logo_url + context['COMPANY_NAME'] = academy.name + + if 'heading' not in data: + context['heading'] = academy.name + return loader.render_to_string('message.html', context, request) @@ -540,7 +549,10 @@ def test_with_mentor_profile(self): model = self.bc.database.create(user=1, token=1, mentor_profile=1, - mentorship_service=1, + mentorship_service={ + 'language': 'en', + 'video_provider': 'DAILY' + }, group=1, permission=permission) @@ -587,7 +599,10 @@ def test_with_mentor_profile__bad_statuses(self): model = self.bc.database.create(user=1, token=1, mentor_profile=mentor_profile, - mentorship_service=1, + mentorship_service={ + 'language': 'en', + 'video_provider': 'DAILY' + }, group=1, permission=permission) @@ -639,7 +654,10 @@ def test_with_mentor_profile__good_statuses__without_mentor_urls(self): model = self.bc.database.create(user=1, token=1, mentor_profile=mentor_profile, - mentorship_service=1, + mentorship_service={ + 'language': 'en', + 'video_provider': 'DAILY' + }, group=1, permission=permission) @@ -706,7 +724,10 @@ def test_with_mentor_profile__good_statuses__with_mentor_urls__with_mentee(self) model = self.bc.database.create(user=1, token=1, mentor_profile=mentor_profile, - mentorship_service=1, + mentorship_service={ + 'language': 'en', + 'video_provider': 'DAILY' + }, group=1, permission=permission) @@ -778,7 +799,10 @@ def test_with_mentor_profile__good_statuses__with_mentor_urls__with_mentee__not_ model = self.bc.database.create(user=1, token=1, mentor_profile=mentor_profile, - mentorship_service=1, + mentorship_service={ + 'language': 'en', + 'video_provider': 'DAILY' + }, group=1, permission=permission) @@ -1177,7 +1201,10 @@ def test_with_mentor_profile__without_user_name(self): model = self.bc.database.create(mentor_profile=mentor_profile, mentorship_session=mentorship_session, user=user, - mentorship_service=1, + mentorship_service={ + 'language': 'en', + 'video_provider': 'DAILY' + }, academy=academy) model.mentorship_session.mentee = None @@ -1222,6 +1249,86 @@ def test_with_mentor_profile__without_user_name(self): self.bc.database.delete('auth.Permission') self.bc.database.delete('auth.User') + """ + 🔽🔽🔽 GET without MentorProfile, good statuses with mentor urls, MentorshipSession without mentee + passing session and mentee but mentee does not exist, user without name + """ + + @patch('breathecode.mentorship.actions.get_pending_sessions_or_create', + MagicMock(side_effect=Exception('Error inside get_pending_sessions_or_create'))) + @patch('breathecode.mentorship.actions.mentor_is_ready', MagicMock()) + @patch('os.getenv', MagicMock(side_effect=apply_get_env({ + 'DAILY_API_URL': URL, + 'DAILY_API_KEY': API_KEY, + }))) + @patch('requests.request', + apply_requests_request_mock([(201, f'{URL}/v1/rooms', { + 'name': ROOM_NAME, + 'url': ROOM_URL, + })])) + def test_error_inside_get_pending_sessions_or_create(self): + cases = [{ + 'status': x, + 'online_meeting_url': self.bc.fake.url(), + 'booking_url': self.bc.fake.url(), + } for x in ['ACTIVE', 'UNLISTED']] + permission = {'codename': 'join_mentorship'} + + id = 0 + for mentor_profile in cases: + id += 1 + + user = {'first_name': '', 'last_name': ''} + base = self.bc.database.create(user=user, token=1, group=1, permission=permission) + + mentorship_session = {'mentee_id': None} + academy = {'available_as_saas': False} + model = self.bc.database.create(mentor_profile=mentor_profile, + mentorship_session=mentorship_session, + user=user, + mentorship_service={ + 'language': 'en', + 'video_provider': 'DAILY' + }, + academy=academy) + + model.mentorship_session.mentee = None + model.mentorship_session.save() + + querystring = self.bc.format.to_querystring({ + 'token': base.token.key, + }) + url = reverse_lazy('mentorship_shortner:meet_slug_service_slug', + kwargs={ + 'mentor_slug': model.mentor_profile.slug, + 'service_slug': model.mentorship_service.slug + }) + f'?{querystring}' + response = self.client.get(url) + + content = self.bc.format.from_bytes(response.content) + expected = render_message('Error inside get_pending_sessions_or_create', academy=model.academy) + + # dump error in external files + if content != expected: + with open('content.html', 'w') as f: + f.write(content) + + with open('expected.html', 'w') as f: + f.write(expected) + + self.assertEqual(content, expected) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(self.bc.database.list_of('mentorship.MentorProfile'), [ + self.bc.format.to_dict(model.mentor_profile), + ]) + self.assertEqual(self.bc.database.list_of('payments.Consumable'), []) + self.assertEqual(self.bc.database.list_of('payments.ConsumptionSession'), []) + + # teardown + self.bc.database.delete('mentorship.MentorProfile') + self.bc.database.delete('auth.Permission') + self.bc.database.delete('auth.User') + # TODO: disabled until have a new feature flags manager # """ # 🔽🔽🔽 GET without MentorProfile, good statuses with mentor urls, MentorshipSession without mentee @@ -1261,7 +1368,7 @@ def test_with_mentor_profile__without_user_name(self): # model = self.bc.database.create(mentor_profile=mentor_profile, # mentorship_session=mentorship_session, # user=user, - # mentorship_service=1, + # mentorship_service={'language': 'en', 'video_provider': 'DAILY'}, # academy=academy) # model.mentorship_session.mentee = None @@ -1337,7 +1444,10 @@ def test_with_mentor_profile__academy_available_as_saas__flag_eq_true__mentee_wi model = self.bc.database.create(mentor_profile=mentor_profile, mentorship_session=mentorship_session, user=user, - mentorship_service=1, + mentorship_service={ + 'language': 'en', + 'video_provider': 'DAILY' + }, academy=academy) model.mentorship_session.mentee = None @@ -1415,7 +1525,10 @@ def test_with_mentor_profile__academy_available_as_saas__flag_eq_true__mentee_wi model = self.bc.database.create(mentor_profile=mentor_profile, mentorship_session=mentorship_session, user=user, - mentorship_service=1, + mentorship_service={ + 'language': 'en', + 'video_provider': 'DAILY' + }, academy=academy, plan={ 'is_renewable': False, @@ -1505,7 +1618,7 @@ def test_with_mentor_profile__academy_available_as_saas__flag_eq_true__mentee_wi how_many = random.randint(1, 100) consumable = {'how_many': how_many} delta = timedelta(seconds=random.randint(1, 1000)) - mentorship_service = {'max_duration': delta} + mentorship_service = {'max_duration': delta, 'language': 'en'} model = self.bc.database.create(mentor_profile=mentor_profile, mentorship_session=mentorship_session, user=user, @@ -1618,7 +1731,7 @@ def test_with_mentor_profile__academy_available_as_saas__flag_eq_true__bypass_me mentorship_session = {'mentee_id': None} academy = {'available_as_saas': True} delta = timedelta(seconds=random.randint(1, 1000)) - mentorship_service = {'max_duration': delta} + mentorship_service = {'max_duration': delta, 'language': 'en'} model = self.bc.database.create(mentor_profile=mentor_profile, mentorship_session=mentorship_session, user=user, @@ -1791,20 +1904,28 @@ def test_with_mentor_profile__ends_at_less_now__with_extend_true(self): mentorship_session_base = {'mentee_id': base.user.id, 'ends_at': ends_at} # session, token - cases = [({ - **mentorship_session_base, - 'allow_mentors_to_extend': True, - }, None), ({ - **mentorship_session_base, - 'allow_mentee_to_extend': True, - }, 1)] + cases = [ + ({ + **mentorship_session_base, + 'allow_mentee_to_extend': True, + 'allow_mentors_to_extend': False, + }, None), + ({ + **mentorship_session_base, + 'allow_mentee_to_extend': False, + 'allow_mentors_to_extend': True, + }, 1), + ] for mentorship_session, token in cases: model = self.bc.database.create(mentor_profile=mentor_profile, mentorship_session=mentorship_session, user=user, token=token, - mentorship_service=1, + mentorship_service={ + 'language': 'en', + 'video_provider': 'DAILY' + }, group=1, permission=base.permission) @@ -1898,9 +2019,13 @@ def test_with_mentor_profile__ends_at_less_now__with_extend_true__extend_session # session, token cases = [({ 'allow_mentors_to_extend': True, - }, None), ({ + 'allow_mentee_to_extend': False, + 'language': 'en', + }, 1), ({ + 'allow_mentee_to_extend': False, 'allow_mentee_to_extend': True, - }, 1)] + 'language': 'en', + }, None)] for mentorship_service, token in cases: model = self.bc.database.create(mentor_profile=mentor_profile, @@ -2000,10 +2125,12 @@ def test_with_mentor_profile__ends_at_less_now__with_extend_true__session_can_no ({ 'allow_mentors_to_extend': False, 'allow_mentee_to_extend': False, + 'language': 'en', }, None), ({ 'allow_mentors_to_extend': False, 'allow_mentee_to_extend': False, + 'language': 'en', }, 1), ] @@ -2114,7 +2241,10 @@ def test_with_mentor_profile__ends_at_less_now__with_extend_true__redirect_to_se mentorship_session=mentorship_session, user=user, token=token, - mentorship_service=1, + mentorship_service={ + 'language': 'en', + 'video_provider': 'DAILY' + }, group=1, permission=base.permission) @@ -2214,7 +2344,10 @@ def test_with_mentor_profile__redirect_to_session__no_saas(self): mentorship_session=mentorship_session, user=user, token=token, - mentorship_service=1, + mentorship_service={ + 'language': 'en', + 'video_provider': 'DAILY' + }, group=1, permission=base.permission) @@ -2313,7 +2446,10 @@ def test_with_mentor_profile__redirect_to_session__saas(self): mentorship_session=mentorship_session, user=user, token=token, - mentorship_service=1, + mentorship_service={ + 'language': 'en', + 'video_provider': 'DAILY' + }, group=1, permission=base.permission, academy=academy) @@ -2390,7 +2526,7 @@ def test_with_mentor_profile__redirect_to_session__saas__(self): consumable = {'how_many': how_many} delta = timedelta(seconds=random.randint(1, 1000)) - mentorship_service = {'max_duration': delta} + mentorship_service = {'max_duration': delta, 'language': 'en'} base = self.bc.database.create(user=user, token=1, group=1, @@ -2666,6 +2802,7 @@ def test__post__auth__no_saas__finantial_status_no_late(bc: Breathecode, client: mentorship_session = { **mentorship_session_base, 'allow_mentee_to_extend': True, + 'name': 'Session 1', } token = 1 @@ -2673,7 +2810,10 @@ def test__post__auth__no_saas__finantial_status_no_late(bc: Breathecode, client: mentorship_session=mentorship_session, user=user, token=token, - mentorship_service=1, + mentorship_service={ + 'language': 'en', + 'video_provider': 'DAILY' + }, group=1, permission=base.permission) @@ -2797,7 +2937,10 @@ def test__post__auth__no_saas__finantial_status_late(bc: Breathecode, client: fx mentorship_session=mentorship_session, user=user, token=token, - mentorship_service=1, + mentorship_service={ + 'language': 'en', + 'video_provider': 'DAILY' + }, group=1, permission=base.permission) diff --git a/breathecode/mentorship/views.py b/breathecode/mentorship/views.py index d6a295d92..c51ca8f84 100644 --- a/breathecode/mentorship/views.py +++ b/breathecode/mentorship/views.py @@ -488,7 +488,10 @@ def __call__(self): # if the mentor is not the user, then we assume is the mentee mentee = self.token.user if is_token_of_mentee else None - sessions = self.get_pending_sessions_or_create(mentor, service, mentee) + try: + sessions = self.get_pending_sessions_or_create(mentor, service, mentee) + except Exception as e: + return render_message(self.request, str(e), status=400, academy=mentor.academy) if not is_token_of_mentee and sessions.count() > 0 and str(sessions.first().id) != self.query_params['session']: return self.render_pick_session(mentor, sessions) diff --git a/breathecode/services/__init__.py b/breathecode/services/__init__.py index 9dddcef2b..052345c3f 100644 --- a/breathecode/services/__init__.py +++ b/breathecode/services/__init__.py @@ -1,4 +1,5 @@ from .datetime_to_iso_format import datetime_to_iso_format # noqa: F401 from .eventbrite import CAMPAIGN, SOURCE, Eventbrite # noqa: F401 from .google_cloud import Datastore, Function, Storage # noqa: F401 +from .google_meet import * # noqa: F401 from .launch_darkly import * # noqa: F401 diff --git a/breathecode/services/google_meet/__init__.py b/breathecode/services/google_meet/__init__.py new file mode 100644 index 000000000..1919cdab5 --- /dev/null +++ b/breathecode/services/google_meet/__init__.py @@ -0,0 +1 @@ +from .google_meet import * # noqa: F401 diff --git a/breathecode/services/google_meet/google_meet.py b/breathecode/services/google_meet/google_meet.py index 2b0d1909f..9fe838196 100644 --- a/breathecode/services/google_meet/google_meet.py +++ b/breathecode/services/google_meet/google_meet.py @@ -1,7 +1,82 @@ -from typing import Optional +from typing import Optional, TypedDict, Unpack +import google.apps.meet_v2.services.conference_records_service.pagers as pagers from asgiref.sync import async_to_sync from google.apps import meet_v2 +from google.apps.meet_v2.types import Space +from google.protobuf.field_mask_pb2 import FieldMask +from google.protobuf.timestamp_pb2 import Timestamp + +__all__ = ['GoogleMeet'] + + +class CreateSpaceRequest(TypedDict): + space: Space + + +class EndActiveConferenceRequest(TypedDict): + name: str + + +class GetConferenceRecordRequest(TypedDict): + name: str + + +class GetParticipantRequest(TypedDict): + name: str + + +class GetParticipantSessionRequest(TypedDict): + name: str + + +class GetRecordingRequest(TypedDict): + name: str + + +class GetSpaceRequest(TypedDict): + name: str + + +class UpdateSpaceRequest(TypedDict): + space: Space + update_mask: FieldMask + + +class GetTranscriptRequest(TypedDict): + name: str + + +class ListConferenceRecordsRequest(TypedDict): + page_size: int + page_token: str + filter: str # in EBNF format, space.meeting_code, space.name, start_time and end_time + + +class ListRecordingsRequest(TypedDict): + parent: str + page_size: int + page_token: str + + +class ListParticipantSessionsRequest(TypedDict): + parent: str + page_size: int + page_token: str + filter: str # in EBNF format, start_time and end_time + + +class ListTranscriptsRequest(TypedDict): + parent: str + page_size: int + page_token: str + + +class ListParticipantsRequest(TypedDict): + parent: str + page_size: int + page_token: str + filter: str # in EBNF format, start_time and end_time class GoogleMeet: @@ -9,9 +84,12 @@ class GoogleMeet: _conference_records_service_client: Optional[meet_v2.ConferenceRecordsServiceAsyncClient] def __init__(self): + from breathecode.setup import resolve_gcloud_credentials + + resolve_gcloud_credentials() + self._spaces_service_client = None self._conference_records_service_client = None - pass async def spaces_service_client(self): if self._spaces_service_client is None: @@ -25,31 +103,28 @@ async def conference_records_service_client(self): return self._conference_records_service_client - async def acreate_meeting(self, **kwargs): + async def acreate_space(self, **kwargs: Unpack[CreateSpaceRequest]) -> meet_v2.Space: # Create a client client = await self.spaces_service_client() # Initialize request argument(s) - request = meet_v2.CreateSpaceRequest() + request = meet_v2.CreateSpaceRequest(**kwargs) # Make the request - response = await client.create_space(request=request) - - # Handle the response - print(response) + return await client.create_space(request=request) @async_to_sync - async def create_meeting(self): - return await self.acreate_meeting() + async def create_space(self, **kwargs: Unpack[CreateSpaceRequest]) -> meet_v2.Space: + return await self.acreate_space(**kwargs) - async def aget_meeting(self): + async def aget_space(self, **kwargs: Unpack[GetSpaceRequest]) -> meet_v2.Space: # Create a client client = await self.spaces_service_client() # Initialize request argument(s) - request = meet_v2.GetSpaceRequest() + request = meet_v2.GetSpaceRequest(**kwargs) # Make the request response = await client.get_space(request=request) @@ -58,15 +133,15 @@ async def aget_meeting(self): print(response) @async_to_sync - async def get_meeting(self): - return await self.aget_meeting() + async def get_space(self, **kwargs: Unpack[GetSpaceRequest]) -> meet_v2.Space: + return await self.aget_space(**kwargs) - async def aupdate_space(self): + async def aupdate_space(self, **kwargs: Unpack[UpdateSpaceRequest]) -> meet_v2.Space: # Create a client client = await self.spaces_service_client() # Initialize request argument(s) - request = meet_v2.UpdateSpaceRequest() + request = meet_v2.UpdateSpaceRequest(**kwargs) # Make the request response = await client.update_space(request=request) @@ -75,117 +150,90 @@ async def aupdate_space(self): print(response) @async_to_sync - async def update_space(self): - return await self.aupdate_space() + async def update_space(self, **kwargs: Unpack[UpdateSpaceRequest]) -> meet_v2.Space: + return await self.aupdate_space(**kwargs) - async def aend_meeting(self, name: str): + async def aend_active_conference(self, **kwargs: Unpack[EndActiveConferenceRequest]) -> None: # Create a client client = await self.spaces_service_client() # Initialize request argument(s) - request = meet_v2.EndActiveConferenceRequest(name=name) + request = meet_v2.EndActiveConferenceRequest(**kwargs) # Make the request - await client.end_active_conference(request=request) + return await client.end_active_conference(request=request) @async_to_sync - async def end_meeting(self, name: str): - return await self.aend_meeting(name) + async def end_active_conference(self, **kwargs: Unpack[EndActiveConferenceRequest]) -> None: + return await self.aend_active_conference(**kwargs) - async def alist_participants(self, parent: str): + async def alist_participants(self, **kwargs: Unpack[ListParticipantsRequest]) -> pagers.ListParticipantsAsyncPager: # Create a client client = await self.conference_records_service_client() # Initialize request argument(s) - request = meet_v2.ListParticipantsRequest(parent=parent) + request = meet_v2.ListParticipantsRequest(**kwargs) # Make the request - page_result = client.list_participants(request=request) - - # Handle the response - async for response in page_result: - print(response) + return await client.list_participants(request=request) - @async_to_sync - async def list_participants(self, parent: str): - return await self.alist_participants(parent) - - async def aget_participant(self, name: str): + async def aget_participant(self, **kwargs: Unpack[GetParticipantRequest]) -> meet_v2.Participant: # Create a client client = await self.conference_records_service_client() # Initialize request argument(s) - request = meet_v2.GetParticipantRequest(name=name) + request = meet_v2.GetParticipantRequest(**kwargs) # Make the request - response = await client.get_participant(request=request) - - # Handle the response - print(response) + return await client.get_participant(request=request) @async_to_sync - async def get_participant(self, name: str): - return await self.aget_participant(name) + async def get_participant(self, **kwargs: Unpack[GetParticipantRequest]) -> meet_v2.Participant: + return await self.aget_participant(**kwargs) - async def alist_participant_sessions(self, parent: str): + async def alist_participant_sessions( + self, **kwargs: Unpack[ListParticipantSessionsRequest]) -> pagers.ListParticipantSessionsAsyncPager: # Create a client client = await self.conference_records_service_client() # Initialize request argument(s) - request = meet_v2.ListParticipantSessionsRequest(parent=parent) + request = meet_v2.ListParticipantSessionsRequest(**kwargs) # Make the request - page_result = client.list_participant_sessions(request=request) - - # Handle the response - async for response in page_result: - print(response) - - @async_to_sync - async def list_participant_sessions(self, parent: str): - return await self.alist_participant_sessions(parent) + return await client.list_participant_sessions(request=request) - async def aget_participant_session(self, name: str): + async def aget_participant_session(self, + **kwargs: Unpack[GetParticipantSessionRequest]) -> meet_v2.ParticipantSession: # Create a client client = await self.conference_records_service_client() # Initialize request argument(s) - request = meet_v2.GetParticipantSessionRequest(name=name) + request = meet_v2.GetParticipantSessionRequest(**kwargs) # Make the request - response = await client.get_participant_session(request=request) - - # Handle the response - print(response) + return await client.get_participant_session(request=request) @async_to_sync - async def get_participant_session(self, name: str): - return await self.aget_participant_session(name) + async def get_participant_session(self, + **kwargs: Unpack[GetParticipantSessionRequest]) -> meet_v2.ParticipantSession: + return await self.aget_participant_session(**kwargs) - async def alist_recordings(self, parent: str): + async def alist_recordings(self, **kwargs: Unpack[ListRecordingsRequest]) -> pagers.ListRecordingsAsyncPager: # Create a client client = await self.conference_records_service_client() # Initialize request argument(s) - request = meet_v2.ListRecordingsRequest(parent=parent) + request = meet_v2.ListRecordingsRequest(**kwargs) # Make the request - page_result = client.list_recordings(request=request) + return await client.list_recordings(request=request) - # Handle the response - async for response in page_result: - print(response) - - @async_to_sync - async def list_recordings(self, parent: str): - return await self.alist_recordings(parent) - - async def aget_recording(self, name: str): + async def aget_recording(self, **kwargs: Unpack[GetRecordingRequest]) -> meet_v2.Recording: # Create a client client = await self.conference_records_service_client() # Initialize request argument(s) - request = meet_v2.GetRecordingRequest(name=name) + request = meet_v2.GetRecordingRequest(**kwargs) # Make the request response = await client.get_recording(request=request) @@ -194,33 +242,25 @@ async def aget_recording(self, name: str): print(response) @async_to_sync - async def get_recording(self, name: str): - return await self.aget_recording(name) + async def get_recording(self, **kwargs: Unpack[GetRecordingRequest]) -> meet_v2.Recording: + return await self.aget_recording(**kwargs) - async def alist_transcripts(self, parent: str): + async def alist_transcripts(self, **kwargs: Unpack[ListTranscriptsRequest]) -> pagers.ListTranscriptsAsyncPager: # Create a client client = await self.conference_records_service_client() # Initialize request argument(s) - request = meet_v2.ListTranscriptsRequest(parent=parent) + request = meet_v2.ListTranscriptsRequest(**kwargs) # Make the request - page_result = client.list_transcripts(request=request) - - # Handle the response - async for response in page_result: - print(response) - - @async_to_sync - async def list_transcripts(self, parent: str): - return await self.alist_transcripts(parent) + return await client.list_transcripts(request=request) - async def aget_transcript(self, name: str): + async def aget_transcript(self, **kwargs: Unpack[GetTranscriptRequest]) -> meet_v2.Transcript: # Create a client client = await self.conference_records_service_client() # Initialize request argument(s) - request = meet_v2.GetTranscriptRequest(name=name) + request = meet_v2.GetTranscriptRequest(**kwargs) # Make the request response = await client.get_transcript(request=request) @@ -229,40 +269,30 @@ async def aget_transcript(self, name: str): print(response) @async_to_sync - async def get_transcript(self, name: str): - return await self.aget_transcript(name) + async def get_transcript(self, **kwargs: Unpack[GetTranscriptRequest]) -> meet_v2.Transcript: + return await self.aget_transcript(**kwargs) - async def alist_conference_records(self): + async def alist_conference_records( + self, **kwargs: Unpack[ListConferenceRecordsRequest]) -> pagers.ListConferenceRecordsAsyncPager: # Create a client client = await self.conference_records_service_client() # Initialize request argument(s) - request = meet_v2.ListConferenceRecordsRequest() + request = meet_v2.ListConferenceRecordsRequest(**kwargs) # Make the request - page_result = client.list_conference_records(request=request) + return await client.list_conference_records(request=request) - # Handle the response - async for response in page_result: - print(response) - - @async_to_sync - async def list_conference_records(self): - return await self.alist_conference_records() - - async def aget_conference_record(self, name: str): + async def aget_conference_record(self, **kwargs: Unpack[GetConferenceRecordRequest]) -> meet_v2.ConferenceRecord: # Create a client client = await self.conference_records_service_client() # Initialize request argument(s) - request = meet_v2.GetConferenceRecordRequest(name=name) + request = meet_v2.GetConferenceRecordRequest(**kwargs) # Make the request - response = await client.get_conference_record(request=request) - - # Handle the response - print(response) + return await client.get_conference_record(request=request) @async_to_sync - async def get_conference_record(self, name: str): - return await self.aget_conference_record(name) + async def get_conference_record(self, **kwargs: Unpack[GetConferenceRecordRequest]) -> meet_v2.ConferenceRecord: + return await self.aget_conference_record(**kwargs) diff --git a/breathecode/tests/mixins/datetime_mixin.py b/breathecode/tests/mixins/datetime_mixin.py index 0791259e6..9abc55adb 100644 --- a/breathecode/tests/mixins/datetime_mixin.py +++ b/breathecode/tests/mixins/datetime_mixin.py @@ -3,17 +3,11 @@ """ import re -from datetime import datetime, timedelta -from django.utils import timezone -from breathecode.utils.datetime_integer import DatetimeInteger - - -def get_utc(): - date = timezone.now() - return date.tzinfo +from datetime import UTC, datetime +from django.utils import timezone -UTC = get_utc() +from breathecode.utils.datetime_integer import DatetimeInteger __all__ = ['DatetimeMixin'] @@ -36,7 +30,7 @@ def datetime_now(self) -> datetime: """ return timezone.now() - def datetime_to_iso(self, date=datetime.utcnow()) -> str: + def datetime_to_iso(self, date=datetime.now(UTC)) -> str: """ Transform a datetime to ISO 8601 format. @@ -72,7 +66,7 @@ def iso_to_datetime(self, iso: str) -> datetime: date = datetime.fromisoformat(string) return timezone.make_aware(date) - def datetime_to_ical(self, date=datetime.utcnow(), utc=True) -> str: + def datetime_to_ical(self, date=datetime.now(UTC), utc=True) -> str: s = f'{date.year:04}{date.month:02}{date.day:02}T{date.hour:02}{date.minute:02}{date.second:02}' if utc: s += 'Z' diff --git a/breathecode/tests/mixins/generate_models_mixin/utils/create_models.py b/breathecode/tests/mixins/generate_models_mixin/utils/create_models.py index 2b286e4a0..e5b2c5d2a 100644 --- a/breathecode/tests/mixins/generate_models_mixin/utils/create_models.py +++ b/breathecode/tests/mixins/generate_models_mixin/utils/create_models.py @@ -1,9 +1,10 @@ import logging from typing import Any -from breathecode.tests.mixins.generate_models_mixin.exceptions import BadArgument from mixer.backend.django import mixer +from breathecode.tests.mixins.generate_models_mixin.exceptions import BadArgument + from .argument_parser import argument_parser __all__ = ['create_models'] @@ -44,7 +45,7 @@ def debug_mixer(attr, path, **kwargs): def create_models(attr, path, **kwargs): - # does not remove this line are use very often + # does not remove this line is used very often # debug_mixer(attr, path, **kwargs) result = [ diff --git a/capyc/django/pytest/fixtures/database.py b/capyc/django/pytest/fixtures/database.py index 7766346b0..24163884a 100644 --- a/capyc/django/pytest/fixtures/database.py +++ b/capyc/django/pytest/fixtures/database.py @@ -352,6 +352,12 @@ def create(cls, **models): pending = {} + keys = [*models.keys()] + + for key in keys: + if models[key] is None or models[key] == 0: + del models[key] + # get descriptors for model_alias, _value in models.items(): try: From edee4875106ab59e291d744726203d0b29418ff9 Mon Sep 17 00:00:00 2001 From: jefer94 Date: Mon, 17 Jun 2024 17:09:45 -0500 Subject: [PATCH 13/35] add migration --- ...ntorshipservice_video_provider_and_more.py | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 breathecode/mentorship/migrations/0029_mentorshipservice_video_provider_and_more.py diff --git a/breathecode/mentorship/migrations/0029_mentorshipservice_video_provider_and_more.py b/breathecode/mentorship/migrations/0029_mentorshipservice_video_provider_and_more.py new file mode 100644 index 000000000..790fae0e2 --- /dev/null +++ b/breathecode/mentorship/migrations/0029_mentorshipservice_video_provider_and_more.py @@ -0,0 +1,106 @@ +# Generated by Django 5.0.6 on 2024-06-17 22:09 + +import breathecode.utils.validators.language +import datetime +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('admissions', '0064_academy_legal_name'), + ('mentorship', '0028_mentorshipsession_questions_and_answers'), + ] + + operations = [ + migrations.AddField( + model_name='mentorshipservice', + name='video_provider', + field=models.CharField(blank=True, + choices=[('DAILY', 'Daily'), ('GOOGLE_MEET', 'Google Meet')], + default=None, + max_length=15), + ), + migrations.AlterField( + model_name='mentorshipservice', + name='allow_mentee_to_extend', + field=models.BooleanField(blank=True, + default=None, + help_text='If true, mentees will be able to extend mentorship session'), + ), + migrations.AlterField( + model_name='mentorshipservice', + name='allow_mentors_to_extend', + field=models.BooleanField(blank=True, + default=None, + help_text='If true, mentors will be able to extend mentorship session'), + ), + migrations.AlterField( + model_name='mentorshipservice', + name='duration', + field=models.DurationField(blank=True, + default=None, + help_text='Default duration for mentorship sessions of this service'), + ), + migrations.AlterField( + model_name='mentorshipservice', + name='language', + field=models.CharField(blank=True, + default=None, + help_text='ISO 639-1 language code + ISO 3166-1 alpha-2 country code, e.g. en-US', + max_length=5, + validators=[breathecode.utils.validators.language.validate_language_code]), + ), + migrations.AlterField( + model_name='mentorshipservice', + name='max_duration', + field=models.DurationField( + blank=True, + default=None, + help_text='Maximum allowed duration or extra time, make it 0 for unlimited meetings'), + ), + migrations.AlterField( + model_name='mentorshipservice', + name='missed_meeting_duration', + field=models.DurationField( + blank=True, + default=None, + help_text="Duration that will be paid when the mentee doesn't come to the session"), + ), + migrations.CreateModel( + name='AcademyMentorshipSettings', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('duration', + models.DurationField(default=datetime.timedelta(seconds=3600), + help_text='Default duration for mentorship sessions of this service')), + ('max_duration', + models.DurationField( + default=datetime.timedelta(seconds=7200), + help_text='Maximum allowed duration or extra time, make it 0 for unlimited meetings')), + ('missed_meeting_duration', + models.DurationField( + default=datetime.timedelta(seconds=600), + help_text="Duration that will be paid when the mentee doesn't come to the session")), + ('language', + models.CharField(default='en', + help_text='ISO 639-1 language code + ISO 3166-1 alpha-2 country code, e.g. en-US', + max_length=5, + validators=[breathecode.utils.validators.language.validate_language_code])), + ('allow_mentee_to_extend', + models.BooleanField(default=True, + help_text='If true, mentees will be able to extend mentorship session')), + ('allow_mentors_to_extend', + models.BooleanField(default=True, + help_text='If true, mentors will be able to extend mentorship session')), + ('video_provider', + models.CharField(choices=[('DAILY', 'Daily'), ('GOOGLE_MEET', 'Google Meet')], + default='GOOGLE_MEET', + max_length=15)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('academy', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='admissions.academy')), + ], + ), + ] From d40a42f192c91b6bde0a75c5e3630b653ba48c0a Mon Sep 17 00:00:00 2001 From: jefer94 Date: Mon, 17 Jun 2024 19:01:36 -0500 Subject: [PATCH 14/35] add hook errors --- .../notify/migrations/0012_hookerror.py | 25 +++++++++++++++++++ breathecode/notify/models.py | 18 ++++++++++--- breathecode/notify/receivers.py | 10 +++++++- breathecode/notify/utils/hook_manager.py | 22 ++++++++++------ .../mentorship_models_mixin.py | 3 ++- 5 files changed, 65 insertions(+), 13 deletions(-) create mode 100644 breathecode/notify/migrations/0012_hookerror.py diff --git a/breathecode/notify/migrations/0012_hookerror.py b/breathecode/notify/migrations/0012_hookerror.py new file mode 100644 index 000000000..2a33a48da --- /dev/null +++ b/breathecode/notify/migrations/0012_hookerror.py @@ -0,0 +1,25 @@ +# Generated by Django 5.0.6 on 2024-06-17 23:57 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('notify', '0011_alter_hook_user'), + ] + + operations = [ + migrations.CreateModel( + name='HookError', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('message', models.CharField(max_length=255)), + ('event', models.CharField(db_index=True, max_length=64, verbose_name='Event')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('hooks', models.ManyToManyField(blank=True, related_name='errors', to=settings.HOOK_CUSTOM_MODEL)), + ], + ), + ] diff --git a/breathecode/notify/models.py b/breathecode/notify/models.py index d49ea7b55..023d1a61d 100644 --- a/breathecode/notify/models.py +++ b/breathecode/notify/models.py @@ -1,11 +1,13 @@ -from django.db import models from collections import OrderedDict -from django.core import serializers + from django.conf import settings from django.contrib.auth.models import User -from breathecode.admissions.models import Academy, Cohort +from django.core import serializers +from django.db import models from rest_framework.exceptions import ValidationError +from breathecode.admissions.models import Academy, Cohort + __all__ = ['UserProxy', 'CohortProxy', 'Device', 'SlackTeam', 'SlackUser', 'SlackUserTeam', 'SlackChannel', 'Hook'] AUTH_USER_MODEL = getattr(settings, 'AUTH_USER_MODEL', 'auth.User') if getattr(settings, 'HOOK_CUSTOM_MODEL', None) is None: @@ -197,3 +199,13 @@ class Hook(AbstractHook): class Meta(AbstractHook.Meta): swappable = 'HOOK_CUSTOM_MODEL' + + +class HookError(models.Model): + """Hook Error.""" + + message = models.CharField(max_length=255) + event = models.CharField('Event', max_length=64, db_index=True) + hooks = models.ManyToManyField(Hook, related_name='errors', blank=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) diff --git a/breathecode/notify/receivers.py b/breathecode/notify/receivers.py index 4b997c2bc..6e330ad96 100644 --- a/breathecode/notify/receivers.py +++ b/breathecode/notify/receivers.py @@ -1,8 +1,9 @@ import logging from typing import Type -from django.db.models.signals import post_delete, post_save +from django.db.models.signals import m2m_changed, post_delete, post_save from django.dispatch import receiver +from django.utils import timezone from breathecode.admissions.models import Cohort, CohortUser from breathecode.admissions.serializers import CohortHookSerializer, CohortUserHookSerializer @@ -18,6 +19,7 @@ from breathecode.mentorship.models import MentorshipSession from breathecode.mentorship.serializers import SessionHookSerializer from breathecode.mentorship.signals import mentorship_session_status +from breathecode.notify.models import HookError from breathecode.payments.models import PlanFinancing, Subscription from breathecode.payments.serializers import GetPlanFinancingSerializer, GetSubscriptionHookSerializer from breathecode.payments.signals import planfinancing_created, subscription_created @@ -186,3 +188,9 @@ def new_subscription_created(sender, instance, **kwargs): 'subscription_created', payload_override=serializer.data, academy_override=instance.academy) + + +@receiver(m2m_changed, sender=HookError.hooks.through) +def update_updated_at(sender, instance, **kwargs): + instance.updated_at = timezone.now() + instance.save() diff --git a/breathecode/notify/utils/hook_manager.py b/breathecode/notify/utils/hook_manager.py index 958f8b8d4..a3bea5222 100644 --- a/breathecode/notify/utils/hook_manager.py +++ b/breathecode/notify/utils/hook_manager.py @@ -1,10 +1,13 @@ import logging -from django.conf import settings -from ..tasks import async_deliver_hook from django.apps import apps as django_apps +from django.conf import settings from django.core.exceptions import ImproperlyConfigured +from breathecode.notify.models import HookError + +from ..tasks import async_deliver_hook + logger = logging.getLogger(__name__) @@ -206,16 +209,19 @@ def deliver_hook(self, hook, instance, payload_override=None, academy_override=N payload = hook.serialize_hook(instance) else: payload = payload_override - + if callable(payload): payload = payload(hook, instance) - + logger.debug(f'Calling delayed task deliver_hook for hook {hook.id}') async_deliver_hook.delay(hook.target, payload, hook_id=hook.id) - + return None - except Exception: - # TODO: FIXME - pass + except Exception as e: + instance, _ = HookError.objects.get_or_create(message=str(e), event=hook.event) + + if instance.hooks.filter(id=instance.id).exists() is False: + instance.hooks.add(instance) + HookManager = HookManagerClass() diff --git a/breathecode/tests/mixins/generate_models_mixin/mentorship_models_mixin.py b/breathecode/tests/mixins/generate_models_mixin/mentorship_models_mixin.py index 8cf59557c..d38558a10 100644 --- a/breathecode/tests/mixins/generate_models_mixin/mentorship_models_mixin.py +++ b/breathecode/tests/mixins/generate_models_mixin/mentorship_models_mixin.py @@ -2,7 +2,8 @@ Collections of mixins used to login in authorize microservice """ from breathecode.tests.mixins.models_mixin import ModelsMixin -from .utils import is_valid, create_models, just_one, get_list + +from .utils import create_models, get_list, is_valid, just_one class MentorshipModelsMixin(ModelsMixin): From 7222f0ef76866f798721b5eec540dc0e554c199e Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Tue, 18 Jun 2024 13:05:34 -0400 Subject: [PATCH 15/35] Update admin.py --- breathecode/marketing/admin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/breathecode/marketing/admin.py b/breathecode/marketing/admin.py index 194ffc12f..90c01c5c5 100644 --- a/breathecode/marketing/admin.py +++ b/breathecode/marketing/admin.py @@ -463,6 +463,7 @@ def formentry(self, obj): @admin.register(Downloadable) class DownloadableAdmin(admin.ModelAdmin): list_display = ('slug', 'name', 'academy', 'status', 'open_link') + raw_id_fields = ['author'] def open_link(self, obj): return format_html(f"open link") From 66ad7d9d37db1d54b1c2318c0a47a0776211cd1b Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Tue, 18 Jun 2024 15:59:25 -0400 Subject: [PATCH 16/35] Update client.py --- breathecode/services/learnpack/client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/breathecode/services/learnpack/client.py b/breathecode/services/learnpack/client.py index 3280d9fbe..f39e0a8d5 100644 --- a/breathecode/services/learnpack/client.py +++ b/breathecode/services/learnpack/client.py @@ -74,12 +74,12 @@ def execute_action(self, webhook_id: int): logger.error('Mark action with error') webhook.status = 'ERROR' - webhook.status_text = ''.join(traceback.format_exception(None, e, e.__traceback__)) + webhook.status_text = str(e)+'\n'.join(traceback.format_exception(None, e, e.__traceback__)) webhook.save() except Exception as e: webhook.status = 'ERROR' - webhook.status_text = str(e) + webhook.status_text = str(e)+'\n'.join(traceback.format_exception(None, e, e.__traceback__)) webhook.save() raise e From cf0ddb977ba822c8e792d0e28c5089caf1bb375f Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Tue, 18 Jun 2024 16:05:23 -0400 Subject: [PATCH 17/35] Update admin.py --- breathecode/assignments/admin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/breathecode/assignments/admin.py b/breathecode/assignments/admin.py index 5b0c63867..166c88784 100644 --- a/breathecode/assignments/admin.py +++ b/breathecode/assignments/admin.py @@ -105,6 +105,7 @@ class UserAttachmentAdmin(admin.ModelAdmin): class FinalProjectAdmin(admin.ModelAdmin): list_display = ['name', 'cohort', 'project_status', 'revision_status', 'visibility_status'] search_fields = ('name', 'cohort__slug', 'repo_url', 'members__email') + filter_horizontal = ['members'] list_filter = ['project_status', 'revision_status', 'visibility_status', 'cohort__academy__slug'] # actions = [mark_as_delivered, mark_as_approved, mark_as_rejected, mark_as_ignored] From 256f5d6a32d13648f5f5e2169ff0a5be807b7796 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Tue, 18 Jun 2024 16:06:53 -0400 Subject: [PATCH 18/35] Update admin.py --- breathecode/assignments/admin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/breathecode/assignments/admin.py b/breathecode/assignments/admin.py index 166c88784..8d283d677 100644 --- a/breathecode/assignments/admin.py +++ b/breathecode/assignments/admin.py @@ -106,6 +106,7 @@ class FinalProjectAdmin(admin.ModelAdmin): list_display = ['name', 'cohort', 'project_status', 'revision_status', 'visibility_status'] search_fields = ('name', 'cohort__slug', 'repo_url', 'members__email') filter_horizontal = ['members'] + raw_id_fields = ['cohort'] list_filter = ['project_status', 'revision_status', 'visibility_status', 'cohort__academy__slug'] # actions = [mark_as_delivered, mark_as_approved, mark_as_rejected, mark_as_ignored] From 69d25185c837e6fdb6f7a7d0183eb696e405739b Mon Sep 17 00:00:00 2001 From: jefer94 Date: Tue, 18 Jun 2024 17:41:45 -0500 Subject: [PATCH 19/35] add migration --- breathecode/notify/admin.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/breathecode/notify/admin.py b/breathecode/notify/admin.py index 5fa55f685..e180461cb 100644 --- a/breathecode/notify/admin.py +++ b/breathecode/notify/admin.py @@ -1,17 +1,20 @@ import logging + +from django import forms from django.contrib import admin from django.contrib.auth.admin import UserAdmin -from django import forms -from .utils.hook_manager import HookManager -from .models import Device, SlackTeam, SlackChannel, SlackUser, UserProxy, CohortProxy, SlackUserTeam -from .actions import sync_slack_team_channel, send_slack -from .tasks import async_slack_team_users -from breathecode.admissions.admin import CohortAdmin as AdmissionsCohortAdmin -from django.utils.html import format_html from django.template.defaultfilters import escape from django.urls import reverse +from django.utils.html import format_html + +from breathecode.admissions.admin import CohortAdmin as AdmissionsCohortAdmin from breathecode.utils import AdminExportCsvMixin +from .actions import send_slack, sync_slack_team_channel +from .models import CohortProxy, Device, HookError, SlackChannel, SlackTeam, SlackUser, SlackUserTeam, UserProxy +from .tasks import async_slack_team_users +from .utils.hook_manager import HookManager + logger = logging.getLogger(__name__) @@ -159,3 +162,10 @@ class HookAdmin(admin.ModelAdmin): admin.site.register(HookModel, HookAdmin) + + +@admin.register(HookError) +class HookErrorAdmin(admin.ModelAdmin): + list_display = ['event', 'message', 'created_at', 'updated_at'] + search_fields = ['message', 'event'] + list_filter = ['event'] From e18951df57b022af72d5d8339dcab1cb4c3d8dc0 Mon Sep 17 00:00:00 2001 From: jefer94 Date: Tue, 18 Jun 2024 18:09:52 -0500 Subject: [PATCH 20/35] install google meet --- Pipfile | 3 + Pipfile.lock | 349 ++++++++++++++++++++++++++++++--------------------- 2 files changed, 211 insertions(+), 141 deletions(-) diff --git a/Pipfile b/Pipfile index 6e7955362..34a213c13 100644 --- a/Pipfile +++ b/Pipfile @@ -141,3 +141,6 @@ linked-services = {extras = ["django", "aiohttp", "requests"], version = "*"} celery-task-manager = {extras = ["django"], version = "*"} django-sql-explorer = {extras = ["xls"], version = "*"} contextlib2 = "*" +google-apps-meet = "*" +google-auth-httplib2 = "*" +google-auth-oauthlib = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 250f7a912..88799a040 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "80d4c86258b6b7f34519eb6f1dff4f5401a51b8afe734f7c06390d91f59967e8" + "sha256": "b025bb615b9184df46f25927099a3c2402b501b99e8f783ec93a2c98b09c0b04" }, "pipfile-spec": 6, "requires": {}, @@ -873,10 +873,10 @@ }, "fastjsonschema": { "hashes": [ - "sha256:3672b47bc94178c9f23dbb654bf47440155d4db9df5f7bc47643315f9c405cd0", - "sha256:e3126a94bdc4623d3de4485f8d468a12f02a67921315ddc87836d6e456dc789d" + "sha256:3d48fc5300ee96f5d116f10fe6f28d938e6008f59a6a025c2649475b87f76a23", + "sha256:5875f0b0fa7a0043a91e93a9b8f793bcbbba9691e7fd83dca95c28ba26d21f0a" ], - "version": "==2.19.1" + "version": "==2.20.0" }, "frozenlist": { "hashes": [ @@ -1020,6 +1020,15 @@ "markers": "python_version >= '3.7'", "version": "==2.19.0" }, + "google-apps-meet": { + "hashes": [ + "sha256:061717edea189670ceda0ef20d6a18237da72816be58e559eb4fd23d4590a127", + "sha256:33e60fffc92b0f114e4b32fcb4b1d0030ea0d3187f4f16a0ebb46835c0730e0c" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==0.1.6" + }, "google-auth": { "hashes": [ "sha256:8df7da660f62757388b8a7f249df13549b3373f24388cb5d2f1dd91cc18180b5", @@ -1028,6 +1037,23 @@ "markers": "python_version >= '3.7'", "version": "==2.30.0" }, + "google-auth-httplib2": { + "hashes": [ + "sha256:38aa7badf48f974f1eb9861794e9c0cb2a0511a4ec0679b1f886d108f5640e05", + "sha256:b65a0a2123300dd71281a7bf6e64d65a0759287df52729bdd1ae2e47dc311a3d" + ], + "index": "pypi", + "version": "==0.2.0" + }, + "google-auth-oauthlib": { + "hashes": [ + "sha256:292d2d3783349f2b0734a0a0207b1e1e322ac193c2c09d8f7c613fb7cc501ea8", + "sha256:297c1ce4cb13a99b5834c74a1fe03252e1e499716718b190f56bcb9c4abc4faf" + ], + "index": "pypi", + "markers": "python_version >= '3.6'", + "version": "==1.2.0" + }, "google-cloud-bigquery": { "hashes": [ "sha256:bc08323ce99dee4e811b7c3d0cde8929f5bf0b1aeaed6bcd75fc89796dd87652", @@ -1090,12 +1116,12 @@ }, "google-cloud-storage": { "hashes": [ - "sha256:91a06b96fb79cf9cdfb4e759f178ce11ea885c79938f89590344d079305f5852", - "sha256:dda485fa503710a828d01246bd16ce9db0823dc51bbca742ce96a6817d58669f" + "sha256:49378abff54ef656b52dca5ef0f2eba9aa83dc2b2c72c78714b03a1a95fe9388", + "sha256:5b393bc766b7a3bc6f5407b9e665b2450d36282614b7945e570b3480a456d1e1" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==2.16.0" + "version": "==2.17.0" }, "google-crc32c": { "hashes": [ @@ -1173,11 +1199,11 @@ }, "google-resumable-media": { "hashes": [ - "sha256:5f18f5fa9836f4b083162064a1c2c98c17239bfda9ca50ad970ccf905f3e625b", - "sha256:79543cfe433b63fd81c0844b7803aba1bb8950b47bedf7d980c38fa123937e08" + "sha256:103ebc4ba331ab1bfdac0250f8033627a2cd7cde09e7ccff9181e31ba4315b2c", + "sha256:eae451a7b2e2cdbaaa0fd2eb00cc8a1ee5e95e16b55597359cbc3d27d7d90e33" ], "markers": "python_version >= '3.7'", - "version": "==2.7.0" + "version": "==2.7.1" }, "googleapis-common-protos": { "hashes": [ @@ -1196,11 +1222,11 @@ }, "graphene-django": { "hashes": [ - "sha256:3fbdd8d4990ecec326c59d68edfcaf9a7bc9c4dbdcbf88b11ac46dfc10240e49", - "sha256:52145037872d2575974c4bb2be224756ffeafe5a4e20f9c4367519622965812b" + "sha256:059ccf25d9a5159f28d7ebf1a648c993ab34deb064e80b70ca096aa22a609556", + "sha256:0fd95c8c1cbe77ae2a5940045ce276803c3acbf200a156731e0c730f2776ae2c" ], "index": "pypi", - "version": "==3.2.1" + "version": "==3.2.2" }, "graphene-django-optimizer": { "hashes": [ @@ -1287,7 +1313,7 @@ "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da", "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33" ], - "markers": "python_version >= '3.7'", + "markers": "python_version >= '3' and platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32')))))", "version": "==3.0.3" }, "grpcio": { @@ -1504,6 +1530,14 @@ "markers": "python_version >= '3.8'", "version": "==1.0.5" }, + "httplib2": { + "hashes": [ + "sha256:14ae0a53c1ba8f3d37e9e27cf37eabb0fb9980f435ba405d546948b009dd64dc", + "sha256:d7a10bc5ef5ab08322488bde8c726eeee5c8618723fdb399597ec58f3d82df81" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==0.22.0" + }, "httpx": { "hashes": [ "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5", @@ -1928,11 +1962,11 @@ }, "more-itertools": { "hashes": [ - "sha256:686b06abe565edfab151cb8fd385a05651e1fdf8f0a14191e4439283421f8684", - "sha256:8fccb480c43d3e99a00087634c06dd02b0d50fbf088b380de5a41a015ec239e1" + "sha256:e5d93ef411224fbcef366a6e8ddc4c5781bc6359d43412a65dd5964e46111463", + "sha256:ea6a02e24a9161e51faad17a8782b92a0df82c12c1c8886fec7f0c3fa1a1b320" ], "markers": "python_version >= '3.8'", - "version": "==10.2.0" + "version": "==10.3.0" }, "msgpack": { "hashes": [ @@ -2119,99 +2153,116 @@ }, "newrelic": { "hashes": [ - "sha256:02db25b0fd2fc835efe4a7f1c92dbc5bbb95125341aba07152041aa6a5666cda", - "sha256:09912303e04bee6aa1fe1c671e87b4e8e55461081a96210895828798f5ba8c3f", - "sha256:1cc3ddb26c0615ba4e18f87453bca57f0688a43d2fcdd50e2771a77515cfc3ba", - "sha256:1f11d9c17b50982fcc39de71f6592a61920ec5e5c29b9105edc9f8fb7f2480b9", - "sha256:2236f70b8c6aa79635f2175e7315d032f3a80dfd65ad9c9ed12a921f5df4c655", - "sha256:40368dca0d423efe40b210686d7018787d4365a24ee1deca136b3b7c9d850325", - "sha256:4404c649b5e6165dcdd59091092c19b292a43cc96520d5ffd718b628fb866096", - "sha256:4d68fc707d896dc7da8d6939bcc1f995bf9e463c2b911fc63250a10e1502a234", - "sha256:4e573d49c1543a488d6567906a9b2cb0c748cdbf80724c322b06874f8e47c789", - "sha256:524ed5bfa09d330746b45e0087765da994ca34802cce032063041e404e58414c", - "sha256:56f4c309a07a2c66243b12d18056c32aa704735469741495642c31be4a1c77fa", - "sha256:5d18236bf4a80fca4eb1db03448ed72bf8e16b84b3a4ed5fcc29bb91c2d05d54", - "sha256:667722cf1f4ed9f6cd99f4fbe247fc2bdb941935528e14a93659ba2c651dc889", - "sha256:6ed4bc2c9a44dfe59958eeecf1f327f0a0fb6324b5e609515bc511944d12db74", - "sha256:744c815f15ec06e441c11a6c57042d2eca8c41401c11de6f47b3e105d952b9bd", - "sha256:77537a020ce84033f39210e46cc43bb3927cec3fb4b34b5c4df802e96fddaedf", - "sha256:7cd462804a6ede617fb3b4b126e9083b3ee8b4ed1250f7cc12299ebacb785432", - "sha256:8ad9cd5459b8c620ab7a876bd5d920c3ef2943948d1262a42289d4f8d16dadab", - "sha256:a4d4e5670082225ca7ef0ee986ef8e6588f4e530a05d43d66f9368459c0b1f18", - "sha256:acf5cdcafd2971933ad2f9e836284957f4a3eababe88f063cf53b1b1f67f1a16", - "sha256:ae0515f7ab19f1a5dd14e31506420d1b86014c5e1340c2a210833248bc765dae", - "sha256:ae84bacfdc60792bd04e681027cc5c58e6737a04c652e9be2eda84abe21f57f5", - "sha256:b8201a33caf7632b2e55e3f9687584ad6956aaf5751485cdb2bad7c428a9b400", - "sha256:bf6757d422954e61082715dbba4208cae17bf3720006bc337c3f87f19ede2876", - "sha256:ceef4fef2a5cffb69e9e1742bd18a35625ca62c3856c7016c22be68ec876753d", - "sha256:d0c18210648889416da3de61aa282248e012cb507ba9841511407f922fff9a52", - "sha256:d3be6c97d007ceb142f908f5ab2444807b44dc600a0b7f3254dc685b5b03fd10", - "sha256:e2576bbec0b640d9b76454dcfd5b2f03078e0bb062a7ea3952a8db7b9972c352", - "sha256:f4605bc4feb114235e242dfe260b75ec85d0894f5400aa7f30e75fbbc0423b3f" + "sha256:02eab15af4a08b870bcfdbc56390ecbb9dcacd144fe77f39a26d1be207bd30f0", + "sha256:0d43a0891bf71333f6a6253cf87dea2c9009e22699a2acfd93608125a33b1936", + "sha256:10cb7f7a78c49580602b90f367f3378264e495f2f3706734f88ced7e7ca9b033", + "sha256:11653fd14f55999c5058b4dde8c721833076c0bd3efe668296725a622e9e7de8", + "sha256:1f6e1bb0df8ff2b54195baac41fddc0e15ea1bdf1deb6af49153487696355181", + "sha256:2010ed2793294a7e3c1057ec301d48997ed05dcef114d4c25120ac771f66bac1", + "sha256:21e7b52d5b214bba3534ced166e6ec991117772815020bec38b0571fdcecbaf4", + "sha256:2b02139458aefba86a4572cb8214f91a942103d24d5502395f64d6d7a4ad3f25", + "sha256:3283885bcf31d9cbf8facb0004508a4eaa652a62471e0b724d26f9738a291979", + "sha256:34b25d1beaf19825409f3d915a5bafa87b7b9230415821422be1e78e988750b7", + "sha256:35d08587e694f5c517e55fb7119f924c64569d2e7ec4968ef761fc1f7bd1f40c", + "sha256:553674a66ef2c2206852b415b74e3c2fb7ed2b92e9800b68394d577f6aa1133e", + "sha256:5f477cdda9b998205084b822089b3ee4a8a2d9cd66b6f12487c9f9002566c5cb", + "sha256:6ceac1d8f13da38fa1b41c8202a91d3b4345e06adb655deaae0df08911fda56f", + "sha256:72dd3eb190c62bb54aa59029f0d6ac1420c2050b3aaf88d947fc7f62ec58d97f", + "sha256:76eb4cc599645a38a459b0002696d9c84844fecb02cf07bc18a4a91f737e438e", + "sha256:7c073f4c26539d6d74fbf4bac7f5046cac578975fb2cf77b156f802f1b39835e", + "sha256:7f1e473eb0505cb91ab9a4155321eabe13a2f6b93fb3c41d6f10e5486276be60", + "sha256:8664e3b9e6ee0f78806b0cf7c90656a1a86d13232c2e0be18a1b1eb452f3f5d1", + "sha256:87670d872c3abc36203e10f93d266c8f36ad2bd06fb54e790001a409f9e2f40f", + "sha256:94369792d61ccf21469c35cf66886c32350a180d8e782c0d28ec66411db29474", + "sha256:9f95eb366ff714bce32476d256551b853247a72398ec46a89148ef5108509aa8", + "sha256:b33539345c7cf349b65a176a30ab38e2998b071512a7450f5c5b89ac6c097006", + "sha256:b5d2d0814e1aa9de5bd55797ff8c426d98200ba46ca14dbca15557d0f17cfb4e", + "sha256:b7903ba71ce5a4b2840f6d3c63ecd0fb3a018d2aceb915b48133c13c4a60185f", + "sha256:bc5c1b8a51946f64c34fc5fa29ce0221c4927a65c7f4435b3b8adeb29b9812d2", + "sha256:d88fa17a515fb002eb14570800e4bfa69ac87ac27e6e2a96bc2bc9b60c80057a", + "sha256:dcec4173cd0f83420e6f61f92955065f1d460075af5e5bf88a5fea746e3cc180", + "sha256:ffc0d8d490de0f12df70db637481aaadb8a43fb6d71ba8866dc14242aa5edad4" ], "index": "pypi", "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", - "version": "==9.10.0" + "version": "==9.11.0" }, "numpy": { "hashes": [ - "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b", - "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818", - "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20", - "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0", - "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", - "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a", - "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea", - "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c", - "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71", - "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110", - "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be", - "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a", - "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a", - "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5", - "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed", - "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd", - "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c", - "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e", - "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0", - "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c", - "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a", - "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b", - "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0", - "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6", - "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2", - "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a", - "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30", - "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218", - "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5", - "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07", - "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2", - "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4", - "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764", - "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef", - "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3", - "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f" + "sha256:04494f6ec467ccb5369d1808570ae55f6ed9b5809d7f035059000a37b8d7e86f", + "sha256:0a43f0974d501842866cc83471bdb0116ba0dffdbaac33ec05e6afed5b615238", + "sha256:0e50842b2295ba8414c8c1d9d957083d5dfe9e16828b37de883f51fc53c4016f", + "sha256:0ec84b9ba0654f3b962802edc91424331f423dcf5d5f926676e0150789cb3d95", + "sha256:17067d097ed036636fa79f6a869ac26df7db1ba22039d962422506640314933a", + "sha256:1cde1753efe513705a0c6d28f5884e22bdc30438bf0085c5c486cdaff40cd67a", + "sha256:1e72728e7501a450288fc8e1f9ebc73d90cfd4671ebbd631f3e7857c39bd16f2", + "sha256:2635dbd200c2d6faf2ef9a0d04f0ecc6b13b3cad54f7c67c61155138835515d2", + "sha256:2ce46fd0b8a0c947ae047d222f7136fc4d55538741373107574271bc00e20e8f", + "sha256:34f003cb88b1ba38cb9a9a4a3161c1604973d7f9d5552c38bc2f04f829536609", + "sha256:354f373279768fa5a584bac997de6a6c9bc535c482592d7a813bb0c09be6c76f", + "sha256:38ecb5b0582cd125f67a629072fed6f83562d9dd04d7e03256c9829bdec027ad", + "sha256:3e8e01233d57639b2e30966c63d36fcea099d17c53bf424d77f088b0f4babd86", + "sha256:3f6bed7f840d44c08ebdb73b1825282b801799e325bcbdfa6bc5c370e5aecc65", + "sha256:4554eb96f0fd263041baf16cf0881b3f5dafae7a59b1049acb9540c4d57bc8cb", + "sha256:46e161722e0f619749d1cd892167039015b2c2817296104487cd03ed4a955995", + "sha256:49d9f7d256fbc804391a7f72d4a617302b1afac1112fac19b6c6cec63fe7fe8a", + "sha256:4d2f62e55a4cd9c58c1d9a1c9edaedcd857a73cb6fda875bf79093f9d9086f85", + "sha256:5f64641b42b2429f56ee08b4f427a4d2daf916ec59686061de751a55aafa22e4", + "sha256:63b92c512d9dbcc37f9d81b123dec99fdb318ba38c8059afc78086fe73820275", + "sha256:6d7696c615765091cc5093f76fd1fa069870304beaccfd58b5dcc69e55ef49c1", + "sha256:79e843d186c8fb1b102bef3e2bc35ef81160ffef3194646a7fdd6a73c6b97196", + "sha256:821eedb7165ead9eebdb569986968b541f9908979c2da8a4967ecac4439bae3d", + "sha256:84554fc53daa8f6abf8e8a66e076aff6ece62de68523d9f665f32d2fc50fd66e", + "sha256:8d83bb187fb647643bd56e1ae43f273c7f4dbcdf94550d7938cfc32566756514", + "sha256:903703372d46bce88b6920a0cd86c3ad82dae2dbef157b5fc01b70ea1cfc430f", + "sha256:9416a5c2e92ace094e9f0082c5fd473502c91651fb896bc17690d6fc475128d6", + "sha256:9a1712c015831da583b21c5bfe15e8684137097969c6d22e8316ba66b5baabe4", + "sha256:9c27f0946a3536403efb0e1c28def1ae6730a72cd0d5878db38824855e3afc44", + "sha256:a356364941fb0593bb899a1076b92dfa2029f6f5b8ba88a14fd0984aaf76d0df", + "sha256:a7039a136017eaa92c1848152827e1424701532ca8e8967fe480fe1569dae581", + "sha256:acd3a644e4807e73b4e1867b769fbf1ce8c5d80e7caaef0d90dcdc640dfc9787", + "sha256:ad0c86f3455fbd0de6c31a3056eb822fc939f81b1618f10ff3406971893b62a5", + "sha256:b4c76e3d4c56f145d41b7b6751255feefae92edbc9a61e1758a98204200f30fc", + "sha256:b6f6a8f45d0313db07d6d1d37bd0b112f887e1369758a5419c0370ba915b3871", + "sha256:c5a59996dc61835133b56a32ebe4ef3740ea5bc19b3983ac60cc32be5a665d54", + "sha256:c73aafd1afca80afecb22718f8700b40ac7cab927b8abab3c3e337d70e10e5a2", + "sha256:cee6cc0584f71adefe2c908856ccc98702baf95ff80092e4ca46061538a2ba98", + "sha256:cef04d068f5fb0518a77857953193b6bb94809a806bd0a14983a8f12ada060c9", + "sha256:cf5d1c9e6837f8af9f92b6bd3e86d513cdc11f60fd62185cc49ec7d1aba34864", + "sha256:e61155fae27570692ad1d327e81c6cf27d535a5d7ef97648a17d922224b216de", + "sha256:e7f387600d424f91576af20518334df3d97bc76a300a755f9a8d6e4f5cadd289", + "sha256:ed08d2703b5972ec736451b818c2eb9da80d66c3e84aed1deeb0c345fefe461b", + "sha256:fbd6acc766814ea6443628f4e6751d0da6593dae29c08c0b2606164db026970c", + "sha256:feff59f27338135776f6d4e2ec7aeeac5d5f7a08a83e80869121ef8164b74af9" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==1.26.4" + "version": "==2.0.0" + }, + "oauthlib": { + "hashes": [ + "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca", + "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918" + ], + "markers": "python_version >= '3.6'", + "version": "==3.2.2" }, "openai": { "hashes": [ - "sha256:1169211a7b326ecbc821cafb427c29bfd0871f9a3e0947dd9e51acb3b0f1df78", - "sha256:621163b56570897ab8389d187f686a53d4771fd6ce95d481c0a9611fe8bc4229" + "sha256:018623c2f795424044675c6230fa3bfbf98d9e0aab45d8fd116f2efb2cfb6b7e", + "sha256:95c8e2da4acd6958e626186957d656597613587195abd0fb2527566a93e76770" ], "index": "pypi", "markers": "python_full_version >= '3.7.1'", - "version": "==1.33.0" + "version": "==1.34.0" }, "packaging": { "hashes": [ - "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", - "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9" + "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", + "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124" ], - "markers": "python_version >= '3.7'", - "version": "==24.0" + "markers": "python_version >= '3.8'", + "version": "==24.1" }, "pandas": { "hashes": [ @@ -2259,10 +2310,10 @@ }, "phonenumberslite": { "hashes": [ - "sha256:20e821bb9b9e2fcaa10cd3618e7606264ed0cf4b04877c938ec5d6211ba9efa6", - "sha256:ebad5c1df879de316899e93b8ea9730a822e1b2ec6d83230f0aa1946a5900153" + "sha256:6ec6fb4a0757f0e070c4b1754817976ff77766e664c445b2e5e411300ff6274e", + "sha256:d40c82229e46cabd2d1f8a245a05fd9dbb8faa309ef31d3c58c8c249fc70f0c1" ], - "version": "==8.13.38" + "version": "==8.13.39" }, "pillow": { "hashes": [ @@ -2388,11 +2439,11 @@ }, "prompt-toolkit": { "hashes": [ - "sha256:45abe60a8300f3c618b23c16c4bb98c6fc80af8ce8b17c7ae92db48db3ee63c1", - "sha256:869c50d682152336e23c4db7f74667639b5047494202ffe7670817053fd57795" + "sha256:0d7bfa67001d5e39d02c224b663abc33687405033a8c422d0d675a5a13361d10", + "sha256:1e1b29cb58080b1e69f207c893a1a7bf16d127a5c30c9d17a25a5d77792e5360" ], "markers": "python_full_version >= '3.7.0'", - "version": "==3.0.46" + "version": "==3.0.47" }, "proto-plus": { "hashes": [ @@ -2649,11 +2700,11 @@ }, "pydantic": { "hashes": [ - "sha256:c46c76a40bb1296728d7a8b99aa73dd70a48c3510111ff290034f860c99c419e", - "sha256:ea91b002777bf643bb20dd717c028ec43216b24a6001a280f83877fd2655d0b4" + "sha256:0c84efd9548d545f63ac0060c1e4d39bb9b14db8b3c0652338aecc07b5adec52", + "sha256:ee8538d41ccb9c0a9ad3e0e5f07bf15ed8015b481ced539a1759d8cc89ae90d0" ], "markers": "python_version >= '3.8'", - "version": "==2.7.3" + "version": "==2.7.4" }, "pydantic-core": { "hashes": [ @@ -2742,11 +2793,11 @@ }, "pyfcm": { "hashes": [ - "sha256:82340ae9d76b5d2bccc3392a6688872016b648d3375c41641e888bc337674d76", - "sha256:aa4a391dfcabb0fffebc28ead0d79f0db113d15c03ea06334b1387804112d69c" + "sha256:b503d6145a1a92b72eb70f93ab63b6692a17f826f334c9beffa1a07b16748e2f", + "sha256:bca1ef37188bef85b547464346d016f63610fa21de4c8589c49e24f2dad23002" ], "index": "pypi", - "version": "==1.5.4" + "version": "==2.0.1" }, "pygithub": { "hashes": [ @@ -2807,6 +2858,14 @@ ], "version": "==24.1.0" }, + "pyparsing": { + "hashes": [ + "sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad", + "sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742" + ], + "markers": "python_version >= '3.1'", + "version": "==3.1.2" + }, "pyrfc3339": { "hashes": [ "sha256:67196cb83b470709c580bb4738b83165e67c6cc60e1f2e4f286cfcb402a926f4", @@ -3030,11 +3089,11 @@ "hiredis" ], "hashes": [ - "sha256:30b47d4ebb6b7a0b9b40c1275a19b87bb6f46b3bed82a89012cf56dea4024ada", - "sha256:3417688621acf6ee368dec4a04dd95881be24efd34c79f00d31f62bb528800ae" + "sha256:38473cd7c6389ad3e44a91f4c3eaf6bcb8a9f746007f29bf4fb20824ff0b2197", + "sha256:c0d6d990850c627bbf7be01c5c4cbaadf67b48593e913bb71c9819c30df37eee" ], "markers": "python_version >= '3.7'", - "version": "==5.0.5" + "version": "==5.0.6" }, "referencing": { "hashes": [ @@ -3053,6 +3112,14 @@ "markers": "python_version >= '3.8'", "version": "==2.32.3" }, + "requests-oauthlib": { + "hashes": [ + "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", + "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9" + ], + "markers": "python_version >= '3.4'", + "version": "==2.0.0" + }, "rpds-py": { "hashes": [ "sha256:05f3d615099bd9b13ecf2fc9cf2d839ad3f20239c678f461c753e93755d629ee", @@ -3303,12 +3370,12 @@ }, "stripe": { "hashes": [ - "sha256:2384749fdc4d252f3c42bb915ef175b7f93253c115311d77db37fbcd24683849", - "sha256:40206520758cfab6b3fbae05f54d3659b018fcee54c955c08e606c91817aa05f" + "sha256:14dd20ef1e6386a52e1f7aea07701fc6a80223706d72e3509e4966adb599e138", + "sha256:cbc526abd0f001c920c323ba7c40cce3dee1647d920e9dbecae3488f37367524" ], "index": "pypi", "markers": "python_version >= '3.6'", - "version": "==9.10.0" + "version": "==9.12.0" }, "text-unidecode": { "hashes": [ @@ -3367,12 +3434,12 @@ }, "twilio": { "hashes": [ - "sha256:cc3e090c3884db7d70e7c647358b9cf1f4d30fd3fbe0412adcae0df8459d29b0", - "sha256:cfe72b12cabac2f0997f1060d53cea14bd1196e2cbda14789e53c7dd762c4349" + "sha256:4908aa0ca9d2b61660d49904d3c296aaa70c8a57869dc26866b71206548c620d", + "sha256:cdd3608f8aa9c7f197410a0e29cf443ba4e8ce3dff37ca674b374e5755559147" ], "index": "pypi", "markers": "python_full_version >= '3.7.0'", - "version": "==9.1.1" + "version": "==9.2.0" }, "twisted": { "extras": [ @@ -3421,11 +3488,11 @@ }, "urllib3": { "hashes": [ - "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d", - "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19" + "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472", + "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168" ], "markers": "python_version >= '3.8'", - "version": "==2.2.1" + "version": "==2.2.2" }, "uvicorn": { "hashes": [ @@ -4001,20 +4068,20 @@ }, "filelock": { "hashes": [ - "sha256:43339835842f110ca7ae60f1e1c160714c5a6afd15a2873419ab185334975c0f", - "sha256:6ea72da3be9b8c82afd3edcf99f2fffbb5076335a5ae4d03248bb5b6c3eae78a" + "sha256:58a2549afdf9e02e10720eaa4d4470f56386d7a6f72edd7d0596337af8ed7ad8", + "sha256:71b3102950e91dfc1bb4209b64be4dc8854f40e5f534428d8684f953ac847fac" ], "markers": "python_version >= '3.8'", - "version": "==3.14.0" + "version": "==3.15.1" }, "flake8": { "hashes": [ - "sha256:33f96621059e65eec474169085dc92bf26e7b2d47366b70be2f67ab80dc25132", - "sha256:a6dfbb75e03252917f2473ea9653f7cd799c3064e54d4c8140044c5c065f53c3" + "sha256:2e416edcc62471a64cea09353f4e7bdba32aeb079b6e360554c659a122b1bc6a", + "sha256:48a07b626b55236e0fb4784ee69a465fbf59d79eec1f5b4785c3d3bc57d17aa5" ], "index": "pypi", "markers": "python_full_version >= '3.8.1'", - "version": "==7.0.0" + "version": "==7.1.0" }, "flake8-bugbear": { "hashes": [ @@ -4203,16 +4270,16 @@ "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da", "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33" ], - "markers": "python_version >= '3.7'", + "markers": "python_version >= '3' and platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32')))))", "version": "==3.0.3" }, "griffe": { "hashes": [ - "sha256:297ec8530d0c68e5b98ff86fb588ebc3aa3559bb5dc21f3caea8d9542a350133", - "sha256:83ce7dcaafd8cb7f43cbf1a455155015a1eb624b1ffd93249e5e1c4a22b2fdb2" + "sha256:07a2fd6a8c3d21d0bbb0decf701d62042ccc8a576645c7f8799fe1f10de2b2de", + "sha256:95119a440a3c932b13293538bdbc405bee4c36428547553dc6b327e7e7d35e5a" ], "markers": "python_version >= '3.8'", - "version": "==0.45.2" + "version": "==0.47.0" }, "grpcio": { "hashes": [ @@ -4439,12 +4506,12 @@ }, "mkdocs-material": { "hashes": [ - "sha256:56aeb91d94cffa43b6296fa4fbf0eb7c840136e563eecfd12c2d9e92e50ba326", - "sha256:5d01fb0aa1c7946a1e3ae8689aa2b11a030621ecb54894e35aabb74c21016312" + "sha256:a7d4a35f6d4a62b0c43a0cfe7e987da0980c13587b5bc3c26e690ad494427ec0", + "sha256:af8cc263fafa98bb79e9e15a8c966204abf15164987569bd1175fd66a7705182" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==9.5.26" + "version": "==9.5.27" }, "mkdocs-material-extensions": { "hashes": [ @@ -4465,12 +4532,12 @@ }, "mkdocstrings-python": { "hashes": [ - "sha256:11ff6d21d3818fb03af82c3ea6225b1534837e17f790aa5f09626524171f949b", - "sha256:321cf9c732907ab2b1fedaafa28765eaa089d89320f35f7206d00ea266889d03" + "sha256:55141806a463fedad0d8d405088612aaea7efa518aa24d4a6227021775c44369", + "sha256:629a7d8bdd38358275dd44078bfc560f85e62ad3f244816b04783f30c4e2fea0" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.10.3" + "version": "==1.10.4" }, "nodeenv": { "hashes": [ @@ -4490,11 +4557,11 @@ }, "packaging": { "hashes": [ - "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", - "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9" + "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", + "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124" ], - "markers": "python_version >= '3.7'", - "version": "==24.0" + "markers": "python_version >= '3.8'", + "version": "==24.1" }, "paginate": { "hashes": [ @@ -4587,11 +4654,11 @@ }, "pycodestyle": { "hashes": [ - "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f", - "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67" + "sha256:442f950141b4f43df752dd303511ffded3a04c2b6fb7f65980574f0c31e6e79c", + "sha256:949a39f6b86c3e1515ba1787c2022131d165a8ad271b11370a8819aa070269e4" ], "markers": "python_version >= '3.8'", - "version": "==2.11.1" + "version": "==2.12.0" }, "pydocstyle": { "hashes": [ @@ -4894,11 +4961,11 @@ }, "urllib3": { "hashes": [ - "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d", - "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19" + "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472", + "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168" ], "markers": "python_version >= '3.8'", - "version": "==2.2.1" + "version": "==2.2.2" }, "virtualenv": { "hashes": [ From 1faf0e4b53f25a138c462581ff825e8edc99e185 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Thu, 20 Jun 2024 11:54:37 -0400 Subject: [PATCH 21/35] Update views.py --- breathecode/assessment/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/breathecode/assessment/views.py b/breathecode/assessment/views.py index ddffdafa7..1d4d59054 100644 --- a/breathecode/assessment/views.py +++ b/breathecode/assessment/views.py @@ -393,7 +393,7 @@ def get(self, request, assessment_slug, threshold_id=None): raise ValidationException(f'Threshold {threshold_id} not found', 404, slug='threshold-not-found') serializer = GetAssessmentThresholdSerializer(single, many=False) - return handler.response(serializer.data) + return Response(serializer.data, status=status.HTTP_200_OK) # get original all assessments (assessments that have no parent) items = AssessmentThreshold.objects.filter(assessment__slug=assessment_slug) From ed74b5feb8091910b0b72cbc932f76689113b824 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Thu, 20 Jun 2024 11:56:06 -0400 Subject: [PATCH 22/35] Update admin.py --- breathecode/assessment/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/breathecode/assessment/admin.py b/breathecode/assessment/admin.py index 7c3628e1b..0e92d5a07 100644 --- a/breathecode/assessment/admin.py +++ b/breathecode/assessment/admin.py @@ -96,7 +96,7 @@ def from_status(s): @admin.register(AssessmentThreshold) class UserAssessmentThresholdAdmin(admin.ModelAdmin): search_fields = ['assessment__slug', 'assessment__title'] - list_display = ['id', 'score_threshold', 'assessment'] + list_display = ['id', 'title', 'score_threshold', 'assessment'] list_filter = ['assessment__slug'] actions = [] From 7a47e40379fcab9b2097f5b8dc8330cfb7261aab Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Thu, 20 Jun 2024 16:24:19 +0000 Subject: [PATCH 23/35] added threshold tags --- .../0010_assessmentthreshold_tags.py | 24 +++++++++++++++++++ breathecode/assessment/models.py | 9 +++++++ breathecode/assessment/views.py | 4 ++++ 3 files changed, 37 insertions(+) create mode 100644 breathecode/assessment/migrations/0010_assessmentthreshold_tags.py diff --git a/breathecode/assessment/migrations/0010_assessmentthreshold_tags.py b/breathecode/assessment/migrations/0010_assessmentthreshold_tags.py new file mode 100644 index 000000000..31ad23af3 --- /dev/null +++ b/breathecode/assessment/migrations/0010_assessmentthreshold_tags.py @@ -0,0 +1,24 @@ +# Generated by Django 5.0.3 on 2024-06-20 16:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assessment', '0009_assessmentthreshold_title'), + ] + + operations = [ + migrations.AddField( + model_name='assessmentthreshold', + name='tags', + field=models.CharField( + blank=True, + default=None, + help_text= + 'Ideal to group thresholds under a taxonomy, that way you can have several groups of thresholds for the same quiz', + max_length=255, + null=True), + ), + ] diff --git a/breathecode/assessment/models.py b/breathecode/assessment/models.py index 28b6706b9..0d30d0984 100644 --- a/breathecode/assessment/models.py +++ b/breathecode/assessment/models.py @@ -137,6 +137,15 @@ class AssessmentThreshold(models.Model): null=True, help_text='Title is good for internal use') + tags = models.CharField( + max_length=255, + default=None, + blank=True, + null=True, + help_text= + 'Ideal to group thresholds under a taxonomy, that way you can have several groups of thresholds for the same quiz' + ) + academy = models.ForeignKey( Academy, on_delete=models.CASCADE, diff --git a/breathecode/assessment/views.py b/breathecode/assessment/views.py index ddffdafa7..86084f115 100644 --- a/breathecode/assessment/views.py +++ b/breathecode/assessment/views.py @@ -406,6 +406,10 @@ def get(self, request, assessment_slug, threshold_id=None): lookup['academy__id'] = int(param) else: lookup['academy__slug'] = param + if 'tag' in self.request.GET: + param = self.request.GET.get('tags') + lookup['tags__icontains'] = param + else: lookup['academy__isnull'] = True From d752fbe30e1316b9c19a246f7c19fa09cdca0f4e Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Thu, 20 Jun 2024 16:24:47 +0000 Subject: [PATCH 24/35] added threshold tags --- breathecode/assessment/admin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/breathecode/assessment/admin.py b/breathecode/assessment/admin.py index 0e92d5a07..14c32ff7f 100644 --- a/breathecode/assessment/admin.py +++ b/breathecode/assessment/admin.py @@ -95,8 +95,8 @@ def from_status(s): @admin.register(AssessmentThreshold) class UserAssessmentThresholdAdmin(admin.ModelAdmin): - search_fields = ['assessment__slug', 'assessment__title'] - list_display = ['id', 'title', 'score_threshold', 'assessment'] + search_fields = ['assessment__slug', 'assessment__title', 'tags'] + list_display = ['id', 'title', 'score_threshold', 'assessment', 'tags'] list_filter = ['assessment__slug'] actions = [] From 2925d967ac29839b2351b5cd2b1bf14c36bed594 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Thu, 20 Jun 2024 14:55:25 -0400 Subject: [PATCH 25/35] Update actions.py --- breathecode/registry/actions.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/breathecode/registry/actions.py b/breathecode/registry/actions.py index 918e999b6..576ca47ad 100644 --- a/breathecode/registry/actions.py +++ b/breathecode/registry/actions.py @@ -718,6 +718,7 @@ def process_asset_config(asset, config): if isinstance(config['video']['solution'], str): asset.solution_video_url = get_video_url(str(config['video']['solution'])) asset.with_video = True + asset.with_solutions = True else: if 'en' in config['video']['solution']: config['video']['solution']['us'] = config['video']['solution']['en'] @@ -725,6 +726,7 @@ def process_asset_config(asset, config): config['video']['solution']['en'] = config['video']['solution']['us'] if asset.lang in config['video']['solution']: + asset.with_solutions = True asset.solution_video_url = get_video_url(str(config['video']['solution'][asset.lang])) asset.with_video = True @@ -732,12 +734,20 @@ def process_asset_config(asset, config): asset.duration = config['duration'] if 'difficulty' in config: asset.difficulty = config['difficulty'].upper() + if 'videoSolutions' in config: + asset.with_solutions = True + asset.with_video = True if 'solution' in config: asset.solution_url = config['solution'] asset.with_solutions = True - if 'projectType' in config: - asset.gitpod = config['projectType'] == 'tutorial' + if 'projectType' in config and config['projectType'] == 'tutorial': + asset.gitpod = True + asset.interactive = True + if 'grading' in config and config['grading'] in ['isolated', 'incremental']: + asset.gitpod = True + asset.interactive = True + if 'technologies' in config: asset.technologies.clear() From 8f5812d56003a47cd30950ddc07f76ca6d7d21cd Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Thu, 20 Jun 2024 16:09:25 -0400 Subject: [PATCH 26/35] Update views.py --- breathecode/provisioning/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/breathecode/provisioning/views.py b/breathecode/provisioning/views.py index 168dbf2c5..cc34380b3 100644 --- a/breathecode/provisioning/views.py +++ b/breathecode/provisioning/views.py @@ -46,7 +46,8 @@ def redirect_new_container(request, token): user = token.user cohort_id = request.GET.get('cohort', None) - if cohort_id is None: return render_message(request, 'Please specificy a cohort in the URL') + if cohort_id is None or cohort_id in ['','undefined']: + return render_message(request, 'Please specificy a cohort in the URL') url = request.GET.get('repo', None) if url is None: From 3d41e694337e8d5ce07107b8c71855f1ee611b7c Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Thu, 20 Jun 2024 16:12:39 -0400 Subject: [PATCH 27/35] Update celery.py --- breathecode/celery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/breathecode/celery.py b/breathecode/celery.py index 6fc8a9910..a0dbea7ad 100644 --- a/breathecode/celery.py +++ b/breathecode/celery.py @@ -7,7 +7,7 @@ # the rest of your Celery file contents go here import os -from datetime import UTC, datetime +from datetime import UTC, datetime, timedelta from typing import TypedDict from celery import Celery From 1b7f6c17889caf66431f7893e4d27dcace5a1bbe Mon Sep 17 00:00:00 2001 From: jefer94 Date: Thu, 20 Jun 2024 15:29:26 -0500 Subject: [PATCH 28/35] add migrations --- ...ntorshipservice_video_provider_and_more.py | 4 ++-- ..._alter_mentorshipservice_video_provider.py | 21 +++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 breathecode/mentorship/migrations/0030_alter_mentorshipservice_video_provider.py diff --git a/breathecode/mentorship/migrations/0029_mentorshipservice_video_provider_and_more.py b/breathecode/mentorship/migrations/0029_mentorshipservice_video_provider_and_more.py index 790fae0e2..371abe86a 100644 --- a/breathecode/mentorship/migrations/0029_mentorshipservice_video_provider_and_more.py +++ b/breathecode/mentorship/migrations/0029_mentorshipservice_video_provider_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 5.0.6 on 2024-06-17 22:09 +# Generated by Django 5.0.6 on 2024-06-20 20:28 import breathecode.utils.validators.language import datetime @@ -19,7 +19,7 @@ class Migration(migrations.Migration): name='video_provider', field=models.CharField(blank=True, choices=[('DAILY', 'Daily'), ('GOOGLE_MEET', 'Google Meet')], - default=None, + default='GOOGLE_MEET', max_length=15), ), migrations.AlterField( diff --git a/breathecode/mentorship/migrations/0030_alter_mentorshipservice_video_provider.py b/breathecode/mentorship/migrations/0030_alter_mentorshipservice_video_provider.py new file mode 100644 index 000000000..13ea932ab --- /dev/null +++ b/breathecode/mentorship/migrations/0030_alter_mentorshipservice_video_provider.py @@ -0,0 +1,21 @@ +# Generated by Django 5.0.6 on 2024-06-20 20:29 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mentorship', '0029_mentorshipservice_video_provider_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='mentorshipservice', + name='video_provider', + field=models.CharField(blank=True, + choices=[('DAILY', 'Daily'), ('GOOGLE_MEET', 'Google Meet')], + default=None, + max_length=15), + ), + ] From f145676e86fd8c495c308a6b91f053f4f31d01d6 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Fri, 21 Jun 2024 12:09:52 -0400 Subject: [PATCH 29/35] Update views.py --- breathecode/assessment/views.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/breathecode/assessment/views.py b/breathecode/assessment/views.py index 26894681b..3e7ff9594 100644 --- a/breathecode/assessment/views.py +++ b/breathecode/assessment/views.py @@ -408,7 +408,10 @@ def get(self, request, assessment_slug, threshold_id=None): lookup['academy__slug'] = param if 'tag' in self.request.GET: param = self.request.GET.get('tags') - lookup['tags__icontains'] = param + if param != "all": + lookup['tags__icontains'] = param + else: + lookup['tags__in'] = ["", None] else: lookup['academy__isnull'] = True From 9972dca268aff83f0b4c877e67d85c6e2c798a5e Mon Sep 17 00:00:00 2001 From: jefer94 Date: Fri, 21 Jun 2024 11:24:29 -0500 Subject: [PATCH 30/35] becomes older datetimes in awake --- breathecode/celery.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/breathecode/celery.py b/breathecode/celery.py index a0dbea7ad..19c914402 100644 --- a/breathecode/celery.py +++ b/breathecode/celery.py @@ -123,7 +123,7 @@ def __exit__(self, *args, **kwargs): if len(data[i]) >= 2: data[i].sort(key=lambda x: x['created_at']) - if datetime.now(UTC) - data[i][-1]['created_at'] < delta: + if datetime.now(UTC) - data[i][-1]['created_at'].replace(tzinfo=UTC) < delta: available[i] = False data[i] = data[i][-2:] else: @@ -131,7 +131,7 @@ def __exit__(self, *args, **kwargs): data[i] = data[i][-1:] elif len(data[i]) == 1: - if datetime.now(UTC) - data[i][0]['created_at'] < delta: + if datetime.now(UTC) - data[i][0]['created_at'].replace(tzinfo=UTC) < delta: available[i] = False else: available[i] = True From 0ca5594d77b15de33c0fc967fe8fed502e233dae Mon Sep 17 00:00:00 2001 From: jefer94 Date: Fri, 21 Jun 2024 11:27:23 -0500 Subject: [PATCH 31/35] fix error --- breathecode/assessment/views.py | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/breathecode/assessment/views.py b/breathecode/assessment/views.py index 3e7ff9594..1cb1950b7 100644 --- a/breathecode/assessment/views.py +++ b/breathecode/assessment/views.py @@ -6,32 +6,28 @@ from rest_framework.permissions import AllowAny from rest_framework.response import Response from rest_framework.views import APIView -from breathecode.utils import ( - APIViewExtensions, - GenerateLookupsMixin, -) from breathecode.authenticate.actions import get_user_language -from breathecode.utils import capable_of +from breathecode.utils import APIViewExtensions, GenerateLookupsMixin, capable_of from breathecode.utils.i18n import translation from capyc.rest_framework.exceptions import ValidationException -from .models import Assessment, AssessmentThreshold, Option, Question, UserAssessment, Answer, AssessmentLayout +from .models import Answer, Assessment, AssessmentLayout, AssessmentThreshold, Option, Question, UserAssessment from .serializers import ( + AnswerSerializer, + AnswerSmallSerializer, AssessmentPUTSerializer, GetAssessmentBigSerializer, + GetAssessmentLayoutSerializer, GetAssessmentSerializer, GetAssessmentThresholdSerializer, - OptionSerializer, - QuestionSerializer, - GetAssessmentLayoutSerializer, - SmallUserAssessmentSerializer, GetUserAssessmentSerializer, + OptionSerializer, PostUserAssessmentSerializer, - PUTUserAssessmentSerializer, - AnswerSerializer, - AnswerSmallSerializer, PublicUserAssessmentSerializer, + PUTUserAssessmentSerializer, + QuestionSerializer, + SmallUserAssessmentSerializer, ) @@ -406,15 +402,15 @@ def get(self, request, assessment_slug, threshold_id=None): lookup['academy__id'] = int(param) else: lookup['academy__slug'] = param + else: + lookup['academy__isnull'] = True + if 'tag' in self.request.GET: param = self.request.GET.get('tags') - if param != "all": + if param != 'all': lookup['tags__icontains'] = param else: - lookup['tags__in'] = ["", None] - - else: - lookup['academy__isnull'] = True + lookup['tags__in'] = ['', None] items = items.filter(**lookup).order_by('-created_at') From c6eff0a721102819669b09fe1f9652f23f4dfcb9 Mon Sep 17 00:00:00 2001 From: jefer94 Date: Fri, 21 Jun 2024 11:33:05 -0500 Subject: [PATCH 32/35] fix other datetimes --- breathecode/celery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/breathecode/celery.py b/breathecode/celery.py index 19c914402..10ea53820 100644 --- a/breathecode/celery.py +++ b/breathecode/celery.py @@ -121,7 +121,7 @@ def __exit__(self, *args, **kwargs): data[i] = [] if len(data[i]) >= 2: - data[i].sort(key=lambda x: x['created_at']) + data[i].sort(key=lambda x: x['created_at'].replace(tzinfo=UTC)) if datetime.now(UTC) - data[i][-1]['created_at'].replace(tzinfo=UTC) < delta: available[i] = False From 922ca1a0678202fa57909181e1e8c41de7cf5fca Mon Sep 17 00:00:00 2001 From: Tomas Gonzalez Date: Mon, 24 Jun 2024 18:27:36 +0000 Subject: [PATCH 33/35] Payment methods added --- breathecode/mentorship/fixtures/dev_data.json | 195 +++++++++--------- breathecode/payments/admin.py | 9 + .../payments/migrations/0047_paymentmethod.py | 32 +++ breathecode/payments/models.py | 13 ++ breathecode/payments/serializers.py | 10 + breathecode/payments/urls.py | 2 + breathecode/payments/views.py | 20 ++ 7 files changed, 182 insertions(+), 99 deletions(-) create mode 100644 breathecode/payments/migrations/0047_paymentmethod.py diff --git a/breathecode/mentorship/fixtures/dev_data.json b/breathecode/mentorship/fixtures/dev_data.json index 8b1dfe681..466cd658c 100644 --- a/breathecode/mentorship/fixtures/dev_data.json +++ b/breathecode/mentorship/fixtures/dev_data.json @@ -1,102 +1,99 @@ [ - { - "model": "mentorship.mentorshipservice", - "pk": 1, - "fields": { - "slug": "geekpal", - "name": "GeekPal 1-1 Coding Mentorship", - "logo_url": null, - "description": "1-1 Mentoring technical session using the GeekPal service.", - "duration": "01:00:00", - "max_duration": "02:00:00", - "missed_meeting_duration": "00:10:00", - "status": "ACTIVE", - "language": "en", - "allow_mentee_to_extend": true, - "allow_mentors_to_extend": true, - "academy": 1, - "created_at": "2022-10-13T14:20:47.106Z", - "updated_at": "2022-10-13T14:20:47.106Z" - } - }, - { - "model": "mentorship.mentorprofile", - "pk": 1, - "fields": { - "name": null, - "slug": "Joe", - "price_per_hour": 8.0, - "one_line_bio": "", - "bio": "", - "academy": 1, - "timezone": "America/New_York", - "online_meeting_url": "https://whereby.com/joedoe", - "token": "cad0c7787bfb147994590b449a2edb19720fbfb4", - "booking_url": "https://calendly.com/joedoe/4geeks", - "status": "ACTIVE", - "email": "joedoe@gmail.com", - "user": 7, - "created_at": "2022-10-13T14:22:18.656Z", - "updated_at": "2022-10-13T14:22:18.656Z", - "rating": null, - "services": [ - 1 - ], - "syllabus": [ - 1 - ] - } - }, - { - "model": "mentorship.mentorshipbill", - "pk": 1, - "fields": { - "status": "DUE", - "status_mesage": "", - "total_duration_in_minutes": 0.0, - "total_duration_in_hours": 0.0, - "total_price": 0.0, - "overtime_minutes": 0.0, - "academy": 1, - "started_at": "2022-10-13T14:24:11Z", - "ended_at": "2022-10-18T14:24:13Z", - "reviewer": 1, - "mentor": 1, - "paid_at": null, - "created_at": "2022-10-13T14:24:20.900Z", - "updated_at": "2022-10-13T14:24:20.900Z" - } - }, - { - "model": "mentorship.mentorshipsession", - "pk": 1, - "fields": { - "name": "kBxxBTTrgapJjZeB21B3", - "is_online": true, - "latitude": null, - "longitude": null, - "mentor": 1, - "service": 1, - "mentee": 8, - "online_meeting_url": "https://4geeks.daily.co/kBxxBTTrgapJjZeB21B3", - "online_recording_url": null, - "status": "COMPLETED", - "status_message": "", - "allow_billing": true, - "bill": null, - "suggested_accounted_duration": null, - "accounted_duration": null, - "agenda": "", - "summary": "It was a great meeting where I helped Mary with all her questions about javascript", - "starts_at": "2022-10-13T14:23:27Z", - "ends_at": "2022-10-13T15:23:28Z", - "started_at": "2022-10-13T14:23:32Z", - "ended_at": "2022-10-13T15:23:34Z", - "mentor_joined_at": "2022-10-13T14:23:38Z", - "mentor_left_at": "2022-10-13T15:23:40Z", - "mentee_left_at": "2022-10-13T15:23:43Z", - "created_at": "2022-10-13T14:23:45.719Z", - "updated_at": "2022-10-13T14:23:45.719Z" - } + { + "model": "mentorship.mentorshipservice", + "pk": 1, + "fields": { + "slug": "geekpal", + "name": "GeekPal 1-1 Coding Mentorship", + "logo_url": null, + "description": "1-1 Mentoring technical session using the GeekPal service.", + "duration": "01:00:00", + "max_duration": "02:00:00", + "missed_meeting_duration": "00:10:00", + "video_provider": "DAILY", + "status": "ACTIVE", + "language": "en", + "allow_mentee_to_extend": true, + "allow_mentors_to_extend": true, + "academy": 1, + "created_at": "2022-10-13T14:20:47.106Z", + "updated_at": "2022-10-13T14:20:47.106Z" } + }, + { + "model": "mentorship.mentorprofile", + "pk": 1, + "fields": { + "name": null, + "slug": "Joe", + "price_per_hour": 8.0, + "one_line_bio": "", + "bio": "", + "academy": 1, + "timezone": "America/New_York", + "online_meeting_url": "https://whereby.com/joedoe", + "token": "cad0c7787bfb147994590b449a2edb19720fbfb4", + "booking_url": "https://calendly.com/joedoe/4geeks", + "status": "ACTIVE", + "email": "joedoe@gmail.com", + "user": 7, + "created_at": "2022-10-13T14:22:18.656Z", + "updated_at": "2022-10-13T14:22:18.656Z", + "rating": null, + "services": [1], + "syllabus": [1] + } + }, + { + "model": "mentorship.mentorshipbill", + "pk": 1, + "fields": { + "status": "DUE", + "status_mesage": "", + "total_duration_in_minutes": 0.0, + "total_duration_in_hours": 0.0, + "total_price": 0.0, + "overtime_minutes": 0.0, + "academy": 1, + "started_at": "2022-10-13T14:24:11Z", + "ended_at": "2022-10-18T14:24:13Z", + "reviewer": 1, + "mentor": 1, + "paid_at": null, + "created_at": "2022-10-13T14:24:20.900Z", + "updated_at": "2022-10-13T14:24:20.900Z" + } + }, + { + "model": "mentorship.mentorshipsession", + "pk": 1, + "fields": { + "name": "kBxxBTTrgapJjZeB21B3", + "is_online": true, + "latitude": null, + "longitude": null, + "mentor": 1, + "service": 1, + "mentee": 8, + "online_meeting_url": "https://4geeks.daily.co/kBxxBTTrgapJjZeB21B3", + "online_recording_url": null, + "status": "COMPLETED", + "status_message": "", + "allow_billing": true, + "bill": null, + "suggested_accounted_duration": null, + "accounted_duration": null, + "agenda": "", + "summary": "It was a great meeting where I helped Mary with all her questions about javascript", + "starts_at": "2022-10-13T14:23:27Z", + "ends_at": "2022-10-13T15:23:28Z", + "started_at": "2022-10-13T14:23:32Z", + "ended_at": "2022-10-13T15:23:34Z", + "mentor_joined_at": "2022-10-13T14:23:38Z", + "mentor_left_at": "2022-10-13T15:23:40Z", + "mentee_left_at": "2022-10-13T15:23:43Z", + "created_at": "2022-10-13T14:23:45.719Z", + "updated_at": "2022-10-13T14:23:45.719Z" + } + } ] diff --git a/breathecode/payments/admin.py b/breathecode/payments/admin.py index 45631bdb4..497bb2956 100644 --- a/breathecode/payments/admin.py +++ b/breathecode/payments/admin.py @@ -39,6 +39,7 @@ ServiceTranslation, Subscription, SubscriptionServiceItem, + PaymentMethod, ) # Register your models here. @@ -370,3 +371,11 @@ class CouponAdmin(admin.ModelAdmin): 'seller__user__last_name' ] raw_id_fields = ['seller'] + + +@admin.register(PaymentMethod) +class PaymentMethodAdmin(admin.ModelAdmin): + list_display = ('title', 'description', 'academy', 'third_party_link') + list_filter = ['academy__name'] + raw_id_fields = ['academy'] + search_fields = ['title', 'academy__name'] diff --git a/breathecode/payments/migrations/0047_paymentmethod.py b/breathecode/payments/migrations/0047_paymentmethod.py new file mode 100644 index 000000000..fb384dc51 --- /dev/null +++ b/breathecode/payments/migrations/0047_paymentmethod.py @@ -0,0 +1,32 @@ +# Generated by Django 5.0.6 on 2024-06-24 17:58 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('admissions', '0064_academy_legal_name'), + ('payments', '0046_consumptionsession_operation_code_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='PaymentMethod', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=120)), + ('description', models.CharField(help_text='Description of the payment method', max_length=255)), + ('third_party_link', + models.URLField(blank=True, default=None, help_text='Link of a third party payment method', + null=True)), + ('academy', + models.ForeignKey(blank=True, + help_text='Academy owner', + null=True, + on_delete=django.db.models.deletion.CASCADE, + to='admissions.academy')), + ], + ), + ] diff --git a/breathecode/payments/models.py b/breathecode/payments/models.py index cb754ff3d..05632cb56 100644 --- a/breathecode/payments/models.py +++ b/breathecode/payments/models.py @@ -1648,3 +1648,16 @@ def get_reputation(self): def __str__(self) -> str: return f'{self.user.email} -> {self.get_reputation()}' + + +class PaymentMethod(models.Model): + """ + Different payment methods of each academy have. + """ + academy = models.ForeignKey(Academy, on_delete=models.CASCADE, blank=True, null=True, help_text='Academy owner') + title = models.CharField(max_length=120, null=False, blank=False) + description = models.CharField(max_length=255, help_text='Description of the payment method') + third_party_link = models.URLField(blank=True, + null=True, + default=None, + help_text='Link of a third party payment method') diff --git a/breathecode/payments/serializers.py b/breathecode/payments/serializers.py index 72c17402a..0b0ee2e32 100644 --- a/breathecode/payments/serializers.py +++ b/breathecode/payments/serializers.py @@ -390,6 +390,7 @@ class GetSubscriptionHookSerializer(GetAbstractIOweYouSerializer): pay_every = serpy.Field() pay_every_unit = serpy.Field() + class GetSubscriptionSerializer(GetAbstractIOweYouSerializer): paid_at = serpy.Field() is_refundable = serpy.Field() @@ -402,6 +403,7 @@ class GetSubscriptionSerializer(GetAbstractIOweYouSerializer): def get_service_items(self, obj): return GetServiceItemSerializer(obj.service_items.filter(), many=True).data + class GetBagSerializer(serpy.Serializer): id = serpy.Field() service_items = serpy.MethodField() @@ -489,3 +491,11 @@ def update(self, instance, validated_data): instance.save() return instance + + +class GetPaymentMethod(serpy.Serializer): + id = serpy.Field() + title = serpy.Field() + description = serpy.Field() + third_party_link = serpy.Field() + academy = GetAcademySmallSerializer(required=False, many=False) diff --git a/breathecode/payments/urls.py b/breathecode/payments/urls.py index 55b439592..2bfb2ffc9 100644 --- a/breathecode/payments/urls.py +++ b/breathecode/payments/urls.py @@ -27,6 +27,7 @@ PlanView, ServiceItemView, ServiceView, + PaymentMethodView, ) app_name = 'payments' @@ -77,4 +78,5 @@ path('bag//coupon', BagCouponView.as_view(), name='bag_id_coupon'), path('checking', CheckingView.as_view(), name='checking'), path('pay', PayView.as_view(), name='pay'), + path('methods', PaymentMethodView.as_view(), name='methods'), ] diff --git a/breathecode/payments/views.py b/breathecode/payments/views.py index 73ce713f4..d4e903d5f 100644 --- a/breathecode/payments/views.py +++ b/breathecode/payments/views.py @@ -48,6 +48,7 @@ ServiceItem, ServiceSet, Subscription, + PaymentMethod, ) from breathecode.payments.serializers import ( GetAcademyServiceSmallSerializer, @@ -69,6 +70,7 @@ POSTAcademyServiceSerializer, PUTAcademyServiceSerializer, ServiceSerializer, + GetPaymentMethod, ) from breathecode.payments.services.stripe import Stripe from breathecode.payments.signals import reimburse_service_units @@ -1858,3 +1860,21 @@ def post(self, request): except Exception as e: transaction.savepoint_rollback(sid) raise e + + +class PaymentMethodView(APIView): + + def get(self, request): + lang = get_user_language(request) + + items = PaymentMethod.objects.all() + lookup = {} + + if 'academy_id' in self.request.GET: + academy_id = self.request.GET.get('academy_id') + lookup['academy__id__iexact'] = academy_id + + items = items.filter(**lookup) + + serializer = GetPaymentMethod(items, many=True) + return Response(serializer.data, status=200) From 22fc0556f34459edb17e61665d98023347edd901 Mon Sep 17 00:00:00 2001 From: Tomas Gonzalez Date: Tue, 25 Jun 2024 16:43:10 +0000 Subject: [PATCH 34/35] Lang added to PaymentMethod --- breathecode/payments/admin.py | 4 ++-- breathecode/payments/migrations/0047_paymentmethod.py | 7 ++++++- breathecode/payments/models.py | 3 +++ breathecode/payments/serializers.py | 1 + 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/breathecode/payments/admin.py b/breathecode/payments/admin.py index 497bb2956..90bac7a18 100644 --- a/breathecode/payments/admin.py +++ b/breathecode/payments/admin.py @@ -375,7 +375,7 @@ class CouponAdmin(admin.ModelAdmin): @admin.register(PaymentMethod) class PaymentMethodAdmin(admin.ModelAdmin): - list_display = ('title', 'description', 'academy', 'third_party_link') - list_filter = ['academy__name'] + list_display = ('title', 'description', 'academy', 'third_party_link', 'lang') + list_filter = ['academy__name', 'lang'] raw_id_fields = ['academy'] search_fields = ['title', 'academy__name'] diff --git a/breathecode/payments/migrations/0047_paymentmethod.py b/breathecode/payments/migrations/0047_paymentmethod.py index fb384dc51..dedb35145 100644 --- a/breathecode/payments/migrations/0047_paymentmethod.py +++ b/breathecode/payments/migrations/0047_paymentmethod.py @@ -1,5 +1,6 @@ -# Generated by Django 5.0.6 on 2024-06-24 17:58 +# Generated by Django 5.0.6 on 2024-06-25 16:38 +import breathecode.utils.validators.language import django.db.models.deletion from django.db import migrations, models @@ -21,6 +22,10 @@ class Migration(migrations.Migration): ('third_party_link', models.URLField(blank=True, default=None, help_text='Link of a third party payment method', null=True)), + ('lang', + models.CharField(help_text='ISO 639-1 language code + ISO 3166-1 alpha-2 country code, e.g. en-US', + max_length=5, + validators=[breathecode.utils.validators.language.validate_language_code])), ('academy', models.ForeignKey(blank=True, help_text='Academy owner', diff --git a/breathecode/payments/models.py b/breathecode/payments/models.py index 05632cb56..405f1bca5 100644 --- a/breathecode/payments/models.py +++ b/breathecode/payments/models.py @@ -1661,3 +1661,6 @@ class PaymentMethod(models.Model): null=True, default=None, help_text='Link of a third party payment method') + lang = models.CharField(max_length=5, + validators=[validate_language_code], + help_text='ISO 639-1 language code + ISO 3166-1 alpha-2 country code, e.g. en-US') diff --git a/breathecode/payments/serializers.py b/breathecode/payments/serializers.py index 0b0ee2e32..5072f9ba8 100644 --- a/breathecode/payments/serializers.py +++ b/breathecode/payments/serializers.py @@ -496,6 +496,7 @@ def update(self, instance, validated_data): class GetPaymentMethod(serpy.Serializer): id = serpy.Field() title = serpy.Field() + lang = serpy.Field() description = serpy.Field() third_party_link = serpy.Field() academy = GetAcademySmallSerializer(required=False, many=False) From 25b35ee0a51aaf49896fc6da523eaa2b3a2f6d1c Mon Sep 17 00:00:00 2001 From: Tomas Gonzalez Date: Tue, 25 Jun 2024 16:47:35 +0000 Subject: [PATCH 35/35] Lang added to PaymentMethod --- breathecode/payments/views.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/breathecode/payments/views.py b/breathecode/payments/views.py index d4e903d5f..99380dfa8 100644 --- a/breathecode/payments/views.py +++ b/breathecode/payments/views.py @@ -1874,6 +1874,10 @@ def get(self, request): academy_id = self.request.GET.get('academy_id') lookup['academy__id__iexact'] = academy_id + if 'lang' in self.request.GET: + lang = self.request.GET.get('lang') + lookup['lang__iexact'] = lang + items = items.filter(**lookup) serializer = GetPaymentMethod(items, many=True)