101 lines
3.8 KiB
Python
101 lines
3.8 KiB
Python
from django.core.management.base import BaseCommand
|
|
from django.db import transaction
|
|
from sbas.models import Question
|
|
import re
|
|
from textwrap import shorten
|
|
|
|
UNWRAP_FIELDS = [
|
|
"stem",
|
|
"feedback",
|
|
"a_answer",
|
|
"a_feedback",
|
|
"b_answer",
|
|
"b_feedback",
|
|
"c_answer",
|
|
"c_feedback",
|
|
"d_answer",
|
|
"d_feedback",
|
|
"e_answer",
|
|
"e_feedback",
|
|
]
|
|
|
|
RE_SINGLE_P = re.compile(r"\s*<p\b[^>]*>(.*)</p>\s*$", flags=re.IGNORECASE | re.DOTALL)
|
|
|
|
|
|
def unwrap_single_p(html: str) -> str:
|
|
if not html or not isinstance(html, str):
|
|
return html
|
|
m = RE_SINGLE_P.fullmatch(html)
|
|
return m.group(1) if m else html
|
|
|
|
|
|
class Command(BaseCommand):
|
|
help = "Normalize Question HTML fields by unwrapping a single root <p>...</p>. Safe: supports --dry-run and batching."
|
|
|
|
def add_arguments(self, parser):
|
|
parser.add_argument("--commit", action="store_true", help="Apply changes; without this flag it's a dry-run.")
|
|
parser.add_argument("--batch-size", type=int, default=200, help="How many objects to fetch per loop.")
|
|
parser.add_argument("--limit", type=int, default=0, help="Limit number of questions processed (0 = all).")
|
|
parser.add_argument("--verbose", action="store_true", help="Show full before/after for each change.")
|
|
parser.add_argument("--start-id", type=int, default=0, help="Start from Question.id >= start-id (useful for resuming).")
|
|
|
|
def handle(self, *args, **opts):
|
|
commit = opts["commit"]
|
|
batch_size = opts["batch_size"]
|
|
limit = opts["limit"] or None
|
|
verbose = opts["verbose"]
|
|
start_id = opts["start_id"]
|
|
|
|
qs = Question.objects.all().order_by("id")
|
|
if start_id:
|
|
qs = qs.filter(id__gte=start_id)
|
|
total = qs.count()
|
|
self.stdout.write(f"Found {total} questions to inspect (start_id={start_id}).")
|
|
|
|
processed = 0
|
|
changed = 0
|
|
|
|
it = qs.iterator(chunk_size=batch_size)
|
|
for q in it:
|
|
processed += 1
|
|
if limit and processed > limit:
|
|
break
|
|
|
|
changed_any = False
|
|
updates = {}
|
|
for f in UNWRAP_FIELDS:
|
|
orig = getattr(q, f, None)
|
|
new = unwrap_single_p(orig)
|
|
if new is not None and new != orig:
|
|
changed_any = True
|
|
updates[f] = new
|
|
|
|
if changed_any:
|
|
changed += 1
|
|
if verbose:
|
|
self.stdout.write(f"Question id={q.id} will change fields: {', '.join(updates.keys())}")
|
|
for f, newval in updates.items():
|
|
oldval = getattr(q, f)
|
|
self.stdout.write(" Field: %s\n BEFORE: %s\n AFTER : %s\n" % (
|
|
f,
|
|
(oldval or "")[:400],
|
|
(newval or "")[:400],
|
|
))
|
|
else:
|
|
sample = ", ".join(f"{k}({shorten((getattr(q,k) or ''), width=60)})" for k in updates.keys())
|
|
self.stdout.write(f"[DRY] id={q.id} -> {sample}" if not commit else f"[APPLY] id={q.id} -> {', '.join(updates.keys())}")
|
|
|
|
if commit:
|
|
for k, v in updates.items():
|
|
setattr(q, k, v)
|
|
try:
|
|
with transaction.atomic():
|
|
q.save(update_fields=list(updates.keys()))
|
|
except Exception as e:
|
|
self.stderr.write(f"Failed to save Question id={q.id}: {e}")
|
|
|
|
if processed % max(1, batch_size) == 0:
|
|
self.stdout.write(f"Processed {processed} questions...")
|
|
|
|
self.stdout.write(f"Done. Processed {processed} questions, {changed} would change{' (committed)' if commit else ' (dry-run)'}.")
|