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
20 changes: 19 additions & 1 deletion hospexplorer/ask/admin.py
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
11 changes: 11 additions & 0 deletions hospexplorer/ask/context_processors.py
Original file line number Diff line number Diff line change
@@ -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 {}
Empty file.
56 changes: 56 additions & 0 deletions hospexplorer/ask/middleware/terms_middleware.py
Original file line number Diff line number Diff line change
@@ -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
17 changes: 16 additions & 1 deletion hospexplorer/ask/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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={
Expand All @@ -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')],
},
),
]
18 changes: 0 additions & 18 deletions hospexplorer/ask/migrations/0002_qarecord_is_error.py

This file was deleted.

20 changes: 20 additions & 0 deletions hospexplorer/ask/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
37 changes: 29 additions & 8 deletions hospexplorer/ask/templates/_base.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,40 @@
<body>
<div class="d-flex" id="wrapper">
<!-- Sidebar-->
<div class="border-end bg-white" id="sidebar-wrapper">
<div class="border-end bg-white d-flex flex-column" id="sidebar-wrapper">
<div class="sidebar-heading border-bottom bg-white">
<img width="50" src="{% static 'assets/asu.png' %}">
Hopper
</div>
<div class="list-group list-group-flush">
{% if user.is_authenticated %}
<div class="p-3 border-top">
<div class="small text-muted mb-2">Signed in as <strong>{{ user.username }}</strong></div>
<a href="{% url 'account_logout' %}" class="btn btn-outline-secondary btn-sm w-100">Sign Out</a>
</div>
{% endif %}
<div class="list-group list-group-flush flex-grow-1">
</div>
{% if user.is_authenticated %}
<!-- User profile at bottom -->
<div class="border-top p-3 dropup">
<a href="#" class="d-flex align-items-center text-decoration-none"
data-bs-toggle="dropdown" aria-expanded="false" id="userProfileDropdown">
<div style="width: 36px; height: 36px; border-radius: 50%; background-color: rgb(140, 29, 64); color: white; display: flex; align-items: center; justify-content: center; font-weight: 600; font-size: 0.9rem; flex-shrink: 0;">
{{ user.username|make_list|first|upper }}
</div>
<span class="ms-2 text-dark" style="font-size: 0.9rem; font-weight: 500;">{{ user.username }}</span>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="ms-auto text-muted" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M7.646 4.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1-.708.708L8 5.707l-5.646 5.647a.5.5 0 0 1-.708-.708l6-6z"/>
</svg>
</a>
<ul class="dropdown-menu shadow-sm w-100" aria-labelledby="userProfileDropdown">
<li>
<a class="dropdown-item" href="{% url 'ask:terms-view' %}">
Terms of Use
{% if terms_accepted %}
<span class="badge bg-success ms-1" style="font-size: 0.65em;">Accepted</span>
{% endif %}
</a>
</li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="{% url 'account_logout' %}">Sign Out</a></li>
</ul>
</div>
{% endif %}
</div>
<!-- Page content wrapper-->
<div id="page-content-wrapper">
Expand Down
30 changes: 30 additions & 0 deletions hospexplorer/ask/templates/terms/terms_accept.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{% extends "account/_base_auth.html" %}
{% load static %}

{% block content %}
<div class="col-11 col-md-8 col-lg-6">
<div class="card" style="border-color: rgb(197, 66, 130); border-width: 2px;">
<div class="card-body p-4">
<div class="text-center mb-4">
<img width="60" src="{% static 'assets/asu.png' %}" alt="Hopper Logo" class="mb-3">
<h3 class="card-title">Terms of Use</h3>
<p class="text-muted">Please review and accept the Terms of Use to continue.</p>
</div>

<div class="border rounded p-3 mb-4" style="max-height: 400px; overflow-y: auto;">
{% include "terms/terms_of_use_content.html" %}
</div>

<form method="post" action="{% url 'ask:terms-accept' %}">
{% csrf_token %}
<div class="d-grid gap-2">
<button type="submit" class="btn btn-lg"
style="background-color: rgb(197, 66, 130); border-color: rgb(197, 66, 130); color: white;">
I Agree to the Terms of Use
</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
64 changes: 64 additions & 0 deletions hospexplorer/ask/templates/terms/terms_of_use_content.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<h4>Terms of Use</h4>
<p><em>Version: {{ terms_version }}</em></p>

<h5>1. Acceptance of Terms</h5>
<p>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.</p>

<h5>2. Eligibility</h5>
<p>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.</p>

<h5>3. User Accounts</h5>
<p>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.</p>

<h5>4. Acceptable Use</h5>
<p>You agree to use the Application only for lawful purposes and in accordance with these Terms. You agree not to:</p>
<ul>
<li>Violate any applicable laws or regulations</li>
<li>Infringe upon the rights of others</li>
<li>Transmit any harmful, offensive, or malicious content</li>
<li>Attempt to gain unauthorized access to the Application or its systems</li>
<li>Interfere with or disrupt the Service or servers</li>
<li>Use automated systems (bots, scrapers) without permission</li>
</ul>

<h5>5. User Content</h5>
<p>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.</p>

<h5>6. Intellectual Property</h5>
<p>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.</p>

<h5>7. Privacy and Data Collection</h5>
<p>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.</p>

<h5>8. Third-Party Services</h5>
<p>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.</p>

<h5>9. Disclaimers</h5>
<p>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:</p>
<ul>
<li>The Service will be uninterrupted, secure, or error-free</li>
<li>The results obtained from use of the Service will be accurate or reliable</li>
<li>Any errors in the Service will be corrected</li>
</ul>
<p>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.</p>

<h5>10. Limitation of Liability</h5>
<p>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.</p>

<h5>11. Indemnification</h5>
<p>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.</p>

<h5>12. Termination</h5>
<p>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.</p>

<h5>13. Governing Law</h5>
<p>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].</p>

<h5>14. Changes to Terms</h5>
<p>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.</p>

<h5>15. Severability</h5>
<p>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.</p>

<h5>16. Contact Information</h5>
<p>If you have any questions about these Terms, please contact us.</p>
25 changes: 25 additions & 0 deletions hospexplorer/ask/templates/terms/terms_view.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{% extends "_base.html" %}

{% block content %}
<div class="mt-4 mb-4">
<h2>Terms of Use</h2>

<div class="card mb-4">
<div class="card-body">
{% include "terms/terms_of_use_content.html" %}
</div>
</div>

{% if acceptance %}
<div class="alert alert-success">
<strong>Accepted.</strong> You accepted version {{ acceptance.terms_version }}
on {{ acceptance.accepted_at|date:"N j, Y, P" }}.
</div>
{% else %}
<div class="alert alert-warning">
<strong>Not accepted.</strong> You have not yet accepted the current terms (version {{ terms_version }}).
<a href="{% url 'ask:terms-accept' %}" style="color: rgb(197, 66, 130);">Accept now</a>.
</div>
{% endif %}
</div>
{% endblock %}
2 changes: 2 additions & 0 deletions hospexplorer/ask/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,6 @@
path("", views.index, name="index"),
path("mock", views.mock_response, name="mock-response"),
path("query", views.query, name="query-llm"),
path("terms/", views.terms_view, name="terms-view"),
path("terms/accept/", views.terms_accept, name="terms-accept"),
]
5 changes: 5 additions & 0 deletions hospexplorer/ask/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
def _get_client_ip(request):
x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR")
if x_forwarded_for:
return x_forwarded_for.split(",")[0].strip()
return request.META.get("REMOTE_ADDR", "0.0.0.0")
Loading