From 773e47b4f5d84e1475e3ab4b1c754d3a4793e724 Mon Sep 17 00:00:00 2001 From: ssorin Date: Thu, 21 Nov 2024 10:59:56 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(dashboard)=20add=20consent=20model?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit add consent models add basic content admin create core app add entity and delivery_point models add basic entity and delivery_point admin add unitest for models consent and core update CHANGELOG.md --- src/dashboard/CHANGELOG.md | 3 +- src/dashboard/Pipfile | 2 +- src/dashboard/Pipfile.lock | 20 ++-- src/dashboard/apps/consent/admin.py | 12 ++ .../apps/consent/migrations/0001_initial.py | 88 ++++++++++++++ src/dashboard/apps/consent/models.py | 61 ++++++++++ src/dashboard/apps/consent/tests/__init__.py | 1 + .../apps/consent/tests/test_models.py | 80 +++++++++++++ src/dashboard/apps/core/__init__.py | 1 + src/dashboard/apps/core/admin.py | 19 +++ src/dashboard/apps/core/apps.py | 13 ++ .../apps/core/migrations/0001_initial.py | 108 +++++++++++++++++ .../apps/core/migrations/__init__.py | 1 + src/dashboard/apps/core/models.py | 76 ++++++++++++ src/dashboard/apps/core/tests/__init__.py | 1 + src/dashboard/apps/core/tests/test_models.py | 112 ++++++++++++++++++ src/dashboard/dashboard/settings.py | 1 + 17 files changed, 587 insertions(+), 12 deletions(-) create mode 100644 src/dashboard/apps/consent/admin.py create mode 100644 src/dashboard/apps/consent/migrations/0001_initial.py create mode 100644 src/dashboard/apps/consent/models.py create mode 100644 src/dashboard/apps/consent/tests/__init__.py create mode 100644 src/dashboard/apps/consent/tests/test_models.py create mode 100644 src/dashboard/apps/core/__init__.py create mode 100644 src/dashboard/apps/core/admin.py create mode 100644 src/dashboard/apps/core/apps.py create mode 100644 src/dashboard/apps/core/migrations/0001_initial.py create mode 100644 src/dashboard/apps/core/migrations/__init__.py create mode 100644 src/dashboard/apps/core/models.py create mode 100644 src/dashboard/apps/core/tests/__init__.py create mode 100644 src/dashboard/apps/core/tests/test_models.py diff --git a/src/dashboard/CHANGELOG.md b/src/dashboard/CHANGELOG.md index dd068fb0..36884042 100644 --- a/src/dashboard/CHANGELOG.md +++ b/src/dashboard/CHANGELOG.md @@ -15,7 +15,8 @@ and this project adheres to - add internationalization and language switcher - add authentication system - introduce new custom user model -- add consent base app +- add consent app with Consent model +- add core app with Entity and DeliveryPoint models [unreleased]: https://github.com/MTES-MCT/qualicharge/compare/main...bootstrap-dashboard-project diff --git a/src/dashboard/Pipfile b/src/dashboard/Pipfile index 6296de6f..ec6d1a31 100644 --- a/src/dashboard/Pipfile +++ b/src/dashboard/Pipfile @@ -7,13 +7,13 @@ name = "pypi" Django = "==5.1.3" django-dsfr = "==1.4.3" django-environ = "==0.11.2" -django-extensions = "==3.2.3" gunicorn = "==23.0.0" psycopg = {extras = ["pool", "binary"], version = "==3.2.3"} whitenoise = "==6.8.2" [dev-packages] black = "==24.10.0" +django-extensions = "==3.2.3" django-stubs = {extras = ["compatible-mypy"], version = "==5.1.1"} djlint = "==1.36.1" honcho = "==2.0.0" diff --git a/src/dashboard/Pipfile.lock b/src/dashboard/Pipfile.lock index 8aa7751a..b843f186 100644 --- a/src/dashboard/Pipfile.lock +++ b/src/dashboard/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "0399df23fe8919eca6a084ad566df1695ab55efd60e5bc4ef69fcccab876f80f" + "sha256": "3f32e0803e61d9eabc94751e7dd8736f4cd7b24c2196e55d4fad247527c5b9aa" }, "pipfile-spec": 6, "requires": { @@ -178,15 +178,6 @@ "markers": "python_version >= '3.6' and python_version < '4'", "version": "==0.11.2" }, - "django-extensions": { - "hashes": [ - "sha256:44d27919d04e23b3f40231c4ab7af4e61ce832ef46d610cc650d53e68328410a", - "sha256:9600b7562f79a92cbf1fde6403c04fee314608fefbb595502e34383ae8203401" - ], - "index": "pypi", - "markers": "python_version >= '3.6'", - "version": "==3.2.3" - }, "django-widget-tweaks": { "hashes": [ "sha256:1c2180681ebb994e922c754804c7ffebbe1245014777ac47897a81f57cc629c7", @@ -508,6 +499,15 @@ "markers": "python_version >= '3.10'", "version": "==5.1.3" }, + "django-extensions": { + "hashes": [ + "sha256:44d27919d04e23b3f40231c4ab7af4e61ce832ef46d610cc650d53e68328410a", + "sha256:9600b7562f79a92cbf1fde6403c04fee314608fefbb595502e34383ae8203401" + ], + "index": "pypi", + "markers": "python_version >= '3.6'", + "version": "==3.2.3" + }, "django-stubs": { "extras": [ "compatible-mypy" diff --git a/src/dashboard/apps/consent/admin.py b/src/dashboard/apps/consent/admin.py new file mode 100644 index 00000000..4ec9042f --- /dev/null +++ b/src/dashboard/apps/consent/admin.py @@ -0,0 +1,12 @@ +"""Dashboard consent admin.""" + +from django.contrib import admin + +from .models import Consent + + +@admin.register(Consent) +class ConsentAdmin(admin.ModelAdmin): + """Consent admin.""" + + pass diff --git a/src/dashboard/apps/consent/migrations/0001_initial.py b/src/dashboard/apps/consent/migrations/0001_initial.py new file mode 100644 index 00000000..5dbb4598 --- /dev/null +++ b/src/dashboard/apps/consent/migrations/0001_initial.py @@ -0,0 +1,88 @@ +# Generated by Django 5.1.3 on 2024-11-21 11:26 + +import django.db.models.deletion +import django.utils.timezone +import uuid +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("qcd_core", "0001_initial"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="Consent", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ( + "created_at", + models.DateTimeField( + default=django.utils.timezone.now, + editable=False, + verbose_name="created at", + ), + ), + ( + "updated_at", + models.DateTimeField( + blank=True, null=True, verbose_name="updated at" + ), + ), + ( + "status", + models.CharField( + choices=[ + ("AWAITING", "Awaiting"), + ("VALIDATED", "Validated"), + ("REVOKED", "Revoked"), + ], + default="AWAITING", + max_length=20, + verbose_name="status", + ), + ), + ("start", models.DateTimeField(verbose_name="start date")), + ("end", models.DateTimeField(verbose_name="end date")), + ( + "revoked_at", + models.DateTimeField( + blank=True, null=True, verbose_name="revoked at" + ), + ), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to=settings.AUTH_USER_MODEL, + verbose_name="created by", + ), + ), + ( + "delivery_point", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="qcd_core.deliverypoint", + ), + ), + ], + options={ + "ordering": ["delivery_point"], + }, + ), + ] diff --git a/src/dashboard/apps/consent/models.py b/src/dashboard/apps/consent/models.py new file mode 100644 index 00000000..cda5d1eb --- /dev/null +++ b/src/dashboard/apps/consent/models.py @@ -0,0 +1,61 @@ +"""Dashboard consent app models.""" + +from django.db import models +from django.utils import timezone +from django.utils.translation import gettext_lazy as _ + +from apps.auth.models import DashboardUser as User +from apps.core.models import DashboardBase, DeliveryPoint + + +class Consent(DashboardBase): + """Represents the consent status for a given delivery point and user. + + Attributes: + - AWAITING: Status indicating that the consent is awaiting validation. + - VALIDATED: Status indicating that the consent has been validated. + - REVOKED: Status indicating that the consent has been revoked. + + - delivery_point (ForeignKey): relation to the delivery point associated with the + consent. + - created_by (ForeignKey): relation to the user giving the consent. + - status (CharField): storing the status of the consent, with choices constrained + by CONSENT_STATUS_CHOICE. + - start (DateTimeField): representing the start date of the consent validity. + - end (DateTimeField): representing the end date of the consent validity. + - revoked_at (DateTimeField): recording the revoked date of the consent, if any. + """ + + AWAITING = "AWAITING" + VALIDATED = "VALIDATED" + REVOKED = "REVOKED" + CONSENT_STATUS_CHOICE = [ + (AWAITING, _("Awaiting")), + (VALIDATED, _("Validated")), + (REVOKED, _("Revoked")), + ] + + delivery_point = models.ForeignKey(DeliveryPoint, on_delete=models.CASCADE) + created_by = models.ForeignKey( + User, on_delete=models.SET_NULL, null=True, verbose_name=_("created by") + ) + status = models.CharField( + _("status"), max_length=20, choices=CONSENT_STATUS_CHOICE, default=AWAITING + ) + + # Validity period + start = models.DateTimeField(_("start date")) + end = models.DateTimeField(_("end date")) + revoked_at = models.DateTimeField(_("revoked at"), null=True, blank=True) + + class Meta: # noqa: D106 + ordering = ["delivery_point"] + + def __str__(self): # noqa: D105 + return f"{self.delivery_point} - {self.updated_at}: {self.status}" + + def save(self, *args, **kwargs): + """Update the revoked_at timestamps if the consent is revoked.""" + if self.status == self.REVOKED: + self.revoked_at = timezone.now() + return super(Consent, self).save(*args, **kwargs) diff --git a/src/dashboard/apps/consent/tests/__init__.py b/src/dashboard/apps/consent/tests/__init__.py new file mode 100644 index 00000000..8bd73224 --- /dev/null +++ b/src/dashboard/apps/consent/tests/__init__.py @@ -0,0 +1 @@ +"""Dashboard consent app tests.""" diff --git a/src/dashboard/apps/consent/tests/test_models.py b/src/dashboard/apps/consent/tests/test_models.py new file mode 100644 index 00000000..f79170d0 --- /dev/null +++ b/src/dashboard/apps/consent/tests/test_models.py @@ -0,0 +1,80 @@ +"""Dashboard consent models tests.""" + +from datetime import timedelta + +import pytest +from django.contrib.auth import get_user_model +from django.utils import formats, timezone + +from apps.consent.models import Consent +from apps.core.models import DeliveryPoint + + +@pytest.mark.django_db +def test_create_consent(): + """Tests the creation of a consent.""" + # create user + User = get_user_model() + user1 = User.objects.create_user(username="user1", password="foo") # noqa: S106 + + # create delivery point + delivery_point = DeliveryPoint.objects.create(provider_id="provider_1234") + + # create consent + consent = Consent.objects.create( + delivery_point=delivery_point, + created_by=user1, + start=timezone.now(), + end=timezone.now() + timedelta(days=90), + ) + assert consent.delivery_point == delivery_point + assert consent.created_by == user1 + assert consent.status == Consent.AWAITING + assert consent.revoked_at is None + assert consent.start is not None + assert consent.end is not None + + # test consent.end is 90 days later than the consent.start + end_date = consent.start + timedelta(days=90) + consent_start = formats.date_format(end_date, "Y/m/d") + consent_end = formats.date_format(consent.end, "Y/m/d") + assert consent_start == consent_end + + # test created_at and updated_at have been updated. + assert consent.created_at is not None + assert consent.updated_at is not None + + +@pytest.mark.django_db +def test_update_consent_status(): + """Tests updating a consent status.""" + # create user + User = get_user_model() + user1 = User.objects.create_user(username="user1", password="foo") # noqa: S106 + + # create delivery point + delivery_point = DeliveryPoint.objects.create(provider_id="provider_1234") + + # create consent + consent = Consent.objects.create( + delivery_point=delivery_point, + created_by=user1, + start=timezone.now(), + end=timezone.now() + timedelta(days=90), + ) + new_updated_at = consent.updated_at + + # update status to VALIDATED + consent.status = Consent.VALIDATED + consent.save() + assert consent.status == Consent.VALIDATED + assert consent.updated_at > new_updated_at + assert consent.revoked_at is None + new_updated_at = consent.updated_at + + # update status to REVOKED + consent.status = Consent.REVOKED + consent.save() + assert consent.status == Consent.REVOKED + assert consent.updated_at > new_updated_at + assert consent.revoked_at is not None diff --git a/src/dashboard/apps/core/__init__.py b/src/dashboard/apps/core/__init__.py new file mode 100644 index 00000000..8c629ff5 --- /dev/null +++ b/src/dashboard/apps/core/__init__.py @@ -0,0 +1 @@ +"""Dashboard core app.""" diff --git a/src/dashboard/apps/core/admin.py b/src/dashboard/apps/core/admin.py new file mode 100644 index 00000000..333db61b --- /dev/null +++ b/src/dashboard/apps/core/admin.py @@ -0,0 +1,19 @@ +"""Dashboard core admin.""" + +from django.contrib import admin + +from .models import DeliveryPoint, Entity + + +@admin.register(Entity) +class EntityAdmin(admin.ModelAdmin): + """Entity admin.""" + + pass + + +@admin.register(DeliveryPoint) +class DeliveryPointAdmin(admin.ModelAdmin): + """Delivery point admin.""" + + pass diff --git a/src/dashboard/apps/core/apps.py b/src/dashboard/apps/core/apps.py new file mode 100644 index 00000000..9daf4d8d --- /dev/null +++ b/src/dashboard/apps/core/apps.py @@ -0,0 +1,13 @@ +"""Dashboard core app base config.""" + +from django.apps import AppConfig +from django.utils.translation import gettext_lazy as _ + + +class CoreConfig(AppConfig): + """Core app config.""" + + default_auto_field = "django.db.models.BigAutoField" + name = "apps.core" + label = "qcd_core" + verbose_name = _("Core") diff --git a/src/dashboard/apps/core/migrations/0001_initial.py b/src/dashboard/apps/core/migrations/0001_initial.py new file mode 100644 index 00000000..a49460cd --- /dev/null +++ b/src/dashboard/apps/core/migrations/0001_initial.py @@ -0,0 +1,108 @@ +# Generated by Django 5.1.3 on 2024-11-21 11:26 + +import django.utils.timezone +import uuid +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="Entity", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ( + "created_at", + models.DateTimeField( + default=django.utils.timezone.now, + editable=False, + verbose_name="created at", + ), + ), + ( + "updated_at", + models.DateTimeField( + blank=True, null=True, verbose_name="updated at" + ), + ), + ( + "name", + models.CharField(max_length=64, unique=True, verbose_name="name"), + ), + ( + "users", + models.ManyToManyField( + to=settings.AUTH_USER_MODEL, verbose_name="users" + ), + ), + ], + options={ + "verbose_name": "Entity", + "verbose_name_plural": "Entities", + "ordering": ["name"], + }, + ), + migrations.CreateModel( + name="DeliveryPoint", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ( + "created_at", + models.DateTimeField( + default=django.utils.timezone.now, + editable=False, + verbose_name="created at", + ), + ), + ( + "updated_at", + models.DateTimeField( + blank=True, null=True, verbose_name="updated at" + ), + ), + ( + "provider_id", + models.CharField(max_length=64, verbose_name="provider id"), + ), + ( + "is_active", + models.BooleanField(default=True, verbose_name="is active"), + ), + ( + "entities", + models.ManyToManyField( + to="qcd_core.entity", verbose_name="entities" + ), + ), + ], + options={ + "verbose_name": "Delivery point", + "verbose_name_plural": "Delivery points", + "ordering": ["provider_id"], + }, + ), + ] diff --git a/src/dashboard/apps/core/migrations/__init__.py b/src/dashboard/apps/core/migrations/__init__.py new file mode 100644 index 00000000..df9a66c9 --- /dev/null +++ b/src/dashboard/apps/core/migrations/__init__.py @@ -0,0 +1 @@ +"""Dashboard core app migrations.""" diff --git a/src/dashboard/apps/core/models.py b/src/dashboard/apps/core/models.py new file mode 100644 index 00000000..91eb4efb --- /dev/null +++ b/src/dashboard/apps/core/models.py @@ -0,0 +1,76 @@ +"""Dashboard core app models.""" + +import uuid + +from django.db import models +from django.utils import timezone +from django.utils.translation import gettext_lazy as _ + +from apps.auth.models import DashboardUser as User + + +class DashboardBase(models.Model): + """Abstract base model, providing common fields and functionality. + + Attributes: + - id (UUIDField): serves as the primary key, automatically generated, not editable. + - created_at (DateTimeField): records when the object was created, not editable by + default. + - updated_at (DateTimeField): records when the object was last updated, editable. + """ + + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + created_at = models.DateTimeField( + _("created at"), editable=False, default=timezone.now + ) + updated_at = models.DateTimeField(_("updated at"), null=True, blank=True) + + class Meta: # noqa: D106 + abstract = True + + def save(self, *args, **kwargs): + """Update the updated_at timestamps.""" + self.updated_at = timezone.now() + return super(DashboardBase, self).save(*args, **kwargs) + + +class Entity(DashboardBase): + """Represents an operator or an aggregator in the system. + + Attributes: + - name (CharField): Name of the entity, unique and maximum length of 64. + - users (ManyToManyField): Users associated with the entity. + """ + + name = models.CharField(_("name"), max_length=64, unique=True) + users = models.ManyToManyField(User, verbose_name=_("users")) + + class Meta: # noqa: D106 + verbose_name = "Entity" + verbose_name_plural = "Entities" + ordering = ["name"] + + def __str__(self): # noqa: D105 + return self.name + + +class DeliveryPoint(DashboardBase): + """Represents a delivery point for electric vehicles. + + Attributes: + - provider_id (CharField): stores the unique identifier for the delivery provider. + - entities (ManyToManyField): linking DeliveryPoint to associated Entity instances. + - is_active (BooleanField): indicating the active status of the delivery point. + """ + + provider_id = models.CharField(_("provider id"), max_length=64) + entities = models.ManyToManyField(Entity, verbose_name=_("entities")) + is_active = models.BooleanField(_("is active"), default=True) + + class Meta: # noqa: D106 + verbose_name = _("Delivery point") + verbose_name_plural = _("Delivery points") + ordering = ["provider_id"] + + def __str__(self): # noqa: D105 + return self.provider_id diff --git a/src/dashboard/apps/core/tests/__init__.py b/src/dashboard/apps/core/tests/__init__.py new file mode 100644 index 00000000..64c59ed7 --- /dev/null +++ b/src/dashboard/apps/core/tests/__init__.py @@ -0,0 +1 @@ +"""Dashboard core app tests.""" diff --git a/src/dashboard/apps/core/tests/test_models.py b/src/dashboard/apps/core/tests/test_models.py new file mode 100644 index 00000000..877ec4fb --- /dev/null +++ b/src/dashboard/apps/core/tests/test_models.py @@ -0,0 +1,112 @@ +"""Dashboard core models tests.""" + +import pytest +from django.contrib.auth import get_user_model +from django.db import IntegrityError + +from apps.core.models import DeliveryPoint, Entity + + +@pytest.mark.django_db +def test_create_entity(): + """Tests the creation of an entity.""" + User = get_user_model() + user1 = User.objects.create_user(username="user1", password="foo") # noqa: S106 + user2 = User.objects.create_user(username="user2", password="foo") # noqa: S106 + + entity = Entity.objects.create(name="abc_entity") + entity.users.add(user1) + entity.users.add(user2) + entity.save() + + # test users have been added. + assert entity.name == "abc_entity" + assert all(user in [user1, user2] for user in entity.users.all()) + + # test created_at and updated_at have been updated. + assert entity.created_at is not None + assert entity.updated_at is not None + + # test IntegrityError: name must not be null + with pytest.raises(IntegrityError): + Entity.objects.create(name=None) + + +@pytest.mark.django_db +def test_update_entity(): + """Tests updating an entity.""" + User = get_user_model() + user1 = User.objects.create_user(username="user1", password="foo") # noqa: S106 + + entity = Entity.objects.create(name="abc_entity") + entity.users.add(user1) + entity.save() + + # test user1 have been removed + entity.users.remove(user1) + assert all(user != user1 for user in entity.users.all()) + + # test updated_at has been updated + assert entity.updated_at > entity.created_at + + +@pytest.mark.django_db +def test_create_delivery_point(): + """Tests the creation of a delivery point.""" + # create users + User = get_user_model() + user1 = User.objects.create_user(username="user1", password="foo") # noqa: S106 + + # create entities + entity1 = Entity.objects.create(name="entity_1") + entity1.users.add(user1) + entity1.save() + + entity2 = Entity.objects.create(name="entity_2") + entity2.users.add(user1) + entity2.save() + + # create delivery point + delivery_point = DeliveryPoint.objects.create(provider_id="provider_1234") + delivery_point.entities.add(entity1) + delivery_point.entities.add(entity2) + delivery_point.save() + + assert delivery_point.provider_id == "provider_1234" + assert delivery_point.is_active is True + + # test entities have been added to delivery point. + assert all(entity in [entity1, entity2] for entity in delivery_point.entities.all()) + + # test created_at and updated_at have been updated. + assert delivery_point.created_at is not None + assert delivery_point.updated_at is not None + + # test IntegrityError: provider must not be null + with pytest.raises(IntegrityError): + DeliveryPoint.objects.create(provider_id=None) + + +@pytest.mark.django_db +def test_update_delivery_point(): + """Tests updating a delivery point.""" + # create users + User = get_user_model() + user1 = User.objects.create_user(username="user1", password="foo") # noqa: S106 + + # create entity + entity1 = Entity.objects.create(name="entity_1") + entity1.users.add(user1) + entity1.save() + + # create delivery point + delivery_point = DeliveryPoint.objects.create(provider_id="provider_1234") + delivery_point.entities.add(entity1) + delivery_point.save() + + # test entity1 have been removed + delivery_point.entities.remove(entity1) + assert all(entity != entity1 for entity in delivery_point.entities.all()) + + # test updated_at has been updated + assert delivery_point.updated_at > delivery_point.created_at diff --git a/src/dashboard/dashboard/settings.py b/src/dashboard/dashboard/settings.py index 4d97216e..710a5794 100644 --- a/src/dashboard/dashboard/settings.py +++ b/src/dashboard/dashboard/settings.py @@ -50,6 +50,7 @@ "dashboard", "apps", "apps.auth", + "apps.core", "apps.home", "apps.consent", ]