Illustrations/web/core/views.py
Joshua Laymon 6426211800 Update
2025-08-12 22:29:18 -05:00

160 lines
8.3 KiB
Python

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})