# 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": } """ 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 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