Skip to content

Commit fbb3551

Browse files
authored
Merge pull request #1501 from MTES-MCT/feature/antispam-forms
Améliorer antispam des formulaires
2 parents 753dfd8 + 7e3454c commit fbb3551

File tree

16 files changed

+125
-18
lines changed

16 files changed

+125
-18
lines changed

frontend/scripts/components/pages/Synthese/components/TerritoryIdentityCard.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ export const TerritoryIdentityCard = ({ landData, className }: TerritoryIdentity
123123

124124
const population = populationData?.[0]?.population || null;
125125
const hasCompetenceUrba = landData.competence_planification;
126-
console.log(landData.competence_planification)
126+
127127
const identityItems = [
128128
{
129129
icon: "bi bi-geo-alt-fill",

frontend/scripts/components/ui/Feedback.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import Lottie, { LottieRefCurrentProps } from 'lottie-react';
44
import { theme } from '@theme';
55
import Button from '@components/ui/Button';
66
import BaseCard from '@components/ui/BaseCard';
7-
import { useSubmitFeedbackMutation } from '@services/api';
7+
import { useSubmitFeedbackMutation, useGetAntispamTokenQuery } from '@services/api';
88

99
import animation from '@animations/cup.json';
1010

@@ -200,6 +200,7 @@ const Feedback: React.FC<FeedbackProps> = ({ context }) => {
200200
const [animatedStar, setAnimatedStar] = useState<number | null>(null);
201201
const [submitted, setSubmitted] = useState(false);
202202
const [submitFeedback, { isLoading }] = useSubmitFeedbackMutation();
203+
const { data: tokenData, refetch: refetchToken } = useGetAntispamTokenQuery();
203204
const lottieRef = useRef<LottieRefCurrentProps>(null);
204205

205206
useEffect(() => {
@@ -219,7 +220,7 @@ const Feedback: React.FC<FeedbackProps> = ({ context }) => {
219220
};
220221

221222
const handleSubmit = async () => {
222-
if (rating === 0 || isLoading) return;
223+
if (rating === 0 || isLoading || !tokenData?.token) return;
223224

224225
try {
225226
const payload = {
@@ -231,15 +232,17 @@ const Feedback: React.FC<FeedbackProps> = ({ context }) => {
231232
land_name: context?.landName ?? '',
232233
page_name: context?.pageName ?? '',
233234
crisp_session_id: getCrispSessionId(),
235+
_token: tokenData.token,
234236
};
235237

236-
await submitFeedback(payload as any).unwrap();
238+
await submitFeedback(payload).unwrap();
237239

238240
lottieRef.current?.goToAndPlay(0);
239241
setSubmitted(true);
240242
} catch {
241243
setSubmitted(true);
242244
}
245+
refetchToken();
243246
};
244247

245248
const displayRating = hoveredRating || rating;

frontend/scripts/components/ui/SearchBar.tsx

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useEffect, ChangeEvent, useState, useRef } from 'react';
1+
import React, { useEffect, ChangeEvent, useState, useRef, useMemo } from 'react';
22
import styled from 'styled-components';
33
import { useSearchTerritoryQuery } from '@services/api';
44

@@ -144,11 +144,16 @@ const SearchBar: React.FC<SearchBarProps> = ({
144144
skip: shouldQueryBeSkipped,
145145
});
146146

147+
const stableExcludeTerritories = useMemo(() => excludeTerritories,
148+
// eslint-disable-next-line react-hooks/exhaustive-deps
149+
[JSON.stringify(excludeTerritories)]
150+
);
151+
147152
const shouldExcludeTerritoryFromResults = (territory: LandDetailResultType): boolean => {
148-
if (excludeTerritories.length === 0) {
153+
if (stableExcludeTerritories.length === 0) {
149154
return false;
150155
}
151-
return excludeTerritories.some(
156+
return stableExcludeTerritories.some(
152157
(excludedTerritory: LandDetailResultType) =>
153158
territory.land_id === excludedTerritory.land_id &&
154159
territory.land_type === excludedTerritory.land_type
@@ -159,14 +164,12 @@ const SearchBar: React.FC<SearchBarProps> = ({
159164
if (shouldQueryBeSkipped || isFetching) {
160165
setData(undefined);
161166
} else {
162-
// Filter out excluded territories
163-
let filteredData = queryData;
164-
if (queryData) {
165-
filteredData = queryData.filter((territory: LandDetailResultType) => !shouldExcludeTerritoryFromResults(territory));
166-
}
167+
const filteredData = queryData
168+
? queryData.filter((territory: LandDetailResultType) => !shouldExcludeTerritoryFromResults(territory))
169+
: undefined;
167170
setData(filteredData);
168171
}
169-
}, [isFetching, queryData, query, shouldQueryBeSkipped, excludeTerritories]);
172+
}, [isFetching, queryData, query, shouldQueryBeSkipped, stableExcludeTerritories]);
170173

171174
useEffect(() => {
172175
const handleClickOutside = (event: MouseEvent) => {

frontend/scripts/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,8 @@ import '../styles/index.css'
66
// Import dsfr
77
import '@gouvfr/dsfr/dist/dsfr.module.min.js'
88

9+
// HTMX
10+
import 'htmx.org'
11+
912
// React
1013
import './react-roots.tsx'

frontend/scripts/services/api.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,9 @@ export const djangoApi = createApi({
334334
getCurrentUser: builder.query<CurrentUserResponse, void>({
335335
query: () => '/api/me/',
336336
}),
337+
getAntispamToken: builder.query<{ token: string }, void>({
338+
query: () => '/api/token/',
339+
}),
337340
submitFeedback: builder.mutation<void, {
338341
rating: number;
339342
comment: string;
@@ -343,6 +346,7 @@ export const djangoApi = createApi({
343346
land_name: string;
344347
page_name: string;
345348
crisp_session_id?: string;
349+
_token: string;
346350
}>({
347351
query: (body) => {
348352
const csrfToken = getCsrfToken();
@@ -383,6 +387,7 @@ const useStartExportPdfMutation = djangoApi.useStartExportPdfMutation;
383387
const useLazyGetExportStatusQuery = djangoApi.useLazyGetExportStatusQuery;
384388

385389
const useSubmitFeedbackMutation = djangoApi.useSubmitFeedbackMutation;
390+
const useGetAntispamTokenQuery = djangoApi.useGetAntispamTokenQuery;
386391

387392
const {
388393
useGetDepartementListQuery,
@@ -448,4 +453,5 @@ export {
448453
useDeleteReportDraftMutation,
449454
useGetCurrentUserQuery,
450455
useSubmitFeedbackMutation,
456+
useGetAntispamTokenQuery,
451457
};

home/api.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from crisp.services import send_feedback_to_crisp
1010
from home.models import PageFeedback
1111
from public_data.models.administration import AdminRef, LandModel
12+
from utils.antispam import AntispamSerializerMixin
1213
from utils.mattermost import Mattermost
1314

1415
logger = logging.getLogger(__name__)
@@ -18,7 +19,7 @@ class FeedbackThrottle(AnonRateThrottle):
1819
rate = "5/minute"
1920

2021

21-
class PageFeedbackSerializer(serializers.ModelSerializer):
22+
class PageFeedbackSerializer(AntispamSerializerMixin, serializers.ModelSerializer):
2223
crisp_session_id = serializers.CharField(required=False, allow_blank=True, write_only=True)
2324

2425
class Meta:
@@ -46,6 +47,7 @@ def validate_page_url(self, value):
4647
return value
4748

4849
def validate(self, data):
50+
data = super().validate(data)
4951
land_type = data.get("land_type")
5052
land_id = data.get("land_id")
5153

home/forms.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
from django import forms
22
from django.templatetags.static import static
33

4+
from utils.antispam import HoneypotFormMixin
5+
46
from .models import Newsletter, SatisfactionFormEntry
57

68

7-
class NewsletterForm(forms.ModelForm):
9+
class NewsletterForm(HoneypotFormMixin, forms.ModelForm):
810
class Meta:
911
model = Newsletter
1012
fields = ("email",)
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
<div class="fade-in fr-alert fr-alert--success fr-alert--sm" role="alert">
22
<h3 class="fr-alert__title">Votre inscription a été prise en compte.</h3>
3-
<p>Vous allez recevoir un e-mail vous demandant de confirmer votre souhait.</p>
3+
<p>Vous allez recevoir un e-mail vous demandant de confirmer votre inscription.</p>
44
</div>

home/templates/home/partials/newsletter_form.html

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@ <h2 class="fr-h5">Abonnez-vous à notre lettre d'information</h2>
88
<p class="fr-text--sm">Ne ratez rien de notre actualité.</p>
99
</div>
1010
<div>
11-
<form hx-post="{% url 'home:nwl-confirmation' %}">
11+
<form hx-post="{% url 'home:nwl-confirmation' %}" hx-target="this" hx-swap="outerHTML">
1212
{% csrf_token %}
13+
<div style="position:absolute;left:-9999px" aria-hidden="true">
14+
<input type="text" name="website" autocomplete="off" tabindex="-1">
15+
</div>
1316
<div class="fr-input-group">
1417
<label class="fr-label" for="newsletter-email">
1518
Votre adresse électronique (ex. : nom@domaine.fr)

project/api_urls.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@
186186
NearestTerritoriesViewset,
187187
)
188188
from public_data.models.urbanisme import LogementVacantAutorisationStatsViewset
189+
from utils.antispam import generate_token
189190

190191
app_name = "api"
191192

@@ -194,6 +195,11 @@
194195
router.register(r"feedback", PageFeedbackViewSet, basename="feedback")
195196

196197

198+
@api_view(["GET"])
199+
def antispam_token_view(request):
200+
return Response({"token": generate_token()})
201+
202+
197203
@api_view(["GET"])
198204
def me_view(request):
199205
"""Retourne les informations de l'utilisateur connecté."""
@@ -396,6 +402,7 @@ def chart_view(request, id, land_type, land_id):
396402
urlpatterns = [
397403
path("", include(router.urls)),
398404
path("me/", me_view, name="me"),
405+
path("token/", antispam_token_view, name="antispam-token"),
399406
path(
400407
"preference/<str:land_type>/<str:land_id>/",
401408
UserLandPreferenceAPIView.as_view(),

0 commit comments

Comments
 (0)