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

zestedesavoir / zds-site / 22937476236

28 Feb 2026 03:19PM UTC coverage: 89.451%. Remained the same
22937476236

push

github

philippemilink
Limite la longueur du commentaire pour l'ajout d'un contributeur

3099 of 4138 branches covered (74.89%)

17094 of 19110 relevant lines covered (89.45%)

1.91 hits per line

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

78.57
/zds/tutorialv2/views/contributors.py
1
from collections import OrderedDict
3✔
2

3
from crispy_forms.bootstrap import StrictButton
3✔
4
from crispy_forms.helper import FormHelper
3✔
5
from crispy_forms.layout import Field, Layout
3✔
6
from django import forms
3✔
7
from django.conf import settings
3✔
8
from django.contrib import messages
3✔
9
from django.contrib.auth.models import User
3✔
10
from django.http import Http404
3✔
11
from django.shortcuts import get_object_or_404, redirect
3✔
12
from django.template.loader import render_to_string
3✔
13
from django.urls import reverse
3✔
14
from django.utils.translation import gettext_lazy as _
3✔
15

16
from zds.member.decorator import LoggedWithReadWriteHability
3✔
17
from zds.member.models import Profile
3✔
18
from zds.member.utils import get_bot_account
3✔
19
from zds.mp.utils import send_mp
3✔
20
from zds.notification.models import NewPublicationSubscription
3✔
21
from zds.tutorialv2 import signals
3✔
22
from zds.tutorialv2.forms import ReviewerTypeModelChoiceField
3✔
23
from zds.tutorialv2.mixins import SingleContentFormViewMixin
3✔
24
from zds.tutorialv2.models import TYPE_CHOICES_DICT
3✔
25
from zds.tutorialv2.models.database import ContentContribution, ContentContributionRole, PublishableContent
3✔
26
from zds.utils.paginator import ZdSPagingListView
3✔
27

28

29
class ContributionForm(forms.Form):
3✔
30
    contribution_role = ReviewerTypeModelChoiceField(
3✔
31
        label=_("Role"),
32
        required=True,
33
        queryset=ContentContributionRole.objects.order_by("title").all(),
34
    )
35

36
    username = forms.CharField(
3✔
37
        label=_("Contributeur"),
38
        required=True,
39
        widget=forms.TextInput(
40
            attrs={"placeholder": _("Pseudo du membre à ajouter."), "data-autocomplete": "{ 'type': 'single' }"}
41
        ),
42
    )
43

44
    comment = forms.CharField(
3✔
45
        label=_("Commentaire"),
46
        required=False,
47
        max_length=ContentContribution._meta.get_field("comment").max_length,
48
        widget=forms.Textarea(attrs={"placeholder": _("Commentaire sur ce contributeur."), "rows": "3"}),
49
    )
50

51
    def __init__(self, content, *args, **kwargs):
3✔
52
        self.helper = FormHelper()
3✔
53
        self.helper.form_class = "modal modal-flex"
3✔
54
        self.helper.form_id = "add-contributor"
3✔
55
        self.helper.form_method = "post"
3✔
56
        self.helper.form_action = reverse("content:add-contributor", kwargs={"pk": content.pk})
3✔
57
        self.helper.layout = Layout(
3✔
58
            Field("username"),
59
            Field("contribution_role"),
60
            Field("comment"),
61
            StrictButton(_("Ajouter"), type="submit", css_class="btn-submit"),
62
        )
63
        super().__init__(*args, **kwargs)
3✔
64

65
    def clean_username(self):
3✔
66
        cleaned_data = super().clean()
1✔
67
        if cleaned_data.get("username"):
1!
68
            username = cleaned_data.get("username")
1✔
69
            user = Profile.objects.contactable_members().filter(user__username__iexact=username.strip().lower()).first()
1✔
70
            if user is not None:
1✔
71
                cleaned_data["user"] = user.user
1✔
72
            else:
73
                self._errors["user"] = self.error_class([_("L'utilisateur sélectionné n'existe pas")])
1✔
74

75
        if "user" not in cleaned_data:
1✔
76
            self._errors["user"] = self.error_class([_("Veuillez renseigner l'utilisateur")])
1✔
77

78
        return cleaned_data
1✔
79

80

81
class AddContributorToContent(LoggedWithReadWriteHability, SingleContentFormViewMixin):
3✔
82
    must_be_author = True
3✔
83
    form_class = ContributionForm
3✔
84
    authorized_for_staff = True
3✔
85

86
    def get_form_kwargs(self):
3✔
87
        kwargs = super().get_form_kwargs()
1✔
88
        kwargs.update({"content": self.object})
1✔
89
        return kwargs
1✔
90

91
    def get(self, request, *args, **kwargs):
3✔
92
        content = self.get_object()
×
93
        url = "content:find-{}".format("tutorial" if content.is_tutorial() else content.type.lower())
×
94
        return redirect(url, self.request.user)
×
95

96
    def form_valid(self, form):
3✔
97
        bot = get_bot_account()
1✔
98
        all_authors_pk = [author.pk for author in self.object.authors.all()]
1✔
99
        user = form.cleaned_data["user"]
1✔
100
        if user.pk in all_authors_pk:
1✔
101
            messages.error(self.request, _("Un auteur ne peut pas être désigné comme contributeur."))
1✔
102
            return redirect(self.object.get_absolute_url())
1✔
103
        else:
104
            contribution_role = form.cleaned_data.get("contribution_role")
1✔
105
            comment = form.cleaned_data.get("comment")
1✔
106
            if ContentContribution.objects.filter(
1✔
107
                user=user, contribution_role=contribution_role, content=self.object
108
            ).exists():
109
                messages.error(
1✔
110
                    self.request,
111
                    _(
112
                        "Ce membre fait déjà partie des "
113
                        'contributeurs à la publication avec pour rôle "{}"'.format(contribution_role.title)
114
                    ),
115
                )
116
                return redirect(self.object.get_absolute_url())
1✔
117

118
            contribution = ContentContribution(
1✔
119
                user=user, contribution_role=contribution_role, comment=comment, content=self.object
120
            )
121
            contribution.save()
1✔
122
            url_index = reverse(self.object.type.lower() + ":find-" + self.object.type.lower(), args=[user.pk])
1✔
123
            send_mp(
1✔
124
                bot,
125
                [user],
126
                _("Contribution à la publication"),
127
                self.versioned_object.title,
128
                render_to_string(
129
                    "tutorialv2/messages/add_contribution_pm.md",
130
                    {
131
                        "content": self.object,
132
                        "url": self.object.get_absolute_url(),
133
                        "index": url_index,
134
                        "user": user.username,
135
                        "role": contribution.contribution_role.title,
136
                    },
137
                ),
138
                send_by_mail=True,
139
                leave=True,
140
            )
141
            signals.contributors_management.send(
1✔
142
                sender=self.__class__, content=self.object, performer=self.request.user, contributor=user, action="add"
143
            )
144
            self.success_url = self.object.get_absolute_url()
1✔
145

146
            return super().form_valid(form)
1✔
147

148
    def form_invalid(self, form):
3✔
149
        messages.error(self.request, form.errors)
1✔
150
        self.success_url = self.object.get_absolute_url()
1✔
151
        return super().form_valid(form)
1✔
152

153

154
class RemoveContributionForm(forms.Form):
3✔
155
    pk_contribution = forms.CharField(
3✔
156
        label=_("Contributeur"),
157
        required=True,
158
    )
159

160

161
class RemoveContributorFromContent(LoggedWithReadWriteHability, SingleContentFormViewMixin):
3✔
162
    form_class = RemoveContributionForm
3✔
163
    must_be_author = True
3✔
164
    authorized_for_staff = True
3✔
165

166
    def form_valid(self, form):
3✔
167
        contribution = get_object_or_404(ContentContribution, pk=form.cleaned_data["pk_contribution"])
1✔
168
        user = contribution.user
1✔
169
        contribution.delete()
1✔
170
        signals.contributors_management.send(
1✔
171
            sender=self.__class__, content=self.object, performer=self.request.user, contributor=user, action="remove"
172
        )
173
        messages.success(
1✔
174
            self.request,
175
            _("Vous avez enlevé {} de la liste des contributeurs de cette publication.").format(user.username),
176
        )
177
        self.success_url = self.object.get_absolute_url()
1✔
178

179
        return super().form_valid(form)
1✔
180

181
    def form_invalid(self, form):
3✔
182
        messages.error(self.request, _("Les contributeurs sélectionnés n'existent pas."))
1✔
183
        self.success_url = self.object.get_absolute_url()
1✔
184
        return super().form_valid(form)
1✔
185

186

187
class ContentOfContributors(ZdSPagingListView):
3✔
188
    type = "ALL"
3✔
189
    context_object_name = "contribution_contents"
3✔
190
    paginate_by = settings.ZDS_APP["content"]["content_per_page"]
3✔
191
    template_name = "tutorialv2/contributions.html"
3✔
192
    model = PublishableContent
3✔
193

194
    sorts = OrderedDict(
3✔
195
        [
196
            ("creation", [lambda q: q.order_by("content__creation_date"), _("Par date de création")]),
197
            ("abc", [lambda q: q.order_by("content__title"), _("Par ordre alphabétique")]),
198
            ("modification", [lambda q: q.order_by("-content__update_date"), _("Par date de dernière modification")]),
199
        ]
200
    )
201
    sort = ""
3✔
202
    filter = ""
3✔
203
    user = None
3✔
204

205
    def dispatch(self, request, *args, **kwargs):
3✔
206
        self.user = get_object_or_404(User, username=self.kwargs["username"])
×
207
        return super().dispatch(request, *args, **kwargs)
×
208

209
    def get_queryset(self):
3✔
210
        if self.type == "ALL":
×
211
            queryset = ContentContribution.objects.filter(user__pk=self.user.pk, content__sha_public__isnull=False)
×
212
        elif self.type in list(TYPE_CHOICES_DICT.keys()):
×
213
            queryset = ContentContribution.objects.filter(
×
214
                user__pk=self.user.pk, content__sha_public__isnull=False, content__type=self.type
215
            )
216
        else:
217
            raise Http404("Ce type de contenu est inconnu dans le système.")
×
218

219
        # Sort.
220
        if "sort" in self.request.GET and self.request.GET["sort"].lower() in self.sorts:
×
221
            self.sort = self.request.GET["sort"]
×
222
        elif not self.sort:
×
223
            self.sort = "abc"
×
224
        queryset = self.sorts[self.sort.lower()][0](queryset)
×
225
        return queryset
×
226

227
    def get_context_data(self, **kwargs):
3✔
228
        context = super().get_context_data(**kwargs)
×
229
        context["sorts"] = []
×
230
        context["sort"] = self.sort.lower()
×
231
        context["subscriber_count"] = NewPublicationSubscription.objects.get_subscriptions(self.user).count()
×
232
        context["type"] = self.type.lower()
×
233
        contents = list(self.object_list.values_list("content", flat=True).distinct())
×
234

235
        queryset = PublishableContent.objects.filter(pk__in=contents)
×
236
        # prefetch:
237
        queryset = (
×
238
            queryset.prefetch_related("authors")
239
            .prefetch_related("subcategory")
240
            .select_related("licence")
241
            .select_related("image")
242
        )
243

244
        context["contribution_tutorials"] = queryset.filter(type="TUTORIAL").all()
×
245
        context["contribution_articles"] = queryset.filter(type="ARTICLE").all()
×
246

247
        context["usr"] = self.user
×
248
        for sort in list(self.sorts.keys()):
×
249
            context["sorts"].append({"key": sort, "text": self.sorts[sort][1]})
×
250
        return context
×
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