Kentlo commited on
Commit
f8a0529
Β·
verified Β·
1 Parent(s): 8c00f47

Sync from GitHub f548297062c1b82c3ebf9d03b3ebc9992ba65cf7

Browse files
cert_study_app/services/quiz_service.py CHANGED
@@ -132,6 +132,29 @@ def normalize_choice_answer(value: str, ordered: bool = False) -> str:
132
 
133
 
134
  def yes_no_labels(value: str) -> list[str]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  text = str(value or "").strip().lower()
136
  if not text:
137
  return []
@@ -145,6 +168,28 @@ def yes_no_labels(value: str) -> list[str]:
145
  return labels if len(labels) >= 2 else []
146
 
147
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  def option_text(options: list[str], label: str) -> str:
149
  label = option_label(label)
150
  for index, option in enumerate(options, 1):
@@ -203,7 +248,19 @@ def effective_answer(question) -> str:
203
  answer = question.answer or ""
204
  question_type = (question.question_type or "").lower()
205
  options = normalize_options(question.get_options()) or extract_options_from_stem(question.stem)
206
- if question_type in {"hotspot", "table_choice", "matching", "ordering", "yes_no"}:
 
 
 
 
 
 
 
 
 
 
 
 
207
  analysis = visual_analysis(question)
208
  statements = analysis.get("statements")
209
  if isinstance(statements, list):
@@ -228,7 +285,7 @@ def effective_answer(question) -> str:
228
  labels = option_labels_from_texts(options, selected, allow_duplicates=True)
229
  if labels:
230
  return ",".join(labels)
231
- if question_type in {"hotspot", "table_choice"} and not yes_no_labels(answer):
232
  explanation_labels = yes_no_labels(question.explanation or "")
233
  if explanation_labels:
234
  return ",".join(explanation_labels)
 
132
 
133
 
134
  def yes_no_labels(value: str) -> list[str]:
135
+ if isinstance(value, (list, dict)):
136
+ raw_value = value
137
+ else:
138
+ raw_value = None
139
+ try:
140
+ raw_value = json.loads(value) if isinstance(value, str) else None
141
+ except Exception:
142
+ raw_value = None
143
+
144
+ if isinstance(raw_value, list):
145
+ values = [
146
+ item.get("value") or item.get("answer") or item.get("selected_answer")
147
+ for item in raw_value
148
+ if isinstance(item, dict)
149
+ ]
150
+ labels = yes_no_labels(",".join(str(item) for item in values if item))
151
+ if labels:
152
+ return labels
153
+ elif isinstance(raw_value, dict):
154
+ labels = yes_no_labels(",".join(str(item) for item in raw_value.values()))
155
+ if labels:
156
+ return labels
157
+
158
  text = str(value or "").strip().lower()
159
  if not text:
160
  return []
 
168
  return labels if len(labels) >= 2 else []
169
 
170
 
171
+ def structured_answer_values(value) -> list[str]:
172
+ if isinstance(value, (list, dict)):
173
+ raw_value = value
174
+ else:
175
+ try:
176
+ raw_value = json.loads(value) if isinstance(value, str) else None
177
+ except Exception:
178
+ raw_value = None
179
+
180
+ if isinstance(raw_value, list):
181
+ values = [
182
+ item.get("value") or item.get("answer") or item.get("selected_answer")
183
+ for item in raw_value
184
+ if isinstance(item, dict)
185
+ ]
186
+ elif isinstance(raw_value, dict):
187
+ values = list(raw_value.values())
188
+ else:
189
+ return []
190
+ return [str(item).strip() for item in values if str(item).strip()]
191
+
192
+
193
  def option_text(options: list[str], label: str) -> str:
194
  label = option_label(label)
195
  for index, option in enumerate(options, 1):
 
248
  answer = question.answer or ""
249
  question_type = (question.question_type or "").lower()
250
  options = normalize_options(question.get_options()) or extract_options_from_stem(question.stem)
251
+ structured_values = structured_answer_values(answer)
252
+ if structured_values:
253
+ structured_yn = yes_no_labels(",".join(structured_values))
254
+ return ",".join(structured_yn or structured_values)
255
+
256
+ is_structured_visual = any(
257
+ keyword in question_type
258
+ for keyword in ["hotspot", "table_choice", "matching", "ordering", "yes_no", "true/false"]
259
+ ) and "in-context" not in question_type
260
+ if is_structured_visual:
261
+ answer_labels = yes_no_labels(answer)
262
+ if answer_labels and "true/false" in question_type:
263
+ return ",".join(answer_labels)
264
  analysis = visual_analysis(question)
265
  statements = analysis.get("statements")
266
  if isinstance(statements, list):
 
285
  labels = option_labels_from_texts(options, selected, allow_duplicates=True)
286
  if labels:
287
  return ",".join(labels)
288
+ if is_structured_visual and not yes_no_labels(answer):
289
  explanation_labels = yes_no_labels(question.explanation or "")
290
  if explanation_labels:
291
  return ",".join(explanation_labels)
streamlit_app.py CHANGED
@@ -680,6 +680,29 @@ def box_choice_labels(question, count: int) -> list[str]:
680
 
681
 
682
  def yes_no_answer_labels(value: str) -> list[str]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
683
  text = str(value or "")
684
  if re.fullmatch(r"\s*[YNyn](?:\s*,\s*[YNyn])+\s*", text):
685
  return [token.upper() for token in re.findall(r"[YNyn]", text)]
@@ -691,8 +714,12 @@ def yes_no_answer_labels(value: str) -> list[str]:
691
 
692
  def is_yes_no_hotspot(question) -> bool:
693
  question_type = (question.get("question_type") or "").lower()
694
- if question_type not in {"hotspot", "table_choice"}:
 
 
695
  return False
 
 
696
  text = " ".join(
697
  [
698
  question.get("question") or "",
@@ -706,6 +733,65 @@ def is_yes_no_hotspot(question) -> bool:
706
  )
707
 
708
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
709
  def visual_statements(question) -> list[dict]:
710
  try:
711
  analysis = json.loads(question.get("visual_analysis_json") or "{}")
@@ -761,12 +847,16 @@ def render_answer_input(question, key_prefix):
761
  if statement_rows:
762
  return render_yes_no_matrix(question, f"{key_prefix}_statements", statement_rows)
763
 
 
 
 
 
764
  if is_yes_no_hotspot(question):
765
  answer_count = len(yes_no_answer_labels(question.get("answer") or "")) or len(
766
  yes_no_answer_labels(question.get("explanation") or "")
767
  )
768
  row_count = max(3, answer_count, len(statement_rows))
769
- rows = yes_no_lines(question.get("question") or "")
770
  if statement_rows:
771
  rows = statement_rows
772
  if len(rows) < row_count:
 
680
 
681
 
682
  def yes_no_answer_labels(value: str) -> list[str]:
683
+ if isinstance(value, (list, dict)):
684
+ raw_value = value
685
+ else:
686
+ raw_value = None
687
+ try:
688
+ raw_value = json.loads(value) if isinstance(value, str) else None
689
+ except Exception:
690
+ raw_value = None
691
+
692
+ if isinstance(raw_value, list):
693
+ values = [
694
+ item.get("value") or item.get("answer") or item.get("selected_answer")
695
+ for item in raw_value
696
+ if isinstance(item, dict)
697
+ ]
698
+ labels = yes_no_answer_labels(",".join(str(item) for item in values if item))
699
+ if labels:
700
+ return labels
701
+ elif isinstance(raw_value, dict):
702
+ labels = yes_no_answer_labels(",".join(str(item) for item in raw_value.values()))
703
+ if labels:
704
+ return labels
705
+
706
  text = str(value or "")
707
  if re.fullmatch(r"\s*[YNyn](?:\s*,\s*[YNyn])+\s*", text):
708
  return [token.upper() for token in re.findall(r"[YNyn]", text)]
 
714
 
715
  def is_yes_no_hotspot(question) -> bool:
716
  question_type = (question.get("question_type") or "").lower()
717
+ if "in-context" in question_type:
718
+ return False
719
+ if not any(keyword in question_type for keyword in ["hotspot", "table_choice", "yes_no", "true/false"]):
720
  return False
721
+ if "true/false" in question_type:
722
+ return True
723
  text = " ".join(
724
  [
725
  question.get("question") or "",
 
733
  )
734
 
735
 
736
+ def statement_option_rows(options: list[str], expected_count: int) -> list[str]:
737
+ rows = []
738
+ for option in options or []:
739
+ text = str(option or "").strip()
740
+ if not text:
741
+ continue
742
+ cleaned = re.sub(r"^\s*(?:[0-9]+|[A-Z])[-.)]\s*", "", text).strip()
743
+ if re.fullmatch(r"예|μ•„λ‹ˆμ˜€|μ•„λ‹ˆμš”|yes|no", cleaned, re.I):
744
+ continue
745
+ rows.append(cleaned)
746
+ if expected_count and len(rows) >= expected_count:
747
+ return rows[:expected_count]
748
+ return rows
749
+
750
+
751
+ def grouped_option_rows(options: list[str]) -> list[dict]:
752
+ grouped = {}
753
+ order = []
754
+ for option in options or []:
755
+ text = str(option or "").strip()
756
+ match = re.match(r"^\s*(\d+)-([A-Z])[\.)]?\s*(.+)$", text, re.I)
757
+ if not match:
758
+ continue
759
+ group_key = match.group(1)
760
+ body = match.group(3).strip()
761
+ row_label = f"ν•­λͺ© {group_key}"
762
+ value = body
763
+ if ":" in body:
764
+ row_label, value = [part.strip() for part in body.split(":", 1)]
765
+ if group_key not in grouped:
766
+ grouped[group_key] = {"label": row_label, "options": []}
767
+ order.append(group_key)
768
+ grouped[group_key]["options"].append(value)
769
+ rows = [grouped[key] for key in order]
770
+ return rows if len(rows) >= 2 and all(row["options"] for row in rows) else []
771
+
772
+
773
+ def render_grouped_option_selects(question, key_prefix, rows):
774
+ selections = []
775
+ all_yes_no = all(
776
+ all(str(option).strip().lower() in {"yes", "no", "예", "μ•„λ‹ˆμ˜€", "μ•„λ‹ˆμš”"} for option in row["options"])
777
+ for row in rows
778
+ )
779
+ st.markdown("#### ν•­λͺ©λ³„ λ‹΅μ•ˆ")
780
+ for index, row in enumerate(rows, 1):
781
+ options = [str(option).strip() for option in row["options"] if str(option).strip()]
782
+ selected = st.selectbox(
783
+ row["label"] or f"ν•­λͺ© {index}",
784
+ ["선택 μ•ˆ 함"] + options,
785
+ key=f"{key_prefix}_grouped_options_{question['id']}_{index}",
786
+ )
787
+ if selected != "선택 μ•ˆ 함":
788
+ if all_yes_no:
789
+ selections.append("Y" if selected.lower() in {"yes", "예"} else "N")
790
+ else:
791
+ selections.append(selected)
792
+ return ",".join(selections) if len(selections) == len(rows) else None
793
+
794
+
795
  def visual_statements(question) -> list[dict]:
796
  try:
797
  analysis = json.loads(question.get("visual_analysis_json") or "{}")
 
847
  if statement_rows:
848
  return render_yes_no_matrix(question, f"{key_prefix}_statements", statement_rows)
849
 
850
+ grouped_rows = grouped_option_rows(options)
851
+ if grouped_rows:
852
+ return render_grouped_option_selects(question, key_prefix, grouped_rows)
853
+
854
  if is_yes_no_hotspot(question):
855
  answer_count = len(yes_no_answer_labels(question.get("answer") or "")) or len(
856
  yes_no_answer_labels(question.get("explanation") or "")
857
  )
858
  row_count = max(3, answer_count, len(statement_rows))
859
+ rows = statement_option_rows(options, row_count) or yes_no_lines(question.get("question") or "")
860
  if statement_rows:
861
  rows = statement_rows
862
  if len(rows) < row_count: