diff --git a/backoffice/management/__init__.py b/backoffice/management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backoffice/management/admin.py b/backoffice/management/admin.py new file mode 100644 index 00000000..8c38f3f3 --- /dev/null +++ b/backoffice/management/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/backoffice/management/apps.py b/backoffice/management/apps.py new file mode 100644 index 00000000..f41e23ee --- /dev/null +++ b/backoffice/management/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ManagementConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "management" diff --git a/backoffice/management/groups.py b/backoffice/management/groups.py new file mode 100644 index 00000000..ab5fa40a --- /dev/null +++ b/backoffice/management/groups.py @@ -0,0 +1,5 @@ +from django.contrib.auth.models import Group + + +admin_group, created = Group.objects.get_or_create(name='admin') +curator_group, created = Group.objects.get_or_create(name='curator') diff --git a/backoffice/management/migrations/__init__.py b/backoffice/management/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backoffice/management/models.py b/backoffice/management/models.py new file mode 100644 index 00000000..71a83623 --- /dev/null +++ b/backoffice/management/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/backoffice/management/permissions.py b/backoffice/management/permissions.py new file mode 100644 index 00000000..f4c69932 --- /dev/null +++ b/backoffice/management/permissions.py @@ -0,0 +1,27 @@ +from django.contrib.auth.models import Group +from rest_framework import permissions + + +def _is_in_group(user, group_name): + try: + return Group.objects.get(name=group_name).user_set.filter(id=user.id).exists() + except Group.DoesNotExist: + return None + + +def _has_group_permission(user, required_groups): + return any([_is_in_group(user, group_name) for group_name in required_groups]) + + +class PermissionCheckBase(permissions.BasePermission): + def has_permission(self, request, view): + has_group_permission = _has_group_permission(request.user, self.required_groups) + return request.user and has_group_permission + + def has_object_permission(self, request, view, obj): + has_group_permission = _has_group_permission(request.user, self.required_groups) + return request.user and has_group_permission + + +class IsAdminOrCuratorUser(PermissionCheckBase): + required_groups = ['admin', 'curator'] diff --git a/backoffice/management/tests/test_permissions.py b/backoffice/management/tests/test_permissions.py new file mode 100644 index 00000000..ace95ee4 --- /dev/null +++ b/backoffice/management/tests/test_permissions.py @@ -0,0 +1,45 @@ +from django.contrib.auth.models import Group +from rest_framework.test import APIRequestFactory, force_authenticate +from rest_framework.views import APIView +from rest_framework.response import Response +from django.test import TestCase +from management.permissions import IsAdminOrCuratorUser +from django.contrib.auth import get_user_model + + +User = get_user_model() + + +class MockView(APIView): + permission_classes = [IsAdminOrCuratorUser] + + def get(self, request): + return Response("Test Response") + + +class PermissionCheckTests(TestCase): + def setUp(self): + self.user = User.objects.create_user(email='testuser@test.com', password='testpassword') + + self.admin_group, _ = Group.objects.get_or_create(name='admin') + self.curator_group, _ = Group.objects.get_or_create(name='curator') + + def test_user_in_required_group(self): + self.user.groups.add(self.admin_group) + + factory = APIRequestFactory() + request = factory.get('/mock/') + force_authenticate(request, user=self.user) + + view = MockView.as_view() + response = view(request) + self.assertEqual(response.status_code, 200) + + def test_user_not_in_required_group(self): + factory = APIRequestFactory() + request = factory.get('/mock/') + force_authenticate(request, user=self.user) + + view = MockView.as_view() + response = view(request) + self.assertEqual(response.status_code, 403) diff --git a/backoffice/management/views.py b/backoffice/management/views.py new file mode 100644 index 00000000..91ea44a2 --- /dev/null +++ b/backoffice/management/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/backoffice/workflows/__init__.py b/backoffice/workflows/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backoffice/workflows/admin.py b/backoffice/workflows/admin.py new file mode 100644 index 00000000..8c38f3f3 --- /dev/null +++ b/backoffice/workflows/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/backoffice/workflows/api/serializers.py b/backoffice/workflows/api/serializers.py new file mode 100644 index 00000000..c4a7ebe6 --- /dev/null +++ b/backoffice/workflows/api/serializers.py @@ -0,0 +1,8 @@ +from rest_framework import serializers +from workflows.models import Workflow + + +class WorkflowSerializer(serializers.ModelSerializer): + class Meta: + model = Workflow + fields = '__all__' diff --git a/backoffice/workflows/api/views.py b/backoffice/workflows/api/views.py new file mode 100644 index 00000000..3b657780 --- /dev/null +++ b/backoffice/workflows/api/views.py @@ -0,0 +1,17 @@ +from workflows.models import Workflow + + +from rest_framework import viewsets +from .serializers import WorkflowSerializer + + +class WorkflowViewSet(viewsets.ModelViewSet): + queryset = Workflow.objects.all() + serializer_class = WorkflowSerializer + + def get_queryset(self): + status = self.request.query_params.get('status') + if status: + return self.queryset.filter(status__status=status) + return self.queryset + \ No newline at end of file diff --git a/backoffice/workflows/apps.py b/backoffice/workflows/apps.py new file mode 100644 index 00000000..44ec7381 --- /dev/null +++ b/backoffice/workflows/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class WorkflowsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "workflows" diff --git a/backoffice/workflows/migrations/0001_initial.py b/backoffice/workflows/migrations/0001_initial.py new file mode 100644 index 00000000..2d4328b4 --- /dev/null +++ b/backoffice/workflows/migrations/0001_initial.py @@ -0,0 +1,63 @@ +# Generated by Django 4.2.6 on 2023-10-16 09:06 + +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="WorkflowData", + fields=[ + ("id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ("data", models.JSONField()), + ], + ), + migrations.CreateModel( + name="WorkflowMeta", + fields=[ + ( + "id", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + primary_key=True, + serialize=False, + to="workflows.workflowdata", + ), + ), + ("core", models.BooleanField()), + ("is_update", models.BooleanField()), + ], + ), + migrations.CreateModel( + name="WorkflowStatus", + fields=[ + ( + "id", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + primary_key=True, + serialize=False, + to="workflows.workflowdata", + ), + ), + ( + "status", + models.CharField( + choices=[ + ("PREPROCESSING", "Preprocessing"), + ("APPROVAL", "Approval"), + ("POSTPROCESSING", "Postprocessing"), + ], + default="PREPROCESSING", + max_length=30, + ), + ), + ], + ), + ] diff --git a/backoffice/workflows/migrations/0002_workflow_remove_workflowmeta_id_and_more.py b/backoffice/workflows/migrations/0002_workflow_remove_workflowmeta_id_and_more.py new file mode 100644 index 00000000..dc659ed1 --- /dev/null +++ b/backoffice/workflows/migrations/0002_workflow_remove_workflowmeta_id_and_more.py @@ -0,0 +1,51 @@ +# Generated by Django 4.2.6 on 2023-10-17 07:16 + +from django.db import migrations, models +import uuid + + +class Migration(migrations.Migration): + dependencies = [ + ("workflows", "0001_initial"), + ] + + operations = [ + migrations.CreateModel( + name="Workflow", + fields=[ + ("id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ("data", models.JSONField()), + ( + "status", + models.CharField( + choices=[ + ("PREPROCESSING", "Preprocessing"), + ("APPROVAL", "Approval"), + ("POSTPROCESSING", "Postprocessing"), + ], + default="PREPROCESSING", + max_length=30, + ), + ), + ("core", models.BooleanField()), + ("is_update", models.BooleanField()), + ], + ), + migrations.RemoveField( + model_name="workflowmeta", + name="id", + ), + migrations.RemoveField( + model_name="workflowstatus", + name="id", + ), + migrations.DeleteModel( + name="WorkflowData", + ), + migrations.DeleteModel( + name="WorkflowMeta", + ), + migrations.DeleteModel( + name="WorkflowStatus", + ), + ] diff --git a/backoffice/workflows/migrations/__init__.py b/backoffice/workflows/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backoffice/workflows/models.py b/backoffice/workflows/models.py new file mode 100644 index 00000000..dd869410 --- /dev/null +++ b/backoffice/workflows/models.py @@ -0,0 +1,28 @@ +from django.db import models +import uuid + + +class Workflow(models.Model): + PREPROCESSING = 'PREPROCESSING' + APPROVAL = 'APPROVAL' + POSTPROCESSING = 'POSTPROCESSING' + STATUS_CHOICES = ( + (PREPROCESSING, 'Preprocessing'), + (APPROVAL, 'Approval'), + (POSTPROCESSING, 'Postprocessing'), + ) + id = models.UUIDField( + primary_key = True, + default = uuid.uuid4, + editable = False + ) + + data = models.JSONField() + status = models.CharField( + max_length=30, + choices=STATUS_CHOICES, + default=PREPROCESSING, + ) + core = models.BooleanField() + is_update = models.BooleanField() + diff --git a/backoffice/workflows/tests.py b/backoffice/workflows/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/backoffice/workflows/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/backoffice/workflows/tests/test_views.py b/backoffice/workflows/tests/test_views.py new file mode 100644 index 00000000..902d7223 --- /dev/null +++ b/backoffice/workflows/tests/test_views.py @@ -0,0 +1,45 @@ +from rest_framework.test import APIClient +from django.contrib.auth import get_user_model +from django.contrib.auth.models import Group +from django.test import TestCase +from django.apps import apps + +User = get_user_model() +Workflow = apps.get_model(app_label='workflows', model_name='Workflow') + +class TestWorkflowViewSet(TestCase): + endpoint = '/api/workflows/' + + def setUp(self): + self.curator_group = Group.objects.create(name="curator") + self.admin_group = Group.objects.create(name="admin") + + self.curator = User.objects.create_user(email='curator@test.com', password='12345') + self.admin = User.objects.create_user(email='admin@test.com', password='12345') + self.user = User.objects.create_user(email='testuser@test.com', password='12345') + + self.curator.groups.add(self.curator_group) + self.admin.groups.add(self.admin_group) + + self.api_client = APIClient() + self.workflow = Workflow.objects.create(data={}, status='APPROVAL', core=True, is_update=False) + + def test_list_curator(self): + self.api_client.force_authenticate(user=self.curator) + response = self.api_client.get(self.endpoint, format="json") + + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.json()), 1) + + def test_list_admin(self): + self.api_client.force_authenticate(user=self.admin) + response = self.api_client.get(self.endpoint, format="json") + + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.json()), 1) + + def test_list_anonymous(self): + self.api_client.force_authenticate(user=self.user) + response = self.api_client.get(self.endpoint, format="json") + + self.assertEqual(response.status_code, 403) diff --git a/backoffice/workflows/urls.py b/backoffice/workflows/urls.py new file mode 100644 index 00000000..d7216815 --- /dev/null +++ b/backoffice/workflows/urls.py @@ -0,0 +1,8 @@ +from django.contrib import admin +from django.urls import include, path + + +urlpatterns = [ + path("workflows/", include("workflows.urls")), + path("admin/", admin.site.urls), +] \ No newline at end of file diff --git a/config/api_router.py b/config/api_router.py index 5d5a2101..d69da48b 100644 --- a/config/api_router.py +++ b/config/api_router.py @@ -2,6 +2,8 @@ from rest_framework.routers import DefaultRouter, SimpleRouter from backoffice.users.api.views import UserViewSet +from backoffice.workflows.api.views import WorkflowViewSet + if settings.DEBUG: router = DefaultRouter() @@ -10,6 +12,8 @@ router.register("users", UserViewSet) +# Workflows +router.register("workflows", WorkflowViewSet, basename='workflows') app_name = "api" urlpatterns = router.urls diff --git a/config/settings/base.py b/config/settings/base.py index f939c427..d415fae3 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -102,6 +102,8 @@ LOCAL_APPS = [ "backoffice.users", + "backoffice.workflows", + "backoffice.management" # Your stuff: custom apps go here ] # https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps @@ -339,7 +341,7 @@ "rest_framework.authentication.SessionAuthentication", "rest_framework.authentication.TokenAuthentication", ), - "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",), + "DEFAULT_PERMISSION_CLASSES": ('backoffice.management.permissions.IsAdminOrCuratorUser', ), "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", } diff --git a/config/settings/local.py b/config/settings/local.py index d8688274..5f9b81e7 100644 --- a/config/settings/local.py +++ b/config/settings/local.py @@ -31,9 +31,12 @@ # EMAIL # ------------------------------------------------------------------------------ -# https://docs.djangoproject.com/en/dev/ref/settings/#email-backend -EMAIL_BACKEND = env("DJANGO_EMAIL_BACKEND", default="django.core.mail.backends.console.EmailBackend") +AUTHENTICATION_BACKENDS = ( + 'django.contrib.auth.backends.ModelBackend', +) +# Disable the email sending for password reset +EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' # django-debug-toolbar # ------------------------------------------------------------------------------ # https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#prerequisites