Update web/core/views.py

This commit is contained in:
Joshua Laymon 2025-08-16 22:30:05 +00:00
parent cc7451b1aa
commit 0e4091d132

View File

@ -1,21 +1,22 @@
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 django.views.decorators.http import require_http_methods
from datetime import date, timedelta from datetime import date, timedelta
import csv import csv
import re import re
from .models import Entry from django.contrib import messages
from django.contrib.auth import authenticate, login
from django.contrib.auth.decorators import login_required, user_passes_test
from django.db.models import Q
from django.http import HttpResponse, JsonResponse
from django.shortcuts import render, redirect, get_object_or_404
from django.views.decorators.http import require_http_methods
from .forms import ImportForm, EntryForm from .forms import ImportForm, EntryForm
from .models import Entry
from .scripture_normalizer import normalize_scripture_field # NEW
from .source_normalizer import normalize_source_field # NEW
from .subject_normalizer import normalize_subject_field # NEW
from .utils import terms, has_wildcards, wildcard_to_regex, import_csv_bytes from .utils import terms, has_wildcards, wildcard_to_regex, import_csv_bytes
from .scripture_normalizer import normalize_scripture_field # <-- NEW
from .source_normalizer import normalize_source_field # NEW
from .subject_normalizer import normalize_subject_field # NEW
from django.http import JsonResponse
# Order + labels used in the Search UI # Order + labels used in the Search UI
FIELD_ORDER = [ FIELD_ORDER = [
@ -390,7 +391,7 @@ def stats_page(request):
) )
# ========= NEW: Scripture Normalizer endpoint ========= # ========= Scripture Normalizer =========
@login_required @login_required
@user_passes_test(is_admin) @user_passes_test(is_admin)
@ -413,7 +414,6 @@ def normalize_scripture(request):
preview = [] preview = []
if apply: if apply:
# write in batches to keep transactions short
from django.db import transaction from django.db import transaction
batch, pending = 500, [] batch, pending = 500, []
for e in qs.iterator(): for e in qs.iterator():
@ -444,11 +444,11 @@ def normalize_scripture(request):
changed += 1 changed += 1
preview.append((e.id, original, normalized)) preview.append((e.id, original, normalized))
preview = preview[:100] # keep the table reasonable preview = preview[:100]
messages.info( messages.info(
request, request,
f"{'Applied' if apply else 'Dryrun'}: {changed} entries " f"{'Applied' if apply else 'Dry-run'}: {changed} entries "
f"{'changed' if apply else 'would change'}; {warnings_total} warnings." f"{'changed' if apply else 'would change'}; {warnings_total} warnings."
) )
return render( return render(
@ -463,8 +463,8 @@ def normalize_scripture(request):
}, },
) )
from django.views.decorators.http import require_http_methods
from django.contrib.auth.decorators import login_required, user_passes_test # ========= Source Normalizer =========
@login_required @login_required
@user_passes_test(is_admin) @user_passes_test(is_admin)
@ -521,7 +521,7 @@ def normalize_source(request):
messages.info( messages.info(
request, request,
f"{'Applied' if apply else 'Dryrun'}: {changed} entries " f"{'Applied' if apply else 'Dry-run'}: {changed} entries "
f"{'changed' if apply else 'would change'}; {warnings_total} warnings." f"{'changed' if apply else 'would change'}; {warnings_total} warnings."
) )
return render( return render(
@ -535,112 +535,10 @@ def normalize_source(request):
"limit": limit, "limit": limit,
}, },
) )
@login_required
def stats_page(request):
from collections import Counter
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()
# ---- Adds per month (existing logic) ----
months = []
y = today.year
m = today.month
for i in range(12):
mm = m - i
yy = y
while mm <= 0:
mm += 12
yy -= 1
from datetime import date as _d
start = _d(yy, mm, 1)
end = _d(yy + 1, 1, 1) if mm == 12 else _d(yy, mm + 1, 1)
label = f"{yy}-{mm:02d}"
months.append((label, start, end))
months = list(reversed(months))
series = [ # ========= Subject Normalizer =========
(label, Entry.objects.filter(date_added__gte=start, date_added__lt=end).count())
for label, start, end in months
]
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]
# ---- Top subjects (existing logic) ----
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)]
# ---- Scripture analytics (NEW) ----
# Expect canonical like: "Matt. 5:14; Ps. 1:1,2; 1 Cor. 13:4-7"
# Split on semicolons; capture book and chap/verses if present.
BOOK_RE = re.compile(
r"^\s*(?P<book>(?:[1-3]\s+)?[A-Za-z\.]+(?:\s+[A-Za-z\.]+){0,2})"
r"(?:\s+(?P<cv>\d+(?::[\d,\-\u2013\u2014]+)?))?\s*$"
)
books_counter = Counter()
refs_counter = Counter()
ref_per_entry_counts = []
entries_with_scripture = 0
scriptures = Entry.objects.exclude(scripture_raw="") \
.values_list("scripture_raw", flat=True)
for raw in scriptures:
pieces = [p for p in re.split(r"\s*;\s*", raw or "") if p.strip()]
if not pieces:
continue
entries_with_scripture += 1
refs_this_entry = 0
for p in pieces:
m = BOOK_RE.match(p)
if not m:
continue
book = (m.group("book") or "").strip()
cv = (m.group("cv") or "").strip()
if not book:
continue
books_counter[book] += 1
if cv:
refs_counter[f"{book} {cv}"] += 1
refs_this_entry += 1
ref_per_entry_counts.append(refs_this_entry)
top_books = books_counter.most_common(10)
top_refs = refs_counter.most_common(10)
avg_refs_per_entry = round(
(sum(ref_per_entry_counts) / len(ref_per_entry_counts))
if ref_per_entry_counts else 0.0, 2
)
book_distribution = books_counter.most_common(30) # handy for future charts
return render(
request,
"stats.html",
{
"total": total,
"last30": last30,
"last365": last365,
"series": series,
"heights": heights,
"top_subjects": top_subjects,
# NEW context for the template
"entries_with_scripture": entries_with_scripture,
"avg_refs_per_entry": avg_refs_per_entry,
"top_books": top_books,
"top_refs": top_refs,
"book_distribution": book_distribution,
},
)
@login_required @login_required
@user_passes_test(is_admin) @user_passes_test(is_admin)
@require_http_methods(["GET", "POST"]) @require_http_methods(["GET", "POST"])
@ -662,7 +560,6 @@ def normalize_subjects(request):
preview = [] preview = []
if apply: if apply:
# write in batches to keep transactions short
from django.db import transaction from django.db import transaction
batch, pending = 500, [] batch, pending = 500, []
for e in qs.iterator(): for e in qs.iterator():
@ -680,7 +577,6 @@ def normalize_subjects(request):
obj.save(update_fields=["subject"]) obj.save(update_fields=["subject"])
pending.clear() pending.clear()
if pending: if pending:
from django.db import transaction
with transaction.atomic(): with transaction.atomic():
for obj in pending: for obj in pending:
obj.save(update_fields=["subject"]) obj.save(update_fields=["subject"])
@ -694,7 +590,7 @@ def normalize_subjects(request):
changed += 1 changed += 1
preview.append((e.id, original, normalized)) preview.append((e.id, original, normalized))
preview = preview[:100] # keep table reasonable preview = preview[:100]
messages.info( messages.info(
request, request,
@ -712,11 +608,9 @@ def normalize_subjects(request):
"limit": limit, "limit": limit,
}, },
) )
from django.http import JsonResponse
# If not already imported above:
# from django.contrib.auth.decorators import login_required # ========= API: Recently Viewed (for 20-word snippet + correct link) =========
# from django.utils import timezone # only if you need now()
# import re # already in your file
@login_required @login_required
def api_get_recent_views(request): def api_get_recent_views(request):
@ -725,12 +619,14 @@ def api_get_recent_views(request):
including the illustration text so the UI can show a 20-word snippet. including the illustration text so the UI can show a 20-word snippet.
""" """
# Model: RecentView with fields: user (FK), entry (FK Entry), viewed_at (DateTime) # Model: RecentView with fields: user (FK), entry (FK Entry), viewed_at (DateTime)
from .models import RecentView # inline to avoid breaking if you split files from .models import RecentView # local import to avoid issues in old migrations
recents = (RecentView.objects recents = (
.filter(user=request.user) RecentView.objects
.select_related('entry') .filter(user=request.user)
.order_by('-viewed_at')[:50]) .select_related("entry")
.order_by("-viewed_at")[:50]
)
items = [] items = []
for rv in recents: for rv in recents: