views.py 19.1 KB
Newer Older
1
from django import http, forms
Enrico Zini's avatar
Enrico Zini committed
2
from django.shortcuts import redirect
3
from django.urls import reverse
Enrico Zini's avatar
Enrico Zini committed
4
from django.core.exceptions import PermissionDenied
5
from django.utils.translation import gettext as _
6
from django.utils.timezone import now
Enrico Zini's avatar
Enrico Zini committed
7
from django.views.generic import View
8
from django.views.generic.edit import FormView
9
10
from django.contrib import messages as django_messages
import logging
11
import backend.models as bmodels
12
import backend.email as bemail
13
import dsa.models as dmodels
14
15
import legacy.models as lmodels
import process.models as pmodels
16
import nm2.lib.forms
17
from nm2.lib.multiform import Multiform
18
from nm2.lib.forms import BootstrapAttrsMixin
19
from nm2.lib import assets
Enrico Zini's avatar
Enrico Zini committed
20
from backend import const
Enrico Zini's avatar
Enrico Zini committed
21
from backend.mixins import VisitorMixin, VisitorTemplateView
22
from collections import Counter
Enrico Zini's avatar
Enrico Zini committed
23
import datetime
Enrico Zini's avatar
Enrico Zini committed
24
import json
25

26
log = logging.getLogger("django.request")
Enrico Zini's avatar
Enrico Zini committed
27

Enrico Zini's avatar
Enrico Zini committed
28

29
30
31
32
33
34
35
36
37
def lookup_or_404(dict, key):
    """
    Lookup a key in a dictionary, raising 404 if not found
    """
    try:
        return dict[key]
    except KeyError:
        raise http.Http404

Enrico Zini's avatar
Enrico Zini committed
38

39
class Managers(VisitorTemplateView):
40
    assets = [assets.DataTablesBootstrap4]
Enrico Zini's avatar
Enrico Zini committed
41
    template_name = "public/managers.html"
42

Enrico Zini's avatar
Enrico Zini committed
43
44
45
46
47
    def get_context_data(self, **kw):
        ctx = super(Managers, self).get_context_data(**kw)
        from django.db import connection

        # Compute statistics indexed by AM id
48
49
50
51
        with connection.cursor() as cursor:
            cursor.execute("""
            SELECT am.id,
                   count(*) as total,
52
53
                   sum(case when legacy_process.is_active then 1 else 0 end) as active,
                   sum(case when legacy_process.progress=%s then 1 else 0 end) as held
54
              FROM am
55
              JOIN legacy_process ON legacy_process.manager_id=am.id
56
57
58
59
60
61
62
63
64
             GROUP BY am.id
            """, (const.PROGRESS_AM_HOLD,))
            stats = {}
            for amid, total, active, held in cursor:
                stats[amid] = (total, active, held)

            cursor.execute("""
            SELECT am.id,
                   count(*) as total,
65
                   sum(case when p.closed_time is null then 1 else 0 end) as active,
66
67
68
69
70
71
72
                   sum(case when pam.paused then 1 else 0 end) as held
              FROM am
              JOIN process_amassignment pam ON pam.am_id=am.id
              JOIN process_process p ON pam.process_id=p.id
             GROUP BY am.id
            """)
            for amid, total, active, held in cursor:
Enrico Zini's avatar
Enrico Zini committed
73
74
                s = stats.get(amid, (0, 0, 0))
                stats[amid] = (s[0] + total, s[1] + active, s[2] + held)
75

Enrico Zini's avatar
Enrico Zini committed
76
77
78
79
80
81
82
83
84
85
        # Read the list of AMs, with default sorting, and annotate with the
        # statistics
        ams = []
        for a in bmodels.AM.objects.all().order_by("-is_am", "person__uid"):
            total, active, held = stats.get(a.id, (0, 0, 0))
            a.stats_total = total
            a.stats_active = active
            a.stats_done = total-active
            a.stats_held = held
            ams.append(a)
86

Enrico Zini's avatar
Enrico Zini committed
87
88
        ctx["ams"] = ams
        return ctx
Enrico Zini's avatar
Enrico Zini committed
89
90


91
SIMPLIFY_STATUS = {
Enrico Zini's avatar
Enrico Zini committed
92
93
    const.STATUS_DC: "new",
    const.STATUS_DC_GA: "new",
94
95
96
97
98
99
100
101
    const.STATUS_DM: "dm",
    const.STATUS_DM_GA: "dm",
    const.STATUS_DD_U: "dd",
    const.STATUS_DD_NU: "dd",
    const.STATUS_EMERITUS_DD: "emeritus",
    const.STATUS_REMOVED_DD: "removed",
}

Enrico Zini's avatar
Enrico Zini committed
102

103
class People(VisitorTemplateView):
Enrico Zini's avatar
Enrico Zini committed
104
    assets = [assets.DataTablesBootstrap4]
Enrico Zini's avatar
Enrico Zini committed
105
    template_name = "public/people.html"
Enrico Zini's avatar
Enrico Zini committed
106

Enrico Zini's avatar
Enrico Zini committed
107
108
109
110
    def get_context_data(self, **kw):
        ctx = super(People, self).get_context_data(**kw)
        status = self.kwargs.get("status", None)

Enrico Zini's avatar
Enrico Zini committed
111
        # def people(request, status=None):
Enrico Zini's avatar
Enrico Zini committed
112
        objects = bmodels.Person.objects.all().order_by("ldap_fields__uid", "fullname")
Enrico Zini's avatar
Enrico Zini committed
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
        show_status = True
        status_sdesc = None
        status_ldesc = None
        if status:
            if status == "dm_all":
                objects = objects.filter(status__in=(const.STATUS_DM, const.STATUS_DM_GA))
                status_sdesc = _("Debian Maintainer")
                status_ldesc = _("Debian Maintainer (with or without guest account)")
            elif status == "dd_all":
                objects = objects.filter(status__in=(const.STATUS_DD_U, const.STATUS_DD_NU))
                status_sdesc = _("Debian Developer")
                status_ldesc = _("Debian Developer (uploading or not)")
            else:
                objects = objects.filter(status=status)
                show_status = False
128
129
                status_sdesc = lookup_or_404(const.ALL_STATUS_BYTAG, status).sdesc
                status_ldesc = lookup_or_404(const.ALL_STATUS_BYTAG, status).sdesc
Enrico Zini's avatar
Enrico Zini committed
130
131
132
133
134
135
136
137
138
139
140
141
142
143

        people = []
        for p in objects:
            p.simple_status = SIMPLIFY_STATUS.get(p.status, None)
            people.append(p)

        ctx.update(
            people=people,
            status=status,
            show_status=show_status,
            status_sdesc=status_sdesc,
            status_ldesc=status_ldesc,
        )
        return ctx
Enrico Zini's avatar
Enrico Zini committed
144

145

Enrico Zini's avatar
Enrico Zini committed
146
147
148
149
150
151
class Members(VisitorTemplateView):
    assets = [assets.DataTablesBootstrap4]
    template_name = "public/members.html"

    def get_context_data(self, **kw):
        ctx = super().get_context_data(**kw)
Enrico Zini's avatar
Enrico Zini committed
152
153
        ctx["members"] = bmodels.Person.objects.select_related("ldap_fields").filter(
                status__in=(const.STATUS_DD_U, const.STATUS_DD_NU)).order_by("fullname")
Enrico Zini's avatar
Enrico Zini committed
154
155
156
        return ctx


Enrico Zini's avatar
Enrico Zini committed
157
158
159
160
161
162
163
164
class AuditLog(VisitorTemplateView):
    template_name = "public/audit_log.html"
    require_visitor = "dd"

    def get_context_data(self, **kw):
        ctx = super(AuditLog, self).get_context_data(**kw)

        audit_log = []
165
        cutoff = now() - datetime.timedelta(days=30)
Enrico Zini's avatar
Enrico Zini committed
166
        for e in bmodels.PersonAuditLog.objects.filter(logdate__gte=cutoff).order_by("-logdate"):
167
            if self.request.user.is_superuser:
Enrico Zini's avatar
Enrico Zini committed
168
                changes = sorted((k, v[0], v[1]) for k, v in list(json.loads(e.changes).items()))
Enrico Zini's avatar
Enrico Zini committed
169
            else:
Enrico Zini's avatar
Enrico Zini committed
170
                changes = sorted((k, v[0], v[1]) for k, v in list(json.loads(e.changes).items()) if k != "fd_comment")
Enrico Zini's avatar
Enrico Zini committed
171
172
173
174
175
176
177
178
179
180
181
            audit_log.append({
                "person": e.person,
                "logdate": e.logdate,
                "author": e.author,
                "notes": e.notes,
                "changes": changes,
            })

        ctx["audit_log"] = audit_log
        return ctx

Enrico Zini's avatar
Enrico Zini committed
182

183
class Stats(VisitorTemplateView):
184
    assets = [assets.DataTablesBootstrap4, assets.Flot]
Enrico Zini's avatar
Enrico Zini committed
185
    template_name = "public/stats.html"
Enrico Zini's avatar
Enrico Zini committed
186

Enrico Zini's avatar
Enrico Zini committed
187
188
189
190
191
192
193
    def get_context_data(self, **kw):
        ctx = super(Stats, self).get_context_data(**kw)
        from django.db.models import Count

        stats = {}

        # Count of people by status
194
195
196
197
        by_status = []
        for row in bmodels.Person.objects.values("status").annotate(count=Count("status")).order_by("status"):
            by_status.append((row["status"], row["count"]))
        by_status.sort(key=lambda x: -x[1])
Enrico Zini's avatar
Enrico Zini committed
198
199
200
201
        stats["by_status"] = by_status

        # Cook up more useful bits for the templates

Enrico Zini's avatar
Enrico Zini committed
202
        ctx["stats"] = stats
Enrico Zini's avatar
Enrico Zini committed
203
204
205

        # List of active processes with statistics
        active_processes = []
206

207
        # Build process list
208
209
        import process.models as pmodels
        from process.mixins import compute_process_status
210
        for p in pmodels.Process.objects.filter(closed_time__isnull=True):
Enrico Zini's avatar
Enrico Zini committed
211
            active_processes.append((p, compute_process_status(p, self.request.user)))
212
        active_processes.sort(key=lambda x: (x[1]["log_first"].logdate if x[1]["log_first"] else x[0].started))
Enrico Zini's avatar
Enrico Zini committed
213
214
        ctx["active_processes"] = active_processes

215
216
217
218
219
220
        # Count processes by status
        by_status = Counter()
        for proc, stat in active_processes:
            by_status[stat["summary"]] += 1
        ctx["by_status"] = sorted(by_status.items())

221
222
223
224
225
226
227
228
229
230
        # Count of applicants by progress (for rrdstats.sh, merge old and new processes)
        by_progress = Counter()
        by_progress["dam_ok"] += by_status["Approved"]
        by_progress["am"] += by_status["AM"]
        by_progress["am_hold"] += by_status["AM Hold"]
        by_progress["am_ok"] += by_status["Waiting for review"]
        by_progress["fd_ok"] += by_status["Frozen for review"]
        by_progress["app_new"] += by_status["Collecting requirements"]
        stats["by_progress"] = by_progress

Enrico Zini's avatar
Enrico Zini committed
231
        return ctx
Enrico Zini's avatar
Enrico Zini committed
232

233
234
235
236
237
238
239
240
241
242
243
244
    def get(self, request, *args, **kwargs):
        context = self.get_context_data(**kwargs)
        # If JSON is requested, dump them right away
        if 'json' in request.GET:
            res = http.HttpResponse(content_type="application/json")
            res["Content-Disposition"] = "attachment; filename=stats.json"
            json.dump(context["stats"], res, indent=1)
            return res
        else:
            return self.render_to_response(context)


Enrico Zini's avatar
Enrico Zini committed
245
def make_findperson_form(request, visitor):
Enrico Zini's avatar
Enrico Zini committed
246
    includes = ["status", "email"]
Enrico Zini's avatar
Enrico Zini committed
247

248
    if visitor.is_superuser:
Enrico Zini's avatar
Enrico Zini committed
249
        includes.append("fd_comment")
Enrico Zini's avatar
Enrico Zini committed
250

251
252
253
    class FindpersonForm(nm2.lib.forms.BootstrapAttrsMixin, forms.ModelForm):
        name = forms.CharField(
                label="Name", required=False, min_length=40, widget=forms.TextInput(attrs={"autofocus": "autofocus"}))
Enrico Zini's avatar
Enrico Zini committed
254
255
        uid = forms.CharField(
                label="Uid", required=False, min_length=40)
256
257
        fpr = forms.CharField(
                label="Fingerprint", required=False, min_length=40, widget=forms.TextInput(attrs={"size": 60}))
258

Enrico Zini's avatar
Enrico Zini committed
259
260
        class Meta:
            model = bmodels.Person
Enrico Zini's avatar
Enrico Zini committed
261
            fields = includes
262
263
264
265

        def clean_fpr(self):
            return bmodels.FingerprintField.clean_fingerprint(self.cleaned_data['fpr'])

266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
        def __init__(self, *args, **kw):
            super().__init__(*args, **kw)
            f = self.fields.get("fd_comment")
            if f is not None:
                f.widget = forms.TextInput(attrs={"class": "form-control"})

            # Reorder fields putting name at the beginning
            sorted_fields = {
                "name": self.fields.pop("name"),
                "uid": self.fields.pop("uid"),
                "fpr": self.fields.pop("fpr"),
            }
            for k, v in self.fields.items():
                sorted_fields[k] = v
            self.fields = sorted_fields

Enrico Zini's avatar
Enrico Zini committed
282
283
    return FindpersonForm

284

285
class Findperson(VisitorTemplateView):
286
    assets = [assets.DataTablesBootstrap4]
Enrico Zini's avatar
Enrico Zini committed
287
288
    template_name = "public/findperson.html"

289
    def get_form_class(self):
Enrico Zini's avatar
Enrico Zini committed
290
        return make_findperson_form(self.request, self.request.user)
Enrico Zini's avatar
Enrico Zini committed
291

292
293
294
    def get_context_data(self, **kw):
        Form = self.get_form_class()
        return super().get_context_data(form=Form())
Enrico Zini's avatar
Enrico Zini committed
295

Enrico Zini's avatar
Enrico Zini committed
296

297
298
299
300
301
302
303
class StatsGraph(VisitorTemplateView):
    template_name = "public/stats_graph.html"

    def get_context_data(self, **kw):
        ctx = super(StatsGraph, self).get_context_data(**kw)

        am_nm = []
Enrico Zini's avatar
flake8    
Enrico Zini committed
304
305
        for p in lmodels.Process.objects.filter(manager__isnull=False).values_list(
                "manager__person__ldap_fields__uid", "person__ldap_fields__uid"):
306
307
            am_nm.append((p[0], p[1]))

Enrico Zini's avatar
flake8    
Enrico Zini committed
308
309
        for a in pmodels.AMAssignment.objects.filter(unassigned_time__isnull=True).values_list(
                "am__person__ldap_fields__uid", "process__person__ldap_fields__uid"):
310
311
            am_nm.append((a[0], a[1]))

312
        adv_nm = []
313
314
315
316
317
318
319
        for p in lmodels.Process.objects.all().select_related("person__ldap_fields"):
            for a in p.advocates.all().select_related("ldap_fields"):
                person_uid = p.person.ldap_fields.uid
                if not person_uid:
                    continue
                adv_nm.append((a.ldap_fields.uid, person_uid))

Enrico Zini's avatar
flake8    
Enrico Zini committed
320
321
        for s in pmodels.Statement.objects.filter(requirement__type="advocate").values_list(
                "fpr__person__ldap_fields__uid", "requirement__process__person__ldap_fields__uid"):
322
            adv_nm.append((s[0], s[1]))
323
324
325
326
327
328
329
330

        ctx = dict(
            am_nm=am_nm,
            adv_nm=adv_nm,
        )

        return ctx

Enrico Zini's avatar
Enrico Zini committed
331

332
YESNO = (
Enrico Zini's avatar
Enrico Zini committed
333
334
    ("yes", "Yes"),
    ("no", "No"),
335
)
336

Enrico Zini's avatar
Enrico Zini committed
337

338
class NewPersonForm(BootstrapAttrsMixin, forms.ModelForm):
339
    fpr = forms.CharField(label="Fingerprint", min_length=40, widget=forms.TextInput(attrs={"size": 60}))
Enrico Zini's avatar
Enrico Zini committed
340
341
    sc_ok = forms.ChoiceField(choices=YESNO, widget=forms.RadioSelect(), label="SC and DFSG agreement")
    dmup_ok = forms.ChoiceField(choices=YESNO, widget=forms.RadioSelect(), label="DMUP agreement")
342

343
    def clean_fpr(self):
344
345
        data = bmodels.FingerprintField.clean_fingerprint(self.cleaned_data['fpr'])
        if bmodels.Fingerprint.objects.filter(fpr=data).exists():
346
347
348
            raise forms.ValidationError(
                    "The GPG fingerprint is already known to this system."
                    " Please contact Front Desk to link your Alioth account to it.")
349
        return data
350

351
352
353
354
355
356
357
358
359
360
361
362
    def clean_sc_ok(self):
        data = self.cleaned_data['sc_ok']
        if data != "yes":
            raise forms.ValidationError("You need to agree with the Debian Social Contract and DFSG to continue")
        return data

    def clean_dmup_ok(self):
        data = self.cleaned_data['dmup_ok']
        if data != "yes":
            raise forms.ValidationError("You need to agree with the DMUP to continue")
        return data

363
364
    class Meta:
        model = bmodels.Person
365
        fields = ["fullname", "email", "bio"]
Enrico Zini's avatar
Enrico Zini committed
366
367
368
        widgets = {
            "bio": forms.Textarea(attrs={'cols': 80, 'rows': 25}),
        }
369

Enrico Zini's avatar
Enrico Zini committed
370

371
class NewPersonLDAPFieldsForm(BootstrapAttrsMixin, forms.ModelForm):
372
373
374
375
376
377
378
379
380
381
382
383
    class Meta:
        model = dmodels.LDAPFields
        fields = ["cn", "mn", "sn", "uid"]


class NewPersonMultiform(Multiform):
    form_classes = {
        "person": NewPersonForm,
        "ldap_fields": NewPersonLDAPFieldsForm,
    }


384
class Newnm(VisitorMixin, FormView):
385
386
387
    """
    Display the new Person form
    """
388
    template_name = "public/newnm.html"
389
    form_class = NewPersonMultiform
390
391
    DAYS_VALID = 3

392
393
394
    def get_success_url(self):
        return redirect("public_newnm_resend_challenge", key=self.request.user.lookup_key)

Enrico Zini's avatar
Enrico Zini committed
395
396
397
398
399
400
401
402
403
404
405
406
407
408
    def get_initial(self):
        # Prefill person.fullname from currently active identities
        person = {}
        for identity in self.request.signon_identities.values():
            if identity.fullname:
                person["fullname"] = identity.fullname
                break

        ldap_fields = {}
        return {
            "person": person,
            "ldap_fields": ldap_fields,
        }

409
    def form_valid(self, form):
410
        if self.request.user.is_authenticated:
Enrico Zini's avatar
Enrico Zini committed
411
            raise PermissionDenied
Enrico Zini's avatar
Enrico Zini committed
412
        if not self.request.signon_identities:
Enrico Zini's avatar
Enrico Zini committed
413
            raise PermissionDenied
414

Enrico Zini's avatar
Enrico Zini committed
415
        person = form["person"].save(commit=False)
Enrico Zini's avatar
Enrico Zini committed
416
        person.status = const.STATUS_DC
417
418
        person.status_changed = now()
        person.make_pending(days_valid=self.DAYS_VALID)
419
        person.save(audit_author=person, audit_notes="new subscription to the site")
Enrico Zini's avatar
Enrico Zini committed
420
421
422
423
424

        ldap_fields = form["ldap_fields"].save(commit=False)
        ldap_fields.person = person
        ldap_fields.save(audit_author=person, audit_notes="new subscription to the site")

Enrico Zini's avatar
Enrico Zini committed
425
426
        for identity in self.request.signon_identities.values():
            identity.person = person
427
            identity.save(audit_author=person, audit_notes="new subscription to the site")
Enrico Zini's avatar
Enrico Zini committed
428

Enrico Zini's avatar
Enrico Zini committed
429
        fpr = form["person"].cleaned_data["fpr"]
430
431
        bmodels.Fingerprint.objects.create(
                person=person, fpr=fpr, is_active=True, audit_author=person, audit_notes="new subscription to the site")
Enrico Zini's avatar
Enrico Zini committed
432

433
434
435
436
        # Redirect to the send challenge page
        return redirect("public_newnm_resend_challenge", key=person.lookup_key)

    def get_context_data(self, **kw):
Enrico Zini's avatar
Enrico Zini committed
437
        ctx = super().get_context_data(**kw)
438
439
        form = ctx["form"]
        errors = []
Enrico Zini's avatar
Enrico Zini committed
440
441
442
443
444
445
446
447
448
449
450
451
        for k, v in form.forms["person"].errors.items():
            if k in ("sc_ok", "dmup_ok"):
                section = "rules"
            else:
                section = k
            errors.append({
                "section": section,
                "label": form.forms["person"].fields[k].label,
                "id": k,
                "errors": v,
            })
        for k, v in form.forms["ldap_fields"].errors.items():
452
453
454
455
456
457
            if k in ("cn", "mn", "sn"):
                section = "name"
            else:
                section = k
            errors.append({
                "section": section,
Enrico Zini's avatar
Enrico Zini committed
458
                "label": form.forms["ldap_fields"].fields[k].label,
459
460
461
462
                "id": k,
                "errors": v,
            })

Enrico Zini's avatar
Enrico Zini committed
463
464
        has_entry = self.request.user.is_authenticated
        is_dd = self.request.user.is_authenticated and "dd" in self.request.user.perms
Enrico Zini's avatar
Enrico Zini committed
465
        require_login = not self.request.signon_identities
Enrico Zini's avatar
Enrico Zini committed
466
        show_apply_form = not require_login and (not has_entry or is_dd)
467
468

        ctx.update(
Enrico Zini's avatar
Enrico Zini committed
469
            person=self.request.user,
470
471
            form=form,
            errors=errors,
Enrico Zini's avatar
Enrico Zini committed
472
473
474
475
            has_entry=has_entry,
            is_dd=is_dd,
            show_apply_form=show_apply_form,
            require_login=require_login,
476
            DAYS_VALID=self.DAYS_VALID,
Enrico Zini's avatar
Enrico Zini committed
477
            wikihelp="https://wiki.debian.org/nm.debian.org/Newnm",
478
479
        )
        return ctx
480

481
482

class NewnmResendChallenge(VisitorMixin, View):
483
484
485
486
    """
    Send/resend the encrypted email nonce for people who just requested a new
    Person record
    """
487
    def get(self, request, key=None, *args, **kw):
488
        from keyring.models import Key, GpgInvocationError
489

Enrico Zini's avatar
Enrico Zini committed
490
        if not self.request.user.is_authenticated:
Enrico Zini's avatar
Enrico Zini committed
491
            raise PermissionDenied()
492

493
494
        # Deal gracefully with someone clicking the reconfirm link after they have
        # already confirmed
Enrico Zini's avatar
Enrico Zini committed
495
496
        if not self.request.user.pending:
            return redirect(self.request.user.get_absolute_url())
497

498
        confirm_url = request.build_absolute_uri(
Enrico Zini's avatar
Enrico Zini committed
499
                reverse("public_newnm_confirm", kwargs=dict(nonce=self.request.user.pending)))
500
501
        plaintext = "Please visit {} to confirm your application at {}\n".format(
                confirm_url,
Enrico Zini's avatar
Enrico Zini committed
502
503
                request.build_absolute_uri(self.request.user.get_absolute_url()))
        key = Key.objects.get_or_download(self.request.user.fpr)
Enrico Zini's avatar
Enrico Zini committed
504
505
        if not key.key_is_fresh():
            key.update_key()
506
507
508
        try:
            encrypted = key.encrypt(plaintext.encode("utf8"))
        except GpgInvocationError:
Enrico Zini's avatar
Enrico Zini committed
509
510
511
            django_messages.error(request, _(
                "Encryption of email challenge failed. "
                "Please try again in a few minutes and if the problem persists, please mail nm@debian.org"))
512
513
514
            log.error("Caught exception while trying to encrypt email challenge.",
                      extra={'status_code': 500, 'request': self.request}, exc_info=True)
        else:
515
            bemail.send_nonce("notification_mails/newperson.txt",
Enrico Zini's avatar
Enrico Zini committed
516
                              self.request.user, encrypted_nonce=encrypted.decode('utf8'))
Enrico Zini's avatar
Enrico Zini committed
517
        return redirect(self.request.user.get_absolute_url())
518

519

520
class NewnmConfirm(VisitorMixin, View):
521
522
523
    """
    Confirm a pending Person object, given its nonce
    """
524
    def get(self, request, nonce, *args, **kw):
Enrico Zini's avatar
Enrico Zini committed
525
        if not self.request.user.is_authenticated:
Enrico Zini's avatar
Enrico Zini committed
526
            raise PermissionDenied
Enrico Zini's avatar
Enrico Zini committed
527
        if self.request.user.pending != nonce:
Enrico Zini's avatar
Enrico Zini committed
528
            raise PermissionDenied
Enrico Zini's avatar
Enrico Zini committed
529
530
531
532
        self.request.user.pending = ""
        self.request.user.expires = now() + datetime.timedelta(days=30)
        self.request.user.save(audit_author=self.request.user, audit_notes="confirmed pending subscription")
        return redirect(self.request.user.get_absolute_url())