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

[Aperçu du besoin] Ajout du bouton "Répondre en co-traitance" #914

Merged
merged 3 commits into from
Sep 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion lemarche/templates/layouts/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
{% block extra_head %}{% endblock %}
</head>

<body id="top" class="{% block body_class %}{% endblock %}">
<body id="top" class="{% block body_class %}{% endblock %}" hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
<!-- allow a user to go to the main content of the page -->
<noscript>
<div class="alert alert-danger" role="status">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<div class="alert alert-success mt-3 mt-lg-0" role="alert">
<p class="mb-0 fs-sm">
Nous avons bien pris en compte votre demande de mise en relation.
Notre équipe revient vers vous dans les plus brefs délais.
</p>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<div class="alert alert-danger mt-3 mt-lg-0" role="alert">
<p class="mb-0 fs-sm">
N'ayant pu identifier votre structure, nous n'avons pas pu prendre en compte votre demande de mise en relation.
</p>
</div>
17 changes: 17 additions & 0 deletions lemarche/templates/tenders/_detail_cta_cocontracting.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<div id="detail_cocontracting_container" class="mt-3">
<div class="card c-card rounded-lg shadow-lg">
<div class="card-body">
<h3>Répondre en co-traitance ?</h3>
<p>Notre équipe vous met en relation avec d’autres structures désireuses de répondre en co-traitance.</p>
{% if user.is_authenticated %}
<button type="button" class="btn btn-primary btn-block" title="Être mis en relation" hx-post="{% url 'tenders:detail-cocontracting-click' tender.slug %}" hx-target="#detail_cocontracting_container">
Être mis en relation
</button>
{% elif siae_id %}
<button type="button" class="btn btn-primary btn-block" title="Être mis en relation" hx-post="{% url 'tenders:detail-cocontracting-click' tender.slug %}?siae_id={{siae_id}}" hx-target="#detail_cocontracting_container">
Être mis en relation
</button>
{% endif %}
</div>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
La structure {{ siae_name|safe }} souhaite répondre en co-traitance

Titre : {{ tender_title|safe }}
Type : {{ tender_kind|safe }}
Contact email de l'ESI: {{ siae_contact_email|safe }}
SIRET : {{ siae_siret|safe }}

Lien dans l'admin : {{ tender_admin_url }}
2 changes: 2 additions & 0 deletions lemarche/templates/tenders/detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -148,11 +148,13 @@
</div>
{% elif not user_siae_has_detail_contact_click_date %}
{% include "tenders/_detail_cta.html" with tender=tender user_can_click=True %}
{% include "tenders/_detail_cta_cocontracting.html" with tender=tender %}
{% endif %}
{% endif %}
{% elif siae_id %}
{% if not siae_has_detail_contact_click_date %}
{% include "tenders/_detail_cta.html" with tender=tender user_can_click=True siae_id=siae_id %}
{% include "tenders/_detail_cta_cocontracting.html" with tender=tender siae_id=siae_id %}
{% else %}
{% include "tenders/_detail_contact.html" with tender=tender %}
{% endif %}
Expand Down
24 changes: 24 additions & 0 deletions lemarche/www/tenders/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -533,3 +533,27 @@ def send_tenders_author_30_days(tender: Tender, kind="feedback"):
}
tender.logs.append(log_item)
tender.save()


def notify_admin_siae_wants_cocontracting(tender: Tender, siae: Siae):
email_subject = f"Marché de l'inclusion : la structure {siae.name} souhaite répondre en co-traitance"
tender_admin_url = get_admin_url_object(tender)
email_body = render_to_string(
"tenders/cocontracting_notification_email_admin_body.txt",
{
"tender_title": tender.title,
"tender_kind": tender.get_kind_display(),
"tender_admin_url": tender_admin_url,
"siae_name": siae.name,
"siae_contact_email": siae.contact_email,
"siae_siret": siae.siret,
},
)
send_mail_async(
email_subject=email_subject,
email_body=email_body,
recipient_list=[settings.NOTIFY_EMAIL],
)

if settings.BITOUBI_ENV == "prod":
api_slack.send_message_to_channel(text=email_body, service_id=settings.SLACK_WEBHOOK_C4_SUPPORT_CHANNEL)
98 changes: 97 additions & 1 deletion lemarche/www/tenders/tests.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import json
from datetime import timedelta
from unittest import mock

from django.conf import settings
from django.contrib.gis.geos import Point
Expand Down Expand Up @@ -849,8 +850,44 @@ def test_tender_contact_details_display(self):
self.assertNotContains(response, "Voir l'appel d'offres")
self.assertContains(response, "Lien partagé")

def test_tender_cocontracting_display(self):
# anonymous
url = reverse("tenders:detail", kwargs={"slug": self.tender_1.slug})
response = self.client.get(url)
self.assertNotContains(response, "Répondre en co-traitance ?")
# anonymous but with siae_id in url
url = reverse("tenders:detail", kwargs={"slug": self.tender_1.slug})
response = self.client.get(f"{url}?siae_id=15")
self.assertContains(response, "Répondre en co-traitance ?")
# siae user interested but has a detail_contact_click_date
self.client.force_login(self.siae_user_1)
url = reverse("tenders:detail", kwargs={"slug": self.tender_1.slug})
response = self.client.get(url)
self.assertNotContains(response, "Répondre en co-traitance ?")
# siae user not concerned
self.client.force_login(self.siae_user_2)
url = reverse("tenders:detail", kwargs={"slug": self.tender_1.slug})
response = self.client.get(url)
self.assertContains(response, "Répondre en co-traitance ?")
# siae user interested
TenderSiae.objects.create(tender=self.tender_1, siae=self.siae_2, email_send_date=timezone.now())
self.client.force_login(self.siae_user_2)
url = reverse("tenders:detail", kwargs={"slug": self.tender_1.slug})
response = self.client.get(url)
self.assertContains(response, "Répondre en co-traitance ?")
# siae user without siae
self.client.force_login(self.siae_user_3)
url = reverse("tenders:detail", kwargs={"slug": self.tender_1.slug})
response = self.client.get(url)
self.assertNotContains(response, "Répondre en co-traitance ?")
# author
self.client.force_login(self.user_buyer_1)
url = reverse("tenders:detail", kwargs={"slug": self.tender_1.slug})
response = self.client.get(url)
self.assertNotContains(response, "Répondre en co-traitance ?")

class TenderDetailContactClickStatViewViewTest(TestCase):

class TenderDetailContactClickStatViewTest(TestCase):
@classmethod
def setUpTestData(cls):
cls.siae = SiaeFactory(name="ZZ ESI")
Expand Down Expand Up @@ -962,6 +999,65 @@ def test_update_tendersiae_stats_on_tender_contact_click_with_siae_id_param(self
)


class TenderDetailCocontractingClickView(TestCase):
@classmethod
def setUpTestData(self):
self.siae = SiaeFactory(name="ZZ ESI")
self.siae_user = UserFactory(kind=User.KIND_SIAE, siaes=[self.siae])
self.user_buyer = UserFactory(kind=User.KIND_BUYER, company_name="Entreprise Buyer")
self.tender = TenderFactory(
kind=tender_constants.KIND_TENDER,
author=self.user_buyer,
amount=tender_constants.AMOUNT_RANGE_100_150,
accept_share_amount=True,
response_kind=[Tender.RESPONSE_KIND_EMAIL],
)
self.tendersiae = TenderSiae.objects.create(
tender=self.tender,
siae=self.siae,
source="EMAIL",
email_send_date=timezone.now(),
email_link_click_date=timezone.now(),
detail_display_date=timezone.now(),
detail_contact_click_date=timezone.now(),
)
TenderQuestionFactory(tender=self.tender)

def test_user_can_notify_cocontracting_wish(self):
url = reverse("tenders:detail-cocontracting-click", kwargs={"slug": self.tender.slug})
with mock.patch("lemarche.www.tenders.tasks.send_mail_async") as mock_send_mail_async:
response = self.client.post(url, data={})
self.assertEqual(response.status_code, 403)
mock_send_mail_async.assert_not_called()

with mock.patch("lemarche.www.tenders.tasks.send_mail_async") as mock_send_mail_async:
response = self.client.post(f"{url}?siae_id=999999", data={})
self.assertContains(response, "nous n'avons pas pu prendre en compte votre demande de mise en relation")
mock_send_mail_async.assert_not_called()

with mock.patch("lemarche.www.tenders.tasks.send_mail_async") as mock_send_mail_async:
response = self.client.post(f"{url}?siae_id={self.siae.id}", data={})
self.assertContains(response, "Nous avons bien pris en compte votre demande de mise en relation")
mock_send_mail_async.assert_called_once()
email_body = mock_send_mail_async.call_args[1]["email_body"]
self.assertTrue(f"La structure {self.siae.name } souhaite répondre en co-traitance" in email_body)

self.client.force_login(self.siae_user)
with mock.patch("lemarche.www.tenders.tasks.send_mail_async") as mock_send_mail_async:
response = self.client.post(url, data={})
self.assertContains(response, "Nous avons bien pris en compte votre demande de mise en relation")
mock_send_mail_async.assert_called_once()
email_body = mock_send_mail_async.call_args[1]["email_body"]
self.assertTrue(f"La structure {self.siae.name } souhaite répondre en co-traitance" in email_body)

user_without_siae = UserFactory(kind=User.KIND_SIAE)
self.client.force_login(user_without_siae)
with mock.patch("lemarche.www.tenders.tasks.send_mail_async") as mock_send_mail_async:
response = self.client.post(url, data={})
self.assertContains(response, "nous n'avons pas pu prendre en compte votre demande de mise en relation")
mock_send_mail_async.assert_not_called()


# TODO: this test doesn't work anymore. find a way to test logging post-email in non-prod environments?
# class TenderTasksTest(TestCase):
# @classmethod
Expand Down
6 changes: 6 additions & 0 deletions lemarche/www/tenders/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from lemarche.www.tenders.views import (
TenderCreateMultiStepView,
TenderDetailCocontractingClickView,
TenderDetailContactClickStatView,
TenderDetailSurveyTransactionedView,
TenderDetailView,
Expand Down Expand Up @@ -32,6 +33,11 @@
path(
"<str:slug>/contact-click-stat", TenderDetailContactClickStatView.as_view(), name="detail-contact-click-stat"
),
path(
"<str:slug>/cocontracting-click",
TenderDetailCocontractingClickView.as_view(),
name="detail-cocontracting-click",
),
path(
"<str:slug>/sondage-transaction",
TenderDetailSurveyTransactionedView.as_view(),
Expand Down
29 changes: 29 additions & 0 deletions lemarche/www/tenders/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
TenderCreateStepSurveyForm,
)
from lemarche.www.tenders.tasks import ( # , send_tender_emails_to_siaes
notify_admin_siae_wants_cocontracting,
notify_admin_tender_created,
send_siae_interested_email_to_author,
)
Expand Down Expand Up @@ -403,6 +404,34 @@ def get_success_message(self, detail_contact_click_confirm):
return f"<strong>{self.object.cta_card_button_text}</strong><br />Pour {self.object.cta_card_button_text.lower()}, vous devez accepter d'être mis en relation avec l'acheteur." # noqa


class TenderDetailCocontractingClickView(SiaeUserRequiredOrSiaeIdParamMixin, DetailView):
"""
Endpoint to handle cocontracting button click
"""

template_name = "tenders/_detail_cocontracting_click_confirm.html"
model = Tender

def get_object(self):
return get_object_or_404(Tender, slug=self.kwargs.get("slug"))

def post(self, request, *args, **kwargs):
self.object = self.get_object()
user = self.request.user

if self.request.user.is_authenticated:
siae = user.siaes.first()
SebastienReuiller marked this conversation as resolved.
Show resolved Hide resolved
else:
siae = Siae.objects.filter(pk=self.request.GET.get("siae_id", None)).first()

if siae:
notify_admin_siae_wants_cocontracting(self.object, siae)
else:
self.template_name = "tenders/_detail_cocontracting_click_error.html"

return self.get(request)


class TenderSiaeListView(TenderAuthorOrAdminRequiredMixin, FormMixin, ListView):
template_name = "tenders/siae_interested_list.html"
form_class = SiaeFilterForm
Expand Down