• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

zestedesavoir / zds-site / 21301029245

23 Jan 2026 09:00PM UTC coverage: 89.343% (+0.003%) from 89.34%
21301029245

Pull #6775

github

web-flow
Merge bf089877e into a92dcf22a
Pull Request #6775: Feat/6633 test que le html qu'on génère est toujours valide

3092 of 4146 branches covered (74.58%)

93 of 103 new or added lines in 16 files covered. (90.29%)

3 existing lines in 2 files now uncovered.

17128 of 19171 relevant lines covered (89.34%)

1.91 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.88
/zds/tutorialv2/forms.py
1
from crispy_forms.bootstrap import StrictButton
3✔
2
from crispy_forms.helper import FormHelper
3✔
3
from crispy_forms.layout import HTML, ButtonHolder, Field, Hidden, Layout, Submit
3✔
4
from django import forms
3✔
5
from django.conf import settings
3✔
6
from django.core.validators import MinLengthValidator
3✔
7
from django.urls import reverse
3✔
8
from django.utils.translation import gettext_lazy as _
3✔
9

10
from zds.tutorialv2.models import TYPE_CHOICES
3✔
11
from zds.tutorialv2.models.database import PublishableContent
3✔
12
from zds.tutorialv2.utils import get_content_version_url
3✔
13
from zds.utils.forms import CommonLayoutEditor, CommonLayoutVersionEditor, IncludeEasyMDE
3✔
14
from zds.utils.models import SubCategory
3✔
15
from zds.utils.validators import InvalidSlugError, slugify_raise_on_invalid
3✔
16

17

18
class FormWithTitle(forms.Form):
3✔
19
    title = forms.CharField(
3✔
20
        label=_("Titre"), max_length=PublishableContent._meta.get_field("title").max_length, required=False
21
    )
22

23
    error_messages = {"bad_slug": _("Le titre « {} » n'est pas autorisé, car son slug est invalide !")}
3✔
24

25
    def clean(self):
3✔
26
        cleaned_data = super().clean()
1✔
27

28
        title = cleaned_data.get("title")
1✔
29

30
        if title is None or not title.strip():
1✔
31
            title = "Titre par défaut"
1✔
32
            cleaned_data["title"] = title
1✔
33

34
        try:
1✔
35
            slugify_raise_on_invalid(title)
1✔
36
        except InvalidSlugError:
1✔
37
            self._errors["title"] = self.error_class([self.error_messages["bad_slug"].format(title)])
1✔
38

39
        return cleaned_data
1✔
40

41

42
class ReviewerTypeModelChoiceField(forms.ModelChoiceField):
3✔
43
    def label_from_instance(self, obj):
3✔
44
        return obj.title
1✔
45

46

47
class ContainerForm(FormWithTitle):
3✔
48
    introduction = forms.CharField(
3✔
49
        label=_("Introduction"),
50
        required=False,
51
        widget=forms.Textarea(
52
            attrs={"placeholder": _("Votre introduction, au format Markdown."), "class": "md-editor preview-source"}
53
        ),
54
    )
55

56
    conclusion = forms.CharField(
3✔
57
        label=_("Conclusion"),
58
        required=False,
59
        widget=forms.Textarea(
60
            attrs={
61
                "placeholder": _("Votre conclusion, au format Markdown."),
62
            }
63
        ),
64
    )
65

66
    msg_commit = forms.CharField(
3✔
67
        label=_("Message de suivi"),
68
        max_length=400,
69
        required=False,
70
        widget=forms.TextInput(attrs={"placeholder": _("Un résumé de vos ajouts et modifications.")}),
71
    )
72

73
    last_hash = forms.CharField(widget=forms.HiddenInput, required=False)
3✔
74

75
    def __init__(self, *args, **kwargs):
3✔
76
        super().__init__(*args, auto_id=kwargs.pop("auto_id", "edit_content_id_%s"), **kwargs)
1✔
77
        self.helper = FormHelper()
1✔
78
        self.helper.form_class = "content-wrapper"
1✔
79
        self.helper.form_method = "post"
1✔
80

81
        self.helper.layout = Layout(
1✔
82
            IncludeEasyMDE(),
83
            Field("title"),
84
            Field("introduction", css_class="md-editor preview-source"),
85
            ButtonHolder(
86
                StrictButton(_("Aperçu"), type="submit", name="preview", css_class="btn btn-grey preview-btn"),
87
            ),
88
            HTML(
89
                '{% if form.introduction.value %}{% include "misc/preview.part.html" \
90
            with text=form.introduction.value %}{% endif %}'
91
            ),
92
            Field("conclusion", css_class="md-editor preview-source"),
93
            ButtonHolder(
94
                StrictButton(_("Aperçu"), type="submit", name="preview", css_class="btn btn-grey preview-btn"),
95
            ),
96
            HTML(
97
                '{% if form.conclusion.value %}{% include "misc/preview.part.html" \
98
            with text=form.conclusion.value %}{% endif %}'
99
            ),
100
            Field("msg_commit"),
101
            Field("last_hash"),
102
            ButtonHolder(
103
                StrictButton(_("Valider"), type="submit"),
104
            ),
105
        )
106

107

108
class ContentForm(ContainerForm):
3✔
109
    type = forms.ChoiceField(choices=TYPE_CHOICES, required=True)
3✔
110

111
    def _create_layout(self):
3✔
112
        self.helper.layout = Layout(
1✔
113
            IncludeEasyMDE(),
114
            Field("title"),
115
            Field("type"),
116
            StrictButton("Valider", type="submit"),
117
        )
118

119
    def __init__(self, *args, **kwargs):
3✔
120
        super().__init__(*args, auto_id=kwargs.pop("auto_id", "create_content_id_%s"), **kwargs)
1✔
121

122
        self.helper = FormHelper()
1✔
123
        self.helper.form_class = "content-wrapper"
1✔
124
        self.helper.form_method = "post"
1✔
125
        self._create_layout()
1✔
126

127
        if "type" in self.initial:
1!
128
            self.helper["type"].wrap(Field, disabled=True)
×
129

130

131
class ExtractForm(FormWithTitle):
3✔
132
    text = forms.CharField(
3✔
133
        label=_("Texte"),
134
        required=False,
135
        widget=forms.Textarea(attrs={"placeholder": _("Votre message, au format Markdown.")}),
136
    )
137

138
    msg_commit = forms.CharField(
3✔
139
        label=_("Message de suivi"),
140
        max_length=400,
141
        required=False,
142
        widget=forms.TextInput(attrs={"placeholder": _("Un résumé de vos ajouts et modifications.")}),
143
    )
144

145
    last_hash = forms.CharField(widget=forms.HiddenInput, required=False)
3✔
146

147
    def __init__(self, *args, **kwargs):
3✔
148
        super().__init__(*args, auto_id=kwargs.pop("auto_id", "extract_form_id_%s"), **kwargs)
1✔
149
        self.helper = FormHelper()
1✔
150
        self.helper.form_class = "content-wrapper"
1✔
151
        self.helper.form_method = "post"
1✔
152
        display_save = bool(self.initial.get("last_hash", False))
1✔
153
        self.helper.layout = Layout(
1✔
154
            Field("title"),
155
            Field("last_hash"),
156
            CommonLayoutVersionEditor(
157
                display_save=display_save, send_label="Sauvegarder et quitter" if display_save else "Envoyer"
158
            ),
159
        )
160

161

162
class ImportForm(forms.Form):
3✔
163
    file = forms.FileField(label=_("Sélectionnez le contenu à importer."), required=True)
3✔
164
    images = forms.FileField(label=_("Fichier zip contenant les images du contenu."), required=False)
3✔
165

166
    def __init__(self, *args, **kwargs):
3✔
167
        self.helper = FormHelper()
×
168
        self.helper.form_class = "content-wrapper"
×
169
        self.helper.form_method = "post"
×
170

171
        self.helper.layout = Layout(
×
172
            Field("file"),
173
            Field("images"),
174
            Submit("import-tuto", _("Importer le .tuto")),
175
        )
NEW
176
        super().__init__(*args, auto_id=kwargs.pop("auto_id", "import_form_id_%s"), **kwargs)
×
177

178
    def clean(self):
3✔
179
        cleaned_data = super().clean()
×
180

181
        # Check that the files extensions are correct
182
        tuto = cleaned_data.get("file")
×
183
        images = cleaned_data.get("images")
×
184

185
        if tuto is not None:
×
186
            ext = tuto.name.split(".")[-1]
×
187
            if ext != "tuto":
×
188
                del cleaned_data["file"]
×
189
                msg = _("Le fichier doit être au format .tuto.")
×
190
                self._errors["file"] = self.error_class([msg])
×
191

192
        if images is not None:
×
193
            ext = images.name.split(".")[-1]
×
194
            if ext != "zip":
×
195
                del cleaned_data["images"]
×
196
                msg = _("Le fichier doit être au format .zip.")
×
197
                self._errors["images"] = self.error_class([msg])
×
198

199

200
class ImportContentForm(forms.Form):
3✔
201
    archive = forms.FileField(label=_("Sélectionnez l'archive de votre contenu."), required=True)
3✔
202
    image_archive = forms.FileField(label=_("Sélectionnez l'archive des images."), required=False)
3✔
203

204
    msg_commit = forms.CharField(
3✔
205
        label=_("Message de suivi"),
206
        max_length=400,
207
        required=False,
208
        widget=forms.TextInput(attrs={"placeholder": _("Un résumé de vos ajouts et modifications.")}),
209
    )
210

211
    def __init__(self, *args, **kwargs):
3✔
212
        super().__init__(*args, auto_id=kwargs.pop("auto_id", "import_content_form_id_%s"), **kwargs)
1✔
213
        self.helper = FormHelper()
1✔
214
        self.helper.form_class = "content-wrapper"
1✔
215
        self.helper.form_method = "post"
1✔
216

217
        self.helper.layout = Layout(
1✔
218
            Field("archive"),
219
            Field("image_archive"),
220
            Field("msg_commit"),
221
            ButtonHolder(
222
                StrictButton("Importer l'archive", type="submit"),
223
            ),
224
        )
225

226
    def clean(self):
3✔
227
        cleaned_data = super().clean()
1✔
228

229
        # Check that the files extensions are correct
230
        archive = cleaned_data.get("archive")
1✔
231

232
        if archive is not None:
1!
233
            ext = archive.name.split(".")[-1]
1✔
234
            if ext != "zip":
1!
235
                del cleaned_data["archive"]
×
236
                msg = _("L'archive doit être au format .zip.")
×
237
                self._errors["archive"] = self.error_class([msg])
×
238

239
        image_archive = cleaned_data.get("image_archive")
1✔
240

241
        if image_archive is not None:
1✔
242
            ext = image_archive.name.split(".")[-1]
1✔
243
            if ext != "zip":
1!
244
                del cleaned_data["image_archive"]
×
245
                msg = _("L'archive doit être au format .zip.")
×
246
                self._errors["image_archive"] = self.error_class([msg])
×
247

248
        return cleaned_data
1✔
249

250

251
class ImportNewContentForm(ImportContentForm):
3✔
252
    subcategory = forms.ModelMultipleChoiceField(
3✔
253
        label=_(
254
            "Sous catégories de votre contenu. Si aucune catégorie ne convient "
255
            "n'hésitez pas à en demander une nouvelle lors de la validation !"
256
        ),
257
        queryset=SubCategory.objects.order_by("title").all(),
258
        required=True,
259
        widget=forms.SelectMultiple(
260
            attrs={
261
                "required": "required",
262
            }
263
        ),
264
    )
265

266
    def __init__(self, *args, **kwargs):
3✔
267
        super().__init__(*args, auto_id=kwargs.pop("auto_id", "import_new_content_id_%s"), **kwargs)
1✔
268

269
        self.helper = FormHelper()
1✔
270
        self.helper.form_class = "content-wrapper"
1✔
271
        self.helper.form_method = "post"
1✔
272

273
        self.helper.layout = Layout(
1✔
274
            Field("archive"),
275
            Field("image_archive"),
276
            Field("subcategory"),
277
            Field("msg_commit"),
278
            ButtonHolder(
279
                StrictButton("Importer l'archive", type="submit"),
280
            ),
281
        )
282

283

284
class BetaForm(forms.Form):
3✔
285
    version = forms.CharField(widget=forms.HiddenInput(attrs={"id": "beta_version"}), required=True)
3✔
286

287
    def __init__(self, *args, **kwargs):
3✔
288
        super().__init__(*args, auto_id=kwargs.pop("auto_id", "put_beta_id_%s"), **kwargs)
1✔
289
        self.helper = FormHelper()
1✔
290
        self.helper.layout = Layout(Field("version"))
1✔
291

292

293
# Notes
294

295

296
class NoteForm(forms.Form):
3✔
297
    text = forms.CharField(
3✔
298
        label="",
299
        widget=forms.Textarea(attrs={"placeholder": _("Votre message, au format Markdown."), "required": "required"}),
300
    )
301
    last_note = forms.IntegerField(label="", widget=forms.HiddenInput(), required=False)
3✔
302

303
    def __init__(self, content, reaction, *args, **kwargs):
3✔
304
        """initialize the form, handle antispam GUI
305
        :param content: the parent content
306
        :type content: zds.tutorialv2.models.database.PublishableContent
307
        :param reaction: the initial reaction if we edit, ``Ǹone```otherwise
308
        :type reaction: zds.tutorialv2.models.database.ContentReaction
309
        :param args:
310
        :param kwargs:
311
        """
312

313
        last_note = kwargs.pop("last_note", 0)
2✔
314

315
        super().__init__(*args, auto_id=kwargs.pop("auto_id", "add_note_id_%s"), **kwargs)
2✔
316
        self.helper = FormHelper()
2✔
317
        self.helper.form_action = reverse("content:add-reaction") + f"?pk={content.pk}"
2✔
318
        self.helper.form_method = "post"
2✔
319

320
        self.helper.layout = Layout(
2✔
321
            CommonLayoutEditor(), Field("last_note") if not last_note else Hidden("last_note", last_note)
322
        )
323

324
        if reaction is not None:  # we're editing an existing comment
2✔
325
            self.helper.layout.append(HTML("{% include 'misc/hat_choice.html' with edited_message=reaction %}"))
2✔
326
        else:
327
            self.helper.layout.append(HTML("{% include 'misc/hat_choice.html' %}"))
2✔
328

329
        if content.antispam():
2✔
330
            if not reaction:
2✔
331
                self.helper["text"].wrap(
1✔
332
                    Field,
333
                    placeholder=_(
334
                        "Vous avez posté il n'y a pas longtemps. Merci de patienter "
335
                        "au moins 15 minutes entre deux messages consécutifs "
336
                        "afin de limiter le flood."
337
                    ),
338
                    disabled=True,
339
                )
340
        elif content.is_locked:
2!
341
            self.helper["text"].wrap(Field, placeholder=_("Ce contenu est verrouillé."), disabled=True)
×
342

343
        if reaction is not None:
2✔
344
            self.initial.setdefault("text", reaction.text)
2✔
345

346
        self.content = content
2✔
347

348
    def clean(self):
3✔
349
        cleaned_data = super().clean()
2✔
350

351
        text = cleaned_data.get("text")
2✔
352

353
        if text is None or not text.strip():
2!
354
            self._errors["text"] = self.error_class([_("Vous devez écrire une réponse !")])
×
355
            if "text" in cleaned_data:
×
356
                del cleaned_data["text"]
×
357

358
        elif len(text) > settings.ZDS_APP["forum"]["max_post_length"]:
2!
359
            self._errors["text"] = self.error_class(
×
360
                [
361
                    _("Ce message est trop long, il ne doit pas dépasser {0} " "caractères.").format(
362
                        settings.ZDS_APP["forum"]["max_post_length"]
363
                    )
364
                ]
365
            )
366
        last_note = cleaned_data.get("last_note", "0")
2✔
367
        if last_note is None:
2✔
368
            last_note = "0"
2✔
369
        is_valid = last_note == "0" or self.content.last_note is None or int(last_note) == self.content.last_note.pk
2✔
370
        if not is_valid:
2✔
371
            self._errors["last_note"] = self.error_class([_("Quelqu'un a posté pendant que vous répondiez")])
1✔
372
        return cleaned_data
2✔
373

374

375
class NoteEditForm(NoteForm):
3✔
376
    def __init__(self, *args, **kwargs):
3✔
377
        super().__init__(*args, auto_id=kwargs.pop("auto_id", "edit_note_id_%s"), **kwargs)
2✔
378

379
        content = kwargs["content"]
2✔
380
        reaction = kwargs["reaction"]
2✔
381

382
        self.helper.form_action = reverse("content:update-reaction") + "?message={}&pk={}".format(
2✔
383
            reaction.pk, content.pk
384
        )
385

386

387
# Validations.
388

389

390
class AskValidationForm(forms.Form):
3✔
391
    text = forms.CharField(
3✔
392
        label="",
393
        required=False,
394
        widget=forms.Textarea(
395
            attrs={"placeholder": _("Commentaire pour votre demande."), "rows": "3", "id": "ask_validation_text"}
396
        ),
397
    )
398

399
    version = forms.CharField(widget=forms.HiddenInput(attrs={"id": "ask_validation_version"}), required=True)
3✔
400

401
    previous_page_url = ""
3✔
402

403
    def __init__(self, content, *args, **kwargs):
3✔
404
        """
405

406
        :param content: the parent content
407
        :type content: zds.tutorialv2.models.database.PublishableContent
408
        :param args:
409
        :param kwargs:
410
        :return:
411
        """
412
        super().__init__(*args, auto_id=kwargs.pop("auto_id", "ask_validation_id_%s"), **kwargs)
3✔
413

414
        # modal form, send back to previous page:
415
        self.previous_page_url = get_content_version_url(content, content.current_version)
3✔
416

417
        self.helper = FormHelper()
3✔
418
        self.helper.form_action = reverse("validation:ask", kwargs={"pk": content.pk, "slug": content.slug})
3✔
419
        self.helper.form_method = "post"
3✔
420
        self.helper.form_class = "modal modal-flex"
3✔
421
        self.helper.form_id = "ask-validation"
3✔
422

423
        self.no_subcategories = content.subcategory.count() == 0
3✔
424
        no_category_msg = HTML(
3✔
425
            _(
426
                """<p><strong>Votre publication n'est dans aucune catégorie.
427
                                    Vous devez <a href="{}">choisir une catégorie</a>
428
                                    avant de demander la validation.</strong></p>""".format(
429
                    reverse("content:edit-categories", kwargs={"pk": content.pk}),
430
                )
431
            )
432
        )
433

434
        self.no_license = not content.licence
3✔
435
        no_license_msg = HTML(
3✔
436
            _(
437
                """<p><strong>Vous n'avez pas choisi de licence pour votre publication.
438
                                   Vous devez <a href="#edit-license" class="open-modal">choisir une licence</a>
439
                                   avant de demander la validation.</strong></p>"""
440
            )
441
        )
442

443
        self.helper.layout = Layout(
3✔
444
            no_category_msg if self.no_subcategories else None,
445
            no_license_msg if self.no_license else None,
446
            Field("text"),
447
            Field("version"),
448
            StrictButton(_("Confirmer"), type="submit", css_class="btn-submit"),
449
        )
450

451
    def clean(self):
3✔
452
        cleaned_data = super().clean()
1✔
453

454
        base_error_msg = "La validation n'a pas été demandée. "
1✔
455

456
        if self.no_subcategories:
1!
457
            error = [_(base_error_msg + "Vous devez choisir au moins une catégorie pour votre publication.")]
×
458
            self.add_error(field=None, error=error)
×
459

460
        if self.no_license:
1!
461
            error = [_(base_error_msg + "Vous devez choisir une licence pour votre publication.")]
×
462
            self.add_error(field=None, error=error)
×
463

464
        return cleaned_data
1✔
465

466

467
class AcceptValidationForm(forms.Form):
3✔
468
    validation = None
3✔
469

470
    text = forms.CharField(
3✔
471
        label="",
472
        required=True,
473
        error_messages={"required": _("Vous devez fournir un commentaire aux validateurs.")},
474
        widget=forms.Textarea(
475
            attrs={
476
                "placeholder": _("Commentaire de publication."),
477
                "rows": "2",
478
                "minlength": "3",
479
                "id": "accept_validation_text",
480
            }
481
        ),
482
        validators=[MinLengthValidator(3, _("Votre commentaire doit faire au moins 3 caractères."))],
483
    )
484

485
    is_major = forms.BooleanField(label=_("Version majeure ?"), required=False, initial=True)
3✔
486

487
    def __init__(self, validation, *args, **kwargs):
3✔
488
        """
489

490
        :param validation: the linked validation request object
491
        :type validation: zds.tutorialv2.models.database.Validation
492
        :param args:
493
        :param kwargs:
494
        :return:
495
        """
496
        self.previous_page_url = get_content_version_url(validation.content, validation.version)
1✔
497

498
        super().__init__(*args, auto_id=kwargs.pop("auto_id", "accept_validation_id_%s"), **kwargs)
1✔
499

500
        # if content is already published, it's probably a minor change, so do not check `is_major`
501
        self.fields["is_major"].initial = not validation.content.sha_public
1✔
502

503
        self.helper = FormHelper()
1✔
504
        self.helper.form_action = reverse("validation:accept", kwargs={"pk": validation.pk})
1✔
505
        self.helper.form_method = "post"
1✔
506
        self.helper.form_class = "modal modal-flex"
1✔
507
        self.helper.form_id = "valid-publish"
1✔
508

509
        self.helper.layout = Layout(
1✔
510
            Field("text", attrs=dict(id="div_accept_validation_text")),
511
            Field("is_major"),
512
            StrictButton(_("Publier"), type="submit", css_class="btn-submit"),
513
        )
514

515

516
class CancelValidationForm(forms.Form):
3✔
517
    text = forms.CharField(
3✔
518
        label="",
519
        required=True,
520
        widget=forms.Textarea(
521
            attrs={"placeholder": _("Pourquoi annuler la validation ?"), "rows": "4", "id": "cancel_text"}
522
        ),
523
    )
524

525
    def __init__(self, validation, *args, **kwargs):
3✔
526
        super().__init__(*args, auto_id=kwargs.pop("auto_id", "cancel_validation_id_%s"), **kwargs)
1✔
527

528
        self.previous_page_url = get_content_version_url(validation.content, validation.version)
1✔
529

530
        self.helper = FormHelper()
1✔
531
        self.helper.form_action = reverse("validation:cancel", kwargs={"pk": validation.pk})
1✔
532
        self.helper.form_method = "post"
1✔
533
        self.helper.form_class = "modal modal-flex"
1✔
534
        self.helper.form_id = "cancel-validation"
1✔
535

536
        self.helper.layout = Layout(
1✔
537
            HTML("<p>Êtes-vous certain de vouloir annuler la validation de ce contenu ?</p>"),
538
            Field("text"),
539
            ButtonHolder(StrictButton(_("Confirmer"), type="submit", css_class="btn-submit")),
540
        )
541

542
    def clean(self):
3✔
543
        cleaned_data = super().clean()
1✔
544

545
        text = cleaned_data.get("text")
1✔
546

547
        if text is None or not text.strip():
1!
548
            self._errors["text"] = self.error_class([_("Merci de fournir une raison à l'annulation.")])
×
549
            if "text" in cleaned_data:
×
550
                del cleaned_data["text"]
×
551

552
        elif len(text) < 3:
1!
553
            self._errors["text"] = self.error_class([_("Votre commentaire doit faire au moins 3 caractères.")])
×
554
            if "text" in cleaned_data:
×
555
                del cleaned_data["text"]
×
556

557
        return cleaned_data
1✔
558

559

560
class RejectValidationForm(forms.Form):
3✔
561
    text = forms.CharField(
3✔
562
        label="",
563
        required=True,
564
        widget=forms.Textarea(attrs={"placeholder": _("Commentaire de rejet."), "rows": "6", "id": "reject_text"}),
565
    )
566

567
    def __init__(self, validation, *args, **kwargs):
3✔
568
        """
569

570
        :param validation: the linked validation request object
571
        :type validation: zds.tutorialv2.models.database.Validation
572
        :param args:
573
        :param kwargs:
574
        :return:
575
        """
576
        super().__init__(*args, auto_id=kwargs.pop("auto_id", "reject_validation_id_%s"), **kwargs)
1✔
577

578
        self.previous_page_url = get_content_version_url(validation.content, validation.version)
1✔
579

580
        self.helper = FormHelper()
1✔
581
        self.helper.form_action = reverse("validation:reject", kwargs={"pk": validation.pk})
1✔
582
        self.helper.form_method = "post"
1✔
583
        self.helper.form_class = "modal modal-flex"
1✔
584
        self.helper.form_id = "reject"
1✔
585

586
        self.helper.layout = Layout(
1✔
587
            Field("text"), ButtonHolder(StrictButton(_("Rejeter"), type="submit", css_class="btn-submit"))
588
        )
589

590
    def clean(self):
3✔
591
        cleaned_data = super().clean()
1✔
592

593
        text = cleaned_data.get("text")
1✔
594

595
        if text is None or not text.strip():
1✔
596
            self._errors["text"] = self.error_class([_("Merci de fournir une raison au rejet.")])
1✔
597
            if "text" in cleaned_data:
1!
598
                del cleaned_data["text"]
×
599

600
        elif len(text) < 3:
1!
601
            self._errors["text"] = self.error_class([_("Votre commentaire doit faire au moins 3 caractères.")])
×
602
            if "text" in cleaned_data:
×
603
                del cleaned_data["text"]
×
604

605
        return cleaned_data
1✔
606

607

608
class RevokeValidationForm(forms.Form):
3✔
609
    version = forms.CharField(widget=forms.HiddenInput(attrs={"id": "rev_version"}), required=True)
3✔
610

611
    text = forms.CharField(
3✔
612
        label="",
613
        required=True,
614
        widget=forms.Textarea(
615
            attrs={"placeholder": _("Pourquoi dépublier ce contenu ?"), "rows": "6", "id": "up_text"}
616
        ),
617
    )
618

619
    def __init__(self, content, *args, **kwargs):
3✔
620
        super().__init__(*args, auto_id=kwargs.pop("auto_id", "revoke_validation_id_%s"), **kwargs)
3✔
621

622
        # modal form, send back to previous page:
623
        self.previous_page_url = content.get_absolute_url_online()
3✔
624

625
        self.helper = FormHelper()
3✔
626
        self.helper.form_action = reverse("validation:revoke", kwargs={"pk": content.pk, "slug": content.slug})
3✔
627
        self.helper.form_method = "post"
3✔
628
        self.helper.form_class = "modal modal-flex"
3✔
629
        self.helper.form_id = "unpublish"
3✔
630

631
        self.helper.layout = Layout(Field("text"))
3✔
632

633
    def clean(self):
3✔
634
        cleaned_data = super().clean()
1✔
635

636
        text = cleaned_data.get("text")
1✔
637

638
        if text is None or not text.strip():
1✔
639
            self._errors["text"] = self.error_class([_("Veuillez fournir la raison de votre dépublication.")])
1✔
640
            if "text" in cleaned_data:
1!
641
                del cleaned_data["text"]
×
642

643
        elif len(text) < 3:
1!
644
            self._errors["text"] = self.error_class([_("Votre commentaire doit faire au moins 3 caractères.")])
×
645
            if "text" in cleaned_data:
×
646
                del cleaned_data["text"]
×
647

648
        return cleaned_data
1✔
649

650

651
class JsFiddleActivationForm(forms.Form):
3✔
652
    js_support = forms.BooleanField(label="À cocher pour activer JSFiddle.", required=False, initial=True)
3✔
653

654
    def __init__(self, *args, **kwargs):
3✔
655
        super().__init__(*args, auto_id=kwargs.pop("auto_id", "js_activation_id_%s"), **kwargs)
3✔
656
        self.helper = FormHelper()
3✔
657
        self.helper.form_action = reverse("content:activate-jsfiddle")
3✔
658
        self.helper.form_method = "post"
3✔
659
        self.helper.form_class = "modal modal-flex"
3✔
660
        self.helper.form_id = "js-activation"
3✔
661

662
        self.helper.layout = Layout(
3✔
663
            Field("js_support"),
664
            ButtonHolder(
665
                StrictButton(_("Valider"), type="submit", css_class="btn-submit"),
666
            ),
667
            Hidden("pk", "{{ content.pk }}"),
668
        )
669

670
    def clean(self):
3✔
671
        cleaned_data = super().clean()
1✔
672
        if "js_support" not in cleaned_data:
1!
673
            cleaned_data["js_support"] = False
×
674
        if "pk" in self.data and self.data["pk"].isdigit():
1!
675
            cleaned_data["pk"] = int(self.data["pk"])
1✔
676
        else:
677
            cleaned_data["pk"] = 0
×
678
        return cleaned_data
1✔
679

680

681
class MoveElementForm(forms.Form):
3✔
682
    child_slug = forms.HiddenInput()
3✔
683
    container_slug = forms.HiddenInput()
3✔
684
    first_level_slug = forms.HiddenInput()
3✔
685
    moving_method = forms.HiddenInput()
3✔
686

687
    MOVE_UP = "up"
3✔
688
    MOVE_DOWN = "down"
3✔
689
    MOVE_AFTER = "after"
3✔
690
    MOVE_BEFORE = "before"
3✔
691

692
    def __init__(self, *args, **kwargs):
3✔
693
        super().__init__(*args, auto_id=kwargs.pop("auto_id", "move_element_id_%s"), **kwargs)
1✔
694
        self.helper = FormHelper()
1✔
695
        self.helper.form_action = reverse("content:move-element")
1✔
696
        self.helper.form_method = "post"
1✔
697
        self.helper.layout = Layout(
1✔
698
            Field("child_slug"),
699
            Field("container_slug"),
700
            Field("first_level_slug"),
701
            Field("moving_method"),
702
            Hidden("pk", "{{ content.pk }}"),
703
        )
704

705

706
class PublicationForm(forms.Form):
3✔
707
    """
708
    The publication form (used only for content without preliminary validation).
709
    """
710

711
    def __init__(self, content, *args, **kwargs):
3✔
712
        super().__init__(*args, auto_id=kwargs.pop("auto_id", "publication_form_id_%s"), **kwargs)
3✔
713

714
        self.previous_page_url = content.get_absolute_url()
3✔
715

716
        self.helper = FormHelper()
3✔
717
        self.helper.form_action = reverse("validation:publish-opinion", kwargs={"pk": content.pk, "slug": content.slug})
3✔
718
        self.helper.form_method = "post"
3✔
719
        self.helper.form_class = "modal modal-flex"
3✔
720
        self.helper.form_id = "valid-publication"
3✔
721

722
        self.no_subcategories = content.subcategory.count() == 0
3✔
723
        no_category_msg = HTML(
3✔
724
            _(
725
                """<p><strong>Votre publication n'est dans aucune catégorie.
726
                                    Vous devez <a href="{}">choisir une catégorie</a>
727
                                    avant de publier.</strong></p>""".format(
728
                    reverse("content:edit-categories", kwargs={"pk": content.pk})
729
                )
730
            )
731
        )
732

733
        self.no_license = not content.licence
3✔
734
        no_license_msg = HTML(
3✔
735
            _(
736
                """<p><strong>Vous n'avez pas choisi de licence pour votre publication.
737
                                   Vous devez <a href="#edit-license" class="open-modal">choisir une licence</a>
738
                                   avant de publier.</strong></p>"""
739
            )
740
        )
741

742
        self.helper.layout = Layout(
3✔
743
            no_category_msg if self.no_subcategories else None,
744
            no_license_msg if self.no_license else None,
745
            HTML(_("<p>Ce billet sera publié directement et n'engage que vous.</p>")),
746
            StrictButton(_("Publier"), type="submit", css_class="btn-submit"),
747
        )
748

749
    def clean(self):
3✔
750
        cleaned_data = super().clean()
1✔
751

752
        base_error_msg = "La publication n'a pas été effectuée. "
1✔
753

754
        if self.no_subcategories:
1!
755
            error = _(base_error_msg + "Vous devez choisir au moins une catégorie pour votre publication.")
×
756
            self.add_error(field=None, error=error)
×
757

758
        if self.no_license:
1!
759
            error = _(base_error_msg + "Vous devez choisir une licence pour votre publication.")
×
760
            self.add_error(field=None, error=error)
×
761

762
        return cleaned_data
1✔
763

764

765
class UnpublicationForm(forms.Form):
3✔
766
    version = forms.CharField(widget=forms.HiddenInput(attrs={"id": "unpublish_version"}))
3✔
767

768
    text = forms.CharField(
3✔
769
        label="",
770
        required=True,
771
        widget=forms.Textarea(
772
            attrs={"placeholder": _("Pourquoi dépublier ce contenu ?"), "rows": "6", "id": "up_reason"}
773
        ),
774
    )
775

776
    def __init__(self, content, *args, **kwargs):
3✔
777
        super().__init__(*args, auto_id=kwargs.pop("auto_id", "unpublication_form_id_%s"), **kwargs)
3✔
778

779
        # modal form, send back to previous page:
780
        self.previous_page_url = content.get_absolute_url_online()
3✔
781

782
        self.helper = FormHelper()
3✔
783
        self.helper.form_action = reverse(
3✔
784
            "validation:unpublish-opinion", kwargs={"pk": content.pk, "slug": content.slug}
785
        )
786

787
        self.helper.form_method = "post"
3✔
788
        self.helper.form_class = "modal modal-flex"
3✔
789
        self.helper.form_id = "unpublish"
3✔
790

791
        self.helper.layout = Layout(Field("text"))
3✔
792

793

794
class PickOpinionForm(forms.Form):
3✔
795
    version = forms.CharField(widget=forms.HiddenInput(attrs={"id": "pick_version"}))
3✔
796

797
    def __init__(self, content, *args, **kwargs):
3✔
798
        super().__init__(*args, auto_id=kwargs.pop("auto_id", "pick_opinion_id_%s"), **kwargs)
3✔
799

800
        # modal form, send back to previous page:
801
        self.previous_page_url = content.get_absolute_url_online()
3✔
802

803
        self.helper = FormHelper()
3✔
804
        self.helper.form_action = reverse("validation:pick-opinion", kwargs={"pk": content.pk, "slug": content.slug})
3✔
805
        self.helper.form_method = "post"
3✔
806
        self.helper.form_class = "modal modal-flex"
3✔
807
        self.helper.form_id = "pick-opinion"
3✔
808

809
        self.helper.layout = Layout(
3✔
810
            HTML(
811
                "<p>Êtes-vous certain(e) de vouloir valider ce billet ? "
812
                "Il pourra maintenant être présent sur la page d’accueil.</p>"
813
            ),
814
            Field("version"),
815
            StrictButton(_("Valider"), type="submit", css_class="btn-submit"),
816
        )
817

818

819
class DoNotPickOpinionForm(forms.Form):
3✔
820
    operation = forms.CharField(widget=forms.HiddenInput())
3✔
821
    redirect = forms.CharField(widget=forms.HiddenInput(), required=False)
3✔
822

823
    def __init__(self, content, *args, **kwargs):
3✔
824
        super().__init__(*args, auto_id=kwargs.pop("auto_id", "donotpick_opinion_id_%s"), **kwargs)
1✔
825

826
        # modal form, send back to previous page:
827
        self.previous_page_url = content.get_absolute_url_online()
1✔
828

829
        self.helper = FormHelper()
1✔
830
        self.helper.form_action = reverse("validation:unpick-opinion", kwargs={"pk": content.pk, "slug": content.slug})
1✔
831
        self.helper.form_method = "post"
1✔
832
        self.helper.form_class = "modal modal-flex"
1✔
833
        self.helper.form_id = "unpick-opinion"
1✔
834

835
        self.helper.layout = Layout(
1✔
836
            HTML(_("<p>Ce billet n'apparaîtra plus dans la liste des billets à choisir.</p>")),
837
            Field("operation"),
838
            StrictButton(_("Valider"), type="submit", css_class="btn-submit"),
839
        )
840

841
    def clean(self):
3✔
842
        cleaned = super().clean()
1✔
843
        cleaned["operation"] = (
1✔
844
            self.data["operation"] if self.data["operation"] in ["NO_PICK", "REJECT", "REMOVE_PUB"] else None
845
        )
846
        cleaned["redirect"] = self.data["redirect"] == "true" if "redirect" in self.data else False
1✔
847
        return cleaned
1✔
848

849
    def is_valid(self):
3✔
850
        base = super().is_valid()
1✔
851
        if not self["operation"]:
1!
852
            self._errors["operation"] = _("Opération invalide, NO_PICK, REJECT ou REMOVE_PUB attendu.")
×
853
            return False
×
854
        return base
1✔
855

856

857
class UnpickOpinionForm(forms.Form):
3✔
858
    version = forms.CharField(widget=forms.HiddenInput(attrs={"id": "unpick_version"}))
3✔
859

860
    text = forms.CharField(
3✔
861
        label="",
862
        required=True,
863
        widget=forms.Textarea(
864
            attrs={
865
                "placeholder": _("Pourquoi retirer ce billet de la liste des billets choisis ?"),
866
                "rows": "6",
867
                "id": "unpick_text",
868
            }
869
        ),
870
    )
871

872
    def __init__(self, content, *args, **kwargs):
3✔
873
        super().__init__(*args, auto_id=kwargs.pop("auto_id", "unpick_opinion_id_%s"), **kwargs)
3✔
874

875
        # modal form, send back to previous page:
876
        self.previous_page_url = content.get_absolute_url_online()
3✔
877

878
        self.helper = FormHelper()
3✔
879
        self.helper.form_action = reverse("validation:unpick-opinion", kwargs={"pk": content.pk, "slug": content.slug})
3✔
880
        self.helper.form_method = "post"
3✔
881
        self.helper.form_class = "modal modal-flex"
3✔
882
        self.helper.form_id = "unpick-opinion"
3✔
883

884
        self.helper.layout = Layout(
3✔
885
            Field("version"), Field("text"), StrictButton(_("Enlever"), type="submit", css_class="btn-submit")
886
        )
887

888

889
class PromoteOpinionToArticleForm(forms.Form):
3✔
890
    version = forms.CharField(widget=forms.HiddenInput(attrs={"id": "promote_version"}))
3✔
891

892
    def __init__(self, content, *args, **kwargs):
3✔
893
        super().__init__(*args, auto_id=kwargs.pop("auto_id", "promote_opinion_id_%s"), **kwargs)
3✔
894

895
        # modal form, send back to previous page:
896
        self.previous_page_url = content.get_absolute_url_online()
3✔
897

898
        self.helper = FormHelper()
3✔
899
        self.helper.form_action = reverse("validation:promote-opinion", kwargs={"pk": content.pk, "slug": content.slug})
3✔
900
        self.helper.form_method = "post"
3✔
901
        self.helper.form_class = "modal modal-flex"
3✔
902
        self.helper.form_id = "convert-opinion"
3✔
903

904
        self.helper.layout = Layout(
3✔
905
            HTML(
906
                """<p>Avez-vous la certitude de vouloir proposer ce billet comme article ?
907
                    Cela copiera le billet pour en faire un article,
908
                    puis créera une demande de validation pour ce dernier.</p>"""
909
            ),
910
            Field("version"),
911
            StrictButton(_("Valider"), type="submit", css_class="btn-submit"),
912
        )
913

914

915
class ContentCompareStatsURLForm(forms.Form):
3✔
916
    urls = forms.MultipleChoiceField(widget=forms.CheckboxSelectMultiple(), required=True)
3✔
917

918
    def __init__(self, urls, *args, **kwargs):
3✔
919
        super().__init__(*args, auto_id=kwargs.pop("auto_id", "compare_stats_url_form_id_%s"), **kwargs)
1✔
920
        self.fields["urls"].choices = urls
1✔
921

922
        self.helper = FormHelper()
1✔
923
        self.helper.layout = Layout(Field("urls"), StrictButton(_("Comparer"), type="submit"))
1✔
924

925
    def clean(self):
3✔
926
        cleaned_data = super().clean()
1✔
927
        urls = cleaned_data.get("urls")
1✔
928
        if not urls:
1!
929
            raise forms.ValidationError(_("Vous devez choisir des URL a comparer"))
×
930
        if len(urls) < 2:
1!
931
            raise forms.ValidationError(_("Il faut au minimum 2 urls à comparer"))
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc