Changeset 0792ddd in flexograder
- Timestamp:
- 11/20/25 13:15:40 (5 months ago)
- Branches:
- fake-data, main, master
- Children:
- e75910d
- Parents:
- aaaa4b8
- Files:
-
- 5 added
- 8 edited
-
builder/catalog_manager.py (modified) (2 diffs)
-
builder/domain.py (added)
-
builder/exam_elements.py (modified) (1 diff)
-
builder/flexo_entity.py (added)
-
builder/question_factory.py (modified) (1 diff)
-
builder/questions.py (added)
-
gui/domain_editor_dialog.py (added)
-
gui/domain_management_dialog.py (added)
-
gui/gui.py (modified) (14 diffs)
-
gui/option_question_editor.py (modified) (5 diffs)
-
tests/test_catalog_manager.py (modified) (2 diffs)
-
tests/test_question_factory.py (modified) (1 diff)
-
tests/test_questions.py (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
-
builder/catalog_manager.py
raaaa4b8 r0792ddd 22 22 """I return a trash bin catalog if requested""" 23 23 if self._trashbin is None: 24 self._trashbin = QuestionCatalog.with_domain(Domain("TRASH", 25 "THE SYSTEMS TRASHBIN", 26 "I manage deleted DRAFTS until purge")) 24 self._trashbin = QuestionCatalog.with_domain_id(domain_id="TRASH_TRASHBIN") 27 25 return self._trashbin 28 26 … … 38 36 key = base_title 39 37 if key in mapping: 40 key = f"{base_title} ({cat.domain } v{cat.version})"38 key = f"{base_title} ({cat.domain_id} v{cat.version})" 41 39 mapping[key] = cat 42 40 return mapping -
builder/exam_elements.py
raaaa4b8 r0792ddd 107 107 108 108 # NOTE: cls(...) will call the correct subclass constructor 109 obj = cls (109 obj = cls.with_domain_id(domain_id=flexo_id.domain_id, 110 110 text=data.get("text", ""), 111 111 topic=data.get("topic", ""), -
builder/question_factory.py
raaaa4b8 r0792ddd 54 54 55 55 # Case 2 – CREATE NEW 56 domain_id = detect_domain(q)56 domain_id = q.get("domain_id", "GEN_GENERIC") 57 57 58 58 # domain-specific optional args -
gui/gui.py
raaaa4b8 r0792ddd 5 5 from pathlib import Path 6 6 from datetime import datetime 7 from flexoentity import FlexOID, logger7 from flexoentity import FlexOID, DomainManager, logger, Domain 8 8 from builder.exam import Exam 9 9 from builder.question_catalog import QuestionCatalog 10 from builder.question_catalog import question_factory 10 11 from builder.media_items import NullMediaItem 11 12 from builder.catalog_manager import CatalogManager … … 13 14 from builder.exam_elements import SingleChoiceQuestion, MultipleChoiceQuestion, InstructionBlock 14 15 from .menu import AppMenu 16 from gui.domain_management_dialog import DomainManagementDialog 15 17 from .actions_panel import ActionsPanel 16 18 from .select_panel import SelectPanel … … 26 28 def __init__(self): 27 29 super().__init__() 30 self.domain_manager = DomainManager() 28 31 self.catalog_manager = CatalogManager() 29 32 self.exam_manager = ExamManager() … … 47 50 48 51 self._recent_actions = [] 49 self.domain_set = set()50 52 self.clipboard = [] # holds copied Question objects 51 53 self.clipboard_action = None # "copy" … … 147 149 simpledialog.askstring("Author", "Enter author:", parent=self) or "unknown" 148 150 ) 149 catalog = QuestionCatalog.with_domain (did)151 catalog = QuestionCatalog.with_domain_id(did) 150 152 catalog.title = title 151 153 catalog.author =author … … 242 244 return [] 243 245 # Always provide deterministic order (e.g. by question ID) 244 return sorted(self.active_catalog.questions, key=lambda q: q. id)246 return sorted(self.active_catalog.questions, key=lambda q: q.flexo_id) 245 247 246 248 def create_exam_dialog(self): … … 265 267 self.target_exam_var.set(active_exam.title) 266 268 267 def get_question_editor_for( parent, question, available_domains):269 def get_question_editor_for(self, parent, question, available_domains): 268 270 """ 269 271 Return the appropriate editor dialog instance for a given question. … … 271 273 """ 272 274 if question.qtype in ("single_choice", "multiple_choice"): 273 return OptionQuestionEditorDialog(parent, question , available_domains)275 return OptionQuestionEditorDialog(parent, question.to_dict(), available_domains) 274 276 if question.qtype in ("instruction", "text"): 275 277 # even though InstructionBlock may subclass Question, 276 278 # it doesn’t need options 277 return DefaultQuestionEditorDialog(parent, question , available_domains)278 else:279 # Fallback for any unknown or simple Question subclass280 return DefaultQuestionEditorDialog(parent, question, available_domains)279 return DefaultQuestionEditorDialog(parent, question.to_dict(), available_domains) 280 281 # Fallback for any unknown or simple Question subclass 282 return DefaultQuestionEditorDialog(parent, question.to_dict(), available_domains) 281 283 282 284 def add_question(self): … … 290 292 media = [] 291 293 292 if qtype == "single_choice": 293 q = SingleChoiceQuestion(text="Neue Frage", 294 options=[], media=media) 295 elif qtype == "multiple_choice": 296 q = MultipleChoiceQuestion(text="Neue Mehrfachfrage", 297 options=[], media=media) 294 if qtype in ["single_choice", "multiple_choice"]: 295 question_dict = {"qtype": qtype, 296 "text": "Neue Frage", 297 "options": [], 298 "media": media} 298 299 elif qtype == "instruction": 299 q = InstructionBlock(text="Neue Instruktion", media=media)300 question_dict = InstructionBlock(text="Neue Instruktion", media=media) 300 301 else: 301 302 messagebox.showerror("Error", f"Unknown question type: {qtype}") 302 303 return 303 304 304 dlg = OptionQuestionEditorDialog(self, q, self.domain_set) 305 print(self.domain_manager.all_domain_ids()) 306 dlg = OptionQuestionEditorDialog(self, question_dict, self.domain_manager.all_domain_ids()) 305 307 self.wait_window(dlg) 306 current_question = dlg.result 307 308 if current_question: 309 current_question.flexo_id = FlexOID.generate( 310 current_question.domain, 311 current_question.entity_type, 312 current_question.state, 313 current_question.text_seed 314 ) 308 question_dict = dlg.result 309 310 if question_dict: 311 question_dict["qtype"] = "single_choice" 312 current_question = question_factory(question_dict) 315 313 active_catalog.add_questions([current_question]) 316 314 self.refresh_all() 317 self.log_action(f"Updated - Question { q.id} updated.")318 self.log_action(f"New Question ID - Assigned ID: {current_question. id}")315 self.log_action(f"Updated - Question {current_question.flexo_id} updated.") 316 self.log_action(f"New Question ID - Assigned ID: {current_question.flexo_id}") 319 317 320 318 def add_selected_question_to_exam(self): … … 332 330 return 333 331 334 if not messagebox.askyesno("Delete", f"Delete question {q. id}?"):332 if not messagebox.askyesno("Delete", f"Delete question {q.flexo_id}?"): 335 333 return 336 334 337 335 try: 338 if self.active_catalog.remove(q. id):336 if self.active_catalog.remove(q.flexo_id): 339 337 self.refresh_all() 340 self.log_action(f"Deleted - Question {q. id} removed.")338 self.log_action(f"Deleted - Question {q.flexo_id} removed.") 341 339 except ValueError as e: 342 340 messagebox.showerror("Forbidden", str(e)) … … 358 356 def edit_selected_question(self): 359 357 q = self.require_selected_question() 360 dlg = self.get_question_editor_for( q, self.domain_set)358 dlg = self.get_question_editor_for(self, q, self.domain_manager.all_domain_ids()) 361 359 self.wait_window(dlg) 362 360 current_question = dlg.result … … 364 362 self.active_catalog.add_questions([current_question]) 365 363 self.refresh_all() 366 self.log_action(f"Updated - Question {q. id} updated.")364 self.log_action(f"Updated - Question {q.flexo_id} updated.") 367 365 368 366 def import_exam_as_temp_catalog(self, exam_path: str): … … 484 482 return 485 483 486 catalogs = self.catalog_manager.catalogs ()484 catalogs = self.catalog_manager.catalogs 487 485 if len(catalogs) < 2: 488 486 messagebox.showinfo(action.title(), "You need at least two catalogs.") … … 606 604 return 607 605 608 entry = simpledialog.askstring("Add Domain", 609 "Enter domain (e.g. ELEK_Elektrotechnik):", 610 parent=self) 611 if not entry: 606 dlg = DomainManagementDialog(self, self.domain_manager) 607 self.wait_window() 608 if not dlg: 612 609 return 613 610 614 611 try: 615 active.add_domain(entry) 616 self.domain_set.update([entry]) 617 self.log_action(f"Added - Domain '{entry}' added to catalog '{active.catalog_id}'.") 612 self.domain_manager.register(Domain.with_domain_id(dlg.result)) 613 self.log_action(f"Added - Domain '{dlg.result}' added to catalog '{active.catalog_id}'.") 618 614 except ValueError as e: 619 615 messagebox.showerror("Invalid Input", str(e)) -
gui/option_question_editor.py
raaaa4b8 r0792ddd 1 1 import tkinter as tk 2 2 from tkinter import ttk, messagebox 3 from flexoentity import EntityState 3 from flexoentity import EntityState, FlexOID 4 4 from builder.exam_elements import AnswerOption 5 from .answer_options_dialog import AnswerOptionsDialog6 from .attach_media_dialog import AttachMediaDialog5 from gui.answer_options_dialog import AnswerOptionsDialog 6 from gui.attach_media_dialog import AttachMediaDialog 7 7 8 8 class OptionQuestionEditorDialog(tk.Toplevel): 9 9 """Dialog for editing OptionQuestions (SingleChoiceQuestion, MultipleChoiceQuestion).""" 10 10 11 def __init__(self, parent, question =None, available_domains=None):11 def __init__(self, parent, question_dict=None, available_domains=None): 12 12 super().__init__(parent) 13 self.title("Edit Options")14 13 self.transient(parent) 15 14 # ensure window is displayed before grabbing … … 17 16 self.wait_visibility() 18 17 self.grab_set() 18 self.question_dict = question_dict 19 19 self.result = None 20 20 21 self.question = question 21 flexo_id_str = self.question_dict.get('flexo_id', "") 22 23 if flexo_id_str: 24 self.flexo_id = FlexOID(flexo_id_str) 25 state_str = self.flexo_id.state_code 26 self.domain_id = self.flexo_id.domain_id 27 else: 28 state_str = self.question_dict.get("state", "D") 29 self.domain_id = self.question_dict.get("domain_id", "") 30 31 self.state = EntityState(state_str) 22 32 self.available_domains = sorted(available_domains or []) 23 self.domain_var = tk.StringVar(value=self.question.domain_code if self.question else "") 24 self.state_var = tk.StringVar(value=self.question.state.name if self.question else EntityState.DRAFT.name) 33 self.state_var = tk.StringVar(value=self.state.name) 25 34 26 self.title(f"Edit Option Question {getattr(self.question, 'flexo_id', '')}") 35 self.domain_var = tk.StringVar(value=self.domain_id) 36 self.title(f"Edit Option Question {flexo_id_str}") 27 37 self.create_widgets() 28 38 self.populate_fields() … … 32 42 frm.pack(fill="both", expand=True) 33 43 34 # Domain44 # -------- Domain ---------- 35 45 ttk.Label(frm, text="Domain").pack(anchor="w") 36 self.cmb_domain = ttk.Combobox(frm, textvariable=self.domain_var, 37 values=self.available_domains, state="readonly") 46 self.cmb_domain = ttk.Combobox( 47 frm, 48 textvariable=self.domain_var, 49 values=self.available_domains, 50 state="readonly", 51 ) 38 52 self.cmb_domain.pack(fill="x", pady=(0, 10)) 39 53 40 # Question text54 # -------- Text ---------- 41 55 ttk.Label(frm, text="Question Text").pack(anchor="w") 42 56 self.txt_question = tk.Text(frm, height=5, wrap="word") 43 57 self.txt_question.pack(fill="x", pady=(0, 10)) 44 58 45 # Status / State59 # -------- State ---------- 46 60 ttk.Label(frm, text="Status").pack(anchor="w") 47 self.cmb_status = ttk.Combobox(frm, textvariable=self.state_var, values=self.question.allowed_transitions(), state="readonly") 61 # If editing an existing question, use its allowed transitions 62 self.cmb_status = ttk.Combobox( 63 frm, 64 textvariable=self.state_var, 65 values=self.state.allowed_transitions(), 66 state="readonly", 67 ) 48 68 self.cmb_status.pack(fill="x", pady=(0, 10)) 49 69 50 # Answers section 51 ttk.Label(frm, text="Answers").pack(anchor="w") 52 self.btn_edit_answers = ttk.Button(frm, text="Edit Answers…", command=self.open_answer_editor) 53 self.btn_edit_answers.pack(anchor="w", pady=(5, 10)) 70 # -------- Options + Media (aligned horizontally) ---------- 71 action_frame = ttk.Frame(frm) 72 action_frame.pack(fill="x", pady=(0, 10)) 54 73 55 ttk.Button(frm, text="Attach Media...", command=self.open_media_dialog).pack(pady=5) 74 self.btn_edit_answers = ttk.Button( 75 action_frame, 76 text="Edit Answers…", 77 command=self.open_answer_editor, 78 width=20 79 ) 80 self.btn_edit_answers.pack(side="left", padx=(0, 5)) 56 81 57 # Bottom buttons 82 self.btn_media = ttk.Button( 83 action_frame, 84 text="Attach Media…", 85 command=self.open_media_dialog, 86 width=20 87 ) 88 self.btn_media.pack(side="left") 89 90 # -------- OK / Cancel ---------- 58 91 btn_frame = ttk.Frame(frm) 59 92 btn_frame.pack(fill="x", pady=(10, 0)) 93 60 94 ttk.Button(btn_frame, text="OK", command=self.on_ok).pack(side="right", padx=5) 61 95 ttk.Button(btn_frame, text="Cancel", command=self.on_cancel).pack(side="right") 62 96 97 # def create_widgets(self): 98 # frm = ttk.Frame(self, padding=10) 99 # frm.pack(fill="both", expand=True) 100 101 # # Domain 102 # ttk.Label(frm, text="Domain").pack(anchor="w") 103 # self.cmb_domain = ttk.Combobox(frm, textvariable=self.domain_var, 104 # values=self.available_domains, state="readonly") 105 # self.cmb_domain.pack(fill="x", pady=(0, 10)) 106 107 # # Question text 108 # ttk.Label(frm, text="Question Text").pack(anchor="w") 109 # self.txt_question = tk.Text(frm, height=5, wrap="word") 110 # self.txt_question.pack(fill="x", pady=(0, 10)) 111 112 # # Status / State 113 # ttk.Label(frm, text="Status").pack(anchor="w") 114 # allowed_transitions = self.state.allowed_transitions() 115 116 # self.cmb_status = ttk.Combobox(frm, textvariable=self.state_var, 117 # values=allowed_transitions, state="readonly") 118 # self.cmb_status.pack(fill="x", pady=(0, 10)) 119 120 # # Answers section 121 # ttk.Label(frm, text="Answers").pack(anchor="w") 122 # self.btn_edit_answers = ttk.Button(frm, text="Edit Answers…", command=self.open_answer_editor) 123 # self.btn_edit_answers.pack(anchor="w", pady=(5, 10)) 124 125 # ttk.Button(frm, text="Attach Media...", command=self.open_media_dialog).pack(pady=5) 126 127 # # Bottom buttons 128 # btn_frame = ttk.Frame(frm) 129 # btn_frame.pack(fill="x", pady=(10, 0)) 130 # ttk.Button(btn_frame, text="OK", command=self.on_ok).pack(side="right", padx=5) 131 # ttk.Button(btn_frame, text="Cancel", command=self.on_cancel).pack(side="right") 132 63 133 def populate_fields(self): 64 134 self.txt_question.delete("1.0", tk.END) 65 text_value = getattr(self.question,"text", "")135 text_value = self.question_dict.get("text", "") 66 136 self.txt_question.insert("1.0", text_value or "") 67 137 68 138 def open_answer_editor(self): 69 139 """Launch the unified AnswerOptionsDialog.""" 70 dlg = AnswerOptionsDialog(self, options=self.question .options)140 dlg = AnswerOptionsDialog(self, options=self.question_dict.get("options", [])) 71 141 if dlg.result: 72 self.question .options= [142 self.question_dict["options"] = [ 73 143 AnswerOption(o["id"], o["text"], o["points"]) for o in dlg.result 74 144 ] … … 76 146 77 147 def open_media_dialog(self): 78 dlg = AttachMediaDialog(self, self.question )148 dlg = AttachMediaDialog(self, self.question_dict) 79 149 dlg.grab_set() 80 150 self.wait_window(dlg) … … 87 157 return 88 158 89 q = self.question.with_domain 90 self.question.text = text 91 self.question.domain_code = self.cmb_domain.get() 92 new_state = EntityState[self.cmb_status.get()] 93 self.question.apply_state_change(new_state) 94 self.result = self.question 159 self.question_dict["text"] = text 160 self.question_dict["domain_id"] = self.cmb_domain.get() 161 self.question_dict["state"] = self.cmb_status.get() 162 self.result = self.question_dict 95 163 self.destroy() 96 164 -
tests/test_catalog_manager.py
raaaa4b8 r0792ddd 4 4 from builder.catalog_manager import CatalogManager 5 5 from builder.exam_elements import SingleChoiceQuestion, AnswerOption 6 from flexoentity import EntityType, EntityState 6 from flexoentity import EntityType, EntityState, Domain 7 7 8 8 … … 21 21 @pytest.fixture 22 22 def sample_catalog(manager): 23 Domain.with_domain_id("TEST_CATALOG") 23 24 opts = [AnswerOption("A", "True", 1), AnswerOption("B", "False", 0)] 24 25 q = SingleChoiceQuestion.with_domain_id(domain_id="TEST_CATALOG", -
tests/test_question_factory.py
raaaa4b8 r0792ddd 2 2 from builder.question_factory import question_factory 3 3 from builder.exam_elements import SingleChoiceQuestion, MultipleChoiceQuestion, TextQuestion 4 from flexoentity import EntityState 4 from flexoentity import EntityState, Domain 5 5 6 6 def test_radio_question_autoid(): 7 q = {" qtype": "single_choice", "text": "What is Ohm’s law?",7 q = {"domain_id": "WIP_TEST", "qtype": "single_choice", "text": "What is Ohm’s law?", 8 8 "options": [{"id": "A", "text": "U = R / I"}, {"id": "B", "text": "U = R * I"}]} 9 9 obj = question_factory(q) 10 10 assert isinstance(obj, SingleChoiceQuestion) 11 11 assert obj.state == EntityState.DRAFT 12 parsed = obj.flexo_id. parsed()12 parsed = obj.flexo_id.to_dict() 13 13 assert parsed["entity_type"] == "I" 14 14 assert parsed["version"] == 1 15 15 16 16 def test_multiple_choice_question_existing_id(): 17 q = {"qtype": "multiple_choice",18 "flexo_id": "AF-Q251022-AB12CD@002A",17 Domain.with_domain_id("WIP_TEST") 18 q = {"domain_id": "WIP_TEST", "qtype": "multiple_choice", 19 19 "text": "Select power units"} 20 20 obj = question_factory(q) 21 21 assert isinstance(obj, MultipleChoiceQuestion) 22 parsed = obj.flexo_id. parsed()23 assert parsed["domain_id"] == " AF"22 parsed = obj.flexo_id.to_dict() 23 assert parsed["domain_id"] == "WIP_TEST" 24 24 assert parsed["state"] == "A" 25 25 26 26 def test_text_question_validation_optional(): 27 q = {"qtype": "text", "text": "Explain", "validation": {"maxlength": 5}} 27 Domain.with_domain_id(domain_id="WIP_TEST") 28 q = {"domain_id": "WIP_TEST", "qtype": "text", "text": "Explain", "validation": {"maxlength": 5}} 28 29 obj = question_factory(q) 29 30 assert isinstance(obj, TextQuestion) -
tests/test_questions.py
raaaa4b8 r0792ddd 7 7 8 8 from builder.exam_elements import InstructionBlock, SingleChoiceQuestion, AnswerOption 9 from flexoentity import EntityType, EntityState 9 from flexoentity import EntityType, EntityState, Domain 10 10 11 11 … … 17 17 18 18 def test_answer_options_display_instruction(self): 19 19 Domain.with_domain_id("EXAM_INSTRUCTIONS") 20 20 instr = InstructionBlock.with_domain_id(domain_id="EXAM_INSTRUCTIONS", 21 21 text="I1", … … 25 25 26 26 def test_answer_options_display_radio(self): 27 Domain.with_domain_id("PY_ARITHM") 27 28 opts = [AnswerOption("A", "Yes", 1), AnswerOption("B", "No", 0)] 28 29 q = SingleChoiceQuestion.with_domain_id(domain_id="PY_ARITHM",
Note:
See TracChangeset
for help on using the changeset viewer.
