APP-Backend / iserv-filter /browser_source.py
Luca448's picture
Claude Sonnet 5.5
feat(iserv): filtered IServ tasks for Muse
e561127
Raw History Blame Contribute Delete
3.57 kB
"""Adapter for the Puppeteer fetcher (src/api/iserv.js of the app backend): its JSON in, tasks in the shape
filter_task takes. Only title, description, dates, tags, the task id inside the url and the provided files are taken;
the url itself (= the school domain) and the fetcher's own `id` (the same url) are dropped.
The fetcher grew additive fields for this (descriptionText, tags, start, due, done, provided, attachmentsFailed).
Without them (an older fetcher) the fields stay empty: no subject, no start date, no done flag, `description` is the
broad block it always returned, and a file that failed to download is simply missing."""
import base64
import re
from iserv import MAX_ATTACHMENTS, iso_date
# the fetcher's own censor() writes these; they become the placeholders of the rest of the output (the NER filter
# would turn "<NAME>" into "<[ORGANISATION]>")
_PLACEHOLDERS = {"<NAME>": "[PERSON]", "<EMAIL>": "[KONTAKT]", "<TELEFON>": "[KONTAKT]"}
_ISO = re.compile(r"\d{4}-\d{2}-\d{2}(T\d{2}:\d{2})?")
def _text(value):
value = value if isinstance(value, str) else ""
for old, new in _PLACEHOLDERS.items():
value = value.replace(old, new)
return value
def _iso(value):
"""Dates are passed on as they are, so only a date in ISO form may pass: anything else would skip the filter."""
return value if isinstance(value, str) and _ISO.fullmatch(value) else None
def _attachment(item):
name = _text(item.get("filename"))
try:
data = base64.b64decode(item["data"], validate=True)
except Exception: # missing or broken base64
return {"name": name, "failed": True}
return {"name": name, "size": len(data), "data": data}
def _failure(item):
name = _text(item.get("filename"))
return {"name": name, "size": None, "data": None} if item.get("reason") == "too_large" else {"name": name, "failed": True}
def _files(item):
"""The files the teacher provided: downloaded ones, then the ones that could not be downloaded. A file of the own
submission or of the teacher's feedback (provided is False) is never taken."""
def dicts(key):
return [f for f in item.get(key) or [] if isinstance(f, dict) and f.get("provided") is not False]
files = [_attachment(f) for f in dicts("attachments")] + [_failure(f) for f in dicts("attachmentsFailed")]
return files[:MAX_ATTACHMENTS] + ([{"skipped": True}] if len(files) > MAX_ATTACHMENTS else [])
def from_browser(items):
"""Tasks for filter_task. When the fetcher finds no rows (empty list, failed login) it invents three tasks
(flagged mock, and without /exercise/show/<id> in their url); they are dropped here."""
tasks = []
for item in items:
m = re.search(r"/exercise/show/(\d+)", item.get("url") or "")
if not m or item.get("mock"):
continue
task = {
"id": int(m.group(1)),
"title": _text(item.get("title")),
# descriptionText: only the task's own description. description: the broad block of older fetchers.
"description": _text(item["descriptionText"] if isinstance(item.get("descriptionText"), str) else item.get("description")),
"tags": _text(item.get("tags")),
"start": _iso(item.get("start")),
"due": _iso(item.get("due")) or iso_date(item.get("deadline") or ""),
"attachments": _files(item),
}
if isinstance(item.get("done"), bool):
task["done"] = item["done"] # no key = unknown: status null
tasks.append(task)
return tasks