Illustrations/web/core/views_user_features.py

165 lines
5.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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)
# [ADD] include search-hit highlighting flag (default True if field missing)
return JsonResponse({
"ok": True,
"font_size": prefs.font_size,
"highlight_search_hits": getattr(prefs, "highlight_search_hits", True),
})
# [ADD] ---------- Search-hit highlighting toggle ----------
@login_required
@require_POST
def api_set_highlight_hits(request):
"""
Toggle per-user 'highlight search hits on entry_view' preference.
Accepts form field: enabled=true/false or 1/0
Returns: {"ok": True, "highlight_search_hits": <bool>}
"""
val = (request.POST.get("enabled") or "").strip().lower()
if val not in ("true", "false", "1", "0"):
return HttpResponseBadRequest("enabled must be true/false")
enabled = val in ("true", "1")
prefs, _ = UserPrefs.objects.get_or_create(user=request.user)
prefs.highlight_search_hits = enabled
prefs.save(update_fields=["highlight_search_hits"])
return JsonResponse({"ok": True, "highlight_search_hits": prefs.highlight_search_hits})
# ---------- Search history ----------
# We use Beacon here, so CSRF headers arent 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