diff --git a/consent/__init__.py b/consent/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/consent/admin.py b/consent/admin.py new file mode 100644 index 00000000..5f1f9457 --- /dev/null +++ b/consent/admin.py @@ -0,0 +1,10 @@ +from django.contrib import admin + +from .models import Consent + + +@admin.register(Consent) +class ConsentAdmin(admin.ModelAdmin): + list_display = ('file', 'type', 'is_active', 'file_hash', 'created_at') + list_filter = ('is_active', 'type') + readonly_fields = ('file_hash', 'created_at') diff --git a/consent/apps.py b/consent/apps.py new file mode 100644 index 00000000..1867de28 --- /dev/null +++ b/consent/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ConsentConfig(AppConfig): + name = 'consent' diff --git a/consent/middleware.py b/consent/middleware.py new file mode 100644 index 00000000..b99d4581 --- /dev/null +++ b/consent/middleware.py @@ -0,0 +1,45 @@ +from django.conf import settings +from django.shortcuts import redirect +from django.urls import reverse + +from .models import Consent, UserConsent +from .utils import is_student_user + +CONSENT_SESSION_KEY = 'consent_version_id' + + +class ConsentMiddleware(object): + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + user = getattr(request, 'user', None) + if not user or not user.is_authenticated: + return self.get_response(request) + + if not is_student_user(user): + return self.get_response(request) + + path = request.path + consent_url = reverse('consent:consent') + if ( + path.startswith('/admin/') + or path.startswith(consent_url) + or path.startswith(settings.STATIC_URL) + or path.startswith(settings.MEDIA_URL) + ): + return self.get_response(request) + + active = Consent.objects.filter(is_active=True).first() + if not active: + return self.get_response(request) + + session_version = request.session.get(CONSENT_SESSION_KEY) + if session_version == active.pk: + return self.get_response(request) + + if UserConsent.objects.filter(user=user, consent=active).exists(): + request.session[CONSENT_SESSION_KEY] = active.pk + return self.get_response(request) + + return redirect('%s?next=%s' % (consent_url, request.get_full_path())) diff --git a/consent/migrations/0001_initial.py b/consent/migrations/0001_initial.py new file mode 100644 index 00000000..35f9fe38 --- /dev/null +++ b/consent/migrations/0001_initial.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='ConsentVersion', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('file_name', models.CharField(max_length=255)), + ('file_hash', models.CharField(max_length=64)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('is_active', models.BooleanField(default=True)), + ], + options={ + 'ordering': ['-created_at'], + }, + ), + migrations.CreateModel( + name='UserConsent', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('accepted_at', models.DateTimeField(auto_now_add=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('consent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='consent.ConsentVersion')), + ], + options={ + 'unique_together': {('user', 'consent')}, + }, + ), + ] diff --git a/consent/migrations/0002_rename_and_add_fields.py b/consent/migrations/0002_rename_and_add_fields.py new file mode 100644 index 00000000..df0a8042 --- /dev/null +++ b/consent/migrations/0002_rename_and_add_fields.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('consent', '0001_initial'), + ] + + operations = [ + migrations.RenameModel( + old_name='ConsentVersion', + new_name='Consent', + ), + migrations.AddField( + model_name='consent', + name='type', + field=models.PositiveIntegerField(default=1), + ), + migrations.RemoveField( + model_name='consent', + name='file_name', + ), + migrations.AddField( + model_name='consent', + name='file', + field=models.FileField(default='', upload_to='consent/'), + preserve_default=False, + ), + ] diff --git a/consent/migrations/__init__.py b/consent/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/consent/models.py b/consent/models.py new file mode 100644 index 00000000..af94e536 --- /dev/null +++ b/consent/models.py @@ -0,0 +1,40 @@ +from django.conf import settings +from django.db import models + +from .utils import compute_file_hash + + +class Consent(models.Model): + file = models.FileField(upload_to='consent/') + file_hash = models.CharField(max_length=64, blank=True) + type = models.PositiveIntegerField(default=1) + created_at = models.DateTimeField(auto_now_add=True) + is_active = models.BooleanField(default=True) + + class Meta: + ordering = ['-created_at'] + + def save(self, *args, **kwargs): + if self.is_active: + Consent.objects.filter(is_active=True).exclude(pk=self.pk).update(is_active=False) + super(Consent, self).save(*args, **kwargs) + if self.file: + new_hash = compute_file_hash(self.file.path) + if new_hash != self.file_hash: + self.file_hash = new_hash + super(Consent, self).save(update_fields=['file_hash']) + + def __str__(self): + return '%s (%s)' % (self.file.name, 'active' if self.is_active else 'inactive') + + +class UserConsent(models.Model): + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) + consent = models.ForeignKey(Consent, on_delete=models.CASCADE) + accepted_at = models.DateTimeField(auto_now_add=True) + + class Meta: + unique_together = ('user', 'consent') + + def __str__(self): + return '%s - v%s' % (self.user, self.consent_id) \ No newline at end of file diff --git a/consent/templates/consent/consent.html b/consent/templates/consent/consent.html new file mode 100644 index 00000000..b09424e3 --- /dev/null +++ b/consent/templates/consent/consent.html @@ -0,0 +1,22 @@ +{% extends 'spoken/templates/base.html' %} + +{% block title %}Consent Required{% endblock %} + +{% block heading %}Consent Required{% endblock %} + +{% block content %} +