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 @@
+ Please review and accept the Terms of Use to continue.
+Version: {{ terms_version }}
+ +By accessing and using Hopper ("the Application," "Service," or "we"), you agree to be bound by these Terms of Use ("Terms"). If you do not agree to these Terms, please do not use the Application.
+ +You must be at least 13 years of age to use this Application. By using the Service, you represent and warrant that you meet this age requirement and have the legal capacity to enter into these Terms.
+ +You may be required to create an account to access certain features. You are responsible for maintaining the confidentiality of your account credentials and for all activities that occur under your account. You agree to notify us immediately of any unauthorized access or use of your account.
+ +You agree to use the Application only for lawful purposes and in accordance with these Terms. You agree not to:
+You retain ownership of any content you submit to the Application. By submitting content, you grant us a non-exclusive, worldwide, royalty-free license to use, display, and distribute your content in connection with operating the Service. You represent that you have all necessary rights to the content you submit.
+ +The Application and its original content, features, and functionality are owned by Hopper and are protected by international copyright, trademark, and other intellectual property laws. You may not copy, modify, distribute, or create derivative works without our express written permission.
+ +Your use of the Application is also governed by our Privacy Policy. We collect and process data as described in our Privacy Policy, including logging interactions for quality assurance, security, and improvement purposes.
+ +The Application may contain links to third-party websites or services that are not owned or controlled by us. We assume no responsibility for the content, privacy policies, or practices of any third-party services.
+ +The Application is provided "AS IS" and "AS AVAILABLE" without warranties of any kind, either express or implied, including but not limited to warranties of merchantability, fitness for a particular purpose, or non-infringement. We do not warrant that:
+Content and responses generated by the Application are for informational purposes only and should not be relied upon as professional, legal, medical, or financial advice.
+ +To the maximum extent permitted by law, Hopper shall not be liable for any indirect, incidental, special, consequential, or punitive damages, or any loss of profits or revenues, whether incurred directly or indirectly, or any loss of data, use, goodwill, or other intangible losses resulting from your use of the Service.
+ +You agree to indemnify, defend, and hold harmless Hopper and its officers, directors, employees, and agents from any claims, liabilities, damages, losses, and expenses arising out of or related to your use of the Service or violation of these Terms.
+ +We reserve the right to suspend or terminate your access to the Application at any time, with or without cause or notice, for any reason including violation of these Terms. Upon termination, your right to use the Service will immediately cease.
+ +These Terms shall be governed by and construed in accordance with the laws of [Your Jurisdiction], without regard to its conflict of law provisions. Any disputes arising from these Terms or your use of the Service shall be resolved in the courts of [Your Jurisdiction].
+ +We reserve the right to modify or replace these Terms at any time at our sole discretion. We will provide notice of any material changes by updating the "Last Updated" date. Your continued use of the Application after any such changes constitutes your acceptance of the new Terms.
+ +If any provision of these Terms is found to be unenforceable or invalid, that provision will be limited or eliminated to the minimum extent necessary, and the remaining provisions will remain in full force and effect.
+ +If you have any questions about these Terms, please contact us.
\ No newline at end of file diff --git a/hospexplorer/ask/templates/terms/terms_view.html b/hospexplorer/ask/templates/terms/terms_view.html new file mode 100644 index 0000000..b38945c --- /dev/null +++ b/hospexplorer/ask/templates/terms/terms_view.html @@ -0,0 +1,25 @@ +{% extends "_base.html" %} + +{% block content %} +