From a91198c7e612403300ccdbd729a64aff02070b55 Mon Sep 17 00:00:00 2001 From: Joshua Laymon Date: Thu, 14 Aug 2025 12:51:39 +0000 Subject: [PATCH] Update web/core/views.py --- web/core/views.py | 104 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/web/core/views.py b/web/core/views.py index 5efcf6e..eeb45cc 100644 --- a/web/core/views.py +++ b/web/core/views.py @@ -457,4 +457,108 @@ def normalize_scripture(request): "preview": preview, "limit": limit, }, + ) + @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() + + # ---- 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 = [ + (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(?:[1-3]\s+)?[A-Za-z\.]+(?:\s+[A-Za-z\.]+){0,2})" + r"(?:\s+(?P\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, + }, ) \ No newline at end of file