diff --git a/zds/tutorialv2/forms.py b/zds/tutorialv2/forms.py index ccb17e347f..ee75febc87 100644 --- a/zds/tutorialv2/forms.py +++ b/zds/tutorialv2/forms.py @@ -9,13 +9,11 @@ from zds.tutorialv2.utils import get_content_version_url from zds.utils.forms import CommonLayoutEditor, CommonLayoutVersionEditor -from zds.utils.models import SubCategory, Licence +from zds.utils.models import SubCategory from zds.tutorialv2.models import TYPE_CHOICES -from zds.tutorialv2.models.help_requests import HelpWriting -from zds.tutorialv2.models.database import PublishableContent, ContentContributionRole, ContentSuggestion +from zds.tutorialv2.models.database import PublishableContent from django.utils.translation import gettext_lazy as _ -from zds.member.models import Profile -from zds.utils.forms import TagValidator, IncludeEasyMDE +from zds.utils.forms import IncludeEasyMDE from zds.utils.validators import with_svg_validator, slugify_raise_on_invalid, InvalidSlugError @@ -48,123 +46,6 @@ def label_from_instance(self, obj): return obj.title -class ContributionForm(forms.Form): - contribution_role = ReviewerTypeModelChoiceField( - label=_("Role"), - required=True, - queryset=ContentContributionRole.objects.order_by("title").all(), - ) - - username = forms.CharField( - label=_("Contributeur"), - required=True, - widget=forms.TextInput( - attrs={"placeholder": _("Pseudo du membre à ajouter."), "data-autocomplete": "{ 'type': 'single' }"} - ), - ) - - comment = forms.CharField( - label=_("Commentaire"), - required=False, - widget=forms.Textarea(attrs={"placeholder": _("Commentaire sur ce contributeur."), "rows": "3"}), - ) - - def __init__(self, content, *args, **kwargs): - self.helper = FormHelper() - self.helper.form_class = "modal modal-flex" - self.helper.form_id = "add-contributor" - self.helper.form_method = "post" - self.helper.form_action = reverse("content:add-contributor", kwargs={"pk": content.pk}) - self.helper.layout = Layout( - Field("username"), - Field("contribution_role"), - Field("comment"), - ButtonHolder( - StrictButton(_("Ajouter"), type="submit", css_class="btn-submit"), - ), - ) - super().__init__(*args, **kwargs) - - def clean_username(self): - cleaned_data = super().clean() - if cleaned_data.get("username"): - username = cleaned_data.get("username") - user = Profile.objects.contactable_members().filter(user__username__iexact=username.strip().lower()).first() - if user is not None: - cleaned_data["user"] = user.user - else: - self._errors["user"] = self.error_class([_("L'utilisateur sélectionné n'existe pas")]) - - if "user" not in cleaned_data: - self._errors["user"] = self.error_class([_("Veuillez renseigner l'utilisateur")]) - - return cleaned_data - - -class RemoveContributionForm(forms.Form): - pk_contribution = forms.CharField( - label=_("Contributeur"), - required=True, - ) - - -class AuthorForm(forms.Form): - username = forms.CharField(label=_("Auteurs à ajouter séparés d'une virgule."), required=True) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.helper = FormHelper() - self.helper.form_class = "content-wrapper" - self.helper.form_method = "post" - self.helper.layout = Layout( - Field("username"), - ButtonHolder( - StrictButton(_("Ajouter"), type="submit"), - ), - ) - - def clean_username(self): - """Check every username and send it to the cleaned_data['user'] list - - :return: a dictionary of all treated data with the users key added - """ - cleaned_data = super().clean() - users = [] - if cleaned_data.get("username"): - for username in cleaned_data.get("username").split(","): - user = ( - Profile.objects.contactable_members() - .filter(user__username__iexact=username.strip().lower()) - .first() - ) - if user is not None: - users.append(user.user) - if len(users) > 0: - cleaned_data["users"] = users - return cleaned_data - - def is_valid(self): - return super().is_valid() and "users" in self.clean() - - -class RemoveAuthorForm(AuthorForm): - def clean_username(self): - """Check every username and send it to the cleaned_data['user'] list - - :return: a dictionary of all treated data with the users key added - """ - cleaned_data = super(AuthorForm, self).clean() - users = [] - for username in cleaned_data.get("username").split(","): - # we can remove all users (bots inclued) - user = Profile.objects.filter(user__username__iexact=username.strip().lower()).first() - if user is not None: - users.append(user.user) - if len(users) > 0: - cleaned_data["users"] = users - return cleaned_data - - class ContainerForm(FormWithTitle): introduction = forms.CharField( label=_("Introduction"), @@ -316,109 +197,6 @@ def clean(self): return cleaned_data -class EditContentTagsForm(forms.Form): - tags = forms.CharField( - label=_("Tags séparés par des virgules (exemple : python,api,web) :"), - max_length=64, - required=False, - widget=forms.TextInput(), - error_messages={"max_length": _("La liste de tags saisie dépasse la longueur maximale autorisée.")}, - ) - - def __init__(self, content, db_content, *args, **kwargs): - self.db_content = db_content - kwargs["initial"] = {"tags": ", ".join(db_content.tags.values_list("title", flat=True))} - super(forms.Form, self).__init__(*args, **kwargs) - - self.fields["tags"].widget.attrs.update( - { - "data-autocomplete": '{ "type": "multiple", "fieldname": "title", "url": "' - + reverse("api:utils:tags-list") - + '?search=%s" }', - } - ) - - self.helper = FormHelper() - self.helper.form_class = "content-wrapper" - self.helper.form_method = "post" - self.helper.form_id = "edit-tags" - self.helper.form_class = "modal modal-flex" - self.helper.form_action = reverse("content:edit-tags", kwargs={"pk": content.pk}) - self.helper.layout = Layout( - HTML( - """

Les tags permettent de grouper les publications plus finement que les catégories. - Par exemple, vous pouvez indiquer une technologie ou une sous-discipline. - Consultez la page des tags pour voir des exemples.""".format( - reverse("content:tags") - ) - ), - Field("tags"), - ButtonHolder(StrictButton("Valider", type="submit")), - ) - self.previous_page_url = reverse("content:view", kwargs={"pk": content.pk, "slug": content.slug}) - - def clean_tags(self): - validator = TagValidator() - cleaned_tags = self.cleaned_data.get("tags") - if not validator.validate_raw_string(cleaned_tags): - self.add_error("tags", self.error_class(validator.errors)) - return cleaned_tags - - -class EditContentLicenseForm(forms.Form): - license = forms.ModelChoiceField( - label=_("Licence de votre publication : "), - queryset=Licence.objects.order_by("title").all(), - required=True, - empty_label=_("Choisir une licence"), - error_messages={ - "required": _("Merci de choisir une licence."), - "invalid_choice": _("Merci de choisir une licence valide dans la liste."), - }, - ) - - update_preferred_license = forms.BooleanField( - label=_("Je souhaite utiliser cette licence comme choix par défaut pour mes futures publications."), - required=False, - ) - - def __init__(self, versioned_content, *args, **kwargs): - kwargs["initial"] = {"license": versioned_content.licence} - super(forms.Form, self).__init__(*args, **kwargs) - - self.helper = FormHelper() - self.helper.form_class = "content-wrapper" - self.helper.form_method = "post" - self.helper.form_id = "edit-license" - self.helper.form_class = "modal modal-flex" - self.helper.form_action = reverse("content:edit-license", kwargs={"pk": versioned_content.pk}) - self.previous_page_url = reverse( - "content:view", kwargs={"pk": versioned_content.pk, "slug": versioned_content.slug} - ) - self._create_layout() - - if "type" in self.initial: - self.helper["type"].wrap(Field, disabled=True) - - def _create_layout(self): - self.helper.layout = Layout( - HTML( - """

{} encourage l'utilisation de licences facilitant le partage, - telles que les licences Creative Commons.

-

Pour choisir la licence de votre publication, aidez-vous de la - présentation - des différentes licences proposées sur le site.

""".format( - settings.ZDS_APP["site"]["literal_name"], - settings.ZDS_APP["site"]["licenses"]["licence_info_title"], - settings.ZDS_APP["site"]["licenses"]["licence_info_link"], - ) - ), - Field("license"), - Field("update_preferred_license"), - ButtonHolder(StrictButton("Valider", type="submit")), - ) - - class ExtractForm(FormWithTitle): text = forms.CharField( label=_("Texte"), @@ -1289,62 +1067,3 @@ def clean(self): raise forms.ValidationError(_("Vous devez choisir des URL a comparer")) if len(urls) < 2: raise forms.ValidationError(_("Il faut au minimum 2 urls à comparer")) - - -class SearchSuggestionForm(forms.Form): - suggestion_pk = forms.CharField( - label="Contenu à suggérer", - required=False, - widget=forms.TextInput(), - ) - excluded_pk = forms.CharField(required=False, widget=forms.HiddenInput(attrs={"class": "excluded_field"})) - - def __init__(self, content, *args, **kwargs): - super().__init__(*args, **kwargs) - - self.fields["suggestion_pk"].widget.attrs.update( - { - "data-autocomplete": '{"type": "multiple_checkbox",' - '"limit": 10,' - '"fieldname": "title",' - '"url": "' + reverse("search:suggestion") + '?q=%s&excluded=%e"}', - "placeholder": "Rechercher un contenu", - } - ) - - self.helper = FormHelper() - self.helper.form_action = reverse("content:add-suggestion", kwargs={"pk": content.pk}) - self.helper.form_class = "modal modal-large" - self.helper.form_id = "add-suggestion" - self.helper.form_method = "post" - - self.helper.layout = Layout( - Field("suggestion_pk"), Field("excluded_pk"), StrictButton(_("Ajouter"), type="submit") - ) - - -class RemoveSuggestionForm(forms.Form): - pk_suggestion = forms.IntegerField( - label=_("Suggestion"), - required=True, - error_messages={"does_not_exist": _("La suggestion sélectionnée n'existe pas.")}, - ) - - def clean_pk_suggestion(self): - pk_suggestion = self.cleaned_data.get("pk_suggestion") - suggestion = ContentSuggestion.objects.filter(id=pk_suggestion).first() - if suggestion is None: - self.add_error("pk_suggestion", self.fields["pk_suggestion"].error_messages["does_not_exist"]) - return pk_suggestion - - -class ToggleHelpForm(forms.Form): - help_wanted = forms.CharField() - activated = forms.BooleanField(required=False) - - def clean(self): - clean_data = super().clean() - clean_data["help_wanted"] = HelpWriting.objects.filter(title=(self.data["help_wanted"] or "").strip()).first() - if not clean_data["help_wanted"]: - self.add_error("help_wanted", _("Inconnu")) - return clean_data diff --git a/zds/tutorialv2/models/events.py b/zds/tutorialv2/models/events.py index 13e87cd57e..67b8485c51 100644 --- a/zds/tutorialv2/models/events.py +++ b/zds/tutorialv2/models/events.py @@ -7,7 +7,8 @@ from zds.tutorialv2.views.authors import AddAuthorToContent, RemoveAuthorFromContent from zds.tutorialv2.views.beta import ManageBetaContent from zds.tutorialv2.views.contributors import AddContributorToContent, RemoveContributorFromContent -from zds.tutorialv2.views.editorialization import EditContentTags, AddSuggestion, RemoveSuggestion +from zds.tutorialv2.views.suggestions import AddSuggestion, RemoveSuggestion +from zds.tutorialv2.views.tags import EditTags from zds.tutorialv2.views.goals import EditGoals from zds.tutorialv2.views.labels import EditLabels from zds.tutorialv2.views.help import ChangeHelp @@ -132,7 +133,7 @@ def record_event_validation_management(sender, performer, signal, content, versi ).save() -@receiver(signals.tags_management, sender=EditContentTags) +@receiver(signals.tags_management, sender=EditTags) def record_event_tags_management(sender, performer, signal, content, **_): Event( performer=performer, diff --git a/zds/tutorialv2/tests/tests_views/tests_addcontributor.py b/zds/tutorialv2/tests/tests_views/tests_addcontributor.py index 944e90334f..abd6293471 100644 --- a/zds/tutorialv2/tests/tests_views/tests_addcontributor.py +++ b/zds/tutorialv2/tests/tests_views/tests_addcontributor.py @@ -9,7 +9,7 @@ from zds.member.tests.factories import ProfileFactory, StaffProfileFactory from zds.tutorialv2.tests.factories import ContentContributionRoleFactory, PublishableContentFactory -from zds.tutorialv2.forms import ContributionForm +from zds.tutorialv2.views.contributors import ContributionForm from zds.tutorialv2.models.database import ContentContribution from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents diff --git a/zds/tutorialv2/tests/tests_views/tests_editcontentlicense.py b/zds/tutorialv2/tests/tests_views/tests_editcontentlicense.py index 0e4eb2a660..1e3bdd42ec 100644 --- a/zds/tutorialv2/tests/tests_views/tests_editcontentlicense.py +++ b/zds/tutorialv2/tests/tests_views/tests_editcontentlicense.py @@ -3,8 +3,7 @@ from zds.tutorialv2.models.database import PublishableContent from zds.member.models import Profile -from zds.tutorialv2.views.contents import EditContentLicense -from zds.tutorialv2.forms import EditContentLicenseForm +from zds.tutorialv2.views.licence import EditContentLicenseForm, EditContentLicense from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents from zds.member.tests.factories import ProfileFactory, StaffProfileFactory from zds.tutorialv2.tests.factories import PublishableContentFactory diff --git a/zds/tutorialv2/tests/tests_views/tests_editcontenttags.py b/zds/tutorialv2/tests/tests_views/tests_editcontenttags.py index 8b3d02a3f6..603bbe414f 100644 --- a/zds/tutorialv2/tests/tests_views/tests_editcontenttags.py +++ b/zds/tutorialv2/tests/tests_views/tests_editcontenttags.py @@ -5,8 +5,7 @@ from django.utils.html import escape from zds.tutorialv2.models.database import PublishableContent -from zds.tutorialv2.views.editorialization import EditContentTags -from zds.tutorialv2.forms import EditContentTagsForm +from zds.tutorialv2.views.tags import EditTagsForm, EditTags from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents from zds.tutorialv2.tests.factories import PublishableContentFactory from zds.member.tests.factories import ProfileFactory, StaffProfileFactory @@ -73,8 +72,8 @@ def setUp(self): # Get information to be reused in tests self.form_url = reverse("content:edit-tags", kwargs={"pk": self.content.pk}) - self.error_messages = EditContentTagsForm.declared_fields["tags"].error_messages - self.success_message = EditContentTags.success_message + self.error_messages = EditTagsForm.declared_fields["tags"].error_messages + self.success_message = EditTags.success_message # Log in with an authorized user (e.g the author of the content) to perform the tests self.client.force_login(self.author.user) @@ -90,7 +89,7 @@ def get_test_cases(self): "success_tags": {"inputs": {"tags": "test, test1"}, "expected_outputs": [self.success_message]}, "stripped_to_empty": {"inputs": {"tags": " "}, "expected_outputs": [self.success_message]}, "tags_string_too_long": { - "inputs": {"tags": "a" * (EditContentTagsForm.declared_fields["tags"].max_length + 1)}, + "inputs": {"tags": "a" * (EditTagsForm.declared_fields["tags"].max_length + 1)}, "expected_outputs": [self.error_messages["max_length"]], }, "invalid_slug_tag": { diff --git a/zds/tutorialv2/tests/tests_views/tests_removesuggestion.py b/zds/tutorialv2/tests/tests_views/tests_removesuggestion.py index 470d1a1ffc..db324199e9 100644 --- a/zds/tutorialv2/tests/tests_views/tests_removesuggestion.py +++ b/zds/tutorialv2/tests/tests_views/tests_removesuggestion.py @@ -7,7 +7,7 @@ from zds.member.tests.factories import ProfileFactory, StaffProfileFactory from zds.tutorialv2.tests.factories import PublishableContentFactory -from zds.tutorialv2.forms import RemoveSuggestionForm +from zds.tutorialv2.views.suggestions import RemoveSuggestionForm from zds.tutorialv2.models.database import ContentSuggestion from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents diff --git a/zds/tutorialv2/urls/urls_contents.py b/zds/tutorialv2/urls/urls_contents.py index e3299d0db5..2cd1f7ba9d 100644 --- a/zds/tutorialv2/urls/urls_contents.py +++ b/zds/tutorialv2/urls/urls_contents.py @@ -1,7 +1,8 @@ from django.urls import path from django.views.generic.base import RedirectView -from zds.tutorialv2.views.contents import CreateContent, EditContent, EditContentLicense, DeleteContent +from zds.tutorialv2.views.contents import CreateContent, EditContent, DeleteContent +from zds.tutorialv2.views.licence import EditContentLicense from zds.tutorialv2.views.display.container import ContainerValidationView from zds.tutorialv2.views.display.content import ContentValidationView from zds.tutorialv2.views.events import EventsList @@ -35,7 +36,8 @@ RemoveContributorFromContent, ContentOfContributors, ) -from zds.tutorialv2.views.editorialization import RemoveSuggestion, AddSuggestion, EditContentTags +from zds.tutorialv2.views.suggestions import RemoveSuggestion, AddSuggestion +from zds.tutorialv2.views.tags import EditTags from zds.tutorialv2.views.lists import TagsListView, ContentOfAuthor, ListContentReactions from zds.tutorialv2.views.alerts import SendContentAlert, SolveContentAlert @@ -208,7 +210,7 @@ def get_version_pages(): # Modify the license path("modifier-licence//", EditContentLicense.as_view(), name="edit-license"), # Modify the tags - path("modifier-tags//", EditContentTags.as_view(), name="edit-tags"), + path("modifier-tags//", EditTags.as_view(), name="edit-tags"), # beta: path("activer-beta///", ManageBetaContent.as_view(action="set"), name="set-beta"), path( diff --git a/zds/tutorialv2/views/authors.py b/zds/tutorialv2/views/authors.py index 362e583767..e77d673884 100644 --- a/zds/tutorialv2/views/authors.py +++ b/zds/tutorialv2/views/authors.py @@ -1,3 +1,7 @@ +from crispy_forms.bootstrap import StrictButton +from crispy_forms.helper import FormHelper +from crispy_forms.layout import Layout, Field +from django import forms from django.contrib import messages from django.contrib.auth.models import User from django.shortcuts import redirect @@ -8,16 +12,71 @@ from zds.gallery.models import UserGallery, GALLERY_WRITE from zds.member.decorator import LoggedWithReadWriteHability +from zds.member.models import Profile from zds.member.utils import get_bot_account from zds.mp.models import is_reachable from zds.tutorialv2 import signals -from zds.tutorialv2.forms import AuthorForm, RemoveAuthorForm from zds.tutorialv2.mixins import SingleContentFormViewMixin from zds.utils.models import get_hat_from_settings from zds.mp.utils import send_mp +class AuthorForm(forms.Form): + username = forms.CharField(label=_("Auteurs à ajouter séparés d'une virgule."), required=True) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.form_class = "content-wrapper" + self.helper.form_method = "post" + self.helper.layout = Layout( + Field("username"), + StrictButton(_("Ajouter"), type="submit"), + ) + + def clean_username(self): + """Check every username and send it to the cleaned_data['user'] list + + :return: a dictionary of all treated data with the users key added + """ + cleaned_data = super().clean() + users = [] + if cleaned_data.get("username"): + for username in cleaned_data.get("username").split(","): + user = ( + Profile.objects.contactable_members() + .filter(user__username__iexact=username.strip().lower()) + .first() + ) + if user is not None: + users.append(user.user) + if len(users) > 0: + cleaned_data["users"] = users + return cleaned_data + + def is_valid(self): + return super().is_valid() and "users" in self.clean() + + +class RemoveAuthorForm(AuthorForm): + def clean_username(self): + """Check every username and send it to the cleaned_data['user'] list + + :return: a dictionary of all treated data with the users key added + """ + cleaned_data = super(AuthorForm, self).clean() + users = [] + for username in cleaned_data.get("username").split(","): + # we can remove all users (bots inclued) + user = Profile.objects.filter(user__username__iexact=username.strip().lower()).first() + if user is not None: + users.append(user.user) + if len(users) > 0: + cleaned_data["users"] = users + return cleaned_data + + class AddAuthorToContent(LoggedWithReadWriteHability, SingleContentFormViewMixin): must_be_author = True form_class = AuthorForm diff --git a/zds/tutorialv2/views/contents.py b/zds/tutorialv2/views/contents.py index 33da3f42dc..e3289eb7e8 100644 --- a/zds/tutorialv2/views/contents.py +++ b/zds/tutorialv2/views/contents.py @@ -5,7 +5,7 @@ from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin from django.db import transaction -from django.shortcuts import redirect, get_object_or_404 +from django.shortcuts import redirect from django.template.loader import render_to_string from django.urls import reverse from django.utils.decorators import method_decorator @@ -15,11 +15,9 @@ from zds.gallery.mixins import ImageCreateMixin, NotAnImage from zds.gallery.models import Gallery from zds.member.decorator import LoggedWithReadWriteHability -from zds.member.models import Profile from zds.member.utils import get_bot_account from zds.tutorialv2.forms import ( ContentForm, - EditContentLicenseForm, ) from zds.tutorialv2.mixins import ( SingleContentFormViewMixin, @@ -229,52 +227,6 @@ def form_valid(self, form): return super().form_valid(form) -class EditContentLicense(LoginRequiredMixin, SingleContentFormViewMixin): - modal_form = True - model = PublishableContent - form_class = EditContentLicenseForm - success_message_license = _("La licence de la publication a bien été changée.") - success_message_profile_update = _("Votre licence préférée a bien été mise à jour.") - - def get_form_kwargs(self): - kwargs = super().get_form_kwargs() - kwargs["versioned_content"] = self.versioned_object - return kwargs - - def form_valid(self, form): - publishable = self.object - - # Update license in database - publishable.licence = form.cleaned_data["license"] - publishable.update_date = datetime.now() - publishable.save() - - # Update license in repository - self.versioned_object.licence = form.cleaned_data["license"] - sha = self.versioned_object.repo_update_top_container( - publishable.title, - publishable.slug, - self.versioned_object.get_introduction(), - self.versioned_object.get_conclusion(), - "Nouvelle licence ({})".format(form.cleaned_data["license"]), - ) - - # Update relationships in database - publishable.sha_draft = sha - publishable.save() - - messages.success(self.request, EditContentLicense.success_message_license) - - # Update the preferred license of the user - if form.cleaned_data["update_preferred_license"]: - profile = get_object_or_404(Profile, user=self.request.user) - profile.licence = form.cleaned_data["license"] - profile.save() - messages.success(self.request, EditContentLicense.success_message_profile_update) - - return redirect(form.previous_page_url) - - class DeleteContent(LoginRequiredMixin, SingleContentViewMixin, DeleteView): model = PublishableContent http_method_names = ["post"] diff --git a/zds/tutorialv2/views/contributors.py b/zds/tutorialv2/views/contributors.py index 70e75c65db..aa810822d8 100644 --- a/zds/tutorialv2/views/contributors.py +++ b/zds/tutorialv2/views/contributors.py @@ -1,5 +1,10 @@ from collections import OrderedDict +from crispy_forms.bootstrap import StrictButton +from crispy_forms.helper import FormHelper +from crispy_forms.layout import Layout, Field +from django import forms + from django.conf import settings from django.contrib import messages from django.contrib.auth.models import User @@ -12,17 +17,76 @@ from django.utils.translation import gettext_lazy as _ from zds.member.decorator import LoggedWithReadWriteHability +from zds.member.models import Profile from zds.member.utils import get_bot_account from zds.notification.models import NewPublicationSubscription from zds.tutorialv2 import signals -from zds.tutorialv2.forms import ContributionForm, RemoveContributionForm +from zds.tutorialv2.forms import ReviewerTypeModelChoiceField from zds.tutorialv2.mixins import SingleContentFormViewMixin from zds.tutorialv2.models import TYPE_CHOICES_DICT -from zds.tutorialv2.models.database import ContentContribution, PublishableContent +from zds.tutorialv2.models.database import ContentContribution, PublishableContent, ContentContributionRole from zds.mp.utils import send_mp from zds.utils.paginator import ZdSPagingListView +class ContributionForm(forms.Form): + contribution_role = ReviewerTypeModelChoiceField( + label=_("Role"), + required=True, + queryset=ContentContributionRole.objects.order_by("title").all(), + ) + + username = forms.CharField( + label=_("Contributeur"), + required=True, + widget=forms.TextInput( + attrs={"placeholder": _("Pseudo du membre à ajouter."), "data-autocomplete": "{ 'type': 'single' }"} + ), + ) + + comment = forms.CharField( + label=_("Commentaire"), + required=False, + widget=forms.Textarea(attrs={"placeholder": _("Commentaire sur ce contributeur."), "rows": "3"}), + ) + + def __init__(self, content, *args, **kwargs): + self.helper = FormHelper() + self.helper.form_class = "modal modal-flex" + self.helper.form_id = "add-contributor" + self.helper.form_method = "post" + self.helper.form_action = reverse("content:add-contributor", kwargs={"pk": content.pk}) + self.helper.layout = Layout( + Field("username"), + Field("contribution_role"), + Field("comment"), + StrictButton(_("Ajouter"), type="submit", css_class="btn-submit"), + ) + super().__init__(*args, **kwargs) + + def clean_username(self): + cleaned_data = super().clean() + if cleaned_data.get("username"): + username = cleaned_data.get("username") + user = Profile.objects.contactable_members().filter(user__username__iexact=username.strip().lower()).first() + if user is not None: + cleaned_data["user"] = user.user + else: + self._errors["user"] = self.error_class([_("L'utilisateur sélectionné n'existe pas")]) + + if "user" not in cleaned_data: + self._errors["user"] = self.error_class([_("Veuillez renseigner l'utilisateur")]) + + return cleaned_data + + +class RemoveContributionForm(forms.Form): + pk_contribution = forms.CharField( + label=_("Contributeur"), + required=True, + ) + + class AddContributorToContent(LoggedWithReadWriteHability, SingleContentFormViewMixin): must_be_author = True form_class = ContributionForm diff --git a/zds/tutorialv2/views/display/content.py b/zds/tutorialv2/views/display/content.py index f9660b4fe9..be89dea196 100644 --- a/zds/tutorialv2/views/display/content.py +++ b/zds/tutorialv2/views/display/content.py @@ -20,15 +20,15 @@ UnpublicationForm, WarnTypoForm, JsFiddleActivationForm, - EditContentLicenseForm, - EditContentTagsForm, PublicationForm, PickOpinionForm, UnpickOpinionForm, PromoteOpinionToArticleForm, - SearchSuggestionForm, - ContributionForm, ) +from zds.tutorialv2.views.contributors import ContributionForm +from zds.tutorialv2.views.suggestions import SearchSuggestionForm +from zds.tutorialv2.views.licence import EditContentLicenseForm +from zds.tutorialv2.views.tags import EditTagsForm from zds.tutorialv2.mixins import SingleContentDetailViewMixin, SingleOnlineContentDetailViewMixin from zds.tutorialv2.models.database import ( ContentSuggestion, @@ -98,7 +98,7 @@ def get_context_data(self, **kwargs): data_form_convert = data_form_revoke context["form_convert"] = PromoteOpinionToArticleForm(self.versioned_object, initial=data_form_convert) context["form_warn_typo"] = WarnTypoForm(self.versioned_object, self.versioned_object) - context["form_edit_tags"] = EditContentTagsForm(self.versioned_object, self.object) + context["form_edit_tags"] = EditTagsForm(self.versioned_object, self.object) context["form_edit_goals"] = EditGoalsForm(self.object) context["form_edit_labels"] = EditLabelsForm(self.object) context["is_antispam"] = self.object.antispam(self.request.user) diff --git a/zds/tutorialv2/views/help.py b/zds/tutorialv2/views/help.py index bf0c5f3f3a..b8d16c937e 100644 --- a/zds/tutorialv2/views/help.py +++ b/zds/tutorialv2/views/help.py @@ -1,5 +1,6 @@ import json +from django import forms from django.conf import settings from django.db.models import Count, Q from django.http import HttpResponse, JsonResponse @@ -7,7 +8,6 @@ from zds.member.decorator import LoggedWithReadWriteHability from zds.tutorialv2 import signals -from zds.tutorialv2.forms import ToggleHelpForm from zds.tutorialv2.mixins import SingleContentFormViewMixin from zds.tutorialv2.models.database import PublishableContent @@ -16,6 +16,18 @@ from zds.utils.paginator import ZdSPagingListView +class ToggleHelpForm(forms.Form): + help_wanted = forms.CharField() + activated = forms.BooleanField(required=False) + + def clean(self): + clean_data = super().clean() + clean_data["help_wanted"] = HelpWriting.objects.filter(title=(self.data["help_wanted"] or "").strip()).first() + if not clean_data["help_wanted"]: + self.add_error("help_wanted", _("Inconnu")) + return clean_data + + class ContentsWithHelps(ZdSPagingListView): """List all tutorial that needs help, i.e registered as needing at least one HelpWriting or is in beta for more documentation, have a look to ZEP 03 specification (fr)""" diff --git a/zds/tutorialv2/views/licence.py b/zds/tutorialv2/views/licence.py new file mode 100644 index 0000000000..8c33e22e01 --- /dev/null +++ b/zds/tutorialv2/views/licence.py @@ -0,0 +1,119 @@ +from datetime import datetime + +from crispy_forms.bootstrap import StrictButton +from crispy_forms.helper import FormHelper +from crispy_forms.layout import Field, Layout, HTML +from django import forms +from django.conf import settings +from django.contrib import messages +from django.contrib.auth.mixins import LoginRequiredMixin +from django.shortcuts import get_object_or_404, redirect +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ + +from zds.member.models import Profile +from zds.tutorialv2.mixins import SingleContentFormViewMixin +from zds.tutorialv2.models.database import PublishableContent + +from zds.utils.models import Licence + + +class EditContentLicenseForm(forms.Form): + license = forms.ModelChoiceField( + label=_("Licence de votre publication : "), + queryset=Licence.objects.order_by("title").all(), + required=True, + empty_label=_("Choisir une licence"), + error_messages={ + "required": _("Merci de choisir une licence."), + "invalid_choice": _("Merci de choisir une licence valide dans la liste."), + }, + ) + + update_preferred_license = forms.BooleanField( + label=_("Je souhaite utiliser cette licence comme choix par défaut pour mes futures publications."), + required=False, + ) + + def __init__(self, versioned_content, *args, **kwargs): + kwargs["initial"] = {"license": versioned_content.licence} + super().__init__(*args, **kwargs) + + self.helper = FormHelper() + self.helper.form_class = "content-wrapper" + self.helper.form_method = "post" + self.helper.form_id = "edit-license" + self.helper.form_class = "modal modal-flex" + self.helper.form_action = reverse("content:edit-license", kwargs={"pk": versioned_content.pk}) + self.previous_page_url = reverse( + "content:view", kwargs={"pk": versioned_content.pk, "slug": versioned_content.slug} + ) + self._create_layout() + + if "type" in self.initial: + self.helper["type"].wrap(Field, disabled=True) + + def _create_layout(self): + self.helper.layout = Layout( + HTML( + """

{} encourage l'utilisation de licences facilitant le partage, + telles que les licences Creative Commons.

+

Pour choisir la licence de votre publication, aidez-vous de la + présentation + des différentes licences proposées sur le site.

""".format( + settings.ZDS_APP["site"]["literal_name"], + settings.ZDS_APP["site"]["licenses"]["licence_info_title"], + settings.ZDS_APP["site"]["licenses"]["licence_info_link"], + ) + ), + Field("license"), + Field("update_preferred_license"), + StrictButton("Valider", type="submit"), + ) + + +class EditContentLicense(LoginRequiredMixin, SingleContentFormViewMixin): + modal_form = True + model = PublishableContent + form_class = EditContentLicenseForm + success_message_license = _("La licence de la publication a bien été changée.") + success_message_profile_update = _("Votre licence préférée a bien été mise à jour.") + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs["versioned_content"] = self.versioned_object + return kwargs + + def form_valid(self, form): + publishable = self.object + new_licence = form.cleaned_data["license"] + + # Update licence in database + publishable.licence = new_licence + publishable.update_date = datetime.now() + publishable.save() + + # Update licence in repository + self.versioned_object.licence = new_licence + sha = self.versioned_object.repo_update_top_container( + publishable.title, + publishable.slug, + self.versioned_object.get_introduction(), + self.versioned_object.get_conclusion(), + f"Nouvelle licence ({new_licence})", + ) + + # Update relationships in database + publishable.sha_draft = sha + publishable.save() + + messages.success(self.request, EditContentLicense.success_message_license) + + # Update the preferred license of the user + if form.cleaned_data["update_preferred_license"]: + profile = get_object_or_404(Profile, user=self.request.user) + profile.licence = new_licence + profile.save() + messages.success(self.request, EditContentLicense.success_message_profile_update) + + return redirect(form.previous_page_url) diff --git a/zds/tutorialv2/views/editorialization.py b/zds/tutorialv2/views/suggestions.py similarity index 71% rename from zds/tutorialv2/views/editorialization.py rename to zds/tutorialv2/views/suggestions.py index 24b6984a5d..a7b92b5bc0 100644 --- a/zds/tutorialv2/views/editorialization.py +++ b/zds/tutorialv2/views/suggestions.py @@ -1,17 +1,67 @@ +from crispy_forms.bootstrap import StrictButton +from crispy_forms.helper import FormHelper +from crispy_forms.layout import Layout, Field +from django import forms from django.contrib import messages from django.contrib.auth.decorators import login_required from django.contrib.auth.mixins import PermissionRequiredMixin from django.core.exceptions import PermissionDenied from django.shortcuts import get_object_or_404, redirect +from django.urls import reverse from django.utils.decorators import method_decorator from django.utils.translation import gettext_lazy as _ from zds.member.decorator import LoggedWithReadWriteHability, can_write_and_read_now -from zds.tutorialv2.forms import RemoveSuggestionForm, EditContentTagsForm from zds.tutorialv2.mixins import SingleContentFormViewMixin from zds.tutorialv2.models.database import ContentSuggestion, PublishableContent import zds.tutorialv2.signals as signals -from zds.utils import get_current_user + + +class SearchSuggestionForm(forms.Form): + suggestion_pk = forms.CharField( + label="Contenu à suggérer", + required=False, + widget=forms.TextInput(), + ) + excluded_pk = forms.CharField(required=False, widget=forms.HiddenInput(attrs={"class": "excluded_field"})) + + def __init__(self, content, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.fields["suggestion_pk"].widget.attrs.update( + { + "data-autocomplete": '{"type": "multiple_checkbox",' + '"limit": 10,' + '"fieldname": "title",' + '"url": "' + reverse("search:suggestion") + '?q=%s&excluded=%e"}', + "placeholder": "Rechercher un contenu", + } + ) + + self.helper = FormHelper() + self.helper.form_action = reverse("content:add-suggestion", kwargs={"pk": content.pk}) + self.helper.form_class = "modal modal-large" + self.helper.form_id = "add-suggestion" + self.helper.form_method = "post" + + self.helper.layout = Layout( + Field("suggestion_pk"), Field("excluded_pk"), StrictButton(_("Ajouter"), type="submit") + ) + + +class RemoveSuggestionForm(forms.Form): + pk_suggestion = forms.IntegerField( + label=_("Suggestion"), + required=True, + error_messages={"does_not_exist": _("La suggestion sélectionnée n'existe pas.")}, + ) + + def clean_pk_suggestion(self): + pk_suggestion = self.cleaned_data.get("pk_suggestion") + suggestion = ContentSuggestion.objects.filter(id=pk_suggestion).first() + if suggestion is None: + self.add_error("pk_suggestion", self.fields["pk_suggestion"].error_messages["does_not_exist"]) + return pk_suggestion class RemoveSuggestion(PermissionRequiredMixin, SingleContentFormViewMixin): @@ -112,24 +162,3 @@ def post(self, request, *args, **kwargs): return redirect(self.object.get_absolute_url_online()) else: return redirect(self.object.get_absolute_url()) - - -class EditContentTags(LoggedWithReadWriteHability, SingleContentFormViewMixin): - modal_form = True - model = PublishableContent - form_class = EditContentTagsForm - success_message = _("Les tags ont bien été modifiés.") - - def get_form_kwargs(self): - kwargs = super().get_form_kwargs() - kwargs["content"] = self.versioned_object - kwargs["db_content"] = self.object - return kwargs - - def form_valid(self, form): - self.object.tags.clear() - self.object.add_tags(form.cleaned_data["tags"].split(",")) - self.object.save() - messages.success(self.request, EditContentTags.success_message) - signals.tags_management.send(sender=self.__class__, performer=get_current_user(), content=self.object) - return redirect(form.previous_page_url) diff --git a/zds/tutorialv2/views/tags.py b/zds/tutorialv2/views/tags.py new file mode 100644 index 0000000000..bf9d3b9782 --- /dev/null +++ b/zds/tutorialv2/views/tags.py @@ -0,0 +1,86 @@ +from crispy_forms.bootstrap import StrictButton +from crispy_forms.helper import FormHelper +from crispy_forms.layout import Layout, HTML, Field +from django import forms +from django.contrib import messages +from django.shortcuts import redirect +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ + +from zds.member.decorator import LoggedWithReadWriteHability +from zds.tutorialv2 import signals as signals +from zds.tutorialv2.mixins import SingleContentFormViewMixin +from zds.tutorialv2.models.database import PublishableContent +from zds.utils import get_current_user + +from zds.utils.forms import TagValidator + + +class EditTagsForm(forms.Form): + tags = forms.CharField( + label=_("Tags séparés par des virgules (exemple : python,api,web) :"), + max_length=64, + required=False, + widget=forms.TextInput(), + error_messages={"max_length": _("La liste de tags saisie dépasse la longueur maximale autorisée.")}, + ) + + def __init__(self, content, db_content, *args, **kwargs): + self.db_content = db_content + kwargs["initial"] = {"tags": ", ".join(db_content.tags.values_list("title", flat=True))} + super().__init__(*args, **kwargs) + + self.fields["tags"].widget.attrs.update( + { + "data-autocomplete": '{ "type": "multiple", "fieldname": "title", "url": "' + + reverse("api:utils:tags-list") + + '?search=%s" }', + } + ) + + self.helper = FormHelper() + self.helper.form_class = "content-wrapper" + self.helper.form_method = "post" + self.helper.form_id = "edit-tags" + self.helper.form_class = "modal modal-flex" + self.helper.form_action = reverse("content:edit-tags", kwargs={"pk": content.pk}) + self.helper.layout = Layout( + HTML( + """

Les tags permettent de grouper les publications plus finement que les catégories. + Par exemple, vous pouvez indiquer une technologie ou une sous-discipline. + Consultez la page des tags pour voir des exemples.""".format( + reverse("content:tags") + ) + ), + Field("tags"), + StrictButton("Valider", type="submit"), + ) + self.previous_page_url = reverse("content:view", kwargs={"pk": content.pk, "slug": content.slug}) + + def clean_tags(self): + validator = TagValidator() + cleaned_tags = self.cleaned_data.get("tags") + if not validator.validate_raw_string(cleaned_tags): + self.add_error("tags", self.error_class(validator.errors)) + return cleaned_tags + + +class EditTags(LoggedWithReadWriteHability, SingleContentFormViewMixin): + modal_form = True + model = PublishableContent + form_class = EditTagsForm + success_message = _("Les tags ont bien été modifiés.") + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs["content"] = self.versioned_object + kwargs["db_content"] = self.object + return kwargs + + def form_valid(self, form): + self.object.tags.clear() + self.object.add_tags(form.cleaned_data["tags"].split(",")) + self.object.save() + messages.success(self.request, EditTags.success_message) + signals.tags_management.send(sender=self.__class__, performer=get_current_user(), content=self.object) + return redirect(form.previous_page_url)