diff --git a/gsl/settings.py b/gsl/settings.py index 2c874b1..556f114 100644 --- a/gsl/settings.py +++ b/gsl/settings.py @@ -70,6 +70,7 @@ # gsl apps: "gsl_core", "gsl_demarches_simplifiees", + "gsl_projet", "gsl_pages", "gsl_oidc", ] diff --git a/gsl_core/migrations/0003_commune_departement_region_adresse_and_more.py b/gsl_core/migrations/0003_commune_departement_region_adresse_and_more.py new file mode 100644 index 0000000..927164e --- /dev/null +++ b/gsl_core/migrations/0003_commune_departement_region_adresse_and_more.py @@ -0,0 +1,143 @@ +# Generated by Django 5.1.1 on 2024-11-06 14:22 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("gsl_core", "0002_collegue_proconnect_chorusdt_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="Commune", + fields=[ + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "insee_code", + models.CharField( + primary_key=True, + serialize=False, + unique=True, + verbose_name="Code INSEE", + ), + ), + ("name", models.CharField(verbose_name="Nom")), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="Departement", + fields=[ + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "insee_code", + models.CharField( + primary_key=True, + serialize=False, + unique=True, + verbose_name="Code INSEE", + ), + ), + ("name", models.CharField(verbose_name="Nom")), + ], + options={ + "verbose_name": "Département", + }, + ), + migrations.CreateModel( + name="Region", + fields=[ + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "insee_code", + models.CharField( + primary_key=True, + serialize=False, + unique=True, + verbose_name="Code INSEE", + ), + ), + ("name", models.CharField(verbose_name="Nom")), + ], + options={ + "verbose_name": "Région", + }, + ), + migrations.CreateModel( + name="Adresse", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("label", models.TextField(verbose_name="Adresse complète")), + ("postal_code", models.CharField(verbose_name="Code postal")), + ("street_address", models.CharField(verbose_name="Adresse")), + ( + "commune", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to="gsl_core.commune", + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.AddField( + model_name="commune", + name="departement", + field=models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, to="gsl_core.departement" + ), + ), + migrations.CreateModel( + name="Arrondissement", + fields=[ + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "insee_code", + models.CharField( + primary_key=True, + serialize=False, + unique=True, + verbose_name="Code INSEE", + ), + ), + ("name", models.CharField(verbose_name="Nom")), + ( + "departement", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to="gsl_core.departement", + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.AddField( + model_name="departement", + name="region", + field=models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, to="gsl_core.region" + ), + ), + ] diff --git a/gsl_core/models.py b/gsl_core/models.py index 8692e33..64c74d3 100644 --- a/gsl_core/models.py +++ b/gsl_core/models.py @@ -2,6 +2,17 @@ from django.db import models +class BaseModel(models.Model): + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + abstract = True + + def __str__(self): + return f"{self._meta.verbose_name} {self.pk}" + + class Collegue(AbstractUser): proconnect_sub = models.UUIDField( "Identifiant unique proconnect", null=True, blank=True @@ -18,3 +29,51 @@ class Collegue(AbstractUser): default="", blank=True, ) + + +class Region(BaseModel): + insee_code = models.CharField("Code INSEE", unique=True, primary_key=True) + name = models.CharField("Nom") + + class Meta: + verbose_name = "Région" + + def __str__(self): + return f"Région {self.name}" + + +class Departement(BaseModel): + insee_code = models.CharField("Code INSEE", unique=True, primary_key=True) + name = models.CharField("Nom") + region = models.ForeignKey(Region, on_delete=models.PROTECT) + + class Meta: + verbose_name = "Département" + + def __str__(self): + return f"Département {self.name}" + + +class Commune(BaseModel): + insee_code = models.CharField("Code INSEE", unique=True, primary_key=True) + name = models.CharField("Nom") + departement = models.ForeignKey(Departement, on_delete=models.PROTECT) + + def __str__(self): + return f"Commune {self.insee_code} {self.name}" + + +class Arrondissement(BaseModel): + insee_code = models.CharField("Code INSEE", unique=True, primary_key=True) + name = models.CharField("Nom") + departement = models.ForeignKey(Departement, on_delete=models.PROTECT) + + def __str__(self): + return f"Arrondissement {self.name}" + + +class Adresse(BaseModel): + label = models.TextField("Adresse complète") + postal_code = models.CharField("Code postal") + commune = models.ForeignKey(Commune, on_delete=models.PROTECT) + street_address = models.CharField("Adresse") diff --git a/gsl_core/tests/factories.py b/gsl_core/tests/factories.py new file mode 100644 index 0000000..07e0b57 --- /dev/null +++ b/gsl_core/tests/factories.py @@ -0,0 +1,58 @@ +import factory + +from ..models import Adresse, Arrondissement, Collegue, Commune, Departement, Region + + +class CollegueFactory(factory.django.DjangoModelFactory): + class Meta: + model = Collegue + + username = factory.Faker("user_name") + email = factory.Faker("email") + is_staff = False + is_active = True + + +class RegionFactory(factory.django.DjangoModelFactory): + class Meta: + model = Region + + insee_code = factory.Sequence(lambda n: f"{n}") + name = factory.Faker("word", locale="fr_FR") + + +class DepartementFactory(factory.django.DjangoModelFactory): + class Meta: + model = Departement + + insee_code = factory.Sequence(lambda n: f"{n}") + name = factory.Faker("word", locale="fr_FR") + region = factory.SubFactory(RegionFactory) + + +class ArrondissementFactory(factory.django.DjangoModelFactory): + class Meta: + model = Arrondissement + + insee_code = factory.Sequence(lambda n: f"{n}") + name = factory.Faker("city", locale="fr_FR") + departement = factory.SubFactory(DepartementFactory) + + +class CommuneFactory(factory.django.DjangoModelFactory): + class Meta: + model = Commune + + insee_code = factory.Sequence(lambda n: f"{n}") + name = factory.Faker("city", locale="fr_FR") + departement = factory.SubFactory(DepartementFactory) + + +class AdresseFactory(factory.django.DjangoModelFactory): + class Meta: + model = Adresse + + label = factory.Faker("address", locale="fr_FR") + postal_code = factory.Faker("postcode", locale="fr_FR") + commune = factory.SubFactory(CommuneFactory) + street_address = factory.Faker("street_address", locale="fr_FR") diff --git a/gsl_core/tests/test_factories.py b/gsl_core/tests/test_factories.py new file mode 100644 index 0000000..23bcec4 --- /dev/null +++ b/gsl_core/tests/test_factories.py @@ -0,0 +1,29 @@ +import pytest + +from ..models import Adresse, Arrondissement, Collegue, Commune, Departement, Region +from .factories import ( + AdresseFactory, + ArrondissementFactory, + CollegueFactory, + CommuneFactory, + DepartementFactory, + RegionFactory, +) + +pytestmark = pytest.mark.django_db + +test_data = ( + (CollegueFactory, Collegue), + (RegionFactory, Region), + (DepartementFactory, Departement), + (ArrondissementFactory, Arrondissement), + (CommuneFactory, Commune), + (AdresseFactory, Adresse), +) + + +@pytest.mark.parametrize("factory,expected_class", test_data) +def test_every_factory_can_be_called_twice(factory, expected_class): + for _ in range(2): + obj = factory() + assert isinstance(obj, expected_class) diff --git a/gsl_demarches_simplifiees/admin.py b/gsl_demarches_simplifiees/admin.py index d05bcec..e415aed 100644 --- a/gsl_demarches_simplifiees/admin.py +++ b/gsl_demarches_simplifiees/admin.py @@ -18,6 +18,36 @@ class DemarcheAdmin(admin.ModelAdmin): class DossierAdmin(admin.ModelAdmin): list_filter = ("ds_demarche__ds_number",) list_display = ("ds_number", "ds_demarche__ds_number", "ds_state") + fieldsets = ( + ( + "Informations générales", + {"fields": ("ds_demarche", "ds_id", "ds_number", "ds_state")}, + ), + ( + "Champs DS", + { + "classes": ("collapse", "open"), + "fields": tuple(field.name for field in Dossier.MAPPED_FIELDS), + }, + ), + ( + "Dates", + { + "classes": ("collapse", "open"), + "fields": ( + "ds_date_depot", + "ds_date_passage_en_construction", + "ds_date_passage_en_instruction", + "ds_date_derniere_modification", + "ds_date_derniere_modification_champs", + ), + }, + ), + ( + "Données brutes", + {"classes": ("collapse", "open"), "fields": ("raw_ds_data",)}, + ), + ) @admin.register(FieldMappingForHuman) diff --git a/gsl_demarches_simplifiees/tests/__init__.py b/gsl_demarches_simplifiees/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gsl_demarches_simplifiees/tests/factories.py b/gsl_demarches_simplifiees/tests/factories.py new file mode 100644 index 0000000..afa98ae --- /dev/null +++ b/gsl_demarches_simplifiees/tests/factories.py @@ -0,0 +1,23 @@ +import factory + +from ..models import Demarche, Dossier + + +class DemarcheFactory(factory.django.DjangoModelFactory): + class Meta: + model = Demarche + + ds_id = factory.Sequence(lambda n: f"demarche-{n}") + ds_number = factory.Faker("random_int", min=1000000, max=9999999) + ds_title = "Titre de la démarche" + ds_state = Demarche.STATE_PUBLIEE + + +class DossierFactory(factory.django.DjangoModelFactory): + class Meta: + model = Dossier + + ds_demarche = factory.SubFactory(DemarcheFactory) + ds_id = factory.Sequence(lambda n: f"dossier-{n}") + ds_number = factory.Faker("random_int", min=1000000, max=9999999) + ds_state = Dossier.STATE_ACCEPTE diff --git a/gsl_demarches_simplifiees/tests/test_factories.py b/gsl_demarches_simplifiees/tests/test_factories.py new file mode 100644 index 0000000..5e64a07 --- /dev/null +++ b/gsl_demarches_simplifiees/tests/test_factories.py @@ -0,0 +1,22 @@ +import pytest + +from gsl_demarches_simplifiees.models import ( + Demarche, + Dossier, +) + +from .factories import DemarcheFactory, DossierFactory + +pytestmark = pytest.mark.django_db + +test_data = ( + (DemarcheFactory, Demarche), + (DossierFactory, Dossier), +) + + +@pytest.mark.parametrize("factory,expected_class", test_data) +def test_every_factory_can_be_called_twice(factory, expected_class): + for _ in range(2): + obj = factory() + assert isinstance(obj, expected_class) diff --git a/gsl_projet/__init__.py b/gsl_projet/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gsl_projet/admin.py b/gsl_projet/admin.py new file mode 100644 index 0000000..d622ceb --- /dev/null +++ b/gsl_projet/admin.py @@ -0,0 +1,13 @@ +from django.contrib import admin + +from .models import Demandeur, Projet + + +@admin.register(Demandeur) +class DemandeurAdmin(admin.ModelAdmin): + pass + + +@admin.register(Projet) +class ProjetAdmin(admin.ModelAdmin): + pass diff --git a/gsl_projet/apps.py b/gsl_projet/apps.py new file mode 100644 index 0000000..e63aa74 --- /dev/null +++ b/gsl_projet/apps.py @@ -0,0 +1,8 @@ +from django.apps import AppConfig + + +class GslProjetConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "gsl_projet" + + verbose_name = "3. Projets" diff --git a/gsl_projet/migrations/0001_initial.py b/gsl_projet/migrations/0001_initial.py new file mode 100644 index 0000000..279ce09 --- /dev/null +++ b/gsl_projet/migrations/0001_initial.py @@ -0,0 +1,102 @@ +# Generated by Django 5.1.1 on 2024-11-06 14:22 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + ("gsl_core", "0003_commune_departement_region_adresse_and_more"), + ("gsl_demarches_simplifiees", "0001_initial"), + ] + + operations = [ + migrations.CreateModel( + name="Demandeur", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("siret", models.CharField(verbose_name="Siret")), + ("name", models.CharField(verbose_name="Nom")), + ( + "address", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + to="gsl_core.adresse", + ), + ), + ( + "arrondissement", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to="gsl_core.arrondissement", + ), + ), + ( + "departement", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to="gsl_core.departement", + ), + ), + ], + ), + migrations.CreateModel( + name="Projet", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "address", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + to="gsl_core.adresse", + ), + ), + ( + "arrondissement", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to="gsl_core.arrondissement", + ), + ), + ( + "demandeur", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to="gsl_projet.demandeur", + ), + ), + ( + "departement", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to="gsl_core.departement", + ), + ), + ( + "dossier_ds", + models.OneToOneField( + on_delete=django.db.models.deletion.PROTECT, + to="gsl_demarches_simplifiees.dossier", + ), + ), + ], + ), + ] diff --git a/gsl_projet/migrations/__init__.py b/gsl_projet/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gsl_projet/models.py b/gsl_projet/models.py new file mode 100644 index 0000000..2f4fe80 --- /dev/null +++ b/gsl_projet/models.py @@ -0,0 +1,37 @@ +from django.db import models + +from gsl_core.models import Adresse, Arrondissement, Collegue, Departement +from gsl_demarches_simplifiees.models import Dossier + + +class Demandeur(models.Model): + siret = models.CharField("Siret") + name = models.CharField("Nom") + + address = models.OneToOneField(Adresse, on_delete=models.CASCADE) + arrondissement = models.ForeignKey(Arrondissement, on_delete=models.PROTECT) + departement = models.ForeignKey(Departement, on_delete=models.PROTECT) + + def __str__(self): + return f"Demandeur {self.name}" + + +class ProjetManager(models.Manager): + def for_user(self, user: Collegue): + return self.filter( + dossier_ds__ds_demarche__ds_instructeurs__ds_email=user.email + ) + + +class Projet(models.Model): + demandeur = models.ForeignKey(Demandeur, on_delete=models.PROTECT) + dossier_ds = models.OneToOneField(Dossier, on_delete=models.PROTECT) + + address = models.OneToOneField(Adresse, on_delete=models.CASCADE) + arrondissement = models.ForeignKey(Arrondissement, on_delete=models.PROTECT) + departement = models.ForeignKey(Departement, on_delete=models.PROTECT) + + objects = ProjetManager() + + def __str__(self): + return f"Projet {self.pk}" diff --git a/gsl_projet/tests/__init__.py b/gsl_projet/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gsl_projet/tests/factories.py b/gsl_projet/tests/factories.py new file mode 100644 index 0000000..4b13a80 --- /dev/null +++ b/gsl_projet/tests/factories.py @@ -0,0 +1,34 @@ +import factory + +from gsl_core.tests.factories import ( + AdresseFactory, + ArrondissementFactory, + DepartementFactory, +) +from gsl_demarches_simplifiees.tests.factories import DossierFactory + +from ..models import Demandeur, Projet + + +class DemandeurFactory(factory.django.DjangoModelFactory): + class Meta: + model = Demandeur + + siret = factory.Sequence(lambda n: f"siret-{n}") + name = factory.Faker("city", locale="fr_FR") + + address = factory.SubFactory(AdresseFactory) + arrondissement = factory.SubFactory(ArrondissementFactory) + departement = factory.SubFactory(DepartementFactory) + + +class ProjetFactory(factory.django.DjangoModelFactory): + class Meta: + model = Projet + + demandeur = factory.SubFactory(DemandeurFactory) + dossier_ds = factory.SubFactory(DossierFactory) + + address = factory.SubFactory(AdresseFactory) + arrondissement = factory.SubFactory(ArrondissementFactory) + departement = factory.SubFactory(DepartementFactory) diff --git a/gsl_projet/tests/test_factories.py b/gsl_projet/tests/test_factories.py new file mode 100644 index 0000000..f0d6317 --- /dev/null +++ b/gsl_projet/tests/test_factories.py @@ -0,0 +1,21 @@ +import pytest + +from ..models import ( + Demandeur, + Projet, +) +from .factories import DemandeurFactory, ProjetFactory + +pytestmark = pytest.mark.django_db + +test_data = ( + (DemandeurFactory, Demandeur), + (ProjetFactory, Projet), +) + + +@pytest.mark.parametrize("factory,expected_class", test_data) +def test_every_factory_can_be_called_twice(factory, expected_class): + for _ in range(2): + obj = factory() + assert isinstance(obj, expected_class) diff --git a/gsl_projet/tests/test_model_projet.py b/gsl_projet/tests/test_model_projet.py new file mode 100644 index 0000000..622124d --- /dev/null +++ b/gsl_projet/tests/test_model_projet.py @@ -0,0 +1,25 @@ +import pytest + +from gsl_core.tests.factories import CollegueFactory +from gsl_demarches_simplifiees.models import Profile +from gsl_demarches_simplifiees.tests.factories import DossierFactory + +from ..models import Projet +from .factories import ProjetFactory + +pytestmark = pytest.mark.django_db + + +def test_user_can_see_a_projet_if_they_are_explicit_instructeur(): + user = CollegueFactory() + dossier = DossierFactory() + dossier.ds_demarche.ds_instructeurs.add(Profile.objects.create(ds_email=user.email)) + + unrelated_projet = ProjetFactory() + related_projet = ProjetFactory(dossier_ds=dossier) + + assert Projet.objects.count() == 2 + assert Projet.objects.for_user(user).count() == 1 + projets_for_user = Projet.objects.for_user(user).all() + assert unrelated_projet not in projets_for_user + assert related_projet in projets_for_user diff --git a/gsl_projet/views.py b/gsl_projet/views.py new file mode 100644 index 0000000..60f00ef --- /dev/null +++ b/gsl_projet/views.py @@ -0,0 +1 @@ +# Create your views here. diff --git a/pyproject.toml b/pyproject.toml index 0e4d3bb..976d7ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,7 @@ dev = [ "diff-cover", "pytest-xdist", "pre-commit", + "factory_boy", ] [tool.pytest.ini_options] diff --git a/requirements-dev.txt b/requirements-dev.txt index 5978c44..09baf68 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -86,6 +86,10 @@ django-widget-tweaks==1.5.0 # via django-dsfr execnet==2.1.1 # via pytest-xdist +factory-boy==3.3.1 + # via gsl (pyproject.toml) +faker==30.8.2 + # via factory-boy filelock==3.16.1 # via virtualenv identify==2.6.1 @@ -146,6 +150,7 @@ python-crontab==3.2.0 python-dateutil==2.9.0.post0 # via # celery + # faker # python-crontab python-dotenv==1.0.1 # via gsl (pyproject.toml) @@ -170,7 +175,9 @@ sqlparse==0.5.1 tabulate==0.9.0 # via django-query-counter typing-extensions==4.12.2 - # via dj-database-url + # via + # dj-database-url + # faker tzdata==2024.2 # via # celery