Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added consent/__init__.py
Empty file.
10 changes: 10 additions & 0 deletions consent/admin.py
Original file line number Diff line number Diff line change
@@ -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')
5 changes: 5 additions & 0 deletions consent/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from django.apps import AppConfig


class ConsentConfig(AppConfig):
name = 'consent'
45 changes: 45 additions & 0 deletions consent/middleware.py
Original file line number Diff line number Diff line change
@@ -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()))
43 changes: 43 additions & 0 deletions consent/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -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')},
},
),
]
33 changes: 33 additions & 0 deletions consent/migrations/0002_rename_and_add_fields.py
Original file line number Diff line number Diff line change
@@ -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,
),
]
Empty file added consent/migrations/__init__.py
Empty file.
40 changes: 40 additions & 0 deletions consent/models.py
Original file line number Diff line number Diff line change
@@ -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)
22 changes: 22 additions & 0 deletions consent/templates/consent/consent.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{% extends 'spoken/templates/base.html' %}

{% block title %}Consent Required{% endblock %}

{% block heading %}Consent Required{% endblock %}

{% block content %}
<div class="panel panel-default">
<div class="panel-heading">
<strong>Consent Terms</strong>
</div>
<div class="panel-body">
<div class="consent-text">
{{ consent_text|linebreaks }}
</div>
<form method="post">
{% csrf_token %}
<button type="submit" class="btn btn-primary">I Accept</button>
</form>
</div>
</div>
{% endblock %}
3 changes: 3 additions & 0 deletions consent/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from django.test import TestCase

# Create your tests here.
9 changes: 9 additions & 0 deletions consent/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from django.conf.urls import url

from .views import consent_view

app_name = 'consent'

urlpatterns = [
url(r'^$', consent_view, name='consent'),
]
17 changes: 17 additions & 0 deletions consent/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import hashlib


def compute_file_hash(file_path):
digest = hashlib.sha256()
with open(file_path, 'rb') as fh:
for chunk in iter(lambda: fh.read(8192), b''):
digest.update(chunk)
return digest.hexdigest()


def is_student_user(user):
if not user.is_authenticated:
return False
if user.is_staff or user.is_superuser:
return False
return True
33 changes: 33 additions & 0 deletions consent/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import os

from django.contrib.auth.decorators import login_required
from django.http import HttpResponseServerError
from django.shortcuts import redirect, render

from .middleware import CONSENT_SESSION_KEY
from .models import Consent, UserConsent


@login_required
def consent_view(request):
active = Consent.objects.filter(is_active=True).first()
if not active:
return HttpResponseServerError('Consent file is not configured.')

if not active.file or not os.path.isfile(active.file.path):
return HttpResponseServerError('Consent file is not configured.')

if request.method == 'POST':
UserConsent.objects.get_or_create(
user=request.user,
consent=active,
)
request.session[CONSENT_SESSION_KEY] = active.pk
return redirect('/')

with open(active.file.path, 'r', encoding='utf-8', errors='replace') as fh:
consent_text = fh.read()

return render(request, 'consent/consent.html', {
'consent_text': consent_text,
})
34 changes: 34 additions & 0 deletions media/consent/terms.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
Spoken Tutorial Project - Consent Agreement

By using this platform, you agree to the following terms:

1. Data Collection
We collect your name, email address, and academic details to provide
training services and issue certificates.

2. Usage of Data
Your personal data will be used solely for the purpose of managing
your training, assessments, and certification on this platform.

3. Communication
You consent to receive emails related to your training progress,
upcoming workshops, and certification updates.

4. Content Usage
Spoken Tutorial content is licensed under Creative Commons and is
intended for educational purposes only. You agree not to use it
for commercial purposes without prior written permission.

5. Code of Conduct
You agree to use the platform responsibly and respect the guidelines
set by the Spoken Tutorial Project, IIT Bombay.

6. Data Retention
Your data will be retained as long as your account is active or as
required for certification and audit purposes.

7. Right to Withdraw
You may withdraw your consent at any time by contacting the
administrator. Withdrawal may affect your access to certain services.

For questions, contact: support@spoken-tutorial.org
3 changes: 3 additions & 0 deletions spoken/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,9 @@
# Youtube API V3
url(r'^youtube/', include('youtube.urls', namespace='youtube')),

# consent
url(r'^consent/', include('consent.urls', namespace='consent')),

# reports
url(r'^reports/', include('reports.urls', namespace='reports')),

Expand Down