This commit is contained in:
Joshua Laymon 2025-08-12 22:29:18 -05:00
parent 3458501272
commit 6426211800
7 changed files with 23 additions and 16 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@ -1 +1 @@
Place illustrations_seed.csv here. Place your seed file as illustrations_seed.csv here if you want auto-import on first boot.

View File

@ -1,6 +1,6 @@
import csv, io, re import csv, io, re
from dateutil import parser as dateparser from dateutil import parser as dateparser
from datetime import date, timedelta from datetime import date
from .models import Entry, ScriptureRef from .models import Entry, ScriptureRef
SCR_REF_RE = re.compile(r"""^\s*([1-3]?\s*[A-Za-z\.]+)\s+(\d+)(?::(\d+))?(?:\s*[-–—]\s*(\d+)(?::(\d+))?)?\s*$""", re.VERBOSE) SCR_REF_RE = re.compile(r"""^\s*([1-3]?\s*[A-Za-z\.]+)\s+(\d+)(?::(\d+))?(?:\s*[-–—]\s*(\d+)(?::(\d+))?)?\s*$""", re.VERBOSE)
@ -11,7 +11,7 @@ def normalize_book(s):
return BOOK_ALIASES.get(b, s.strip()) return BOOK_ALIASES.get(b, s.strip())
def parse_scripture(s): def parse_scripture(s):
items=[]; items=[]
for p in [x.strip() for x in (s or '').split(';') if x.strip()]: for p in [x.strip() for x in (s or '').split(';') if x.strip()]:
m = SCR_REF_RE.match(p) m = SCR_REF_RE.match(p)
if not m: items.append(None); continue if not m: items.append(None); continue
@ -69,7 +69,6 @@ def import_csv_bytes(b: bytes, dry_run=True):
obj.save(); obj.scripture_refs.all().delete(); report["updated"]+=1 obj.save(); obj.scripture_refs.all().delete(); report["updated"]+=1
else: else:
obj=Entry.objects.create(**data); report["inserted"]+=1 obj=Entry.objects.create(**data); report["inserted"]+=1
from .models import ScriptureRef
for it in parsed: for it in parsed:
if it: ScriptureRef.objects.create(entry=obj, **it) if it: ScriptureRef.objects.create(entry=obj, **it)
except Exception as e: except Exception as e:

View File

@ -43,7 +43,6 @@ def search_page(request):
field_options=[{"name":k,"label":label,"checked":bool(selected.get(k))} for k,label in FIELD_ORDER] field_options=[{"name":k,"label":label,"checked":bool(selected.get(k))} for k,label in FIELD_ORDER]
q=request.GET.get("q","").strip() q=request.GET.get("q","").strip()
if q: if q:
# AND across terms, OR across selected fields per term
term_list=terms(q) term_list=terms(q)
qs=Entry.objects.all() qs=Entry.objects.all()
fields=[f for f,sel in selected.items() if sel] fields=[f for f,sel in selected.items() if sel]
@ -56,7 +55,9 @@ def search_page(request):
request.session["result_ids"]=ids request.session["result_ids"]=ids
if ids: if ids:
entry=Entry.objects.get(pk=ids[0]) entry=Entry.objects.get(pk=ids[0])
return render(request,"entry_view.html",{"entry":entry,"locked":True,"position":1,"count":len(ids),"from_search":True}) 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() total=Entry.objects.count()
return render(request,"search.html",{"q":q,"selected":selected,"field_options":field_options,"total":total}) 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]}) 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]})
@ -67,7 +68,9 @@ def nav_next(request):
if not ids: return redirect("search") if not ids: return redirect("search")
idx=int(request.GET.get("i","0")); idx=min(idx+1, len(ids)-1) idx=int(request.GET.get("i","0")); idx=min(idx+1, len(ids)-1)
entry=get_object_or_404(Entry, pk=ids[idx]) 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)}) 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 @login_required
def nav_prev(request): def nav_prev(request):
@ -75,14 +78,18 @@ def nav_prev(request):
if not ids: return redirect("search") if not ids: return redirect("search")
idx=int(request.GET.get("i","0")); idx=max(idx-1, 0) idx=int(request.GET.get("i","0")); idx=max(idx-1, 0)
entry=get_object_or_404(Entry, pk=ids[idx]) 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)}) 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 @login_required
def entry_view(request, entry_id): def entry_view(request, entry_id):
entry=get_object_or_404(Entry, pk=entry_id) entry=get_object_or_404(Entry, pk=entry_id)
ids=request.session.get("result_ids",[]); count=len(ids) ids=request.session.get("result_ids",[]); count=len(ids)
position=ids.index(entry.id)+1 if entry.id in ids else 1 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}) 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 @login_required
def entry_edit(request, entry_id): def entry_edit(request, entry_id):
@ -141,10 +148,12 @@ def stats_page(request):
last365=Entry.objects.filter(date_added__gte=today - timedelta(days=365)).count() last365=Entry.objects.filter(date_added__gte=today - timedelta(days=365)).count()
buckets=month_buckets_last_12(today) 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] 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 from collections import Counter
counts=Counter() counts=Counter()
for subj in Entry.objects.exclude(subject="").values_list("subject", flat=True): 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()]: for tag in [t.strip() for t in subj.split(",") if t.strip()]:
counts[tag.lower()]+=1 counts[tag.lower()]+=1
top_subjects=[{"name":n.title(),"count":c} for n,c in counts.most_common(10)] 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}) return render(request,"stats.html",{"total":total,"last30":last30,"last365":last365,"series":series,"heights":heights,"top_subjects":top_subjects})

View File

@ -4,7 +4,7 @@ BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = os.getenv("DJANGO_SECRET_KEY","dev-insecure") SECRET_KEY = os.getenv("DJANGO_SECRET_KEY","dev-insecure")
DEBUG = os.getenv("DJANGO_DEBUG","False") == "True" DEBUG = os.getenv("DJANGO_DEBUG","False") == "True"
ALLOWED_HOSTS = os.getenv("DJANGO_ALLOWED_HOSTS","*").split(",") ALLOWED_HOSTS = [h.strip() for h in os.getenv("DJANGO_ALLOWED_HOSTS","*").split(",") if h.strip()]
CSRF_TRUSTED_ORIGINS = [x.strip() for x in os.getenv("CSRF_TRUSTED_ORIGINS","").split(",") if x.strip()] CSRF_TRUSTED_ORIGINS = [x.strip() for x in os.getenv("CSRF_TRUSTED_ORIGINS","").split(",") if x.strip()]
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")

View File

@ -18,11 +18,11 @@
<div class="row"> <div class="row">
<div> <div>
<label>Subject</label> <label>Subject</label>
<div class="chips">{% for t in entry.subject.split(',') %}{% if t.strip %}<span class="chip">{{ t.strip }}</span>{% endif %}{% endfor %}</div> <div class="chips">{% for t in subject_list %}<span class="chip">{{ t }}</span>{% endfor %}</div>
</div> </div>
<div> <div>
<label>Scripture</label> <label>Scripture</label>
<div class="chips">{% for t in entry.scripture_raw.split(';') %}{% if t.strip %}<span class="chip" style="background:#eef4ff;">{{ t.strip }}</span>{% endif %}{% endfor %}</div> <div class="chips">{% for t in scripture_list %}<span class="chip" style="background:#eef4ff;">{{ t }}</span>{% endfor %}</div>
</div> </div>
</div> </div>
<div class="spacer"></div> <div class="spacer"></div>

View File

@ -13,9 +13,8 @@
<div class="card"> <div class="card">
<div class="small">Entries per month (by Date Added)</div> <div class="small">Entries per month (by Date Added)</div>
<div style="display:flex; gap:6px; align-items:flex-end; height:120px; margin-top:8px;"> <div style="display:flex; gap:6px; align-items:flex-end; height:120px; margin-top:8px;">
{% with peak=series|map:'1' %}{% endwith %} {% for label, value, height in heights %}
{% for label, value in series %} <div title="{{ label }}: {{ value }}" style="width:24px; background:#dbe7ff; border:1px solid #c8d6ff; height: {{ height }}px;"></div>
<div title="{{ label }}: {{ value }}" style="width:24px; background:#dbe7ff; border:1px solid #c8d6ff; height: {{ value|add:5 }}px;"></div>
{% endfor %} {% endfor %}
</div> </div>
<div class="small" style="display:flex; gap:8px; flex-wrap:wrap; margin-top:6px;"> <div class="small" style="display:flex; gap:8px; flex-wrap:wrap; margin-top:6px;">