From 40d12cb142fa538daf1fee27ad13372cf97055bc Mon Sep 17 00:00:00 2001 From: jefer94 Date: Sat, 16 Dec 2023 01:45:39 -0500 Subject: [PATCH 1/5] can monetize microservices --- .../assignments/permissions/__init__.py | 0 .../assignments/permissions/consumers.py | 11 + .../assignments/permissions/contexts.py | 0 breathecode/assignments/permissions/flags.py | 8 + .../urls/tests_me_task_id_coderevision.py | 550 ++++++------ breathecode/assignments/views.py | 3 + .../commands/tests_set_permissions.py | 4 +- .../tests/urls/tests_authorize_slug.py | 2 +- breathecode/certificate/tasks.py | 14 +- .../tests/tasks/tests_remove_screenshot.py | 87 +- breathecode/commons/tasks.py | 9 +- .../tests_build_live_classes_from_timeslot.py | 2 +- breathecode/events/views.py | 39 +- breathecode/marketing/actions.py | 4 +- breathecode/marketing/models.py | 5 +- .../marketing/tests/urls/tests_lead.py | 359 ++++---- .../tests_meet_slug_service_slug.py | 1 + breathecode/payments/models.py | 166 ++-- .../tasks/tests_refund_mentoring_session.py | 798 +++++++++--------- .../tests/tasks/tests_renew_consumables.py | 1 + .../tests/urls/tests_consumable_checkout.py | 28 +- .../provisioning/tests/tasks/tests_upload.py | 26 +- .../tests_async_create_asset_thumbnail.py | 2 +- .../tests/urls/tests_academy_asset.py | 47 +- .../payments_models_mixin.py | 12 + conftest.py | 47 +- 26 files changed, 1171 insertions(+), 1054 deletions(-) create mode 100644 breathecode/assignments/permissions/__init__.py create mode 100644 breathecode/assignments/permissions/consumers.py create mode 100644 breathecode/assignments/permissions/contexts.py create mode 100644 breathecode/assignments/permissions/flags.py diff --git a/breathecode/assignments/permissions/__init__.py b/breathecode/assignments/permissions/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/breathecode/assignments/permissions/consumers.py b/breathecode/assignments/permissions/consumers.py new file mode 100644 index 000000000..bc5417dcc --- /dev/null +++ b/breathecode/assignments/permissions/consumers.py @@ -0,0 +1,11 @@ +import logging +from breathecode.utils.decorators import PermissionContextType + +logger = logging.getLogger(__name__) + + +def code_revision_service(context: PermissionContextType, args: tuple, + kwargs: dict) -> tuple[dict, tuple, dict]: + + context['consumables'] = context['consumables'].filter(app_service__service='code_revision') + return (context, args, kwargs) diff --git a/breathecode/assignments/permissions/contexts.py b/breathecode/assignments/permissions/contexts.py new file mode 100644 index 000000000..e69de29bb diff --git a/breathecode/assignments/permissions/flags.py b/breathecode/assignments/permissions/flags.py new file mode 100644 index 000000000..b9e2626a9 --- /dev/null +++ b/breathecode/assignments/permissions/flags.py @@ -0,0 +1,8 @@ +__all__ = ['api'] + + +class API: + ... + + +api = API() diff --git a/breathecode/assignments/tests/urls/tests_me_task_id_coderevision.py b/breathecode/assignments/tests/urls/tests_me_task_id_coderevision.py index ffafeb86b..5ab1e8175 100644 --- a/breathecode/assignments/tests/urls/tests_me_task_id_coderevision.py +++ b/breathecode/assignments/tests/urls/tests_me_task_id_coderevision.py @@ -4,249 +4,319 @@ import json import random from unittest.mock import MagicMock, call, patch +from rest_framework.test import APIClient from django.urls.base import reverse_lazy +import pytest from rest_framework import status +from breathecode.tests.mixins.breathecode_mixin.breathecode import Breathecode from breathecode.utils.service import Service -from ..mixins import AssignmentsTestCase - - -class MediaTestSuite(AssignmentsTestCase): - - # When: no auth - # Then: response 401 - def test_no_auth(self): - url = reverse_lazy('assignments:me_task_id_coderevision', kwargs={'task_id': 1}) - response = self.client.get(url) - - json = response.json() - expected = {'detail': 'Authentication credentials were not provided.', 'status_code': 401} - - self.assertEqual(json, expected) - self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - self.assertEqual(self.bc.database.list_of('assignments.Task'), []) - - # When: no tasks - # Then: response 404 - def test__get__no_tasks(self): - expected = {'data': {'getTask': {'id': random.randint(1, 100)}}} - query = { - self.bc.fake.slug(): self.bc.fake.slug(), - self.bc.fake.slug(): self.bc.fake.slug(), - self.bc.fake.slug(): self.bc.fake.slug(), - } - - mock = MagicMock() - mock.raw = iter([json.dumps(expected).encode()]) - mock.headers = {'Content-Type': 'application/json'} - code = random.randint(200, 299) - mock.status_code = code - mock.reason = 'OK' - - model = self.bc.database.create(profile_academy=1) - self.bc.request.authenticate(model.user) - - url = reverse_lazy('assignments:me_task_id_coderevision', - kwargs={'task_id': 1}) + '?' + self.bc.format.querystring(query) - - with patch.multiple('breathecode.utils.service.Service', - __init__=MagicMock(return_value=None), - get=MagicMock(return_value=mock)): - response = self.client.get(url) - self.bc.check.calls(Service.get.call_args_list, []) - - self.assertEqual(response.getvalue().decode('utf-8'), '{"detail":"task-not-found","status_code":404}') - self.assertEqual(response.status_code, 404) - self.assertEqual(self.bc.database.list_of('assignments.Task'), []) - - # When: no github accounts - # Then: response 200 - def test__get__no_github_accounts(self): - expected = {'data': {'getTask': {'id': random.randint(1, 100)}}} - query = { - self.bc.fake.slug(): self.bc.fake.slug(), - self.bc.fake.slug(): self.bc.fake.slug(), - self.bc.fake.slug(): self.bc.fake.slug(), - } - - mock = MagicMock() - mock.raw = iter([json.dumps(expected).encode()]) - mock.headers = {'Content-Type': 'application/json'} - code = random.randint(200, 299) - mock.status_code = code - mock.reason = 'OK' - - task = {'github_url': self.bc.fake.url()} - model = self.bc.database.create(profile_academy=1, task=task) - self.bc.request.authenticate(model.user) - - url = reverse_lazy('assignments:me_task_id_coderevision', - kwargs={'task_id': 1}) + '?' + self.bc.format.querystring(query) - - with patch.multiple('breathecode.utils.service.Service', - __init__=MagicMock(return_value=None), - get=MagicMock(return_value=mock)): - response = self.client.get(url) - self.bc.check.calls(Service.get.call_args_list, []) - - self.assertEqual(response.getvalue().decode('utf-8'), - '{"detail":"github-account-not-connected","status_code":400}') - self.assertEqual(response.status_code, 400) - self.assertEqual(self.bc.database.list_of('assignments.Task'), [self.bc.format.to_dict(model.task)]) - - # When: auth - # Then: response 200 - def test__get__auth(self): - expected = {'data': {'getTask': {'id': random.randint(1, 100)}}} - query = { - self.bc.fake.slug(): self.bc.fake.slug(), - self.bc.fake.slug(): self.bc.fake.slug(), - self.bc.fake.slug(): self.bc.fake.slug(), - } - - mock = MagicMock() - mock.raw = iter([json.dumps(expected).encode()]) - mock.headers = {'Content-Type': 'application/json'} - code = random.randint(200, 299) - mock.status_code = code - mock.reason = 'OK' - - task = {'github_url': self.bc.fake.url()} - credentials_github = {'username': self.bc.fake.slug()} - model = self.bc.database.create(profile_academy=1, task=task, credentials_github=credentials_github) - self.bc.request.authenticate(model.user) - - url = reverse_lazy('assignments:me_task_id_coderevision', - kwargs={'task_id': 1}) + '?' + self.bc.format.querystring(query) - - with patch.multiple('breathecode.utils.service.Service', - __init__=MagicMock(return_value=None), - get=MagicMock(return_value=mock)): - response = self.client.get(url) - self.bc.check.calls(Service.get.call_args_list, [ - call('/v1/finetuning/me/coderevision', - params={ - **query, - 'repo': model.task.github_url, - 'github_username': model.credentials_github.username, - }, - stream=True), - ]) - - self.assertEqual(response.getvalue().decode('utf-8'), json.dumps(expected)) - self.assertEqual(response.status_code, code) - self.assertEqual(self.bc.database.list_of('assignments.Task'), [self.bc.format.to_dict(model.task)]) - - # When: no tasks - # Then: response 404 - def test__post__no_tasks(self): - expected = {'data': {'getTask': {'id': random.randint(1, 100)}}} - query = { - self.bc.fake.slug(): self.bc.fake.slug(), - self.bc.fake.slug(): self.bc.fake.slug(), - self.bc.fake.slug(): self.bc.fake.slug(), - } - - mock = MagicMock() - mock.raw = iter([json.dumps(expected).encode()]) - mock.headers = {'Content-Type': 'application/json'} - code = random.randint(200, 299) - mock.status_code = code - mock.reason = 'OK' - - model = self.bc.database.create(profile_academy=1) - self.bc.request.authenticate(model.user) - - url = reverse_lazy('assignments:me_task_id_coderevision', - kwargs={'task_id': 1}) + '?' + self.bc.format.querystring(query) - - with patch.multiple('breathecode.utils.service.Service', - __init__=MagicMock(return_value=None), - post=MagicMock(return_value=mock)): - response = self.client.post(url) - self.bc.check.calls(Service.post.call_args_list, []) - - self.assertEqual(response.getvalue().decode('utf-8'), '{"detail":"task-not-found","status_code":404}') - self.assertEqual(response.status_code, 404) - self.assertEqual(self.bc.database.list_of('assignments.Task'), []) - - # When: no github accounts - # Then: response 200 - def test__post__no_github_accounts(self): - expected = {'data': {'getTask': {'id': random.randint(1, 100)}}} - query = { - self.bc.fake.slug(): self.bc.fake.slug(), - self.bc.fake.slug(): self.bc.fake.slug(), - self.bc.fake.slug(): self.bc.fake.slug(), - } - - mock = MagicMock() - mock.raw = iter([json.dumps(expected).encode()]) - mock.headers = {'Content-Type': 'application/json'} - code = random.randint(200, 299) - mock.status_code = code - mock.reason = 'OK' - - task = {'github_url': self.bc.fake.url()} - model = self.bc.database.create(profile_academy=1, task=task) - self.bc.request.authenticate(model.user) - - url = reverse_lazy('assignments:me_task_id_coderevision', - kwargs={'task_id': 1}) + '?' + self.bc.format.querystring(query) - - with patch.multiple('breathecode.utils.service.Service', - __init__=MagicMock(return_value=None), - post=MagicMock(return_value=mock)): - response = self.client.post(url) - self.bc.check.calls(Service.post.call_args_list, []) - - self.assertEqual(response.getvalue().decode('utf-8'), - '{"detail":"github-account-not-connected","status_code":400}') - self.assertEqual(response.status_code, 400) - self.assertEqual(self.bc.database.list_of('assignments.Task'), [self.bc.format.to_dict(model.task)]) - - # When: auth - # Then: response 200 - def test__post__auth(self): - expected = {'data': {'getTask': {'id': random.randint(1, 100)}}} - query = { - self.bc.fake.slug(): self.bc.fake.slug(), - self.bc.fake.slug(): self.bc.fake.slug(), - self.bc.fake.slug(): self.bc.fake.slug(), - } - - mock = MagicMock() - mock.raw = iter([json.dumps(expected).encode()]) - mock.headers = {'Content-Type': 'application/json'} - code = random.randint(200, 299) - mock.status_code = code - mock.reason = 'OK' - - task = {'github_url': self.bc.fake.url()} - credentials_github = {'username': self.bc.fake.slug()} - model = self.bc.database.create(profile_academy=1, task=task, credentials_github=credentials_github) - self.bc.request.authenticate(model.user) - - url = reverse_lazy('assignments:me_task_id_coderevision', - kwargs={'task_id': 1}) + '?' + self.bc.format.querystring(query) - - with patch.multiple('breathecode.utils.service.Service', - __init__=MagicMock(return_value=None), - post=MagicMock(return_value=mock)): - response = self.client.post(url, query, format='json') - self.bc.check.calls(Service.post.call_args_list, [ - call('/v1/finetuning/coderevision/', - data=query, - params={ - **query, - 'repo': model.task.github_url, - 'github_username': model.credentials_github.username, - }, - stream=True), - ]) - - self.assertEqual(response.getvalue().decode('utf-8'), json.dumps(expected)) - self.assertEqual(response.status_code, code) - self.assertEqual(self.bc.database.list_of('assignments.Task'), [self.bc.format.to_dict(model.task)]) + +@pytest.fixture(autouse=True) +def setup(db): + # setup logic + yield + # teardown logic + + +# When: no auth +# Then: response 401 +def test_no_auth(bc: Breathecode, client: APIClient): + url = reverse_lazy('assignments:me_task_id_coderevision', kwargs={'task_id': 1}) + response = client.get(url) + + json = response.json() + expected = {'detail': 'Authentication credentials were not provided.', 'status_code': 401} + + assert json == expected + assert response.status_code == status.HTTP_401_UNAUTHORIZED + assert bc.database.list_of('assignments.Task') == [] + + +# When: no tasks +# Then: response 404 +def test__get__no_tasks(bc: Breathecode, client: APIClient): + expected = {'data': {'getTask': {'id': random.randint(1, 100)}}} + query = { + bc.fake.slug(): bc.fake.slug(), + bc.fake.slug(): bc.fake.slug(), + bc.fake.slug(): bc.fake.slug(), + } + + mock = MagicMock() + mock.raw = iter([json.dumps(expected).encode()]) + mock.headers = {'Content-Type': 'application/json'} + code = random.randint(200, 299) + mock.status_code = code + mock.reason = 'OK' + + model = bc.database.create(profile_academy=1) + client.force_authenticate(model.user) + + url = reverse_lazy('assignments:me_task_id_coderevision', kwargs={'task_id': 1 + }) + '?' + bc.format.querystring(query) + + with patch.multiple('breathecode.utils.service.Service', + __init__=MagicMock(return_value=None), + get=MagicMock(return_value=mock)): + response = client.get(url) + bc.check.calls(Service.get.call_args_list, []) + + assert response.getvalue().decode('utf-8') == '{"detail":"task-not-found","status_code":404}' + assert response.status_code == 404 + assert bc.database.list_of('assignments.Task') == [] + + +# When: no github accounts +# Then: response 200 +def test__get__no_github_accounts(bc: Breathecode, client: APIClient): + expected = {'data': {'getTask': {'id': random.randint(1, 100)}}} + query = { + bc.fake.slug(): bc.fake.slug(), + bc.fake.slug(): bc.fake.slug(), + bc.fake.slug(): bc.fake.slug(), + } + + mock = MagicMock() + mock.raw = iter([json.dumps(expected).encode()]) + mock.headers = {'Content-Type': 'application/json'} + code = random.randint(200, 299) + mock.status_code = code + mock.reason = 'OK' + + task = {'github_url': bc.fake.url()} + model = bc.database.create(profile_academy=1, task=task) + client.force_authenticate(model.user) + + url = reverse_lazy('assignments:me_task_id_coderevision', kwargs={'task_id': 1 + }) + '?' + bc.format.querystring(query) + + with patch.multiple('breathecode.utils.service.Service', + __init__=MagicMock(return_value=None), + get=MagicMock(return_value=mock)): + response = client.get(url) + bc.check.calls(Service.get.call_args_list, []) + + assert response.getvalue().decode( + 'utf-8') == '{"detail":"github-account-not-connected","status_code":400}' + assert response.status_code == 400 + assert bc.database.list_of('assignments.Task') == [bc.format.to_dict(model.task)] + + +# When: auth +# Then: response 200 +def test__get__auth(bc: Breathecode, client: APIClient): + expected = {'data': {'getTask': {'id': random.randint(1, 100)}}} + query = { + bc.fake.slug(): bc.fake.slug(), + bc.fake.slug(): bc.fake.slug(), + bc.fake.slug(): bc.fake.slug(), + } + + mock = MagicMock() + mock.raw = iter([json.dumps(expected).encode()]) + mock.headers = {'Content-Type': 'application/json'} + code = random.randint(200, 299) + mock.status_code = code + mock.reason = 'OK' + + task = {'github_url': bc.fake.url()} + credentials_github = {'username': bc.fake.slug()} + model = bc.database.create(profile_academy=1, task=task, credentials_github=credentials_github) + client.force_authenticate(model.user) + + url = reverse_lazy('assignments:me_task_id_coderevision', kwargs={'task_id': 1 + }) + '?' + bc.format.querystring(query) + + with patch.multiple('breathecode.utils.service.Service', + __init__=MagicMock(return_value=None), + get=MagicMock(return_value=mock)): + response = client.get(url) + bc.check.calls(Service.get.call_args_list, [ + call('/v1/finetuning/me/coderevision', + params={ + **query, + 'repo': model.task.github_url, + 'github_username': model.credentials_github.username, + }, + stream=True), + ]) + + assert response.getvalue().decode('utf-8') == json.dumps(expected) + assert response.status_code == code + assert bc.database.list_of('assignments.Task') == [bc.format.to_dict(model.task)] + + +# When: no tasks +# Then: response 404 +def test__post__no_consumables(bc: Breathecode, client: APIClient): + expected = {'data': {'getTask': {'id': random.randint(1, 100)}}} + query = { + bc.fake.slug(): bc.fake.slug(), + bc.fake.slug(): bc.fake.slug(), + bc.fake.slug(): bc.fake.slug(), + } + + mock = MagicMock() + mock.raw = iter([json.dumps(expected).encode()]) + mock.headers = {'Content-Type': 'application/json'} + code = random.randint(200, 299) + mock.status_code = code + mock.reason = 'OK' + + model = bc.database.create(profile_academy=1) + client.force_authenticate(model.user) + + url = reverse_lazy('assignments:me_task_id_coderevision', kwargs={'task_id': 1 + }) + '?' + bc.format.querystring(query) + + with patch.multiple('breathecode.utils.service.Service', + __init__=MagicMock(return_value=None), + post=MagicMock(return_value=mock)): + response = client.post(url) + bc.check.calls(Service.post.call_args_list, []) + + assert response.getvalue().decode('utf-8') == '{"detail":"not-enough-consumables","status_code":402}' + assert response.status_code == 402 + assert bc.database.list_of('assignments.Task') == [] + + +# When: no tasks +# Then: response 404 +def test__post__no_tasks(bc: Breathecode, client: APIClient): + expected = {'data': {'getTask': {'id': random.randint(1, 100)}}} + query = { + bc.fake.slug(): bc.fake.slug(), + bc.fake.slug(): bc.fake.slug(), + bc.fake.slug(): bc.fake.slug(), + } + + mock = MagicMock() + mock.raw = iter([json.dumps(expected).encode()]) + mock.headers = {'Content-Type': 'application/json'} + code = random.randint(200, 299) + mock.status_code = code + mock.reason = 'OK' + + permission = {'codename': 'get_code_review'} + app_service = {'service': 'code_revision'} + model = bc.database.create(profile_academy=1, + permission=permission, + group=1, + consumable=1, + service=1, + app_service=app_service) + client.force_authenticate(model.user) + + url = reverse_lazy('assignments:me_task_id_coderevision', kwargs={'task_id': 1 + }) + '?' + bc.format.querystring(query) + + with patch.multiple('breathecode.utils.service.Service', + __init__=MagicMock(return_value=None), + post=MagicMock(return_value=mock)): + response = client.post(url) + bc.check.calls(Service.post.call_args_list, []) + + assert response.getvalue().decode('utf-8') == '{"detail":"task-not-found","status_code":404}' + assert response.status_code == 404 + assert bc.database.list_of('assignments.Task') == [] + + +# When: no github accounts +# Then: response 200 +def test__post__no_github_accounts(bc: Breathecode, client: APIClient): + expected = {'data': {'getTask': {'id': random.randint(1, 100)}}} + query = { + bc.fake.slug(): bc.fake.slug(), + bc.fake.slug(): bc.fake.slug(), + bc.fake.slug(): bc.fake.slug(), + } + + mock = MagicMock() + mock.raw = iter([json.dumps(expected).encode()]) + mock.headers = {'Content-Type': 'application/json'} + code = random.randint(200, 299) + mock.status_code = code + mock.reason = 'OK' + + task = {'github_url': bc.fake.url()} + permission = {'codename': 'get_code_review'} + app_service = {'service': 'code_revision'} + model = bc.database.create(profile_academy=1, + task=task, + permission=permission, + group=1, + consumable=1, + service=1, + app_service=app_service) + client.force_authenticate(model.user) + + url = reverse_lazy('assignments:me_task_id_coderevision', kwargs={'task_id': 1 + }) + '?' + bc.format.querystring(query) + + with patch.multiple('breathecode.utils.service.Service', + __init__=MagicMock(return_value=None), + post=MagicMock(return_value=mock)): + response = client.post(url) + bc.check.calls(Service.post.call_args_list, []) + + assert response.getvalue().decode( + 'utf-8') == '{"detail":"github-account-not-connected","status_code":400}' + assert response.status_code == 400 + assert bc.database.list_of('assignments.Task') == [bc.format.to_dict(model.task)] + + +# When: auth +# Then: response 200 +def test__post__auth(bc: Breathecode, client: APIClient): + expected = {'data': {'getTask': {'id': random.randint(1, 100)}}} + query = { + bc.fake.slug(): bc.fake.slug(), + bc.fake.slug(): bc.fake.slug(), + bc.fake.slug(): bc.fake.slug(), + } + + mock = MagicMock() + mock.raw = iter([json.dumps(expected).encode()]) + mock.headers = {'Content-Type': 'application/json'} + code = random.randint(200, 299) + mock.status_code = code + mock.reason = 'OK' + + task = {'github_url': bc.fake.url()} + credentials_github = {'username': bc.fake.slug()} + permission = {'codename': 'get_code_review'} + app_service = {'service': 'code_revision'} + model = bc.database.create(profile_academy=1, + task=task, + credentials_github=credentials_github, + permission=permission, + group=1, + consumable=1, + service=1, + app_service=app_service) + client.force_authenticate(model.user) + + url = reverse_lazy('assignments:me_task_id_coderevision', kwargs={'task_id': 1 + }) + '?' + bc.format.querystring(query) + + with patch.multiple('breathecode.utils.service.Service', + __init__=MagicMock(return_value=None), + post=MagicMock(return_value=mock)): + response = client.post(url, query, format='json') + bc.check.calls(Service.post.call_args_list, [ + call('/v1/finetuning/coderevision/', + data=query, + params={ + **query, + 'repo': model.task.github_url, + 'github_username': model.credentials_github.username, + }, + stream=True), + ]) + + assert response.getvalue().decode('utf-8') == json.dumps(expected) + assert response.status_code == code + assert bc.database.list_of('assignments.Task') == [bc.format.to_dict(model.task)] diff --git a/breathecode/assignments/views.py b/breathecode/assignments/views.py index de27f6d42..317695b71 100644 --- a/breathecode/assignments/views.py +++ b/breathecode/assignments/views.py @@ -1,4 +1,5 @@ from django.http import HttpResponseRedirect, StreamingHttpResponse +from breathecode.assignments.permissions.consumers import code_revision_service from breathecode.authenticate.actions import get_user_language from breathecode.authenticate.models import ProfileAcademy import logging, hashlib, os @@ -15,6 +16,7 @@ from rest_framework.decorators import api_view from rest_framework.response import Response from rest_framework import status +from breathecode.utils.decorators import has_permission from breathecode.utils.service import Service from .models import Task, FinalProject, UserAttachment from .actions import deliver_task @@ -905,6 +907,7 @@ def get(self, request, task_id=None): return resource + @has_permission('get_code_review', consumer=code_revision_service) def post(self, request, task_id): lang = get_user_language(request) params = {} diff --git a/breathecode/authenticate/tests/management/commands/tests_set_permissions.py b/breathecode/authenticate/tests/management/commands/tests_set_permissions.py index 5072d6e9e..3bebaed63 100644 --- a/breathecode/authenticate/tests/management/commands/tests_set_permissions.py +++ b/breathecode/authenticate/tests/management/commands/tests_set_permissions.py @@ -72,8 +72,8 @@ def setUp(self): # the behavior of permissions is not exact, this changes every time you add a model self.latest_content_type_id = content_type.id self.latest_permission_id = permission.id - self.job_content_type_id = self.latest_content_type_id - 45 - self.can_delete_job_permission_id = self.latest_permission_id - 181 + self.job_content_type_id = self.latest_content_type_id - 46 + self.can_delete_job_permission_id = self.latest_permission_id - 185 """ 🔽🔽🔽 format of PERMISSIONS diff --git a/breathecode/authenticate/tests/urls/tests_authorize_slug.py b/breathecode/authenticate/tests/urls/tests_authorize_slug.py index 74d6d0f16..4ece3bb0a 100644 --- a/breathecode/authenticate/tests/urls/tests_authorize_slug.py +++ b/breathecode/authenticate/tests/urls/tests_authorize_slug.py @@ -222,7 +222,7 @@ def test_app_require_an_agreement__with_scopes(self): new_scopes=[]) # dump error in external files - if content != expected or True: + if content != expected: with open('content.html', 'w') as f: f.write(content) diff --git a/breathecode/certificate/tasks.py b/breathecode/certificate/tasks.py index 697b33c3e..f2520d872 100644 --- a/breathecode/certificate/tasks.py +++ b/breathecode/certificate/tasks.py @@ -1,6 +1,7 @@ +from breathecode.certificate.models import UserSpecialty from breathecode.utils import getLogger from breathecode.admissions.models import CohortUser -from breathecode.utils.decorators.task import TaskPriority, task +from breathecode.utils.decorators.task import AbortTask, RetryTask, TaskPriority, task # Get an instance of a logger logger = getLogger(__name__) @@ -19,14 +20,15 @@ def take_screenshot(self, certificate_id, **_): def remove_screenshot(self, certificate_id, **_): from .actions import remove_certificate_screenshot - logger.debug('Starting remove_screenshot') + logger.info('Starting remove_screenshot') try: - remove_certificate_screenshot(certificate_id) - except Exception: - return False + res = remove_certificate_screenshot(certificate_id) + except UserSpecialty.DoesNotExist: + raise RetryTask(f'UserSpecialty {certificate_id} does not exist') - return True + if res is False: + raise AbortTask('UserSpecialty does not have any screenshot, it is skipped') @task(bind=True, priority=TaskPriority.CERTIFICATE.value) diff --git a/breathecode/certificate/tests/tasks/tests_remove_screenshot.py b/breathecode/certificate/tests/tasks/tests_remove_screenshot.py index dd6317a67..8ac87761c 100644 --- a/breathecode/certificate/tests/tasks/tests_remove_screenshot.py +++ b/breathecode/certificate/tests/tasks/tests_remove_screenshot.py @@ -1,30 +1,75 @@ -""" -Tasks tests -""" -from unittest.mock import MagicMock, patch, call +from unittest.mock import MagicMock, call + +import pytest + +from breathecode.tests.mixins.breathecode_mixin.breathecode import Breathecode from ...tasks import remove_screenshot -from ..mixins import CertificateTestCase -import breathecode.certificate.actions as actions -class ActionCertificateScreenshotTestCase(CertificateTestCase): - """Tests action remove_screenshot""" +@pytest.fixture(autouse=True) +def get_patch(db, monkeypatch): + + def wrapper(key, value): + if isinstance(value, Exception): + m4 = MagicMock(side_effect=value) + + else: + m4 = MagicMock(side_effect=lambda x: value if key == x else 1000) + + monkeypatch.setattr('breathecode.certificate.actions.remove_certificate_screenshot', m4) + + return m1, m2, m3, m4 + + m1 = MagicMock() + m2 = MagicMock() + m3 = MagicMock() + monkeypatch.setattr('logging.Logger.info', m1) + monkeypatch.setattr('logging.Logger.warning', m2) + monkeypatch.setattr('logging.Logger.error', m3) + + yield wrapper + + +def test_returns_true(bc: Breathecode, get_patch, get_int): + """remove_screenshot don't call open in development environment""" + + key = get_int() + info_mock, warn_mock, error_mock, action_mock = get_patch(key, True) + + remove_screenshot(key) + + assert info_mock.call_args_list == [call('Starting remove_screenshot')] + assert warn_mock.call_args_list == [] + assert error_mock.call_args_list == [] + assert action_mock.call_args_list == [call(key)] + + +def test_returns_false(bc: Breathecode, get_patch, get_int): + """remove_screenshot don't call open in development environment""" + + key = get_int() + info_mock, warn_mock, error_mock, action_mock = get_patch(key, False) + + remove_screenshot(key) - @patch('breathecode.certificate.actions.remove_certificate_screenshot', MagicMock()) - def test_remove_screenshot__call_all_properly(self): - """remove_screenshot don't call open in development environment""" + assert info_mock.call_args_list == [call('Starting remove_screenshot')] + assert warn_mock.call_args_list == [] + assert error_mock.call_args_list == [ + call('UserSpecialty does not have any screenshot, it is skipped', exc_info=True) + ] + assert action_mock.call_args_list == [call(key)] - result = remove_screenshot(1) - self.assertTrue(result) - self.assertEqual(actions.remove_certificate_screenshot.call_args_list, [call(1)]) +def test_returns_an_exception(bc: Breathecode, get_patch, get_int, fake): + """remove_screenshot don't call open in development environment""" - @patch('breathecode.certificate.actions.remove_certificate_screenshot', - MagicMock(side_effect=Exception())) - def test_remove_screenshot__remove_certificate_screenshot_raise_a_exception(self): - """remove_screenshot don't call open in development environment""" + key = get_int() + exc = fake.pystr() + info_mock, warn_mock, error_mock, action_mock = get_patch(key, Exception(exc)) - result = remove_screenshot(1) + remove_screenshot(key) - self.assertFalse(result) - self.assertEqual(actions.remove_certificate_screenshot.call_args_list, [call(1)]) + assert info_mock.call_args_list == [call('Starting remove_screenshot')] + assert warn_mock.call_args_list == [] + assert error_mock.call_args_list == [call(exc, exc_info=True)] + assert action_mock.call_args_list == [call(key)] diff --git a/breathecode/commons/tasks.py b/breathecode/commons/tasks.py index dfbe36bdb..e8ecd34e8 100644 --- a/breathecode/commons/tasks.py +++ b/breathecode/commons/tasks.py @@ -2,7 +2,7 @@ import logging from celery import shared_task from datetime import timedelta -from breathecode.commons.actions import is_output_enable +from breathecode.commons import actions from breathecode.commons.models import TaskManager from django.utils import timezone from breathecode.utils import CACHE_DESCRIPTORS @@ -169,14 +169,15 @@ def clean_task(self, key: str, task_manager_id: int): model_cls = getattr(module, model) if model_cls not in CACHE_DESCRIPTORS: - raise AbortTask(f'Cache not implemented for {model_cls.__name__}, skipping', log=is_output_enable()) + raise AbortTask(f'Cache not implemented for {model_cls.__name__}, skipping', + log=actions.is_output_enable()) cache = CACHE_DESCRIPTORS[model_cls] try: cache.clear() - if is_output_enable(): + if actions.is_output_enable(): logger.debug(f'Cache cleaned for {key}') except Exception: - raise RetryTask(f'Could not clean the cache {key}', log=is_output_enable()) + raise RetryTask(f'Could not clean the cache {key}', log=actions.is_output_enable()) diff --git a/breathecode/events/tests/tasks/tests_build_live_classes_from_timeslot.py b/breathecode/events/tests/tasks/tests_build_live_classes_from_timeslot.py index 5457f9ff1..2acb03f71 100644 --- a/breathecode/events/tests/tasks/tests_build_live_classes_from_timeslot.py +++ b/breathecode/events/tests/tasks/tests_build_live_classes_from_timeslot.py @@ -14,7 +14,7 @@ UTC_NOW = timezone.now() DATE = datetime(year=2022, month=12, day=30, hour=9, minute=24, second=0, microsecond=0, tzinfo=pytz.UTC) -URANDOM = os.urandom(20) +URANDOM = os.urandom(16) def live_class_item(data={}): diff --git a/breathecode/events/views.py b/breathecode/events/views.py index 0f30f91ef..41ec1ba17 100644 --- a/breathecode/events/views.py +++ b/breathecode/events/views.py @@ -108,9 +108,7 @@ def get_events(request): class EventPublicView(APIView): - """ - List all snippets, or create a new snippet. - """ + permission_classes = [AllowAny] def get(self, request, event_slug=None, format=None): @@ -131,9 +129,6 @@ def get(self, request, event_slug=None, format=None): class EventView(APIView): - """ - List all snippets, or create a new snippet. - """ def get(self, request, format=None): @@ -405,9 +400,7 @@ def get(self, request, hash, academy_id=None): class AcademyEventView(APIView, GenerateLookupsMixin): - """ - List all snippets, or create a new snippet. - """ + extensions = APIViewExtensions(cache=EventCache, sort='-starting_at', paginate=True) @capable_of('read_event') @@ -618,9 +611,6 @@ def get(self, request, event_id, academy_id=None): class EventTypeView(APIView): - """ - List all snippets, or create a new snippet. - """ def get(self, request, format=None): @@ -642,9 +632,6 @@ def get(self, request, format=None): class AcademyEventTypeView(APIView): - """ - List all snippets, or create a new snippet. - """ @capable_of('read_event_type') def get(self, request, academy_id=None, event_type_slug=None): @@ -695,9 +682,7 @@ def put(self, request, academy_id, event_type_slug=None): class EventTypeVisibilitySettingView(APIView): - """ - Show the visibility settings of a EventType. - """ + """Show the visibility settings of a EventType.""" extensions = APIViewExtensions(sort='-id') @@ -850,9 +835,6 @@ def get(self, request, event_id): class EventMeCheckinView(APIView): - """ - List all snippets, or create a new snippet. - """ def put(self, request, event_id): lang = get_user_language(request) @@ -922,9 +904,6 @@ def post(self, request, event_id): class AcademyEventCheckinView(APIView): - """ - List all snippets, or create a new snippet. - """ extensions = APIViewExtensions(sort='-created_at', paginate=True) @@ -985,9 +964,6 @@ def eventbrite_webhook(request, organization_id): class AcademyOrganizerView(APIView): - """ - List all snippets - """ @capable_of('read_organization') def get(self, request, academy_id=None): @@ -1002,9 +978,6 @@ def get(self, request, academy_id=None): # list venues class AcademyOrganizationOrganizerView(APIView): - """ - List all snippets - """ @capable_of('read_organization') def get(self, request, academy_id=None): @@ -1037,9 +1010,6 @@ def delete(self, request, academy_id=None, organizer_id=None): # list venues class AcademyOrganizationView(APIView): - """ - List all snippets - """ @capable_of('read_organization') def get(self, request, academy_id=None): @@ -1110,9 +1080,6 @@ def get(self, request, academy_id=None): # list venues class AcademyVenueView(APIView): - """ - List all snippets - """ @capable_of('read_event') def get(self, request, format=None, academy_id=None, user_id=None): diff --git a/breathecode/marketing/actions.py b/breathecode/marketing/actions.py index 448be74d7..5bb74210e 100644 --- a/breathecode/marketing/actions.py +++ b/breathecode/marketing/actions.py @@ -13,6 +13,7 @@ from breathecode.utils.validation_exception import ValidationException from breathecode.utils import getLogger import numpy as np +from django.db.models import Q logger = getLogger(__name__) @@ -270,7 +271,8 @@ def register_new_lead(form_entry=None): raise ValidationException('Missing location information') ac_academy = None - alias = AcademyAlias.objects.filter(active_campaign_slug=form_entry['location']).first() + alias = AcademyAlias.objects.filter( + Q(active_campaign_slug=form_entry['location']) | Q(academy__slug=form_entry['location'])).first() try: if alias is not None: diff --git a/breathecode/marketing/models.py b/breathecode/marketing/models.py index e9cac4069..e9c5a3917 100644 --- a/breathecode/marketing/models.py +++ b/breathecode/marketing/models.py @@ -73,6 +73,7 @@ class AcademyAlias(models.Model): applies to the academy it will look for matching alias to find the lead academy. """ + slug = models.SlugField(primary_key=True) active_campaign_slug = models.SlugField() academy = models.ForeignKey(Academy, on_delete=models.CASCADE) @@ -527,9 +528,7 @@ def is_duplicate(self, incoming_lead): return False def set_attribution_id(self): - """ - We'll keep the attribution id consistent as long as there is not sale made. - """ + """We'll keep the attribution id consistent as long as there is not sale made.""" if self.email is None: return None diff --git a/breathecode/marketing/tests/urls/tests_lead.py b/breathecode/marketing/tests/urls/tests_lead.py index 4eeeb3686..c9f20a66f 100644 --- a/breathecode/marketing/tests/urls/tests_lead.py +++ b/breathecode/marketing/tests/urls/tests_lead.py @@ -1,14 +1,19 @@ """ Test /academy/lead """ +from datetime import datetime from decimal import Decimal +import re import string from random import choice, choices, randint -from unittest.mock import PropertyMock, patch, MagicMock +from unittest.mock import MagicMock, PropertyMock from django.urls.base import reverse_lazy +import pytest from rest_framework import status from faker import Faker -from ..mixins import MarketingTestCase +from rest_framework.test import APIClient + +from breathecode.tests.mixins.breathecode_mixin.breathecode import Breathecode fake = Faker() @@ -143,17 +148,6 @@ def form_entry_field(data={}): } -class FakeRecaptcha: - - class RiskAnalysis: - - def __init__(self, *args, **kwargs): - self.score = 0.9 - - def __init__(self, *args, **kwargs): - self.risk_analysis = self.RiskAnalysis() - - def generate_form_entry_kwargs(data={}): """That random values is too long that i prefer have it in one function""" return { @@ -196,189 +190,186 @@ def generate_form_entry_kwargs(data={}): } -class LeadTestSuite(MarketingTestCase): - """ - 🔽🔽🔽 Passing nothing - """ +class FakeRecaptcha: + + class RiskAnalysis: + + def __init__(self, *args, **kwargs): + self.score = 0.9 + + def __init__(self, *args, **kwargs): + self.risk_analysis = self.RiskAnalysis() + + +def assertDatetime(date: datetime) -> bool: + if not isinstance(date, str): + assert isinstance(date, datetime) + return True + + try: + string = re.sub(r'Z$', '', date) + datetime.fromisoformat(string) + return True + except Exception: + assert 0 + + +@pytest.fixture(autouse=True) +def setup_db(db, monkeypatch): + monkeypatch.setattr('breathecode.services.google_cloud.Recaptcha.__init__', lambda: None) + monkeypatch.setattr('breathecode.services.google_cloud.Recaptcha.create_assessment', + MagicMock(return_value=FakeRecaptcha())) + monkeypatch.setattr('uuid.UUID.int', PropertyMock(return_value=1000)) + yield - @patch.multiple( - 'breathecode.services.google_cloud.Recaptcha', - __init__=MagicMock(return_value=None), - create_assessment=MagicMock(return_value=FakeRecaptcha()), - ) - @patch('breathecode.notify.utils.hook_manager.HookManagerClass.process_model_event', MagicMock()) - @patch('uuid.UUID.int', PropertyMock(return_value=1000)) - def test_lead__without_data(self): - url = reverse_lazy('marketing:lead') - response = self.client.post(url, format='json') - json = response.json() +# When: Passing nothing +def test_lead__without_data(bc: Breathecode, client: APIClient): + url = reverse_lazy('marketing:lead') - self.assertDatetime(json['created_at']) - self.assertDatetime(json['updated_at']) - del json['created_at'] - del json['updated_at'] + response = client.post(url, format='json') + json = response.json() - expected = post_serializer(data={ + assertDatetime(json['created_at']) + assertDatetime(json['updated_at']) + del json['created_at'] + del json['updated_at'] + + expected = post_serializer(data={ + 'attribution_id': None, + }) + + assert json == expected + assert response.status_code == status.HTTP_201_CREATED + + assert bc.database.list_of('marketing.FormEntry') == [ + form_entry_field({ + 'id': 1, + 'academy_id': None, + 'storage_status': 'ERROR', + 'storage_status_text': 'Missing location information', 'attribution_id': None, }) + ] - self.assertEqual(json, expected) - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - - self.assertEqual(self.bc.database.list_of('marketing.FormEntry'), [ - form_entry_field({ - 'id': 1, - 'academy_id': None, - 'storage_status': 'ERROR', - 'storage_status_text': 'Missing location information', - 'attribution_id': None, - }) - ]) - - """ - 🔽🔽🔽 Validations of fields - """ - - @patch.multiple( - 'breathecode.services.google_cloud.Recaptcha', - __init__=MagicMock(return_value=None), - create_assessment=MagicMock(return_value=FakeRecaptcha()), - ) - @patch('breathecode.notify.utils.hook_manager.HookManagerClass.process_model_event', MagicMock()) - def test_lead__with__bad_data(self): - url = reverse_lazy('marketing:lead') - - data = generate_form_entry_kwargs() - response = self.client.post(url, data, format='json') - - json = response.json() - expected = { - 'phone': ["Phone number must be entered in the format: '+99999999'. Up to 15 digits allowed."], - 'language': ['Ensure this field has no more than 2 characters.'] - } - - self.assertEqual(json, expected) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - """ - 🔽🔽🔽 Passing required fields - """ - - @patch.multiple( - 'breathecode.services.google_cloud.Recaptcha', - __init__=MagicMock(return_value=None), - create_assessment=MagicMock(return_value=FakeRecaptcha()), - ) - @patch('breathecode.notify.utils.hook_manager.HookManagerClass.process_model_event', MagicMock()) - @patch('uuid.UUID.int', PropertyMock(return_value=1000)) - def test_lead__with__data(self): - url = reverse_lazy('marketing:lead') - - data = generate_form_entry_kwargs({ - 'phone': '123456789', - 'language': 'en', - }) - response = self.client.post(url, data, format='json') - json = response.json() +# When: Validations of fields +def test_lead__with__bad_data(bc: Breathecode, client: APIClient): + url = reverse_lazy('marketing:lead') + + data = generate_form_entry_kwargs() + response = client.post(url, data, format='json') + + json = response.json() + expected = { + 'phone': ["Phone number must be entered in the format: '+99999999'. Up to 15 digits allowed."], + 'language': ['Ensure this field has no more than 2 characters.'] + } + + assert json == expected + assert response.status_code == status.HTTP_400_BAD_REQUEST + + +# When: Passing required fields +def test_lead__with__data(bc: Breathecode, client: APIClient): + url = reverse_lazy('marketing:lead') + + data = generate_form_entry_kwargs({ + 'phone': '123456789', + 'language': 'en', + }) - self.assertDatetime(json['created_at']) - self.assertDatetime(json['updated_at']) - del json['created_at'] - del json['updated_at'] + response = client.post(url, data, format='json') + json = response.json() - expected = post_serializer({ + assertDatetime(json['created_at']) + assertDatetime(json['updated_at']) + del json['created_at'] + del json['updated_at'] + + expected = post_serializer({ + **data, + 'id': 1, + 'academy': None, + 'latitude': bc.format.to_decimal_string(data['latitude']), + 'longitude': bc.format.to_decimal_string(data['longitude']), + 'attribution_id': '75b36c508866d18732305da14fe9a0', + }) + + assert json == expected + assert response.status_code == status.HTTP_201_CREATED + assert bc.database.list_of('marketing.FormEntry') == [ + form_entry_field({ **data, 'id': 1, - 'academy': None, - 'latitude': self.bc.format.to_decimal_string(data['latitude']), - 'longitude': self.bc.format.to_decimal_string(data['longitude']), + 'academy_id': None, + 'latitude': Decimal(data['latitude']), + 'longitude': Decimal(data['longitude']), + 'storage_status': 'ERROR', + 'storage_status_text': f"No academy found with slug {data['location']}", + 'attribution_id': '75b36c508866d18732305da14fe9a0', + }) + ] + + +# When: Passing slug of Academy or AcademyAlias +@pytest.mark.parametrize( + 'academy,academy_alias,academy_id', + [ + ({ + 'slug': 'midgard' + }, None, None), + ({ + 'slug': 'midgard' + }, 1, None), # + (1, { + 'active_campaign_slug': 'midgard' + }, 1), + ]) +def test_passing_slug_of_academy_or_academy_alias(bc: Breathecode, client: APIClient, academy, academy_alias, + academy_id): + model = bc.database.create(academy=academy, academy_alias=academy_alias) + url = reverse_lazy('marketing:lead') + + data = generate_form_entry_kwargs({ + 'phone': '123456789', + 'language': 'en', + 'location': 'midgard', + }) + + response = client.post(url, data, format='json') + json = response.json() + + assertDatetime(json['created_at']) + assertDatetime(json['updated_at']) + del json['created_at'] + del json['updated_at'] + + expected = post_serializer({ + **data, + 'id': model.academy.id, + 'academy': academy_id, + 'latitude': bc.format.to_decimal_string(data['latitude']), + 'longitude': bc.format.to_decimal_string(data['longitude']), + 'attribution_id': '75b36c508866d18732305da14fe9a0', + }) + + assert json == expected + assert response.status_code == status.HTTP_201_CREATED + assert bc.database.list_of('marketing.FormEntry') == [ + form_entry_field({ + **data, + 'id': model.academy.id, + 'academy_id': academy_id, + 'latitude': Decimal(data['latitude']), + 'longitude': Decimal(data['longitude']), + 'storage_status': 'ERROR', + 'storage_status_text': 'No academy found with slug midgard', 'attribution_id': '75b36c508866d18732305da14fe9a0', }) + ] - self.assertEqual(json, expected) - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(self.bc.database.list_of('marketing.FormEntry'), [ - form_entry_field({ - **data, - 'id': 1, - 'academy_id': None, - 'latitude': Decimal(data['latitude']), - 'longitude': Decimal(data['longitude']), - 'storage_status': 'ERROR', - 'storage_status_text': f"No academy found with slug {data['location']}", - 'attribution_id': '75b36c508866d18732305da14fe9a0', - }) - ]) - - """ - 🔽🔽🔽 Passing slug of Academy or AcademyAlias - """ - - @patch.multiple( - 'breathecode.services.google_cloud.Recaptcha', - __init__=MagicMock(return_value=None), - create_assessment=MagicMock(return_value=FakeRecaptcha()), - ) - @patch('breathecode.notify.utils.hook_manager.HookManagerClass.process_model_event', MagicMock()) - @patch('uuid.UUID.int', PropertyMock(return_value=1000)) - def test_passing_slug_of_academy_or_academy_alias(self): - cases = [ - ({ - 'slug': 'midgard' - }, None), - ({ - 'slug': 'midgard' - }, 1), - (1, { - 'active_campaign_slug': 'midgard' - }), - ] - - for academy, academy_alias in cases: - model = self.generate_models(academy=academy, academy_alias=academy_alias) - url = reverse_lazy('marketing:lead') - - data = generate_form_entry_kwargs({ - 'phone': '123456789', - 'language': 'en', - 'location': 'midgard', - }) - - response = self.client.post(url, data, format='json') - json = response.json() - - self.assertDatetime(json['created_at']) - self.assertDatetime(json['updated_at']) - del json['created_at'] - del json['updated_at'] - - expected = post_serializer({ - **data, - 'id': model.academy.id, - 'academy': model.academy.id if model.academy.id not in [1, 2] else None, - 'latitude': self.bc.format.to_decimal_string(data['latitude']), - 'longitude': self.bc.format.to_decimal_string(data['longitude']), - 'attribution_id': '75b36c508866d18732305da14fe9a0', - }) - - self.assertEqual(json, expected) - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(self.bc.database.list_of('marketing.FormEntry'), [ - form_entry_field({ - **data, - 'id': model.academy.id, - 'academy_id': model.academy.id if model.academy.id not in [1, 2] else None, - 'latitude': Decimal(data['latitude']), - 'longitude': Decimal(data['longitude']), - 'storage_status': 'ERROR', - 'storage_status_text': 'No academy found with slug midgard', - 'attribution_id': '75b36c508866d18732305da14fe9a0', - }) - ]) - - # teardown - self.bc.database.delete('admissions.Academy') - self.bc.database.delete('marketing.AcademyAlias') - self.bc.database.delete('marketing.FormEntry') + # teardown + bc.database.delete('admissions.Academy') + bc.database.delete('marketing.AcademyAlias') + bc.database.delete('marketing.FormEntry') 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 a53b1ec1f..58973475c 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 @@ -26,6 +26,7 @@ def format_consumable(data={}): return { + 'app_service_id': None, 'cohort_set_id': None, 'event_type_set_id': None, 'how_many': 0, diff --git a/breathecode/payments/models.py b/breathecode/payments/models.py index 0f1bd5696..6f0c300b3 100644 --- a/breathecode/payments/models.py +++ b/breathecode/payments/models.py @@ -13,7 +13,7 @@ from django.db.models import QuerySet from breathecode.admissions.models import DRAFT, Academy, Cohort, Country -from breathecode.authenticate.models import UserInvite +from breathecode.authenticate.models import App, UserInvite from breathecode.events.models import EventType from breathecode.authenticate.actions import get_user_settings from breathecode.mentorship.models import MentorshipService @@ -39,9 +39,7 @@ class Currency(models.Model): - """ - Represents a currency. - """ + """Represents a currency.""" code = models.CharField(max_length=3, unique=True, @@ -72,9 +70,7 @@ def __str__(self) -> str: class AbstractPriceByUnit(models.Model): - """ - This model is used to store the price of a Product or a Service. - """ + """This model is used to store the price of a Product or a Service.""" price_per_unit = models.FloatField(default=0, help_text='Price per unit') currency = models.ForeignKey(Currency, on_delete=models.CASCADE, help_text='Currency') @@ -87,9 +83,7 @@ class Meta: class AbstractPriceByTime(models.Model): - """ - This model is used to store the price of a Product or a Service. - """ + """This model is used to store the price of a Product or a Service.""" price_per_month = models.FloatField(default=None, blank=True, null=True, help_text='Price per month') price_per_quarter = models.FloatField(default=None, blank=True, null=True, help_text='Price per quarter') @@ -105,9 +99,7 @@ class Meta: class AbstractAmountByTime(models.Model): - """ - This model is used to store the price of a Product or a Service. - """ + """This model is used to store the price of a Product or a Service.""" amount_per_month = models.FloatField(default=0, help_text='Amount per month') amount_per_quarter = models.FloatField(default=0, help_text='Amount per quarter') @@ -135,9 +127,7 @@ class Meta: class AbstractAsset(models.Model): - """ - This model represents a product or a service that can be sold. - """ + """This model represents a product or a service that can be sold.""" slug = models.CharField( max_length=60, @@ -174,17 +164,25 @@ class Meta: COHORT_SET = 'COHORT_SET' MENTORSHIP_SERVICE_SET = 'MENTORSHIP_SERVICE_SET' EVENT_TYPE_SET = 'EVENT_TYPE_SET' +CHAT_SUPPORT = 'CHAT_SUPPORT' +CODE_REVIEW = 'CODE_REVIEW' +AI_INTERACTION = 'AI_INTERACTION' +LEARNPACK_BUILD = 'LEARNPACK_BUILD' +LEARNPACK_TEST = 'LEARNPACK_TEST' SERVICE_TYPES = [ (COHORT_SET, 'Cohort set'), (MENTORSHIP_SERVICE_SET, 'Mentorship service set'), (EVENT_TYPE_SET, 'Event type set'), + (CHAT_SUPPORT, 'Chat support'), + (CODE_REVIEW, 'Code review'), + (AI_INTERACTION, 'AI interaction'), + (LEARNPACK_BUILD, 'Learnpack build'), + (LEARNPACK_TEST, 'Learnpack test'), ] class Service(AbstractAsset): - """ - Represents the service that can be purchased by the customer. - """ + """Represents the service that can be purchased by the customer.""" groups = models.ManyToManyField(Group, blank=True, @@ -223,9 +221,7 @@ def __str__(self) -> str: class AbstractServiceItem(models.Model): - """ - Common fields for ServiceItem and Consumable. - """ + """Common fields for ServiceItem and Consumable.""" # the unit between a service and a product are different unit_type = models.CharField(max_length=10, @@ -243,9 +239,7 @@ class Meta: # this class is used as referenced of units of a service can be used class ServiceItem(AbstractServiceItem): - """ - This model is used as referenced of units of a service can be used. - """ + """This model is used as referenced of units of a service can be used.""" service = models.ForeignKey(Service, on_delete=models.CASCADE, help_text='Service') is_renewable = models.BooleanField( @@ -283,9 +277,7 @@ def __str__(self) -> str: class ServiceItemFeature(models.Model): - """ - This model is used as referenced of units of a service can be used. - """ + """This model is used as referenced of units of a service can be used.""" service_item = models.ForeignKey(ServiceItem, on_delete=models.CASCADE, help_text='Service item') lang = models.CharField(max_length=5, @@ -300,9 +292,7 @@ def __str__(self) -> str: class FinancingOption(models.Model): - """ - This model is used as referenced of units of a service can be used. - """ + """This model is used as referenced of units of a service can be used.""" _lang = 'en' @@ -331,9 +321,7 @@ def __str__(self) -> str: class CohortSet(models.Model): - """ - Cohort set. - """ + """Cohort set.""" _lang = 'en' @@ -375,9 +363,7 @@ class CohortSetTranslation(models.Model): class CohortSetCohort(models.Model): - """ - M2M between CohortSet and Cohort. - """ + """M2M between CohortSet and Cohort.""" _lang = 'en' @@ -408,9 +394,7 @@ def save(self, *args, **kwargs) -> None: class MentorshipServiceSet(models.Model): - """ - M2M between plan and ServiceItem - """ + """M2M between plan and ServiceItem.""" slug = models.SlugField( max_length=100, @@ -436,9 +420,7 @@ class MentorshipServiceSetTranslation(models.Model): class EventTypeSet(models.Model): - """ - M2M between plan and ServiceItem - """ + """M2M between plan and ServiceItem.""" slug = models.SlugField( max_length=100, @@ -552,9 +534,7 @@ def save(self, *args, **kwargs) -> None: class Plan(AbstractPriceByTime): - """ - A plan is a group of services that can be purchased by a user. - """ + """A plan is a group of services that can be purchased by a user.""" slug = models.CharField( max_length=60, @@ -759,9 +739,8 @@ class PlanOfferTranslation(models.Model): class Bag(AbstractAmountByTime): - """ - Represents a credit that can be used by a user to use a service. - """ + """Represents a credit that can be used by a user to use a service.""" + objects = LockManager() status = models.CharField(max_length=8, @@ -831,9 +810,7 @@ def __str__(self) -> str: class Invoice(models.Model): - """ - Represents a payment made by a user - """ + """Represents a payment made by a user.""" amount = models.FloatField( default=0, @@ -898,9 +875,7 @@ def __str__(self) -> str: class AbstractIOweYou(models.Model): - """ - Common fields for all I owe you. - """ + """Common fields for all I owe you.""" status = models.CharField(max_length=13, choices=SUBSCRIPTION_STATUS, @@ -951,9 +926,7 @@ class Meta: class PlanFinancing(AbstractIOweYou): - """ - Allows to financing a plan - """ + """Allows to financing a plan.""" # in this day the financing needs being paid again next_payment_at = models.DateTimeField(help_text='Next payment date') @@ -999,9 +972,7 @@ def save(self, *args, **kwargs) -> None: class Subscription(AbstractIOweYou): - """ - Allows to create a subscription to a plan and services. - """ + """Allows to create a subscription to a plan and services.""" _lang = 'en' @@ -1082,10 +1053,31 @@ def __str__(self) -> str: return str(self.service_item) +class AppService(models.Model): + app = models.ForeignKey(App, + on_delete=models.CASCADE, + help_text='Subscription', + null=True, + blank=True, + default=None) + service = models.SlugField(help_text='Microservice slug') + + def clean(self) -> None: + if self.__class__.objects.filter(app=self.app, service=self.service).exists(): + raise forms.ValidationError('App service already exists') + return super().clean() + + def save(self, *args, **kwargs): + self.full_clean() + + super().save(*args, **kwargs) + + def __str__(self) -> str: + return self.app.slug + ' -> ' + self.service + + class Consumable(AbstractServiceItem): - """ - This model is used to represent the units of a service that can be consumed. - """ + """This model is used to represent the units of a service that can be consumed.""" service_item = models.ForeignKey( ServiceItem, @@ -1101,19 +1093,26 @@ class Consumable(AbstractServiceItem): default=None, blank=True, null=True, - help_text='Cohort which the consumable belongs to') + help_text='Cohort set which the consumable belongs to') event_type_set = models.ForeignKey(EventTypeSet, on_delete=models.CASCADE, default=None, blank=True, null=True, - help_text='Event type which the consumable belongs to') - mentorship_service_set = models.ForeignKey(MentorshipServiceSet, - on_delete=models.CASCADE, - default=None, - blank=True, - null=True, - help_text='Mentorship service which the consumable belongs to') + help_text='Event type set which the consumable belongs to') + mentorship_service_set = models.ForeignKey( + MentorshipServiceSet, + on_delete=models.CASCADE, + default=None, + blank=True, + null=True, + help_text='Mentorship service set which the consumable belongs to') + app_service = models.ForeignKey(AppService, + on_delete=models.CASCADE, + default=None, + blank=True, + null=True, + help_text='App service which the consumable belongs to') valid_until = models.DateTimeField( null=True, @@ -1202,7 +1201,7 @@ def clean(self) -> None: resources = [self.cohort_set] if self.id: - resources += [self.event_type_set, self.mentorship_service_set] + resources += [self.event_type_set, self.mentorship_service_set, self.app_service] how_many_resources_are_set = len([ r for r in resources @@ -1378,9 +1377,7 @@ def will_consume(self, how_many: float = 1.0) -> None: class PlanServiceItem(models.Model): - """ - M2M between plan and ServiceItem - """ + """M2M between plan and ServiceItem.""" _lang = 'en' @@ -1389,9 +1386,7 @@ class PlanServiceItem(models.Model): class PlanServiceItemHandler(models.Model): - """ - M2M between plan and ServiceItem - """ + """M2M between plan and ServiceItem.""" handler = models.ForeignKey(PlanServiceItem, on_delete=models.CASCADE, help_text='Plan service item') @@ -1431,9 +1426,7 @@ def __str__(self) -> str: class ServiceStockScheduler(models.Model): - """ - This model is used to represent the units of a service that can be consumed. - """ + """This model is used to represent the units of a service that can be consumed.""" # all this section are M2M service items, in the first case we have a query with subscription and service # item for schedule the renovations @@ -1513,8 +1506,9 @@ def __str__(self) -> str: class FinancialReputation(models.Model): """ - The purpose of this model is to store the reputation of a user, if the user has a bad reputation, the - user will not be able to buy services. + Store the reputation of a user. + + If the user has a bad reputation, the user will not be able to buy services. """ user = models.OneToOneField(User, @@ -1535,9 +1529,7 @@ class FinancialReputation(models.Model): updated_at = models.DateTimeField(auto_now=True, editable=False) def get_reputation(self): - """ - Returns the worst reputation between the two. - """ + """Get the worst reputation available made by an user.""" if self.in_4geeks == FRAUD or self.in_stripe == FRAUD: return FRAUD diff --git a/breathecode/payments/tests/tasks/tests_refund_mentoring_session.py b/breathecode/payments/tests/tasks/tests_refund_mentoring_session.py index 5c8d1af53..77e02e1dc 100644 --- a/breathecode/payments/tests/tasks/tests_refund_mentoring_session.py +++ b/breathecode/payments/tests/tasks/tests_refund_mentoring_session.py @@ -3,432 +3,388 @@ """ import logging import random -from unittest.mock import MagicMock, call, patch +from unittest.mock import MagicMock, call from django.utils import timezone - -from breathecode.tests.mixins.legacy import LegacyAPITestCase +import pytest +from breathecode.tests.mixins.breathecode_mixin.breathecode import Breathecode from ...tasks import refund_mentoring_session UTC_NOW = timezone.now() -class TestPayments(LegacyAPITestCase): - # When: no mentoring session - # Then: do nothing - @patch('logging.Logger.info', MagicMock()) - @patch('logging.Logger.error', MagicMock()) - @patch('django.utils.timezone.now', MagicMock(return_value=UTC_NOW)) - @patch('breathecode.mentorship.signals.mentorship_session_status.send', MagicMock()) - @patch('breathecode.payments.signals.grant_service_permissions.send', MagicMock()) - def test_0_items(self): - - refund_mentoring_session.delay(1) - - self.bc.check.calls(logging.Logger.info.call_args_list, [ - call('Starting refund_mentoring_session for mentoring session 1'), - ]) - self.bc.check.calls(logging.Logger.error.call_args_list, [ - call('MentoringSession with id 1 not found or is invalid', exc_info=True), - ]) - - self.assertEqual(self.bc.database.list_of('mentorship.MentorshipSession'), []) - self.assertEqual(self.bc.database.list_of('payments.ConsumptionSession'), []) - self.assertEqual(self.bc.database.list_of('payments.Consumable'), []) - - # Given: 1 MentoringSession - # When: not have mentee, service and have a bad status - # Then: not found mentorship session - @patch('logging.Logger.info', MagicMock()) - @patch('logging.Logger.error', MagicMock()) - @patch('django.utils.timezone.now', MagicMock(return_value=UTC_NOW)) - @patch('breathecode.mentorship.signals.mentorship_session_status.send', MagicMock()) - @patch('breathecode.payments.signals.grant_service_permissions.send', MagicMock()) - def test_1_mentoring_session__nothing_provide(self): - - model = self.bc.database.create(mentorship_session=1) - - # remove prints from mixer - logging.Logger.info.call_args_list = [] - logging.Logger.error.call_args_list = [] - - refund_mentoring_session.delay(1) - - self.bc.check.calls(logging.Logger.info.call_args_list, [ - call('Starting refund_mentoring_session for mentoring session 1'), - ]) - self.bc.check.calls(logging.Logger.error.call_args_list, [ - call('MentoringSession with id 1 not found or is invalid', exc_info=True), - ]) - - self.assertEqual(self.bc.database.list_of('mentorship.MentorshipSession'), [ - self.bc.format.to_dict(model.mentorship_session), - ]) - self.assertEqual(self.bc.database.list_of('payments.ConsumptionSession'), []) - self.assertEqual(self.bc.database.list_of('payments.Consumable'), []) - - # Given: 1 MentoringSession and 1 User - # When: have mentee and not have service and have a bad status - # Then: not found mentorship session - @patch('logging.Logger.info', MagicMock()) - @patch('logging.Logger.error', MagicMock()) - @patch('django.utils.timezone.now', MagicMock(return_value=UTC_NOW)) - @patch('breathecode.mentorship.signals.mentorship_session_status.send', MagicMock()) - @patch('breathecode.payments.signals.grant_service_permissions.send', MagicMock()) - def test_1_mentoring_session__just_with_mentee(self): - - user = {'groups': []} - model = self.bc.database.create(mentorship_session=1, user=user, group=1, permission=1) - - # remove prints from mixer - logging.Logger.info.call_args_list = [] - logging.Logger.error.call_args_list = [] - - refund_mentoring_session.delay(1) - - self.bc.check.calls(logging.Logger.info.call_args_list, [ - call('Starting refund_mentoring_session for mentoring session 1'), - ]) - self.bc.check.calls(logging.Logger.error.call_args_list, [ - call('MentoringSession with id 1 not found or is invalid', exc_info=True), - ]) - - self.assertEqual(self.bc.database.list_of('mentorship.MentorshipSession'), [ - self.bc.format.to_dict(model.mentorship_session), - ]) - self.assertEqual(self.bc.database.list_of('payments.ConsumptionSession'), []) - self.assertEqual(self.bc.database.list_of('payments.Consumable'), []) - - self.bc.check.queryset_with_pks(model.user.groups.all(), []) - - # Given: 1 MentoringSession and 1 MentorshipService - # When: have service and not have mentee and have a bad status - # Then: not found mentorship session - @patch('logging.Logger.info', MagicMock()) - @patch('logging.Logger.error', MagicMock()) - @patch('django.utils.timezone.now', MagicMock(return_value=UTC_NOW)) - @patch('breathecode.mentorship.signals.mentorship_session_status.send', MagicMock()) - @patch('breathecode.payments.signals.grant_service_permissions.send', MagicMock()) - def test_1_mentoring_session__just_with_service(self): - - model = self.bc.database.create(mentorship_session=1, mentorship_service=1) - - # remove prints from mixer - logging.Logger.info.call_args_list = [] - logging.Logger.error.call_args_list = [] - - refund_mentoring_session.delay(1) - - self.bc.check.calls(logging.Logger.info.call_args_list, [ - call('Starting refund_mentoring_session for mentoring session 1'), - ]) - self.bc.check.calls(logging.Logger.error.call_args_list, [ - call('MentoringSession with id 1 not found or is invalid', exc_info=True), - ]) - - self.assertEqual(self.bc.database.list_of('mentorship.MentorshipSession'), [ - self.bc.format.to_dict(model.mentorship_session), - ]) - self.assertEqual(self.bc.database.list_of('payments.ConsumptionSession'), []) - self.assertEqual(self.bc.database.list_of('payments.Consumable'), []) - - # Given: 1 MentoringSession - # When: not have service, mentee and have a right status - # Then: not found mentorship session - @patch('logging.Logger.info', MagicMock()) - @patch('logging.Logger.error', MagicMock()) - @patch('django.utils.timezone.now', MagicMock(return_value=UTC_NOW)) - @patch('breathecode.mentorship.signals.mentorship_session_status.send', MagicMock()) - @patch('breathecode.payments.signals.grant_service_permissions.send', MagicMock()) - def test_1_mentoring_session__just_with_right_status(self): - - mentorship_session = {'status': random.choice(['PENDING', 'STARTED', 'COMPLETED'])} - model = self.bc.database.create(mentorship_session=mentorship_session) - - # remove prints from mixer - logging.Logger.info.call_args_list = [] - logging.Logger.error.call_args_list = [] - - refund_mentoring_session.delay(1) - - self.bc.check.calls(logging.Logger.info.call_args_list, [ - call('Starting refund_mentoring_session for mentoring session 1'), - ]) - self.bc.check.calls(logging.Logger.error.call_args_list, [ - call('MentoringSession with id 1 not found or is invalid', exc_info=True), - ]) - - self.assertEqual(self.bc.database.list_of('mentorship.MentorshipSession'), [ - self.bc.format.to_dict(model.mentorship_session), - ]) - self.assertEqual(self.bc.database.list_of('payments.ConsumptionSession'), []) - self.assertEqual(self.bc.database.list_of('payments.Consumable'), []) - - # Given: 1 MentoringSession, 1 User and 1 MentorshipService - # When: have service, mentee and have a right status - # Then: not found mentorship session - @patch('logging.Logger.info', MagicMock()) - @patch('logging.Logger.error', MagicMock()) - @patch('django.utils.timezone.now', MagicMock(return_value=UTC_NOW)) - @patch('breathecode.mentorship.signals.mentorship_session_status.send', MagicMock()) - @patch('breathecode.payments.signals.grant_service_permissions.send', MagicMock()) - def test_1_mentoring_session__all_elements_given(self): - - mentorship_session = {'status': random.choice(['FAILED', 'IGNORED'])} - - user = {'groups': []} - model = self.bc.database.create(mentorship_session=mentorship_session, - user=user, - mentorship_service=1, - group=1, - permission=1) - - # remove prints from mixer - logging.Logger.info.call_args_list = [] - logging.Logger.error.call_args_list = [] - - refund_mentoring_session.delay(1) - - self.bc.check.calls(logging.Logger.info.call_args_list, [ - call('Starting refund_mentoring_session for mentoring session 1'), - ]) - self.bc.check.calls(logging.Logger.error.call_args_list, [ - call('ConsumptionSession not found for mentorship session 1', exc_info=True), - ]) - - self.assertEqual(self.bc.database.list_of('mentorship.MentorshipSession'), [ - self.bc.format.to_dict(model.mentorship_session), - ]) - self.assertEqual(self.bc.database.list_of('payments.ConsumptionSession'), []) - self.assertEqual(self.bc.database.list_of('payments.Consumable'), []) - - self.bc.check.queryset_with_pks(model.user.groups.all(), []) - - # Given: 1 MentoringSession, 1 User, 1 ConsumptionSession, 1 Consumable and 1 MentorshipServiceSet - # When: consumption session is pending - # Then: not refund consumable - @patch('logging.Logger.info', MagicMock()) - @patch('logging.Logger.error', MagicMock()) - @patch('django.utils.timezone.now', MagicMock(return_value=UTC_NOW)) - @patch('breathecode.mentorship.signals.mentorship_session_status.send', MagicMock()) - @patch('breathecode.payments.signals.grant_service_permissions.send', MagicMock()) - def test_consumption_session_is_pending(self): - - mentorship_session = {'status': random.choice(['FAILED', 'IGNORED'])} - how_many_consumables = random.randint(1, 10) - how_mawy_will_consume = random.randint(1, how_many_consumables) - consumable = {'how_many': how_many_consumables} - consumption_session = {'how_many': how_mawy_will_consume, 'status': 'PENDING'} - - user = {'groups': []} - model = self.bc.database.create(mentorship_session=mentorship_session, - user=user, - mentorship_service=1, - consumption_session=consumption_session, - consumable=consumable, - mentorship_service_set=1, - group=1, - permission=1) - - # remove prints from mixer - logging.Logger.info.call_args_list = [] - logging.Logger.error.call_args_list = [] - - refund_mentoring_session.delay(1) - - self.bc.check.calls(logging.Logger.info.call_args_list, [ - call('Starting refund_mentoring_session for mentoring session 1'), - ]) - self.bc.check.calls(logging.Logger.error.call_args_list, []) - - self.assertEqual(self.bc.database.list_of('mentorship.MentorshipSession'), [ - self.bc.format.to_dict(model.mentorship_session), - ]) - - self.assertEqual(self.bc.database.list_of('payments.ConsumptionSession'), [ - { - **self.bc.format.to_dict(model.consumption_session), - 'status': 'CANCELLED', - }, - ]) - - self.assertEqual(self.bc.database.list_of('payments.Consumable'), [ - self.bc.format.to_dict(model.consumable), - ]) - - self.bc.check.queryset_with_pks(model.user.groups.all(), []) - - # Given: 1 MentoringSession, 1 User, 1 ConsumptionSession, 1 Consumable and 1 MentorshipServiceSet - # When: consumption session is done - # Then: not refund consumable - @patch('logging.Logger.info', MagicMock()) - @patch('logging.Logger.error', MagicMock()) - @patch('django.utils.timezone.now', MagicMock(return_value=UTC_NOW)) - @patch('breathecode.mentorship.signals.mentorship_session_status.send', MagicMock()) - @patch('breathecode.payments.signals.grant_service_permissions.send', MagicMock()) - def test_consumption_session_is_done(self, enable_signals): - enable_signals('breathecode.payments.signals.reimburse_service_units') - - mentorship_session = {'status': random.choice(['FAILED', 'IGNORED'])} - how_many_consumables = random.randint(1, 10) - how_mawy_will_consume = random.randint(1, 10) - consumable = {'how_many': how_many_consumables} - consumption_session = {'how_many': how_mawy_will_consume, 'status': 'DONE'} - - user = {'groups': []} - model = self.bc.database.create(mentorship_session=mentorship_session, - user=user, - mentorship_service=1, - consumption_session=consumption_session, - consumable=consumable, - mentorship_service_set=1, - group=1, - permission=1) - - # remove prints from mixer - logging.Logger.info.call_args_list = [] - logging.Logger.error.call_args_list = [] - - refund_mentoring_session.delay(1) - - self.bc.check.calls(logging.Logger.info.call_args_list, [ - call('Starting refund_mentoring_session for mentoring session 1'), - call('Refunding consumption session because it was discounted'), - ]) - self.bc.check.calls(logging.Logger.error.call_args_list, []) - - self.assertEqual(self.bc.database.list_of('mentorship.MentorshipSession'), [ - self.bc.format.to_dict(model.mentorship_session), - ]) - - self.assertEqual(self.bc.database.list_of('payments.ConsumptionSession'), [ - { - **self.bc.format.to_dict(model.consumption_session), - 'status': 'CANCELLED', - }, - ]) - - self.assertEqual(self.bc.database.list_of('payments.Consumable'), [{ - **self.bc.format.to_dict(model.consumable), - 'how_many': - how_many_consumables + how_mawy_will_consume, - }]) - - self.bc.check.queryset_with_pks(model.user.groups.all(), []) - - # Given: 1 MentoringSession, 1 User, 1 ConsumptionSession, 1 Consumable and 1 MentorshipServiceSet - # When: consumption session is done - # Then: not refund consumable - @patch('logging.Logger.info', MagicMock()) - @patch('logging.Logger.error', MagicMock()) - @patch('django.utils.timezone.now', MagicMock(return_value=UTC_NOW)) - @patch('breathecode.mentorship.signals.mentorship_session_status.send', MagicMock()) - @patch('breathecode.payments.signals.grant_service_permissions.send', MagicMock()) - def test_consumption_session_is_cancelled(self): - - mentorship_session = {'status': random.choice(['FAILED', 'IGNORED'])} - how_many_consumables = random.randint(1, 10) - how_mawy_will_consume = random.randint(1, 10) - consumable = {'how_many': how_many_consumables} - consumption_session = {'how_many': how_mawy_will_consume, 'status': 'CANCELLED'} - - user = {'groups': []} - model = self.bc.database.create(mentorship_session=mentorship_session, - user=user, - mentorship_service=1, - consumption_session=consumption_session, - consumable=consumable, - mentorship_service_set=1, - group=1, - permission=1) - - # remove prints from mixer - logging.Logger.info.call_args_list = [] - logging.Logger.error.call_args_list = [] - - refund_mentoring_session.delay(1) - - self.bc.check.calls(logging.Logger.info.call_args_list, [ - call('Starting refund_mentoring_session for mentoring session 1'), - ]) - self.bc.check.calls(logging.Logger.error.call_args_list, [ - call('ConsumptionSession not found for mentorship session 1', exc_info=True), - ]) - - self.assertEqual(self.bc.database.list_of('mentorship.MentorshipSession'), [ - self.bc.format.to_dict(model.mentorship_session), - ]) - - self.assertEqual(self.bc.database.list_of('payments.ConsumptionSession'), [ - self.bc.format.to_dict(model.consumption_session), - ]) - - self.assertEqual(self.bc.database.list_of('payments.Consumable'), [ - self.bc.format.to_dict(model.consumable), - ]) - - self.bc.check.queryset_with_pks(model.user.groups.all(), []) - - # Given: 1 MentoringSession, 1 User, 1 ConsumptionSession, 1 Consumable and 1 MentorshipServiceSet - # When: consumption session is done and consumable how many is 0 - # Then: not refund consumable - @patch('logging.Logger.info', MagicMock()) - @patch('logging.Logger.error', MagicMock()) - @patch('django.utils.timezone.now', MagicMock(return_value=UTC_NOW)) - @patch('breathecode.mentorship.signals.mentorship_session_status.send', MagicMock()) - def test_consumable_wasted(self, enable_signals): - enable_signals( - 'breathecode.payments.signals.consume_service', - 'breathecode.payments.signals.grant_service_permissions', - 'breathecode.payments.signals.lose_service_permissions', - 'breathecode.payments.signals.reimburse_service_units', # - ) - - mentorship_session = {'status': random.choice(['FAILED', 'IGNORED'])} - how_many_consumables = 0 - how_mawy_will_consume = random.randint(1, 10) - consumable = {'how_many': how_many_consumables} - consumption_session = {'how_many': how_mawy_will_consume, 'status': 'DONE'} - - user = {'groups': []} - groups = [{'permissions': n} for n in range(1, 4)] - model = self.bc.database.create(mentorship_session=mentorship_session, - user=user, - mentorship_service=1, - consumption_session=consumption_session, - consumable=consumable, - mentorship_service_set=1, - group=groups, - permission=2) - - # remove prints from mixer - logging.Logger.info.call_args_list = [] - logging.Logger.error.call_args_list = [] - - refund_mentoring_session.delay(1) - - self.bc.check.calls(logging.Logger.info.call_args_list, [ - call('Starting refund_mentoring_session for mentoring session 1'), - call('Refunding consumption session because it was discounted'), - ]) - self.bc.check.calls(logging.Logger.error.call_args_list, []) - - self.assertEqual(self.bc.database.list_of('mentorship.MentorshipSession'), [ - self.bc.format.to_dict(model.mentorship_session), - ]) - - self.assertEqual(self.bc.database.list_of('payments.ConsumptionSession'), [ - { - **self.bc.format.to_dict(model.consumption_session), - 'status': 'CANCELLED', - }, - ]) - - self.assertEqual(self.bc.database.list_of('payments.Consumable'), [{ - **self.bc.format.to_dict(model.consumable), - 'how_many': - how_many_consumables + how_mawy_will_consume, - }]) - - self.bc.check.queryset_with_pks(model.user.groups.all(), [1, 2, 3]) +@pytest.fixture(autouse=True) +def setup_db(db, monkeypatch, enable_signals): + enable_signals( + 'breathecode.payments.signals.consume_service', + 'breathecode.payments.signals.grant_service_permissions', + 'breathecode.payments.signals.lose_service_permissions', + 'breathecode.payments.signals.reimburse_service_units', # + ) + monkeypatch.setattr('logging.Logger.info', MagicMock()) + monkeypatch.setattr('logging.Logger.error', MagicMock()) + monkeypatch.setattr('django.utils.timezone.now', MagicMock(return_value=UTC_NOW)) + yield + + +# When: no mentoring session +# Then: do nothing +def test_0_items(bc: Breathecode): + refund_mentoring_session.delay(1) + + bc.check.calls(logging.Logger.info.call_args_list, [ + call('Starting refund_mentoring_session for mentoring session 1'), + ]) + bc.check.calls(logging.Logger.error.call_args_list, [ + call('MentoringSession with id 1 not found or is invalid', exc_info=True), + ]) + + assert bc.database.list_of('mentorship.MentorshipSession') == [] + assert bc.database.list_of('payments.ConsumptionSession') == [] + assert bc.database.list_of('payments.Consumable') == [] + + +# Given: 1 MentoringSession +# When: not have mentee, service and have a bad status +# Then: not found mentorship session +def test_1_mentoring_session__nothing_provide(bc: Breathecode): + model = bc.database.create(mentorship_session=1) + + # remove prints from mixer + logging.Logger.info.call_args_list = [] + logging.Logger.error.call_args_list = [] + + refund_mentoring_session.delay(1) + + bc.check.calls(logging.Logger.info.call_args_list, [ + call('Starting refund_mentoring_session for mentoring session 1'), + ]) + bc.check.calls(logging.Logger.error.call_args_list, [ + call('MentoringSession with id 1 not found or is invalid', exc_info=True), + ]) + + assert bc.database.list_of('mentorship.MentorshipSession') == [ + bc.format.to_dict(model.mentorship_session), + ] + assert bc.database.list_of('payments.ConsumptionSession') == [] + assert bc.database.list_of('payments.Consumable') == [] + + +# Given: 1 MentoringSession and 1 User +# When: have mentee and not have service and have a bad status +# Then: not found mentorship session +def test_1_mentoring_session__just_with_mentee(bc: Breathecode, get_queryset_pks): + user = {'groups': []} + model = bc.database.create(mentorship_session=1, user=user, group=1, permission=1) + + # remove prints from mixer + logging.Logger.info.call_args_list = [] + logging.Logger.error.call_args_list = [] + + refund_mentoring_session.delay(1) + + bc.check.calls(logging.Logger.info.call_args_list, [ + call('Starting refund_mentoring_session for mentoring session 1'), + ]) + bc.check.calls(logging.Logger.error.call_args_list, [ + call('MentoringSession with id 1 not found or is invalid', exc_info=True), + ]) + + assert bc.database.list_of('mentorship.MentorshipSession') == [ + bc.format.to_dict(model.mentorship_session), + ] + assert bc.database.list_of('payments.ConsumptionSession') == [] + assert bc.database.list_of('payments.Consumable') == [] + + get_queryset_pks(model.user.groups.all()) == [] + + +# Given: 1 MentoringSession and 1 MentorshipService +# When: have service and not have mentee and have a bad status +# Then: not found mentorship session +def test_1_mentoring_session__just_with_service(bc: Breathecode): + model = bc.database.create(mentorship_session=1, mentorship_service=1) + + # remove prints from mixer + logging.Logger.info.call_args_list = [] + logging.Logger.error.call_args_list = [] + + refund_mentoring_session.delay(1) + + bc.check.calls(logging.Logger.info.call_args_list, [ + call('Starting refund_mentoring_session for mentoring session 1'), + ]) + bc.check.calls(logging.Logger.error.call_args_list, [ + call('MentoringSession with id 1 not found or is invalid', exc_info=True), + ]) + + assert bc.database.list_of('mentorship.MentorshipSession') == [ + bc.format.to_dict(model.mentorship_session), + ] + assert bc.database.list_of('payments.ConsumptionSession') == [] + assert bc.database.list_of('payments.Consumable') == [] + + +# Given: 1 MentoringSession +# When: not have service, mentee and have a right status +# Then: not found mentorship session +def test_1_mentoring_session__just_with_right_status(bc: Breathecode): + mentorship_session = {'status': random.choice(['PENDING', 'STARTED', 'COMPLETED'])} + model = bc.database.create(mentorship_session=mentorship_session) + + # remove prints from mixer + logging.Logger.info.call_args_list = [] + logging.Logger.error.call_args_list = [] + + refund_mentoring_session.delay(1) + + bc.check.calls(logging.Logger.info.call_args_list, [ + call('Starting refund_mentoring_session for mentoring session 1'), + ]) + bc.check.calls(logging.Logger.error.call_args_list, [ + call('MentoringSession with id 1 not found or is invalid', exc_info=True), + ]) + + assert bc.database.list_of('mentorship.MentorshipSession') == [ + bc.format.to_dict(model.mentorship_session), + ] + assert bc.database.list_of('payments.ConsumptionSession') == [] + assert bc.database.list_of('payments.Consumable') == [] + + +# Given: 1 MentoringSession, 1 User and 1 MentorshipService +# When: have service, mentee and have a right status +# Then: not found mentorship session +def test_1_mentoring_session__all_elements_given(bc: Breathecode, get_queryset_pks): + mentorship_session = {'status': random.choice(['FAILED', 'IGNORED'])} + + user = {'groups': []} + model = bc.database.create(mentorship_session=mentorship_session, + user=user, + mentorship_service=1, + group=1, + permission=1) + + # remove prints from mixer + logging.Logger.info.call_args_list = [] + logging.Logger.error.call_args_list = [] + + refund_mentoring_session.delay(1) + + bc.check.calls(logging.Logger.info.call_args_list, [ + call('Starting refund_mentoring_session for mentoring session 1'), + ]) + bc.check.calls(logging.Logger.error.call_args_list, [ + call('ConsumptionSession not found for mentorship session 1', exc_info=True), + ]) + + assert bc.database.list_of('mentorship.MentorshipSession') == [ + bc.format.to_dict(model.mentorship_session), + ] + assert bc.database.list_of('payments.ConsumptionSession') == [] + assert bc.database.list_of('payments.Consumable') == [] + + get_queryset_pks(model.user.groups.all()) == [] + + +# Given: 1 MentoringSession, 1 User, 1 ConsumptionSession, 1 Consumable and 1 MentorshipServiceSet +# When: consumption session is pending +# Then: not refund consumable +def test_consumption_session_is_pending(bc: Breathecode, get_queryset_pks): + mentorship_session = {'status': random.choice(['FAILED', 'IGNORED'])} + how_many_consumables = random.randint(1, 10) + how_mawy_will_consume = random.randint(1, how_many_consumables) + consumable = {'how_many': how_many_consumables} + consumption_session = {'how_many': how_mawy_will_consume, 'status': 'PENDING'} + + user = {'groups': []} + model = bc.database.create(mentorship_session=mentorship_session, + user=user, + mentorship_service=1, + consumption_session=consumption_session, + consumable=consumable, + mentorship_service_set=1, + group=1, + permission=1) + + # remove prints from mixer + logging.Logger.info.call_args_list = [] + logging.Logger.error.call_args_list = [] + + refund_mentoring_session.delay(1) + + bc.check.calls(logging.Logger.info.call_args_list, [ + call('Starting refund_mentoring_session for mentoring session 1'), + ]) + bc.check.calls(logging.Logger.error.call_args_list, []) + + assert bc.database.list_of('mentorship.MentorshipSession') == [ + bc.format.to_dict(model.mentorship_session), + ] + + assert bc.database.list_of('payments.ConsumptionSession') == [ + { + **bc.format.to_dict(model.consumption_session), + 'status': 'CANCELLED', + }, + ] + + assert bc.database.list_of('payments.Consumable') == [ + bc.format.to_dict(model.consumable), + ] + + get_queryset_pks(model.user.groups.all()) == [] + + +# Given: 1 MentoringSession, 1 User, 1 ConsumptionSession, 1 Consumable and 1 MentorshipServiceSet +# When: consumption session is done +# Then: not refund consumable +def test_consumption_session_is_done(bc: Breathecode, get_queryset_pks): + mentorship_session = {'status': random.choice(['FAILED', 'IGNORED'])} + how_many_consumables = random.randint(1, 10) + how_mawy_will_consume = random.randint(1, 10) + consumable = {'how_many': how_many_consumables} + consumption_session = {'how_many': how_mawy_will_consume, 'status': 'DONE'} + + user = {'groups': []} + model = bc.database.create(mentorship_session=mentorship_session, + user=user, + mentorship_service=1, + consumption_session=consumption_session, + consumable=consumable, + mentorship_service_set=1, + group=1, + permission=1) + + # remove prints from mixer + logging.Logger.info.call_args_list = [] + logging.Logger.error.call_args_list = [] + + refund_mentoring_session.delay(1) + + bc.check.calls(logging.Logger.info.call_args_list, [ + call('Starting refund_mentoring_session for mentoring session 1'), + call('Refunding consumption session because it was discounted'), + ]) + bc.check.calls(logging.Logger.error.call_args_list, []) + + assert bc.database.list_of('mentorship.MentorshipSession') == [ + bc.format.to_dict(model.mentorship_session), + ] + + assert bc.database.list_of('payments.ConsumptionSession') == [ + { + **bc.format.to_dict(model.consumption_session), + 'status': 'CANCELLED', + }, + ] + + assert bc.database.list_of('payments.Consumable') == [{ + **bc.format.to_dict(model.consumable), + 'how_many': + how_many_consumables + how_mawy_will_consume, + }] + + get_queryset_pks(model.user.groups.all()) == [] + + +# Given: 1 MentoringSession, 1 User, 1 ConsumptionSession, 1 Consumable and 1 MentorshipServiceSet +# When: consumption session is done +# Then: not refund consumable +def test_consumption_session_is_cancelled(bc: Breathecode, get_queryset_pks): + mentorship_session = {'status': random.choice(['FAILED', 'IGNORED'])} + how_many_consumables = random.randint(1, 10) + how_mawy_will_consume = random.randint(1, 10) + consumable = {'how_many': how_many_consumables} + consumption_session = {'how_many': how_mawy_will_consume, 'status': 'CANCELLED'} + + user = {'groups': []} + model = bc.database.create(mentorship_session=mentorship_session, + user=user, + mentorship_service=1, + consumption_session=consumption_session, + consumable=consumable, + mentorship_service_set=1, + group=1, + permission=1) + + # remove prints from mixer + logging.Logger.info.call_args_list = [] + logging.Logger.error.call_args_list = [] + + refund_mentoring_session.delay(1) + + bc.check.calls(logging.Logger.info.call_args_list, [ + call('Starting refund_mentoring_session for mentoring session 1'), + ]) + bc.check.calls(logging.Logger.error.call_args_list, [ + call('ConsumptionSession not found for mentorship session 1', exc_info=True), + ]) + + assert bc.database.list_of('mentorship.MentorshipSession') == [ + bc.format.to_dict(model.mentorship_session), + ] + + assert bc.database.list_of('payments.ConsumptionSession') == [ + bc.format.to_dict(model.consumption_session), + ] + + assert bc.database.list_of('payments.Consumable') == [ + bc.format.to_dict(model.consumable), + ] + + get_queryset_pks(model.user.groups.all()) == [] + + +# Given: 1 MentoringSession, 1 User, 1 ConsumptionSession, 1 Consumable and 1 MentorshipServiceSet +# When: consumption session is done and consumable how many is 0 +# Then: not refund consumable +def test_consumable_wasted(bc: Breathecode, get_queryset_pks): + mentorship_session = {'status': random.choice(['FAILED', 'IGNORED'])} + how_many_consumables = 0 + how_mawy_will_consume = random.randint(1, 10) + consumable = {'how_many': how_many_consumables} + consumption_session = {'how_many': how_mawy_will_consume, 'status': 'DONE'} + + user = {'groups': []} + groups = [{'permissions': n} for n in range(1, 4)] + model = bc.database.create(mentorship_session=mentorship_session, + user=user, + mentorship_service=1, + consumption_session=consumption_session, + consumable=consumable, + mentorship_service_set=1, + group=groups, + permission=2) + + # remove prints from mixer + logging.Logger.info.call_args_list = [] + logging.Logger.error.call_args_list = [] + + refund_mentoring_session.delay(1) + + bc.check.calls(logging.Logger.info.call_args_list, [ + call('Starting refund_mentoring_session for mentoring session 1'), + call('Refunding consumption session because it was discounted'), + ]) + bc.check.calls(logging.Logger.error.call_args_list, []) + + assert bc.database.list_of('mentorship.MentorshipSession') == [ + bc.format.to_dict(model.mentorship_session), + ] + + assert bc.database.list_of('payments.ConsumptionSession') == [ + { + **bc.format.to_dict(model.consumption_session), + 'status': 'CANCELLED', + }, + ] + + assert bc.database.list_of('payments.Consumable') == [{ + **bc.format.to_dict(model.consumable), + 'how_many': + how_many_consumables + how_mawy_will_consume, + }] + + assert get_queryset_pks(model.user.groups.all()) == [1, 2, 3] diff --git a/breathecode/payments/tests/tasks/tests_renew_consumables.py b/breathecode/payments/tests/tasks/tests_renew_consumables.py index 5917a8110..8c64666fe 100644 --- a/breathecode/payments/tests/tasks/tests_renew_consumables.py +++ b/breathecode/payments/tests/tasks/tests_renew_consumables.py @@ -19,6 +19,7 @@ def consumable_item(data={}): return { + 'app_service_id': None, 'cohort_set_id': None, 'event_type_set_id': None, 'how_many': -1, diff --git a/breathecode/payments/tests/urls/tests_consumable_checkout.py b/breathecode/payments/tests/urls/tests_consumable_checkout.py index f8a6b0c8f..0c321c5f9 100644 --- a/breathecode/payments/tests/urls/tests_consumable_checkout.py +++ b/breathecode/payments/tests/urls/tests_consumable_checkout.py @@ -648,12 +648,14 @@ def test__x_mentorship_service_set_bought(self): }), ]) self.assertEqual(self.bc.database.list_of('payments.Consumable'), [ - format_consumable_item(data={ - 'mentorship_service_set_id': 1, - 'service_item_id': 1, - 'user_id': 1, - 'how_many': how_many, - }), + format_consumable_item( + data={ + 'mentorship_service_set_id': 1, + 'service_item_id': 1, + 'app_service_id': None, + 'user_id': 1, + 'how_many': how_many, + }), ]) self.assertEqual(self.bc.database.list_of('authenticate.UserSetting'), [ format_user_setting({'lang': 'en'}), @@ -725,12 +727,14 @@ def test__x_event_type_set_bought(self): 'amount': amount, })]) self.assertEqual(self.bc.database.list_of('payments.Consumable'), [ - format_consumable_item(data={ - 'event_type_set_id': 1, - 'service_item_id': 1, - 'user_id': 1, - 'how_many': how_many, - }), + format_consumable_item( + data={ + 'event_type_set_id': 1, + 'service_item_id': 1, + 'app_service_id': None, + 'user_id': 1, + 'how_many': how_many, + }), ]) self.assertEqual(self.bc.database.list_of('authenticate.UserSetting'), [ format_user_setting({'lang': 'en'}), diff --git a/breathecode/provisioning/tests/tasks/tests_upload.py b/breathecode/provisioning/tests/tasks/tests_upload.py index 7fde55bfd..fa1d89ebe 100644 --- a/breathecode/provisioning/tests/tasks/tests_upload.py +++ b/breathecode/provisioning/tests/tasks/tests_upload.py @@ -235,6 +235,16 @@ def github_academy_user_data(data={}): } +def get_last_task_manager_id(bc): + task_manager_cls = bc.database.get_model('commons.TaskManager') + task_manager = task_manager_cls.objects.order_by('-id').first() + + if task_manager is None: + return 0 + + return task_manager.id + + class RandomFileTestSuite(ProvisioningTestCase): # When: random csv is uploaded and the file does not exists # Then: the task should not create any bill or activity @@ -999,6 +1009,8 @@ def test_pagination(self): logging.Logger.info.call_args_list = [] logging.Logger.error.call_args_list = [] + task_manager_id = get_last_task_manager_id(self.bc) + 1 + slug = self.bc.fake.slug() with patch('breathecode.services.google_cloud.File.download', MagicMock(side_effect=csv_file_mock(csv))): @@ -1062,9 +1074,9 @@ def test_pagination(self): self.bc.check.calls(logging.Logger.error.call_args_list, []) self.bc.check.calls(tasks.upload.delay.call_args_list, [ - call(slug, page=1, task_manager_id=1), - call(slug, page=2, task_manager_id=1), - call(slug, page=3, task_manager_id=1), + call(slug, page=1, task_manager_id=task_manager_id), + call(slug, page=2, task_manager_id=task_manager_id), + call(slug, page=3, task_manager_id=task_manager_id), ]) self.bc.check.calls(tasks.calculate_bill_amounts.delay.call_args_list, [call(slug)]) @@ -1652,6 +1664,8 @@ def test_pagination(self): logging.Logger.info.call_args_list = [] logging.Logger.error.call_args_list = [] + task_manager_id = get_last_task_manager_id(self.bc) + 1 + slug = self.bc.fake.slug() with patch('breathecode.services.google_cloud.File.download', MagicMock(side_effect=csv_file_mock(csv))): @@ -1714,9 +1728,9 @@ def test_pagination(self): self.bc.check.calls(logging.Logger.error.call_args_list, []) self.bc.check.calls(tasks.upload.delay.call_args_list, [ - call(slug, page=1, task_manager_id=1), - call(slug, page=2, task_manager_id=1), - call(slug, page=3, task_manager_id=1), + call(slug, page=1, task_manager_id=task_manager_id), + call(slug, page=2, task_manager_id=task_manager_id), + call(slug, page=3, task_manager_id=task_manager_id), ]) self.bc.check.calls(tasks.calculate_bill_amounts.delay.call_args_list, [call(slug)]) diff --git a/breathecode/registry/tests/tasks/tests_async_create_asset_thumbnail.py b/breathecode/registry/tests/tasks/tests_async_create_asset_thumbnail.py index 7165caca9..2611f4a6a 100644 --- a/breathecode/registry/tests/tasks/tests_async_create_asset_thumbnail.py +++ b/breathecode/registry/tests/tasks/tests_async_create_asset_thumbnail.py @@ -51,7 +51,7 @@ def test__without_asset(self): async_create_asset_thumbnail.delay('slug') self.assertEqual(self.bc.database.list_of('media.Media'), []) - self.assertEqual(Logger.warn.call_args_list, [call('Asset with slug slug not found')]) + self.assertEqual(Logger.warn.call_args_list, []) self.assertEqual(Logger.error.call_args_list, [call('Asset with slug slug not found', exc_info=True)]) """ diff --git a/breathecode/registry/tests/urls/tests_academy_asset.py b/breathecode/registry/tests/urls/tests_academy_asset.py index bacf22d10..157132f57 100644 --- a/breathecode/registry/tests/urls/tests_academy_asset.py +++ b/breathecode/registry/tests/urls/tests_academy_asset.py @@ -116,6 +116,7 @@ def post_serializer(academy, category, data={}): 'url': None, 'visibility': 'PUBLIC', 'with_solutions': False, + 'assets_related': [], 'with_video': False, **data, } @@ -186,7 +187,7 @@ def test__without_auth(self): 'detail': 'Authentication credentials were not provided.', 'status_code': status.HTTP_401_UNAUTHORIZED }) - self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + assert response.status_code == status.HTTP_401_UNAUTHORIZED self.assertEqual(self.bc.database.list_of('registry.Asset'), []) def test__without_capability(self): @@ -201,8 +202,8 @@ def test__without_capability(self): 'detail': "You (user: 1) don't have this capability: read_asset for academy 1" } - self.assertEqual(json, expected) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + assert json == expected + assert response.status_code == status.HTTP_403_FORBIDDEN self.assertEqual(self.bc.database.list_of('registry.Asset'), []) def test__post__without_category(self): @@ -220,8 +221,8 @@ def test__post__without_category(self): 'status_code': 400, } - self.assertEqual(json, expected) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + assert json == expected + assert response.status_code == status.HTTP_400_BAD_REQUEST self.assertEqual(self.bc.database.list_of('registry.Asset'), []) @patch('breathecode.registry.tasks.async_pull_from_github.delay', MagicMock()) @@ -252,8 +253,8 @@ def test__post__with__all__mandatory__properties(self): del data['category'] expected = post_serializer(model.academy, model.asset_category, data=data) - self.assertEqual(json, expected) - self.assertEqual(response.status_code, status.HTTP_201_CREATED) + assert json == expected + assert response.status_code == status.HTTP_201_CREATED self.assertEqual(tasks.async_pull_from_github.delay.call_args_list, [call('model_slug')]) self.assertEqual(self.bc.database.list_of('registry.Asset'), [database_item(model.academy, model.asset_category, data)]) @@ -283,8 +284,8 @@ def test_asset__put_many_without_id(self): expected = {'detail': 'without-id', 'status_code': 400} - self.assertEqual(json, expected) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + assert json == expected + assert response.status_code == status.HTTP_400_BAD_REQUEST def test_asset__put_many_with_wrong_id(self): """Test Asset bulk update""" @@ -312,8 +313,8 @@ def test_asset__put_many_with_wrong_id(self): expected = {'detail': 'not-found', 'status_code': 404} - self.assertEqual(json, expected) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + assert json == expected + assert response.status_code == status.HTTP_404_NOT_FOUND def test_asset__put_many(self): """Test Asset bulk update""" @@ -358,8 +359,8 @@ def test_asset__put_many(self): put_serializer(model.academy, model.asset_category, asset) for i, asset in enumerate(model.asset) ] - self.assertEqual(json, expected) - self.assertEqual(response.status_code, status.HTTP_200_OK) + assert json == expected + assert response.status_code == status.HTTP_200_OK @patch('django.utils.timezone.now', MagicMock(return_value=UTC_NOW)) def test_asset__put_many_with_test_status_ok(self): @@ -414,8 +415,8 @@ def test_asset__put_many_with_test_status_ok(self): }) ] - self.assertEqual(json, expected) - self.assertEqual(response.status_code, status.HTTP_200_OK) + assert json == expected + assert response.status_code == status.HTTP_200_OK @patch('django.utils.timezone.now', MagicMock(return_value=UTC_NOW)) def test_asset__put_many_with_test_status_warning(self): @@ -470,8 +471,8 @@ def test_asset__put_many_with_test_status_warning(self): }) ] - self.assertEqual(json, expected) - self.assertEqual(response.status_code, status.HTTP_200_OK) + assert json == expected + assert response.status_code == status.HTTP_200_OK @patch('django.utils.timezone.now', MagicMock(return_value=UTC_NOW)) def test_asset__put_many_with_test_status_pending(self): @@ -516,8 +517,8 @@ def test_asset__put_many_with_test_status_pending(self): 'status_code': 400 } - self.assertEqual(json, expected) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + assert json == expected + assert response.status_code == status.HTTP_400_BAD_REQUEST @patch('django.utils.timezone.now', MagicMock(return_value=UTC_NOW)) def test_asset__put_many_with_test_status_error(self): @@ -562,8 +563,8 @@ def test_asset__put_many_with_test_status_error(self): 'status_code': 400 } - self.assertEqual(json, expected) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + assert json == expected + assert response.status_code == status.HTTP_400_BAD_REQUEST @patch('django.utils.timezone.now', MagicMock(return_value=UTC_NOW)) def test_asset__put_many_with_test_status_Needs_Resync(self): @@ -608,5 +609,5 @@ def test_asset__put_many_with_test_status_Needs_Resync(self): 'status_code': 400 } - self.assertEqual(json, expected) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + assert json == expected + assert response.status_code == status.HTTP_400_BAD_REQUEST diff --git a/breathecode/tests/mixins/generate_models_mixin/payments_models_mixin.py b/breathecode/tests/mixins/generate_models_mixin/payments_models_mixin.py index 28f7a9064..a2644be5e 100644 --- a/breathecode/tests/mixins/generate_models_mixin/payments_models_mixin.py +++ b/breathecode/tests/mixins/generate_models_mixin/payments_models_mixin.py @@ -41,6 +41,7 @@ def generate_payments_models(self, provisioning_price=False, cohort_set=False, cohort_set_translation=False, + app_service=False, models={}, **kwargs): """Generate models""" @@ -396,6 +397,14 @@ def generate_payments_models(self, models['subscription_service_item'] = create_models(subscription_service_item, 'payments.SubscriptionServiceItem', **kargs) + if not 'app_service' in models and is_valid(app_service): + kargs = {} + + if 'app' in models: + kargs['app'] = just_one(models['app']) + + models['app_service'] = create_models(app_service, 'payments.AppService', **kargs) + if not 'consumable' in models and (is_valid(consumable) or is_valid(consumption_session)): kargs = {} @@ -414,6 +423,9 @@ def generate_payments_models(self, if 'event_type_set' in models: kargs['event_type_set'] = just_one(models['event_type_set']) + if 'app_service' in models: + kargs['app_service'] = just_one(models['app_service']) + models['consumable'] = create_models(consumable, 'payments.Consumable', **kargs) if not 'consumption_session' in models and is_valid(consumption_session): diff --git a/conftest.py b/conftest.py index 7bac4eed1..d99a835e7 100644 --- a/conftest.py +++ b/conftest.py @@ -54,6 +54,15 @@ def wrapper(num): yield wrapper +@pytest.fixture +def get_int(): + + def wrapper(min=0, max=1000): + return random.randint(min, max) + + yield wrapper + + @pytest.fixture def get_kwargs(fake): @@ -260,20 +269,39 @@ def enable_signals(monkeypatch, signals): monkeypatch.setattr(ModelSignal, 'send', lambda *args, **kwargs: None) monkeypatch.setattr(ModelSignal, 'send_robust', lambda *args, **kwargs: None) - #TODO: get a list of signals that will be enabled - def enable(*to_enable): + def enable(*to_enable, debug=False): monkeypatch.setattr(Signal, 'send', original_signal_send) monkeypatch.setattr(Signal, 'send_robust', original_signal_send_robust) monkeypatch.setattr(ModelSignal, 'send', original_model_signal_send) monkeypatch.setattr(ModelSignal, 'send_robust', original_model_signal_send_robust) - if to_enable: + if to_enable or debug: to_disable = [x for x in signals if x not in to_enable] for signal in to_disable: - monkeypatch.setattr(f'{signal}.send', lambda *args, **kwargs: None) - monkeypatch.setattr(f'{signal}.send_robust', lambda *args, **kwargs: None) + + def apply_mock(module): + + def send_mock(*args, **kwargs): + if debug: + print(module) + try: + print(' args\n ', args) + except Exception: + pass + + try: + print(' kwargs\n ', kwargs) + except Exception: + pass + + print('\n') + + monkeypatch.setattr(module, send_mock) + + apply_mock(f'{signal}.send') + apply_mock(f'{signal}.send_robust') yield enable @@ -371,3 +399,12 @@ def wrapper(size): @pytest.fixture(scope='module') def fake(): return _fake + + +@pytest.fixture() +def get_queryset_pks(): + + def wrapper(queryset): + return [x.pk for x in queryset] + + yield wrapper From e87780f140945e666c69f8dac41dbae182ffb89e Mon Sep 17 00:00:00 2001 From: jefer94 Date: Sat, 16 Dec 2023 01:47:26 -0500 Subject: [PATCH 2/5] add migration --- .../migrations/0041_auto_20231216_0647.py | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 breathecode/payments/migrations/0041_auto_20231216_0647.py diff --git a/breathecode/payments/migrations/0041_auto_20231216_0647.py b/breathecode/payments/migrations/0041_auto_20231216_0647.py new file mode 100644 index 000000000..b41b721d1 --- /dev/null +++ b/breathecode/payments/migrations/0041_auto_20231216_0647.py @@ -0,0 +1,85 @@ +# Generated by Django 3.2.23 on 2023-12-16 06:47 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('authenticate', '0048_auto_20231128_1224'), + ('payments', '0040_alter_serviceitem_is_renewable'), + ] + + operations = [ + migrations.AlterField( + model_name='consumable', + name='cohort_set', + field=models.ForeignKey(blank=True, + default=None, + help_text='Cohort set which the consumable belongs to', + null=True, + on_delete=django.db.models.deletion.CASCADE, + to='payments.cohortset'), + ), + migrations.AlterField( + model_name='consumable', + name='event_type_set', + field=models.ForeignKey(blank=True, + default=None, + help_text='Event type set which the consumable belongs to', + null=True, + on_delete=django.db.models.deletion.CASCADE, + to='payments.eventtypeset'), + ), + migrations.AlterField( + model_name='consumable', + name='mentorship_service_set', + field=models.ForeignKey(blank=True, + default=None, + help_text='Mentorship service set which the consumable belongs to', + null=True, + on_delete=django.db.models.deletion.CASCADE, + to='payments.mentorshipserviceset'), + ), + migrations.AlterField( + model_name='service', + name='type', + field=models.CharField(choices=[('COHORT_SET', 'Cohort set'), + ('MENTORSHIP_SERVICE_SET', 'Mentorship service set'), + ('EVENT_TYPE_SET', 'Event type set'), + ('CHAT_SUPPORT', 'Chat support'), ('CODE_REVIEW', 'Code review'), + ('AI_INTERACTION', 'AI interaction'), + ('LEARNPACK_BUILD', 'Learnpack build'), + ('LEARNPACK_TEST', 'Learnpack test')], + default='COHORT_SET', + help_text='Service type', + max_length=22), + ), + migrations.CreateModel( + name='AppService', + fields=[ + ('id', + models.BigAutoField(auto_created=True, primary_key=True, serialize=False, + verbose_name='ID')), + ('service', models.SlugField(help_text='Microservice slug')), + ('app', + models.ForeignKey(blank=True, + default=None, + help_text='Subscription', + null=True, + on_delete=django.db.models.deletion.CASCADE, + to='authenticate.app')), + ], + ), + migrations.AddField( + model_name='consumable', + name='app_service', + field=models.ForeignKey(blank=True, + default=None, + help_text='App service which the consumable belongs to', + null=True, + on_delete=django.db.models.deletion.CASCADE, + to='payments.appservice'), + ), + ] From 5a31e2f562dcfe8c2dfabd6811fa6d2b4fa46d2c Mon Sep 17 00:00:00 2001 From: jefer94 Date: Mon, 18 Dec 2023 13:57:44 -0500 Subject: [PATCH 3/5] add pagination 2.0 and fix tests --- breathecode/authenticate/receivers.py | 41 +- breathecode/authenticate/signals.py | 7 + .../tests/urls/tests_academy_student.py | 42 +- .../tests/urls/tests_authorize_slug.py | 93 +- breathecode/events/models.py | 5 +- .../tests_build_live_classes_from_timeslot.py | 60 +- .../marketing/tests/urls/tests_lead.py | 31 +- breathecode/registry/receivers.py | 4 +- .../tests/urls/tests_academy_asset.py | 865 +++++++++--------- breathecode/registry/views.py | 2 +- breathecode/settings.py | 3 + .../registry_models_mixin.py | 4 +- .../api_view_extension_handlers.py | 12 +- .../api_view_extensions/extension_base.py | 5 +- .../extensions/pagination_extension.py | 48 +- .../tests_api_view_extensions.py | 49 +- 16 files changed, 703 insertions(+), 568 deletions(-) diff --git a/breathecode/authenticate/receivers.py b/breathecode/authenticate/receivers.py index cefa94185..3634e8b6c 100644 --- a/breathecode/authenticate/receivers.py +++ b/breathecode/authenticate/receivers.py @@ -6,7 +6,8 @@ from django.db.models.signals import post_delete, post_save, pre_delete from breathecode.admissions.signals import student_edu_status_updated from breathecode.admissions.models import CohortUser -from breathecode.authenticate.signals import invite_status_updated +from breathecode.authenticate.signals import (invite_status_updated, user_info_updated, user_info_deleted, + app_scope_updated, cohort_user_deleted) from breathecode.authenticate.models import UserInvite from django.dispatch import receiver from .tasks import async_remove_from_organization, async_add_to_organization @@ -19,6 +20,12 @@ @receiver(post_save) +def update_user_group(sender, instance, created: bool, **_): + # redirect to other signal to be able to mock it + user_info_updated.send(sender=sender, instance=instance, created=created) + + +@receiver(user_info_updated) def set_user_group(sender, instance, created: bool, **_): group = None groups = None @@ -54,6 +61,12 @@ def set_user_group(sender, instance, created: bool, **_): @receiver(post_delete) +def delete_user_group(sender, instance, **_): + # redirect to other signal to be able to mock it + user_info_deleted.send(sender=sender, instance=instance) + + +@receiver(user_info_deleted) def unset_user_group(sender, instance, **_): should_be_deleted = False group = None @@ -84,6 +97,11 @@ def unset_user_group(sender, instance, **_): @receiver(pre_delete, sender=CohortUser) +def delete_cohort_user(sender, instance, **_): + cohort_user_deleted.send(sender=sender, instance=instance) + + +@receiver(cohort_user_deleted, sender=CohortUser) def post_delete_cohort_user(sender, instance, **_): # never ending cohorts cannot be in synch with github @@ -114,30 +132,27 @@ def post_save_cohort_user(sender, instance, **_): @receiver(post_save, sender=AppRequiredScope) def increment_on_update_required_scope(sender: Type[AppRequiredScope], instance: AppRequiredScope, **_): - if AppUserAgreement.objects.filter(app=instance.app, - agreement_version=instance.app.agreement_version).exists(): - instance.app.agreement_version += 1 - instance.app.save() + app_scope_updated.send(sender=sender, instance=instance) @receiver(post_save, sender=AppOptionalScope) def increment_on_update_optional_scope(sender: Type[AppOptionalScope], instance: AppOptionalScope, **_): - if AppUserAgreement.objects.filter(app=instance.app, - agreement_version=instance.app.agreement_version).exists(): - instance.app.agreement_version += 1 - instance.app.save() + app_scope_updated.send(sender=sender, instance=instance) @receiver(pre_delete, sender=AppRequiredScope) def increment_on_delete_required_scope(sender: Type[AppRequiredScope], instance: AppRequiredScope, **_): - if AppUserAgreement.objects.filter(app=instance.app, - agreement_version=instance.app.agreement_version).exists(): - instance.app.agreement_version += 1 - instance.app.save() + app_scope_updated.send(sender=sender, instance=instance) @receiver(pre_delete, sender=AppOptionalScope) def increment_on_delete_optional_scope(sender: Type[AppOptionalScope], instance: AppOptionalScope, **_): + app_scope_updated.send(sender=sender, instance=instance) + + +@receiver(app_scope_updated) +def update_app_scope(sender: Type[AppOptionalScope | AppRequiredScope], + instance: AppOptionalScope | AppRequiredScope, **_): if AppUserAgreement.objects.filter(app=instance.app, agreement_version=instance.app.agreement_version).exists(): instance.app.agreement_version += 1 diff --git a/breathecode/authenticate/signals.py b/breathecode/authenticate/signals.py index e833c3c3b..8bbf14e83 100644 --- a/breathecode/authenticate/signals.py +++ b/breathecode/authenticate/signals.py @@ -5,3 +5,10 @@ # ProfileAcademy accepted academy_invite_accepted = dispatch.Signal() profile_academy_saved = dispatch.Signal() + +# post_delete and post_save for User, ProfileAcademy and MentorProfileMentorProfile +user_info_updated = dispatch.Signal() +user_info_deleted = dispatch.Signal() + +app_scope_updated = dispatch.Signal() +cohort_user_deleted = dispatch.Signal() diff --git a/breathecode/authenticate/tests/urls/tests_academy_student.py b/breathecode/authenticate/tests/urls/tests_academy_student.py index b549f5afa..5d29317b7 100644 --- a/breathecode/authenticate/tests/urls/tests_academy_student.py +++ b/breathecode/authenticate/tests/urls/tests_academy_student.py @@ -754,7 +754,7 @@ def test_academy_student__post__no_user__invite_is_false(self): 'user_id': 1, }]) - self.assertEqual(actions.send_email_message.call_args_list, []) + assert actions.send_email_message.call_args_list == [] self.assertEqual(self.bc.database.list_of('payments.Plan'), []) @patch('breathecode.notify.actions.send_email_message', MagicMock()) @@ -787,7 +787,7 @@ def test_academy_student__post__no_invite(self): 'user_id': 1, }]) - self.assertEqual(actions.send_email_message.call_args_list, []) + assert actions.send_email_message.call_args_list == [] self.assertEqual(self.bc.database.list_of('payments.Plan'), []) @patch('breathecode.notify.actions.send_email_message', MagicMock()) @@ -818,7 +818,7 @@ def test_academy_student__post__exists_profile_academy_with_this_email__is_none( self.assertEqual(self.bc.database.list_of('authenticate.ProfileAcademy'), [self.bc.format.to_dict(model.profile_academy)]) - self.assertEqual(actions.send_email_message.call_args_list, []) + assert actions.send_email_message.call_args_list == [] self.assertEqual(self.bc.database.list_of('payments.Plan'), []) @patch('breathecode.notify.actions.send_email_message', MagicMock()) @@ -849,7 +849,7 @@ def test_academy_student__post__exists_profile_academy_with_this_email__with_ema self.assertEqual(self.bc.database.list_of('authenticate.ProfileAcademy'), [self.bc.format.to_dict(model.profile_academy)]) - self.assertEqual(actions.send_email_message.call_args_list, []) + assert actions.send_email_message.call_args_list == [] self.assertEqual(self.bc.database.list_of('payments.Plan'), []) @patch('breathecode.notify.actions.send_email_message', MagicMock()) @@ -882,7 +882,7 @@ def test_academy_student__post__user_with_not_student_role(self): 'user_id': 1, }]) - self.assertEqual(actions.send_email_message.call_args_list, []) + assert actions.send_email_message.call_args_list == [] self.assertEqual(self.bc.database.list_of('payments.Plan'), []) """ @@ -920,7 +920,7 @@ def test_academy_student__post__without_role_student(self): ]) self.assertEqual(self.bc.database.list_of('authenticate.UserInvite'), []) - self.assertEqual(actions.send_email_message.call_args_list, []) + assert actions.send_email_message.call_args_list == [] self.assertEqual(self.bc.database.list_of('payments.Plan'), []) """ @@ -960,7 +960,7 @@ def test_academy_student__post__with_cohort_in_body(self): ]) self.assertEqual(self.bc.database.list_of('authenticate.UserInvite'), []) - self.assertEqual(actions.send_email_message.call_args_list, []) + assert actions.send_email_message.call_args_list == [] self.assertEqual(self.bc.database.list_of('payments.Plan'), []) """ @@ -999,7 +999,7 @@ def test_academy_student__post__with_user_but_not_found(self): ]) self.assertEqual(self.bc.database.list_of('authenticate.UserInvite'), []) - self.assertEqual(actions.send_email_message.call_args_list, []) + assert actions.send_email_message.call_args_list == [] self.assertEqual(self.bc.database.list_of('payments.Plan'), []) """ @@ -1064,7 +1064,7 @@ def test_academy_student__post__with_user_and_cohort_in_data(self): url = os.getenv('API_URL') + '/v1/auth/academy/html/invite?' + querystr self.assertEqual(self.bc.database.list_of('authenticate.UserInvite'), []) - self.assertEqual(actions.send_email_message.call_args_list, [ + assert actions.send_email_message.call_args_list == [ call( 'academy_invite', model.user[1].email, { 'subject': @@ -1091,7 +1091,7 @@ def test_academy_student__post__with_user_and_cohort_in_data(self): 'LINK': url, }), - ]) + ] self.assertEqual(self.bc.database.list_of('payments.Plan'), []) """ @@ -1157,7 +1157,7 @@ def test_academy_student__post__with_user__it_ignore_the_param_plans(self): url = os.getenv('API_URL') + '/v1/auth/academy/html/invite?' + querystr self.assertEqual(self.bc.database.list_of('authenticate.UserInvite'), []) - self.assertEqual(actions.send_email_message.call_args_list, [ + assert actions.send_email_message.call_args_list == [ call( 'academy_invite', model.user[1].email, { 'subject': @@ -1184,7 +1184,7 @@ def test_academy_student__post__with_user__it_ignore_the_param_plans(self): 'LINK': url, }), - ]) + ] self.assertEqual(self.bc.database.list_of('payments.Plan'), []) """ @@ -1258,14 +1258,14 @@ def test_academy_student__post__without_user_in_data(self): 'syllabus_id': None, }), ]) - self.assertEqual(actions.send_email_message.call_args_list, [ + assert actions.send_email_message.call_args_list == [ call('welcome_academy', 'dude@dude.dude', { 'email': 'dude@dude.dude', 'subject': 'Welcome to 4Geeks.com', 'LINK': url, 'FIST_NAME': 'Kenny' }) - ]) + ] self.assertEqual(self.bc.database.list_of('payments.Plan'), []) """ @@ -1310,7 +1310,7 @@ def test_academy_student__post__without_user_in_data__plan_not_found(self): str(TOKEN) + '?' + querystr self.assertEqual(self.bc.database.list_of('authenticate.UserInvite'), []) - self.assertEqual(actions.send_email_message.call_args_list, []) + assert actions.send_email_message.call_args_list == [] self.assertEqual(self.bc.database.list_of('payments.Plan'), []) """ @@ -1391,14 +1391,14 @@ def test_academy_student__post__without_user_in_data__with_plan(self): 'longitude': None, }), ]) - self.assertEqual(actions.send_email_message.call_args_list, [ + assert actions.send_email_message.call_args_list == [ call('welcome_academy', 'dude@dude.dude', { 'email': 'dude@dude.dude', 'subject': 'Welcome to 4Geeks.com', 'LINK': url, 'FIST_NAME': 'Kenny' }) - ]) + ] self.assertEqual(self.bc.database.list_of('payments.Plan'), [ self.bc.format.to_dict(model.plan), ]) @@ -1449,7 +1449,7 @@ def test_academy_student__post__without_user_in_data__invite_already_exists__coh self.bc.format.to_dict(model.user_invite), ]) - self.assertEqual(actions.send_email_message.call_args_list, []) + assert actions.send_email_message.call_args_list == [] @patch('breathecode.notify.actions.send_email_message', MagicMock()) @patch('random.getrandbits', MagicMock(side_effect=getrandbits)) @@ -1535,14 +1535,14 @@ def test_academy_student__post__without_user_in_data__invite_already_exists__dif }), ]) - self.assertEqual(actions.send_email_message.call_args_list, [ + assert actions.send_email_message.call_args_list == [ call('welcome_academy', 'dude2@dude.dude', { 'email': 'dude2@dude.dude', 'subject': 'Welcome to 4Geeks.com', 'LINK': url, 'FIST_NAME': 'Kenny' }) - ]) + ] self.assertEqual(self.bc.database.list_of('payments.Plan'), []) @patch('breathecode.notify.actions.send_email_message', MagicMock()) @@ -1587,7 +1587,7 @@ def test_academy_student__post__without_user_in_data__user_already_exists(self): self.bc.format.to_dict(model.user_invite), ]) - self.assertEqual(actions.send_email_message.call_args_list, []) + assert actions.send_email_message.call_args_list == [] self.assertEqual(self.bc.database.list_of('payments.Plan'), []) diff --git a/breathecode/authenticate/tests/urls/tests_authorize_slug.py b/breathecode/authenticate/tests/urls/tests_authorize_slug.py index 4ece3bb0a..997ae27a6 100644 --- a/breathecode/authenticate/tests/urls/tests_authorize_slug.py +++ b/breathecode/authenticate/tests/urls/tests_authorize_slug.py @@ -80,6 +80,27 @@ def render_authorization(app, required_scopes=[], optional_scopes=[], selected_s }, request) +def fix_data(apps=[], scopes=[]): + + def fixer(s): + for word in ['permissions', 'required', 'optional', 'checked', 'New', 'new']: + s = s.replace(word, 'x') + + return s + + for app in apps: + app.name = fixer(app.name) + app.slug = fixer(app.slug) + app.description = fixer(app.description) + app.save() + + for scope in scopes: + scope.name = fixer(scope.name) + scope.slug = fixer(scope.slug) + scope.description = fixer(scope.description) + scope.save() + + class GetTestSuite(AuthTestCase): # When: no auth # Then: return 302 @@ -126,6 +147,8 @@ def test_app_does_not_require_an_agreement(self): app = {'require_an_agreement': False} model = self.bc.database.create(user=1, token=1, app=app) + fix_data([model.app]) + querystring = self.bc.format.to_querystring({'token': model.token.key}) url = reverse_lazy('authenticate:authorize_slug', kwargs={'app_slug': model.app.slug }) + f'?{querystring}' @@ -155,6 +178,8 @@ def test_app_require_an_agreement(self): app = {'require_an_agreement': True} model = self.bc.database.create(user=1, token=1, app=app) + fix_data([model.app]) + querystring = self.bc.format.to_querystring({'token': model.token.key}) url = reverse_lazy('authenticate:authorize_slug', kwargs={'app_slug': model.app.slug }) + f'?{querystring}' @@ -177,11 +202,19 @@ def test_app_require_an_agreement(self): self.bc.format.to_dict(model.app), ]) - self.assertTrue('permissions' not in content) - self.assertTrue('required' not in content) - self.assertTrue('optional' not in content) - self.assertTrue(content.count('checked') == 0) - self.assertTrue(content.count('New') == 0) + try: + self.assertTrue('permissions' not in content) + self.assertTrue('required' not in content) + self.assertTrue('optional' not in content) + self.assertTrue(content.count('checked') == 0) + self.assertTrue(content.count('New') == 0) + except Exception as e: + with open('content.html', 'w') as f: + f.write(content) + + with open('expected.html', 'w') as f: + f.write(expected) + raise e # When: app require an agreement, with scopes # Then: return 200 @@ -210,6 +243,8 @@ def test_app_require_an_agreement__with_scopes(self): app_required_scope=app_required_scopes, app_optional_scope=app_optional_scopes) + fix_data([model.app], model.scope) + querystring = self.bc.format.to_querystring({'token': model.token.key}) url = reverse_lazy('authenticate:authorize_slug', kwargs={'app_slug': model.app.slug }) + f'?{querystring}' @@ -235,11 +270,19 @@ def test_app_require_an_agreement__with_scopes(self): self.bc.format.to_dict(model.app), ]) - self.assertTrue('permissions' in content) - self.assertTrue('required' in content) - self.assertTrue('optional' in content) - self.assertTrue(content.count('checked') == 4) - self.assertTrue(content.count('New') == 0) + try: + self.assertTrue('permissions' in content) + self.assertTrue('required' in content) + self.assertTrue('optional' in content) + self.assertTrue(content.count('checked') == 4) + self.assertTrue(content.count('New') == 0) + except Exception as e: + with open('content.html', 'w') as f: + f.write(content) + + with open('expected.html', 'w') as f: + f.write(expected) + raise e # When: app require an agreement, with scopes, it requires update the agreement # Then: return 200 @@ -274,6 +317,8 @@ def test_app_require_an_agreement__with_scopes__updating_agreement(self): app_required_scope=app_required_scopes, app_optional_scope=app_optional_scopes) + fix_data([model.app], model.scope) + querystring = self.bc.format.to_querystring({'token': model.token.key}) url = reverse_lazy('authenticate:authorize_slug', kwargs={'app_slug': model.app.slug }) + f'?{querystring}' @@ -300,11 +345,19 @@ def test_app_require_an_agreement__with_scopes__updating_agreement(self): self.bc.format.to_dict(model.app), ]) - self.assertTrue('permissions' in content) - self.assertTrue('required' in content) - self.assertTrue('optional' in content) - self.assertTrue(content.count('checked') == 3) - self.assertTrue(content.count('New') == 0) + try: + self.assertTrue('permissions' in content) + self.assertTrue('required' in content) + self.assertTrue('optional' in content) + self.assertTrue(content.count('checked') == 3) + self.assertTrue(content.count('New') == 0) + except Exception as e: + with open('content.html', 'w') as f: + f.write(content) + + with open('expected.html', 'w') as f: + f.write(expected) + raise e # When: app require an agreement, with scopes, it requires update the agreement # Then: return 200 @@ -339,6 +392,8 @@ def test_app_require_an_agreement__with_scopes__updating_agreement____(self): app_required_scope=app_required_scopes, app_optional_scope=app_optional_scopes) + fix_data([model.app], model.scope) + querystring = self.bc.format.to_querystring({'token': model.token.key}) url = reverse_lazy('authenticate:authorize_slug', kwargs={'app_slug': model.app.slug }) + f'?{querystring}' @@ -418,6 +473,8 @@ def test_app_does_not_require_an_agreement(self): app = {'require_an_agreement': False, 'agreement_version': 1} model = self.bc.database.create(user=1, token=1, app=app) + fix_data([model.app]) + querystring = self.bc.format.to_querystring({'token': model.token.key}) url = reverse_lazy('authenticate:authorize_slug', kwargs={'app_slug': model.app.slug }) + f'?{querystring}' @@ -471,6 +528,8 @@ def test_user_without_agreement(self): app_required_scope=app_required_scopes, app_optional_scope=app_optional_scopes) + fix_data([model.app], model.scope) + querystring = self.bc.format.to_querystring({'token': model.token.key}) url = reverse_lazy('authenticate:authorize_slug', kwargs={'app_slug': model.app.slug }) + f'?{querystring}' @@ -539,6 +598,8 @@ def test_user_with_agreement__scopes_not_changed(self): app_optional_scope=app_optional_scopes, app_user_agreement=app_user_agreement) + fix_data([model.app], model.scope) + querystring = self.bc.format.to_querystring({'token': model.token.key}) url = reverse_lazy('authenticate:authorize_slug', kwargs={'app_slug': model.app.slug }) + f'?{querystring}' @@ -606,6 +667,8 @@ def test_user_with_agreement__scopes_changed(self): app_user_agreement=app_user_agreement, optional_scope_set=optional_scope_set) + fix_data([model.app], model.scope) + querystring = self.bc.format.to_querystring({'token': model.token.key}) url = reverse_lazy('authenticate:authorize_slug', kwargs={'app_slug': model.app.slug }) + f'?{querystring}' diff --git a/breathecode/events/models.py b/breathecode/events/models.py index 696079213..15f186ac9 100644 --- a/breathecode/events/models.py +++ b/breathecode/events/models.py @@ -396,8 +396,11 @@ class LiveClass(models.Model): created_at = models.DateTimeField(auto_now_add=True, editable=False) updated_at = models.DateTimeField(auto_now=True, editable=False) + def _get_hash(self): + return binascii.hexlify(os.urandom(20)).decode() + def save(self, *args, **kwargs): if not self.pk: - self.hash = binascii.hexlify(os.urandom(20)).decode() + self.hash = self._get_hash() return super().save(*args, **kwargs) diff --git a/breathecode/events/tests/tasks/tests_build_live_classes_from_timeslot.py b/breathecode/events/tests/tasks/tests_build_live_classes_from_timeslot.py index 2acb03f71..e0dfad10f 100644 --- a/breathecode/events/tests/tasks/tests_build_live_classes_from_timeslot.py +++ b/breathecode/events/tests/tasks/tests_build_live_classes_from_timeslot.py @@ -1,16 +1,14 @@ -import binascii from datetime import timedelta, datetime import logging -import random import os import pytz from unittest.mock import MagicMock, call, patch from breathecode.events.tasks import build_live_classes_from_timeslot from ..mixins.new_events_tests_case import EventTestCase -from ...signals import event_saved import breathecode.events.actions as actions from django.utils import timezone +from breathecode.events.models import LiveClass UTC_NOW = timezone.now() DATE = datetime(year=2022, month=12, day=30, hour=9, minute=24, second=0, microsecond=0, tzinfo=pytz.UTC) @@ -115,15 +113,15 @@ def test_one_cohort_time_slot_with_ending_date_in_the_past(self): @patch.object(logging.Logger, 'error', MagicMock()) @patch.object(logging.Logger, 'debug', MagicMock()) @patch('django.utils.timezone.now', MagicMock(return_value=DATE)) - @patch('os.urandom', MagicMock(return_value=URANDOM)) - @patch('binascii.hexlify', MagicMock(side_effect=[ - b'r1', - b'r2', - b'r3', - b'r4', - b'r5', - b'r6', - ])) + @patch('breathecode.events.models.LiveClass._get_hash', + MagicMock(side_effect=[ + 'r1', + 'r2', + 'r3', + 'r4', + 'r5', + 'r6', + ])) def test_one_cohort_time_slot_with_ending_date_in_the_future__weekly(self): base_date = DATE cohort = { @@ -206,8 +204,7 @@ def test_one_cohort_time_slot_with_ending_date_in_the_future__weekly(self): 'remote_meeting_url': model.cohort.online_meeting_url, }), ]) - self.assertEqual(os.urandom.call_args_list, [call(20) for _ in range(6)]) - self.assertEqual(binascii.hexlify.call_args_list, [call(URANDOM) for _ in range(6)]) + assert LiveClass._get_hash.call_args_list == [call() for _ in range(6)] """ 🔽🔽🔽 with 1 CohortTimeSlot, Cohort with ending_date in the future, it's weekly @@ -217,15 +214,15 @@ def test_one_cohort_time_slot_with_ending_date_in_the_future__weekly(self): @patch.object(logging.Logger, 'error', MagicMock()) @patch.object(logging.Logger, 'debug', MagicMock()) @patch('django.utils.timezone.now', MagicMock(return_value=DATE)) - @patch('os.urandom', MagicMock(return_value=URANDOM)) - @patch('binascii.hexlify', MagicMock(side_effect=[ - b'r1', - b'r2', - b'r3', - b'r4', - b'r5', - b'r6', - ])) + @patch('breathecode.events.models.LiveClass._get_hash', + MagicMock(side_effect=[ + 'r1', + 'r2', + 'r3', + 'r4', + 'r5', + 'r6', + ])) def test_one_cohort_time_slot_with_ending_date_in_the_future__monthly(self): base_date = DATE cohort = { @@ -268,8 +265,7 @@ def test_one_cohort_time_slot_with_ending_date_in_the_future__monthly(self): 'remote_meeting_url': model.cohort.online_meeting_url, }), ]) - self.assertEqual(os.urandom.call_args_list, [call(20) for _ in range(1)]) - self.assertEqual(binascii.hexlify.call_args_list, [call(URANDOM) for _ in range(1)]) + assert LiveClass._get_hash.call_args_list == [call()] """ 🔽🔽🔽 with 1 CohortTimeSlot, Cohort with ending_date in the future, it's daily @@ -279,8 +275,15 @@ def test_one_cohort_time_slot_with_ending_date_in_the_future__monthly(self): @patch.object(logging.Logger, 'error', MagicMock()) @patch.object(logging.Logger, 'debug', MagicMock()) @patch('django.utils.timezone.now', MagicMock(return_value=DATE)) - @patch('os.urandom', MagicMock(return_value=URANDOM)) - @patch('binascii.hexlify', MagicMock(side_effect=[bytes(f'r{n}', 'utf-8') for n in range(1, 7)])) + @patch('breathecode.events.models.LiveClass._get_hash', + MagicMock(side_effect=[ + 'r1', + 'r2', + 'r3', + 'r4', + 'r5', + 'r6', + ])) def test_one_cohort_time_slot_with_ending_date_in_the_future__daily(self): base_date = DATE cohort = { @@ -362,5 +365,4 @@ def test_one_cohort_time_slot_with_ending_date_in_the_future__daily(self): 'remote_meeting_url': model.cohort.online_meeting_url, }), ]) - self.assertEqual(os.urandom.call_args_list, [call(20) for _ in range(6)]) - self.assertEqual(binascii.hexlify.call_args_list, [call(URANDOM) for _ in range(6)]) + assert LiveClass._get_hash.call_args_list == [call() for _ in range(6)] diff --git a/breathecode/marketing/tests/urls/tests_lead.py b/breathecode/marketing/tests/urls/tests_lead.py index c9f20a66f..fa2dba031 100644 --- a/breathecode/marketing/tests/urls/tests_lead.py +++ b/breathecode/marketing/tests/urls/tests_lead.py @@ -313,22 +313,20 @@ def test_lead__with__data(bc: Breathecode, client: APIClient): # When: Passing slug of Academy or AcademyAlias -@pytest.mark.parametrize( - 'academy,academy_alias,academy_id', - [ - ({ - 'slug': 'midgard' - }, None, None), - ({ - 'slug': 'midgard' - }, 1, None), # - (1, { - 'active_campaign_slug': 'midgard' - }, 1), - ]) +@pytest.mark.parametrize('academy,academy_alias,academy_id', [ + ({ + 'slug': 'midgard' + }, None, None), + ({ + 'slug': 'midgard' + }, 1, None), + (1, { + 'active_campaign_slug': 'midgard' + }, 1), +]) def test_passing_slug_of_academy_or_academy_alias(bc: Breathecode, client: APIClient, academy, academy_alias, academy_id): - model = bc.database.create(academy=academy, academy_alias=academy_alias) + model = bc.database.create(academy=academy, academy_alias=academy_alias, active_campaig_academy=1) url = reverse_lazy('marketing:lead') data = generate_form_entry_kwargs({ @@ -368,8 +366,3 @@ def test_passing_slug_of_academy_or_academy_alias(bc: Breathecode, client: APICl 'attribution_id': '75b36c508866d18732305da14fe9a0', }) ] - - # teardown - bc.database.delete('admissions.Academy') - bc.database.delete('marketing.AcademyAlias') - bc.database.delete('marketing.FormEntry') diff --git a/breathecode/registry/receivers.py b/breathecode/registry/receivers.py index c261c7cae..97ba8a24d 100644 --- a/breathecode/registry/receivers.py +++ b/breathecode/registry/receivers.py @@ -31,7 +31,7 @@ def post_asset_slug_modified(sender, instance: Asset, **kwargs): a = Asset.objects.get(id=instance.id) a.all_translations.add(instance) instance.save() - async_update_frontend_asset_cache(instance.slug) + async_update_frontend_asset_cache.delay(instance.slug) @receiver(asset_title_modified, sender=Asset) @@ -41,7 +41,7 @@ def asset_title_was_updated(sender, instance, **kwargs): if instance.status != 'PUBLISHED': return False - async_update_frontend_asset_cache(instance.slug) + async_update_frontend_asset_cache.delay(instance.slug) bucket_name = os.getenv('SCREENSHOTS_BUCKET', None) if bucket_name is None or bucket_name == '': diff --git a/breathecode/registry/tests/urls/tests_academy_asset.py b/breathecode/registry/tests/urls/tests_academy_asset.py index 157132f57..b93c2509d 100644 --- a/breathecode/registry/tests/urls/tests_academy_asset.py +++ b/breathecode/registry/tests/urls/tests_academy_asset.py @@ -4,10 +4,13 @@ from unittest.mock import MagicMock, patch, call from django.urls.base import reverse_lazy +import pytest from rest_framework import status +from breathecode.tests.mixins.breathecode_mixin.breathecode import Breathecode from breathecode.tests.mixins.legacy import LegacyAPITestCase from breathecode.registry import tasks from django.utils import timezone +from rest_framework.test import APIClient UTC_NOW = timezone.now() @@ -70,6 +73,7 @@ def database_item(academy, category, data={}): def post_serializer(academy, category, data={}): + translations = {} return { 'academy': { @@ -112,7 +116,7 @@ def post_serializer(academy, category, data={}): 'technologies': [], 'test_status': None, 'title': 'model_title', - 'translations': {}, + 'translations': translations, 'url': None, 'visibility': 'PUBLIC', 'with_solutions': False, @@ -174,440 +178,427 @@ def put_serializer(academy, category, asset, data={}): } -class TestRegistryAsset(LegacyAPITestCase): - - def test__without_auth(self): - """Test /certificate without auth""" - url = reverse_lazy('registry:academy_asset') - response = self.client.get(url) - json = response.json() - - self.assertEqual( - json, { - 'detail': 'Authentication credentials were not provided.', - 'status_code': status.HTTP_401_UNAUTHORIZED - }) - assert response.status_code == status.HTTP_401_UNAUTHORIZED - self.assertEqual(self.bc.database.list_of('registry.Asset'), []) - - def test__without_capability(self): - """Test /certificate without auth""" - self.headers(academy=1) - url = reverse_lazy('registry:academy_asset') - self.generate_models(authenticate=True) - response = self.client.get(url) - json = response.json() - expected = { - 'status_code': 403, - 'detail': "You (user: 1) don't have this capability: read_asset for academy 1" - } - - assert json == expected - assert response.status_code == status.HTTP_403_FORBIDDEN - self.assertEqual(self.bc.database.list_of('registry.Asset'), []) - - def test__post__without_category(self): - """Test /Asset without category""" - model = self.generate_models(role=1, capability='crud_asset', profile_academy=1, academy=1, user=1) - - self.bc.request.authenticate(model.user) - self.bc.request.set_headers(academy=1) - url = reverse_lazy('registry:academy_asset') - data = {'slug': 'model_slug', 'asset_type': 'PROJECT', 'lang': 'es'} - response = self.client.post(url, data, format='json') - json = response.json() - expected = { - 'detail': 'no-category', - 'status_code': 400, - } - - assert json == expected - assert response.status_code == status.HTTP_400_BAD_REQUEST - self.assertEqual(self.bc.database.list_of('registry.Asset'), []) - - @patch('breathecode.registry.tasks.async_pull_from_github.delay', MagicMock()) - def test__post__with__all__mandatory__properties(self): - """Test /Asset creation with all mandatory properties""" - model = self.bc.database.create( - role=1, - capability='crud_asset', - profile_academy=1, - academy=1, - user=1, - asset_category=1, - ) - - self.bc.request.authenticate(model.user) - self.bc.request.set_headers(academy=1) - - url = reverse_lazy('registry:academy_asset') - data = { - 'slug': 'model_slug', - 'asset_type': 'PROJECT', - 'lang': 'us', - 'category': 1, - 'title': 'model_slug' - } - response = self.client.post(url, data, format='json') - json = response.json() - del data['category'] - expected = post_serializer(model.academy, model.asset_category, data=data) - - assert json == expected - assert response.status_code == status.HTTP_201_CREATED - self.assertEqual(tasks.async_pull_from_github.delay.call_args_list, [call('model_slug')]) - self.assertEqual(self.bc.database.list_of('registry.Asset'), - [database_item(model.academy, model.asset_category, data)]) - - def test_asset__put_many_without_id(self): - """Test Asset bulk update""" - self.headers(academy=1) - - model = self.generate_models(authenticate=True, - profile_academy=True, - capability='crud_asset', - role='potato', - asset_category=True, - asset={ - 'category_id': 1, - 'academy_id': 1, - 'slug': 'asset-1' - }) - - url = reverse_lazy('registry:academy_asset') - data = [{ - 'category': 1, - }] - - response = self.client.put(url, data, format='json') - json = response.json() - - expected = {'detail': 'without-id', 'status_code': 400} - - assert json == expected - assert response.status_code == status.HTTP_400_BAD_REQUEST - - def test_asset__put_many_with_wrong_id(self): - """Test Asset bulk update""" - self.headers(academy=1) - - model = self.generate_models(authenticate=True, - profile_academy=True, - capability='crud_asset', - role='potato', - asset_category=True, - asset={ - 'category_id': 1, - 'academy_id': 1, - 'slug': 'asset-1' - }) - - url = reverse_lazy('registry:academy_asset') - data = [{ - 'category': 1, - 'id': 2, - }] - - response = self.client.put(url, data, format='json') - json = response.json() - - expected = {'detail': 'not-found', 'status_code': 404} - - assert json == expected - assert response.status_code == status.HTTP_404_NOT_FOUND - - def test_asset__put_many(self): - """Test Asset bulk update""" - self.headers(academy=1) - - model = self.generate_models( - authenticate=True, - profile_academy=True, - capability='crud_asset', - role='potato', - asset_category={'lang': 'es'}, - asset=[{ - 'category_id': 1, - 'lang': 'es', - 'academy_id': 1, - 'slug': 'asset-1' - }, { - 'category_id': 1, - 'lang': 'es', - 'academy_id': 1, - 'slug': 'asset-2' - }], - ) - - url = reverse_lazy('registry:academy_asset') - data = [{ - 'id': 1, - 'category': 1, +@pytest.fixture(autouse=True) +def setup(db, monkeypatch): + monkeypatch.setattr('breathecode.registry.signals.asset_slug_modified.send', MagicMock()) + yield + + +def test__without_auth(bc: Breathecode, client: APIClient): + """Test /certificate without auth""" + url = reverse_lazy('registry:academy_asset') + response = client.get(url) + json = response.json() + + assert json == { + 'detail': 'Authentication credentials were not provided.', + 'status_code': status.HTTP_401_UNAUTHORIZED + } + assert response.status_code == status.HTTP_401_UNAUTHORIZED + assert bc.database.list_of('registry.Asset') == [] + + +def test__without_capability(bc: Breathecode, client: APIClient): + """Test /certificate without auth""" + url = reverse_lazy('registry:academy_asset') + model = bc.database.create(user=1) + client.force_authenticate(user=model.user) + + response = client.get(url, HTTP_ACADEMY=1) + json = response.json() + expected = { + 'status_code': 403, + 'detail': "You (user: 1) don't have this capability: read_asset for academy 1" + } + + assert json == expected + assert response.status_code == status.HTTP_403_FORBIDDEN + assert bc.database.list_of('registry.Asset') == [] + + +def test__post__without_category(bc: Breathecode, client: APIClient): + """Test /Asset without category""" + model = bc.database.create(role=1, capability='crud_asset', profile_academy=1, academy=1, user=1) + + client.force_authenticate(user=model.user) + url = reverse_lazy('registry:academy_asset') + data = {'slug': 'model_slug', 'asset_type': 'PROJECT', 'lang': 'es'} + response = client.post(url, data, format='json', HTTP_ACADEMY=1) + json = response.json() + expected = { + 'detail': 'no-category', + 'status_code': 400, + } + + assert json == expected + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert bc.database.list_of('registry.Asset') == [] + + +@patch('breathecode.registry.tasks.async_pull_from_github.delay', MagicMock()) +def test__post__with__all__mandatory__properties(bc: Breathecode, client: APIClient): + """Test /Asset creation with all mandatory properties""" + model = bc.database.create( + role=1, + capability='crud_asset', + profile_academy=1, + academy=1, + user=1, + asset_category=1, + ) + + client.force_authenticate(user=model.user) + + url = reverse_lazy('registry:academy_asset') + data = {'slug': 'model_slug', 'asset_type': 'PROJECT', 'lang': 'us', 'category': 1, 'title': 'model_slug'} + response = client.post(url, data, format='json', HTTP_ACADEMY=1) + json = response.json() + del data['category'] + expected = post_serializer(model.academy, model.asset_category, data=data) + + assert json == expected + assert response.status_code == status.HTTP_201_CREATED + assert tasks.async_pull_from_github.delay.call_args_list == [call('model_slug')] + assert bc.database.list_of('registry.Asset') == [database_item(model.academy, model.asset_category, data)] + + +def test_asset__put_many_without_id(bc: Breathecode, client: APIClient): + """Test Asset bulk update""" + + model = bc.database.create(user=1, + profile_academy=True, + capability='crud_asset', + role='potato', + asset_category=True, + asset={ + 'category_id': 1, + 'academy_id': 1, + 'slug': 'asset-1' + }) + client.force_authenticate(user=model.user) + + url = reverse_lazy('registry:academy_asset') + data = [{ + 'category': 1, + }] + + response = client.put(url, data, format='json', HTTP_ACADEMY=1) + json = response.json() + + expected = {'detail': 'without-id', 'status_code': 400} + + assert json == expected + assert response.status_code == status.HTTP_400_BAD_REQUEST + + +def test_asset__put_many_with_wrong_id(bc: Breathecode, client: APIClient): + """Test Asset bulk update""" + + model = bc.database.create(user=1, + profile_academy=True, + capability='crud_asset', + role='potato', + asset_category=True, + asset={ + 'category_id': 1, + 'academy_id': 1, + 'slug': 'asset-1' + }) + client.force_authenticate(user=model.user) + + url = reverse_lazy('registry:academy_asset') + data = [{ + 'category': 1, + 'id': 2, + }] + + response = client.put(url, data, format='json', HTTP_ACADEMY=1) + json = response.json() + + expected = {'detail': 'not-found', 'status_code': 404} + + assert json == expected + assert response.status_code == status.HTTP_404_NOT_FOUND + + +def test_asset__put_many(bc: Breathecode, client: APIClient): + """Test Asset bulk update""" + + model = bc.database.create( + user=1, + profile_academy=True, + capability='crud_asset', + role='potato', + asset_category={'lang': 'es'}, + asset=[{ + 'category_id': 1, + 'lang': 'es', + 'academy_id': 1, + 'slug': 'asset-1' }, { - 'id': 2, - 'category': 1, - }] - - response = self.client.put(url, data, format='json') - json = response.json() - - for item in json: - del item['created_at'] - del item['updated_at'] - - expected = [ - put_serializer(model.academy, model.asset_category, asset) for i, asset in enumerate(model.asset) - ] - - assert json == expected - assert response.status_code == status.HTTP_200_OK - - @patch('django.utils.timezone.now', MagicMock(return_value=UTC_NOW)) - def test_asset__put_many_with_test_status_ok(self): - """Test Asset bulk update""" - self.headers(academy=1) - - slug = self.bc.fake.slug() - - model = self.generate_models(authenticate=True, - profile_academy=True, - capability='crud_asset', - role='potato', - asset_category={'lang': 'es'}, - asset={ - 'category_id': 1, - 'academy_id': 1, - 'slug': 'asset-1', - 'visibility': 'PRIVATE', - 'test_status': 'OK', - 'lang': 'es', - }) - - title = self.bc.fake.slug() - date = timezone.now() - - url = reverse_lazy('registry:academy_asset') - data = [{ - 'category': 1, - 'created_at': self.bc.datetime.to_iso_string(UTC_NOW), - 'updated_at': self.bc.datetime.to_iso_string(UTC_NOW), - 'title': title, - 'id': 1, - 'visibility': 'PUBLIC', - 'asset_type': 'VIDEO', - }] - - response = self.client.put(url, data, format='json') - json = response.json() - - expected = [ - put_serializer(model.academy, - model.asset_category, - model.asset, - data={ - 'test_status': 'OK', - 'created_at': self.bc.datetime.to_iso_string(UTC_NOW), - 'updated_at': self.bc.datetime.to_iso_string(UTC_NOW), - 'title': title, - 'id': 1, - 'visibility': 'PUBLIC', - 'asset_type': 'VIDEO', - }) - ] - - assert json == expected - assert response.status_code == status.HTTP_200_OK - - @patch('django.utils.timezone.now', MagicMock(return_value=UTC_NOW)) - def test_asset__put_many_with_test_status_warning(self): - """Test Asset bulk update""" - self.headers(academy=1) - - slug = self.bc.fake.slug() - - model = self.generate_models(authenticate=True, - profile_academy=True, - capability='crud_asset', - role='potato', - asset_category={'lang': 'es'}, - asset={ - 'category_id': 1, - 'academy_id': 1, - 'slug': 'asset-1', - 'visibility': 'PRIVATE', - 'test_status': 'WARNING', - 'lang': 'es', - }) - - title = self.bc.fake.slug() - date = timezone.now() - - url = reverse_lazy('registry:academy_asset') - data = [{ - 'category': 1, - 'created_at': self.bc.datetime.to_iso_string(UTC_NOW), - 'updated_at': self.bc.datetime.to_iso_string(UTC_NOW), - 'title': title, - 'id': 1, - 'visibility': 'PUBLIC', - 'asset_type': 'VIDEO', - }] - - response = self.client.put(url, data, format='json') - json = response.json() - - expected = [ - put_serializer(model.academy, - model.asset_category, - model.asset, - data={ - 'test_status': 'WARNING', - 'created_at': self.bc.datetime.to_iso_string(UTC_NOW), - 'updated_at': self.bc.datetime.to_iso_string(UTC_NOW), - 'title': title, - 'id': 1, - 'visibility': 'PUBLIC', - 'asset_type': 'VIDEO', - }) - ] - - assert json == expected - assert response.status_code == status.HTTP_200_OK - - @patch('django.utils.timezone.now', MagicMock(return_value=UTC_NOW)) - def test_asset__put_many_with_test_status_pending(self): - """Test Asset bulk update""" - self.headers(academy=1) - - slug = self.bc.fake.slug() - - model = self.generate_models(authenticate=True, - profile_academy=True, - capability='crud_asset', - role='potato', - asset_category={'lang': 'es'}, - asset={ - 'category_id': 1, - 'academy_id': 1, - 'slug': 'asset-1', - 'visibility': 'PRIVATE', - 'test_status': 'PENDING', - 'lang': 'es', - }) - - title = self.bc.fake.slug() - date = timezone.now() - - url = reverse_lazy('registry:academy_asset') - data = [{ - 'category': 1, - 'created_at': self.bc.datetime.to_iso_string(UTC_NOW), - 'updated_at': self.bc.datetime.to_iso_string(UTC_NOW), - 'title': title, - 'id': 1, - 'visibility': 'PUBLIC', - 'asset_type': 'VIDEO', - }] - - response = self.client.put(url, data, format='json') - json = response.json() - - expected = { - 'detail': 'This asset has to pass tests successfully before publishing', - 'status_code': 400 - } - - assert json == expected - assert response.status_code == status.HTTP_400_BAD_REQUEST - - @patch('django.utils.timezone.now', MagicMock(return_value=UTC_NOW)) - def test_asset__put_many_with_test_status_error(self): - """Test Asset bulk update""" - self.headers(academy=1) - - slug = self.bc.fake.slug() - - model = self.generate_models(authenticate=True, - profile_academy=True, - capability='crud_asset', - role='potato', - asset_category={'lang': 'es'}, - asset={ - 'category_id': 1, - 'academy_id': 1, - 'slug': 'asset-1', - 'visibility': 'PRIVATE', - 'test_status': 'ERROR', - 'lang': 'es', - }) - - title = self.bc.fake.slug() - date = timezone.now() - - url = reverse_lazy('registry:academy_asset') - data = [{ - 'category': 1, - 'created_at': self.bc.datetime.to_iso_string(UTC_NOW), - 'updated_at': self.bc.datetime.to_iso_string(UTC_NOW), - 'title': title, - 'id': 1, - 'visibility': 'PUBLIC', - 'asset_type': 'VIDEO', - }] - - response = self.client.put(url, data, format='json') - json = response.json() - - expected = { - 'detail': 'This asset has to pass tests successfully before publishing', - 'status_code': 400 - } - - assert json == expected - assert response.status_code == status.HTTP_400_BAD_REQUEST - - @patch('django.utils.timezone.now', MagicMock(return_value=UTC_NOW)) - def test_asset__put_many_with_test_status_Needs_Resync(self): - """Test Asset bulk update""" - self.headers(academy=1) - - slug = self.bc.fake.slug() - - model = self.generate_models(authenticate=True, - profile_academy=True, - capability='crud_asset', - role='potato', - asset_category={'lang': 'es'}, - asset={ - 'category_id': 1, - 'academy_id': 1, - 'slug': 'asset-1', - 'visibility': 'PRIVATE', - 'test_status': 'NEEDS_RESYNC', - 'lang': 'es', - }) - - title = self.bc.fake.slug() - date = timezone.now() - - url = reverse_lazy('registry:academy_asset') - data = [{ - 'category': 1, - 'created_at': self.bc.datetime.to_iso_string(UTC_NOW), - 'updated_at': self.bc.datetime.to_iso_string(UTC_NOW), - 'title': title, - 'id': 1, - 'visibility': 'PUBLIC', - 'asset_type': 'VIDEO', - }] - - response = self.client.put(url, data, format='json') - json = response.json() - - expected = { - 'detail': 'This asset has to pass tests successfully before publishing', - 'status_code': 400 - } - - assert json == expected - assert response.status_code == status.HTTP_400_BAD_REQUEST + 'category_id': 1, + 'lang': 'es', + 'academy_id': 1, + 'slug': 'asset-2' + }], + ) + client.force_authenticate(user=model.user) + + url = reverse_lazy('registry:academy_asset') + data = [{ + 'id': 1, + 'category': 1, + }, { + 'id': 2, + 'category': 1, + }] + + response = client.put(url, data, format='json', HTTP_ACADEMY=1) + json = response.json() + + for item in json: + del item['created_at'] + del item['updated_at'] + + expected = [ + put_serializer(model.academy, model.asset_category, asset) for i, asset in enumerate(model.asset) + ] + + assert json == expected + assert response.status_code == status.HTTP_200_OK + + +@patch('django.utils.timezone.now', MagicMock(return_value=UTC_NOW)) +def test_asset__put_many_with_test_status_ok(bc: Breathecode, client: APIClient): + """Test Asset bulk update""" + + model = bc.database.create(user=1, + profile_academy=True, + capability='crud_asset', + role='potato', + asset_category={'lang': 'es'}, + asset={ + 'category_id': 1, + 'academy_id': 1, + 'slug': 'asset-1', + 'visibility': 'PRIVATE', + 'test_status': 'OK', + 'lang': 'es', + }) + client.force_authenticate(user=model.user) + + title = bc.fake.slug() + date = timezone.now() + + url = reverse_lazy('registry:academy_asset') + data = [{ + 'category': 1, + 'created_at': bc.datetime.to_iso_string(UTC_NOW), + 'updated_at': bc.datetime.to_iso_string(UTC_NOW), + 'title': title, + 'id': 1, + 'visibility': 'PUBLIC', + 'asset_type': 'VIDEO', + }] + + response = client.put(url, data, format='json', HTTP_ACADEMY=1) + json = response.json() + + expected = [ + put_serializer(model.academy, + model.asset_category, + model.asset, + data={ + 'test_status': 'OK', + 'created_at': bc.datetime.to_iso_string(UTC_NOW), + 'updated_at': bc.datetime.to_iso_string(UTC_NOW), + 'title': title, + 'id': 1, + 'visibility': 'PUBLIC', + 'asset_type': 'VIDEO', + }) + ] + + assert json == expected + assert response.status_code == status.HTTP_200_OK + + +@patch('django.utils.timezone.now', MagicMock(return_value=UTC_NOW)) +def test_asset__put_many_with_test_status_warning(bc: Breathecode, client: APIClient): + """Test Asset bulk update""" + + model = bc.database.create(user=1, + profile_academy=True, + capability='crud_asset', + role='potato', + asset_category={'lang': 'es'}, + asset={ + 'category_id': 1, + 'academy_id': 1, + 'slug': 'asset-1', + 'visibility': 'PRIVATE', + 'test_status': 'WARNING', + 'lang': 'es', + }) + client.force_authenticate(user=model.user) + + title = bc.fake.slug() + date = timezone.now() + + url = reverse_lazy('registry:academy_asset') + data = [{ + 'category': 1, + 'created_at': bc.datetime.to_iso_string(UTC_NOW), + 'updated_at': bc.datetime.to_iso_string(UTC_NOW), + 'title': title, + 'id': 1, + 'visibility': 'PUBLIC', + 'asset_type': 'VIDEO', + }] + + response = client.put(url, data, format='json', HTTP_ACADEMY=1) + json = response.json() + + expected = [ + put_serializer(model.academy, + model.asset_category, + model.asset, + data={ + 'test_status': 'WARNING', + 'created_at': bc.datetime.to_iso_string(UTC_NOW), + 'updated_at': bc.datetime.to_iso_string(UTC_NOW), + 'title': title, + 'id': 1, + 'visibility': 'PUBLIC', + 'asset_type': 'VIDEO', + }) + ] + + assert json == expected + assert response.status_code == status.HTTP_200_OK + + +@patch('django.utils.timezone.now', MagicMock(return_value=UTC_NOW)) +def test_asset__put_many_with_test_status_pending(bc: Breathecode, client: APIClient): + """Test Asset bulk update""" + + model = bc.database.create(user=1, + profile_academy=True, + capability='crud_asset', + role='potato', + asset_category={'lang': 'es'}, + asset={ + 'category_id': 1, + 'academy_id': 1, + 'slug': 'asset-1', + 'visibility': 'PRIVATE', + 'test_status': 'PENDING', + 'lang': 'es', + }) + client.force_authenticate(user=model.user) + + title = bc.fake.slug() + date = timezone.now() + + url = reverse_lazy('registry:academy_asset') + data = [{ + 'category': 1, + 'created_at': bc.datetime.to_iso_string(UTC_NOW), + 'updated_at': bc.datetime.to_iso_string(UTC_NOW), + 'title': title, + 'id': 1, + 'visibility': 'PUBLIC', + 'asset_type': 'VIDEO', + }] + + response = client.put(url, data, format='json', HTTP_ACADEMY=1) + json = response.json() + + expected = {'detail': 'This asset has to pass tests successfully before publishing', 'status_code': 400} + + assert json == expected + assert response.status_code == status.HTTP_400_BAD_REQUEST + + +@patch('django.utils.timezone.now', MagicMock(return_value=UTC_NOW)) +def test_asset__put_many_with_test_status_error(bc: Breathecode, client: APIClient): + """Test Asset bulk update""" + + model = bc.database.create(user=1, + profile_academy=True, + capability='crud_asset', + role='potato', + asset_category={'lang': 'es'}, + asset={ + 'category_id': 1, + 'academy_id': 1, + 'slug': 'asset-1', + 'visibility': 'PRIVATE', + 'test_status': 'ERROR', + 'lang': 'es', + }) + client.force_authenticate(user=model.user) + + title = bc.fake.slug() + date = timezone.now() + + url = reverse_lazy('registry:academy_asset') + data = [{ + 'category': 1, + 'created_at': bc.datetime.to_iso_string(UTC_NOW), + 'updated_at': bc.datetime.to_iso_string(UTC_NOW), + 'title': title, + 'id': 1, + 'visibility': 'PUBLIC', + 'asset_type': 'VIDEO', + }] + + response = client.put(url, data, format='json', HTTP_ACADEMY=1) + json = response.json() + + expected = {'detail': 'This asset has to pass tests successfully before publishing', 'status_code': 400} + + assert json == expected + assert response.status_code == status.HTTP_400_BAD_REQUEST + + +@patch('django.utils.timezone.now', MagicMock(return_value=UTC_NOW)) +def test_asset__put_many_with_test_status_Needs_Resync(bc: Breathecode, client: APIClient): + """Test Asset bulk update""" + + model = bc.database.create(user=1, + profile_academy=True, + capability='crud_asset', + role='potato', + asset_category={'lang': 'es'}, + asset={ + 'category_id': 1, + 'academy_id': 1, + 'slug': 'asset-1', + 'visibility': 'PRIVATE', + 'test_status': 'NEEDS_RESYNC', + 'lang': 'es', + }) + client.force_authenticate(user=model.user) + + title = bc.fake.slug() + date = timezone.now() + + url = reverse_lazy('registry:academy_asset') + data = [{ + 'category': 1, + 'created_at': bc.datetime.to_iso_string(UTC_NOW), + 'updated_at': bc.datetime.to_iso_string(UTC_NOW), + 'title': title, + 'id': 1, + 'visibility': 'PUBLIC', + 'asset_type': 'VIDEO', + }] + + response = client.put(url, data, format='json', HTTP_ACADEMY=1) + json = response.json() + + expected = {'detail': 'This asset has to pass tests successfully before publishing', 'status_code': 400} + + assert json == expected + assert response.status_code == status.HTTP_400_BAD_REQUEST diff --git a/breathecode/registry/views.py b/breathecode/registry/views.py index 104f925a7..ee9208905 100644 --- a/breathecode/registry/views.py +++ b/breathecode/registry/views.py @@ -918,7 +918,7 @@ def get(self, request, asset_slug=None, academy_id=None): need_translation = self.request.GET.get('need_translation', False) if need_translation == 'true': - items = items.annotate(num_translations=Count('all_translations')).filter(num_translations__lte=1) \ + items = items.annotate(num_translations=Count('all_translations')).filter(num_translations__lte=1) items = items.filter(**lookup).distinct() items = handler.queryset(items) diff --git a/breathecode/settings.py b/breathecode/settings.py index 57d9e6373..6c5ad1589 100644 --- a/breathecode/settings.py +++ b/breathecode/settings.py @@ -120,6 +120,9 @@ ), } +if os.getenv('ENABLE_DEFAULT_PAGINATION', 'y') in ['t', 'true', 'True', 'TRUE', '1', 'yes', 'y']: + REST_FRAMEWORK['PAGE_SIZE'] = 20 + MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'whitenoise.middleware.WhiteNoiseMiddleware', diff --git a/breathecode/tests/mixins/generate_models_mixin/registry_models_mixin.py b/breathecode/tests/mixins/generate_models_mixin/registry_models_mixin.py index e41179e23..6aad159ca 100644 --- a/breathecode/tests/mixins/generate_models_mixin/registry_models_mixin.py +++ b/breathecode/tests/mixins/generate_models_mixin/registry_models_mixin.py @@ -55,7 +55,9 @@ def generate_registry_models(self, models['asset_keyword'] = create_models(asset_keyword, 'registry.AssetKeyword', **kargs) if not 'asset' in models and (is_valid(asset) or is_valid(asset_alias) or is_valid(asset_comment)): - kargs = {} + kargs = { + 'all_translations': [], + } if 'asset_technology' in models: kargs['technologies'] = get_list(models['asset_technology']) diff --git a/breathecode/utils/api_view_extensions/api_view_extension_handlers.py b/breathecode/utils/api_view_extensions/api_view_extension_handlers.py index 023b51c25..7770fa90d 100644 --- a/breathecode/utils/api_view_extensions/api_view_extension_handlers.py +++ b/breathecode/utils/api_view_extensions/api_view_extension_handlers.py @@ -56,7 +56,7 @@ def __init__(self, request: WSGIRequest, valid_extensions: Optional[set[Extensio self._spy_extension_arguments(**kwargs) def queryset(self, queryset: QuerySet[Any]) -> QuerySet[Any]: - """Apply mutations over queryset""" + """Apply mutations over queryset.""" # The extension can decide if act or not extensions_allowed = [ @@ -70,7 +70,7 @@ def queryset(self, queryset: QuerySet[Any]) -> QuerySet[Any]: return queryset def response(self, data: dict | list[dict], format='application/json'): - """Get the response of endpoint""" + """Get the response of endpoint.""" headers = {} @@ -92,13 +92,9 @@ def _register_valid_extensions(self) -> None: self._spy_extensions(sorted([x.__name__ for x in self._extensions])) def _spy_extensions(self, _: list[str]) -> None: - """ - That is used for spy the extensions is being used. - """ + """Spy the extensions is being used in the tests.""" ... def _spy_extension_arguments(self, **_) -> None: - """ - That is used for spy the extension arguments is being used. - """ + """Spy the extension arguments is being used in the tests.""" ... diff --git a/breathecode/utils/api_view_extensions/extension_base.py b/breathecode/utils/api_view_extensions/extension_base.py index 23191b1cb..d1feb3d04 100644 --- a/breathecode/utils/api_view_extensions/extension_base.py +++ b/breathecode/utils/api_view_extensions/extension_base.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Any, Optional from django.db.models import QuerySet from django.core.handlers.wsgi import WSGIRequest @@ -32,3 +32,6 @@ def _set_request(self, request: WSGIRequest) -> None: def _optional_dependencies(self, **kwargs): ... + + def __str__(self) -> str: + return self.__class__.__name__ diff --git a/breathecode/utils/api_view_extensions/extensions/pagination_extension.py b/breathecode/utils/api_view_extensions/extensions/pagination_extension.py index 95a594897..1ca2075e6 100644 --- a/breathecode/utils/api_view_extensions/extensions/pagination_extension.py +++ b/breathecode/utils/api_view_extensions/extensions/pagination_extension.py @@ -1,4 +1,5 @@ from collections import OrderedDict +import os from typing import Any, Optional from breathecode.utils.api_view_extensions.extension_base import ExtensionBase from breathecode.utils.api_view_extensions.priorities.mutator_order import MutatorOrder @@ -12,13 +13,17 @@ OFFSET_QUERY_PARAM = 'offset' LIMIT_QUERY_PARAM = 'limit' MAX_LIMIT = None -DEFAULT_LIMIT = 1000 + +if os.getenv('ENABLE_DEFAULT_PAGINATION', 'y') in ['t', 'true', 'True', 'TRUE', '1', 'yes', 'y']: + DEFAULT_LIMIT = 20 + +else: + DEFAULT_LIMIT = 1000 def _positive_int(integer_string, strict=False, cutoff=None): - """ - Cast a string to a strictly positive integer. - """ + """Cast a string to a strictly positive integer.""" + ret = int(integer_string) if ret < 0 or (ret == 0 and strict): raise ValueError() @@ -36,34 +41,36 @@ class PaginationExtension(ExtensionBase): def __init__(self, paginate: bool, **kwargs) -> None: self._paginate = paginate + self._is_list = False def _can_modify_queryset(self) -> bool: return self._paginate def _get_order_of_mutator(self) -> int: - return MutatorOrder.PAGINATION + return int(MutatorOrder.PAGINATION) def _can_modify_response(self) -> bool: - return self._paginate and self._is_paginate() + return self._paginate def _get_order_of_response(self) -> int: - return int(ResponseOrder.PAGINATION) if self._is_paginate() else -1 + return int(ResponseOrder.PAGINATION) def _is_paginate(self): return bool(self._request.GET.get(LIMIT_QUERY_PARAM) or self._request.GET.get(OFFSET_QUERY_PARAM)) def _apply_queryset_mutation(self, queryset: QuerySet[Any]): - if not self._is_paginate(): - return queryset - - self._use_envelope = True - if str(self._request.GET.get('envelope')).lower() in ['false', '0']: - self._use_envelope = False - + self._use_envelope = False + self._is_list = True self._count = self._get_count(queryset) self._offset = self._get_offset() self._limit = self._get_limit() - return queryset[self._offset:self._offset + self._limit] + + if self._is_paginate() and self._request.GET.get( + 'envelope', '').lower() in ['false', 'f', '0', 'no', 'n', 'off', '']: + self._use_envelope = True + + self._queryset = queryset[self._offset:self._offset + self._limit] + return self._queryset def _apply_response_mutation(self, data: list[dict] | dict, @@ -72,6 +79,9 @@ def _apply_response_mutation(self, if headers is None: headers = {} + if not self._is_list: + return (data, headers) + next_url = self._parse_comma(self._get_next_link()) previous_url = self._parse_comma(self._get_previous_link()) first_url = self._parse_comma(self._get_first_link()) @@ -88,7 +98,9 @@ def _apply_response_mutation(self, links.append('<{}>; rel="{}"'.format(url, label)) headers = {**headers, 'Link': ', '.join(links)} if links else {**headers} - headers['x-total-count'] = self._count + headers['X-Total-Count'] = self._count + headers['X-Per-Page'] = self._limit + headers['X-Page'] = int(self._offset / self._limit) + 1 if self._use_envelope: data = OrderedDict([('count', self._count), ('first', first_url), ('next', next_url), @@ -104,9 +116,7 @@ def _parse_comma(self, string: str): return string.replace('%2C', ',') def _get_count(self, queryset: QuerySet[Any] | list): - """ - Determine an object count, supporting either querysets or regular lists. - """ + """Determine an object count, supporting either querysets or regular lists.""" try: return queryset.count() diff --git a/breathecode/utils/tests/api_view_extensions/tests_api_view_extensions.py b/breathecode/utils/tests/api_view_extensions/tests_api_view_extensions.py index cb78041b2..9ecff7702 100644 --- a/breathecode/utils/tests/api_view_extensions/tests_api_view_extensions.py +++ b/breathecode/utils/tests/api_view_extensions/tests_api_view_extensions.py @@ -30,6 +30,46 @@ def serialize_cache_object(data, headers={}): return res +def assert_pagination(headers: dict, limit, offset, lenght): + assert 'Link' in headers + assert 'X-Total-Count' in headers + assert 'X-Page' in headers + assert 'X-Per-Page' in headers + + assert headers['X-Total-Count'] == str(lenght) + # assert headers['X-Total-Page'] == str(int(lenght / limit)) + assert headers['X-Page'] == str(int(offset / limit) + 1) + assert headers['X-Per-Page'] == str(limit) + + if offset == 0: + assert headers['Link'] == ( + f'; rel="next", ' + f'= 0 else 0}>; rel="last"' + ) + elif offset + limit >= lenght: + previous_offset = offset - limit if offset - limit >= 0 else 0 + + if previous_offset: + previous_offset_section = f'&offset={previous_offset}' + + else: + previous_offset_section = '' + + assert headers['Link'] == ( + f'; rel="first", ' + f'; rel="previous"' + ) + else: + raise NotImplemented('This case is not implemented') + + +def assert_no_pagination(headers: dict, limit, offset, lenght): + assert 'Link' not in headers + assert 'X-Total-Count' not in headers + assert 'X-Page' not in headers + assert 'X-Per-Page' not in headers + + class GetCohortSerializer(serpy.Serializer): id = serpy.Field() slug = serpy.Field() @@ -798,7 +838,6 @@ def test_sort__get__ten_cohorts(self): 🔽🔽🔽 Pagination True """ - @pytest.mark.skip(reason='It was not prioritized in the scrum') def test_pagination__get__activate__25_cohorts_just_get_20(self): cache.clear() @@ -814,6 +853,7 @@ def test_pagination__get__activate__25_cohorts_just_get_20(self): self.assertEqual(json.loads(response.content.decode('utf-8')), expected) self.assertEqual(response.status_code, status.HTTP_200_OK) + assert_pagination(response.headers, limit=20, offset=0, lenght=25) def test_pagination__get__activate__with_10_cohorts__get_first_five(self): cache.clear() @@ -837,6 +877,7 @@ def test_pagination__get__activate__with_10_cohorts__get_first_five(self): self.assertEqual(json.loads(response.content.decode('utf-8')), expected) self.assertEqual(response.status_code, status.HTTP_200_OK) + assert_pagination(response.headers, limit=5, offset=0, lenght=10) def test_pagination__get__activate__with_10_cohorts__get_last_five(self): cache.clear() @@ -860,6 +901,7 @@ def test_pagination__get__activate__with_10_cohorts__get_last_five(self): self.assertEqual(json.loads(response.content.decode('utf-8')), expected) self.assertEqual(response.status_code, status.HTTP_200_OK) + assert_pagination(response.headers, limit=5, offset=5, lenght=10) def test_pagination__get__activate__with_10_cohorts__after_last_five(self): cache.clear() @@ -883,6 +925,7 @@ def test_pagination__get__activate__with_10_cohorts__after_last_five(self): self.assertEqual(json.loads(response.content.decode('utf-8')), expected) self.assertEqual(response.status_code, status.HTTP_200_OK) + assert_pagination(response.headers, limit=5, offset=10, lenght=10) """ 🔽🔽🔽 Pagination False @@ -904,6 +947,7 @@ def test_pagination__get__deactivate__105_cohorts_just_get_100(self): self.assertEqual(json.loads(brotli.decompress(response.content)), expected) self.assertEqual(response.status_code, status.HTTP_200_OK) + assert_no_pagination(response.headers, limit=20, offset=0, lenght=25) def test_pagination__get__deactivate__with_10_cohorts__get_first_five(self): cache.clear() @@ -920,6 +964,7 @@ def test_pagination__get__deactivate__with_10_cohorts__get_first_five(self): self.assertEqual(json.loads(response.content.decode('utf-8')), expected) self.assertEqual(response.status_code, status.HTTP_200_OK) + assert_no_pagination(response.headers, limit=20, offset=0, lenght=25) def test_pagination__get__deactivate__with_10_cohorts__get_last_five(self): cache.clear() @@ -936,6 +981,7 @@ def test_pagination__get__deactivate__with_10_cohorts__get_last_five(self): self.assertEqual(json.loads(response.content.decode('utf-8')), expected) self.assertEqual(response.status_code, status.HTTP_200_OK) + assert_no_pagination(response.headers, limit=20, offset=0, lenght=25) def test_pagination__get__deactivate__with_10_cohorts__after_last_five(self): cache.clear() @@ -952,6 +998,7 @@ def test_pagination__get__deactivate__with_10_cohorts__after_last_five(self): self.assertEqual(json.loads(response.content.decode('utf-8')), expected) self.assertEqual(response.status_code, status.HTTP_200_OK) + assert_no_pagination(response.headers, limit=20, offset=0, lenght=25) class ApiViewExtensionsGetIdTestSuite(UtilsTestCase): From a17e5880d0f09f86977cbf287e6c0338c5ff4b20 Mon Sep 17 00:00:00 2001 From: jefer94 Date: Mon, 18 Dec 2023 14:21:18 -0500 Subject: [PATCH 4/5] update migration --- ...o_20231216_0647.py => 0041_auto_20231218_1920.py} | 9 ++++----- breathecode/payments/models.py | 12 ++++-------- .../utils/api_view_extensions/extension_base.py | 4 ++-- 3 files changed, 10 insertions(+), 15 deletions(-) rename breathecode/payments/migrations/{0041_auto_20231216_0647.py => 0041_auto_20231218_1920.py} (91%) diff --git a/breathecode/payments/migrations/0041_auto_20231216_0647.py b/breathecode/payments/migrations/0041_auto_20231218_1920.py similarity index 91% rename from breathecode/payments/migrations/0041_auto_20231216_0647.py rename to breathecode/payments/migrations/0041_auto_20231218_1920.py index b41b721d1..9c6d01059 100644 --- a/breathecode/payments/migrations/0041_auto_20231216_0647.py +++ b/breathecode/payments/migrations/0041_auto_20231218_1920.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.23 on 2023-12-16 06:47 +# Generated by Django 3.2.23 on 2023-12-18 19:20 from django.db import migrations, models import django.db.models.deletion @@ -48,10 +48,9 @@ class Migration(migrations.Migration): field=models.CharField(choices=[('COHORT_SET', 'Cohort set'), ('MENTORSHIP_SERVICE_SET', 'Mentorship service set'), ('EVENT_TYPE_SET', 'Event type set'), - ('CHAT_SUPPORT', 'Chat support'), ('CODE_REVIEW', 'Code review'), - ('AI_INTERACTION', 'AI interaction'), - ('LEARNPACK_BUILD', 'Learnpack build'), - ('LEARNPACK_TEST', 'Learnpack test')], + ('CHAT_SUPPORT', 'Chat support'), + ('BUILD_PROJECT', 'Build project'), + ('TEST_PROJECT', 'Test project')], default='COHORT_SET', help_text='Service type', max_length=22), diff --git a/breathecode/payments/models.py b/breathecode/payments/models.py index 6f0c300b3..dcf65471c 100644 --- a/breathecode/payments/models.py +++ b/breathecode/payments/models.py @@ -165,19 +165,15 @@ class Meta: MENTORSHIP_SERVICE_SET = 'MENTORSHIP_SERVICE_SET' EVENT_TYPE_SET = 'EVENT_TYPE_SET' CHAT_SUPPORT = 'CHAT_SUPPORT' -CODE_REVIEW = 'CODE_REVIEW' -AI_INTERACTION = 'AI_INTERACTION' -LEARNPACK_BUILD = 'LEARNPACK_BUILD' -LEARNPACK_TEST = 'LEARNPACK_TEST' +BUILD_PROJECT = 'BUILD_PROJECT' +TEST_PROJECT = 'TEST_PROJECT' SERVICE_TYPES = [ (COHORT_SET, 'Cohort set'), (MENTORSHIP_SERVICE_SET, 'Mentorship service set'), (EVENT_TYPE_SET, 'Event type set'), (CHAT_SUPPORT, 'Chat support'), - (CODE_REVIEW, 'Code review'), - (AI_INTERACTION, 'AI interaction'), - (LEARNPACK_BUILD, 'Learnpack build'), - (LEARNPACK_TEST, 'Learnpack test'), + (BUILD_PROJECT, 'Build project'), + (TEST_PROJECT, 'Test project'), ] diff --git a/breathecode/utils/api_view_extensions/extension_base.py b/breathecode/utils/api_view_extensions/extension_base.py index d1feb3d04..1383a0b56 100644 --- a/breathecode/utils/api_view_extensions/extension_base.py +++ b/breathecode/utils/api_view_extensions/extension_base.py @@ -1,4 +1,4 @@ -from typing import Any, Optional +from typing import Optional from django.db.models import QuerySet from django.core.handlers.wsgi import WSGIRequest @@ -30,7 +30,7 @@ def _apply_response_mutation(self, queryset: QuerySet[any]) -> QuerySet[any]: def _set_request(self, request: WSGIRequest) -> None: self._request = request - def _optional_dependencies(self, **kwargs): + def _optional_dependencies(self, **kwargs) -> None: ... def __str__(self) -> str: From 30737cfea22d94e5a7a6e389b573bcc504d51d5c Mon Sep 17 00:00:00 2001 From: jefer94 Date: Mon, 18 Dec 2023 14:46:05 -0500 Subject: [PATCH 5/5] update migration --- ...{0041_auto_20231218_1920.py => 0041_auto_20231218_1945.py} | 4 ++-- breathecode/payments/models.py | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) rename breathecode/payments/migrations/{0041_auto_20231218_1920.py => 0041_auto_20231218_1945.py} (97%) diff --git a/breathecode/payments/migrations/0041_auto_20231218_1920.py b/breathecode/payments/migrations/0041_auto_20231218_1945.py similarity index 97% rename from breathecode/payments/migrations/0041_auto_20231218_1920.py rename to breathecode/payments/migrations/0041_auto_20231218_1945.py index 9c6d01059..5153ba8fa 100644 --- a/breathecode/payments/migrations/0041_auto_20231218_1920.py +++ b/breathecode/payments/migrations/0041_auto_20231218_1945.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.23 on 2023-12-18 19:20 +# Generated by Django 3.2.23 on 2023-12-18 19:45 from django.db import migrations, models import django.db.models.deletion @@ -48,7 +48,7 @@ class Migration(migrations.Migration): field=models.CharField(choices=[('COHORT_SET', 'Cohort set'), ('MENTORSHIP_SERVICE_SET', 'Mentorship service set'), ('EVENT_TYPE_SET', 'Event type set'), - ('CHAT_SUPPORT', 'Chat support'), + ('CHAT_SUPPORT', 'Chat support'), ('CODE_REVIEW', 'Code review'), ('BUILD_PROJECT', 'Build project'), ('TEST_PROJECT', 'Test project')], default='COHORT_SET', diff --git a/breathecode/payments/models.py b/breathecode/payments/models.py index dcf65471c..7305ec99e 100644 --- a/breathecode/payments/models.py +++ b/breathecode/payments/models.py @@ -165,6 +165,7 @@ class Meta: MENTORSHIP_SERVICE_SET = 'MENTORSHIP_SERVICE_SET' EVENT_TYPE_SET = 'EVENT_TYPE_SET' CHAT_SUPPORT = 'CHAT_SUPPORT' +CODE_REVIEW = 'CODE_REVIEW' BUILD_PROJECT = 'BUILD_PROJECT' TEST_PROJECT = 'TEST_PROJECT' SERVICE_TYPES = [ @@ -172,6 +173,7 @@ class Meta: (MENTORSHIP_SERVICE_SET, 'Mentorship service set'), (EVENT_TYPE_SET, 'Event type set'), (CHAT_SUPPORT, 'Chat support'), + (CODE_REVIEW, 'Code review'), (BUILD_PROJECT, 'Build project'), (TEST_PROJECT, 'Test project'), ]