Changeset 0792ddd in flexograder


Ignore:
Timestamp:
11/20/25 13:15:40 (5 months ago)
Author:
Enrico Schwass <ennoausberlin@…>
Branches:
fake-data, main, master
Children:
e75910d
Parents:
aaaa4b8
Message:

new entity creation rules enforced

Files:
5 added
8 edited

Legend:

Unmodified
Added
Removed
  • builder/catalog_manager.py

    raaaa4b8 r0792ddd  
    2222        """I return a trash bin catalog if requested"""
    2323        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")
    2725        return self._trashbin
    2826 
     
    3836            key = base_title
    3937            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})"
    4139            mapping[key] = cat
    4240        return mapping
  • builder/exam_elements.py

    raaaa4b8 r0792ddd  
    107107
    108108        # NOTE: cls(...) will call the correct subclass constructor
    109         obj = cls(
     109        obj = cls.with_domain_id(domain_id=flexo_id.domain_id,
    110110            text=data.get("text", ""),
    111111            topic=data.get("topic", ""),
  • builder/question_factory.py

    raaaa4b8 r0792ddd  
    5454
    5555    # Case 2 – CREATE NEW
    56     domain_id = detect_domain(q)
     56    domain_id = q.get("domain_id", "GEN_GENERIC")
    5757
    5858    # domain-specific optional args
  • gui/gui.py

    raaaa4b8 r0792ddd  
    55from pathlib import Path
    66from datetime import datetime
    7 from flexoentity import FlexOID, logger
     7from flexoentity import FlexOID, DomainManager, logger, Domain
    88from builder.exam import Exam
    99from builder.question_catalog import QuestionCatalog
     10from builder.question_catalog import question_factory
    1011from builder.media_items import NullMediaItem
    1112from builder.catalog_manager import CatalogManager
     
    1314from builder.exam_elements import SingleChoiceQuestion, MultipleChoiceQuestion, InstructionBlock
    1415from .menu import AppMenu
     16from gui.domain_management_dialog import DomainManagementDialog
    1517from .actions_panel import ActionsPanel
    1618from .select_panel import SelectPanel
     
    2628    def __init__(self):
    2729        super().__init__()
     30        self.domain_manager = DomainManager()
    2831        self.catalog_manager = CatalogManager()
    2932        self.exam_manager = ExamManager()
     
    4750
    4851        self._recent_actions = []
    49         self.domain_set = set()
    5052        self.clipboard = []  # holds copied Question objects
    5153        self.clipboard_action = None  # "copy"
     
    147149            simpledialog.askstring("Author", "Enter author:", parent=self) or "unknown"
    148150        )
    149         catalog = QuestionCatalog.with_domain(did)
     151        catalog = QuestionCatalog.with_domain_id(did)
    150152        catalog.title = title
    151153        catalog.author =author
     
    242244            return []
    243245        # 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)
    245247
    246248    def create_exam_dialog(self):
     
    265267            self.target_exam_var.set(active_exam.title)
    266268
    267     def get_question_editor_for(parent, question, available_domains):
     269    def get_question_editor_for(self, parent, question, available_domains):
    268270        """
    269271        Return the appropriate editor dialog instance for a given question.
     
    271273        """
    272274        if question.qtype in ("single_choice", "multiple_choice"):
    273             return OptionQuestionEditorDialog(parent, question, available_domains)
     275            return OptionQuestionEditorDialog(parent, question.to_dict(), available_domains)
    274276        if question.qtype in ("instruction", "text"):
    275277            # even though InstructionBlock may subclass Question,
    276278            # it doesn’t need options
    277             return DefaultQuestionEditorDialog(parent, question, available_domains)
    278         else:
    279             # Fallback for any unknown or simple Question subclass
    280             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)
    281283
    282284    def add_question(self):
     
    290292        media = []
    291293
    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}
    298299        elif qtype == "instruction":
    299             q = InstructionBlock(text="Neue Instruktion", media=media)
     300            question_dict = InstructionBlock(text="Neue Instruktion", media=media)
    300301        else:
    301302            messagebox.showerror("Error", f"Unknown question type: {qtype}")
    302303            return
    303304
    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())
    305307        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)
    315313            active_catalog.add_questions([current_question])
    316314            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}")
    319317
    320318    def add_selected_question_to_exam(self):
     
    332330            return
    333331
    334         if not messagebox.askyesno("Delete", f"Delete question {q.id}?"):
     332        if not messagebox.askyesno("Delete", f"Delete question {q.flexo_id}?"):
    335333            return
    336334
    337335        try:
    338             if self.active_catalog.remove(q.id):
     336            if self.active_catalog.remove(q.flexo_id):
    339337                self.refresh_all()
    340                 self.log_action(f"Deleted - Question {q.id} removed.")
     338                self.log_action(f"Deleted - Question {q.flexo_id} removed.")
    341339        except ValueError as e:
    342340            messagebox.showerror("Forbidden", str(e))
     
    358356    def edit_selected_question(self):
    359357        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())
    361359        self.wait_window(dlg)
    362360        current_question = dlg.result
     
    364362            self.active_catalog.add_questions([current_question])
    365363            self.refresh_all()
    366             self.log_action(f"Updated - Question {q.id} updated.")
     364            self.log_action(f"Updated - Question {q.flexo_id} updated.")
    367365
    368366    def import_exam_as_temp_catalog(self, exam_path: str):
     
    484482            return
    485483
    486         catalogs = self.catalog_manager.catalogs()
     484        catalogs = self.catalog_manager.catalogs
    487485        if len(catalogs) < 2:
    488486            messagebox.showinfo(action.title(), "You need at least two catalogs.")
     
    606604            return
    607605
    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:
    612609            return
    613610
    614611        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}'.")
    618614        except ValueError as e:
    619615            messagebox.showerror("Invalid Input", str(e))
  • gui/option_question_editor.py

    raaaa4b8 r0792ddd  
    11import tkinter as tk
    22from tkinter import ttk, messagebox
    3 from flexoentity import EntityState
     3from flexoentity import EntityState, FlexOID
    44from builder.exam_elements import AnswerOption
    5 from .answer_options_dialog import AnswerOptionsDialog
    6 from .attach_media_dialog import AttachMediaDialog
     5from gui.answer_options_dialog import AnswerOptionsDialog
     6from gui.attach_media_dialog import AttachMediaDialog
    77
    88class OptionQuestionEditorDialog(tk.Toplevel):
    99    """Dialog for editing OptionQuestions (SingleChoiceQuestion, MultipleChoiceQuestion)."""
    1010
    11     def __init__(self, parent, question=None, available_domains=None):
     11    def __init__(self, parent, question_dict=None, available_domains=None):
    1212        super().__init__(parent)
    13         self.title("Edit Options")
    1413        self.transient(parent)
    1514        # ensure window is displayed before grabbing
     
    1716        self.wait_visibility()
    1817        self.grab_set()
     18        self.question_dict = question_dict
    1919        self.result = None
    2020
    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)
    2232        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)
    2534
    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}")
    2737        self.create_widgets()
    2838        self.populate_fields()
     
    3242        frm.pack(fill="both", expand=True)
    3343
    34         # Domain
     44        # -------- Domain ----------
    3545        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        )
    3852        self.cmb_domain.pack(fill="x", pady=(0, 10))
    3953
    40         # Question text
     54        # -------- Text ----------
    4155        ttk.Label(frm, text="Question Text").pack(anchor="w")
    4256        self.txt_question = tk.Text(frm, height=5, wrap="word")
    4357        self.txt_question.pack(fill="x", pady=(0, 10))
    4458
    45         # Status / State
     59        # -------- State ----------
    4660        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        )
    4868        self.cmb_status.pack(fill="x", pady=(0, 10))
    4969
    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))
    5473
    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))
    5681
    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 ----------
    5891        btn_frame = ttk.Frame(frm)
    5992        btn_frame.pack(fill="x", pady=(10, 0))
     93
    6094        ttk.Button(btn_frame, text="OK", command=self.on_ok).pack(side="right", padx=5)
    6195        ttk.Button(btn_frame, text="Cancel", command=self.on_cancel).pack(side="right")
    6296
     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
    63133    def populate_fields(self):
    64134        self.txt_question.delete("1.0", tk.END)
    65         text_value = getattr(self.question, "text", "")
     135        text_value = self.question_dict.get("text", "")
    66136        self.txt_question.insert("1.0", text_value or "")
    67137
    68138    def open_answer_editor(self):
    69139        """Launch the unified AnswerOptionsDialog."""
    70         dlg = AnswerOptionsDialog(self, options=self.question.options)
     140        dlg = AnswerOptionsDialog(self, options=self.question_dict.get("options", []))
    71141        if dlg.result:
    72             self.question.options = [
     142            self.question_dict["options"] = [
    73143                AnswerOption(o["id"], o["text"], o["points"]) for o in dlg.result
    74144            ]
     
    76146
    77147    def open_media_dialog(self):
    78         dlg = AttachMediaDialog(self, self.question)
     148        dlg = AttachMediaDialog(self, self.question_dict)
    79149        dlg.grab_set()
    80150        self.wait_window(dlg)
     
    87157            return
    88158
    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
    95163        self.destroy()
    96164
  • tests/test_catalog_manager.py

    raaaa4b8 r0792ddd  
    44from builder.catalog_manager import CatalogManager
    55from builder.exam_elements import SingleChoiceQuestion, AnswerOption
    6 from flexoentity import EntityType, EntityState
     6from flexoentity import EntityType, EntityState, Domain
    77
    88
     
    2121@pytest.fixture
    2222def sample_catalog(manager):
     23    Domain.with_domain_id("TEST_CATALOG")
    2324    opts = [AnswerOption("A", "True", 1), AnswerOption("B", "False", 0)]
    2425    q = SingleChoiceQuestion.with_domain_id(domain_id="TEST_CATALOG",
  • tests/test_question_factory.py

    raaaa4b8 r0792ddd  
    22from builder.question_factory import question_factory
    33from builder.exam_elements import SingleChoiceQuestion, MultipleChoiceQuestion, TextQuestion
    4 from flexoentity import EntityState
     4from flexoentity import EntityState, Domain
    55
    66def 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?",
    88         "options": [{"id": "A", "text": "U = R / I"}, {"id": "B", "text": "U = R * I"}]}
    99    obj = question_factory(q)
    1010    assert isinstance(obj, SingleChoiceQuestion)
    1111    assert obj.state == EntityState.DRAFT
    12     parsed = obj.flexo_id.parsed()
     12    parsed = obj.flexo_id.to_dict()
    1313    assert parsed["entity_type"] == "I"
    1414    assert parsed["version"] == 1
    1515
    1616def 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",
    1919         "text": "Select power units"}
    2020    obj = question_factory(q)
    2121    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"
    2424    assert parsed["state"] == "A"
    2525
    2626def 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}}
    2829    obj = question_factory(q)
    2930    assert isinstance(obj, TextQuestion)
  • tests/test_questions.py

    raaaa4b8 r0792ddd  
    77
    88from builder.exam_elements import InstructionBlock, SingleChoiceQuestion, AnswerOption
    9 from flexoentity import EntityType, EntityState
     9from flexoentity import EntityType, EntityState, Domain
    1010
    1111
     
    1717
    1818    def test_answer_options_display_instruction(self):
    19 
     19        Domain.with_domain_id("EXAM_INSTRUCTIONS")
    2020        instr = InstructionBlock.with_domain_id(domain_id="EXAM_INSTRUCTIONS",
    2121                                                text="I1",
     
    2525
    2626    def test_answer_options_display_radio(self):
     27        Domain.with_domain_id("PY_ARITHM")
    2728        opts = [AnswerOption("A", "Yes", 1), AnswerOption("B", "No", 0)]
    2829        q = SingleChoiceQuestion.with_domain_id(domain_id="PY_ARITHM",
Note: See TracChangeset for help on using the changeset viewer.