Skip to content

Commit

Permalink
Dépôt de besoin : utiliser django-sesame pour générer les liens du so…
Browse files Browse the repository at this point in the history
…ndage J+30 (#880)

* Install django-sesame

* Transform TenderDetailSurveyTransactionedView using sesame get_user

* Custom sesame mixin

* Add tests
  • Loading branch information
raphodn authored Aug 29, 2023
1 parent 746929e commit 185f62d
Show file tree
Hide file tree
Showing 9 changed files with 186 additions and 61 deletions.
12 changes: 12 additions & 0 deletions config/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,11 @@
# Authentication
# ------------------------------------------------------------------------------

AUTHENTICATION_BACKENDS = [
"django.contrib.auth.backends.ModelBackend",
"sesame.backends.ModelBackend",
]

# Password validation
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators

Expand All @@ -268,6 +273,13 @@
LOGOUT_REDIRECT_URL = "/"


# Django Sesame
# https://django-sesame.readthedocs.io/en/stable/index.html
# ------------------------------------------------------------------------------

SESAME_TOKEN_NAME = "token"


# Internationalization
# https://docs.djangoproject.com/en/3.2/topics/i18n/
# ------------------------------------------------------------------------------
Expand Down
42 changes: 28 additions & 14 deletions lemarche/utils/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from django.contrib.auth.views import redirect_to_login
from django.http import HttpResponseForbidden, HttpResponseRedirect
from django.urls import reverse_lazy
from sesame.utils import get_user as sesame_get_user

from lemarche.tenders import constants as tender_constants
from lemarche.tenders.models import Tender
Expand Down Expand Up @@ -136,20 +137,6 @@ def handle_no_permission(self):
return HttpResponseRedirect(reverse_lazy("dashboard:home"))


class TenderAuthorRequiredMixin(LoginRequiredUserPassesTestMixin):
"""
Restrict access to the Tender's author
"""

def test_func(self):
user = self.request.user
tender_slug = self.kwargs.get("slug")
return user.is_authenticated and (tender_slug in user.tenders.values_list("slug", flat=True))

def handle_no_permission(self):
return HttpResponseForbidden()


class TenderAuthorOrAdminRequiredMixin(LoginRequiredUserPassesTestMixin):
"""
Restrict access to the Tender's author (or Admin)
Expand Down Expand Up @@ -182,3 +169,30 @@ def test_func(self):
def handle_no_permission(self):
messages.add_message(self.request, messages.WARNING, "Vous n'avez pas accès à cette page.")
return HttpResponseRedirect(reverse_lazy("wagtail_serve", args=("",)))


class SesameTokenRequiredUserPassesTestMixin(UserPassesTestMixin):
"""
Custom mixin that checks that a valid django-sesame token is passed
"""

def dispatch(self, request, *args, **kwargs):
user = sesame_get_user(self.request)
if not user:
return HttpResponseForbidden()
# add user to request
request.user = user
return super().dispatch(request, *args, **kwargs)


class SesameTenderAuthorRequiredMixin(SesameTokenRequiredUserPassesTestMixin):
"""
Restrict access to the Tender's author
"""

def test_func(self):
tender_slug = self.kwargs.get("slug")
return tender_slug in self.request.user.tenders.values_list("slug", flat=True)

def handle_no_permission(self):
return HttpResponseRedirect(reverse_lazy("tenders:detail", args=[self.kwargs.get("slug")]))
8 changes: 8 additions & 0 deletions lemarche/www/tenders/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

from django.conf import settings
from django.template.loader import render_to_string
from django.urls import reverse
from django.utils import timezone
from sesame.utils import get_query_string as sesame_get_query_string

from lemarche.siaes.models import Siae
from lemarche.tenders.models import PartnerShareTender, Tender, TenderSiae
Expand Down Expand Up @@ -500,6 +502,12 @@ def send_tenders_author_30_days(tender: Tender, kind="feedback"):

if kind == "transactioned_question":
template_id = settings.MAILJET_TENDERS_AUTHOR_TRANSACTIONED_QUESTION_30D_TEMPLATE_ID
user_sesame_query_string = sesame_get_query_string(tender.author) # TODO: sesame scope parameter
answer_url_with_sesame_token = (
reverse("detail-survey-transactioned", args=[tender.slug]) + user_sesame_query_string
)
variables["ANSWER_YES_URL"] = answer_url_with_sesame_token + "&answer=true"
variables["ANSWER_NO_URL"] = answer_url_with_sesame_token + "&answer=false"
else:
template_id = settings.MAILJET_TENDERS_AUTHOR_FEEDBACK_30D_TEMPLATE_ID

Expand Down
148 changes: 106 additions & 42 deletions lemarche/www/tenders/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from django.test import TestCase
from django.urls import reverse
from django.utils import timezone
from sesame.utils import get_query_string as sesame_get_query_string

from lemarche.perimeters.factories import PerimeterFactory
from lemarche.perimeters.models import Perimeter
Expand Down Expand Up @@ -456,7 +457,7 @@ def test_only_author_or_admin_can_view_non_validated_tender(self):
# anonymous
url = reverse("tenders:detail", kwargs={"slug": tender.slug})
response = self.client.get(url)
self.assertEqual(response.status_code, 302) # redirect
self.assertEqual(response.status_code, 302)
# self.assertContains(response.url, "/accounts/login/?next=/besoins/")
# author & admin
for user in [self.user_buyer_1, self.user_admin]:
Expand All @@ -469,7 +470,7 @@ def test_only_author_or_admin_can_view_non_validated_tender(self):
self.client.force_login(user)
url = reverse("tenders:detail", kwargs={"slug": tender.slug})
response = self.client.get(url)
self.assertEqual(response.status_code, 302) # redirect
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, "/")

def test_tender_basic_fields_display(self):
Expand Down Expand Up @@ -818,18 +819,18 @@ def test_anonymous_user_cannot_call_tender_contact_click(self):
self.assertTrue(response.url.startswith("/accounts/login/"))

def test_only_siae_user_can_call_tender_contact_click(self):
# authorized
for user in [self.siae_user_1, self.siae_user_2]:
self.client.force_login(user)
url = reverse("tenders:detail-contact-click-stat", kwargs={"slug": self.tender.slug})
response = self.client.post(url, data={"detail_contact_click_confirm": "false"})
self.assertEqual(response.status_code, 302) # redirect
# forbidden
for user in [self.user_buyer_1, self.user_buyer_2, self.user_partner, self.user_admin]:
self.client.force_login(user)
url = reverse("tenders:detail-contact-click-stat", kwargs={"slug": self.tender.slug})
response = self.client.post(url, data={"detail_contact_click_confirm": "false"})
self.assertEqual(response.status_code, 403)
# authorized
for user in [self.siae_user_1, self.siae_user_2]:
self.client.force_login(user)
url = reverse("tenders:detail-contact-click-stat", kwargs={"slug": self.tender.slug})
response = self.client.post(url, data={"detail_contact_click_confirm": "false"})
self.assertEqual(response.status_code, 302)

def test_update_tendersiae_stats_on_tender_contact_click(self):
siae_2 = SiaeFactory(name="ABC Insertion")
Expand All @@ -848,7 +849,7 @@ def test_update_tendersiae_stats_on_tender_contact_click(self):
# click on button
url = reverse("tenders:detail-contact-click-stat", kwargs={"slug": self.tender.slug})
response = self.client.post(url, data={"detail_contact_click_confirm": "true"})
self.assertEqual(response.status_code, 302) # redirect
self.assertEqual(response.status_code, 302)
siae_2_detail_contact_click_date = self.tender.tendersiae_set.first().detail_contact_click_date
self.assertNotEqual(siae_2_detail_contact_click_date, None)
self.assertEqual(self.tender.tendersiae_set.last().detail_contact_click_date, None)
Expand All @@ -860,7 +861,7 @@ def test_update_tendersiae_stats_on_tender_contact_click(self):
# Note: button will disappear on reload
url = reverse("tenders:detail-contact-click-stat", kwargs={"slug": self.tender.slug})
response = self.client.post(url, data={"detail_contact_click_confirm": "false"})
self.assertEqual(response.status_code, 302) # redirect
self.assertEqual(response.status_code, 302)
self.assertEqual(
self.tender.tendersiae_set.first().detail_contact_click_date, siae_2_detail_contact_click_date
)
Expand Down Expand Up @@ -947,6 +948,13 @@ def test_anonymous_user_cannot_view_tender_siae_interested_list(self):
self.assertTrue(response.url.startswith("/accounts/login/"))

def test_only_tender_author_can_view_tender_1_siae_interested_list(self):
# forbidden
for user in [self.user_buyer_2, self.user_partner, self.siae_user_1, self.siae_user_2]:
self.client.force_login(user)
url = reverse("tenders:detail-siae-list", kwargs={"slug": self.tender_1.slug})
response = self.client.get(url)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, "/besoins/")
# authorized
self.client.force_login(self.user_buyer_1)
url = reverse("tenders:detail-siae-list", kwargs={"slug": self.tender_1.slug})
Expand All @@ -957,13 +965,6 @@ def test_only_tender_author_can_view_tender_1_siae_interested_list(self):
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.context["siaes"]), 3) # detail_contact_click_date
# forbidden
for user in [self.user_buyer_2, self.user_partner, self.siae_user_1, self.siae_user_2]:
self.client.force_login(user)
url = reverse("tenders:detail-siae-list", kwargs={"slug": self.tender_1.slug})
response = self.client.get(url)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, "/besoins/")

def test_viewing_tender_siae_interested_list_should_update_stats(self):
self.assertIsNone(self.tender_1.siae_list_last_seen_date)
Expand Down Expand Up @@ -1051,42 +1052,105 @@ def setUpTestData(cls):
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)
self.assertEqual(response.status_code, 302)
self.assertTrue(response.url.startswith("/accounts/login/"))
self.assertEqual(response.status_code, 403)

def test_only_tender_author_can_call_tender_survey_transactioned(self):
# authorized
self.client.force_login(self.user_buyer_1)
url = reverse("tenders:detail-survey-transactioned", kwargs={"slug": self.tender.slug})
response = self.client.get(url)
self.assertEqual(response.status_code, 302) # redirect
def test_only_tender_author_with_sesame_token_can_call_tender_survey_transactioned(self):
# forbidden
for user in [self.siae_user_1, self.siae_user_2, self.user_buyer_2, self.user_partner, self.user_admin]:
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)
url = reverse("tenders:detail-survey-transactioned", kwargs={"slug": self.tender.slug})
response = self.client.get(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
)
response = self.client.get(url)
self.assertEqual(response.status_code, 302)
self.assertRedirects(response, reverse("tenders:detail", kwargs={"slug": self.tender.slug}))
# but the user is not logged in !
url = reverse("dashboard:home")
response = self.client.get(url)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, "/accounts/login/?next=/profil/")

def test_update_tender_stats_on_tender_survey_transactioned(self):
def test_update_tender_stats_on_tender_survey_transactioned_answer_true(self):
user_sesame_query_string = sesame_get_query_string(self.user_buyer_1)
self.assertEqual(self.tender.survey_transactioned_answer, None)
# load without answer
self.client.force_login(self.user_buyer_1)
url = reverse("tenders:detail-survey-transactioned", kwargs={"slug": self.tender.slug})
response = self.client.get(url)
self.assertEqual(response.status_code, 302) # redirect
url = (
reverse("tenders:detail-survey-transactioned", kwargs={"slug": self.tender.slug})
+ user_sesame_query_string
)
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.assertEqual(Tender.objects.get(id=self.tender.id).survey_transactioned_answer, None)
# self.assertNotContains(response, "Merci pour vote réponse")
# load with answer
self.client.force_login(self.user_buyer_1)
url = reverse("tenders:detail-survey-transactioned", kwargs={"slug": self.tender.slug}) + "?answer=True"
response = self.client.get(url)
self.assertEqual(response.status_code, 302) # redirect
url = (
reverse("tenders:detail-survey-transactioned", kwargs={"slug": self.tender.slug})
+ user_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, "Merci pour vote réponse")
self.assertEqual(Tender.objects.get(id=self.tender.id).survey_transactioned_answer, True)
# self.assertContains(response, "Merci pour vote réponse")
# reload with answer, ignore changes
self.client.force_login(self.user_buyer_1)
url = reverse("tenders:detail-survey-transactioned", kwargs={"slug": self.tender.slug}) + "?answer=False"
response = self.client.get(url)
self.assertEqual(response.status_code, 302) # redirect
url = (
reverse("tenders:detail-survey-transactioned", kwargs={"slug": self.tender.slug})
+ user_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.assertEqual(Tender.objects.get(id=self.tender.id).survey_transactioned_answer, True)
# self.assertContains(response, "Votre réponse a déjà été prise en compte")

def test_update_tender_stats_on_tender_survey_transactioned_answer_false(self):
user_sesame_query_string = sesame_get_query_string(self.user_buyer_1)
self.assertEqual(self.tender.survey_transactioned_answer, None)
# load without answer
url = (
reverse("tenders:detail-survey-transactioned", kwargs={"slug": self.tender.slug})
+ user_sesame_query_string
)
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.assertEqual(Tender.objects.get(id=self.tender.id).survey_transactioned_answer, None)
# load with answer
url = (
reverse("tenders:detail-survey-transactioned", kwargs={"slug": self.tender.slug})
+ user_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, "Merci pour vote réponse")
self.assertEqual(Tender.objects.get(id=self.tender.id).survey_transactioned_answer, False)
# reload with answer, ignore changes
url = (
reverse("tenders:detail-survey-transactioned", kwargs={"slug": self.tender.slug})
+ user_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.assertEqual(Tender.objects.get(id=self.tender.id).survey_transactioned_answer, False)
15 changes: 11 additions & 4 deletions lemarche/www/tenders/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@
from lemarche.users.models import User
from lemarche.utils.data import get_choice
from lemarche.utils.mixins import (
SesameTenderAuthorRequiredMixin,
TenderAuthorOrAdminRequiredIfNotValidatedMixin,
TenderAuthorOrAdminRequiredMixin,
TenderAuthorRequiredMixin,
)
from lemarche.www.siaes.forms import SiaeFilterForm
from lemarche.www.tenders.forms import (
Expand Down Expand Up @@ -430,20 +430,27 @@ def get_context_data(self, **kwargs):
return context


class TenderDetailSurveyTransactionedView(TenderAuthorRequiredMixin, UpdateView):
class TenderDetailSurveyTransactionedView(SesameTenderAuthorRequiredMixin, UpdateView):
"""
Endpoint to store the tender author J+30 survey answer
"""

model = Tender

def get(self, request, *args, **kwargs):
""" """
"""
Tender.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:
if survey_transactioned_answer in ["True", "False"]:
# transform survey_transactioned_answer into bool
survey_transactioned_answer = survey_transactioned_answer == "True"
# update survey_transactioned_answer
Tender.objects.filter(id=self.object.id).update(
survey_transactioned_answer=survey_transactioned_answer,
Expand Down
Loading

0 comments on commit 185f62d

Please sign in to comment.