File size: 3,568 Bytes
e561127
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
"""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