from django.shortcuts import render, redirect, get_object_or_404 from django.contrib.auth import authenticate, login from django.contrib.auth.decorators import login_required, user_passes_test from django.http import HttpResponse, HttpResponseForbidden, HttpResponseRedirect from django.contrib import messages from django.db.models import Q, Count from django.utils.timezone import now from datetime import date, timedelta import csv, io from .models import Entry from .forms import ImportForm, EntryForm from .utils import import_csv_bytes, wildcard_to_like, terms, month_buckets_last_12 def is_admin(user): return user.is_superuser or user.is_staff def login_view(request): if request.user.is_authenticated: return redirect("search") ctx = {} if request.method == "POST": username = request.POST.get("username") password = request.POST.get("password") user = authenticate(request, username=username, password=password) if user: login(request, user) return redirect("search") else: ctx["error"] = "Invalid credentials" return render(request, "login.html", ctx) @login_required def search_page(request): # Defaults: Subject, Illustration, Application checked default_fields = {"subject": True, "illustration": True, "application": True, "scripture_raw": False, "source": False, "talk_title": False, "talk_number": False, "entry_code": False} if request.method == "GET": # read selection from query or use defaults selected = {k: (request.GET.get(k,"on" if v else "") != "") for k,v in default_fields.items()} q = request.GET.get("q","").strip() results = [] count = 0 idx = 0 if q: like = wildcard_to_like(q) term_list = terms(q) # Build Q across selected fields, ANDing each term fields = [f for f,sel in selected.items() if sel] qs = Entry.objects.all() for t in term_list: pattern = wildcard_to_like(t) clause = Q() for f in fields: clause |= Q(**{f+"__icontains": t.replace("*","").replace("?","")}) qs = qs.filter(clause) ids = list(qs.order_by("-date_added","-id").values_list("id", flat=True)) request.session["result_ids"] = ids count = len(ids) if count: idx = 0 entry = Entry.objects.get(pk=ids[idx]) return render(request, "entry_view.html", { "entry": entry, "locked": True, "position": idx+1, "count": count, "from_search": True }) # If no query or no results, render search page total = Entry.objects.count() return render(request, "search.html", { "q": q, "selected": selected, "total": total }) return render(request, "search.html", {"selected": default_fields}) @login_required def nav_next(request): ids = request.session.get("result_ids", []) if not ids: return redirect("search") idx = int(request.GET.get("i","0")) idx = min(idx+1, len(ids)-1) entry = get_object_or_404(Entry, pk=ids[idx]) return render(request, "entry_view.html", {"entry": entry, "locked": True, "position": idx+1, "count": len(ids)}) @login_required def nav_prev(request): ids = request.session.get("result_ids", []) if not ids: return redirect("search") idx = int(request.GET.get("i","0")) idx = max(idx-1, 0) entry = get_object_or_404(Entry, pk=ids[idx]) return render(request, "entry_view.html", {"entry": entry, "locked": True, "position": idx+1, "count": len(ids)}) @login_required def entry_view(request, entry_id): entry = get_object_or_404(Entry, pk=entry_id) ids = request.session.get("result_ids", []) count = len(ids) position = ids.index(entry.id)+1 if entry.id in ids else 1 return render(request, "entry_view.html", {"entry": entry, "locked": True, "position": position, "count": count}) @login_required def entry_edit(request, entry_id): entry = get_object_or_404(Entry, pk=entry_id) if request.method == "POST": form = EntryForm(request.POST) if form.is_valid(): for k,v in form.cleaned_data.items(): setattr(entry, k, v) entry.save() messages.success(request, "Entry saved.") return redirect("entry_view", entry_id=entry.id) else: initial = { "subject": entry.subject, "illustration": entry.illustration, "application": entry.application, "scripture_raw": entry.scripture_raw, "source": entry.source, "talk_number": entry.talk_number, "talk_title": entry.talk_title, "entry_code": entry.entry_code, "date_added": entry.date_added, "date_edited": entry.date_edited } form = EntryForm(initial=initial) ids = request.session.get("result_ids", []) count = len(ids) position = ids.index(entry.id)+1 if entry.id in ids else 1 return render(request, "entry_edit.html", {"entry": entry, "form": form, "position": position, "count": count}) @login_required def entry_delete(request, entry_id): entry = get_object_or_404(Entry, pk=entry_id) if request.method == "POST": entry.delete() messages.success(request, "Entry deleted.") return redirect("search") return render(request, "entry_delete_confirm.html", {"entry": entry}) @login_required @user_passes_test(is_admin) def import_wizard(request): from .utils import import_csv_bytes if request.method == "POST": form = ImportForm(request.POST, request.FILES) if form.is_valid(): f = form.cleaned_data["file"].read() dry = form.cleaned_data["dry_run"] try: report = import_csv_bytes(f, dry_run=dry) return render(request, "import_result.html", {"report": report, "dry_run": dry}) except Exception as e: messages.error(request, f"Import failed: {e}") else: form = ImportForm() return render(request, "import_wizard.html", {"form": form}) @login_required @user_passes_test(is_admin) def export_csv(request): response = HttpResponse(content_type='text/csv') ts = now().strftime("%Y-%m-%d_%H-%M-%S") response['Content-Disposition'] = f'attachment; filename="illustrations_backup_{ts}.csv"' writer = csv.writer(response) writer.writerow(["Subject","Illustration","Application","Scripture","Source","Talk Number","Talk Title","Code","Date","Date Edited"]) for e in Entry.objects.all().order_by("id"): writer.writerow([ e.subject, e.illustration, e.application, e.scripture_raw, e.source, e.talk_number if e.talk_number is not None else "", e.talk_title, e.entry_code, e.date_added.isoformat() if e.date_added else "", e.date_edited.isoformat() if e.date_edited else "", ]) return response @login_required def stats_page(request): total = Entry.objects.count() today = date.today() last30 = Entry.objects.filter(date_added__gte=today - timedelta(days=30)).count() last365 = Entry.objects.filter(date_added__gte=today - timedelta(days=365)).count() # Sparkline: counts per month for last 12 months (by date_added, blanks excluded) buckets = month_buckets_last_12(today) series = [] for label, start, end in buckets: c = Entry.objects.filter(date_added__gte=start, date_added__lt=end).count() series.append((label, c)) # Top subjects (split by commas, case-insensitive) from collections import Counter counts = Counter() for subj in Entry.objects.exclude(subject="").values_list("subject", flat=True): for tag in [t.strip() for t in subj.split(",") if t.strip()]: counts[tag.lower()] += 1 top_subjects = [{"name": n.title(), "count": c} for n,c in counts.most_common(10)] return render(request, "stats.html", { "total": total, "last30": last30, "last365": last365, "series": series, "top_subjects": top_subjects })