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

zestedesavoir / zds-site / 8625617051

31 Mar 2024 04:43PM UTC coverage: 88.752% (+0.05%) from 88.699%
8625617051

push

github

web-flow
Ajoute la modification du titre et du sous-titre d'une publication depuis une modale (#6590)

4740 of 5934 branches covered (79.88%)

126 of 126 new or added lines in 5 files covered. (100.0%)

3 existing lines in 2 files now uncovered.

16515 of 18608 relevant lines covered (88.75%)

1.88 hits per line

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

86.24
/zds/tutorialv2/forms.py
1
from django import forms
3✔
2
from django.conf import settings
3✔
3

4
from crispy_forms.bootstrap import StrictButton
3✔
5
from crispy_forms.helper import FormHelper
3✔
6
from crispy_forms.layout import HTML, Layout, Submit, Field, ButtonHolder, Hidden
3✔
7
from django.urls import reverse
3✔
8
from django.core.validators import MinLengthValidator
3✔
9

10
from zds.tutorialv2.utils import get_content_version_url
3✔
11
from zds.utils.forms import CommonLayoutEditor, CommonLayoutVersionEditor
3✔
12
from zds.utils.models import SubCategory
3✔
13
from zds.tutorialv2.models import TYPE_CHOICES
3✔
14
from zds.tutorialv2.models.database import PublishableContent
3✔
15
from django.utils.translation import gettext_lazy as _
3✔
16
from zds.utils.forms import IncludeEasyMDE
3✔
17
from zds.utils.validators import with_svg_validator, slugify_raise_on_invalid, InvalidSlugError
3✔
18

19

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

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

27
    def clean(self):
3✔
28
        cleaned_data = super().clean()
1✔
29

30
        title = cleaned_data.get("title")
1✔
31

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

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

41
        return cleaned_data
1✔
42

43

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

48

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

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

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

75
    last_hash = forms.CharField(widget=forms.HiddenInput, required=False)
3✔
76

77
    def __init__(self, *args, **kwargs):
3✔
78
        super().__init__(*args, **kwargs)
1✔
79
        self.helper = FormHelper()
1✔
80
        self.helper.form_class = "content-wrapper"
1✔
81
        self.helper.form_method = "post"
1✔
82

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

109

110
class ContentForm(ContainerForm):
3✔
111
    description = forms.CharField(
3✔
112
        label=_("Description"),
113
        max_length=PublishableContent._meta.get_field("description").max_length,
114
        required=False,
115
    )
116

117
    image = forms.FileField(
3✔
118
        label=_("Sélectionnez le logo du contenu (max. {} Ko).").format(
119
            str(settings.ZDS_APP["gallery"]["image_max_size"] / 1024)
120
        ),
121
        validators=[with_svg_validator],
122
        required=False,
123
    )
124

125
    type = forms.ChoiceField(choices=TYPE_CHOICES, required=False)
3✔
126

127
    subcategory = forms.ModelMultipleChoiceField(
3✔
128
        label=_("Sélectionnez les catégories qui correspondent à votre contenu."),
129
        queryset=SubCategory.objects.order_by("title").all(),
130
        required=False,
131
        widget=forms.CheckboxSelectMultiple(),
132
    )
133

134
    source = forms.URLField(
3✔
135
        label=_(
136
            """Si votre contenu est publié en dehors de Zeste de Savoir (blog, site personnel, etc.),
137
                       indiquez le lien de la publication originale : """
138
        ),
139
        max_length=PublishableContent._meta.get_field("source").max_length,
140
        required=False,
141
        widget=forms.TextInput(attrs={"placeholder": _("https://...")}),
142
    )
143

144
    def _create_layout(self):
3✔
145
        self.helper.layout = Layout(
1✔
146
            IncludeEasyMDE(),
147
            Field("title"),
148
            Field("description"),
149
            Field("type"),
150
            Field("image"),
151
            Field("introduction", css_class="md-editor preview-source"),
152
            ButtonHolder(
153
                StrictButton(_("Aperçu"), type="preview", name="preview", css_class="btn btn-grey preview-btn"),
154
            ),
155
            HTML(
156
                '{% if form.introduction.value %}{% include "misc/preview.part.html" \
157
            with text=form.introduction.value %}{% endif %}'
158
            ),
159
            Field("conclusion", css_class="md-editor preview-source"),
160
            ButtonHolder(
161
                StrictButton(_("Aperçu"), type="preview", name="preview", css_class="btn btn-grey preview-btn"),
162
            ),
163
            HTML(
164
                '{% if form.conclusion.value %}{% include "misc/preview.part.html" \
165
            with text=form.conclusion.value %}{% endif %}'
166
            ),
167
            Field("last_hash"),
168
            Field("source"),
169
            Field("subcategory", template="crispy/checkboxselectmultiple.html"),
170
        )
171

172
        self.helper.layout.append(Field("msg_commit"))
1✔
173
        self.helper.layout.append(ButtonHolder(StrictButton("Valider", type="submit")))
1✔
174

175
    def __init__(self, *args, **kwargs):
3✔
176
        super().__init__(*args, **kwargs)
1✔
177

178
        self.helper = FormHelper()
1✔
179
        self.helper.form_class = "content-wrapper"
1✔
180
        self.helper.form_method = "post"
1✔
181
        self._create_layout()
1✔
182

183
        if "type" in self.initial:
1!
UNCOV
184
            self.helper["type"].wrap(Field, disabled=True)
×
185

186
    def clean(self):
3✔
187
        cleaned_data = super().clean()
1✔
188
        image = cleaned_data.get("image", None)
1✔
189
        if image is not None and image.size > settings.ZDS_APP["gallery"]["image_max_size"]:
1!
190
            self._errors["image"] = self.error_class(
×
191
                [
192
                    _("Votre logo est trop lourd, la limite autorisée est de {} Ko").format(
193
                        settings.ZDS_APP["gallery"]["image_max_size"] / 1024
194
                    )
195
                ]
196
            )
197
        return cleaned_data
1✔
198

199

200
class EditContentForm(ContentForm):
3✔
201
    title = None
3✔
202
    description = None
3✔
203
    type = None
3✔
204

205
    def _create_layout(self):
3✔
206
        self.helper.layout = Layout(
1✔
207
            IncludeEasyMDE(),
208
            Field("image"),
209
            Field("introduction", css_class="md-editor preview-source"),
210
            StrictButton(_("Aperçu"), type="preview", name="preview", css_class="btn btn-grey preview-btn"),
211
            HTML(
212
                '{% if form.introduction.value %}{% include "misc/preview.part.html" \
213
                with text=form.introduction.value %}{% endif %}'
214
            ),
215
            Field("conclusion", css_class="md-editor preview-source"),
216
            StrictButton(_("Aperçu"), type="preview", name="preview", css_class="btn btn-grey preview-btn"),
217
            HTML(
218
                '{% if form.conclusion.value %}{% include "misc/preview.part.html" \
219
                with text=form.conclusion.value %}{% endif %}'
220
            ),
221
            Field("last_hash"),
222
            Field("source"),
223
            Field("subcategory", template="crispy/checkboxselectmultiple.html"),
224
            Field("msg_commit"),
225
            StrictButton("Valider", type="submit"),
226
        )
227

228

229
class ExtractForm(FormWithTitle):
3✔
230
    text = forms.CharField(
3✔
231
        label=_("Texte"),
232
        required=False,
233
        widget=forms.Textarea(attrs={"placeholder": _("Votre message, au format Markdown.")}),
234
    )
235

236
    msg_commit = forms.CharField(
3✔
237
        label=_("Message de suivi"),
238
        max_length=400,
239
        required=False,
240
        widget=forms.TextInput(attrs={"placeholder": _("Un résumé de vos ajouts et modifications.")}),
241
    )
242

243
    last_hash = forms.CharField(widget=forms.HiddenInput, required=False)
3✔
244

245
    def __init__(self, *args, **kwargs):
3✔
246
        super().__init__(*args, **kwargs)
1✔
247
        self.helper = FormHelper()
1✔
248
        self.helper.form_class = "content-wrapper"
1✔
249
        self.helper.form_method = "post"
1✔
250
        display_save = bool(self.initial.get("last_hash", False))
1✔
251
        self.helper.layout = Layout(
1✔
252
            Field("title"),
253
            Field("last_hash"),
254
            CommonLayoutVersionEditor(
255
                display_save=display_save, send_label="Sauvegarder et quitter" if display_save else "Envoyer"
256
            ),
257
        )
258

259

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

264
    def __init__(self, *args, **kwargs):
3✔
265
        self.helper = FormHelper()
×
266
        self.helper.form_class = "content-wrapper"
×
267
        self.helper.form_method = "post"
×
268

269
        self.helper.layout = Layout(
×
270
            Field("file"),
271
            Field("images"),
272
            Submit("import-tuto", _("Importer le .tuto")),
273
        )
274
        super().__init__(*args, **kwargs)
×
275

276
    def clean(self):
3✔
277
        cleaned_data = super().clean()
×
278

279
        # Check that the files extensions are correct
280
        tuto = cleaned_data.get("file")
×
281
        images = cleaned_data.get("images")
×
282

283
        if tuto is not None:
×
284
            ext = tuto.name.split(".")[-1]
×
285
            if ext != "tuto":
×
286
                del cleaned_data["file"]
×
287
                msg = _("Le fichier doit être au format .tuto.")
×
288
                self._errors["file"] = self.error_class([msg])
×
289

290
        if images is not None:
×
291
            ext = images.name.split(".")[-1]
×
292
            if ext != "zip":
×
293
                del cleaned_data["images"]
×
294
                msg = _("Le fichier doit être au format .zip.")
×
295
                self._errors["images"] = self.error_class([msg])
×
296

297

298
class ImportContentForm(forms.Form):
3✔
299
    archive = forms.FileField(label=_("Sélectionnez l'archive de votre contenu."), required=True)
3✔
300
    image_archive = forms.FileField(label=_("Sélectionnez l'archive des images."), required=False)
3✔
301

302
    msg_commit = forms.CharField(
3✔
303
        label=_("Message de suivi"),
304
        max_length=400,
305
        required=False,
306
        widget=forms.TextInput(attrs={"placeholder": _("Un résumé de vos ajouts et modifications.")}),
307
    )
308

309
    def __init__(self, *args, **kwargs):
3✔
310
        super().__init__(*args, **kwargs)
1✔
311
        self.helper = FormHelper()
1✔
312
        self.helper.form_class = "content-wrapper"
1✔
313
        self.helper.form_method = "post"
1✔
314

315
        self.helper.layout = Layout(
1✔
316
            Field("archive"),
317
            Field("image_archive"),
318
            Field("msg_commit"),
319
            ButtonHolder(
320
                StrictButton("Importer l'archive", type="submit"),
321
            ),
322
        )
323

324
    def clean(self):
3✔
325
        cleaned_data = super().clean()
1✔
326

327
        # Check that the files extensions are correct
328
        archive = cleaned_data.get("archive")
1✔
329

330
        if archive is not None:
1!
331
            ext = archive.name.split(".")[-1]
1✔
332
            if ext != "zip":
1!
333
                del cleaned_data["archive"]
×
334
                msg = _("L'archive doit être au format .zip.")
×
335
                self._errors["archive"] = self.error_class([msg])
×
336

337
        image_archive = cleaned_data.get("image_archive")
1✔
338

339
        if image_archive is not None:
1✔
340
            ext = image_archive.name.split(".")[-1]
1✔
341
            if ext != "zip":
1!
342
                del cleaned_data["image_archive"]
×
343
                msg = _("L'archive doit être au format .zip.")
×
344
                self._errors["image_archive"] = self.error_class([msg])
×
345

346
        return cleaned_data
1✔
347

348

349
class ImportNewContentForm(ImportContentForm):
3✔
350
    subcategory = forms.ModelMultipleChoiceField(
3✔
351
        label=_(
352
            "Sous catégories de votre contenu. Si aucune catégorie ne convient "
353
            "n'hésitez pas à en demander une nouvelle lors de la validation !"
354
        ),
355
        queryset=SubCategory.objects.order_by("title").all(),
356
        required=True,
357
        widget=forms.SelectMultiple(
358
            attrs={
359
                "required": "required",
360
            }
361
        ),
362
    )
363

364
    def __init__(self, *args, **kwargs):
3✔
365
        super().__init__(*args, **kwargs)
1✔
366

367
        self.helper = FormHelper()
1✔
368
        self.helper.form_class = "content-wrapper"
1✔
369
        self.helper.form_method = "post"
1✔
370

371
        self.helper.layout = Layout(
1✔
372
            Field("archive"),
373
            Field("image_archive"),
374
            Field("subcategory"),
375
            Field("msg_commit"),
376
            ButtonHolder(
377
                StrictButton("Importer l'archive", type="submit"),
378
            ),
379
        )
380

381

382
class BetaForm(forms.Form):
3✔
383
    version = forms.CharField(widget=forms.HiddenInput, required=True)
3✔
384

385

386
# Notes
387

388

389
class NoteForm(forms.Form):
3✔
390
    text = forms.CharField(
3✔
391
        label="",
392
        widget=forms.Textarea(attrs={"placeholder": _("Votre message, au format Markdown."), "required": "required"}),
393
    )
394
    last_note = forms.IntegerField(label="", widget=forms.HiddenInput(), required=False)
3✔
395

396
    def __init__(self, content, reaction, *args, **kwargs):
3✔
397
        """initialize the form, handle antispam GUI
398
        :param content: the parent content
399
        :type content: zds.tutorialv2.models.database.PublishableContent
400
        :param reaction: the initial reaction if we edit, ``Ǹone```otherwise
401
        :type reaction: zds.tutorialv2.models.database.ContentReaction
402
        :param args:
403
        :param kwargs:
404
        """
405

406
        last_note = kwargs.pop("last_note", 0)
2✔
407

408
        super().__init__(*args, **kwargs)
2✔
409
        self.helper = FormHelper()
2✔
410
        self.helper.form_action = reverse("content:add-reaction") + f"?pk={content.pk}"
2✔
411
        self.helper.form_method = "post"
2✔
412

413
        self.helper.layout = Layout(
2✔
414
            CommonLayoutEditor(), Field("last_note") if not last_note else Hidden("last_note", last_note)
415
        )
416

417
        if reaction is not None:  # we're editing an existing comment
2✔
418
            self.helper.layout.append(HTML("{% include 'misc/hat_choice.html' with edited_message=reaction %}"))
2✔
419
        else:
420
            self.helper.layout.append(HTML("{% include 'misc/hat_choice.html' %}"))
2✔
421

422
        if content.antispam():
2✔
423
            if not reaction:
2✔
424
                self.helper["text"].wrap(
1✔
425
                    Field,
426
                    placeholder=_(
427
                        "Vous avez posté il n'y a pas longtemps. Merci de patienter "
428
                        "au moins 15 minutes entre deux messages consécutifs "
429
                        "afin de limiter le flood."
430
                    ),
431
                    disabled=True,
432
                )
433
        elif content.is_locked:
2!
434
            self.helper["text"].wrap(Field, placeholder=_("Ce contenu est verrouillé."), disabled=True)
×
435

436
        if reaction is not None:
2✔
437
            self.initial.setdefault("text", reaction.text)
2✔
438

439
        self.content = content
2✔
440

441
    def clean(self):
3✔
442
        cleaned_data = super().clean()
2✔
443

444
        text = cleaned_data.get("text")
2✔
445

446
        if text is None or not text.strip():
2!
447
            self._errors["text"] = self.error_class([_("Vous devez écrire une réponse !")])
×
448
            if "text" in cleaned_data:
×
449
                del cleaned_data["text"]
×
450

451
        elif len(text) > settings.ZDS_APP["forum"]["max_post_length"]:
2!
452
            self._errors["text"] = self.error_class(
×
453
                [
454
                    _("Ce message est trop long, il ne doit pas dépasser {0} " "caractères.").format(
455
                        settings.ZDS_APP["forum"]["max_post_length"]
456
                    )
457
                ]
458
            )
459
        last_note = cleaned_data.get("last_note", "0")
2✔
460
        if last_note is None:
2✔
461
            last_note = "0"
2✔
462
        is_valid = last_note == "0" or self.content.last_note is None or int(last_note) == self.content.last_note.pk
2✔
463
        if not is_valid:
2✔
464
            self._errors["last_note"] = self.error_class([_("Quelqu'un a posté pendant que vous répondiez")])
1✔
465
        return cleaned_data
2✔
466

467

468
class NoteEditForm(NoteForm):
3✔
469
    def __init__(self, *args, **kwargs):
3✔
470
        super().__init__(*args, **kwargs)
2✔
471

472
        content = kwargs["content"]
2✔
473
        reaction = kwargs["reaction"]
2✔
474

475
        self.helper.form_action = reverse("content:update-reaction") + "?message={}&pk={}".format(
2✔
476
            reaction.pk, content.pk
477
        )
478

479

480
# Validations.
481

482

483
class AskValidationForm(forms.Form):
3✔
484
    text = forms.CharField(
3✔
485
        label="",
486
        required=False,
487
        widget=forms.Textarea(
488
            attrs={"placeholder": _("Commentaire pour votre demande."), "rows": "3", "id": "ask_validation_text"}
489
        ),
490
    )
491

492
    version = forms.CharField(widget=forms.HiddenInput(), required=True)
3✔
493

494
    previous_page_url = ""
3✔
495

496
    def __init__(self, content, *args, **kwargs):
3✔
497
        """
498

499
        :param content: the parent content
500
        :type content: zds.tutorialv2.models.database.PublishableContent
501
        :param args:
502
        :param kwargs:
503
        :return:
504
        """
505
        super().__init__(*args, **kwargs)
3✔
506

507
        # modal form, send back to previous page:
508
        self.previous_page_url = get_content_version_url(content, content.current_version)
3✔
509

510
        self.helper = FormHelper()
3✔
511
        self.helper.form_action = reverse("validation:ask", kwargs={"pk": content.pk, "slug": content.slug})
3✔
512
        self.helper.form_method = "post"
3✔
513
        self.helper.form_class = "modal modal-flex"
3✔
514
        self.helper.form_id = "ask-validation"
3✔
515

516
        self.no_subcategories = content.subcategory.count() == 0
3✔
517
        no_category_msg = HTML(
3✔
518
            _(
519
                """<p><strong>Votre publication n'est dans aucune catégorie.
520
                                    Vous devez <a href="{}#{}">choisir une catégorie</a>
521
                                    avant de demander la validation.</strong></p>""".format(
522
                    reverse("content:edit", kwargs={"pk": content.pk, "slug": content.slug}), "div_id_subcategory"
523
                )
524
            )
525
        )
526

527
        self.no_license = not content.licence
3✔
528
        no_license_msg = HTML(
3✔
529
            _(
530
                """<p><strong>Vous n'avez pas choisi de licence pour votre publication.
531
                                   Vous devez <a href="#edit-license" class="open-modal">choisir une licence</a>
532
                                   avant de demander la validation.</strong></p>"""
533
            )
534
        )
535

536
        self.helper.layout = Layout(
3✔
537
            no_category_msg if self.no_subcategories else None,
538
            no_license_msg if self.no_license else None,
539
            Field("text"),
540
            Field("version"),
541
            StrictButton(_("Confirmer"), type="submit", css_class="btn-submit"),
542
        )
543

544
    def clean(self):
3✔
545
        cleaned_data = super().clean()
1✔
546

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

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

553
        if self.no_license:
1!
554
            error = [_(base_error_msg + "Vous devez choisir une licence pour votre publication.")]
×
555
            self.add_error(field=None, error=error)
×
556

557
        return cleaned_data
1✔
558

559

560
class AcceptValidationForm(forms.Form):
3✔
561
    validation = None
3✔
562

563
    text = forms.CharField(
3✔
564
        label="",
565
        required=True,
566
        error_messages={"required": _("Vous devez fournir un commentaire aux validateurs.")},
567
        widget=forms.Textarea(attrs={"placeholder": _("Commentaire de publication."), "rows": "2", "minlength": "3"}),
568
        validators=[MinLengthValidator(3, _("Votre commentaire doit faire au moins 3 caractères."))],
569
    )
570

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

573
    def __init__(self, validation, *args, **kwargs):
3✔
574
        """
575

576
        :param validation: the linked validation request object
577
        :type validation: zds.tutorialv2.models.database.Validation
578
        :param args:
579
        :param kwargs:
580
        :return:
581
        """
582
        self.previous_page_url = get_content_version_url(validation.content, validation.version)
1✔
583

584
        super().__init__(*args, **kwargs)
1✔
585

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

589
        self.helper = FormHelper()
1✔
590
        self.helper.form_action = reverse("validation:accept", kwargs={"pk": validation.pk})
1✔
591
        self.helper.form_method = "post"
1✔
592
        self.helper.form_class = "modal modal-flex"
1✔
593
        self.helper.form_id = "valid-publish"
1✔
594

595
        self.helper.layout = Layout(
1✔
596
            Field("text"), Field("is_major"), StrictButton(_("Publier"), type="submit", css_class="btn-submit")
597
        )
598

599

600
class CancelValidationForm(forms.Form):
3✔
601
    text = forms.CharField(
3✔
602
        label="",
603
        required=True,
604
        widget=forms.Textarea(
605
            attrs={"placeholder": _("Pourquoi annuler la validation ?"), "rows": "4", "id": "cancel_text"}
606
        ),
607
    )
608

609
    def __init__(self, validation, *args, **kwargs):
3✔
610
        super().__init__(*args, **kwargs)
1✔
611

612
        self.previous_page_url = get_content_version_url(validation.content, validation.version)
1✔
613

614
        self.helper = FormHelper()
1✔
615
        self.helper.form_action = reverse("validation:cancel", kwargs={"pk": validation.pk})
1✔
616
        self.helper.form_method = "post"
1✔
617
        self.helper.form_class = "modal modal-flex"
1✔
618
        self.helper.form_id = "cancel-validation"
1✔
619

620
        self.helper.layout = Layout(
1✔
621
            HTML("<p>Êtes-vous certain de vouloir annuler la validation de ce contenu ?</p>"),
622
            Field("text"),
623
            ButtonHolder(StrictButton(_("Confirmer"), type="submit", css_class="btn-submit")),
624
        )
625

626
    def clean(self):
3✔
627
        cleaned_data = super().clean()
1✔
628

629
        text = cleaned_data.get("text")
1✔
630

631
        if text is None or not text.strip():
1!
632
            self._errors["text"] = self.error_class([_("Merci de fournir une raison à l'annulation.")])
×
633
            if "text" in cleaned_data:
×
634
                del cleaned_data["text"]
×
635

636
        elif len(text) < 3:
1!
637
            self._errors["text"] = self.error_class([_("Votre commentaire doit faire au moins 3 caractères.")])
×
638
            if "text" in cleaned_data:
×
639
                del cleaned_data["text"]
×
640

641
        return cleaned_data
1✔
642

643

644
class RejectValidationForm(forms.Form):
3✔
645
    text = forms.CharField(
3✔
646
        label="",
647
        required=True,
648
        widget=forms.Textarea(attrs={"placeholder": _("Commentaire de rejet."), "rows": "6", "id": "reject_text"}),
649
    )
650

651
    def __init__(self, validation, *args, **kwargs):
3✔
652
        """
653

654
        :param validation: the linked validation request object
655
        :type validation: zds.tutorialv2.models.database.Validation
656
        :param args:
657
        :param kwargs:
658
        :return:
659
        """
660
        super().__init__(*args, **kwargs)
1✔
661

662
        self.previous_page_url = get_content_version_url(validation.content, validation.version)
1✔
663

664
        self.helper = FormHelper()
1✔
665
        self.helper.form_action = reverse("validation:reject", kwargs={"pk": validation.pk})
1✔
666
        self.helper.form_method = "post"
1✔
667
        self.helper.form_class = "modal modal-flex"
1✔
668
        self.helper.form_id = "reject"
1✔
669

670
        self.helper.layout = Layout(
1✔
671
            Field("text"), ButtonHolder(StrictButton(_("Rejeter"), type="submit", css_class="btn-submit"))
672
        )
673

674
    def clean(self):
3✔
675
        cleaned_data = super().clean()
1✔
676

677
        text = cleaned_data.get("text")
1✔
678

679
        if text is None or not text.strip():
1✔
680
            self._errors["text"] = self.error_class([_("Merci de fournir une raison au rejet.")])
1✔
681
            if "text" in cleaned_data:
1!
682
                del cleaned_data["text"]
×
683

684
        elif len(text) < 3:
1!
685
            self._errors["text"] = self.error_class([_("Votre commentaire doit faire au moins 3 caractères.")])
×
686
            if "text" in cleaned_data:
×
687
                del cleaned_data["text"]
×
688

689
        return cleaned_data
1✔
690

691

692
class RevokeValidationForm(forms.Form):
3✔
693
    version = forms.CharField(widget=forms.HiddenInput())
3✔
694

695
    text = forms.CharField(
3✔
696
        label="",
697
        required=True,
698
        widget=forms.Textarea(
699
            attrs={"placeholder": _("Pourquoi dépublier ce contenu ?"), "rows": "6", "id": "up_text"}
700
        ),
701
    )
702

703
    def __init__(self, content, *args, **kwargs):
3✔
704
        super().__init__(*args, **kwargs)
3✔
705

706
        # modal form, send back to previous page:
707
        self.previous_page_url = content.get_absolute_url_online()
3✔
708

709
        self.helper = FormHelper()
3✔
710
        self.helper.form_action = reverse("validation:revoke", kwargs={"pk": content.pk, "slug": content.slug})
3✔
711
        self.helper.form_method = "post"
3✔
712
        self.helper.form_class = "modal modal-flex"
3✔
713
        self.helper.form_id = "unpublish"
3✔
714

715
        self.helper.layout = Layout(
3✔
716
            Field("text"), Field("version"), StrictButton(_("Dépublier"), type="submit", css_class="btn-submit")
717
        )
718

719
    def clean(self):
3✔
720
        cleaned_data = super().clean()
1✔
721

722
        text = cleaned_data.get("text")
1✔
723

724
        if text is None or not text.strip():
1✔
725
            self._errors["text"] = self.error_class([_("Veuillez fournir la raison de votre dépublication.")])
1✔
726
            if "text" in cleaned_data:
1!
727
                del cleaned_data["text"]
×
728

729
        elif len(text) < 3:
1!
730
            self._errors["text"] = self.error_class([_("Votre commentaire doit faire au moins 3 caractères.")])
×
731
            if "text" in cleaned_data:
×
732
                del cleaned_data["text"]
×
733

734
        return cleaned_data
1✔
735

736

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

740
    def __init__(self, *args, **kwargs):
3✔
741
        super().__init__(*args, **kwargs)
3✔
742
        self.helper = FormHelper()
3✔
743
        self.helper.form_action = reverse("content:activate-jsfiddle")
3✔
744
        self.helper.form_method = "post"
3✔
745
        self.helper.form_class = "modal modal-flex"
3✔
746
        self.helper.form_id = "js-activation"
3✔
747

748
        self.helper.layout = Layout(
3✔
749
            Field("js_support"),
750
            ButtonHolder(
751
                StrictButton(_("Valider"), type="submit", css_class="btn-submit"),
752
            ),
753
            Hidden("pk", "{{ content.pk }}"),
754
        )
755

756
    def clean(self):
3✔
757
        cleaned_data = super().clean()
1✔
758
        if "js_support" not in cleaned_data:
1!
759
            cleaned_data["js_support"] = False
×
760
        if "pk" in self.data and self.data["pk"].isdigit():
1!
761
            cleaned_data["pk"] = int(self.data["pk"])
1✔
762
        else:
763
            cleaned_data["pk"] = 0
×
764
        return cleaned_data
1✔
765

766

767
class MoveElementForm(forms.Form):
3✔
768
    child_slug = forms.HiddenInput()
3✔
769
    container_slug = forms.HiddenInput()
3✔
770
    first_level_slug = forms.HiddenInput()
3✔
771
    moving_method = forms.HiddenInput()
3✔
772

773
    MOVE_UP = "up"
3✔
774
    MOVE_DOWN = "down"
3✔
775
    MOVE_AFTER = "after"
3✔
776
    MOVE_BEFORE = "before"
3✔
777

778
    def __init__(self, *args, **kwargs):
3✔
779
        super().__init__(*args, **kwargs)
1✔
780
        self.helper = FormHelper()
1✔
781
        self.helper.form_action = reverse("content:move-element")
1✔
782
        self.helper.form_method = "post"
1✔
783
        self.helper.layout = Layout(
1✔
784
            Field("child_slug"),
785
            Field("container_slug"),
786
            Field("first_level_slug"),
787
            Field("moving_method"),
788
            Hidden("pk", "{{ content.pk }}"),
789
        )
790

791

792
class WarnTypoForm(forms.Form):
3✔
793
    text = forms.CharField(
3✔
794
        label="",
795
        required=True,
796
        widget=forms.Textarea(attrs={"placeholder": _("Expliquez la faute"), "rows": "3", "id": "warn_text"}),
797
    )
798

799
    target = forms.CharField(widget=forms.HiddenInput(), required=False)
3✔
800
    version = forms.CharField(widget=forms.HiddenInput(), required=True)
3✔
801

802
    def __init__(self, content, targeted, public=True, *args, **kwargs):
3✔
803
        super().__init__(*args, **kwargs)
3✔
804

805
        self.content = content
3✔
806
        self.targeted = targeted
3✔
807

808
        # modal form, send back to previous page if any:
809
        if public:
3✔
810
            self.previous_page_url = targeted.get_absolute_url_online()
3✔
811
        else:
812
            self.previous_page_url = targeted.get_absolute_url_beta()
3✔
813

814
        # add an additional link to send PM if needed
815
        type_ = _("l'article")
3✔
816

817
        if content.is_tutorial:
3✔
818
            type_ = _("le tutoriel")
3✔
819
        elif content.is_opinion:
3✔
820
            type_ = _("le billet")
2✔
821

822
        if targeted.get_tree_depth() == 0:
3✔
823
            pm_title = _("J'ai trouvé une faute dans {} « {} ».").format(type_, targeted.title)
3✔
824
        else:
825
            pm_title = _("J'ai trouvé une faute dans le chapitre « {} ».").format(targeted.title)
1✔
826

827
        usernames = ""
3✔
828
        num_of_authors = content.authors.count()
3✔
829
        for index, user in enumerate(content.authors.all()):
3✔
830
            if index != 0:
3✔
831
                usernames += "&"
1✔
832
            usernames += "username=" + user.username
3✔
833

834
        msg = _('<p>Pas assez de place ? <a href="{}?title={}&{}">Envoyez un MP {}</a> !</a>').format(
3✔
835
            reverse("mp:create"), pm_title, usernames, _("à l'auteur") if num_of_authors == 1 else _("aux auteurs")
836
        )
837

838
        version = content.sha_beta
3✔
839
        if public:
3✔
840
            version = content.sha_public
3✔
841

842
        # create form
843
        self.helper = FormHelper()
3✔
844
        self.helper.form_action = reverse("content:warn-typo") + f"?pk={content.pk}"
3✔
845
        self.helper.form_method = "post"
3✔
846
        self.helper.form_class = "modal modal-flex"
3✔
847
        self.helper.form_id = "warn-typo-modal"
3✔
848
        self.helper.layout = Layout(
3✔
849
            Field("target"),
850
            Field("text"),
851
            HTML(msg),
852
            Hidden("pk", "{{ content.pk }}"),
853
            Hidden("version", version),
854
            ButtonHolder(StrictButton(_("Envoyer"), type="submit", css_class="btn-submit")),
855
        )
856

857
    def clean(self):
3✔
858
        cleaned_data = super().clean()
1✔
859

860
        text = cleaned_data.get("text")
1✔
861

862
        if text is None or not text.strip():
1!
863
            self._errors["text"] = self.error_class([_("Vous devez indiquer la faute commise.")])
×
864
            if "text" in cleaned_data:
×
865
                del cleaned_data["text"]
×
866

867
        elif len(text) < 3:
1!
868
            self._errors["text"] = self.error_class([_("Votre commentaire doit faire au moins 3 caractères.")])
×
869
            if "text" in cleaned_data:
×
870
                del cleaned_data["text"]
×
871

872
        return cleaned_data
1✔
873

874

875
class PublicationForm(forms.Form):
3✔
876
    """
877
    The publication form (used only for content without preliminary validation).
878
    """
879

880
    def __init__(self, content, *args, **kwargs):
3✔
881
        super().__init__(*args, **kwargs)
3✔
882

883
        self.previous_page_url = content.get_absolute_url()
3✔
884

885
        self.helper = FormHelper()
3✔
886
        self.helper.form_action = reverse("validation:publish-opinion", kwargs={"pk": content.pk, "slug": content.slug})
3✔
887
        self.helper.form_method = "post"
3✔
888
        self.helper.form_class = "modal modal-flex"
3✔
889
        self.helper.form_id = "valid-publication"
3✔
890

891
        self.no_subcategories = content.subcategory.count() == 0
3✔
892
        no_category_msg = HTML(
3✔
893
            _(
894
                """<p><strong>Votre publication n'est dans aucune catégorie.
895
                                    Vous devez <a href="{}#{}">choisir une catégorie</a>
896
                                    avant de publier.</strong></p>""".format(
897
                    reverse("content:edit", kwargs={"pk": content.pk, "slug": content.slug}), "div_id_subcategory"
898
                )
899
            )
900
        )
901

902
        self.no_license = not content.licence
3✔
903
        no_license_msg = HTML(
3✔
904
            _(
905
                """<p><strong>Vous n'avez pas choisi de licence pour votre publication.
906
                                   Vous devez <a href="#edit-license" class="open-modal">choisir une licence</a>
907
                                   avant de publier.</strong></p>"""
908
            )
909
        )
910

911
        self.helper.layout = Layout(
3✔
912
            no_category_msg if self.no_subcategories else None,
913
            no_license_msg if self.no_license else None,
914
            HTML(_("<p>Ce billet sera publié directement et n'engage que vous.</p>")),
915
            StrictButton(_("Publier"), type="submit", css_class="btn-submit"),
916
        )
917

918
    def clean(self):
3✔
919
        cleaned_data = super().clean()
1✔
920

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

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

927
        if self.no_license:
1!
928
            error = _(base_error_msg + "Vous devez choisir une licence pour votre publication.")
×
929
            self.add_error(field=None, error=error)
×
930

931
        return cleaned_data
1✔
932

933

934
class UnpublicationForm(forms.Form):
3✔
935
    version = forms.CharField(widget=forms.HiddenInput())
3✔
936

937
    text = forms.CharField(
3✔
938
        label="",
939
        required=True,
940
        widget=forms.Textarea(
941
            attrs={"placeholder": _("Pourquoi dépublier ce contenu ?"), "rows": "6", "id": "up_reason"}
942
        ),
943
    )
944

945
    def __init__(self, content, *args, **kwargs):
3✔
946
        super().__init__(*args, **kwargs)
3✔
947

948
        # modal form, send back to previous page:
949
        self.previous_page_url = content.get_absolute_url_online()
3✔
950

951
        self.helper = FormHelper()
3✔
952
        self.helper.form_action = reverse(
3✔
953
            "validation:unpublish-opinion", kwargs={"pk": content.pk, "slug": content.slug}
954
        )
955

956
        self.helper.form_method = "post"
3✔
957
        self.helper.form_class = "modal modal-flex"
3✔
958
        self.helper.form_id = "unpublish"
3✔
959

960
        self.helper.layout = Layout(
3✔
961
            Field("text"), Field("version"), StrictButton(_("Dépublier"), type="submit", css_class="btn-submit")
962
        )
963

964

965
class PickOpinionForm(forms.Form):
3✔
966
    version = forms.CharField(widget=forms.HiddenInput())
3✔
967

968
    def __init__(self, content, *args, **kwargs):
3✔
969
        super().__init__(*args, **kwargs)
3✔
970

971
        # modal form, send back to previous page:
972
        self.previous_page_url = content.get_absolute_url_online()
3✔
973

974
        self.helper = FormHelper()
3✔
975
        self.helper.form_action = reverse("validation:pick-opinion", kwargs={"pk": content.pk, "slug": content.slug})
3✔
976
        self.helper.form_method = "post"
3✔
977
        self.helper.form_class = "modal modal-flex"
3✔
978
        self.helper.form_id = "pick-opinion"
3✔
979

980
        self.helper.layout = Layout(
3✔
981
            HTML(
982
                "<p>Êtes-vous certain(e) de vouloir valider ce billet ? "
983
                "Il pourra maintenant être présent sur la page d’accueil.</p>"
984
            ),
985
            Field("version"),
986
            StrictButton(_("Valider"), type="submit", css_class="btn-submit"),
987
        )
988

989

990
class DoNotPickOpinionForm(forms.Form):
3✔
991
    operation = forms.CharField(widget=forms.HiddenInput())
3✔
992
    redirect = forms.CharField(widget=forms.HiddenInput(), required=False)
3✔
993

994
    def __init__(self, content, *args, **kwargs):
3✔
995
        super().__init__(*args, **kwargs)
1✔
996

997
        # modal form, send back to previous page:
998
        self.previous_page_url = content.get_absolute_url_online()
1✔
999

1000
        self.helper = FormHelper()
1✔
1001
        self.helper.form_action = reverse("validation:unpick-opinion", kwargs={"pk": content.pk, "slug": content.slug})
1✔
1002
        self.helper.form_method = "post"
1✔
1003
        self.helper.form_class = "modal modal-flex"
1✔
1004
        self.helper.form_id = "unpick-opinion"
1✔
1005

1006
        self.helper.layout = Layout(
1✔
1007
            HTML(_("<p>Ce billet n'apparaîtra plus dans la liste des billets à choisir.</p>")),
1008
            Field("operation"),
1009
            StrictButton(_("Valider"), type="submit", css_class="btn-submit"),
1010
        )
1011

1012
    def clean(self):
3✔
1013
        cleaned = super().clean()
1✔
1014
        cleaned["operation"] = (
1✔
1015
            self.data["operation"] if self.data["operation"] in ["NO_PICK", "REJECT", "REMOVE_PUB"] else None
1016
        )
1017
        cleaned["redirect"] = self.data["redirect"] == "true" if "redirect" in self.data else False
1✔
1018
        return cleaned
1✔
1019

1020
    def is_valid(self):
3✔
1021
        base = super().is_valid()
1✔
1022
        if not self["operation"]:
1!
1023
            self._errors["operation"] = _("Opération invalide, NO_PICK, REJECT ou REMOVE_PUB attendu.")
×
1024
            return False
×
1025
        return base
1✔
1026

1027

1028
class UnpickOpinionForm(forms.Form):
3✔
1029
    version = forms.CharField(widget=forms.HiddenInput())
3✔
1030

1031
    text = forms.CharField(
3✔
1032
        label="",
1033
        required=True,
1034
        widget=forms.Textarea(
1035
            attrs={"placeholder": _("Pourquoi retirer ce billet de la liste des billets choisis ?"), "rows": "6"}
1036
        ),
1037
    )
1038

1039
    def __init__(self, content, *args, **kwargs):
3✔
1040
        super().__init__(*args, **kwargs)
3✔
1041

1042
        # modal form, send back to previous page:
1043
        self.previous_page_url = content.get_absolute_url_online()
3✔
1044

1045
        self.helper = FormHelper()
3✔
1046
        self.helper.form_action = reverse("validation:unpick-opinion", kwargs={"pk": content.pk, "slug": content.slug})
3✔
1047
        self.helper.form_method = "post"
3✔
1048
        self.helper.form_class = "modal modal-flex"
3✔
1049
        self.helper.form_id = "unpick-opinion"
3✔
1050

1051
        self.helper.layout = Layout(
3✔
1052
            Field("version"), Field("text"), StrictButton(_("Enlever"), type="submit", css_class="btn-submit")
1053
        )
1054

1055

1056
class PromoteOpinionToArticleForm(forms.Form):
3✔
1057
    version = forms.CharField(widget=forms.HiddenInput())
3✔
1058

1059
    def __init__(self, content, *args, **kwargs):
3✔
1060
        super().__init__(*args, **kwargs)
3✔
1061

1062
        # modal form, send back to previous page:
1063
        self.previous_page_url = content.get_absolute_url_online()
3✔
1064

1065
        self.helper = FormHelper()
3✔
1066
        self.helper.form_action = reverse("validation:promote-opinion", kwargs={"pk": content.pk, "slug": content.slug})
3✔
1067
        self.helper.form_method = "post"
3✔
1068
        self.helper.form_class = "modal modal-flex"
3✔
1069
        self.helper.form_id = "convert-opinion"
3✔
1070

1071
        self.helper.layout = Layout(
3✔
1072
            HTML(
1073
                """<p>Avez-vous la certitude de vouloir proposer ce billet comme article ?
1074
                    Cela copiera le billet pour en faire un article,
1075
                    puis créera une demande de validation pour ce dernier.</p>"""
1076
            ),
1077
            Field("version"),
1078
            StrictButton(_("Valider"), type="submit", css_class="btn-submit"),
1079
        )
1080

1081

1082
class ContentCompareStatsURLForm(forms.Form):
3✔
1083
    urls = forms.MultipleChoiceField(widget=forms.CheckboxSelectMultiple(), required=True)
3✔
1084

1085
    def __init__(self, urls, *args, **kwargs):
3✔
1086
        super().__init__(*args, **kwargs)
1✔
1087
        self.fields["urls"].choices = urls
1✔
1088

1089
        self.helper = FormHelper()
1✔
1090
        self.helper.layout = Layout(Field("urls"), StrictButton(_("Comparer"), type="submit"))
1✔
1091

1092
    def clean(self):
3✔
1093
        cleaned_data = super().clean()
1✔
1094
        urls = cleaned_data.get("urls")
1✔
1095
        if not urls:
1!
1096
            raise forms.ValidationError(_("Vous devez choisir des URL a comparer"))
×
1097
        if len(urls) < 2:
1!
1098
            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

© 2025 Coveralls, Inc