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 from django.contrib import messages from django.db.models import Q from datetime import date, timedelta import csv from .models import Entry from .forms import ImportForm, EntryForm from .utils import import_csv_bytes, wildcard_to_like, terms, month_buckets_last_12 FIELD_ORDER=[ ("subject","Subject"), ("illustration","Illustration"), ("application","Application"), ("scripture_raw","Scripture"), ("source","Source"), ("talk_title","Talk Title"), ("talk_number","Talk Number"), ("entry_code","Code"), ] 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": u=request.POST.get("username"); p=request.POST.get("password") user=authenticate(request, username=u, password=p) if user: login(request,user); return redirect("search") ctx["error"]="Invalid credentials" return render(request,"login.html",ctx) @login_required def search_page(request): 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": selected={k:(request.GET.get(k,"on" if v else "")!="") for k,v in default_fields.items()} field_options=[{"name":k,"label":label,"checked":bool(selected.get(k))} for k,label in FIELD_ORDER] q=request.GET.get("q","").strip() if q: term_list=terms(q) qs=Entry.objects.all() fields=[f for f,sel in selected.items() if sel] for t in term_list: 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 if ids: entry=Entry.objects.get(pk=ids[0]) subject_list=[t.strip() for t in (entry.subject or "").split(",") if t.strip()] scripture_list=[t.strip() for t in (entry.scripture_raw or "").split(";") if t.strip()] return render(request,"entry_view.html",{"entry":entry,"subject_list":subject_list,"scripture_list":scripture_list,"locked":True,"position":1,"count":len(ids),"from_search":True}) total=Entry.objects.count() return render(request,"search.html",{"q":q,"selected":selected,"field_options":field_options,"total":total}) return render(request,"search.html",{"selected":default_fields,"field_options":[{"name":k,"label":lbl,"checked":default_fields.get(k,False)} for k,lbl in FIELD_ORDER]}) @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]) subject_list=[t.strip() for t in (entry.subject or "").split(",") if t.strip()] scripture_list=[t.strip() for t in (entry.scripture_raw or "").split(";") if t.strip()] return render(request,"entry_view.html",{"entry":entry,"subject_list":subject_list,"scripture_list":scripture_list,"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]) subject_list=[t.strip() for t in (entry.subject or "").split(",") if t.strip()] scripture_list=[t.strip() for t in (entry.scripture_raw or "").split(";") if t.strip()] return render(request,"entry_view.html",{"entry":entry,"subject_list":subject_list,"scripture_list":scripture_list,"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 subject_list=[t.strip() for t in (entry.subject or "").split(",") if t.strip()] scripture_list=[t.strip() for t in (entry.scripture_raw or "").split(";") if t.strip()] return render(request,"entry_view.html",{"entry":entry,"subject_list":subject_list,"scripture_list":scripture_list,"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: form=EntryForm(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}) 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): if request.method=="POST": form=ImportForm(request.POST, request.FILES) if form.is_valid(): try: report=import_csv_bytes(form.cleaned_data["file"].read(), dry_run=form.cleaned_data["dry_run"]) return render(request,"import_result.html",{"report":report,"dry_run":form.cleaned_data["dry_run"]}) 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): ts=date.today().strftime("%Y-%m-%d") response=HttpResponse(content_type='text/csv') response['Content-Disposition']=f'attachment; filename="illustrations_backup_{ts}.csv"' w=csv.writer(response) w.writerow(["Subject","Illustration","Application","Scripture","Source","Talk Number","Talk Title","Code","Date","Date Edited"]) for e in Entry.objects.all().order_by("id"): w.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() buckets=month_buckets_last_12(today) series=[(label, Entry.objects.filter(date_added__gte=start, date_added__lt=end).count()) for label,start,end in buckets] peak=max((v for _,v in series), default=1) heights=[(label, value, 8 + int((value/peak)*100) if peak else 8) for label,value in series] 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,"heights":heights,"top_subjects":top_subjects})