Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Option pour remplacer un ingrédient demandé par un ingrédient existant #1334

Merged
merged 32 commits into from
Jan 7, 2025
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
dab1c7e
Move alert handling to separate file
hfroot Nov 26, 2024
b4594b1
Add first elements of UI for search of ingredient
hfroot Nov 26, 2024
ad3dd7c
Fetch element detail, display synonyms
hfroot Nov 26, 2024
e569633
Activate replace button when ingredient selected
hfroot Nov 26, 2024
fbc13dc
Ability to add an existing ingredient to a declared one
hfroot Nov 28, 2024
803d024
Add success replace alert with element link
hfroot Nov 28, 2024
3449deb
Proper alignment when showing label of autocomplete as well as search…
hfroot Dec 3, 2024
459cc1c
Front: prevent demand being replaced with ingredient of different type
hfroot Dec 3, 2024
567f87f
Merge branch 'staging' into replace-ingredient-request
hfroot Dec 5, 2024
d595bb7
Add merge migration
hfroot Dec 5, 2024
93fac1d
Refactor API to reduce code repetition
hfroot Dec 19, 2024
58246dc
Refactor from REST to RPC
hfroot Dec 19, 2024
7a081f0
Require passing replacement element type to avoid accidentally replac…
hfroot Dec 19, 2024
f88a103
Prevent the replacement cross-type
hfroot Dec 19, 2024
705bdc1
Update front with new API and add request info and notes to response
hfroot Dec 19, 2024
60c77b3
When a demand is replaced, it is no longer new
hfroot Dec 19, 2024
c1371e4
Minimal solution for explaining the ingredient name change in declara…
hfroot Dec 19, 2024
8863961
Recommend instructor to return declaration to recalculate substances
hfroot Dec 19, 2024
b5a217a
Only show warning if replacement adds substances to composition
hfroot Dec 19, 2024
5786381
Merge branch 'staging' into replace-ingredient-request
hfroot Dec 19, 2024
e554e92
Merge migration
hfroot Dec 19, 2024
9f8b2ff
Merge branch 'staging' into replace-ingredient-request
alemangui Dec 27, 2024
1e2566e
Adds merge migration
alemangui Dec 27, 2024
4a07319
Merge branch 'staging' into replace-ingredient-request
hfroot Jan 6, 2025
198532d
Update warning message
hfroot Jan 6, 2025
1601c52
Change autocomplete API to use 'required' for consistency
hfroot Jan 6, 2025
db5169f
Add permissions checks on info and reject endpoints
hfroot Jan 6, 2025
0073130
Fix error handling
hfroot Jan 6, 2025
f986101
Update frontend/src/views/DeclaredElementPage/index.vue
hfroot Jan 6, 2025
6251920
Move comment to GH
hfroot Jan 6, 2025
84fa063
Add id to declared element error message
hfroot Jan 6, 2025
450c030
Update frontend/src/components/DeclarationSummary/index.vue
alemangui Jan 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 37 additions & 64 deletions api/serializers/declaration.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,23 @@ def update(self, instance, validated_data):
)


class DeclaredIngredientCommonSerializer(PrivateFieldsSerializer):
class DeclaredElementNestedField:
hfroot marked this conversation as resolved.
Show resolved Hide resolved
def create(self, validated_data):
# DRF ne gère pas automatiquement la création des nested-fields :
# https://www.django-rest-framework.org/api-guide/serializers/#writable-nested-representations
element = validated_data.pop(self.nested_field_name, None)
if element:
try:
validated_data[self.nested_field_name] = self.nested_model.objects.get(pk=element.get("id"))
except self.nested_model.DoesNotExist:
raise ProjectAPIException(
field_errors={f"declared_{self.nested_field_name}s": "L'ingrédient spécifiée n'existe pas."}
alemangui marked this conversation as resolved.
Show resolved Hide resolved
)

return super().create(validated_data)


class DeclaredIngredientCommonSerializer(DeclaredElementNestedField, PrivateFieldsSerializer):
private_fields = ("request_private_notes", "request_status")


Expand All @@ -113,6 +129,9 @@ class DeclaredPlantSerializer(DeclaredIngredientCommonSerializer):
used_part = serializers.PrimaryKeyRelatedField(queryset=PlantPart.objects.all(), required=False, allow_null=True)
declaration = serializers.PrimaryKeyRelatedField(read_only=True)

nested_field_name = "plant"
nested_model = Plant

class Meta:
model = DeclaredPlant
fields = ADDABLE_ELEMENT_FIELDS + (
Expand All @@ -125,25 +144,17 @@ class Meta:
"unit",
"quantity",
"preparation",
"type",
)

def create(self, validated_data):
# DRF ne gère pas automatiquement la création des nested-fields :
# https://www.django-rest-framework.org/api-guide/serializers/#writable-nested-representations
plant = validated_data.pop("plant", None)
if plant:
try:
validated_data["plant"] = Plant.objects.get(pk=plant.get("id"))
except Plant.DoesNotExist:
raise ProjectAPIException(field_errors={"declared_plants": "La plante spécifiée n'existe pas."})

return super().create(validated_data)


class DeclaredMicroorganismSerializer(DeclaredIngredientCommonSerializer):
element = PassthroughMicroorganismSerializer(required=False, source="microorganism", allow_null=True)
declaration = serializers.PrimaryKeyRelatedField(read_only=True)

nested_field_name = "microorganism"
nested_model = Microorganism

class Meta:
model = DeclaredMicroorganism
fields = ADDABLE_ELEMENT_FIELDS + (
Expand All @@ -156,28 +167,18 @@ class Meta:
"activated",
"strain",
"quantity",
"type",
)

def create(self, validated_data):
# DRF ne gère pas automatiquement la création des nested-fields :
# https://www.django-rest-framework.org/api-guide/serializers/#writable-nested-representations
microorganism = validated_data.pop("microorganism", None)
if microorganism:
try:
validated_data["microorganism"] = Microorganism.objects.get(pk=microorganism.get("id"))
except Microorganism.DoesNotExist:
raise ProjectAPIException(
field_errors={"declared_microorganisms": "Le micro-organisme spécifié n'existe pas."}
)

return super().create(validated_data)


class DeclaredIngredientSerializer(DeclaredIngredientCommonSerializer):
element = PassthroughIngredientSerializer(required=False, source="ingredient", allow_null=True)
unit = serializers.PrimaryKeyRelatedField(queryset=SubstanceUnit.objects.all(), required=False, allow_null=True)
declaration = serializers.PrimaryKeyRelatedField(read_only=True)

nested_field_name = "ingredient"
nested_model = Ingredient

class Meta:
model = DeclaredIngredient
fields = ADDABLE_ELEMENT_FIELDS + (
Expand All @@ -189,25 +190,17 @@ class Meta:
"active",
"quantity",
"unit",
"type",
)

def create(self, validated_data):
# DRF ne gère pas automatiquement la création des nested-fields :
# https://www.django-rest-framework.org/api-guide/serializers/#writable-nested-representations
ingredient = validated_data.pop("ingredient", None)
if ingredient:
try:
validated_data["ingredient"] = Ingredient.objects.get(pk=ingredient.get("id"))
except Ingredient.DoesNotExist:
raise ProjectAPIException(field_errors={"declared_ingredients": "L'ingrédient spécifié n'existe pas."})

return super().create(validated_data)


class DeclaredSubstanceSerializer(DeclaredIngredientCommonSerializer):
element = PassthroughSubstanceSerializer(required=False, source="substance", allow_null=True)
declaration = serializers.PrimaryKeyRelatedField(read_only=True)

nested_field_name = "substance"
nested_model = Substance

class Meta:
model = DeclaredSubstance
fields = ADDABLE_ELEMENT_FIELDS + (
Expand All @@ -218,25 +211,17 @@ class Meta:
"active",
"quantity",
"unit",
"type",
)

def create(self, validated_data):
# DRF ne gère pas automatiquement la création des nested-fields :
# https://www.django-rest-framework.org/api-guide/serializers/#writable-nested-representations
substance = validated_data.pop("substance", None)
if substance:
try:
validated_data["substance"] = Substance.objects.get(pk=substance.get("id"))
except Substance.DoesNotExist:
raise ProjectAPIException(field_errors={"declared_substances": "La substance spécifiée n'existe pas."})

return super().create(validated_data)


class ComputedSubstanceSerializer(serializers.ModelSerializer):
class ComputedSubstanceSerializer(DeclaredElementNestedField, serializers.ModelSerializer):
substance = PassthroughSubstanceSerializer()
unit = serializers.PrimaryKeyRelatedField(queryset=SubstanceUnit.objects.all(), required=False, allow_null=True)

nested_field_name = "substance"
nested_model = Substance

class Meta:
model = ComputedSubstance
fields = (
Expand All @@ -246,18 +231,6 @@ class Meta:
"unit",
)

def create(self, validated_data):
# DRF ne gère pas automatiquement la création des nested-fields :
# https://www.django-rest-framework.org/api-guide/serializers/#writable-nested-representations
substance = validated_data.pop("substance", None)
if substance:
try:
validated_data["substance"] = Substance.objects.get(pk=substance.get("id"))
except Substance.DoesNotExist:
raise ProjectAPIException(field_errors={"declared_substances": "La substance spécifiée n'existe pas."})

return super().create(validated_data)


class AttachmentSerializer(IdPassthrough, serializers.ModelSerializer):
file = Base64FileField(required=False, allow_null=True)
Expand Down
134 changes: 116 additions & 18 deletions api/tests/test_declaration.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
SupervisorRoleFactory,
VisaRoleFactory,
)
from data.models import Attachment, Declaration, DeclaredMicroorganism, Snapshot
from data.models import Attachment, Declaration, Snapshot, DeclaredMicroorganism, DeclaredPlant

from .utils import authenticate

Expand Down Expand Up @@ -1638,7 +1638,9 @@ def test_get_single_declared_plant(self):
declaration = DeclarationFactory()
plant = DeclaredPlantFactory(declaration=declaration)

response = self.client.get(reverse("api:declared_plant", kwargs={"pk": plant.id}), format="json")
response = self.client.get(
reverse("api:declared_element", kwargs={"pk": plant.id, "type": "plant"}), format="json"
hfroot marked this conversation as resolved.
Show resolved Hide resolved
)
body = response.json()
self.assertEqual(body["id"], plant.id)

Expand All @@ -1650,7 +1652,7 @@ def test_get_single_declared_microorganism(self):
microorganism = DeclaredMicroorganismFactory(declaration=declaration)

response = self.client.get(
reverse("api:declared_microorganism", kwargs={"pk": microorganism.id}), format="json"
reverse("api:declared_element", kwargs={"pk": microorganism.id, "type": "microorganism"}), format="json"
)
body = response.json()
self.assertEqual(body["id"], microorganism.id)
Expand All @@ -1662,7 +1664,9 @@ def test_get_single_declared_substance(self):
declaration = DeclarationFactory()
substance = DeclaredSubstanceFactory(declaration=declaration)

response = self.client.get(reverse("api:declared_substance", kwargs={"pk": substance.id}), format="json")
response = self.client.get(
reverse("api:declared_element", kwargs={"pk": substance.id, "type": "substance"}), format="json"
)
body = response.json()
self.assertEqual(body["id"], substance.id)

Expand All @@ -1673,52 +1677,103 @@ def test_get_single_declared_ingredient(self):
declaration = DeclarationFactory()
ingredient = DeclaredIngredientFactory(declaration=declaration)

response = self.client.get(reverse("api:declared_ingredient", kwargs={"pk": ingredient.id}), format="json")
response = self.client.get(
reverse("api:declared_element", kwargs={"pk": ingredient.id, "type": "ingredient"}), format="json"
)
body = response.json()
self.assertEqual(body["id"], ingredient.id)

@authenticate
def test_cannot_get_declared_element_unknown_type(self):
InstructionRoleFactory(user=authenticate.user)

declaration = DeclarationFactory()
ingredient = DeclaredIngredientFactory(declaration=declaration)

response = self.client.get(
reverse("api:declared_element", kwargs={"pk": ingredient.id, "type": "unknown"}), format="json"
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(
response.json()["globalError"],
"Unknown type: 'unknown' not in ['plant', 'microorganism', 'substance', 'ingredient']",
)

def test_get_declared_element_not_allowed_not_authenticated(self):
response = self.client.get(reverse("api:declared_plant", kwargs={"pk": 1}), format="json")
response = self.client.get(reverse("api:declared_element", kwargs={"pk": 1, "type": "plant"}), format="json")
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

response = self.client.get(reverse("api:declared_microorganism", kwargs={"pk": 1}), format="json")
response = self.client.get(
reverse("api:declared_element", kwargs={"pk": 1, "type": "microorganism"}), format="json"
)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

response = self.client.get(reverse("api:declared_substance", kwargs={"pk": 1}), format="json")
response = self.client.get(
reverse("api:declared_element", kwargs={"pk": 1, "type": "substance"}), format="json"
)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

response = self.client.get(reverse("api:declared_ingredient", kwargs={"pk": 1}), format="json")
response = self.client.get(
reverse("api:declared_element", kwargs={"pk": 1, "type": "ingredient"}), format="json"
)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

@authenticate
def test_get_declared_element_not_allowed_not_instructor(self):
response = self.client.get(reverse("api:declared_plant", kwargs={"pk": 1}), format="json")
response = self.client.get(reverse("api:declared_element", kwargs={"pk": 1, "type": "plant"}), format="json")
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

response = self.client.get(reverse("api:declared_microorganism", kwargs={"pk": 1}), format="json")
response = self.client.get(
reverse("api:declared_element", kwargs={"pk": 1, "type": "microorganism"}), format="json"
)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

response = self.client.get(reverse("api:declared_substance", kwargs={"pk": 1}), format="json")
response = self.client.get(
reverse("api:declared_element", kwargs={"pk": 1, "type": "substance"}), format="json"
)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

response = self.client.get(reverse("api:declared_ingredient", kwargs={"pk": 1}), format="json")
response = self.client.get(
reverse("api:declared_element", kwargs={"pk": 1, "type": "ingredient"}), format="json"
)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

@authenticate
def test_update_declared_microorganism(self):
def test_request_info_declared_element(self):
InstructionRoleFactory(user=authenticate.user)

declaration = DeclarationFactory()
microorganism = DeclaredMicroorganismFactory(declaration=declaration)
self.assertNotEqual(microorganism.request_status, DeclaredMicroorganism.AddableStatus.INFORMATION)

self.client.patch(
reverse("api:declared_microorganism", kwargs={"pk": microorganism.id}),
{"requestStatus": DeclaredMicroorganism.AddableStatus.INFORMATION, "requestPrivateNotes": "some notes"},
response = self.client.post(
reverse("api:declared_element_request_info", kwargs={"pk": microorganism.id, "type": "microorganism"}),
hfroot marked this conversation as resolved.
Show resolved Hide resolved
{"requestPrivateNotes": "some notes"},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.json()["requestStatus"], DeclaredMicroorganism.AddableStatus.INFORMATION)
microorganism.refresh_from_db()
self.assertEqual(microorganism.request_private_notes, "some notes")
self.assertEqual(microorganism.request_status, "INFORMATION")
self.assertEqual(microorganism.request_status, DeclaredMicroorganism.AddableStatus.INFORMATION)

@authenticate
def test_reject_declared_element(self):
hfroot marked this conversation as resolved.
Show resolved Hide resolved
InstructionRoleFactory(user=authenticate.user)

declaration = DeclarationFactory()
microorganism = DeclaredMicroorganismFactory(declaration=declaration)
self.assertNotEqual(microorganism.request_status, DeclaredMicroorganism.AddableStatus.REJECTED)

response = self.client.post(
reverse("api:declared_element_reject", kwargs={"pk": microorganism.id, "type": "microorganism"}),
{"requestPrivateNotes": "some notes"},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
microorganism.refresh_from_db()
self.assertEqual(microorganism.request_private_notes, "some notes")
self.assertEqual(microorganism.request_status, DeclaredMicroorganism.AddableStatus.REJECTED)

@authenticate
def test_fields_hidden_from_declarant(self):
Expand Down Expand Up @@ -1753,3 +1808,46 @@ def test_status_visible_to_instructor(self):
m = declared_microorganisms[0]
self.assertIn("requestStatus", m)
self.assertIn("requestPrivateNotes", m)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Est-ce que ça vaut la peine d'ajouter un test sur les permissions pour ces nouveaux endpoints ? Similaire à test_get_declared_element_not_allowed_not_instructor

@authenticate
def test_replace_declared_plant(self):
hfroot marked this conversation as resolved.
Show resolved Hide resolved
InstructionRoleFactory(user=authenticate.user)

declaration = DeclarationFactory()
declared_plant = DeclaredPlantFactory(declaration=declaration, new=True)
self.assertNotEqual(declared_plant.request_status, DeclaredPlant.AddableStatus.REPLACED)
plant = PlantFactory()

response = self.client.post(
reverse("api:declared_element_replace", kwargs={"pk": declared_plant.id, "type": "plant"}),
{"element": {"id": plant.id, "type": "plant"}},
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)
self.assertFalse(declared_plant.new)
self.assertEqual(declared_plant.plant, plant)

@authenticate
def test_cannot_replace_element_different_type(self):
"""
Pour reduire le scope de changements, temporairement bloque le remplacement d'une demande
avec un element d'un type different
"""
InstructionRoleFactory(user=authenticate.user)

declaration = DeclarationFactory()
declared_plant = DeclaredPlantFactory(declaration=declaration)
self.assertEqual(declared_plant.request_status, DeclaredPlant.AddableStatus.REQUESTED)
microorganism = MicroorganismFactory()

response = self.client.post(
reverse("api:declared_element_replace", kwargs={"pk": declared_plant.id, "type": "plant"}),
{"element": {"id": microorganism.id, "type": "microorganism"}},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.json())
declared_plant.refresh_from_db()
self.assertEqual(declared_plant.request_status, DeclaredPlant.AddableStatus.REQUESTED)
self.assertNotEqual(declared_plant.plant, microorganism)
Loading
Loading