Merge pull request 'seperate out WOL publication codes, offer GUI for viewing, and add new KC publication source' (#6) from develop into main
Reviewed-on: https://git.lan/joshlaymon/Illustrations/pulls/6
This commit is contained in:
commit
a8008c37b4
@ -1245,4 +1245,51 @@ def set_theme(request):
|
|||||||
|
|
||||||
request.session["theme"] = theme
|
request.session["theme"] = theme
|
||||||
messages.success(request, f"Theme set to {theme.title()}.")
|
messages.success(request, f"Theme set to {theme.title()}.")
|
||||||
return redirect("settings_home")
|
return redirect("settings_home")
|
||||||
|
|
||||||
|
# web/core/views.py
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.auth.decorators import login_required, user_passes_test
|
||||||
|
from django.http import JsonResponse, HttpResponseBadRequest
|
||||||
|
from django.views.decorators.http import require_POST
|
||||||
|
|
||||||
|
@require_POST
|
||||||
|
@login_required
|
||||||
|
@user_passes_test(lambda u: u.is_superuser)
|
||||||
|
def api_update_pub_codes(request):
|
||||||
|
"""
|
||||||
|
Accepts a 'json' field (string) that should parse to {"pub_codes": [..]}.
|
||||||
|
Normalizes, de-duplicates, and writes to web/static/data/wol-pub-codes.v1.json.
|
||||||
|
"""
|
||||||
|
payload = request.POST.get("json") or (request.body.decode("utf-8") if request.body else "")
|
||||||
|
if not payload:
|
||||||
|
return HttpResponseBadRequest("Missing 'json'.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = json.loads(payload)
|
||||||
|
except Exception as e:
|
||||||
|
return HttpResponseBadRequest(f"Invalid JSON: {e}")
|
||||||
|
|
||||||
|
if not isinstance(data, dict) or "pub_codes" not in data or not isinstance(data["pub_codes"], list):
|
||||||
|
return HttpResponseBadRequest('JSON must be an object with a "pub_codes" array.')
|
||||||
|
|
||||||
|
# Normalize to unique, lowercase, trimmed strings
|
||||||
|
seen = set()
|
||||||
|
codes = []
|
||||||
|
for c in data["pub_codes"]:
|
||||||
|
s = str(c or "").strip().lower()
|
||||||
|
if s and s not in seen:
|
||||||
|
seen.add(s)
|
||||||
|
codes.append(s)
|
||||||
|
|
||||||
|
# Write back to static data file
|
||||||
|
target_path = os.path.join(settings.BASE_DIR, "web", "static", "data", "wol-pub-codes.v1.json")
|
||||||
|
try:
|
||||||
|
with open(target_path, "w", encoding="utf-8") as f:
|
||||||
|
json.dump({"pub_codes": codes}, f, ensure_ascii=False, indent=2)
|
||||||
|
except Exception as e:
|
||||||
|
return HttpResponseBadRequest(f"Could not write file: {e}")
|
||||||
|
|
||||||
|
return JsonResponse({"ok": True, "count": len(codes)})
|
||||||
@ -1,6 +1,6 @@
|
|||||||
# illustrations/urls.py
|
# illustrations/urls.py
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import path, re_path
|
from django.urls import path, re_path, include
|
||||||
from django.contrib.auth import views as auth_views
|
from django.contrib.auth import views as auth_views
|
||||||
from django.views.generic import TemplateView # NEW: for manifest / service worker / offline
|
from django.views.generic import TemplateView # NEW: for manifest / service worker / offline
|
||||||
|
|
||||||
@ -54,6 +54,7 @@ urlpatterns = [
|
|||||||
path("api/get-recent-views/", feat_views.api_get_recent_views, name="api_get_recent_views"),
|
path("api/get-recent-views/", feat_views.api_get_recent_views, name="api_get_recent_views"),
|
||||||
path("settings/", core_views.settings_home, name="settings_home"),
|
path("settings/", core_views.settings_home, name="settings_home"),
|
||||||
path("settings/theme/", core_views.set_theme, name="set_theme"), # ✅ FIXED
|
path("settings/theme/", core_views.set_theme, name="set_theme"), # ✅ FIXED
|
||||||
|
path("api/update-pub-codes/", core_views.api_update_pub_codes, name="api_update_pub_codes"),
|
||||||
|
|
||||||
# --- NEW (PWA) ---
|
# --- NEW (PWA) ---
|
||||||
re_path(
|
re_path(
|
||||||
|
|||||||
10
web/static/data/wol-pub-codes.v1.json
Normal file
10
web/static/data/wol-pub-codes.v1.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"pub_codes": [
|
||||||
|
"wp", "ws", "yb", "mwb", "w", "g",
|
||||||
|
"ap", "apf", "be", "bh", "br", "bt", "btg", "cf", "cl", "ct", "dp",
|
||||||
|
"fg", "fy", "gt", "hb", "im", "ip", "it", "jv", "ka", "kj", "kl",
|
||||||
|
"lf", "lff", "ll", "ly", "my", "od", "pe", "po", "pt", "rr", "rs",
|
||||||
|
"sg", "sh", "si", "td", "tp", "tr", "ts", "un", "jy",
|
||||||
|
"uw", "su", "re", "lvs", "lp", "yy", "yp2", "yp", "sl", "pm", "kc"
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -1,22 +1,47 @@
|
|||||||
/* source-validator.v1.js
|
/* source-validator.v1.js
|
||||||
Centralizes logic for deciding if a "Source" string should link to WOL.
|
Centralizes logic for deciding if a "Source" string should link to WOL.
|
||||||
|
Loads publication codes from /static/data/wol-pub-codes.v1.json.
|
||||||
|
|
||||||
Exposes:
|
Exposes:
|
||||||
- SourceValidator.isWOLSource(text) -> boolean
|
- SourceValidator.isWOLSource(text) -> boolean
|
||||||
- SourceValidator.buildWOLSearchURL(text) -> string
|
- SourceValidator.buildWOLSearchURL(text) -> string
|
||||||
*/
|
*/
|
||||||
window.SourceValidator = (function () {
|
window.SourceValidator = (function () {
|
||||||
// Publications / codes that produce valid WOL links.
|
// ---- Load publication codes (sync so callers can use API immediately) ----
|
||||||
// Added: uw, su, re, lvs, rs (rs was already present).
|
function loadPubCodesSync() {
|
||||||
const PUB_CODES = [
|
try {
|
||||||
"wp","ws","yb","mwb","w","g","ap","apf","be","bh","br","bt","btg","cf","cl","ct","dp",
|
var xhr = new XMLHttpRequest();
|
||||||
"fg","fy","gt","hb","im","ip","it","jv","ka","kj","kl","lf","lff","ll","ly","my","od",
|
xhr.open("GET", "/static/data/wol-pub-codes.v1.json", false); // synchronous
|
||||||
"pe","po","pt","rr","rs","sg","sh","si","td","tp","tr","ts","un","jy",
|
xhr.send(null);
|
||||||
"uw","su","re","lvs","lp","yy","yp2","yp","jv","sl","pm"// new
|
if (xhr.status >= 200 && xhr.status < 300) {
|
||||||
];
|
var data = JSON.parse(xhr.responseText || "{}");
|
||||||
|
if (data && Array.isArray(data.pub_codes)) {
|
||||||
|
// de-duplicate and normalize to lowercase strings
|
||||||
|
var uniq = Object.create(null), out = [];
|
||||||
|
for (var i = 0; i < data.pub_codes.length; i++) {
|
||||||
|
var c = String(data.pub_codes[i] || "").trim().toLowerCase();
|
||||||
|
if (!c) continue;
|
||||||
|
if (!uniq[c]) { uniq[c] = 1; out.push(c); }
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// fall through to fallback
|
||||||
|
}
|
||||||
|
// Fallback (very small set) — only used if JSON cannot be loaded
|
||||||
|
return ["w", "wp", "ws", "g", "rs"];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Publications / codes loaded from JSON
|
||||||
|
var PUB_CODES = loadPubCodesSync();
|
||||||
|
|
||||||
|
// Choose the longest matching code at the start (so "ws" beats "w").
|
||||||
|
var PUB_CODES_SORTED = PUB_CODES.slice().sort(function (a, b) { return b.length - a.length; });
|
||||||
|
|
||||||
// Year validation rules (applies only if a year can be parsed from the source).
|
// Year validation rules (applies only if a year can be parsed from the source).
|
||||||
// Watchtower (w/wp/ws) back to 1950; Awake (g) back to 1970.
|
// Watchtower (w/wp/ws) back to 1950; Awake (g) back to 1970.
|
||||||
const YEAR_RULES = [
|
var YEAR_RULES = [
|
||||||
{ codes: ["w","wp","ws"], minYear: 1950 },
|
{ codes: ["w","wp","ws"], minYear: 1950 },
|
||||||
{ codes: ["g"], minYear: 1970 }
|
{ codes: ["g"], minYear: 1970 }
|
||||||
];
|
];
|
||||||
@ -24,12 +49,10 @@ window.SourceValidator = (function () {
|
|||||||
// Normalize helper
|
// Normalize helper
|
||||||
function normalize(s) { return (s || "").trim().toLowerCase(); }
|
function normalize(s) { return (s || "").trim().toLowerCase(); }
|
||||||
|
|
||||||
// Choose the longest matching code at the start (so "ws" beats "w").
|
|
||||||
const PUB_CODES_SORTED = [...PUB_CODES].sort((a,b)=>b.length-a.length);
|
|
||||||
|
|
||||||
function leadingCode(textLower) {
|
function leadingCode(textLower) {
|
||||||
for (const code of PUB_CODES_SORTED) {
|
for (var i = 0; i < PUB_CODES_SORTED.length; i++) {
|
||||||
if (textLower.startsWith(code)) return code;
|
var code = PUB_CODES_SORTED[i];
|
||||||
|
if (textLower.indexOf(code) === 0) return code;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -37,60 +60,51 @@ window.SourceValidator = (function () {
|
|||||||
// Try to extract a year that appears right after the leading code (allow spaces),
|
// Try to extract a year that appears right after the leading code (allow spaces),
|
||||||
// accepting either 4-digit (e.g., 1955, 2001) or 2-digit (e.g., 55, 95, 12) forms.
|
// accepting either 4-digit (e.g., 1955, 2001) or 2-digit (e.g., 55, 95, 12) forms.
|
||||||
function extractYearAfterCode(textLower, code) {
|
function extractYearAfterCode(textLower, code) {
|
||||||
let s = textLower.slice(code.length).trim();
|
var s = textLower.slice(code.length).trim();
|
||||||
|
|
||||||
// 1) Look for a 4-digit year first
|
// 1) Look for a 4-digit year first (1800–2099)
|
||||||
let m = s.match(/\b(1[89]\d{2}|20\d{2})\b/); // 1800-2099 (broad, but OK)
|
var m = s.match(/\b(1[89]\d{2}|20\d{2})\b/);
|
||||||
if (m) {
|
if (m) return parseInt(m[1], 10);
|
||||||
return parseInt(m[1], 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2) If not found, accept a 2-digit year at the *start* of the remainder,
|
// 2) If not found, accept a 2-digit year at the *start* of the remainder
|
||||||
// or right after an optional space: e.g., "w55 1/1", "w 95", "g70 1/22"
|
// (e.g., "w55 1/1", "w 95", "g70 1/22")
|
||||||
m = s.match(/^\s*(\d{2})\b/);
|
m = s.match(/^\s*(\d{2})\b/);
|
||||||
if (m) {
|
if (m) {
|
||||||
const yy = parseInt(m[1], 10);
|
var yy = parseInt(m[1], 10);
|
||||||
// Infer century based on publication + threshold logic
|
if (code === "g") return yy >= 70 ? (1900 + yy) : (2000 + yy);
|
||||||
// - For Watchtower: 50–99 -> 1950–1999; 00–49 -> 2000–2049
|
if (code === "w" || code === "wp" || code === "ws")
|
||||||
// - For Awake: 70–99 -> 1970–1999; 00–69 -> 2000–2069
|
return yy >= 50 ? (1900 + yy) : (2000 + yy);
|
||||||
if (code === "g") {
|
|
||||||
return yy >= 70 ? (1900 + yy) : (2000 + yy);
|
|
||||||
}
|
|
||||||
if (code === "w" || code === "wp" || code === "ws") {
|
|
||||||
return yy >= 50 ? (1900 + yy) : (2000 + yy);
|
|
||||||
}
|
|
||||||
// For other pubs, if they ever include 2-digit years, assume 1900+yy≥70 else 2000+yy
|
|
||||||
return yy >= 70 ? (1900 + yy) : (2000 + yy);
|
return yy >= 70 ? (1900 + yy) : (2000 + yy);
|
||||||
}
|
}
|
||||||
|
|
||||||
// No recognizable year → don't enforce year limits
|
// No recognizable year → don't enforce year limits
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function passesYearRuleIfPresent(textLower, code) {
|
function passesYearRuleIfPresent(textLower, code) {
|
||||||
const rule = YEAR_RULES.find(r => r.codes.includes(code));
|
var rule = null;
|
||||||
|
for (var i=0;i<YEAR_RULES.length;i++){
|
||||||
|
if (YEAR_RULES[i].codes.indexOf(code) !== -1) { rule = YEAR_RULES[i]; break; }
|
||||||
|
}
|
||||||
if (!rule) return true; // no year rule for this pub
|
if (!rule) return true; // no year rule for this pub
|
||||||
|
|
||||||
const y = extractYearAfterCode(textLower, code);
|
var y = extractYearAfterCode(textLower, code);
|
||||||
if (y == null) return true; // no year present → allow
|
if (y == null) return true; // no year present → allow
|
||||||
return y >= rule.minYear;
|
return y >= rule.minYear;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isWOLSource(text) {
|
function isWOLSource(text) {
|
||||||
const t = normalize(text);
|
var t = normalize(text);
|
||||||
if (!t) return false;
|
if (!t) return false;
|
||||||
|
var code = leadingCode(t);
|
||||||
const code = leadingCode(t);
|
|
||||||
if (!code) return false;
|
if (!code) return false;
|
||||||
|
|
||||||
// If starts with a known pub code, it’s WOL-capable — but enforce year rules where applicable.
|
// If starts with a known pub code, it’s WOL-capable — but enforce year rules where applicable.
|
||||||
return passesYearRuleIfPresent(t, code);
|
return passesYearRuleIfPresent(t, code);
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildWOLSearchURL(text) {
|
function buildWOLSearchURL(text) {
|
||||||
const q = encodeURIComponent(text || "");
|
var q = encodeURIComponent(text || "");
|
||||||
return `https://wol.jw.org/en/wol/l/r1/lp-e?q=${q}`;
|
return "https://wol.jw.org/en/wol/l/r1/lp-e?q=" + q;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { isWOLSource, buildWOLSearchURL };
|
return { isWOLSource: isWOLSource, buildWOLSearchURL: buildWOLSearchURL };
|
||||||
})();
|
})();
|
||||||
@ -162,6 +162,25 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- Publication Codes (JSON) – superusers only -->
|
||||||
|
<section class="card cc-panel">
|
||||||
|
<div class="cc-panel-head">
|
||||||
|
<div class="cc-kicker">Comms</div>
|
||||||
|
<h2 class="cc-panel-title">Publication Codes (JSON)</h2>
|
||||||
|
</div>
|
||||||
|
<div class="cc-panel-body">
|
||||||
|
<div class="cc-row">
|
||||||
|
<textarea id="pubCodesEditor" class="tool-input" rows="14"
|
||||||
|
style="width:100%; font-family: ui-monospace, SFMono-Regular, Menlo, monospace;"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="cc-actions">
|
||||||
|
<button type="button" id="pubCodesReloadBtn" class="btn">Reload</button>
|
||||||
|
<button type="button" id="pubCodesSaveBtn" class="btn btn-primary">Save</button>
|
||||||
|
</div>
|
||||||
|
<div id="pubCodesStatus" class="tiny" style="margin-top:6px; color:#64748b;"></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -335,6 +354,71 @@ textarea.tool-input{min-height:140px;resize:vertical;width:100%;}
|
|||||||
});
|
});
|
||||||
window.addEventListener('keydown', (e)=>{ if (e.key === 'Escape' && modal.style.display === 'flex') closeModal(); });
|
window.addEventListener('keydown', (e)=>{ if (e.key === 'Escape' && modal.style.display === 'flex') closeModal(); });
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
// === Publication Codes Editor (superuser) ===
|
||||||
|
(function(){
|
||||||
|
const ta = document.getElementById('pubCodesEditor');
|
||||||
|
const btnR = document.getElementById('pubCodesReloadBtn');
|
||||||
|
const btnS = document.getElementById('pubCodesSaveBtn');
|
||||||
|
const stat = document.getElementById('pubCodesStatus');
|
||||||
|
if (!ta || !btnR || !btnS) return; // not superuser or card missing
|
||||||
|
|
||||||
|
function setStatus(msg, ok=true){
|
||||||
|
if (!stat) return;
|
||||||
|
stat.textContent = msg;
|
||||||
|
stat.style.color = ok ? '#64748b' : '#b91c1c';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reloadJSON(){
|
||||||
|
try{
|
||||||
|
setStatus('Loading…');
|
||||||
|
const r = await fetch('/static/data/wol-pub-codes.v1.json', { cache:'no-store' });
|
||||||
|
if (!r.ok) throw new Error('HTTP '+r.status);
|
||||||
|
const data = await r.json();
|
||||||
|
ta.value = JSON.stringify(data, null, 2);
|
||||||
|
setStatus('Loaded.');
|
||||||
|
}catch(err){
|
||||||
|
setStatus('Failed to load JSON: ' + (err?.message||err), false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveJSON(){
|
||||||
|
let parsed;
|
||||||
|
try{
|
||||||
|
parsed = JSON.parse(ta.value);
|
||||||
|
if (!parsed || typeof parsed !== 'object' || !Array.isArray(parsed.pub_codes))
|
||||||
|
throw new Error('JSON must be an object with a "pub_codes" array.');
|
||||||
|
}catch(err){
|
||||||
|
setStatus('Invalid JSON: ' + (err?.message||err), false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try{
|
||||||
|
setStatus('Saving…');
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('json', JSON.stringify(parsed));
|
||||||
|
const r = await fetch("{% url 'api_update_pub_codes' %}", {
|
||||||
|
method: 'POST',
|
||||||
|
body: fd,
|
||||||
|
credentials: 'same-origin',
|
||||||
|
headers: { 'X-CSRFToken': (document.cookie.match(/(^|;)\s*csrftoken\s*=\s*([^;]+)/)||[]).pop() || '' }
|
||||||
|
});
|
||||||
|
if (!r.ok) {
|
||||||
|
const txt = await r.text().catch(()=>String(r.status));
|
||||||
|
throw new Error(txt.slice(0,200));
|
||||||
|
}
|
||||||
|
const out = await r.json().catch(()=>({ok:false}));
|
||||||
|
if (!out.ok) throw new Error('Server rejected the update.');
|
||||||
|
setStatus('Saved. ' + out.count + ' codes.');
|
||||||
|
}catch(err){
|
||||||
|
setStatus('Save failed: ' + (err?.message||err), false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
btnR.addEventListener('click', reloadJSON);
|
||||||
|
btnS.addEventListener('click', saveJSON);
|
||||||
|
reloadJSON(); // initial load
|
||||||
|
})();
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
v4.1.22
|
v4.1.24
|
||||||
Loading…
Reference in New Issue
Block a user