diff --git a/hospexplorer/ask/admin.py b/hospexplorer/ask/admin.py index 3910bbc..703d770 100644 --- a/hospexplorer/ask/admin.py +++ b/hospexplorer/ask/admin.py @@ -1,5 +1,23 @@ from django.contrib import admin -from ask.models import QARecord +from ask.models import TermsAcceptance, QARecord + + +@admin.register(TermsAcceptance) +class TermsAcceptanceAdmin(admin.ModelAdmin): + list_display = ("user", "terms_version", "accepted_at", "ip_address") + list_filter = ("terms_version", "accepted_at") + search_fields = ("user__username", "user__email", "ip_address") + readonly_fields = ("user", "terms_version", "accepted_at", "ip_address") + ordering = ("-accepted_at",) + + def has_add_permission(self, request): + return False + + def has_change_permission(self, request, obj=None): + return False + + def has_delete_permission(self, request, obj=None): + return False @admin.register(QARecord) diff --git a/hospexplorer/ask/context_processors.py b/hospexplorer/ask/context_processors.py new file mode 100644 index 0000000..278b271 --- /dev/null +++ b/hospexplorer/ask/context_processors.py @@ -0,0 +1,11 @@ +from django.conf import settings + + +# Django context processor registered in settings TEMPLATES +# Injects terms_accepted (bool) and terms_version into every template context, +# reads from the session cache set by TermsAcceptanceMiddleware to avoid hitting the db on every request +def terms_status(request): + if hasattr(request, "user") and request.user.is_authenticated: + accepted = request.session.get("terms_accepted_version") == settings.TERMS_VERSION + return {"terms_accepted": accepted, "terms_version": settings.TERMS_VERSION} + return {} diff --git a/hospexplorer/ask/middleware/__init__.py b/hospexplorer/ask/middleware/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hospexplorer/ask/middleware/terms_middleware.py b/hospexplorer/ask/middleware/terms_middleware.py new file mode 100644 index 0000000..774baf1 --- /dev/null +++ b/hospexplorer/ask/middleware/terms_middleware.py @@ -0,0 +1,56 @@ +from django.shortcuts import redirect +from django.conf import settings +from django.urls import resolve +from ask.models import TermsAcceptance + + +class TermsAcceptanceMiddleware: + EXEMPT_URL_NAMES = {"terms-accept", "terms-view"} + + EXEMPT_URL_PREFIXES = ("accounts/", "admin/") + + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + if self._requires_terms_check(request): + current_version = settings.TERMS_VERSION + + # Check session cache first + if request.session.get("terms_accepted_version") == current_version: + return self.get_response(request) + + # Session miss, check DB if user has accepted the terms + has_accepted = TermsAcceptance.objects.filter( + user=request.user, + terms_version=current_version, + ).exists() + + if has_accepted: + # Cache in session so we dont have to call the db again this session + request.session["terms_accepted_version"] = current_version + else: + return redirect("ask:terms-accept") + + return self.get_response(request) + + def _requires_terms_check(self, request): + if not hasattr(request, "user") or not request.user.is_authenticated: + return False + + path = request.path + app_root = getattr(settings, "APP_ROOT", "") + + for prefix in self.EXEMPT_URL_PREFIXES: + full_prefix = f"/{app_root}{prefix}" + if path.startswith(full_prefix): + return False + + try: + resolved = resolve(path) + if resolved.url_name in self.EXEMPT_URL_NAMES: + return False + except Exception: + return False + + return True diff --git a/hospexplorer/ask/migrations/0001_initial.py b/hospexplorer/ask/migrations/0001_initial.py index 98d680a..c3bfede 100644 --- a/hospexplorer/ask/migrations/0001_initial.py +++ b/hospexplorer/ask/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 6.0.1 on 2026-01-30 19:36 +# Generated by Django 6.0.1 on 2026-02-20 19:23 import django.db.models.deletion from django.conf import settings @@ -23,6 +23,7 @@ class Migration(migrations.Migration): ('answer_text', models.TextField(blank=True, default='')), ('answer_raw_response', models.JSONField(default=dict)), ('answer_timestamp', models.DateTimeField(blank=True, null=True)), + ('is_error', models.BooleanField(default=False)), ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='qa_records', to=settings.AUTH_USER_MODEL)), ], options={ @@ -32,4 +33,18 @@ class Migration(migrations.Migration): 'indexes': [models.Index(fields=['user', '-question_timestamp'], name='ask_qarecor_user_id_f4353f_idx')], }, ), + migrations.CreateModel( + name='TermsAcceptance', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('terms_version', models.CharField(max_length=20)), + ('accepted_at', models.DateTimeField(auto_now_add=True)), + ('ip_address', models.GenericIPAddressField()), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='terms_acceptances', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['-accepted_at'], + 'indexes': [models.Index(fields=['user', 'terms_version'], name='ask_termsac_user_id_b13742_idx')], + }, + ), ] diff --git a/hospexplorer/ask/migrations/0002_qarecord_is_error.py b/hospexplorer/ask/migrations/0002_qarecord_is_error.py deleted file mode 100644 index 10ddcac..0000000 --- a/hospexplorer/ask/migrations/0002_qarecord_is_error.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 6.0.1 on 2026-02-04 23:32 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('ask', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='qarecord', - name='is_error', - field=models.BooleanField(default=False), - ), - ] diff --git a/hospexplorer/ask/models.py b/hospexplorer/ask/models.py index aa69d52..993ec71 100644 --- a/hospexplorer/ask/models.py +++ b/hospexplorer/ask/models.py @@ -2,6 +2,26 @@ from django.conf import settings +class TermsAcceptance(models.Model): + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="terms_acceptances", + ) + terms_version = models.CharField(max_length=20) + accepted_at = models.DateTimeField(auto_now_add=True) + ip_address = models.GenericIPAddressField() + + class Meta: + ordering = ["-accepted_at"] + indexes = [ + models.Index(fields=["user", "terms_version"]), + ] + + def __str__(self): + return f"{self.user.username} accepted v{self.terms_version} on {self.accepted_at}" + + class QARecord(models.Model): """ Stores a question-answer pair from user interactions with the LLM. diff --git a/hospexplorer/ask/templates/_base.html b/hospexplorer/ask/templates/_base.html index 6665c53..c2ceb3f 100644 --- a/hospexplorer/ask/templates/_base.html +++ b/hospexplorer/ask/templates/_base.html @@ -17,19 +17,40 @@
-