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

liqd / adhocracy-plus / 18908688697

29 Oct 2025 12:59PM UTC coverage: 44.622% (-44.5%) from 89.135%
18908688697

Pull #2986

github

web-flow
Merge 1dfde8ee7 into 445e1d498
Pull Request #2986: Draft: Speed up Github Ci Tests

3012 of 6750 relevant lines covered (44.62%)

0.45 hits per line

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

54.29
/apps/projects/views.py
1
import itertools
1✔
2

3
from django.contrib import messages
1✔
4
from django.contrib.auth import get_user_model
1✔
5
from django.contrib.sites.shortcuts import get_current_site
1✔
6
from django.shortcuts import get_object_or_404
1✔
7
from django.shortcuts import redirect
1✔
8
from django.urls import reverse_lazy
1✔
9
from django.utils.functional import cached_property
1✔
10
from django.utils.translation import gettext_lazy as _
1✔
11
from django.utils.translation import ngettext
1✔
12
from django.views import generic
1✔
13
from rules.contrib.views import LoginRequiredMixin
1✔
14
from rules.contrib.views import PermissionRequiredMixin
1✔
15

16
from adhocracy4.dashboard import mixins as a4dashboard_mixins
1✔
17
from adhocracy4.dashboard import signals as a4dashboard_signals
1✔
18
from adhocracy4.dashboard.components.forms.views import ProjectComponentFormView
1✔
19
from adhocracy4.modules import models as module_models
1✔
20
from adhocracy4.projects import models as project_models
1✔
21
from adhocracy4.projects.mixins import DisplayProjectOrModuleMixin
1✔
22
from adhocracy4.projects.mixins import PhaseDispatchMixin
1✔
23
from adhocracy4.projects.mixins import ProjectMixin
1✔
24
from adhocracy4.projects.mixins import ProjectModuleDispatchMixin
1✔
25
from apps.projects.models import ProjectInsight
1✔
26

27
from . import dashboard
1✔
28
from . import forms
1✔
29
from . import models
1✔
30

31
User = get_user_model()
1✔
32

33

34
class ParticipantInviteDetailView(generic.DetailView):
1✔
35
    model = models.ParticipantInvite
1✔
36
    slug_field = "token"
1✔
37
    slug_url_kwarg = "invite_token"
1✔
38

39
    def dispatch(self, request, invite_token, *args, **kwargs):
1✔
40
        if request.user.is_authenticated:
×
41
            return redirect(
×
42
                "project-participant-invite-update",
43
                organisation_slug=self.get_object().project.organisation.slug,
44
                invite_token=invite_token,
45
            )
46
        else:
47
            return super().dispatch(request, *args, **kwargs)
×
48

49

50
class ParticipantInviteUpdateView(LoginRequiredMixin, generic.UpdateView):
1✔
51
    model = models.ParticipantInvite
1✔
52
    form_class = forms.ParticipantInviteForm
1✔
53
    slug_field = "token"
1✔
54
    slug_url_kwarg = "invite_token"
1✔
55

56
    def form_valid(self, form):
1✔
57
        if form.is_accepted():
×
58
            form.instance.accept(self.request.user)
×
59
            return redirect(form.instance.project.get_absolute_url())
×
60
        else:
61
            form.instance.reject()
×
62
            return redirect("/")
×
63

64

65
class ModeratorInviteDetailView(generic.DetailView):
1✔
66
    model = models.ModeratorInvite
1✔
67
    slug_field = "token"
1✔
68
    slug_url_kwarg = "invite_token"
1✔
69

70
    def dispatch(self, request, invite_token, *args, **kwargs):
1✔
71
        if request.user.is_authenticated:
×
72
            return redirect(
×
73
                "project-moderator-invite-update",
74
                organisation_slug=self.get_object().project.organisation.slug,
75
                invite_token=invite_token,
76
            )
77
        else:
78
            return super().dispatch(request, *args, **kwargs)
×
79

80

81
class ModeratorInviteUpdateView(LoginRequiredMixin, generic.UpdateView):
1✔
82
    model = models.ModeratorInvite
1✔
83
    form_class = forms.ModeratorInviteForm
1✔
84
    slug_field = "token"
1✔
85
    slug_url_kwarg = "invite_token"
1✔
86

87
    def form_valid(self, form):
1✔
88
        if form.is_accepted():
×
89
            form.instance.accept(self.request.user)
×
90
            return redirect(form.instance.project.get_absolute_url())
×
91
        else:
92
            form.instance.reject()
×
93
            return redirect("/")
×
94

95

96
class AbstractProjectUserInviteListView(
1✔
97
    ProjectMixin,
98
    a4dashboard_mixins.DashboardBaseMixin,
99
    a4dashboard_mixins.DashboardComponentMixin,
100
    generic.base.TemplateResponseMixin,
101
    generic.edit.FormMixin,
102
    generic.detail.SingleObjectMixin,
103
    generic.edit.ProcessFormView,
104
):
105
    form_class = forms.InviteUsersFromEmailForm
1✔
106
    invite_model = None
1✔
107

108
    def get(self, request, *args, **kwargs):
1✔
109
        self.object = self.get_object()
×
110
        return super().get(request, *args, **kwargs)
×
111

112
    def post(self, request, *args, **kwargs):
1✔
113
        self.object = self.get_object()
×
114
        if "submit_action" in request.POST:
×
115
            if request.POST["submit_action"] == "remove_user":
×
116
                pk = int(request.POST["user_pk"])
×
117
                user = get_object_or_404(User, pk=pk)
×
118
                related_users = getattr(self.object, self.related_users_field)
×
119
                related_users.remove(user)
×
120
                messages.success(request, self.success_message_removal)
×
121
            elif request.POST["submit_action"] == "remove_invite":
×
122
                pk = int(request.POST["invite_pk"])
×
123
                if self.invite_model.objects.filter(pk=pk).exists():
×
124
                    invite = self.invite_model.objects.get(pk=pk)
×
125
                    invite.delete()
×
126
                    messages.success(request, _("Invitation succesfully removed."))
×
127
                else:
128
                    messages.info(
×
129
                        request, _("Invitation was already accepted or removed.")
130
                    )
131

132
            response = redirect(self.get_success_url())
×
133
        else:
134
            response = super().post(request, *args, **kwargs)
×
135

136
        self._send_component_updated_signal()
×
137
        return response
×
138

139
    def filter_existing(self, emails):
1✔
140
        related_users = getattr(self.object, self.related_users_field)
×
141
        related_emails = [u.email for u in related_users.all()]
×
142
        existing = []
×
143
        filtered_emails = []
×
144
        for email in emails:
×
145
            if email in related_emails:
×
146
                existing.append(email)
×
147
            else:
148
                filtered_emails.append(email)
×
149
        return filtered_emails, existing
×
150

151
    def filter_pending(self, emails):
1✔
152
        pending = []
×
153
        filtered_emails = []
×
154
        for email in emails:
×
155
            if self.invite_model.objects.filter(
×
156
                email=email, project=self.project
157
            ).exists():
158
                pending.append(email)
×
159
            else:
160
                filtered_emails.append(email)
×
161
        return filtered_emails, pending
×
162

163
    def form_valid(self, form):
1✔
164
        emails = list(
×
165
            set(
166
                itertools.chain(
167
                    form.cleaned_data["add_users"],
168
                    form.cleaned_data["add_users_upload"],
169
                )
170
            )
171
        )
172

173
        emails, existing = self.filter_existing(emails)
×
174
        if existing:
×
175
            messages.error(
×
176
                self.request,
177
                _("Following users already accepted an invitation: ")
178
                + ", ".join(existing),
179
            )
180

181
        emails, pending = self.filter_pending(emails)
×
182
        if pending:
×
183
            messages.error(
×
184
                self.request,
185
                _("Following users are already invited: ") + ", ".join(pending),
186
            )
187

188
        for email in emails:
×
189
            self.invite_model.objects.invite(
×
190
                creator=self.request.user,
191
                project=self.project,
192
                email=email,
193
                site=get_current_site(self.request),
194
            )
195

196
        messages.success(
×
197
            self.request,
198
            ngettext(
199
                self.success_message[0], self.success_message[1], len(emails)
200
            ).format(len(emails)),
201
        )
202

203
        return redirect(self.get_success_url())
×
204

205
    def get_form_kwargs(self):
1✔
206
        kwargs = super().get_form_kwargs()
×
207
        kwargs["labels"] = (self.add_user_field_label, self.add_user_upload_field_label)
×
208
        return kwargs
×
209

210
    def _send_component_updated_signal(self):
1✔
211
        a4dashboard_signals.project_component_updated.send(
×
212
            sender=self.component.__class__,
213
            project=self.project,
214
            component=self.component,
215
            user=self.request.user,
216
        )
217

218

219
class DashboardProjectModeratorsView(AbstractProjectUserInviteListView):
1✔
220
    model = project_models.Project
1✔
221
    slug_url_kwarg = "project_slug"
1✔
222
    template_name = "a4_candy_projects/project_moderators.html"
1✔
223
    permission_required = "a4projects.change_project"
1✔
224
    menu_item = "project"
1✔
225

226
    related_users_field = "moderators"
1✔
227
    add_user_field_label = _("Invite moderators via email")
1✔
228
    add_user_upload_field_label = _("Invite moderators via file upload")
1✔
229
    success_message = (_("{} moderator invited."), _("{} moderators invited."))
1✔
230
    success_message_removal = _("Moderator successfully removed.")
1✔
231

232
    invite_model = models.ModeratorInvite
1✔
233

234
    def get_permission_object(self):
1✔
235
        return self.project
×
236

237

238
class DashboardProjectParticipantsView(AbstractProjectUserInviteListView):
1✔
239
    model = project_models.Project
1✔
240
    slug_url_kwarg = "project_slug"
1✔
241
    template_name = "a4_candy_projects/project_participants.html"
1✔
242
    permission_required = "a4projects.change_project"
1✔
243
    menu_item = "project"
1✔
244

245
    related_users_field = "participants"
1✔
246
    add_user_field_label = _("Invite users via email")
1✔
247
    add_user_upload_field_label = _("Invite users via file upload")
1✔
248
    success_message = (_("{} participant invited."), _("{} participants invited."))
1✔
249
    success_message_removal = _("Participant successfully removed.")
1✔
250

251
    invite_model = models.ParticipantInvite
1✔
252

253
    def get_permission_object(self):
1✔
254
        return self.project
×
255

256

257
class ProjectDeleteView(PermissionRequiredMixin, generic.DeleteView):
1✔
258
    model = project_models.Project
1✔
259
    permission_required = "a4projects.delete_project"
1✔
260
    http_method_names = ["post"]
1✔
261
    success_message = _("Project '%(name)s' was deleted successfully.")
1✔
262

263
    def get_success_url(self):
1✔
264
        return reverse_lazy(
×
265
            "a4dashboard:project-list",
266
            kwargs={"organisation_slug": self.get_object().organisation.slug},
267
        )
268

269
    def form_valid(self, request, *args, **kwargs):
1✔
270
        obj = self.get_object()
×
271
        messages.success(self.request, self.success_message % obj.__dict__)
×
272
        return super().form_valid(request, *args, **kwargs)
×
273

274

275
class ProjectDetailView(
1✔
276
    PermissionRequiredMixin,
277
    ProjectModuleDispatchMixin,
278
    DisplayProjectOrModuleMixin,
279
):
280
    model = models.Project
1✔
281
    permission_required = "a4projects.view_project"
1✔
282
    template_name = "a4_candy_projects/project_detail.html"
1✔
283

284
    def get_permission_object(self):
1✔
285
        return self.project
×
286

287
    @cached_property
1✔
288
    def is_project_view(self):
1✔
289
        return self.get_current_modules()
×
290

291
    @property
1✔
292
    def raise_exception(self):
1✔
293
        return self.request.user.is_authenticated
×
294

295

296
class ProjectResultInsightComponentFormView(ProjectComponentFormView):
1✔
297
    def get_context_data(self, **kwargs):
1✔
298
        context = super().get_context_data(**kwargs)
×
299
        ProjectInsight.update_context(
×
300
            project=self.project, context=context, dashboard=True
301
        )
302

303
        if self.request.POST:
×
304
            context["insight_form"] = dashboard.ProjectInsightForm(
×
305
                data=self.request.POST, instance=self.project.insight
306
            )
307
        else:
308
            context["insight_form"] = dashboard.ProjectInsightForm(
×
309
                instance=self.project.insight
310
            )
311

312
        return context
×
313

314
    def form_valid(self, form):
1✔
315
        context = self.get_context_data()
×
316
        insight_form = context["insight_form"]
×
317
        if insight_form.is_valid():
×
318
            insight_form.save()
×
319
        return super().form_valid(form)
×
320

321

322
class ModuleDetailView(PermissionRequiredMixin, PhaseDispatchMixin):
1✔
323
    model = module_models.Module
1✔
324
    permission_required = "a4projects.view_project"
1✔
325
    slug_url_kwarg = "module_slug"
1✔
326

327
    @cached_property
1✔
328
    def project(self):
1✔
329
        return self.module.project
×
330

331
    @cached_property
1✔
332
    def module(self):
1✔
333
        return self.get_object()
×
334

335
    def get_permission_object(self):
1✔
336
        return self.project
×
337

338
    def get_context_data(self, **kwargs):
1✔
339
        """Append project and module to the template context."""
340
        if "project" not in kwargs:
×
341
            kwargs["project"] = self.project
×
342
        if "module" not in kwargs:
×
343
            kwargs["module"] = self.module
×
344
        return super().get_context_data(**kwargs)
×
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