138 lines
4.5 KiB
Python
138 lines
4.5 KiB
Python
# core/views_user_features.py
|
||
from django.contrib.auth.decorators import login_required
|
||
from django.http import JsonResponse, HttpResponseBadRequest
|
||
from django.views.decorators.http import require_POST, require_GET
|
||
from django.views.decorators.csrf import csrf_exempt
|
||
from django.utils import timezone
|
||
from django.forms.models import model_to_dict
|
||
|
||
from .models import Entry # existing
|
||
from .models_user import UserPrefs, SearchHistory, ViewedIllustration
|
||
from django.views.decorators.cache import never_cache
|
||
|
||
|
||
# ---------- Font size prefs ----------
|
||
@login_required
|
||
@require_POST
|
||
def api_set_font_size(request):
|
||
size = (request.POST.get("size") or "").lower()
|
||
if size not in ("small", "default", "large", "xlarge"):
|
||
return JsonResponse({"ok": False, "error": "bad_size"}, status=400)
|
||
prefs, _ = UserPrefs.objects.get_or_create(user=request.user)
|
||
prefs.font_size = size
|
||
prefs.save(update_fields=["font_size"])
|
||
return JsonResponse({"ok": True})
|
||
|
||
|
||
@login_required
|
||
@require_GET
|
||
def api_get_prefs(request):
|
||
prefs, _ = UserPrefs.objects.get_or_create(user=request.user)
|
||
return JsonResponse({"ok": True, "font_size": prefs.font_size})
|
||
"highlight_search_hits": getattr(prefs, "highlight_search_hits", True),
|
||
|
||
|
||
# ---------- Search history ----------
|
||
# We use Beacon here, so CSRF headers aren’t available -> mark this one csrf_exempt
|
||
@login_required
|
||
@csrf_exempt # keep this since you use sendBeacon
|
||
@require_POST
|
||
def api_log_search(request):
|
||
q = (request.POST.get("q") or "").strip()
|
||
|
||
# Collect selected fields from form-style keys: sel[subject]=on
|
||
selected = {}
|
||
for k in request.POST:
|
||
if k.startswith("sel[") and k.endswith("]"):
|
||
name = k[4:-1]
|
||
selected[name] = request.POST.get(k) in ("1", "true", "on", "yes")
|
||
|
||
# De-dupe consecutive identical searches; if identical, bump timestamp
|
||
last = SearchHistory.objects.filter(user=request.user).order_by("-created_at").first()
|
||
if last and last.q == q and last.selected == selected:
|
||
# bump to "most recent"
|
||
last.created_at = timezone.now()
|
||
last.save(update_fields=["created_at"])
|
||
else:
|
||
SearchHistory.objects.create(user=request.user, q=q, selected=selected)
|
||
|
||
# Trim to last 10
|
||
ids = list(
|
||
SearchHistory.objects.filter(user=request.user)
|
||
.order_by("-created_at")
|
||
.values_list("id", flat=True)[:10]
|
||
)
|
||
SearchHistory.objects.filter(user=request.user).exclude(id__in=ids).delete()
|
||
return JsonResponse({"ok": True})
|
||
|
||
|
||
@login_required
|
||
@require_GET
|
||
@never_cache
|
||
def api_get_search_history(request):
|
||
items_qs = (
|
||
SearchHistory.objects.filter(user=request.user)
|
||
.order_by("-created_at")
|
||
.values("q", "selected", "created_at")
|
||
)
|
||
data = [
|
||
{"q": i["q"], "selected": i["selected"], "ts": i["created_at"].isoformat()}
|
||
for i in items_qs
|
||
]
|
||
resp = JsonResponse({"ok": True, "items": data})
|
||
resp["Cache-Control"] = "no-store"
|
||
return resp
|
||
|
||
|
||
|
||
# ---------- Recently viewed ----------
|
||
@login_required
|
||
@require_POST
|
||
def api_log_view(request, entry_id):
|
||
try:
|
||
e = Entry.objects.get(pk=entry_id)
|
||
except Entry.DoesNotExist:
|
||
return JsonResponse({"ok": False, "error": "notfound"}, status=404)
|
||
|
||
ViewedIllustration.objects.create(user=request.user, entry=e)
|
||
|
||
# Keep only the 50 most recent
|
||
ids = list(
|
||
ViewedIllustration.objects.filter(user=request.user)
|
||
.order_by("-viewed_at")
|
||
.values_list("id", flat=True)[:50]
|
||
)
|
||
ViewedIllustration.objects.filter(user=request.user).exclude(id__in=ids).delete()
|
||
return JsonResponse({"ok": True})
|
||
|
||
|
||
@login_required
|
||
@require_GET
|
||
@never_cache
|
||
def api_get_recent_views(request):
|
||
rows = (
|
||
ViewedIllustration.objects.filter(user=request.user)
|
||
.select_related("entry")
|
||
.order_by("-viewed_at")[:10]
|
||
)
|
||
|
||
def first_words(text, n=20):
|
||
import re as _re
|
||
clean = _re.sub(r"\s+", " ", (text or "")).strip()
|
||
if not clean:
|
||
return ""
|
||
words = clean.split(" ")
|
||
return clean if len(words) <= n else " ".join(words[:n]) + "…"
|
||
|
||
data = [
|
||
{
|
||
"entry_id": r.entry_id,
|
||
"viewed_at": r.viewed_at.isoformat(),
|
||
"illustration": r.entry.illustration or "", # <-- what the UI expects
|
||
"snippet": first_words(r.entry.illustration or "", 20),
|
||
}
|
||
for r in rows
|
||
]
|
||
resp = JsonResponse({"ok": True, "items": data})
|
||
resp["Cache-Control"] = "no-store"
|
||
return resp |