From 91d3b6a78b0aa0cb6d76a48080f28d8c45b23bd7 Mon Sep 17 00:00:00 2001 From: Helen Root Date: Wed, 8 Jan 2025 14:07:57 +0100 Subject: [PATCH 01/40] Pass synonymes to replacement modal; start layout --- .../DeclaredElementPage/ReplacementSearch.vue | 3 ++- .../src/views/DeclaredElementPage/index.vue | 26 +++++++++++++++++-- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/frontend/src/views/DeclaredElementPage/ReplacementSearch.vue b/frontend/src/views/DeclaredElementPage/ReplacementSearch.vue index c0d981765..5a0b01b93 100644 --- a/frontend/src/views/DeclaredElementPage/ReplacementSearch.vue +++ b/frontend/src/views/DeclaredElementPage/ReplacementSearch.vue @@ -28,11 +28,12 @@ const selectedOption = ref() const selectOption = (option) => { selectedOption.value = option // quick and temporary display + emit("replacement", option) fetchElement(getApiType(option.objectType), option.objectType, option.id).then((item) => { selectedOption.value = item + emit("replacement", item) }) // TODO: erase search term from search bar? - emit("replacement", option) } // TODO: turn into service ? It was taken from another file diff --git a/frontend/src/views/DeclaredElementPage/index.vue b/frontend/src/views/DeclaredElementPage/index.vue index c4cc38bde..6f84cca2e 100644 --- a/frontend/src/views/DeclaredElementPage/index.vue +++ b/frontend/src/views/DeclaredElementPage/index.vue @@ -19,6 +19,21 @@ Ce n'est pas possible pour l'instant de remplacer une demande avec un ingrédient d'un type different. Veuillez contacter l'équipe Compl'Alim pour effectuer la substitution.

+
+

Synonymes

+
+ + + +
+ +
@@ -77,7 +92,8 @@ watch(element, (newElement) => { }) // Actions -const modalToOpen = ref(false) +// const modalToOpen = ref(false) +const modalToOpen = ref("replace") const closeModal = () => (modalToOpen.value = false) const notes = ref() @@ -89,7 +105,13 @@ const openModal = (type) => { } } -const replacement = ref() +// const replacement = ref() +const replacement = ref({ + id: 3, + name: "Test modal", + synonyms: [{ id: 1, name: "My existing synonym" }], + objectType: props.type, +}) const cannotReplace = computed(() => replacement.value?.objectType !== element.value.type) const actionButtons = computed(() => [ From ab1714e4e96dece4ad05d2496d4e6a7d391bc433 Mon Sep 17 00:00:00 2001 From: Helen Root Date: Wed, 8 Jan 2025 14:21:23 +0100 Subject: [PATCH 02/40] Move to own file --- .../DeclaredElementPage/ManageSynonyms.vue | 38 +++++++++++++++++++ .../src/views/DeclaredElementPage/index.vue | 18 ++------- 2 files changed, 41 insertions(+), 15 deletions(-) create mode 100644 frontend/src/views/DeclaredElementPage/ManageSynonyms.vue diff --git a/frontend/src/views/DeclaredElementPage/ManageSynonyms.vue b/frontend/src/views/DeclaredElementPage/ManageSynonyms.vue new file mode 100644 index 000000000..f8d83006f --- /dev/null +++ b/frontend/src/views/DeclaredElementPage/ManageSynonyms.vue @@ -0,0 +1,38 @@ + + + diff --git a/frontend/src/views/DeclaredElementPage/index.vue b/frontend/src/views/DeclaredElementPage/index.vue index 6f84cca2e..1e068d7cd 100644 --- a/frontend/src/views/DeclaredElementPage/index.vue +++ b/frontend/src/views/DeclaredElementPage/index.vue @@ -20,19 +20,7 @@ Veuillez contacter l'équipe Compl'Alim pour effectuer la substitution.

-

Synonymes

-
- - - -
- +
@@ -55,6 +43,7 @@ import { headers } from "@/utils/data-fetching" import ElementInfo from "./ElementInfo" import ElementAlert from "./ElementAlert" import ReplacementSearch from "./ReplacementSearch" +import ManageSynonyms from "./ManageSynonyms" const props = defineProps({ type: String, id: String }) @@ -92,8 +81,7 @@ watch(element, (newElement) => { }) // Actions -// const modalToOpen = ref(false) -const modalToOpen = ref("replace") +const modalToOpen = ref(false) const closeModal = () => (modalToOpen.value = false) const notes = ref() From f9a076048bfb7c21bfeb88f59b4271422685a4fa Mon Sep 17 00:00:00 2001 From: Helen Root Date: Wed, 8 Jan 2025 14:24:38 +0100 Subject: [PATCH 03/40] Delete line --- frontend/src/views/DeclaredElementPage/ManageSynonyms.vue | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/frontend/src/views/DeclaredElementPage/ManageSynonyms.vue b/frontend/src/views/DeclaredElementPage/ManageSynonyms.vue index f8d83006f..9cbb1829d 100644 --- a/frontend/src/views/DeclaredElementPage/ManageSynonyms.vue +++ b/frontend/src/views/DeclaredElementPage/ManageSynonyms.vue @@ -11,7 +11,7 @@ icon-only tertiary no-outline - @click="deleteSynonym(s)" + @click="deleteSynonym(idx)" />
@@ -19,6 +19,7 @@ From d7184d6738abe9479e17393405f1cac47475c3c8 Mon Sep 17 00:00:00 2001 From: Perrine Letellier Date: Thu, 9 Jan 2025 11:26:51 +0100 Subject: [PATCH 04/40] add property declared_in_teleicare --- api/serializers/declaration.py | 2 ++ data/models/declaration.py | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/api/serializers/declaration.py b/api/serializers/declaration.py index 24945017a..7b0903c2d 100644 --- a/api/serializers/declaration.py +++ b/api/serializers/declaration.py @@ -578,6 +578,8 @@ class Meta: model = Declaration fields = ( "id", + "siccrf_id", + "declared_in_teleicare", "status", "author", "company", diff --git a/data/models/declaration.py b/data/models/declaration.py index 020c1e732..68e8a600f 100644 --- a/data/models/declaration.py +++ b/data/models/declaration.py @@ -390,6 +390,10 @@ def response_limit_date(self): def __str__(self): return f"Déclaration « {self.name} »" + @property + def declared_in_teleicare(self): + return self.siccrf_id is not None + @property def computed_substances_with_max_quantity_exceeded(self): substances_with_max_quantity_exceeded = self.computed_substances.exclude( From 420a1e430d9fbc5c53505ab425bd5dff886db5a5 Mon Sep 17 00:00:00 2001 From: Perrine Letellier Date: Thu, 9 Jan 2025 11:27:21 +0100 Subject: [PATCH 05/40] change DsfrAlert to display news --- frontend/src/views/DeclarationsHomePage/index.vue | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/views/DeclarationsHomePage/index.vue b/frontend/src/views/DeclarationsHomePage/index.vue index 63c63d3d9..0a5f29210 100644 --- a/frontend/src/views/DeclarationsHomePage/index.vue +++ b/frontend/src/views/DeclarationsHomePage/index.vue @@ -16,9 +16,9 @@
From 03bfac0e610ea46da772e939d6294f416198e3ad Mon Sep 17 00:00:00 2001 From: Helen Root Date: Thu, 9 Jan 2025 15:27:03 +0100 Subject: [PATCH 06/40] Simplify interface for first PR --- .../DeclaredElementPage/ManageSynonyms.vue | 42 ++++++++----------- .../src/views/DeclaredElementPage/index.vue | 2 +- 2 files changed, 19 insertions(+), 25 deletions(-) diff --git a/frontend/src/views/DeclaredElementPage/ManageSynonyms.vue b/frontend/src/views/DeclaredElementPage/ManageSynonyms.vue index 9cbb1829d..53b76b1d9 100644 --- a/frontend/src/views/DeclaredElementPage/ManageSynonyms.vue +++ b/frontend/src/views/DeclaredElementPage/ManageSynonyms.vue @@ -1,41 +1,35 @@ diff --git a/frontend/src/views/DeclaredElementPage/index.vue b/frontend/src/views/DeclaredElementPage/index.vue index 1e068d7cd..3b69159c6 100644 --- a/frontend/src/views/DeclaredElementPage/index.vue +++ b/frontend/src/views/DeclaredElementPage/index.vue @@ -20,7 +20,7 @@ Veuillez contacter l'équipe Compl'Alim pour effectuer la substitution.

- +
From 5912add1e3968e7be007b5c088ba83bb8cf03113 Mon Sep 17 00:00:00 2001 From: Helen Root Date: Thu, 9 Jan 2025 15:50:55 +0100 Subject: [PATCH 07/40] Front for adding one synonym --- .../DeclaredElementPage/ManageSynonyms.vue | 26 ++++++++++--------- .../src/views/DeclaredElementPage/index.vue | 20 +++++++++----- 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/frontend/src/views/DeclaredElementPage/ManageSynonyms.vue b/frontend/src/views/DeclaredElementPage/ManageSynonyms.vue index 53b76b1d9..e1cafa86c 100644 --- a/frontend/src/views/DeclaredElementPage/ManageSynonyms.vue +++ b/frontend/src/views/DeclaredElementPage/ManageSynonyms.vue @@ -4,13 +4,13 @@ label="Ajouter une synonyme (optionnelle)" label-visible :hint="requestName ? `Le nom de la demande : ${requestName}` : ''" - v-model="newSynonym" class="mb-2" + @update:modelValue="updateNewSynonym" /> -
+

Les synonymes existantes :

    -
  • {{ s.name }}
  • +
  • {{ s.name }}
@@ -18,18 +18,20 @@ diff --git a/frontend/src/views/DeclaredElementPage/index.vue b/frontend/src/views/DeclaredElementPage/index.vue index 3b69159c6..21ade9f7c 100644 --- a/frontend/src/views/DeclaredElementPage/index.vue +++ b/frontend/src/views/DeclaredElementPage/index.vue @@ -20,7 +20,11 @@ Veuillez contacter l'équipe Compl'Alim pour effectuer la substitution.

- +
@@ -93,13 +97,14 @@ const openModal = (type) => { } } -// const replacement = ref() -const replacement = ref({ - id: 3, - name: "Test modal", - synonyms: [{ id: 1, name: "My existing synonym" }], - objectType: props.type, +const replacement = ref() +const synonyms = ref() +watch(replacement, () => { + if (replacement.value.synonyms) { + synonyms.value = JSON.parse(JSON.stringify(replacement.value.synonyms)) // initialise synonyms that might be updated + } }) + const cannotReplace = computed(() => replacement.value?.objectType !== element.value.type) const actionButtons = computed(() => [ @@ -145,6 +150,7 @@ const modals = computed(() => { onClick() { const payload = { element: { id: replacement.value?.id, type: replacement.value?.objectType }, + synonyms: synonyms.value, } // TODO: clear search if we stay on page updateElement("replace", payload).then(closeModal) From 7a16f2528387deec57fc184c65343a7fcbe1bcff Mon Sep 17 00:00:00 2001 From: Perrine Letellier Date: Thu, 9 Jan 2025 17:47:39 +0100 Subject: [PATCH 08/40] Declaration from Teleicare have creation_date and modification_date that should not be influenced by auto_now --- api/serializers/declaration.py | 2 +- data/etl/teleicare_history/extractor.py | 54 ++++++++++++++++++++++--- 2 files changed, 49 insertions(+), 7 deletions(-) diff --git a/api/serializers/declaration.py b/api/serializers/declaration.py index 7b0903c2d..76a4a2a97 100644 --- a/api/serializers/declaration.py +++ b/api/serializers/declaration.py @@ -61,7 +61,7 @@ class PassthroughSubstanceSerializer(IdPassthrough, SubstanceSerializer): class DeclaredListSerializer(serializers.ListSerializer): """ Pour les modèles liés et les list serializers on a besoin de spécifier le comportement - dans une mise à jour car DRF ne peut pas le déviner: + dans une mise à jour car DRF ne peut pas le deviner: https://www.django-rest-framework.org/api-guide/serializers/#customizing-multiple-update """ diff --git a/data/etl/teleicare_history/extractor.py b/data/etl/teleicare_history/extractor.py index a74576226..8a0bd5bbc 100644 --- a/data/etl/teleicare_history/extractor.py +++ b/data/etl/teleicare_history/extractor.py @@ -1,6 +1,7 @@ +import contextlib import logging import re -from datetime import date, datetime +from datetime import date, datetime, timezone from django.core.exceptions import ValidationError from django.db import IntegrityError @@ -20,6 +21,30 @@ logger = logging.getLogger(__name__) +@contextlib.contextmanager +def suppress_autotime(model, fields): + """ + Décorateur pour annuler temporairement le auto_now et auto_now_add de certains champs + Copié depuis https://stackoverflow.com/questions/7499767/temporarily-disable-auto-now-auto-now-add + """ + _original_values = {} + for field in model._meta.local_fields: + if field.name in fields: + _original_values[field.name] = { + "auto_now": field.auto_now, + "auto_now_add": field.auto_now_add, + } + field.auto_now = False + field.auto_now_add = False + try: + yield + finally: + for field in model._meta.local_fields: + if field.name in fields: + field.auto_now = _original_values[field.name]["auto_now"] + field.auto_now_add = _original_values[field.name]["auto_now_add"] + + def convert_phone_number(phone_number_to_parse): if phone_number_to_parse: phone_number = PhoneNumber.from_string(phone_number_to_parse, region="FR") @@ -123,9 +148,6 @@ def match_companies_on_siret_or_vat(create_if_not_exist=False): def get_most_recent(list_of_declarations): - def convert_str_date(value): - return datetime.strptime(value, "%m/%d/%Y %H:%M:%S %p").date() - most_recent_date = date.min for ica_declaration in list_of_declarations: current_date = convert_str_date(ica_declaration.dcl_date) @@ -136,6 +158,14 @@ def convert_str_date(value): return list_of_declarations.get(dcl_date=most_recente_dcl_date) +def convert_str_date(value, aware=False): + dt = datetime.strptime(value, "%m/%d/%Y %H:%M:%S %p") + if aware: + return dt.replace(tzinfo=timezone.utc) + else: + return dt.date() + + # Pour les déclarations TeleIcare, le status correspond au champ IcaVersionDeclaration.stattdcl_ident DECLARATION_STATUS_MAPPING = { 1: Declaration.DeclarationStatus.ONGOING_INSTRUCTION, # 'en cours' @@ -181,7 +211,18 @@ def create_declaration_from_teleicare_history(): except Company.DoesNotExist as e: logger.error(e.message) continue + declaration_creation_date = ( + convert_str_date(latest_ica_declaration.dcl_date, aware=True) + if latest_ica_declaration.dcl_date + else "" + ) declaration = Declaration( + creation_date=declaration_creation_date, + modification_date=convert_str_date( + latest_ica_declaration.dcl_date_fin_commercialisation, aware=True + ) + if latest_ica_declaration.dcl_date_fin_commercialisation + else declaration_creation_date, siccrf_id=ica_complement_alimentaire.cplalim_ident, galenic_formulation=GalenicFormulation.objects.get( siccrf_id=ica_complement_alimentaire.frmgal_ident @@ -211,8 +252,9 @@ def create_declaration_from_teleicare_history(): else DECLARATION_STATUS_MAPPING[latest_ica_version_declaration.stattdcl_ident], ) try: - declaration.save() - nb_created_declarations += 1 + with suppress_autotime(declaration, ["creation_date", "modification_date"]): + declaration.save() + nb_created_declarations += 1 except IntegrityError: # cette Déclaration a déjà été créée pass From ff46c7c7e1314ab3c12cf6929c1857fed6ddad24 Mon Sep 17 00:00:00 2001 From: Perrine Letellier Date: Thu, 9 Jan 2025 17:48:18 +0100 Subject: [PATCH 09/40] WIP: display historic declaration in front --- .../CompanyDeclarationsPage/CompanyDeclarationsTable.vue | 2 +- .../src/views/DeclarationsHomePage/DeclarationsTable.vue | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/frontend/src/views/CompanyDeclarationsPage/CompanyDeclarationsTable.vue b/frontend/src/views/CompanyDeclarationsPage/CompanyDeclarationsTable.vue index b6e41dd8d..e05e894b6 100644 --- a/frontend/src/views/CompanyDeclarationsPage/CompanyDeclarationsTable.vue +++ b/frontend/src/views/CompanyDeclarationsPage/CompanyDeclarationsTable.vue @@ -33,7 +33,7 @@ const rows = computed(() => company: x.company?.socialName, mandatedCompany: x.mandatedCompany?.socialName, }, - `${x.author.firstName} ${x.author.lastName}`, + x.author ? `${x.author.firstName} ${x.author.lastName}` : "", getStatusTagForCell(x.status, true), timeAgo(x.creationDate), timeAgo(x.modificationDate), diff --git a/frontend/src/views/DeclarationsHomePage/DeclarationsTable.vue b/frontend/src/views/DeclarationsHomePage/DeclarationsTable.vue index c90c335f4..4972c05b0 100644 --- a/frontend/src/views/DeclarationsHomePage/DeclarationsTable.vue +++ b/frontend/src/views/DeclarationsHomePage/DeclarationsTable.vue @@ -23,7 +23,7 @@ const emit = defineEmits("open") // Les données pour la table const headers = computed(() => { if (useShortTable.value) return ["Nom", "État"] - return ["ID", "Nom du produit", "Entreprise", "Déclarant·e", "État", "Date de modification", ""] + return ["ID", "", "Nom du produit", "Entreprise", "Déclarant·e", "État", "Date de modification", ""] }) const rows = computed(() => { @@ -39,6 +39,13 @@ const rows = computed(() => { return props.data.results.map((d) => ({ rowData: [ d.id, + d.declaredInTeleicare + ? { + component: "DsfrBadge", + label: "Déclaration soumise sur Teleicare", + type: "info", + } + : "", { component: "router-link", text: d.name, From afcb2703796aca540ed27593265530c271230938 Mon Sep 17 00:00:00 2001 From: Helen Root Date: Mon, 13 Jan 2025 16:35:35 +0100 Subject: [PATCH 10/40] Add failing test for backend --- api/tests/test_declaration.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/api/tests/test_declaration.py b/api/tests/test_declaration.py index 98c2c37c0..351967020 100644 --- a/api/tests/test_declaration.py +++ b/api/tests/test_declaration.py @@ -36,6 +36,7 @@ SubstanceUnitFactory, SupervisorRoleFactory, VisaRoleFactory, + PlantSynonymFactory, ) from data.models import Attachment, Declaration, Snapshot, DeclaredMicroorganism, DeclaredPlant @@ -1898,3 +1899,32 @@ def test_cannot_replace_element_different_type(self): declared_plant.refresh_from_db() self.assertEqual(declared_plant.request_status, DeclaredPlant.AddableStatus.REQUESTED) self.assertNotEqual(declared_plant.plant, microorganism) + + @authenticate + def test_can_add_synonym_on_replace(self): + """ + C'est possible d'envoyer une liste avec un nouvel element pour + ajouter une synonyme et laisser des synonymes existantes non-modifiées + """ + InstructionRoleFactory(user=authenticate.user) + + declaration = DeclarationFactory() + declared_plant = DeclaredPlantFactory(declaration=declaration, new=True) + plant = PlantFactory() + synonym = PlantSynonymFactory.create(name="Eucalyptus Plant", standard_name=plant) + + response = self.client.post( + reverse("api:declared_element_replace", kwargs={"pk": declared_plant.id, "type": "plant"}), + { + "element": {"id": plant.id, "type": "plant"}, + "synonyms": [{"id": synonym.id, "name": "Eucalyptus Plant"}, {"name": "New synonym"}], + }, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK, response.json()) + declared_plant.refresh_from_db() + self.assertEqual(declared_plant.request_status, DeclaredPlant.AddableStatus.REPLACED) + plant.refresh_from_db() + self.assertEqual(plant.plantsynonym_set.count(), 2) + self.assertTrue(plant.plantsynonym_set.get(name="New synonym").exists()) + self.assertEqual(plant.plantsynonym_set.get(id=synonym.id).name, synonym.name) From e5474cc5f7a4ae6e21ec660cafc61b9c8b50a1e4 Mon Sep 17 00:00:00 2001 From: Helen Root Date: Mon, 13 Jan 2025 16:58:11 +0100 Subject: [PATCH 11/40] Add backend logic --- api/tests/test_declaration.py | 2 +- api/views/declaration/declared_element.py | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/api/tests/test_declaration.py b/api/tests/test_declaration.py index 351967020..44ac78e8f 100644 --- a/api/tests/test_declaration.py +++ b/api/tests/test_declaration.py @@ -1926,5 +1926,5 @@ def test_can_add_synonym_on_replace(self): self.assertEqual(declared_plant.request_status, DeclaredPlant.AddableStatus.REPLACED) plant.refresh_from_db() self.assertEqual(plant.plantsynonym_set.count(), 2) - self.assertTrue(plant.plantsynonym_set.get(name="New synonym").exists()) + self.assertIsNotNone(plant.plantsynonym_set.get(name="New synonym")) self.assertEqual(plant.plantsynonym_set.get(id=synonym.id).name, synonym.name) diff --git a/api/views/declaration/declared_element.py b/api/views/declaration/declared_element.py index 4267635af..6f3d9744c 100644 --- a/api/views/declaration/declared_element.py +++ b/api/views/declaration/declared_element.py @@ -15,6 +15,10 @@ Microorganism, Substance, Ingredient, + PlantSynonym, + MicroorganismSynonym, + SubstanceSynonym, + IngredientSynonym, ) from api.serializers import ( DeclaredElementSerializer, @@ -59,21 +63,25 @@ class ElementMappingMixin: "plant": { "model": DeclaredPlant, "element_model": Plant, + "synonym_model": PlantSynonym, "serializer": DeclaredPlantSerializer, }, "microorganism": { "model": DeclaredMicroorganism, "element_model": Microorganism, + "synonym_model": MicroorganismSynonym, "serializer": DeclaredMicroorganismSerializer, }, "substance": { "model": DeclaredSubstance, "element_model": Substance, + "synonym_model": SubstanceSynonym, "serializer": DeclaredSubstanceSerializer, }, "ingredient": { "model": DeclaredIngredient, "element_model": Ingredient, + "synonym_model": IngredientSynonym, "serializer": DeclaredIngredientSerializer, }, } @@ -102,6 +110,10 @@ def type_serializer(self): def element_model(self): return self.type_info["element_model"] + @property + def synonym_model(self): + return self.type_info["synonym_model"] + class DeclaredElementView(RetrieveAPIView, ElementMappingMixin): permission_classes = [(IsInstructor | IsVisor)] @@ -161,3 +173,14 @@ def _update_element(self, element, request): setattr(element, self.element_type, existing_element) element.request_status = self.type_model.AddableStatus.REPLACED element.new = False + + synonyms = request.data.get("synonyms", []) + + for synonym in synonyms: + if not synonym.get("id"): + # add new synonym + try: + name = synonym.get("name") + except KeyError: + raise ParseError(detail="Must provide 'name' to create new synonym") + self.synonym_model.objects.create(standard_name=existing_element, name=name) From b0e4330d0b45afa83906987519ab1498f589bada Mon Sep 17 00:00:00 2001 From: Perrine Letellier Date: Tue, 14 Jan 2025 14:06:36 +0100 Subject: [PATCH 12/40] chore: reword DsfrAlert --- frontend/src/views/DeclarationsHomePage/index.vue | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/views/DeclarationsHomePage/index.vue b/frontend/src/views/DeclarationsHomePage/index.vue index 0a5f29210..dc525d781 100644 --- a/frontend/src/views/DeclarationsHomePage/index.vue +++ b/frontend/src/views/DeclarationsHomePage/index.vue @@ -17,7 +17,9 @@
From 8b84e3c8dbaee11c6357372ac351378fc264e34e Mon Sep 17 00:00:00 2001 From: Perrine Letellier Date: Tue, 14 Jan 2025 16:01:58 +0100 Subject: [PATCH 13/40] add DsfrBadge in DeclarationTable and DsfrAlert above all DeclarationPages --- api/serializers/declaration.py | 3 +++ .../CompanyDeclarationsTable.vue | 10 ++++++- .../views/CompanyDeclarationsPage/index.vue | 2 ++ .../DeclarationsTable.vue | 21 ++++++++------- .../src/views/DeclarationsHomePage/index.vue | 13 +++------- .../InstructionDeclarationsTable.vue | 10 ++++++- frontend/src/views/InstructionPage/index.vue | 16 +++++++++++- frontend/src/views/ProducerFormPage/index.vue | 26 ++++++++++++++++--- 8 files changed, 76 insertions(+), 25 deletions(-) diff --git a/api/serializers/declaration.py b/api/serializers/declaration.py index 4a2493096..dbbcb75c0 100644 --- a/api/serializers/declaration.py +++ b/api/serializers/declaration.py @@ -272,6 +272,8 @@ class Meta: model = Declaration fields = ( "id", + "siccrf_id", + "declared_in_teleicare", "status", "author", "company", @@ -441,6 +443,7 @@ class Meta: model = Declaration fields = ( "id", + "declared_in_teleicare", "article", "status", "author", diff --git a/frontend/src/views/CompanyDeclarationsPage/CompanyDeclarationsTable.vue b/frontend/src/views/CompanyDeclarationsPage/CompanyDeclarationsTable.vue index e05e894b6..0154665bd 100644 --- a/frontend/src/views/CompanyDeclarationsPage/CompanyDeclarationsTable.vue +++ b/frontend/src/views/CompanyDeclarationsPage/CompanyDeclarationsTable.vue @@ -22,7 +22,15 @@ const headers = ["ID", "Nom du produit", "Entreprise", "Auteur", "État", "Date const rows = computed(() => props.data?.results?.map((x) => ({ rowData: [ - x.id, + x.declaredInTeleicare + ? { + component: "DsfrBadge", + label: "issue de Teleicare", + type: "info", + small: true, + noIcon: true, + } + : x.id, { component: "router-link", text: x.name, diff --git a/frontend/src/views/CompanyDeclarationsPage/index.vue b/frontend/src/views/CompanyDeclarationsPage/index.vue index b60219746..d3655e5ac 100644 --- a/frontend/src/views/CompanyDeclarationsPage/index.vue +++ b/frontend/src/views/CompanyDeclarationsPage/index.vue @@ -7,6 +7,7 @@ { text: 'Les déclarations de mon entreprise' }, ]" /> +
@@ -72,6 +73,7 @@ import { getPagesForPagination } from "@/utils/components" import CompanyDeclarationsTable from "./CompanyDeclarationsTable" import ProgressSpinner from "@/components/ProgressSpinner" import StatusFilter from "@/components/StatusFilter.vue" +import HistoryAlert from "@/components/HistoryAlert.vue" const route = useRoute() const store = useRootStore() diff --git a/frontend/src/views/DeclarationsHomePage/DeclarationsTable.vue b/frontend/src/views/DeclarationsHomePage/DeclarationsTable.vue index 4972c05b0..ef3ffde57 100644 --- a/frontend/src/views/DeclarationsHomePage/DeclarationsTable.vue +++ b/frontend/src/views/DeclarationsHomePage/DeclarationsTable.vue @@ -23,7 +23,7 @@ const emit = defineEmits("open") // Les données pour la table const headers = computed(() => { if (useShortTable.value) return ["Nom", "État"] - return ["ID", "", "Nom du produit", "Entreprise", "Déclarant·e", "État", "Date de modification", ""] + return ["ID", "Nom du produit", "Entreprise", "Déclarant·e", "État", "Date de modification", ""] }) const rows = computed(() => { @@ -38,14 +38,15 @@ const rows = computed(() => { return props.data.results.map((d) => ({ rowData: [ - d.id, d.declaredInTeleicare ? { component: "DsfrBadge", - label: "Déclaration soumise sur Teleicare", + label: "issue de Teleicare", type: "info", + small: true, + noIcon: true, } - : "", + : d.id, { component: "router-link", text: d.name, @@ -60,11 +61,13 @@ const rows = computed(() => { d.author ? `${d.author.firstName} ${d.author.lastName}` : "", getStatusTagForCell(d.status, true), timeAgo(d.modificationDate), - { - component: "router-link", - text: "Dupliquer", - to: { name: "NewDeclaration", query: { duplicate: d.id } }, - }, + d.declaredInTeleicare + ? "" + : { + component: "router-link", + text: "Dupliquer", + to: { name: "NewDeclaration", query: { duplicate: d.id } }, + }, ], })) }) diff --git a/frontend/src/views/DeclarationsHomePage/index.vue b/frontend/src/views/DeclarationsHomePage/index.vue index dc525d781..4f0b166ed 100644 --- a/frontend/src/views/DeclarationsHomePage/index.vue +++ b/frontend/src/views/DeclarationsHomePage/index.vue @@ -13,16 +13,8 @@ @click="createNewDeclaration" />
-
- -
+ +
@@ -93,6 +85,7 @@ import { storeToRefs } from "pinia" import { getPagesForPagination } from "@/utils/components" import PaginationSizeSelect from "@/components/PaginationSizeSelect" import StatusFilter from "@/components/StatusFilter" +import HistoryAlert from "@/components/HistoryAlert.vue" const store = useRootStore() const { loggedUser } = storeToRefs(store) diff --git a/frontend/src/views/InstructionDeclarationsPage/InstructionDeclarationsTable.vue b/frontend/src/views/InstructionDeclarationsPage/InstructionDeclarationsTable.vue index ae832bbba..c85a89979 100644 --- a/frontend/src/views/InstructionDeclarationsPage/InstructionDeclarationsTable.vue +++ b/frontend/src/views/InstructionDeclarationsPage/InstructionDeclarationsTable.vue @@ -33,7 +33,15 @@ const rows = computed(() => component: CircleIndicators, declaration: x, }, - x.id, + x.declaredInTeleicare + ? { + component: "DsfrBadge", + label: "issue de Teleicare", + type: "info", + small: true, + noIcon: true, + } + : x.id, { component: "router-link", text: x.name, diff --git a/frontend/src/views/InstructionPage/index.vue b/frontend/src/views/InstructionPage/index.vue index e2b71270f..97a74b78f 100644 --- a/frontend/src/views/InstructionPage/index.vue +++ b/frontend/src/views/InstructionPage/index.vue @@ -27,11 +27,25 @@ + +

L'import de l'historique est en cours. Les informations suivantes arriveront bientôt :

+
    +
  • la composition
  • +
  • les entreprises mandantes s'il y en a
  • +
  • l'étiquettage
  • +
  • l'attestation
  • +
+
- + +

L'import de l'historique est en cours. Les informations suivantes arriveront bientôt :

+
    +
  • la composition
  • +
  • les entreprises mandantes s'il y en a
  • +
  • l'étiquettage
  • +
  • l'attestation
  • +
+
+ @@ -28,7 +48,7 @@ From b1a36bc608189ef5715803e77ccf272363c21140 Mon Sep 17 00:00:00 2001 From: Alejandro MG Date: Tue, 14 Jan 2025 22:36:06 +0100 Subject: [PATCH 14/40] Adds necessary fields for populations and conditions --- api/serializers/condition.py | 4 + api/serializers/population.py | 2 + data/admin/condition.py | 3 + data/admin/population.py | 1 + data/models/condition.py | 16 ++++ data/models/population.py | 13 ++++ frontend/src/utils/mappings.js | 8 ++ .../PopulationsCheckboxes.vue | 73 +++++++++++++++++++ .../src/views/ProducerFormPage/ProductTab.vue | 27 +------ 9 files changed, 123 insertions(+), 24 deletions(-) create mode 100644 frontend/src/views/ProducerFormPage/PopulationsCheckboxes.vue diff --git a/api/serializers/condition.py b/api/serializers/condition.py index 040979204..7e0de7817 100644 --- a/api/serializers/condition.py +++ b/api/serializers/condition.py @@ -1,4 +1,5 @@ from rest_framework import serializers + from data.models import Condition @@ -9,5 +10,8 @@ class Meta: "name", "id", "name_en", + "min_age", + "max_age", + "category", ] read_only_fields = fields diff --git a/api/serializers/population.py b/api/serializers/population.py index 2cfacb300..b0bec33b6 100644 --- a/api/serializers/population.py +++ b/api/serializers/population.py @@ -1,4 +1,5 @@ from rest_framework import serializers + from data.models import Population @@ -11,5 +12,6 @@ class Meta: "is_obsolete", "min_age", "max_age", + "category", ] read_only_fields = fields diff --git a/data/admin/condition.py b/data/admin/condition.py index 3403f0036..361a3b8a9 100644 --- a/data/admin/condition.py +++ b/data/admin/condition.py @@ -17,9 +17,12 @@ class ConditionAdmin(admin.ModelAdmin): fields = [ "name", "ca_name", + "category", "siccrf_name_en", "is_obsolete", "ca_is_obsolete", + "min_age", + "max_age", "creation_date", "modification_date", ] diff --git a/data/admin/population.py b/data/admin/population.py index 23abb32bf..b4c479da8 100644 --- a/data/admin/population.py +++ b/data/admin/population.py @@ -17,6 +17,7 @@ class PopulationAdmin(admin.ModelAdmin): fields = [ "name", "ca_name", + "category", "is_obsolete", "ca_is_obsolete", "is_defined_by_anses", diff --git a/data/models/condition.py b/data/models/condition.py index 4b24ba07b..f611d214a 100644 --- a/data/models/condition.py +++ b/data/models/condition.py @@ -1,15 +1,31 @@ from django.db import models + from simple_history.models import HistoricalRecords from .abstract_models import CommonModel class Condition(CommonModel): + class ConditionCategory(models.TextChoices): + AGE = "AGE", "âge" + MEDICAL = "MEDICAL", "conditions médicales spécifiques" + PREGNANCY = "PREGNANCY", "grossesse et allaitement" + MEDICAMENTS = "MEDICAMENTS", "interactions médicamenteuses" + OTHER = "OTHER", "autres" + class Meta: verbose_name = "condition de santé / facteurs de risque" siccrf_name_en = models.TextField(blank=True, verbose_name="nom en anglais selon la base SICCRF") + min_age = models.FloatField(blank=True, null=True, default=None) + max_age = models.FloatField(blank=True, null=True, default=None) history = HistoricalRecords(inherit=True, excluded_fields=["name", "is_obsolete"]) + category = models.CharField( + max_length=50, + choices=ConditionCategory.choices, + default=ConditionCategory.OTHER, + verbose_name="categorie", + ) @property def name_en(self): diff --git a/data/models/population.py b/data/models/population.py index 05561cbdf..5a747f4f9 100644 --- a/data/models/population.py +++ b/data/models/population.py @@ -1,10 +1,17 @@ from django.db import models + from simple_history.models import HistoricalRecords from .abstract_models import CommonModel class Population(CommonModel): + class PopulationCategory(models.TextChoices): + AGE = "AGE", "âge" + MEDICAL = "MEDICAL", "conditions médicales spécifiques" + PREGNANCY = "PREGNANCY", "grossesse et allaitement" + OTHER = "OTHER", "autres" + class Meta: verbose_name = "Population cible" verbose_name_plural = "Populations cibles" @@ -13,3 +20,9 @@ class Meta: max_age = models.FloatField(blank=True, null=True, default=None) is_defined_by_anses = models.BooleanField(default=False) history = HistoricalRecords(inherit=True, excluded_fields=["name", "is_obsolete"]) + category = models.CharField( + max_length=50, + choices=PopulationCategory.choices, + default=PopulationCategory.OTHER, + verbose_name="categorie", + ) diff --git a/frontend/src/utils/mappings.js b/frontend/src/utils/mappings.js index 38c112890..19771dd85 100644 --- a/frontend/src/utils/mappings.js +++ b/frontend/src/utils/mappings.js @@ -254,6 +254,14 @@ export const authorizationModesMapping = { EU: "Autorisé dans un État membre de l'UE ou EEE", } +export const populationCategoriesMapping = { + AGE: "Âge", + MEDICAL: "Conditions médicales spécifiques", + PREGNANCY: "Grossesse et allaitement", + MEDICAMENTS: "Interactions médicamenteuses", + OTHER: "Autres", +} + export const getAuthorizationModeInFrench = (type) => { return authorizationModesMapping[type] || null } diff --git a/frontend/src/views/ProducerFormPage/PopulationsCheckboxes.vue b/frontend/src/views/ProducerFormPage/PopulationsCheckboxes.vue new file mode 100644 index 000000000..a0464d356 --- /dev/null +++ b/frontend/src/views/ProducerFormPage/PopulationsCheckboxes.vue @@ -0,0 +1,73 @@ + + + diff --git a/frontend/src/views/ProducerFormPage/ProductTab.vue b/frontend/src/views/ProducerFormPage/ProductTab.vue index 444475f2b..c4079a45c 100644 --- a/frontend/src/views/ProducerFormPage/ProductTab.vue +++ b/frontend/src/views/ProducerFormPage/ProductTab.vue @@ -143,23 +143,7 @@ - -
-
- - -
-
-
+ { - const checkboxColumnNumbers = { - sm: 1, - md: 2, - lg: 2, - xl: 3, - } + const checkboxColumnNumbers = { sm: 1, md: 2, lg: 2, xl: 3 } numberOfColumns.value = checkboxColumnNumbers[getCurrentBreakpoint()] || 1 }, { immediate: true } ) -const orderedPopulations = computed(() => transformArrayByColumn(populations.value, numberOfColumns.value)) const orderedConditions = computed(() => transformArrayByColumn(conditions.value, numberOfColumns.value)) const orderedEffects = computed(() => transformArrayByColumn(effects.value, numberOfColumns.value)) From e42366b41f8a2feeb8f58db5e5fa1c7f845f44d0 Mon Sep 17 00:00:00 2001 From: hfroot <9282816+hfroot@users.noreply.github.com> Date: Wed, 15 Jan 2025 14:23:26 +0100 Subject: [PATCH 15/40] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Alejandro M Guillén --- api/tests/test_declaration.py | 2 +- frontend/src/views/DeclaredElementPage/ManageSynonyms.vue | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/api/tests/test_declaration.py b/api/tests/test_declaration.py index 44ac78e8f..7c7df26e7 100644 --- a/api/tests/test_declaration.py +++ b/api/tests/test_declaration.py @@ -1904,7 +1904,7 @@ def test_cannot_replace_element_different_type(self): def test_can_add_synonym_on_replace(self): """ C'est possible d'envoyer une liste avec un nouvel element pour - ajouter une synonyme et laisser des synonymes existantes non-modifiées + ajouter un synonyme et laisser des synonymes existantes non-modifiées """ InstructionRoleFactory(user=authenticate.user) diff --git a/frontend/src/views/DeclaredElementPage/ManageSynonyms.vue b/frontend/src/views/DeclaredElementPage/ManageSynonyms.vue index e1cafa86c..970b00c5d 100644 --- a/frontend/src/views/DeclaredElementPage/ManageSynonyms.vue +++ b/frontend/src/views/DeclaredElementPage/ManageSynonyms.vue @@ -1,14 +1,14 @@ From 9381d3cbcca23aa1c136e0d3e64d074ce02277b7 Mon Sep 17 00:00:00 2001 From: Helen Root Date: Wed, 15 Jan 2025 14:45:28 +0100 Subject: [PATCH 18/40] Remove unnecessary if - will always have at least empty array for synonyms --- frontend/src/views/DeclaredElementPage/index.vue | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frontend/src/views/DeclaredElementPage/index.vue b/frontend/src/views/DeclaredElementPage/index.vue index 32112d5c3..1508fe611 100644 --- a/frontend/src/views/DeclaredElementPage/index.vue +++ b/frontend/src/views/DeclaredElementPage/index.vue @@ -100,9 +100,7 @@ const openModal = (type) => { const replacement = ref() const synonyms = ref() watch(replacement, () => { - if (replacement.value.synonyms) { - synonyms.value = JSON.parse(JSON.stringify(replacement.value.synonyms)) // initialise synonyms that might be updated - } + synonyms.value = JSON.parse(JSON.stringify(replacement.value.synonyms)) // initialise synonyms that might be updated }) const cannotReplace = computed(() => replacement.value?.objectType !== element.value.type) From 1cb720b7f11843ba4abf5896dcd8fcd0ef9a7531 Mon Sep 17 00:00:00 2001 From: Perrine Letellier Date: Thu, 16 Jan 2025 11:51:59 +0100 Subject: [PATCH 19/40] avoid comments on historic declarations --- .../components/DeclarationSummary/index.vue | 187 +++++++++--------- frontend/src/views/InstructionPage/index.vue | 5 +- frontend/src/views/ProducerFormPage/index.vue | 8 +- frontend/src/views/VisaPage/index.vue | 13 +- 4 files changed, 114 insertions(+), 99 deletions(-) diff --git a/frontend/src/components/DeclarationSummary/index.vue b/frontend/src/components/DeclarationSummary/index.vue index 3ec80eab0..4074503e6 100644 --- a/frontend/src/components/DeclarationSummary/index.vue +++ b/frontend/src/components/DeclarationSummary/index.vue @@ -21,99 +21,102 @@
- -

- Composition - -

- - - - - - - - - - -

Substances contenues dans la composition :

- -

Les ingrédients suivants, ajoutés pour remplacer une demande, rajoutent des substances dans la composition.

-

- Veuillez vérifier que les doses totales des substances restent pertinentes. Si besoin, renvoyez la déclaration - vers le déclarant pour les mettre à jour. -

-
    -
  • - {{ i.element.name }} -
  • -
-
- - -

- Adresse sur l'étiquetage - -

- - -

- Pièces jointes - -

-
- +

+ Composition + +

+ + + + + + + + + + +

Substances contenues dans la composition :

+ +

+ Les ingrédients suivants, ajoutés pour remplacer une demande, rajoutent des substances dans la composition. +

+

+ Veuillez vérifier que les doses totales des substances restent pertinentes. Si besoin, renvoyez la déclaration + vers le déclarant pour les mettre à jour. +

+
    +
  • + {{ i.element.name }} +
  • +
+
+ + +

+ Adresse sur l'étiquetage + +

+ + +

+ Pièces jointes + +

+
+ +
diff --git a/frontend/src/views/InstructionPage/index.vue b/frontend/src/views/InstructionPage/index.vue index 97a74b78f..0131b6472 100644 --- a/frontend/src/views/InstructionPage/index.vue +++ b/frontend/src/views/InstructionPage/index.vue @@ -87,7 +87,7 @@ @forward="selectedTabIndex += 1" :removeSaveLabel="true" > -