Changeset e75910d in flexograder


Ignore:
Timestamp:
11/22/25 17:46:21 (4 months ago)
Author:
Enrico Schwass <ennoausberlin@…>
Branches:
fake-data, main, master
Children:
858a4dc
Parents:
0792ddd
Message:

all tests green, new json format and domain handling added

Files:
2 added
11 edited

Legend:

Unmodified
Added
Removed
  • builder/exam.py

    r0792ddd re75910d  
    66import zipfile
    77
    8 from flexoentity import FlexoEntity, EntityType, EntityState
     8from flexoentity import FlexoEntity, EntityType, EntityState, Domain, FlexOID
    99from .exam_elements import ExamElement, ChoiceQuestion
    1010from .question_factory import question_factory
     11from .domain_manager import DomainManager
    1112
    1213@dataclass
     
    7576    submit_note: str = ""
    7677    author: str = "unknown"
     78    domains: Dict[str, Dict[str, Any]] = field(default_factory=dict)
    7779    questions: list[ExamElement] = field(default_factory=list)
    7880    meta_extra: Optional[Dict[str, Any]] = field(default_factory=dict)
     
    9395        qid = str(qid)
    9496        return next((q for q in self.questions if q.flexo_id == qid), None)
    95 
    96     def __post_init__(self):
    97         super().__post_init__()
    9897
    9998    def ensure_page(self, title: str) -> ExamPage:
     
    118117    def to_dict(self):
    119118        base = super().to_dict()
     119
    120120        base.update({
    121             "meta": {
    122                 "exam_id": str(self.flexo_id),
     121            "exam": {
     122                "flexo_id": self.flexo_id,
    123123                "title": self.title,
    124124                "duration": self.duration,
     
    130130                **self.meta_extra,
    131131            },
     132            "domains": self.domains,
    132133            "layout": self.layout.to_dict(),
    133134            "questions": [q.to_dict() for q in self.questions],
    134135        })
     136
    135137        return base
    136138
    137139    @classmethod
    138140    def from_dict(cls, data: dict):
    139         meta = data.get("meta", {})
    140 
    141         # --- Case 1: New unified structure ---
    142         if "questions" in data and "layout" in data:
    143             questions = [question_factory(qd) for qd in data.get("questions", [])]
    144             layout_data = data.get("layout", {}) or {}
    145             pages = [ExamPage.from_dict(p) for p in layout_data.get("pages", [])]
    146             layout = ExamLayout(pages=pages)
    147 
    148         # --- Case 2: Legacy structure with embedded questions ---
    149         elif "pages" in data:
    150             pages_raw = data.get("pages", [])
    151             all_questions = []
    152             layout_pages = []
    153 
    154             for p in pages_raw:
    155                 title = p.get("title", "")
    156                 qs = [question_factory(q) for q in p.get("questions", [])]
    157                 all_questions.extend(qs)
    158                 layout_pages.append(
    159                     ExamPage(
    160                         title=title,
    161                         question_ids=[str(getattr(q, "flexo_id",
    162                                                   getattr(q, "id", ""))) for q in qs],
    163                     )
    164                 )
    165 
    166             questions = all_questions
    167             layout = ExamLayout(pages=layout_pages)
    168 
    169         else:
    170             # Unknown structure — minimal fallback
    171             questions = []
    172             layout = ExamLayout()
    173 
    174         print("Q:", questions)
    175 
    176         return cls.with_domain_id(domain_id="TEST",
    177                                   title=meta.get("title", ""),
    178                                   duration=meta.get("duration", ""),
    179                                   allowed_aids=meta.get("allowed_aids", ""),
    180                                   headline=meta.get("headline", ""),
    181                                   intro_note=meta.get("intro_note", ""),
    182                                   submit_note=meta.get("submit_note", ""),
    183                                   author=meta.get("author", "unknown"),
    184                                   questions=questions,
    185                                   meta_extra={k: v for k, v in meta.items() if k not in {
    186                                       "title", "duration", "allowed_aids", "headline",
    187                                       "intro_note", "submit_note", "author", "domain"
    188                                   }},
    189                                   layout=layout,
    190                                   )
     141        meta = data.get("exam", {})
     142        domains = data.get("domains", {})
     143       
     144        flexo_id = FlexOID(meta.get("flexo_id"))
     145
     146        questions = [question_factory(qd) for qd in data.get("questions", [])]
     147        layout_data = data.get("layout", {}) or {}
     148        pages = [ExamPage.from_dict(p) for p in layout_data.get("pages", [])]
     149        layout = ExamLayout(pages=pages)
     150
     151        return cls(flexo_id=flexo_id,
     152                   title=meta.get("title", ""),
     153                   duration=meta.get("duration", ""),
     154                   allowed_aids=meta.get("allowed_aids", ""),
     155                   headline=meta.get("headline", ""),
     156                   intro_note=meta.get("intro_note", ""),
     157                   submit_note=meta.get("submit_note", ""),
     158                   author=meta.get("author", "unknown"),
     159                   questions=questions,
     160                   meta_extra={k: v for k, v in meta.items() if k not in {
     161                       "title", "duration", "allowed_aids", "headline",
     162                       "intro_note", "submit_note", "author", "domain"
     163                   }},
     164                   layout=layout,
     165                   _in_factory=True
     166                   )
    191167
    192168    @classmethod
  • builder/exam_elements.py

    r0792ddd re75910d  
    107107
    108108        # NOTE: cls(...) will call the correct subclass constructor
    109         obj = cls.with_domain_id(domain_id=flexo_id.domain_id,
     109        obj = cls(
    110110            text=data.get("text", ""),
    111111            topic=data.get("topic", ""),
    112112            flexo_id=flexo_id,
     113            _in_factory=True
    113114        )
    114115
  • builder/media_items.py

    r0792ddd re75910d  
    55from datetime import datetime
    66from dataclasses import dataclass, field
    7 from flexoentity import FlexOID, EntityType, EntityState, FlexoEntity
     7from flexoentity import FlexOID, EntityType, EntityState, FlexoEntity, logger
    88
    99@dataclass
     
    1616    caption: str = ""
    1717    author: str = ""
    18     created_at: datetime = field(default_factory=datetime.utcnow)
    1918    ExportPrefix = "media/"
    2019
     
    3635        return "file"
    3736
     37    @property
     38    def type(self) -> str:
     39        # logical type used for JSON, defaults to mtype
     40        return self.mtype
     41
    3842    @classmethod
    3943    def default(cls):
     
    4347        super().__post_init__()
    4448
     49    @property
    4550    def text_seed(self) -> str:
    46         try:
    47             content_hash = hashlib.blake2s(Path(self.src).read_bytes()).hexdigest()[:12]
    48         except FileNotFoundError:
    49             content_hash = "MISSING"
    50 
    51         return f"{self.src}|{content_hash}|{self.title}|{self.caption}|{self.mtype}"
     51        return f"{self.src}|{self.title}|{self.caption}|{self.mtype}"
    5252
    5353    def to_dict(self):
     
    5757            "title": self.title,
    5858            "caption": self.caption,
    59             "type": self.mtype,
     59            "type": self.type,
    6060        })
    6161        return base
     
    6363    @classmethod
    6464    def from_dict(cls, data):
     65        flexo_id = FlexOID(data["flexo_id"]) if "flexo_id" in data else None
     66
    6567        obj = cls(
     68            flexo_id=flexo_id,
    6669            src=data.get("src", ""),
    6770            title=data.get("title", ""),
    68             caption=data.get("caption", "")
     71            caption=data.get("caption", ""),
     72            author=data.get("author", ""),
     73            fingerprint=data.get("fingerprint"),
     74            state=EntityState(data["state"]) if "state" in data else EntityState(flexo_id.state_code),
     75            version=data.get("version"),
     76            _in_factory=True,
    6977        )
    70         obj.flexo_id = FlexOID.from_string(data["flexo_id"]) if "flexo_id" in data else None
    71         obj.state = data.get("state", "DRAFT")
    72         obj.version = data.get("version", 1)
     78
    7379        return obj
    74    
     80    
    7581    def to_html(self):
    7682        raise NotImplementedError
     
    122128        return f'<a href="{prefix + self.src}" download>{self.label}</a>'
    123129
    124 
    125130@dataclass
    126131class NullMediaItem(MediaItem):
    127132
     133    def __post_init__(self):
     134        # Null objects bypass FlexOEntity lifecycle: no identity
     135        self.flexo_id = None
     136        self.fingerprint = None
     137
    128138    @classmethod
    129139    def default(cls):
    130         """Return a canonical default NullMediaItem instance."""
    131         return cls.with_domain_id(domain_id="NULL_MEDIA")
     140        return cls(_in_factory=True)
    132141
    133     def __post_init__(self):
    134         super().__post_init__()
     142    def to_dict(self):
     143        return {}
    135144
    136145    @property
     
    141150        return prefix
    142151
    143     def to_dict(self):
    144         return {}
     152def media_factory(m: dict) -> MediaItem:
    145153
    146 # FIXME: We should really check if all attributes are initialized correctly
    147 
    148 def media_factory(m: dict) -> MediaItem:
    149154    mtype = m.get("type", "").lower()
    150     src = m.get("src", "")
    151     domain = m.get("domain", "GEN")
    152     label = m.get("label", None)
    153155
    154156    cls = {
     
    156158        "audio": AudioItem,
    157159        "video": VideoItem,
    158         "download": DownloadItem
     160        "download": DownloadItem,
    159161    }.get(mtype, NullMediaItem)
    160162
    161     m = cls.with_domain_id(domain, src=src) if cls is not NullMediaItem else NullMediaItem.default()
    162     return m
     163    if cls is NullMediaItem:
     164        return NullMediaItem.default()
     165
     166    # Prefer from_dict if a FlexOID is provided
     167    if "flexo_id" in m:
     168        return cls.from_dict(m)
     169
     170    # Otherwise, generate a new entity
     171    domain_id = m.get("domain_id", "GEN")
     172    return cls.with_domain_id(domain_id, src=m.get("src", ""))
  • builder/question_catalog.py

    r0792ddd re75910d  
    44from .exam_elements import ExamElement
    55from builder.question_factory import question_factory
    6 from flexoentity import FlexoEntity, EntityType, EntityState
     6from flexoentity import FlexoEntity, EntityType, EntityState, FlexOID
    77
    88
     
    1919    def default(cls):
    2020        """Return a minimal default catalog (for GUI defaults or deserialization)."""
    21         return cls(
    22             domain="GEN",
     21        return cls.with_domain_id(
     22            domain_id="GEN",
    2323            title="Untitled Catalog",
    2424            author="unknown",
     
    4242    @property
    4343    def empty(self):
    44         return not self._questions
     44        return not self.questions
    4545
    4646    @property
     
    103103        return qs
    104104
    105     # ─────────────────────────────────────────────
    106     # ID helper
    107     # ─────────────────────────────────────────────
    108105    def next_question_id(self, domain: str) -> str:
    109106        abbrev = domain.split("_", 1)[0].upper() if "_" in domain else domain.upper()
     
    116113        return f"{abbrev}_{next_num:03d}"
    117114
    118     # ─────────────────────────────────────────────
    119     # Serialization
    120     # ─────────────────────────────────────────────
    121115    def to_dict(self):
    122116        d = super().to_dict()
    123117        d.update({
    124             "id": str(self.flexo_id),
     118            "flexo_id": self.flexo_id,
    125119            "title": self.title,
    126120            "author": self.author,
     
    131125    @classmethod
    132126    def from_dict(cls, data: dict) -> "QuestionCatalog":
     127        # Extract ID
     128        flexo_id = FlexOID(data.get("flexo_id"))
     129        if not flexo_id:
     130            raise ValueError("Catalog JSON missing 'flexo_id'")
     131
     132        # Construct entity using FlexOEntity semantics
    133133        obj = cls(
    134             flexo_id=data.get("flexo_id"),
     134            flexo_id=flexo_id,
    135135            title=data.get("title", ""),
    136136            author=data.get("author", "unknown"),
     137            fingerprint=data.get("fingerprint"),
     138            _in_factory=True,
    137139        )
     140
     141        # Restore questions
    138142        obj.questions = [question_factory(qd) for qd in data.get("questions", [])]
     143
    139144        return obj
  • examples/KILE_EXAM.json

    r0792ddd re75910d  
    11{
    2   "domain": "GENERAL",
    3   "entity_type": "CATALOG",
    4   "state": "DRAFT",
    5   "flexo_id": "GENERAL-C251110-B854C645BF40@001D",
    6   "fingerprint": "DE1B419F5C11AA13",
    7   "origin": null,
    8   "originator_id": "00000000-0000-0000-0000-000000000000",
    9   "owner_id": "00000000-0000-0000-0000-000000000000",
    10   "meta": {
     2  "exam": {
    113    "exam_id": "EX-C251110-5EBCA549643D@001D",
    12     "title": "KID-Test",
    13     "duration": "",
    14     "allowed_aids": "",
    15     "headline": "",
    16     "intro_note": "",
    17     "submit_note": "",
    18     "author": "KILE"
     4      "domain_id": "GENERAL",
     5      "entity_type": "CATALOG",
     6      "state": "DRAFT",
     7      "flexo_id": "GENERAL-C251110-B854C645BF40@001D",
     8      "fingerprint": "DE1B419F5C11AA13",
     9      "origin": null,
     10      "originator_id": "00000000-0000-0000-0000-000000000000",
     11      "owner_id": "00000000-0000-0000-0000-000000000000",
     12      "title": "KID-Test",
     13      "duration": "",
     14      "allowed_aids": "",
     15      "headline": "",
     16      "intro_note": "",
     17      "submit_note": "",
     18      "author": "KILE"
    1919  },
    2020  "layout": {
     
    6969  "questions": [
    7070    {
    71       "domain": "AI_BASICS",
     71      "domain_id": "AI_BASICS",
    7272      "entity_type": "ITEM",
    7373      "state": "DRAFT",
     
    105105    },
    106106    {
    107       "domain": "AI_DTREE",
     107      "domain_id": "AI_DTREE",
    108108      "entity_type": "ITEM",
    109109      "state": "DRAFT",
     
    141141    },
    142142    {
    143       "domain": "AI_ETHICS",
     143      "domain_id": "AI_ETHICS",
    144144      "entity_type": "ITEM",
    145145      "state": "DRAFT",
     
    177177    },
    178178    {
    179       "domain": "AI_ML",
     179      "domain_id": "AI_ML",
    180180      "entity_type": "ITEM",
    181181      "state": "DRAFT",
     
    213213    },
    214214    {
    215       "domain": "AI_NLP",
     215      "domain_id": "AI_NLP",
    216216      "entity_type": "ITEM",
    217217      "state": "DRAFT",
     
    249249    },
    250250    {
    251       "domain": "AI_NN",
     251      "domain_id": "AI_NN",
    252252      "entity_type": "ITEM",
    253253      "state": "DRAFT",
     
    285285    },
    286286    {
    287       "domain": "AI_TEXT",
     287      "domain_id": "AI_TEXT",
    288288      "entity_type": "ITEM",
    289289      "state": "DRAFT",
     
    321321    },
    322322    {
    323       "domain": "API",
     323      "domain_id": "API",
    324324      "entity_type": "ITEM",
    325325      "state": "DRAFT",
     
    357357    },
    358358    {
    359       "domain": "CS_ALGO",
     359      "domain_id": "CS_ALGO",
    360360      "entity_type": "ITEM",
    361361      "state": "DRAFT",
     
    393393    },
    394394    {
    395       "domain": "DATA_BIG",
     395      "domain_id": "DATA_BIG",
    396396      "entity_type": "ITEM",
    397397      "state": "DRAFT",
     
    429429    },
    430430    {
    431       "domain": "DATA_FORMATS",
     431      "domain_id": "DATA_FORMATS",
    432432      "entity_type": "ITEM",
    433433      "state": "DRAFT",
     
    465465    },
    466466    {
    467       "domain": "DB_SQL",
     467      "domain_id": "DB_SQL",
    468468      "entity_type": "ITEM",
    469469      "state": "DRAFT",
     
    501501    },
    502502    {
    503       "domain": "PROG_LANG",
     503      "domain_id": "PROG_LANG",
    504504      "entity_type": "ITEM",
    505505      "state": "DRAFT",
     
    537537    },
    538538    {
    539       "domain": "SOFT_ENG",
     539      "domain_id": "SOFT_ENG",
    540540      "entity_type": "ITEM",
    541541      "state": "DRAFT",
     
    573573    },
    574574    {
    575       "domain": "SOFT_METHODS",
     575      "domain_id": "SOFT_METHODS",
    576576      "entity_type": "ITEM",
    577577      "state": "DRAFT",
     
    609609    },
    610610    {
    611       "domain": "WEB_ARCH",
     611      "domain_id": "WEB_ARCH",
    612612      "entity_type": "ITEM",
    613613      "state": "DRAFT",
     
    645645    },
    646646    {
    647       "domain": "WEB_CSS",
     647      "domain_id": "WEB_CSS",
    648648      "entity_type": "ITEM",
    649649      "state": "DRAFT",
     
    681681    },
    682682    {
    683       "domain": "WEB_DEV",
     683      "domain_id": "WEB_DEV",
    684684      "entity_type": "ITEM",
    685685      "state": "DRAFT",
     
    717717    },
    718718    {
    719       "domain": "WEB_FRAMEWORKS",
     719      "domain_id": "WEB_FRAMEWORKS",
    720720      "entity_type": "ITEM",
    721721      "state": "DRAFT",
     
    753753    },
    754754    {
    755       "domain": "WEB_HTML",
     755      "domain_id": "WEB_HTML",
    756756      "entity_type": "ITEM",
    757757      "state": "DRAFT",
  • examples/demo/example_exam.json

    r0792ddd re75910d  
    11{
    2   "meta": {
    3       "title": "DEMO EXAM",
    4       "duration": "30 Minuten",
    5       "allowed_aids": "keine",
    6       "headline": "DATAV Schutzbereich 2, 10039691S",
    7       "intro_note": "Bitte lesen Sie jede Frage sorgfältig und beachten Sie die Hinweise.",
    8       "submit_note": "Sie können Ihre Antworten jetzt einreichen. Eine spätere Änderung ist nicht mehr möglich.",
    9       "exam_id": "DM-2025-10-001",
    10       "created_on": "2025-10-07T08:30:00Z",
    11       "created_by": "OSF Schwass"
     2  "exam": {
     3    "title": "DEMO EXAM",
     4    "duration": "30 Minuten",
     5    "allowed_aids": "keine",
     6    "headline": "DATAV Schutzbereich 2, 10039691S",
     7    "intro_note": "Bitte lesen Sie jede Frage sorgfältig und beachten Sie die Hinweise.",
     8    "submit_note": "Sie können Ihre Antworten jetzt einreichen. Eine spätere Änderung ist nicht mehr möglich.",
     9    "flexo_id": "WIP_TEST-E251123-3F38CAEA66F1@001D",
     10    "domain_id": "WIP_TEST",
     11    "created_by": "OSF Schwass"
    1212  },
     13  "domains": {
     14    "WIP_TEST": {
     15      "flexo_id": "WIP_TEST-D251123-7C8F5A4B1E22@001D",
     16      "domain_id": "WIP_TEST",
     17      "fullname": "WIP_TEST",
     18      "description": "",
     19      "classification": "UNCLASSIFIED"
     20    },
     21    "SYSIDENT": {
     22      "flexo_id": "SYSIDENT-D251123-91A3D0C4F8BF@001D",
     23      "domain_id": "SYSIDENT",
     24      "fullname": "SYSIDENT",
     25      "description": "",
     26      "classification": "UNCLASSIFIED"
     27    },
     28    "SYSINFO": {
     29      "flexo_id": "SYSINFO-D251123-8542CCE78BAE@001D",
     30      "domain_id": "SYSINFO",
     31      "fullname": "SYSINFO",
     32      "description": "",
     33      "classification": "UNCLASSIFIED"
     34    },
     35    "SIGNALS": {
     36      "flexo_id": "SIGNALS-D251123-29D06F3B2E11@001D",
     37      "domain_id": "SIGNALS",
     38      "fullname": "SIGNALS",
     39      "description": "",
     40      "classification": "UNCLASSIFIED"
     41    },
     42    "MILSYMBOLS": {
     43      "flexo_id": "MILSYMBOLS-D251123-C4B8FE932A10@001D",
     44      "domain_id": "MILSYMBOLS",
     45      "fullname": "MILSYMBOLS",
     46      "description": "",
     47      "classification": "UNCLASSIFIED"
     48    }
     49  },
     50  "layout": {
    1351    "pages": [
    14         {
    15             "title": "Personaldaten",
    16             "questions": [
    17                 {
    18                     "id": "IDENT_001",
    19                     "qtype": "candidate_id",
    20                     "domain": "SYSIDENT",
    21                     "topic": "identification",
    22                     "text": "Bitte geben Sie Ihre Personaldaten ein:",
    23                     "fields": [
    24                         {
    25                             "id": "last_name",
    26                             "label": "Nachname",
    27                             "validation": {
    28                                 "required": true,
    29                                 "pattern": "^[A-Za-zÀ-ÖØ-öø-ÿĀ-žŽžßñÑçÇ ]{1,30}$",
    30                                 "maxlength": 30
    31                             }
    32                         },
    33                         {
    34                             "id": "first_name",
    35                             "label": "Vorname",
    36                             "validation": {
    37                                 "required": true,
    38                                 "pattern": "^[A-Za-zÀ-ÖØ-öø-ÿĀ-žŽžßñÑçÇ ]{1,30}$",
    39                                 "maxlength": 30
    40                             }
    41                         },
    42                         {
    43                             "id": "personal_id",
    44                             "label": "Personalnummer",
    45                             "validation": {
    46                                 "required": true,
    47                                 "pattern": "^\\d{8}",
    48                                 "maxlength": 8
    49                             }
    50                         }
    51                     ]
    52                 }
    53             ]
    54         },
    55         {
    56             "title": "Referenz",
    57             "questions": [
    58                 {
    59                     "id": "INFO_801",
    60                     "qtype": "instruction",
    61                     "domain": "SYSINFO",
    62                     "topic": "certification",
    63                     "text": "Laden Sie hier die verlinkte Referenz herunter",
    64                     "media": [{"type": "download", "src": "other/AmateurfunkI.pdf"}]
    65                 }
    66             ]
    67         },         
    68         {
    69             "title": "Modulation",
    70             "questions": [
    71                 {
    72                     "id": "SIGNALS_802",
    73                     "qtype": "single_choice",
    74                     "domain": "SIGNALS",
    75                     "topic": "forms",
    76                     "text": "Welche Aussage über modulierte Signale ist richtig?",
    77                     "options": [
    78                         {"id": "A", "points": 1, "text": "Bei FM ändert sich die Amplitude des Sendesignals bei Modulation nicht."},
    79                         {"id": "B", "points": 0, "text": "Bei SSB ändert sich die Amplitude des Sendesignals bei Modulation nicht."},
    80                         {"id": "C", "points": 0, "text": "Bei FM ändert sich die Amplitude des Sendesignals bei Modulation im Rhythmus der Sprache."},
    81                         {"id": "D", "points": 0, "text": "Bei AM ändert sich die Amplitude des Sendesignals bei Modulation nicht."}
    82                     ]
    83                 },
    84                 {
    85                     "id": "SIGNALS_803",
    86                     "qtype": "multiple_choice",
    87                     "domain": "SIGNALS",
    88                     "topic": "forms",
    89                     "text": "Welche der folgenden Antworten beschreiben Modulationsverfahren?",
    90                     "options": [
    91                         {"id": "A", "points": 1, "text": "FM"},
    92                         {"id": "B", "points": 1, "text": "AM"},
    93                         {"id": "C", "points": 1, "text": "QAM"},
    94                         {"id": "D", "points": 0, "text": "PSK"}
    95                     ]
    96                 }
    97             ]
    98         },
    99         {
    100             "title": "Taktische Zeichen",
    101             "questions": [
    102                 {
    103                     "id": "MILITARY_804",
    104                     "qtype": "single_choice",
    105                     "domain": "MILSYMBOLS",
    106                     "topic": "radar",
    107                     "text": "Welches taktische Zeichen sehen Sie?",
    108                     "options": [
    109                         {"id": "A", "points": 1, "text": "Ground track Signal Intercept Radar Early Warning"},
    110                         {"id": "B", "points": 0, "text": "Electronic Warfare Armoured Wheeled Vehicle"}
    111                     ],
    112                     "media": [{"type": "image", "src": "images/GroundTrackSignalInterceptRadarEarlyWarning.svg"}]
    113                 }
    114             ]
    115         },
    116         {
    117             "title": "Signale",
    118             "questions": [
    119                 {
    120                     "id": "SIGNALS_805",
    121                     "qtype": "single_choice",
    122                     "domain": "SIGNALS",
    123                     "topic": "sound",
    124                     "text": "Was hören Sie?",
    125                     "options": [
    126                         {"id": "A", "points": 0, "text": "Fernschreiber"},
    127                         {"id": "B", "points": 1, "text": "Tastfunk"}
    128                     ],
    129                     "media": [{"type": "audio", "src": "audio/Code_40.mp3"}]
    130                 },
    131                 {
    132                     "id": "SIGNALS_806",
    133                     "qtype": "single_choice",
    134                     "domain": "SIGNALS",
    135                     "topic": "visual",
    136                     "text": "Was sehen Sie?",
    137                     "options": [
    138                         {"id": "A", "points": 0, "text": "Fernschreiber"},
    139                         {"id": "B", "points": 1, "text": "Störungen"}
    140                     ],
    141                     "media": [{"type": "video", "src": "videos/spectrum.mp4"}]
    142                 }
    143             ] 
    144         }
     52      {
     53        "title": "Personaldaten",
     54        "question_ids": [
     55          "SYSIDENT-I251123-D6F51D9FECD4@001D"
     56        ]
     57      },
     58      {
     59        "title": "Referenz",
     60        "question_ids": [
     61          "SYSINFO-I251123-B24BF34EFDF9@001D"
     62        ]
     63      },
     64      {
     65        "title": "Modulation",
     66        "question_ids": [
     67          "SIGNALS-I251123-F4E3A8331A44@001D",
     68          "SIGNALS-I251123-A70926DEB078@001D"
     69        ]
     70      },
     71      {
     72        "title": "Taktische Zeichen",
     73        "question_ids": [
     74          "MILSYMBOLS-I251123-D58722226DB2@001D"
     75        ]
     76      },
     77      {
     78        "title": "Signale",
     79        "question_ids": [
     80          "SIGNALS-I251123-9FC703C70C41@001D",
     81          "SIGNALS-I251123-79D775506384@001D"
     82        ]
     83      }
    14584    ]
     85  },
     86  "questions": [
     87    {
     88      "flexo_id": "SYSIDENT-I251123-D6F51D9FECD4@001D",
     89      "qtype": "candidate_id",
     90      "domain_id": "SYSIDENT",
     91      "topic": "identification",
     92      "text": "Bitte geben Sie Ihre Personaldaten ein:",
     93      "fields": [
     94        {
     95          "id": "last_name",
     96          "label": "Nachname",
     97          "validation": {
     98            "required": true,
     99            "pattern": "^[A-Za-zÀ-ÖØ-öø-ÿĀ-žŽžßñÑçÇ ]{1,30}$",
     100            "maxlength": 30
     101          }
     102        },
     103        {
     104          "id": "first_name",
     105          "label": "Vorname",
     106          "validation": {
     107            "required": true,
     108            "pattern": "^[A-Za-zÀ-ÖØ-öø-ÿĀ-žŽžßñÑçÇ ]{1,30}$",
     109            "maxlength": 30
     110          }
     111        },
     112        {
     113          "id": "personal_id",
     114          "label": "Personalnummer",
     115          "validation": {
     116            "required": true,
     117            "pattern": "^\\d{8}",
     118            "maxlength": 8
     119          }
     120        }
     121      ]
     122    },
     123    {
     124      "flexo_id": "SYSINFO-I251123-B24BF34EFDF9@001D",
     125      "qtype": "instruction",
     126      "domain_id": "SYSINFO",
     127      "topic": "certification",
     128      "text": "Laden Sie hier die verlinkte Referenz herunter",
     129      "media": [
     130        {
     131          "type": "download",
     132          "src": "other/AmateurfunkI.pdf"
     133        }
     134      ]
     135    },
     136    {
     137      "flexo_id": "SIGNALS-I251123-F4E3A8331A44@001D",
     138      "qtype": "single_choice",
     139      "domain_id": "SIGNALS",
     140      "topic": "forms",
     141      "text": "Welche Aussage über modulierte Signale ist richtig?",
     142      "options": [
     143        {
     144          "id": "A",
     145          "points": 1,
     146          "text": "Bei FM ändert sich die Amplitude des Sendesignals bei Modulation nicht."
     147        },
     148        {
     149          "id": "B",
     150          "points": 0,
     151          "text": "Bei SSB ändert sich die Amplitude des Sendesignals bei Modulation nicht."
     152        },
     153        {
     154          "id": "C",
     155          "points": 0,
     156          "text": "Bei FM ändert sich die Amplitude des Sendesignals bei Modulation im Rhythmus der Sprache."
     157        },
     158        {
     159          "id": "D",
     160          "points": 0,
     161          "text": "Bei AM ändert sich die Amplitude des Sendesignals bei Modulation nicht."
     162        }
     163      ]
     164    },
     165    {
     166      "flexo_id": "SIGNALS-I251123-A70926DEB078@001D",
     167      "qtype": "multiple_choice",
     168      "domain_id": "SIGNALS",
     169      "topic": "forms",
     170      "text": "Welche der folgenden Antworten beschreiben Modulationsverfahren?",
     171      "options": [
     172        {
     173          "id": "A",
     174          "points": 1,
     175          "text": "FM"
     176        },
     177        {
     178          "id": "B",
     179          "points": 1,
     180          "text": "AM"
     181        },
     182        {
     183          "id": "C",
     184          "points": 1,
     185          "text": "QAM"
     186        },
     187        {
     188          "id": "D",
     189          "points": 0,
     190          "text": "PSK"
     191        }
     192      ]
     193    },
     194    {
     195      "flexo_id": "MILSYMBOLS-I251123-D58722226DB2@001D",
     196      "qtype": "single_choice",
     197      "domain_id": "MILSYMBOLS",
     198      "topic": "radar",
     199      "text": "Welches taktische Zeichen sehen Sie?",
     200      "options": [
     201        {
     202          "id": "A",
     203          "points": 1,
     204          "text": "Ground track Signal Intercept Radar Early Warning"
     205        },
     206        {
     207          "id": "B",
     208          "points": 0,
     209          "text": "Electronic Warfare Armoured Wheeled Vehicle"
     210        }
     211      ],
     212      "media": [
     213        {
     214          "type": "image",
     215          "src": "images/GroundTrackSignalInterceptRadarEarlyWarning.svg"
     216        }
     217      ]
     218    },
     219    {
     220      "flexo_id": "SIGNALS-I251123-9FC703C70C41@001D",
     221      "qtype": "single_choice",
     222      "domain_id": "SIGNALS",
     223      "topic": "sound",
     224      "text": "Was hören Sie?",
     225      "options": [
     226        {
     227          "id": "A",
     228          "points": 0,
     229          "text": "Fernschreiber"
     230        },
     231        {
     232          "id": "B",
     233          "points": 1,
     234          "text": "Tastfunk"
     235        }
     236      ],
     237      "media": [
     238        {
     239          "type": "audio",
     240          "src": "audio/Code_40.mp3"
     241        }
     242      ]
     243    },
     244    {
     245      "flexo_id": "SIGNALS-I251123-79D775506384@001D",
     246      "qtype": "single_choice",
     247      "domain_id": "SIGNALS",
     248      "topic": "visual",
     249      "text": "Was sehen Sie?",
     250      "options": [
     251        {
     252          "id": "A",
     253          "points": 0,
     254          "text": "Fernschreiber"
     255        },
     256        {
     257          "id": "B",
     258          "points": 1,
     259          "text": "Störungen"
     260        }
     261      ],
     262      "media": [
     263        {
     264          "type": "video",
     265          "src": "videos/spectrum.mp4"
     266        }
     267      ]
     268    }
     269  ]
    146270}
  • gui/gui.py

    r0792ddd re75910d  
    303303            return
    304304
    305         print(self.domain_manager.all_domain_ids())
    306305        dlg = OptionQuestionEditorDialog(self, question_dict, self.domain_manager.all_domain_ids())
    307306        self.wait_window(dlg)
     
    364363            self.log_action(f"Updated - Question {q.flexo_id} updated.")
    365364
     365    def find_all_domain_ids(self, obj, result: set):
     366        if isinstance(obj, dict):
     367            for k, v in obj.items():
     368                if k == "domain_id":
     369                    result.add(v)
     370                else:
     371                    self.find_all_domain_ids(v, result)
     372        elif isinstance(obj, list):
     373            for item in obj:
     374                self.find_all_domain_ids(item, result)
     375
     376        return result
     377           
    366378    def import_exam_as_temp_catalog(self, exam_path: str):
    367         exam = Exam.from_json_file(exam_path)  # or however you deserialize
    368 
     379        temp_id = "TEMP_" + datetime.utcnow().strftime("%H%M%S")
     380        with open(exam_path, "r", encoding="utf-8") as f:
     381            data = json.load(f)
     382
     383        for each in self.find_all_domain_ids(data, set()):
     384            if each not in self.domain_manager.all_domain_ids():
     385                Domain.with_domain_id(each)
     386        exam = Exam.from_dict(data)  # or however you deserialize
     387        print(exam.to_dict())
     388        # print(exam)
    369389        ids = [q.flexo_id for q in exam.questions]
    370390        duplicates = [i for i in set(ids) if ids.count(i) > 1]
     
    372392
    373393        # Create a temporary catalog
    374         temp_id = "TEMP_" + datetime.utcnow().strftime("%H%M%S")
    375394        # FIXME: Check flexo_id creation
    376         temp_catalog = QuestionCatalog(
     395        domain = Domain.with_domain_id(temp_id)
     396        temp_catalog = QuestionCatalog.with_domain_id(
     397            domain_id=domain.domain_id,
    377398            title=f"Imported from {Path(exam_path).name}",
    378399            author="import",
     
    406427        if not path:
    407428            return
    408         try:
    409             self.import_exam_as_temp_catalog(path)
    410         except Exception as e:
    411             messagebox.showerror("Error", f"Could not load exam:\n{e}")
    412             return
     429        # try:
     430        self.import_exam_as_temp_catalog(path)
     431        #except Exception as e:
     432        #    messagebox.showerror("Error", f"Could not load exam:\n{e}")
     433        #    return
    413434
    414435    def require_selected_question(self):
  • gui/session_manager.py

    r0792ddd re75910d  
    3333                path = self.exam_dir / f"{ex_id}.json"
    3434
     35                print(each.to_dict)
    3536                if isinstance(each.flexo_id, str):
    3637                    print(f"[WARN] Catalog {each.title} has string flexo_id: {each.flexo_id}")
  • tests/conftest.py

    r0792ddd re75910d  
    3434                                              text="Please enter your candidate ID and name.",
    3535                                              fields=["Name", "Candidate ID"],
    36                                               media=[NullMediaItem.default()],
     36                                              media=[],
    3737                                              )
    3838
    3939    q_instr = InstructionBlock.with_domain_id(domain_id="EXAM_INSTRUCTION",
    4040                                              text="Read each question carefully before answering.",
    41                                               media=[NullMediaItem.default()],
     41                                              media=[],
    4242                                              )
    4343
     
    4848                                                      AnswerOption("B", "Current = Voltage × Resistance", 0),
    4949                                                  ],
    50                                                   media=[NullMediaItem.default()],
     50                                                  media=[],
    5151                                                  )
    5252
     
    5858                                                                  AnswerOption("C", "Joule per second", 1),
    5959                                                              ],
    60                                                               media=[NullMediaItem.default()],
     60                                                              media=[],
    6161                                                              )
    6262
     
    6464                                         text="Explain briefly how resistance affects current flow.",
    6565                                         validation=Validation(maxlength=50),
    66                                          media=[NullMediaItem.default()],
     66                                         media=[],
    6767                                         )
    6868
  • tests/test_exam.py

    r0792ddd re75910d  
    3333        sample_exam.layout.add_page("Page 1")
    3434        sample_exam.layout.pages[-1].question_ids.append(q.flexo_id)
     35     
     36        data = sample_exam.to_dict()
    3537
    36         data = sample_exam.to_dict()
     38        print("Exam data\n", data)
    3739        json_data = json.dumps(data)
    3840        print("DATA:", json_data)
  • tests/test_question_factory.py

    r0792ddd re75910d  
    2222    parsed = obj.flexo_id.to_dict()
    2323    assert parsed["domain_id"] == "WIP_TEST"
    24     assert parsed["state"] == "A"
     24    assert parsed["state"] == "D"
     25
    2526
    2627def test_text_question_validation_optional():
    2728    Domain.with_domain_id(domain_id="WIP_TEST")
    28     q = {"domain_id": "WIP_TEST", "qtype": "text", "text": "Explain", "validation": {"maxlength": 5}}
     29    q = {"domain_id": "WIP_TEST", "qtype": "text",
     30         "text": "Explain", "validation": {"maxlength": 5}}
    2931    obj = question_factory(q)
    3032    assert isinstance(obj, TextQuestion)
Note: See TracChangeset for help on using the changeset viewer.