views.py 21.8 KB
Newer Older
1
2
# coding: utf8
# nm.debian.org AM interaction
3
#
4
# Copyright (C) 2013--2015  Enrico Zini <enrico@debian.org>
5
6
7
8
9
10
11
12
13
14
15
16
17
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
18
19
20
21
from __future__ import print_function
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
22
from django import http, template, forms
23
from django.conf import settings
Enrico Zini's avatar
Enrico Zini committed
24
from django.shortcuts import render, render_to_response, redirect
Enrico Zini's avatar
Enrico Zini committed
25
from django.contrib.auth.decorators import login_required
Enrico Zini's avatar
Enrico Zini committed
26
from django.utils.translation import ugettext as _
27
from django.core.urlresolvers import reverse
28
from django.core.exceptions import PermissionDenied
29
from django.views.generic import View
30
from django.views.generic.edit import FormView
Enrico Zini's avatar
Enrico Zini committed
31
from django.utils.timezone import now
32
from django.db import transaction
33
import backend.models as bmodels
34
import minechangelogs.models as mmodels
Enrico Zini's avatar
Enrico Zini committed
35
from backend import const
36
from backend.mixins import VisitorMixin, VisitPersonMixin, VisitorTemplateView, VisitPersonTemplateView
37
import backend.email
Enrico Zini's avatar
Enrico Zini committed
38
import json
39
import datetime
40
import os
41

42
43
44
45
46
47
48
49
class AMMain(VisitorTemplateView):
    require_visitor = "am"
    template_name = "restricted/ammain.html"

    def get_context_data(self, **kw):
        from django.db.models import Min, Max
        ctx = super(AMMain, self).get_context_data(**kw)

50
        ctx["am_available"] = bmodels.AM.list_available(free_only=True)
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102

        if self.visitor.am.is_fd or self.visitor.am.is_dam:
            DISPATCH = {
                const.PROGRESS_APP_NEW: "prog_app_new",
                const.PROGRESS_APP_RCVD: "prog_app_new",
                const.PROGRESS_ADV_RCVD: "prog_app_new",
                const.PROGRESS_POLL_SENT: "prog_poll_sent",
                const.PROGRESS_APP_OK: "prog_app_ok",
                const.PROGRESS_AM_RCVD: "prog_am_rcvd",
                const.PROGRESS_AM_OK: "prog_am_ok",
                const.PROGRESS_FD_OK: "prog_fd_ok",
                const.PROGRESS_DAM_OK: "prog_dam_ok",
            }
            for p in bmodels.Process.objects.filter(is_active=True, progress__in=DISPATCH.keys()) \
                            .annotate(
                                started=Min("log__logdate"),
                                last_change=Max("log__logdate")) \
                            .order_by("started"):
                tgt = DISPATCH.get(p.progress, None)
                if tgt is not None:
                    p.annotate_with_duration_stats()
                    ctx.setdefault(tgt, []).append(p)

            DISPATCH = {
                const.PROGRESS_APP_HOLD: "prog_app_hold",
                const.PROGRESS_FD_HOLD: "prog_app_hold",
                const.PROGRESS_DAM_HOLD: "prog_app_hold",
            }
            for p in bmodels.Process.objects.filter(is_active=True, manager=None, progress__in=DISPATCH.keys()) \
                            .annotate(
                                started=Min("log__logdate"),
                                last_change=Max("log__logdate")) \
                            .order_by("started"):
                tgt = DISPATCH.get(p.progress, None)
                if tgt is not None:
                    p.annotate_with_duration_stats()
                    ctx.setdefault(tgt, []).append(p)

            DISPATCH = {
                const.PROGRESS_FD_HOLD: "prog_fd_hold",
                const.PROGRESS_DAM_HOLD: "prog_dam_hold",
            }
            for p in bmodels.Process.objects.filter(is_active=True, progress__in=DISPATCH.keys()) \
                            .exclude(manager=None) \
                            .annotate(
                                started=Min("log__logdate"),
                                last_change=Max("log__logdate")) \
                            .order_by("started"):
                tgt = DISPATCH.get(p.progress, None)
                if tgt is not None:
                    p.annotate_with_duration_stats()
                    ctx.setdefault(tgt, []).append(p)
103

104
105

        DISPATCH = {
106
107
108
109
110
111
112
113
114
115
            const.PROGRESS_AM_RCVD: "am_prog_rcvd",
            const.PROGRESS_AM: "am_prog_am",
            const.PROGRESS_AM_HOLD: "am_prog_hold",
            const.PROGRESS_AM_OK: "am_prog_done",
            const.PROGRESS_FD_HOLD: "am_prog_done",
            const.PROGRESS_FD_OK: "am_prog_done",
            const.PROGRESS_DAM_HOLD: "am_prog_done",
            const.PROGRESS_DAM_OK: "am_prog_done",
            const.PROGRESS_DONE: "am_prog_done",
            const.PROGRESS_CANCELLED: "am_prog_done",
Enrico Zini's avatar
Enrico Zini committed
116
        }
117
        for p in bmodels.Process.objects.filter(manager=self.visitor.am, progress__in=DISPATCH.keys()) \
Enrico Zini's avatar
Enrico Zini committed
118
119
120
121
122
123
                        .annotate(
                            started=Min("log__logdate"),
                            last_change=Max("log__logdate")) \
                        .order_by("started"):
            tgt = DISPATCH.get(p.progress, None)
            if tgt is not None:
124
                p.annotate_with_duration_stats()
Enrico Zini's avatar
Enrico Zini committed
125
126
                ctx.setdefault(tgt, []).append(p)

127
        return ctx
Enrico Zini's avatar
Enrico Zini committed
128

Enrico Zini's avatar
Enrico Zini committed
129
class AMProfile(VisitPersonTemplateView):
130
131
132
    require_visitor = "am"
    template_name = "restricted/amprofile.html"

133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
    def make_am_form(self):
        includes = ["slots"]
        visitor_am = self.visitor.am

        if visitor_am.is_fd:
            includes.append("is_fd")
        if visitor_am.is_dam:
            includes.append("is_dam")
        if visitor_am.is_admin:
            includes.append("fd_comment")

        class AMForm(forms.ModelForm):
            class Meta:
                model = bmodels.AM
                fields = includes
        return AMForm

150
151
152
    def get_context_data(self, **kw):
        ctx = super(AMProfile, self).get_context_data(**kw)
        from django.db.models import Min
153
        AMForm = self.make_am_form()
Enrico Zini's avatar
Enrico Zini committed
154
        am = self.person.am
Enrico Zini's avatar
Enrico Zini committed
155
        form = AMForm(instance=am)
156
157
158
159
160
161
        processes = bmodels.Process.objects.filter(manager=am).annotate(started=Min("log__logdate")).order_by("-started")
        ctx.update(
            am=am,
            processes=processes,
            form=form,
        )
Enrico Zini's avatar
Enrico Zini committed
162
        return ctx
Enrico Zini's avatar
Enrico Zini committed
163

Enrico Zini's avatar
Enrico Zini committed
164
165
166
    def post(self, request, *args, **kw):
        if self.person.pk != self.visitor.pk and not self.visitor.is_admin:
            raise PermissionDenied
167

168
169
        AMForm = self.make_am_form()
        form = AMForm(request.POST, instance=self.person.am)
170
171
172
173
174
175
        if form.is_valid():
            form.save()
            # TODO: message that it has been saved

        context = self.get_context_data(**kw)
        return self.render_to_response(context)
Enrico Zini's avatar
Enrico Zini committed
176

Enrico Zini's avatar
Enrico Zini committed
177
class Person(VisitPersonTemplateView):
178
179
180
    """
    Edit a person's information
    """
181
    template_name = "restricted/person.html"
182

Enrico Zini's avatar
Enrico Zini committed
183
184
185
    def get_person_form(self):
        perms = self.vperms.perms

186
187
188
189
190
        # Check permissions
        if "edit_bio" not in perms and "edit_ldap" not in perms:
            raise PermissionDenied

        # Build the form to edit the person
Enrico Zini's avatar
Enrico Zini committed
191
192
        includes = []
        if "edit_ldap" in perms:
193
            includes.extend(("cn", "mn", "sn", "email", "uid"))
Enrico Zini's avatar
Enrico Zini committed
194
195
196
197
        if self.visitor.is_admin:
            includes.extend(("status", "fd_comment", "expires", "pending"))
        if "edit_bio" in perms:
            includes.append("bio")
198
199
200
201

        class PersonForm(forms.ModelForm):
            class Meta:
                model = bmodels.Person
Enrico Zini's avatar
Enrico Zini committed
202
                fields = includes
203
204
205
206
207
        return PersonForm

    def get_context_data(self, **kw):
        ctx = super(Person, self).get_context_data(**kw)
        if "form" not in ctx:
Enrico Zini's avatar
Enrico Zini committed
208
209
            ctx["form"] = self.get_person_form()(instance=self.person)
        return ctx
210

Enrico Zini's avatar
Enrico Zini committed
211
212
    def post(self, request, *args, **kw):
        form = self.get_person_form()(request.POST, instance=self.person)
213
        if form.is_valid():
214
215
            p = form.save(commit=False)
            p.save(audit_author=self.visitor, audit_notes="edited Person information")
216

217
            # TODO: message that it has been saved
218
219

            # Redirect to the person view
Enrico Zini's avatar
Enrico Zini committed
220
            return redirect(self.person.get_absolute_url())
221

222
223
        context = self.get_context_data(form=form, **kw)
        return self.render_to_response(context)
224
225


Enrico Zini's avatar
Enrico Zini committed
226
227
class NewProcess(VisitPersonTemplateView):
    template_name = "restricted/advocate.html"
228
229
230
231
    """
    Create a new process
    """

Enrico Zini's avatar
Enrico Zini committed
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
    dd_statuses = frozenset((const.STATUS_DD_U, const.STATUS_DD_NU))
    dm_statuses = frozenset((const.STATUS_DM, const.STATUS_DM_GA))

    def get_existing_process(self, applying_for):
        "Get the existing process, if any"
        procs = list(self.person.processes.filter(is_active=True, applying_for=applying_for))
        if len(procs) > 1:
            return http.HttpResponseServerError(
                    "There is more than one active process applying for {}."
                    "Please ask Front Desk people to fix that before proceeding".format(applying_for))
        return procs[0] if procs else None

    def get_context_data(self, **kw):
        from django.db.models import Min, Max
        ctx = super(NewProcess, self).get_context_data(**kw)
        applying_for = self.kwargs["applying_for"]
        if applying_for not in self.vperms.advocate_targets:
249
            raise PermissionDenied
250

Enrico Zini's avatar
Enrico Zini committed
251
252
253
254
255
256
257
258
259
260
261
262
        # Checks and warnings

        # When applying for uploading DD
        if applying_for == const.STATUS_DD_U:
            if self.person.status not in self.dm_statuses:
                # Warn about the person not being a DM
                ctx["warn_no_dm"] = True
            else:
                # Check when the person first became DM
                became_dm = None
                for p in self.person.processes.filter(is_active=False, applying_for__in=self.dm_statuses) \
                                            .annotate(last_change=Max("log__logdate")) \
263
                                            .order_by("last_change"):
Enrico Zini's avatar
Enrico Zini committed
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
                    became_dm = p.last_change
                if not became_dm:
                    became_dm = self.person.status_changed

                ts_now = now()
                if became_dm + datetime.timedelta(days=6*30) > ts_now:
                    # Warn about not having been DM for 6 months
                    ctx["warn_early_dm"] = became_dm

        ctx["existing_process"] = self.get_existing_process(applying_for)
        ctx["applying_for"] = applying_for
        return ctx

    def post(self, request, applying_for, key, *args, **kw):
        applying_for = self.kwargs["applying_for"]
        if applying_for not in self.vperms.advocate_targets:
            raise PermissionDenied

Enrico Zini's avatar
Enrico Zini committed
282
283
284
285
286
        advtext = request.POST["text"].strip()
        if not advtext:
            context = self.get_context_data(**kw)
            return self.render_to_response(context)

Enrico Zini's avatar
Enrico Zini committed
287
288
        process = self.get_existing_process(applying_for)
        if not process:
Enrico Zini's avatar
Enrico Zini committed
289
290
            # Create the process if one does not exist yet
            process = bmodels.Process.objects.create(
Enrico Zini's avatar
Enrico Zini committed
291
292
293
294
295
296
                person=self.person,
                progress=const.PROGRESS_ADV_RCVD,
                is_active=True,
                applying_as=self.person.status,
                applying_for=applying_for,
            )
Enrico Zini's avatar
Enrico Zini committed
297
298
299
300
301
302
303
304
305
306
307
308
309
310

            # Log the creation
            text = "Process created by {} advocating {}".format(self.visitor.lookup_key, self.person.lookup_key)
            if self.impersonator:
                text = "[{} as {}] {}".format(self.impersonator.lookup_key, self.visitor.lookup_key, text)
            bmodels.Log.objects.create(
                changed_by=self.visitor,
                process=process,
                progress=process.progress,
                is_public=True,
                logtext=text,
            )

        # Add the advocate
311
312
        process.advocates.add(self.visitor)

Enrico Zini's avatar
Enrico Zini committed
313
314
315
316
        # Log the advocacy
        if self.impersonator:
            advtext = "[{} as {}] {}".format(self.impersonator.lookup_key, self.visitor.lookup_key, advtext)
        lt = bmodels.Log.objects.create(
317
318
319
            changed_by=self.visitor,
            process=process,
            progress=process.progress,
Enrico Zini's avatar
Enrico Zini committed
320
321
            is_public=True,
            logtext=advtext,
322
        )
323

Enrico Zini's avatar
Enrico Zini committed
324
325
        # Send mail
        backend.email.send_notification("notification_mails/advocacy.txt", lt)
326
        return redirect('public_process', key=process.lookup_key)
Enrico Zini's avatar
Enrico Zini committed
327

328
class DBExport(VisitorMixin, View):
Enrico Zini's avatar
Enrico Zini committed
329
    require_visitor = "dd"
Enrico Zini's avatar
Enrico Zini committed
330

Enrico Zini's avatar
Enrico Zini committed
331
    def get(self, request, *args, **kw):
332
333
334
335
336
337
        if "full" in request.GET:
            if not self.visitor.is_admin:
                raise PermissionDenied
            full = True
        else:
            full = False
Enrico Zini's avatar
Enrico Zini committed
338

339
        people = list(bmodels.export_db(full))
Enrico Zini's avatar
Enrico Zini committed
340

341
342
343
344
345
346
        class Serializer(json.JSONEncoder):
            def default(self, o):
                if hasattr(o, "strftime"):
                    return o.strftime("%Y-%m-%d %H:%M:%S")
                return json.JSONEncoder.default(self, o)

Enrico Zini's avatar
Enrico Zini committed
347
        res = http.HttpResponse(content_type="application/json")
348
349
350
351
352
353
        if full:
            res["Content-Disposition"] = "attachment; filename=nm-full.json"
        else:
            res["Content-Disposition"] = "attachment; filename=nm-mock.json"
        json.dump(people, res, cls=Serializer, indent=1)
        return res
354
355
356
357
358
359


class MinechangelogsForm(forms.Form):
    query = forms.CharField(
        required=True,
        label=_("Query"),
360
        help_text=_("Enter one keyword per line. Changelog entries to be shown must match at least one keyword. You often need to tweak the keywords to improve the quality of results. Note that keyword matching is case-sensitive."),
361
362
        widget=forms.Textarea(attrs=dict(rows=5, cols=40))
    )
363
364
365
366
367
    download = forms.BooleanField(
        required=False,
        label=_("Download"),
        help_text=_("Activate this field to download the changelog instead of displaying it"),
    )
368
369

def minechangelogs(request, key=None):
Enrico Zini's avatar
Enrico Zini committed
370
371
    if request.user.is_anonymous():
        raise PermissionDenied
372
373
374
375
376
    entries = None
    info = mmodels.info()
    info["max_ts"] = datetime.datetime.fromtimestamp(info["max_ts"])
    info["last_indexed"] = datetime.datetime.fromtimestamp(info["last_indexed"])

Enrico Zini's avatar
Enrico Zini committed
377
    if key:
378
        person = bmodels.Person.lookup_or_404(key)
Enrico Zini's avatar
Enrico Zini committed
379
380
381
    else:
        person = None

382
    keywords=None
383
384
385
386
387
    if request.method == 'POST':
        form = MinechangelogsForm(request.POST)
        if form.is_valid():
            query = form.cleaned_data["query"]
            keywords = [x.strip() for x in query.split("\n")]
388
389
            entries = mmodels.query(keywords)
            if form.cleaned_data["download"]:
390
391
392
393
394
                def send_entries():
                    for e in entries:
                        yield e
                        yield "\n\n"
                res = http.HttpResponse(send_entries(), content_type="text/plain")
395
396
397
398
399
400
401
                if person:
                    res["Content-Disposition"] = 'attachment; filename=changelogs-%s.txt' % person.lookup_key
                else:
                    res["Content-Disposition"] = 'attachment; filename=changelogs.txt'
                return res
            else:
                entries = list(entries)
402
    else:
Enrico Zini's avatar
Enrico Zini committed
403
        if person:
404
405
406
407
408
409
410
411
412
413
414
415
            query = [
                person.fullname,
                person.email,
            ]
            if person.uid:
                query.append(person.uid)
            form = MinechangelogsForm(initial=dict(query="\n".join(query)))
        else:
            form = MinechangelogsForm()

    return render_to_response("restricted/minechangelogs.html",
                              dict(
416
                                  keywords=keywords,
417
418
419
                                  form=form,
                                  info=info,
                                  entries=entries,
Enrico Zini's avatar
Enrico Zini committed
420
                                  person=person,
421
422
423
                              ),
                              context_instance=template.RequestContext(request))

424
class Impersonate(View):
Enrico Zini's avatar
Enrico Zini committed
425
    def get(self, request, key=None, *args, **kw):
Enrico Zini's avatar
Enrico Zini committed
426
        visitor = request.user
Enrico Zini's avatar
Enrico Zini committed
427
        if not visitor.is_authenticated() or not visitor.is_admin: raise PermissionDenied
428
429
        if key is None:
            del request.session["impersonate"]
430
        else:
431
432
            person = bmodels.Person.lookup_or_404(key)
            request.session["impersonate"] = person.lookup_key
433

434
435
436
437
438
        url = request.GET.get("url", None)
        if url is None:
            return redirect('home')
        else:
            return redirect(url)
Enrico Zini's avatar
Enrico Zini committed
439

440
def _assign_am(request, visitor, nm, am):
Enrico Zini's avatar
Enrico Zini committed
441
442
443
444
445
446
    import textwrap
    nm.manager = am
    nm.progress = const.PROGRESS_AM_RCVD
    nm.save()
    # Parameters for the following templates
    parms = dict(
447
448
        fduid=visitor.uid,
        fdname=visitor.fullname,
Enrico Zini's avatar
Enrico Zini committed
449
450
451
452
453
454
455
        amuid=am.person.uid,
        amname=am.person.fullname,
        nmname=nm.person.fullname,
        nmcurstatus=const.ALL_STATUS_DESCS[nm.person.status],
        nmnewstatus=const.ALL_STATUS_DESCS[nm.applying_for],
        procurl=request.build_absolute_uri(reverse("public_process", kwargs=dict(key=nm.lookup_key))),
    )
456
    l = bmodels.Log.for_process(nm, changed_by=visitor)
Enrico Zini's avatar
Enrico Zini committed
457
458
    l.logtext = "Assigned to %(amuid)s" % parms
    if 'impersonate' in request.session:
459
        l.logtext = "[%s as %s] %s" % (request.user, visitor.lookup_key, l.logtext)
Enrico Zini's avatar
Enrico Zini committed
460
461
    l.save()

462
463
464
class MailArchive(VisitorMixin, View):
    def get(self, request, key, *args, **kw):
        process = bmodels.Process.lookup_or_404(key)
Enrico Zini's avatar
Enrico Zini committed
465
        vperms = process.permissions_of(self.visitor)
466

Enrico Zini's avatar
Enrico Zini committed
467
        if "view_mbox" not in vperms.perms:
468
469
470
471
472
473
474
475
476
            raise PermissionDenied

        fname = process.mailbox_file
        if fname is None:
            from django.http import Http404
            raise Http404

        user_fname = "%s.mbox" % (process.person.uid or process.person.email)

Enrico Zini's avatar
Enrico Zini committed
477
        res = http.HttpResponse(content_type="application/octet-stream")
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
        res["Content-Disposition"] = "attachment; filename=%s.gz" % user_fname

        # Compress the mailbox and pass it to the request
        from gzip import GzipFile
        import os.path
        import shutil
        # The last mtime argument seems to only be supported in python 2.7
        outfd = GzipFile(user_fname, "wb", 9, res) #, os.path.getmtime(fname))
        try:
            with open(fname) as infd:
                shutil.copyfileobj(infd, outfd)
        finally:
            outfd.close()
        return res

class DisplayMailArchive(VisitorTemplateView):
    template_name = "restricted/display-mail-archive.html"
    def get_context_data(self, **kw):
        ctx = super(DisplayMailArchive, self).get_context_data(**kw)
        key = self.kwargs["key"]
        process = bmodels.Process.lookup_or_404(key)
Enrico Zini's avatar
Enrico Zini committed
499
        vperms = process.permissions_of(self.visitor)
500

Enrico Zini's avatar
Enrico Zini committed
501
        if "view_mbox" not in vperms.perms:
502
503
504
505
506
507
508
509
510
511
512
            raise PermissionDenied

        fname = process.mailbox_file
        if fname is None:
            from django.http import Http404
            raise Http404

        ctx["mails"] = backend.email.get_mbox_as_dicts(fname)
        ctx["process"] = process
        ctx["class"] = "clickable"
        return ctx
Marco Bardelli's avatar
Marco Bardelli committed
513

514
515
516
class AssignAM(VisitorTemplateView):
    template_name = "restricted/assign-am.html"
    require_visitor = "admin"
Enrico Zini's avatar
Enrico Zini committed
517

518
    def get_context_data(self, **kw):
Enrico Zini's avatar
Enrico Zini committed
519
        ctx = super(AssignAM, self).get_context_data(**kw)
520
521
522
523
        key = self.kwargs["key"]
        process = bmodels.Process.lookup_or_404(key)
        if process.manager is not None:
            raise PermissionDenied
Enrico Zini's avatar
Enrico Zini committed
524

525
        # List free AMs
526
        ams = bmodels.AM.list_available(free_only=False)
527
528
529
530
531
532
533

        ctx.update(
            process=process,
            person=process.person,
            ams=ams,
        )
        return ctx
Enrico Zini's avatar
Enrico Zini committed
534

535
536
    def post(self, request, key, *args, **kw):
        process = bmodels.Process.lookup_or_404(key)
Enrico Zini's avatar
Enrico Zini committed
537
538
        am_key = request.POST.get("am", None)
        am = bmodels.AM.lookup_or_404(am_key)
539
        _assign_am(request, self.visitor, process, am)
Enrico Zini's avatar
Enrico Zini committed
540
        return redirect(process.get_absolute_url())
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558

class MailboxStats(VisitorTemplateView):
    template_name = "restricted/mailbox-stats.html"
    require_visitor = "admin"

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

        try:
            with open(os.path.join(settings.DATA_DIR, 'mbox_stats.json'), "rt") as infd:
                stats = json.load(infd)
        except OSError:
            stats = {}

        for email, st in stats["emails"].items():
            st["person"] = bmodels.Person.lookup_by_email(email)
            st["date_first_py"] = datetime.datetime.fromtimestamp(st["date_first"])
            st["date_last_py"] = datetime.datetime.fromtimestamp(st["date_last"])
Enrico Zini's avatar
Enrico Zini committed
559
            if "median" not in st or st["median"] is None:
560
561
562
563
564
565
566
567
568
                st["median_py"] = None
            else:
                st["median_py"] = datetime.timedelta(seconds=st["median"])
                st["median_hours"] = st["median_py"].seconds // 3600

        ctx.update(
            emails=sorted(stats["emails"].items()),
        )
        return ctx
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592


class NewFingerprintForm(forms.ModelForm):
    class Meta:
        model = bmodels.Fingerprint
        fields = ["fpr"]


class PersonFingerprints(VisitPersonMixin, FormView):
    template_name = "restricted/person_fingerprints.html"
    require_vperms = "edit_ldap"
    form_class = NewFingerprintForm

    # TODO: add template

    @transaction.atomic
    def form_valid(self, form):
        fpr = form.save(commit=False)
        fpr.user = self.person
        fpr.is_active = True
        fpr.save(audit_author=self.visitor, audit_notes="added new fingerprint")
        # Ensure that only the new fingerprint is the active one
        self.person.fprs.exclude(pk=fpr.pk).update(is_active=False)
        return redirect("restricted_person_fingerprints", key=self.person.lookup_key)