mixins.py 10.2 KB
Newer Older
1
from __future__ import annotations
2
from django.utils.translation import gettext as _
Enrico Zini's avatar
Enrico Zini committed
3
from django.core.exceptions import PermissionDenied
Enrico Zini's avatar
Enrico Zini committed
4
from django.shortcuts import get_object_or_404
5
from django.urls import reverse
6
from django.utils.timezone import now
7
from django.utils.html import format_html, mark_safe, escape
Enrico Zini's avatar
Enrico Zini committed
8
from backend.mixins import VisitPersonMixin
9
from nmlayout.mixins import NavLink
Enrico Zini's avatar
Enrico Zini committed
10
11
from . import models as pmodels

Mattia Rizzolo's avatar
Mattia Rizzolo committed
12

13
14
15
16
17
18
19
20
21
22
def compute_process_status(process, visitor, visit_perms=None):
    """
    Return a dict with the process status:
    {
        "requirements_ok": [list of Requirement],
        "requirements_missing": [list of Requirement],
        "log_first": Log,
        "log_last": Log,
    }
    """
23
24
    from process.models import REQUIREMENT_TYPES_DICT
    from process.permissions import ProcessVisitorPermissions
25
26
27
28
29
    # person_perms = visit_perms
    if visit_perms and isinstance(visit_perms, ProcessVisitorPermissions):
        process_perms = visit_perms
    else:
        process_perms = None
30
31
    rok = []
    rnok = []
32
    rapproval = None
33
34
    requirements = {}
    for r in process.requirements.all():
35
36
        if r.type == "approval":
            rapproval = r
37
38
39
40
41
42
43
44
45
46
47
48
49
        if r.approved_by:
            rok.append(r)
        else:
            rnok.append(r)
        requirements[r.type] = r

    # Compute the list of advocates
    adv = requirements.get("advocate", None)
    advocates = set()
    if adv is not None:
        for s in adv.statements.all():
            advocates.add(s.uploaded_by)

50
    view_private_logs = False
51
    if not visitor.is_authenticated:
52
        pass
53
    elif visitor.is_superuser:
54
55
56
57
58
59
        view_private_logs = True
    elif process_perms is None:
        process_perms = process.permissions_of(visitor)
        if "view_private_log" in process_perms:
            view_private_logs = True

Mattia Rizzolo's avatar
Mattia Rizzolo committed
60
61
    log = process.log.order_by("logdate").select_related(
        "changed_by", "requirement")
62
    if not view_private_logs:
63
        from django.db.models import Q
64
65
66
67
        q = Q(is_public=True)
        if visitor.is_authenticated:
            q = q | Q(changed_by=visitor)
        log = log.filter(q)
68

69
70
    log = list(log)

71
72
    am_assignment = process.current_am_assignment

Enrico Zini's avatar
Enrico Zini committed
73
    if process.closed:
74
75
76
77
78
79
        summary = _("Closed")
        if process.rt_ticket is not None:
            rt_link = mark_safe(f'<a href="https://rt.debian.org/{process.rt_ticket}">#{process.rt_ticket}</a>')
            summary_html = format_html(_("Closed. RT ticket: {}"), rt_link)
        else:
            summary_html = escape(summary)
80
81
82
83
84
85
86
87
88
    elif process.approved_by:
        summary_bits = []

        # A process should be frozen before its approval!!
        if not process.frozen_by:
            summary_bits.append(_("Process should have been frozen before approval!!"))

        # If a RT ticket is opened or if DAM has approved, then it's approved, end of story.
        if process.rt_ticket is not None or process.has_dam_approval():
89
            summary = _("Approved")
90
91
            summary_bits.append(_("Approved by {} on {}."))
        # FD approval here
92
        else:
93
94
95
96
            (latest_comment_date, first_next_statement_date) = process.latest_approval_activities()
            # A DD comment in the approval statement stops the clock
            if (latest_comment_date is not None and
               (first_next_statement_date is None or
Enrico Zini's avatar
Enrico Zini committed
97
                   latest_comment_date > first_next_statement_date)):
98
99
100
101
102
103
                summary = _("Approval needs review")
                summary_bits.append(_("A review of {}'s approval on {} is needed."))
            else:
                summary = _("FD Approved")
                summary_bits.append(_("FD Approved by {} on {}."))

104
105
        approved_by_link = process.approved_by.a_link
        approved_time = process.approved_time.strftime("%Y-%m-%d")
106
107
108
109
110

        summary_text = " ".join(summary_bits)

        if process.rt_ticket is not None:
            rt_link = mark_safe(f'<a href="https://rt.debian.org/{process.rt_ticket}">#{process.rt_ticket}</a>')
111
            summary_text += " " + _("RT ticket: {}")
112
113
114
115
116
117
118
119
            summary_html = format_html(summary_text, approved_by_link, approved_time, rt_link)
        else:
            summary_html = format_html(summary_text, approved_by_link, approved_time)
    elif process.frozen_by:
        frozen_by_link = process.frozen_by.a_link
        frozen_time = process.frozen_time.strftime("%Y-%m-%d")
        summary = _("Frozen for review")
        summary_html = format_html(_("Frozen by {} on {}"), frozen_by_link, frozen_time)
120
121
    elif not rnok or rnok == [rapproval]:
        if rapproval.approved_by:
122
            summary = _("Waiting for DAM feedback")
123
124
125
126
127
            summary_html = escape(
                    _("Inconsistent state: has approval statement but is not marked frozen for review or approved"))
        else:
            summary = _("Waiting for review")
            summary_html = escape(summary)
128
129
    elif am_assignment is not None:
        if am_assignment.paused:
130
            summary = _("AM Hold")
131
        else:
132
133
            summary = _("AM")
        summary_html = escape(summary)
134
    else:
135
136
        summary = _("Collecting requirements")
        summary_html = escape(summary)
137

138
139
    return {
        "requirements": requirements,
Enrico Zini's avatar
Enrico Zini committed
140
141
        "requirements_sorted": sorted(
            list(requirements.values()), key=lambda x: REQUIREMENT_TYPES_DICT[x.type].sort_order),
142
143
144
145
146
        "requirements_ok": sorted(rok, key=lambda x: REQUIREMENT_TYPES_DICT[x.type].sort_order),
        "requirements_missing": sorted(rnok, key=lambda x: REQUIREMENT_TYPES_DICT[x.type].sort_order),
        "log_first": log[0] if log else None,
        "log_last": log[-1] if log else None,
        "log": log,
Enrico Zini's avatar
Enrico Zini committed
147
        "advocates": sorted(advocates, key=lambda x: x.ldap_fields.uid),
148
        "summary": summary,
149
        "summary_html": summary_html,
150
151
    }

Enrico Zini's avatar
Enrico Zini committed
152
153
154

class VisitProcessMixin(VisitPersonMixin):
    """
155
156
    Visit a person process. Adds self.person, self.process and
    self.visit_perms with the permissions the visitor has over the person
Enrico Zini's avatar
Enrico Zini committed
157
    """
Mattia Rizzolo's avatar
Mattia Rizzolo committed
158

Enrico Zini's avatar
Enrico Zini committed
159
160
161
    def get_person(self):
        return self.process.person

162
    def get_visit_perms(self):
163
        return self.process.permissions_of(self.request.user)
Enrico Zini's avatar
Enrico Zini committed
164

165
166
    def get_process_menu_entries(self):
        res = super().get_process_menu_entries()
167
        if self.request.user.is_authenticated:
168
            if self.request.user.is_superuser:
169
                res.append(NavLink(self.process.get_admin_url(), _("Admin process"), "microchip"))
170
171
172
173
            if self.process.applying_for == "dd_e":
                res.append(NavLink(
                    reverse("mia:wat_ping", kwargs={"key": self.person.lookup_key}), _("Resend WAT ping"), "heartbeat"))
                res.append(NavLink(reverse("mia:wat_remove", kwargs={"pk": self.process.pk}), _("WAT remove")))
174
175
            if "proc_close" in self.visit_perms:
                res.append(NavLink(
176
                    reverse("process_cancel", kwargs={"pk": self.process.pk}), _("Cancel"), "remove"))
177
178
        return res

179
    def get_process(self):
180
        return get_object_or_404(pmodels.Process.objects.select_related("person"), pk=self.kwargs["pk"])
181
182
183

    def load_objects(self):
        self.process = self.get_process()
184
        super().load_objects()
Enrico Zini's avatar
Enrico Zini committed
185
186

    def get_context_data(self, **kw):
187
        ctx = super().get_context_data(**kw)
Enrico Zini's avatar
Enrico Zini committed
188
        ctx["process"] = self.process
Enrico Zini's avatar
Enrico Zini committed
189
        ctx["wikihelp"] = "https://wiki.debian.org/nm.debian.org/Process"
Enrico Zini's avatar
Enrico Zini committed
190
        return ctx
191
192

    def compute_process_status(self):
193
        return compute_process_status(self.process, self.request.user, self.visit_perms)
194
195
196
197
198


class RequirementMixin(VisitProcessMixin):
    # Requirement type. If not found, check self.kwargs["type"]
    type = None
199
    wikihelp = {
200
201
202
203
204
        "intent": "https://wiki.debian.org/DebianDeveloper/JoinTheProject/NewMember/IntentStep",
        "sc_dmup": "https://wiki.debian.org/DebianDeveloper/JoinTheProject/NewMember/CommitmentsStep",
        "advocate": "https://wiki.debian.org/DebianDeveloper/JoinTheProject/NewMember/AdvocacyStep",
        "keycheck": "https://wiki.debian.org/DebianDeveloper/JoinTheProject/NewMember/IdentificationStep",
        "am_ok": "https://wiki.debian.org/DebianDeveloper/JoinTheProject/NewMember/ApplicationManagerStep",
205
    }
206
207
208
209
210
211
212
213
214
215
216

    def get_requirement_type(self):
        if self.type:
            return self.type
        else:
            return self.kwargs.get("type", None)

    def get_requirement(self):
        process = get_object_or_404(pmodels.Process, pk=self.kwargs["pk"])
        return get_object_or_404(pmodels.Requirement, process=process, type=self.get_requirement_type())

217
218
    def get_process_menu_entries(self):
        res = super().get_process_menu_entries()
219
        if self.request.user.is_staff:
220
            res.append(NavLink(self.requirement.get_admin_url(), _("Admin requirement"), "microchip"))
221
222
        return res

223
    def get_visit_perms(self):
224
        return self.requirement.permissions_of(self.request.user)
225
226
227
228
229
230
231
232
233
234

    def get_process(self):
        return self.requirement.process

    def load_objects(self):
        self.requirement = self.get_requirement()
        super(RequirementMixin, self).load_objects()

    def get_context_data(self, **kw):
        ctx = super(RequirementMixin, self).get_context_data(**kw)
235
        ctx["now"] = now()
236
237
238
        ctx["requirement"] = self.requirement
        ctx["type"] = self.requirement.type
        ctx["type_desc"] = pmodels.REQUIREMENT_TYPES_DICT[self.requirement.type].desc
Mattia Rizzolo's avatar
Mattia Rizzolo committed
239
240
        ctx["explain_template"] = "process/explain_statement_" + \
            self.requirement.type + ".html"
241
        ctx["status"] = self.requirement.compute_status()
242
        ctx["wikihelp"] = self.wikihelp.get(self.requirement.type)
243
244
245
246
247
248
249
        return ctx


class StatementMixin(RequirementMixin):
    def load_objects(self):
        super(StatementMixin, self).load_objects()
        if "st" in self.kwargs:
Mattia Rizzolo's avatar
Mattia Rizzolo committed
250
251
            self.statement = get_object_or_404(
                pmodels.Statement, pk=self.kwargs["st"])
252
253
254
255
256
257
258
            if self.statement.requirement != self.requirement:
                raise PermissionDenied
        else:
            self.statement = None

    def get_context_data(self, **kw):
        ctx = super(StatementMixin, self).get_context_data(**kw)
259
260
        ctx["fpr"] = self.request.user.fpr
        ctx["keyid"] = self.request.user.fpr[-16:]
261
262
263
        ctx["statement"] = self.statement
        ctx["now"] = now()
        return ctx