diff --git a/lemarche/tenders/admin.py b/lemarche/tenders/admin.py index e2cc5286b..0d36f9d88 100644 --- a/lemarche/tenders/admin.py +++ b/lemarche/tenders/admin.py @@ -16,7 +16,7 @@ from lemarche.perimeters.admin import PerimeterRegionFilter from lemarche.tenders import constants from lemarche.tenders.forms import TenderAdminForm -from lemarche.tenders.models import PartnerShareTender, Tender, TenderQuestion +from lemarche.tenders.models import PartnerShareTender, Tender, TenderQuestion, TenderStepsData from lemarche.utils.admin.admin_site import admin_site from lemarche.utils.apis import api_hubspot from lemarche.utils.fields import ChoiceArrayField, pretty_print_readonly_jsonfield @@ -591,3 +591,32 @@ def logs_display(self, partnersharetender=None): return "-" logs_display.short_description = PartnerShareTender._meta.get_field("logs").verbose_name + + +@admin.register(TenderStepsData, site=admin_site) +class TenderStepsDataAdmin(admin.ModelAdmin): + list_display = ["created_at", "updated_at", "uuid"] + + readonly_fields = [ + "id", + "created_at", + "updated_at", + "uuid", + "steps_data_display", + ] + + def steps_data_display(self, tender_steps_data: TenderStepsData = None): + if tender_steps_data: + return pretty_print_readonly_jsonfield(tender_steps_data.steps_data) + return "-" + + steps_data_display.short_description = "Données saisies dans les étapes" + + def has_add_permission(self, request): + return False + + def has_change_permission(self, request, obj=None): + return False + + def has_delete_permission(self, request, obj=None): + return False diff --git a/lemarche/tenders/migrations/0058_tenderstepsdata.py b/lemarche/tenders/migrations/0058_tenderstepsdata.py new file mode 100644 index 000000000..42d9cbda0 --- /dev/null +++ b/lemarche/tenders/migrations/0058_tenderstepsdata.py @@ -0,0 +1,43 @@ +# Generated by Django 4.2.2 on 2023-10-09 13:35 + +import django.utils.timezone +from django.db import migrations, models +from django_extensions.db.fields import ShortUUIDField +from shortuuid import uuid + + +class Migration(migrations.Migration): + dependencies = [ + ("tenders", "0057_alter_tender_siae_transactioned"), + ] + + operations = [ + migrations.CreateModel( + name="TenderStepsData", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ( + "uuid", + ShortUUIDField( + auto_created=True, + blank=True, + db_index=True, + default=uuid, + editable=False, + unique=True, + verbose_name="Identifiant UUID", + ), + ), + ( + "created_at", + models.DateTimeField(default=django.utils.timezone.now, verbose_name="Date de création"), + ), + ("updated_at", models.DateTimeField(auto_now=True, verbose_name="Date de modification")), + ("steps_data", models.JSONField(default=list, editable=False, verbose_name="Données des étapes")), + ], + options={ + "verbose_name": "Besoin d'achat - Données des étapes", + "verbose_name_plural": "Besoins d'achat - Données des étapes", + }, + ), + ] diff --git a/lemarche/tenders/models.py b/lemarche/tenders/models.py index dc261e9ac..2c97ecaa0 100644 --- a/lemarche/tenders/models.py +++ b/lemarche/tenders/models.py @@ -11,6 +11,8 @@ from django.utils.functional import cached_property from django.utils.text import slugify from django_better_admin_arrayfield.models.fields import ArrayField +from django_extensions.db.fields import ShortUUIDField +from shortuuid import uuid from lemarche.perimeters.models import Perimeter from lemarche.siaes import constants as siae_constants @@ -761,3 +763,31 @@ class Meta: @cached_property def perimeters_list_string(self) -> str: return ", ".join(self.perimeters.values_list("name", flat=True)) + + +class TenderStepsData(models.Model): + FIELDS_TO_REDACT = [ + "contact-contact_email", + "contact-contact_phone", + "contact-contact_last_name", + "contact-contact_first_name", + ] + + created_at = models.DateTimeField(verbose_name="Date de création", default=timezone.now) + updated_at = models.DateTimeField(verbose_name="Date de modification", auto_now=True) + uuid = ShortUUIDField( + verbose_name="Identifiant UUID", + default=uuid, + editable=False, + unique=True, + db_index=True, + auto_created=True, + ) + steps_data = models.JSONField(verbose_name="Données des étapes", editable=False, default=list) + + class Meta: + verbose_name = "Besoin d'achat - Données des étapes" + verbose_name_plural = "Besoins d'achat - Données des étapes" + + def __str__(self): + return f"{self.uuid} - {self.created_at}" diff --git a/lemarche/www/pages/views.py b/lemarche/www/pages/views.py index 52fe8cd04..b9ad96b6f 100644 --- a/lemarche/www/pages/views.py +++ b/lemarche/www/pages/views.py @@ -15,7 +15,7 @@ from lemarche.sectors.models import Sector from lemarche.siaes.models import Siae, SiaeGroup from lemarche.tenders import constants as tender_constants -from lemarche.tenders.models import Tender +from lemarche.tenders.models import Tender, TenderStepsData from lemarche.users.models import User from lemarche.utils.tracker import track from lemarche.www.pages.forms import ( @@ -357,6 +357,11 @@ def csrf_failure(request, reason=""): # noqa C901 tender.save() tender.set_siae_found_list() + # remove steps data + uuid = request.session.get("tender_steps_data_uuid", None) + if uuid: + TenderStepsData.objects.filter(uuid=uuid).delete() + if settings.BITOUBI_ENV == "prod": notify_admin_tender_created(tender) diff --git a/lemarche/www/tenders/tests.py b/lemarche/www/tenders/tests.py index e99dc36e6..020dd3f62 100644 --- a/lemarche/www/tenders/tests.py +++ b/lemarche/www/tenders/tests.py @@ -18,7 +18,7 @@ from lemarche.siaes.models import Siae from lemarche.tenders import constants as tender_constants from lemarche.tenders.factories import TenderFactory, TenderQuestionFactory -from lemarche.tenders.models import Tender, TenderSiae +from lemarche.tenders.models import Tender, TenderSiae, TenderStepsData from lemarche.users.factories import UserFactory from lemarche.users.models import User from lemarche.www.tenders.views import TenderCreateMultiStepView @@ -83,12 +83,21 @@ def _check_every_step(self, tenders_step_data, final_redirect_page: str = revers # make sure that after the create tender we are redirected to ?? self.assertEqual(response.status_code, 200) self.assertRedirects(response, final_redirect_page) + # has the step datas been cleaned ? + self.assertEqual(TenderStepsData.objects.count(), 0) return response else: self.assertEqual(response.status_code, 200) current_errors = response.context_data["form"].errors self.assertEquals(current_errors, {}) + # Is the step data stored correctly ? + tender_step_data = TenderStepsData.objects.first() + self.assertEqual( + data_step["tender_create_multi_step_view-current_step"], + tender_step_data.steps_data[-1]["tender_create_multi_step_view-current_step"], + ) + def test_anyone_can_access_create_tender(self): # anonymous url = reverse("tenders:create") diff --git a/lemarche/www/tenders/views.py b/lemarche/www/tenders/views.py index c33c2abf3..ea11a18f7 100644 --- a/lemarche/www/tenders/views.py +++ b/lemarche/www/tenders/views.py @@ -14,7 +14,7 @@ from lemarche.siaes.models import Siae from lemarche.tenders import constants as tender_constants -from lemarche.tenders.models import Tender, TenderSiae +from lemarche.tenders.models import Tender, TenderSiae, TenderStepsData from lemarche.users.models import User from lemarche.utils.data import get_choice from lemarche.utils.mixins import ( @@ -148,6 +148,35 @@ def get_context_data(self, form, **kwargs): context.update({"tender": tender_dict}) return context + def process_step(self, form): + """ + Save step data + """ + data = form.data.copy() + if "csrfmiddlewaretoken" in data: + del data["csrfmiddlewaretoken"] + + # Hide personal data + for field_to_redacted in TenderStepsData.FIELDS_TO_REDACT: + if field_to_redacted in data: + data[field_to_redacted] = "[REDACTED]" + + data["timestamp"] = timezone.now().isoformat() + + uuid = self.request.session.get("tender_steps_data_uuid", None) + if uuid: + try: + tender_steps_data = TenderStepsData.objects.get(uuid=uuid) + tender_steps_data.steps_data.append(data) + tender_steps_data.save() + except TenderStepsData.DoesNotExist: + tender_steps_data = TenderStepsData.objects.create(uuid=uuid, steps_data=[data]) + else: + tender_steps_data = TenderStepsData.objects.create(steps_data=[data]) + self.request.session["tender_steps_data_uuid"] = tender_steps_data.uuid + + return form.data + def save_instance_tender(self, tender_dict: dict, form_dict: dict, is_draft: bool): tender_status = tender_constants.STATUS_DRAFT if is_draft else tender_constants.STATUS_PUBLISHED tender_published_at = None if is_draft else timezone.now() @@ -201,6 +230,11 @@ def done(self, _, form_dict, **kwargs): self.save_instance_tender(tender_dict=tender_dict, form_dict=form_dict, is_draft=is_draft) self.instance.set_siae_found_list() + # remove steps data + uuid = self.request.session.get("tender_steps_data_uuid", None) + if uuid: + TenderStepsData.objects.filter(uuid=uuid).delete() + # we notify the admin team if settings.BITOUBI_ENV == "prod": notify_admin_tender_created(self.instance)