"""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 "" into "<[ORGANISATION]>") _PLACEHOLDERS = {"": "[PERSON]", "": "[KONTAKT]", "": "[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/ 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