From ef46d20e67696214ca52520d87e712afcf2c4a90 Mon Sep 17 00:00:00 2001 From: Raphael Odini Date: Thu, 8 Feb 2024 12:57:46 +0100 Subject: [PATCH] =?UTF-8?q?D=C3=A9p=C3=B4t=20de=20besoin=20:=20sondage=20a?= =?UTF-8?q?ux=20prestataires=20:=20formulaire=20et=20vue=20(#1072)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * New TenderDetailSiaeSurveyTransactionedView. Add form & mixin * Update tests. Improve breadcrumb, add tender info --- .../tenders/survey_transactioned_detail.html | 10 +- lemarche/utils/mixins.py | 15 ++- lemarche/www/tenders/forms.py | 27 +++- lemarche/www/tenders/tests.py | 124 ++++++++++++++---- lemarche/www/tenders/urls.py | 6 + lemarche/www/tenders/views.py | 75 +++++++++++ 6 files changed, 226 insertions(+), 31 deletions(-) diff --git a/lemarche/templates/tenders/survey_transactioned_detail.html b/lemarche/templates/tenders/survey_transactioned_detail.html index 7bb7ebcf0..b4d155436 100644 --- a/lemarche/templates/tenders/survey_transactioned_detail.html +++ b/lemarche/templates/tenders/survey_transactioned_detail.html @@ -12,7 +12,7 @@ @@ -43,6 +43,14 @@

+
+ +
{% if tender.survey_transactioned_answer == None or tender.survey_transactioned_answer == True %} diff --git a/lemarche/utils/mixins.py b/lemarche/utils/mixins.py index a211673dd..1098c71d9 100644 --- a/lemarche/utils/mixins.py +++ b/lemarche/utils/mixins.py @@ -72,7 +72,7 @@ class SiaeMemberRequiredMixin(LoginRequiredUserPassesTestMixin): def test_func(self): user = self.request.user - siae_slug = self.kwargs.get("slug") + siae_slug = self.kwargs.get("siae_slug") or self.kwargs.get("slug") return user.is_authenticated and (siae_slug in user.siaes.values_list("slug", flat=True)) def handle_no_permission(self): @@ -206,3 +206,16 @@ def test_func(self): def handle_no_permission(self): return HttpResponseRedirect(reverse_lazy("tenders:detail", args=[self.kwargs.get("slug")])) + + +class SesameSiaeMemberRequiredMixin(SesameTokenRequiredUserPassesTestMixin): + """ + Restrict access to the Tender's author + """ + + def test_func(self): + siae_slug = self.kwargs.get("siae_slug") or self.kwargs.get("slug") + return siae_slug in self.request.user.siaes.values_list("slug", flat=True) + + def handle_no_permission(self): + return HttpResponseRedirect(reverse_lazy("dashboard:home")) diff --git a/lemarche/www/tenders/forms.py b/lemarche/www/tenders/forms.py index 26dbc1d54..076d65c5f 100644 --- a/lemarche/www/tenders/forms.py +++ b/lemarche/www/tenders/forms.py @@ -5,7 +5,7 @@ from lemarche.sectors.models import Sector from lemarche.tenders import constants as tender_constants -from lemarche.tenders.models import Tender +from lemarche.tenders.models import Tender, TenderSiae from lemarche.users.models import User from lemarche.utils.fields import GroupedModelMultipleChoiceField @@ -315,3 +315,28 @@ def __init__(self, tender_survey_transactioned_answer=None, *args, **kwargs): self.fields["survey_transactioned_answer"].disabled = True if tender_survey_transactioned_answer is False: self.fields["survey_transactioned_amount"].widget = forms.HiddenInput() + + +class TenderSiaeSurveyTransactionedForm(forms.ModelForm): + class Meta: + model = TenderSiae + fields = [ + "survey_transactioned_answer", + "survey_transactioned_amount", + "survey_transactioned_feedback", + ] + + def __init__(self, tender_survey_transactioned_answer=None, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["survey_transactioned_answer"].label = "Avez-vous contractualisé avec le client ?" + self.fields["survey_transactioned_amount"].label = "Quel est le montant de la transaction ? (facultatif)" + self.fields["survey_transactioned_feedback"].label = "Partagez-nous votre retour d'expérience (facultatif)" + self.fields["survey_transactioned_feedback"].widget.attrs.update( + { + "placeholder": "Lors de mon expérience avec le Marché de l'inclusion :\n- j'ai apprécié ...\n- j'ai moins aimé ...\n- vous pourriez vous améliorer dans ..." # noqa + } + ) + if tender_survey_transactioned_answer is not None: + self.fields["survey_transactioned_answer"].disabled = True + if tender_survey_transactioned_answer is False: + self.fields["survey_transactioned_amount"].widget = forms.HiddenInput() diff --git a/lemarche/www/tenders/tests.py b/lemarche/www/tenders/tests.py index c832de4fd..43e17f5d7 100644 --- a/lemarche/www/tenders/tests.py +++ b/lemarche/www/tenders/tests.py @@ -1696,10 +1696,10 @@ def setUpTestData(cls): cls.user_admin = UserFactory(kind=User.KIND_ADMIN) cls.tender = TenderFactory(kind=tender_constants.KIND_TENDER, author=cls.user_buyer_1, siaes=[cls.siae]) cls.user_buyer_1_sesame_query_string = sesame_get_query_string(cls.user_buyer_1) + cls.url = reverse("tenders:detail-survey-transactioned", kwargs={"slug": cls.tender.slug}) def test_anonymous_user_cannot_call_tender_survey_transactioned(self): - url = reverse("tenders:detail-survey-transactioned", kwargs={"slug": self.tender.slug}) - response = self.client.post(url) + response = self.client.post(self.url) self.assertEqual(response.status_code, 403) def test_only_tender_author_with_sesame_token_can_call_tender_survey_transactioned(self): @@ -1713,28 +1713,20 @@ def test_only_tender_author_with_sesame_token_can_call_tender_survey_transaction self.user_admin, ]: self.client.force_login(user) - url = reverse("tenders:detail-survey-transactioned", kwargs={"slug": self.tender.slug}) - response = self.client.get(url) + response = self.client.get(self.url) self.assertEqual(response.status_code, 403) # logout the last user to be sure self.client.logout() # authorized user_sesame_query_string = sesame_get_query_string(self.user_buyer_1) - url = ( - reverse("tenders:detail-survey-transactioned", kwargs={"slug": self.tender.slug}) - + user_sesame_query_string - ) + url = self.url + user_sesame_query_string response = self.client.get(url, follow=True) self.assertEqual(response.status_code, 200) # full form displayed (but should never happen) def test_update_tender_stats_on_tender_survey_transactioned_answer_true(self): # load with answer 'True': partial form - url = ( - reverse("tenders:detail-survey-transactioned", kwargs={"slug": self.tender.slug}) - + self.user_buyer_1_sesame_query_string - + "&answer=True" - ) + url = self.url + self.user_buyer_1_sesame_query_string + "&answer=True" response = self.client.get(url, follow=True) self.assertEqual(response.status_code, 200) self.assertTrue(Tender.objects.get(id=self.tender.id).survey_transactioned_answer) @@ -1749,11 +1741,7 @@ def test_update_tender_stats_on_tender_survey_transactioned_answer_true(self): self.assertTrue(Tender.objects.get(id=self.tender.id).survey_transactioned_answer) self.assertEqual(Tender.objects.get(id=self.tender.id).survey_transactioned_amount, 1000) # reload with answer, ignore changes and redirect - url = ( - reverse("tenders:detail-survey-transactioned", kwargs={"slug": self.tender.slug}) - + self.user_buyer_1_sesame_query_string - + "&answer=False" - ) + url = self.url + self.user_buyer_1_sesame_query_string + "&answer=False" response = self.client.get(url, follow=True) self.assertEqual(response.status_code, 200) # redirect self.assertRedirects(response, reverse("tenders:detail", kwargs={"slug": self.tender.slug})) @@ -1763,11 +1751,7 @@ def test_update_tender_stats_on_tender_survey_transactioned_answer_true(self): def test_update_tender_stats_on_tender_survey_transactioned_answer_false(self): # load with answer 'False': partial form - url = ( - reverse("tenders:detail-survey-transactioned", kwargs={"slug": self.tender.slug}) - + self.user_buyer_1_sesame_query_string - + "&answer=False" - ) + url = self.url + self.user_buyer_1_sesame_query_string + "&answer=False" response = self.client.get(url, follow=True) self.assertEqual(response.status_code, 200) self.assertFalse(Tender.objects.get(id=self.tender.id).survey_transactioned_answer) @@ -1780,14 +1764,98 @@ def test_update_tender_stats_on_tender_survey_transactioned_answer_false(self): self.assertFalse(Tender.objects.get(id=self.tender.id).survey_transactioned_answer) self.assertIsNone(Tender.objects.get(id=self.tender.id).survey_transactioned_amount) # reload with answer, ignore changes - url = ( - reverse("tenders:detail-survey-transactioned", kwargs={"slug": self.tender.slug}) - + self.user_buyer_1_sesame_query_string - + "&answer=True" - ) + url = self.url + self.user_buyer_1_sesame_query_string + "&answer=True" response = self.client.get(url, follow=True) self.assertEqual(response.status_code, 200) # redirect self.assertRedirects(response, reverse("tenders:detail", kwargs={"slug": self.tender.slug})) self.assertContains(response, "Votre réponse a déjà été prise en compte") self.assertFalse(Tender.objects.get(id=self.tender.id).survey_transactioned_answer) self.assertFalse(Tender.objects.get(id=self.tender.id).siae_transactioned) + + +class TenderDetailSiaeSurveyTransactionedViewTest(TestCase): + @classmethod + def setUpTestData(cls): + cls.siae = SiaeFactory(name="ZZ ESI") + cls.siae_user_1 = UserFactory(kind=User.KIND_SIAE, siaes=[cls.siae]) + cls.siae_user_2 = UserFactory(kind=User.KIND_SIAE) + cls.user_buyer_1 = UserFactory(kind=User.KIND_BUYER) + cls.user_buyer_2 = UserFactory(kind=User.KIND_BUYER) + cls.user_partner = UserFactory(kind=User.KIND_PARTNER) + cls.user_admin = UserFactory(kind=User.KIND_ADMIN) + cls.tender = TenderFactory(kind=tender_constants.KIND_TENDER, author=cls.user_buyer_1) + cls.tendersiae = TenderSiae.objects.create(tender=cls.tender, siae=cls.siae) + cls.url = reverse( + "tenders:detail-siae-survey-transactioned", kwargs={"slug": cls.tender.slug, "siae_slug": cls.siae.slug} + ) + cls.user_siae_1_sesame_query_string = sesame_get_query_string(cls.siae_user_1) + + def test_anonymous_user_cannot_call_tender_siae_survey_transactioned(self): + response = self.client.post(self.url) + self.assertEqual(response.status_code, 403) + + def test_only_tender_author_with_sesame_token_can_call_tender_siae_survey_transactioned(self): + # forbidden + for user in [ + self.siae_user_1, + self.siae_user_2, + self.user_buyer_1, + self.user_buyer_2, + self.user_partner, + self.user_admin, + ]: + self.client.force_login(user) + response = self.client.get(self.url) + self.assertEqual(response.status_code, 403) + # logout the last user to be sure + self.client.logout() + # authorized + user_sesame_query_string = sesame_get_query_string(self.siae_user_1) + url = self.url + user_sesame_query_string + response = self.client.get(url, follow=True) + self.assertEqual(response.status_code, 200) + # full form displayed (but should never happen) + + def test_update_tender_stats_on_tender_siae_survey_transactioned_answer_true(self): + # load with answer 'True': partial form + url = self.url + self.user_siae_1_sesame_query_string + "&answer=True" + response = self.client.get(url, follow=True) + self.assertEqual(response.status_code, 200) + self.assertTrue(TenderSiae.objects.get(tender=self.tender, siae=self.siae).survey_transactioned_answer) + # fill in form + response = self.client.post( + url, data={"survey_transactioned_amount": 1000, "survey_transactioned_feedback": "Feedback"}, follow=True + ) + self.assertEqual(response.status_code, 200) # redirect + self.assertRedirects(response, reverse("tenders:detail", kwargs={"slug": self.tender.slug})) + self.assertContains(response, "Merci pour votre réponse") + self.assertTrue(TenderSiae.objects.get(tender=self.tender, siae=self.siae).survey_transactioned_answer) + self.assertEqual(TenderSiae.objects.get(tender=self.tender, siae=self.siae).survey_transactioned_amount, 1000) + # reload with answer, ignore changes and redirect + url = self.url + self.user_siae_1_sesame_query_string + "&answer=False" + response = self.client.get(url, follow=True) + self.assertEqual(response.status_code, 200) # redirect + self.assertRedirects(response, reverse("tenders:detail", kwargs={"slug": self.tender.slug})) + self.assertContains(response, "Votre réponse a déjà été prise en compte") + self.assertTrue(TenderSiae.objects.get(tender=self.tender, siae=self.siae).survey_transactioned_answer) + + def test_update_tender_stats_on_tender_siae_survey_transactioned_answer_false(self): + # load with answer 'False': partial form + url = self.url + self.user_siae_1_sesame_query_string + "&answer=False" + response = self.client.get(url, follow=True) + self.assertEqual(response.status_code, 200) + self.assertFalse(TenderSiae.objects.get(tender=self.tender, siae=self.siae).survey_transactioned_answer) + # fill in form + response = self.client.post(url, data={"survey_transactioned_feedback": "Feedback"}, follow=True) + self.assertEqual(response.status_code, 200) # redirect + self.assertRedirects(response, reverse("tenders:detail", kwargs={"slug": self.tender.slug})) + self.assertContains(response, "Merci pour votre réponse") + self.assertFalse(TenderSiae.objects.get(tender=self.tender, siae=self.siae).survey_transactioned_answer) + self.assertIsNone(TenderSiae.objects.get(tender=self.tender, siae=self.siae).survey_transactioned_amount) + # reload with answer, ignore changes + url = self.url + self.user_siae_1_sesame_query_string + "&answer=True" + response = self.client.get(url, follow=True) + self.assertEqual(response.status_code, 200) # redirect + self.assertRedirects(response, reverse("tenders:detail", kwargs={"slug": self.tender.slug})) + self.assertContains(response, "Votre réponse a déjà été prise en compte") + self.assertFalse(TenderSiae.objects.get(tender=self.tender, siae=self.siae).survey_transactioned_answer) diff --git a/lemarche/www/tenders/urls.py b/lemarche/www/tenders/urls.py index 02446598b..93f7fff2c 100644 --- a/lemarche/www/tenders/urls.py +++ b/lemarche/www/tenders/urls.py @@ -6,6 +6,7 @@ TenderDetailCocontractingClickView, TenderDetailContactClickStatView, TenderDetailNotInterestedClickView, + TenderDetailSiaeSurveyTransactionedView, TenderDetailSurveyTransactionedView, TenderDetailView, TenderListView, @@ -49,4 +50,9 @@ TenderDetailSurveyTransactionedView.as_view(), name="detail-survey-transactioned", ), + path( + "/prestataires//sondage-transaction", + TenderDetailSiaeSurveyTransactionedView.as_view(), + name="detail-siae-survey-transactioned", + ), ] diff --git a/lemarche/www/tenders/views.py b/lemarche/www/tenders/views.py index f646fa2da..058765d23 100644 --- a/lemarche/www/tenders/views.py +++ b/lemarche/www/tenders/views.py @@ -19,6 +19,7 @@ from lemarche.users.models import User from lemarche.utils.data import get_choice from lemarche.utils.mixins import ( + SesameSiaeMemberRequiredMixin, SesameTenderAuthorRequiredMixin, SiaeUserRequiredOrSiaeIdParamMixin, TenderAuthorOrAdminRequiredIfNotSentMixin, @@ -31,6 +32,7 @@ TenderCreateStepDetailForm, TenderCreateStepGeneralForm, TenderCreateStepSurveyForm, + TenderSiaeSurveyTransactionedForm, TenderSurveyTransactionedForm, ) from lemarche.www.tenders.tasks import ( # , send_tender_emails_to_siaes @@ -624,6 +626,79 @@ def get(self, request, *args, **kwargs): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["tender"] = self.object + context["parent_title"] = TITLE_DETAIL_PAGE_OTHERS + return context + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs["tender_survey_transactioned_answer"] = self.object.survey_transactioned_answer + return kwargs + + def form_valid(self, form): + super().form_valid(form) + messages.add_message(self.request, messages.SUCCESS, self.get_success_message()) + return HttpResponseRedirect(self.get_success_url()) + + def get_success_url(self): + success_url = reverse_lazy("tenders:detail", args=[self.kwargs.get("slug")]) + return success_url + + def get_success_message(self, already_answered=False): + if already_answered: + return "Votre réponse a déjà été prise en compte." + return "Merci pour votre réponse !" + + +class TenderDetailSiaeSurveyTransactionedView(SesameSiaeMemberRequiredMixin, UpdateView): + """ + Endpoint to store the tender siae survey transactioned answer + """ + + template_name = "tenders/survey_transactioned_detail.html" # same template as author survey + form_class = TenderSiaeSurveyTransactionedForm + queryset = TenderSiae.objects.all() + # success_message (see get_success_message() below) + # success_url (see get_success_url() below) + + def get(self, request, *args, **kwargs): + """ + TenderSiae.survey_transactioned_answer field is updated only if: + - the user should be the tender author (thanks to SesameTenderAuthorRequiredMixin) + - the field is None in the database (first time answering) + - the GET parameter 'answer' is passed + """ + self.object = self.get_object() + survey_transactioned_answer = request.GET.get("answer", None) + # first time answering + if self.object.survey_transactioned_answer is None: + if survey_transactioned_answer in ["True", "False"]: + # transform survey_transactioned_answer into bool + survey_transactioned_answer = survey_transactioned_answer == "True" + # update survey_transactioned_answer + TenderSiae.objects.filter(id=self.object.id).update( + survey_transactioned_answer=survey_transactioned_answer, + survey_transactioned_answer_date=timezone.now(), + updated_at=timezone.now(), + ) + else: + pass + # TODO or not? "answer" should always be passed + return super().get(request, *args, **kwargs) + # already answered + else: + messages.add_message(self.request, messages.WARNING, self.get_success_message(already_answered=True)) + return HttpResponseRedirect(self.get_success_url()) + + def get_object(self): + self.tender = Tender.objects.get(slug=self.kwargs.get("slug")) + self.siae = Siae.objects.get(slug=self.kwargs.get("siae_slug")) + return get_object_or_404(TenderSiae, tender=self.tender, siae=self.siae) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["tender"] = self.tender + context["siae"] = self.siae + context["parent_title"] = TITLE_DETAIL_PAGE_SIAE return context def get_form_kwargs(self):