Changeset e75910d in flexograder
- Timestamp:
- 11/22/25 17:46:21 (4 months ago)
- Branches:
- fake-data, main, master
- Children:
- 858a4dc
- Parents:
- 0792ddd
- Files:
-
- 2 added
- 11 edited
-
builder/domain_manager.py (added)
-
builder/exam.py (modified) (5 diffs)
-
builder/exam_elements.py (modified) (1 diff)
-
builder/media_items.py (modified) (9 diffs)
-
builder/question_catalog.py (modified) (6 diffs)
-
examples/KILE_EXAM.json (modified) (21 diffs)
-
examples/demo/example_exam.json (modified) (1 diff)
-
gui/gui.py (modified) (4 diffs)
-
gui/session_manager.py (modified) (1 diff)
-
tests/conftest.py (modified) (4 diffs)
-
tests/test_domain.py (added)
-
tests/test_exam.py (modified) (1 diff)
-
tests/test_question_factory.py (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
builder/exam.py
r0792ddd re75910d 6 6 import zipfile 7 7 8 from flexoentity import FlexoEntity, EntityType, EntityState 8 from flexoentity import FlexoEntity, EntityType, EntityState, Domain, FlexOID 9 9 from .exam_elements import ExamElement, ChoiceQuestion 10 10 from .question_factory import question_factory 11 from .domain_manager import DomainManager 11 12 12 13 @dataclass … … 75 76 submit_note: str = "" 76 77 author: str = "unknown" 78 domains: Dict[str, Dict[str, Any]] = field(default_factory=dict) 77 79 questions: list[ExamElement] = field(default_factory=list) 78 80 meta_extra: Optional[Dict[str, Any]] = field(default_factory=dict) … … 93 95 qid = str(qid) 94 96 return next((q for q in self.questions if q.flexo_id == qid), None) 95 96 def __post_init__(self):97 super().__post_init__()98 97 99 98 def ensure_page(self, title: str) -> ExamPage: … … 118 117 def to_dict(self): 119 118 base = super().to_dict() 119 120 120 base.update({ 121 " meta": {122 " exam_id": str(self.flexo_id),121 "exam": { 122 "flexo_id": self.flexo_id, 123 123 "title": self.title, 124 124 "duration": self.duration, … … 130 130 **self.meta_extra, 131 131 }, 132 "domains": self.domains, 132 133 "layout": self.layout.to_dict(), 133 134 "questions": [q.to_dict() for q in self.questions], 134 135 }) 136 135 137 return base 136 138 137 139 @classmethod 138 140 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 ) 191 167 192 168 @classmethod -
builder/exam_elements.py
r0792ddd re75910d 107 107 108 108 # NOTE: cls(...) will call the correct subclass constructor 109 obj = cls .with_domain_id(domain_id=flexo_id.domain_id,109 obj = cls( 110 110 text=data.get("text", ""), 111 111 topic=data.get("topic", ""), 112 112 flexo_id=flexo_id, 113 _in_factory=True 113 114 ) 114 115 -
builder/media_items.py
r0792ddd re75910d 5 5 from datetime import datetime 6 6 from dataclasses import dataclass, field 7 from flexoentity import FlexOID, EntityType, EntityState, FlexoEntity 7 from flexoentity import FlexOID, EntityType, EntityState, FlexoEntity, logger 8 8 9 9 @dataclass … … 16 16 caption: str = "" 17 17 author: str = "" 18 created_at: datetime = field(default_factory=datetime.utcnow)19 18 ExportPrefix = "media/" 20 19 … … 36 35 return "file" 37 36 37 @property 38 def type(self) -> str: 39 # logical type used for JSON, defaults to mtype 40 return self.mtype 41 38 42 @classmethod 39 43 def default(cls): … … 43 47 super().__post_init__() 44 48 49 @property 45 50 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}" 52 52 53 53 def to_dict(self): … … 57 57 "title": self.title, 58 58 "caption": self.caption, 59 "type": self. mtype,59 "type": self.type, 60 60 }) 61 61 return base … … 63 63 @classmethod 64 64 def from_dict(cls, data): 65 flexo_id = FlexOID(data["flexo_id"]) if "flexo_id" in data else None 66 65 67 obj = cls( 68 flexo_id=flexo_id, 66 69 src=data.get("src", ""), 67 70 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, 69 77 ) 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 73 79 return obj 74 80 75 81 def to_html(self): 76 82 raise NotImplementedError … … 122 128 return f'<a href="{prefix + self.src}" download>{self.label}</a>' 123 129 124 125 130 @dataclass 126 131 class NullMediaItem(MediaItem): 127 132 133 def __post_init__(self): 134 # Null objects bypass FlexOEntity lifecycle: no identity 135 self.flexo_id = None 136 self.fingerprint = None 137 128 138 @classmethod 129 139 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) 132 141 133 def __post_init__(self):134 super().__post_init__()142 def to_dict(self): 143 return {} 135 144 136 145 @property … … 141 150 return prefix 142 151 143 def to_dict(self): 144 return {} 152 def media_factory(m: dict) -> MediaItem: 145 153 146 # FIXME: We should really check if all attributes are initialized correctly147 148 def media_factory(m: dict) -> MediaItem:149 154 mtype = m.get("type", "").lower() 150 src = m.get("src", "")151 domain = m.get("domain", "GEN")152 label = m.get("label", None)153 155 154 156 cls = { … … 156 158 "audio": AudioItem, 157 159 "video": VideoItem, 158 "download": DownloadItem 160 "download": DownloadItem, 159 161 }.get(mtype, NullMediaItem) 160 162 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 4 4 from .exam_elements import ExamElement 5 5 from builder.question_factory import question_factory 6 from flexoentity import FlexoEntity, EntityType, EntityState 6 from flexoentity import FlexoEntity, EntityType, EntityState, FlexOID 7 7 8 8 … … 19 19 def default(cls): 20 20 """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", 23 23 title="Untitled Catalog", 24 24 author="unknown", … … 42 42 @property 43 43 def empty(self): 44 return not self. _questions44 return not self.questions 45 45 46 46 @property … … 103 103 return qs 104 104 105 # ─────────────────────────────────────────────106 # ID helper107 # ─────────────────────────────────────────────108 105 def next_question_id(self, domain: str) -> str: 109 106 abbrev = domain.split("_", 1)[0].upper() if "_" in domain else domain.upper() … … 116 113 return f"{abbrev}_{next_num:03d}" 117 114 118 # ─────────────────────────────────────────────119 # Serialization120 # ─────────────────────────────────────────────121 115 def to_dict(self): 122 116 d = super().to_dict() 123 117 d.update({ 124 " id": str(self.flexo_id),118 "flexo_id": self.flexo_id, 125 119 "title": self.title, 126 120 "author": self.author, … … 131 125 @classmethod 132 126 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 133 133 obj = cls( 134 flexo_id= data.get("flexo_id"),134 flexo_id=flexo_id, 135 135 title=data.get("title", ""), 136 136 author=data.get("author", "unknown"), 137 fingerprint=data.get("fingerprint"), 138 _in_factory=True, 137 139 ) 140 141 # Restore questions 138 142 obj.questions = [question_factory(qd) for qd in data.get("questions", [])] 143 139 144 return obj -
examples/KILE_EXAM.json
r0792ddd re75910d 1 1 { 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": { 11 3 "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" 19 19 }, 20 20 "layout": { … … 69 69 "questions": [ 70 70 { 71 "domain ": "AI_BASICS",71 "domain_id": "AI_BASICS", 72 72 "entity_type": "ITEM", 73 73 "state": "DRAFT", … … 105 105 }, 106 106 { 107 "domain ": "AI_DTREE",107 "domain_id": "AI_DTREE", 108 108 "entity_type": "ITEM", 109 109 "state": "DRAFT", … … 141 141 }, 142 142 { 143 "domain ": "AI_ETHICS",143 "domain_id": "AI_ETHICS", 144 144 "entity_type": "ITEM", 145 145 "state": "DRAFT", … … 177 177 }, 178 178 { 179 "domain ": "AI_ML",179 "domain_id": "AI_ML", 180 180 "entity_type": "ITEM", 181 181 "state": "DRAFT", … … 213 213 }, 214 214 { 215 "domain ": "AI_NLP",215 "domain_id": "AI_NLP", 216 216 "entity_type": "ITEM", 217 217 "state": "DRAFT", … … 249 249 }, 250 250 { 251 "domain ": "AI_NN",251 "domain_id": "AI_NN", 252 252 "entity_type": "ITEM", 253 253 "state": "DRAFT", … … 285 285 }, 286 286 { 287 "domain ": "AI_TEXT",287 "domain_id": "AI_TEXT", 288 288 "entity_type": "ITEM", 289 289 "state": "DRAFT", … … 321 321 }, 322 322 { 323 "domain ": "API",323 "domain_id": "API", 324 324 "entity_type": "ITEM", 325 325 "state": "DRAFT", … … 357 357 }, 358 358 { 359 "domain ": "CS_ALGO",359 "domain_id": "CS_ALGO", 360 360 "entity_type": "ITEM", 361 361 "state": "DRAFT", … … 393 393 }, 394 394 { 395 "domain ": "DATA_BIG",395 "domain_id": "DATA_BIG", 396 396 "entity_type": "ITEM", 397 397 "state": "DRAFT", … … 429 429 }, 430 430 { 431 "domain ": "DATA_FORMATS",431 "domain_id": "DATA_FORMATS", 432 432 "entity_type": "ITEM", 433 433 "state": "DRAFT", … … 465 465 }, 466 466 { 467 "domain ": "DB_SQL",467 "domain_id": "DB_SQL", 468 468 "entity_type": "ITEM", 469 469 "state": "DRAFT", … … 501 501 }, 502 502 { 503 "domain ": "PROG_LANG",503 "domain_id": "PROG_LANG", 504 504 "entity_type": "ITEM", 505 505 "state": "DRAFT", … … 537 537 }, 538 538 { 539 "domain ": "SOFT_ENG",539 "domain_id": "SOFT_ENG", 540 540 "entity_type": "ITEM", 541 541 "state": "DRAFT", … … 573 573 }, 574 574 { 575 "domain ": "SOFT_METHODS",575 "domain_id": "SOFT_METHODS", 576 576 "entity_type": "ITEM", 577 577 "state": "DRAFT", … … 609 609 }, 610 610 { 611 "domain ": "WEB_ARCH",611 "domain_id": "WEB_ARCH", 612 612 "entity_type": "ITEM", 613 613 "state": "DRAFT", … … 645 645 }, 646 646 { 647 "domain ": "WEB_CSS",647 "domain_id": "WEB_CSS", 648 648 "entity_type": "ITEM", 649 649 "state": "DRAFT", … … 681 681 }, 682 682 { 683 "domain ": "WEB_DEV",683 "domain_id": "WEB_DEV", 684 684 "entity_type": "ITEM", 685 685 "state": "DRAFT", … … 717 717 }, 718 718 { 719 "domain ": "WEB_FRAMEWORKS",719 "domain_id": "WEB_FRAMEWORKS", 720 720 "entity_type": "ITEM", 721 721 "state": "DRAFT", … … 753 753 }, 754 754 { 755 "domain ": "WEB_HTML",755 "domain_id": "WEB_HTML", 756 756 "entity_type": "ITEM", 757 757 "state": "DRAFT", -
examples/demo/example_exam.json
r0792ddd re75910d 1 1 { 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" 12 12 }, 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": { 13 51 "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 } 145 84 ] 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 ] 146 270 } -
gui/gui.py
r0792ddd re75910d 303 303 return 304 304 305 print(self.domain_manager.all_domain_ids())306 305 dlg = OptionQuestionEditorDialog(self, question_dict, self.domain_manager.all_domain_ids()) 307 306 self.wait_window(dlg) … … 364 363 self.log_action(f"Updated - Question {q.flexo_id} updated.") 365 364 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 366 378 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) 369 389 ids = [q.flexo_id for q in exam.questions] 370 390 duplicates = [i for i in set(ids) if ids.count(i) > 1] … … 372 392 373 393 # Create a temporary catalog 374 temp_id = "TEMP_" + datetime.utcnow().strftime("%H%M%S")375 394 # 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, 377 398 title=f"Imported from {Path(exam_path).name}", 378 399 author="import", … … 406 427 if not path: 407 428 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 return429 # 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 413 434 414 435 def require_selected_question(self): -
gui/session_manager.py
r0792ddd re75910d 33 33 path = self.exam_dir / f"{ex_id}.json" 34 34 35 print(each.to_dict) 35 36 if isinstance(each.flexo_id, str): 36 37 print(f"[WARN] Catalog {each.title} has string flexo_id: {each.flexo_id}") -
tests/conftest.py
r0792ddd re75910d 34 34 text="Please enter your candidate ID and name.", 35 35 fields=["Name", "Candidate ID"], 36 media=[ NullMediaItem.default()],36 media=[], 37 37 ) 38 38 39 39 q_instr = InstructionBlock.with_domain_id(domain_id="EXAM_INSTRUCTION", 40 40 text="Read each question carefully before answering.", 41 media=[ NullMediaItem.default()],41 media=[], 42 42 ) 43 43 … … 48 48 AnswerOption("B", "Current = Voltage × Resistance", 0), 49 49 ], 50 media=[ NullMediaItem.default()],50 media=[], 51 51 ) 52 52 … … 58 58 AnswerOption("C", "Joule per second", 1), 59 59 ], 60 media=[ NullMediaItem.default()],60 media=[], 61 61 ) 62 62 … … 64 64 text="Explain briefly how resistance affects current flow.", 65 65 validation=Validation(maxlength=50), 66 media=[ NullMediaItem.default()],66 media=[], 67 67 ) 68 68 -
tests/test_exam.py
r0792ddd re75910d 33 33 sample_exam.layout.add_page("Page 1") 34 34 sample_exam.layout.pages[-1].question_ids.append(q.flexo_id) 35 36 data = sample_exam.to_dict() 35 37 36 data = sample_exam.to_dict()38 print("Exam data\n", data) 37 39 json_data = json.dumps(data) 38 40 print("DATA:", json_data) -
tests/test_question_factory.py
r0792ddd re75910d 22 22 parsed = obj.flexo_id.to_dict() 23 23 assert parsed["domain_id"] == "WIP_TEST" 24 assert parsed["state"] == "A" 24 assert parsed["state"] == "D" 25 25 26 26 27 def test_text_question_validation_optional(): 27 28 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}} 29 31 obj = question_factory(q) 30 32 assert isinstance(obj, TextQuestion)
Note:
See TracChangeset
for help on using the changeset viewer.
