chgksuite 0.25.0b4__py3-none-any.whl → 0.26.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,15 +3,16 @@ import os
3
3
  import re
4
4
 
5
5
  import toml
6
+
7
+ from chgksuite.common import log_wrap, replace_escaped, tryint
8
+ from chgksuite.composer.composer_common import BaseExporter, backtick_replace, parseimg
6
9
  from pptx import Presentation
7
10
  from pptx.dml.color import RGBColor
8
11
  from pptx.enum.text import MSO_AUTO_SIZE, MSO_VERTICAL_ANCHOR, PP_ALIGN
12
+ from pptx.enum.lang import MSO_LANGUAGE_ID
9
13
  from pptx.util import Inches as PptxInches
10
14
  from pptx.util import Pt as PptxPt
11
15
 
12
- from chgksuite.common import log_wrap, replace_escaped, tryint
13
- from chgksuite.composer.composer_common import BaseExporter, backtick_replace, parseimg
14
-
15
16
 
16
17
  class PptxExporter(BaseExporter):
17
18
  def __init__(self, *args, **kwargs):
@@ -48,6 +49,17 @@ class PptxExporter(BaseExporter):
48
49
  textbox = slide.shapes.add_textbox(left, top, width, height)
49
50
  return textbox
50
51
 
52
+ def add_run(self, para, text, color=None):
53
+ r = para.add_run()
54
+ r.text = text
55
+ if color is None:
56
+ color = self.c["textbox"].get("color")
57
+ if color:
58
+ r.font.color.rgb = RGBColor(*color)
59
+ if self.args.language == "ru":
60
+ r.font.language_id = MSO_LANGUAGE_ID.RUSSIAN
61
+ return r
62
+
51
63
  def pptx_format(self, el, para, tf, slide, replace_spaces=True):
52
64
  def r_sp(text):
53
65
  if replace_spaces:
@@ -60,15 +72,13 @@ class PptxExporter(BaseExporter):
60
72
  licount = 0
61
73
  for li in el[1]:
62
74
  licount += 1
63
- r = para.add_run()
64
- r.text = "\n{}. ".format(licount)
75
+ self.add_run(para, "\n{}. ".format(licount))
65
76
  self.pptx_format(li, para, tf, slide)
66
77
  else:
67
78
  licount = 0
68
79
  for li in el:
69
80
  licount += 1
70
- r = para.add_run()
71
- r.text = "\n{}. ".format(licount)
81
+ self.add_run(para, "\n{}. ".format(licount))
72
82
  self.pptx_format(li, para, tf, slide)
73
83
 
74
84
  if isinstance(el, str):
@@ -77,23 +87,20 @@ class PptxExporter(BaseExporter):
77
87
 
78
88
  for run in self.parse_4s_elem(el):
79
89
  if run[0] == "screen":
80
- r = para.add_run()
81
- r.text = r_sp(run[1]["for_screen"])
90
+ self.add_run(para, r_sp(run[1]["for_screen"]))
82
91
 
83
92
  elif run[0] == "linebreak":
84
- r = para.add_run("\n")
93
+ self.add_run(para, "\n")
85
94
 
86
95
  elif run[0] == "strike":
87
- r = para.add_run()
88
- r.text = r_sp(run[1])
96
+ r = self.add_run(para, r_sp(run[1]))
89
97
  r.font.strike = True # TODO: doesn't work as of 2023-12-24, cf. https://github.com/scanny/python-pptx/issues/339
90
98
 
91
99
  elif run[0] == "img":
92
100
  pass # image processing is moved to other places
93
101
 
94
102
  else:
95
- r = para.add_run()
96
- r.text = r_sp(run[1])
103
+ r = self.add_run(para, r_sp(run[1]))
97
104
  if "italic" in run[0]:
98
105
  r.font.italic = True
99
106
  if "bold" in run[0]:
@@ -166,25 +173,28 @@ class PptxExporter(BaseExporter):
166
173
  txt = txt.upper()
167
174
  self.set_question_number(slide, number=txt)
168
175
  else:
169
- r = p.add_run()
170
- r.text = self._replace_no_break(self.pptx_process_text(section[0][1]))
176
+ r = self.add_run(
177
+ p, self._replace_no_break(self.pptx_process_text(section[0][1]))
178
+ )
171
179
  r.font.size = PptxPt(self.c["text_size_grid"]["section"])
172
180
  add_line_break = True
173
181
  if editor:
174
- r = p.add_run()
175
- r.text = self._replace_no_break(
176
- ("\n" if add_line_break else "")
177
- + self.pptx_process_text(editor[0][1])
178
- + "\n"
182
+ r = self.add_run(
183
+ p,
184
+ self._replace_no_break(
185
+ ("\n\n" if add_line_break else "")
186
+ + self.pptx_process_text(editor[0][1])
187
+ ),
179
188
  )
180
189
  add_line_break = True
181
190
  if meta:
182
191
  for element in meta:
183
- r = p.add_run()
184
- r.text = self._replace_no_break(
185
- ("\n" if add_line_break else "")
186
- + self.pptx_process_text(element[1])
187
- + "\n"
192
+ r = self.add_run(
193
+ p,
194
+ self._replace_no_break(
195
+ ("\n\n" if add_line_break else "")
196
+ + self.pptx_process_text(element[1])
197
+ ),
188
198
  )
189
199
  add_line_break = True
190
200
 
@@ -211,8 +221,11 @@ class PptxExporter(BaseExporter):
211
221
  title = slide.shapes.title
212
222
  title.text = title_text[0][1]
213
223
  if date_text:
214
- subtitle = slide.placeholders[1]
215
- subtitle.text = date_text[0][1]
224
+ try:
225
+ subtitle = slide.placeholders[1]
226
+ subtitle.text = date_text[0][1]
227
+ except KeyError:
228
+ pass
216
229
  for block in (editor_block, section_block):
217
230
  self._process_block(block)
218
231
 
@@ -223,11 +236,12 @@ class PptxExporter(BaseExporter):
223
236
  qtf = qntextbox.text_frame
224
237
  qtf_p = self.init_paragraph(qtf)
225
238
  if self.c["number_textbox"].get("align"):
226
- qtf_p.alignment = getattr(PP_ALIGN, self.c["number_textbox"]["align"].upper())
227
- qtf_r = qtf_p.add_run()
239
+ qtf_p.alignment = getattr(
240
+ PP_ALIGN, self.c["number_textbox"]["align"].upper()
241
+ )
228
242
  if self.c.get("question_number_format") == "caps" and tryint(number):
229
243
  number = f"ВОПРОС {number}"
230
- qtf_r.text = number
244
+ qtf_r = self.add_run(qtf_p, number)
231
245
  if self.c["number_textbox"].get("bold"):
232
246
  qtf_r.font.bold = True
233
247
  if self.c["number_textbox"].get("color"):
@@ -278,6 +292,15 @@ class PptxExporter(BaseExporter):
278
292
  base_top = PptxInches(self.c["textbox"]["top"])
279
293
  base_width = PptxInches(self.c["textbox"]["width"])
280
294
  base_height = PptxInches(self.c["textbox"]["height"])
295
+ if self.c.get("disable_autolayout"):
296
+ slide.shapes.add_picture(
297
+ image["imgfile"],
298
+ left=base_left,
299
+ top=base_top,
300
+ width=img_base_width,
301
+ height=img_base_height,
302
+ )
303
+ return self.get_textbox(slide), 1
281
304
  big_mode = (
282
305
  image["big"] and not self.c.get("text_is_duplicated") and allowbigimage
283
306
  )
@@ -390,9 +413,13 @@ class PptxExporter(BaseExporter):
390
413
  def process_question_text(self, q):
391
414
  image = self._get_image_from_4s(q["question"])
392
415
  handout = self._get_handout_from_4s(q["question"])
393
- if image:
416
+ add_handout_on_separate_slide = self.c.get("add_handout_on_separate_slide")
417
+ add_handout_on_separate_slide = (
418
+ add_handout_on_separate_slide is None or add_handout_on_separate_slide
419
+ )
420
+ if image and add_handout_on_separate_slide:
394
421
  self.add_slide_with_image(image, number=self.number)
395
- elif handout:
422
+ elif handout and add_handout_on_separate_slide:
396
423
  self.add_slide_with_handout(handout, number=self.number)
397
424
  slide = self.prs.slides.add_slide(self.BLANK_SLIDE)
398
425
  text_is_duplicated = bool(self.c.get("text_is_duplicated"))
@@ -411,9 +438,11 @@ class PptxExporter(BaseExporter):
411
438
  fields = ["answer"]
412
439
  if q.get("zachet") and self.c.get("add_zachet"):
413
440
  fields.append("zachet")
441
+ if q.get("nezachet") and self.c.get("add_zachet"):
442
+ fields.append("nezachet")
414
443
  if self.c["add_comment"] and "comment" in q:
415
444
  fields.append("comment")
416
- if self.c["add_source"] and "source" in q:
445
+ if self.c.get("add_source") and "source" in q:
417
446
  fields.append("source")
418
447
  textbox = None
419
448
  coeff = 1
@@ -435,6 +464,10 @@ class PptxExporter(BaseExporter):
435
464
  text_for_size += "\n" + self.recursive_join(
436
465
  self.pptx_process_text(q["zachet"], strip_brackets=False)
437
466
  )
467
+ if q.get("nezachet") and self.c.get("add_zachet"):
468
+ text_for_size += "\n" + self.recursive_join(
469
+ self.pptx_process_text(q["nezachet"], strip_brackets=False)
470
+ )
438
471
  if q.get("comment") and self.c.get("add_comment"):
439
472
  text_for_size += "\n" + self.recursive_join(
440
473
  self.pptx_process_text(q["comment"])
@@ -451,34 +484,34 @@ class PptxExporter(BaseExporter):
451
484
  p = self.init_paragraph(tf, size=self.c["force_text_size_answer"])
452
485
  else:
453
486
  p = self.init_paragraph(tf, text=text_for_size, coeff=coeff)
454
- r = p.add_run()
455
- r.text = f"{self.get_label(q, 'answer')}: "
487
+ r = self.add_run(p, f"{self.get_label(q, 'answer')}: ")
456
488
  r.font.bold = True
457
489
  self.pptx_format(
458
490
  self.pptx_process_text(q["answer"], strip_brackets=False), p, tf, slide
459
491
  )
460
492
  if q.get("zachet") and self.c.get("add_zachet"):
461
493
  zachet_text = self.pptx_process_text(q["zachet"], strip_brackets=False)
462
- r = p.add_run()
463
- r.text = f"\n{self.get_label(q, 'zachet')}: "
494
+ r = self.add_run(p, f"\n{self.get_label(q, 'zachet')}: ")
464
495
  r.font.bold = True
465
496
  self.pptx_format(zachet_text, p, tf, slide)
497
+ if q.get("nezachet") and self.c.get("add_zachet"):
498
+ nezachet_text = self.pptx_process_text(q["nezachet"], strip_brackets=False)
499
+ r = self.add_run(p, f"\n{self.get_label(q, 'nezachet')}: ")
500
+ r.font.bold = True
501
+ self.pptx_format(nezachet_text, p, tf, slide)
466
502
  if self.c["add_comment"] and "comment" in q:
467
503
  comment_text = self.pptx_process_text(q["comment"])
468
- r = p.add_run()
469
- r.text = f"\n{self.get_label(q, 'comment')}: "
504
+ r = self.add_run(p, f"\n{self.get_label(q, 'comment')}: ")
470
505
  r.font.bold = True
471
506
  self.pptx_format(comment_text, p, tf, slide)
472
- if self.c["add_source"] and "source" in q:
507
+ if self.c.get("add_source") and "source" in q:
473
508
  source_text = self.pptx_process_text(q["source"])
474
- r = p.add_run()
475
- r.text = f"\n{self.get_label(q, 'source')}: "
509
+ r = self.add_run(p, f"\n{self.get_label(q, 'source')}: ")
476
510
  r.font.bold = True
477
511
  self.pptx_format(source_text, p, tf, slide)
478
- if self.c["add_author"] and "author" in q:
512
+ if self.c.get("add_author") and "author" in q:
479
513
  author_text = self.pptx_process_text(q["author"])
480
- r = p.add_run()
481
- r.text = f"\n{self.get_label(q, 'author')}: "
514
+ r = self.add_run(p, f"\n{self.get_label(q, 'author')}: ")
482
515
  r.font.bold = True
483
516
  self.pptx_format(author_text, p, tf, slide)
484
517
 
@@ -510,7 +543,7 @@ class PptxExporter(BaseExporter):
510
543
  return element["size"]
511
544
  return self.c["text_size_grid"]["smallest"]
512
545
 
513
- def init_paragraph(self, text_frame, text=None, coeff=1, size=None):
546
+ def init_paragraph(self, text_frame, text=None, coeff=1, size=None, color=None):
514
547
  p = text_frame.paragraphs[0]
515
548
  p.font.name = self.c["font"]["name"]
516
549
  if size:
@@ -16,6 +16,11 @@ from chgksuite.composer.composer_common import BaseExporter, parseimg
16
16
  from chgksuite.composer.telegram_bot import run_bot_in_thread
17
17
 
18
18
 
19
+ def get_text(msg_data):
20
+ if "message" in msg_data and "text" in msg_data["message"]:
21
+ return msg_data["message"]["text"]
22
+
23
+
19
24
  class TelegramExporter(BaseExporter):
20
25
  def __init__(self, *args, **kwargs):
21
26
  super().__init__(*args, **kwargs)
@@ -37,6 +42,7 @@ class TelegramExporter(BaseExporter):
37
42
  self.channel_id = None # Target channel ID
38
43
  self.chat_id = None # Discussion group ID linked to the channel
39
44
  self.auth_uuid = uuid.uuid4().hex[:8]
45
+ self.chat_auth_uuid = uuid.uuid4().hex[:8]
40
46
  self.init_telegram()
41
47
 
42
48
  def check_connectivity(self):
@@ -119,6 +125,9 @@ class TelegramExporter(BaseExporter):
119
125
 
120
126
  if result:
121
127
  msg_data = json.loads(result["raw_data"])
128
+ if msg_data["message"]["chat"]["type"] != "private":
129
+ print("You should post to the PRIVATE chat, not to the channel/group")
130
+ continue
122
131
  self.control_chat_id = msg_data["message"]["chat"]["id"]
123
132
  self.send_api_request(
124
133
  "sendMessage",
@@ -852,7 +861,7 @@ class TelegramExporter(BaseExporter):
852
861
 
853
862
  # Wait for a forwarded message with channel information
854
863
  channel_id = self.wait_for_forwarded_message(
855
- entity_type="channel", check_type=True, string_id=channel_result
864
+ entity_type="channel", check_type=True
856
865
  )
857
866
  if channel_id:
858
867
  self.save_username(channel_result, channel_id)
@@ -860,6 +869,7 @@ class TelegramExporter(BaseExporter):
860
869
  raise Exception("Failed to get channel ID from forwarded message")
861
870
  else:
862
871
  raise Exception("Channel ID is undefined")
872
+
863
873
  # Handle chat resolution
864
874
  if isinstance(chat_result, int):
865
875
  chat_id = chat_result
@@ -868,14 +878,15 @@ class TelegramExporter(BaseExporter):
868
878
  if not chat_id:
869
879
  print("\n" + "=" * 50)
870
880
  print(
871
- "Please forward any message from the discussion group to the bot."
881
+ f"Please write a message in the discussion group with text: {self.chat_auth_uuid}"
872
882
  )
873
883
  print("This will allow me to extract the group ID automatically.")
884
+ print("The bot MUST be added do the group and made admin, else it won't work!")
874
885
  print("=" * 50 + "\n")
875
886
 
876
887
  # Wait for a forwarded message with chat information
877
888
  chat_id = self.wait_for_forwarded_message(
878
- entity_type="chat", check_type=False, string_id=chat_result
889
+ entity_type="chat", check_type=False
879
890
  )
880
891
  if not chat_id:
881
892
  self.logger.error("Failed to get chat ID from forwarded message")
@@ -883,15 +894,13 @@ class TelegramExporter(BaseExporter):
883
894
  while chat_id == channel_id:
884
895
  error_msg = (
885
896
  "Chat ID and channel ID are the same. The problem may be that "
886
- "you forwarded a message from discussion group that itself was automatically forwarded "
887
- "from the channel by Telegram. Please forward a message that was sent directly in the discussion group."
897
+ "you posted a message in the channel, not in the discussion group."
888
898
  )
889
899
  self.logger.error(error_msg)
890
900
  chat_id = self.wait_for_forwarded_message(
891
901
  entity_type="chat",
892
902
  check_type=False,
893
903
  add_msg=error_msg,
894
- string_id=chat_result,
895
904
  )
896
905
  if chat_id:
897
906
  self.save_username(chat_result, chat_id)
@@ -904,7 +913,10 @@ class TelegramExporter(BaseExporter):
904
913
  raise Exception("Chat ID is undefined")
905
914
 
906
915
  self.channel_id = f"-100{channel_id}"
907
- self.chat_id = f"-100{chat_id}"
916
+ if not str(chat_id).startswith("-100"):
917
+ self.chat_id = f"-100{chat_id}"
918
+ else:
919
+ self.chat_id = chat_id
908
920
 
909
921
  self.logger.info(
910
922
  f"Using channel ID {self.channel_id} and discussion group ID {self.chat_id}"
@@ -1088,7 +1100,10 @@ class TelegramExporter(BaseExporter):
1088
1100
  failure_message = "❌ Failed to extract channel ID."
1089
1101
  else:
1090
1102
  entity_name = "discussion group"
1091
- instruction_message = "🔄 Please forward any message from the discussion group\n\n⚠️ IMPORTANT: Do NOT forward messages that were automatically posted from the channel. Forward messages that were sent directly in the discussion group."
1103
+ instruction_message = (
1104
+ f"🔄 Please post to the discussion group a message with text: {self.chat_auth_uuid}\n\n"
1105
+ "⚠️ IMPORTANT: Bot should be added to the discussion group and have ADMIN rights!"
1106
+ )
1092
1107
  success_message = "✅ Successfully extracted discussion group ID: {}"
1093
1108
  failure_message = "❌ Failed to extract discussion group ID."
1094
1109
 
@@ -1105,6 +1120,7 @@ class TelegramExporter(BaseExporter):
1105
1120
  resolved = False
1106
1121
  retry_count = 0
1107
1122
  max_retries = 30 # 5 minutes (10 seconds per retry)
1123
+ extracted_id = None
1108
1124
 
1109
1125
  # Extract channel ID for comparison if we're looking for a discussion group
1110
1126
  channel_numeric_id = None
@@ -1117,11 +1133,15 @@ class TelegramExporter(BaseExporter):
1117
1133
 
1118
1134
  # Look for a forwarded message in recent messages
1119
1135
  cursor = self.db_conn.cursor()
1136
+ if self.created_at:
1137
+ threshold = "'" + self.created_at + "'"
1138
+ else:
1139
+ threshold = "datetime('now', '-2 minutes')"
1120
1140
  cursor.execute(
1121
- """
1141
+ f"""
1122
1142
  SELECT raw_data, created_at
1123
1143
  FROM messages
1124
- WHERE created_at > datetime('now', '-2 minutes')
1144
+ WHERE created_at > {threshold}
1125
1145
  ORDER BY created_at DESC
1126
1146
  """
1127
1147
  )
@@ -1129,58 +1149,63 @@ class TelegramExporter(BaseExporter):
1129
1149
  messages = cursor.fetchall()
1130
1150
 
1131
1151
  for row in messages:
1152
+ if self.args.debug:
1153
+ self.logger.info(row["raw_data"])
1132
1154
  if self.created_at and row["created_at"] < self.created_at:
1133
1155
  break
1134
1156
  msg_data = json.loads(row["raw_data"])
1135
- if msg_data["message"]["chat"]["id"] != self.control_chat_id:
1136
- continue
1137
- if "message" in msg_data and "forward_from_chat" in msg_data["message"]:
1138
- forward_info = msg_data["message"]["forward_from_chat"]
1139
-
1140
- # Extract chat ID from the message
1141
- chat_id = forward_info.get("id")
1142
- # Remove -100 prefix if present
1143
- if str(chat_id).startswith("-100"):
1144
- extracted_id = int(str(chat_id)[4:])
1145
- else:
1146
- extracted_id = chat_id
1147
-
1148
- # If we're looking for a discussion group, verify it's not the same as the channel ID
1149
- if entity_type == "chat" and channel_numeric_id:
1150
- if extracted_id == channel_numeric_id:
1151
- self.logger.warning(
1152
- "User forwarded a message from the channel, not the discussion group"
1153
- )
1154
- self.send_api_request(
1155
- "sendMessage",
1156
- {
1157
- "chat_id": self.control_chat_id,
1158
- "text": "⚠️ You forwarded a message from the channel, not from the discussion group.\n\nPlease forward a message that was originally sent IN the discussion group, not an automatic repost from the channel.",
1159
- },
1160
- )
1161
- # Skip this message and continue waiting
1162
- continue
1163
-
1164
- # For channels, check the type; for chats, accept any type except "channel" if check_type is False
1165
- if (check_type and forward_info.get("type") == "channel") or (
1166
- not check_type
1167
- ):
1168
- resolved = True
1169
- self.created_at = row["created_at"]
1170
- self.logger.info(
1171
- f"Extracted {entity_name} ID: {extracted_id} from forwarded message"
1157
+ if entity_type == "chat":
1158
+ if get_text(msg_data) != self.chat_auth_uuid:
1159
+ continue
1160
+ extracted_id = msg_data["message"]["chat"]["id"]
1161
+ if extracted_id == channel_numeric_id or extracted_id == self.control_chat_id:
1162
+ self.logger.warning(
1163
+ "User posted a message in the channel, not the discussion group"
1172
1164
  )
1173
-
1174
- # Send confirmation message
1175
1165
  self.send_api_request(
1176
1166
  "sendMessage",
1177
1167
  {
1178
1168
  "chat_id": self.control_chat_id,
1179
- "text": success_message.format(extracted_id),
1169
+ "text": (
1170
+ "⚠️ You posted a message in the channel, not in the discussion group."
1171
+ )
1180
1172
  },
1181
1173
  )
1174
+ # Skip this message and continue waiting
1175
+ continue
1176
+ elif entity_type == "channel":
1177
+ if msg_data["message"]["chat"]["id"] != self.control_chat_id:
1178
+ continue
1179
+ if "message" in msg_data and "forward_from_chat" in msg_data["message"]:
1180
+ forward_info = msg_data["message"]["forward_from_chat"]
1181
+
1182
+ # Extract chat ID from the message
1183
+ chat_id = forward_info.get("id")
1184
+ # Remove -100 prefix if present
1185
+ if str(chat_id).startswith("-100"):
1186
+ extracted_id = int(str(chat_id)[4:])
1187
+ else:
1188
+ extracted_id = chat_id
1189
+ # For channels, check the type; for chats, accept any type except "channel" if check_type is False
1190
+ if extracted_id and ((check_type and forward_info.get("type") == "channel") or (
1191
+ not check_type
1192
+ )):
1193
+ resolved = True
1194
+ self.created_at = row["created_at"]
1195
+ self.logger.info(
1196
+ f"Extracted {entity_name} ID: {extracted_id} from forwarded message"
1197
+ )
1198
+
1199
+ # Send confirmation message
1200
+ self.send_api_request(
1201
+ "sendMessage",
1202
+ {
1203
+ "chat_id": self.control_chat_id,
1204
+ "text": success_message.format(extracted_id),
1205
+ },
1206
+ )
1182
1207
 
1183
- return extracted_id
1208
+ return extracted_id
1184
1209
 
1185
1210
  retry_count += 1
1186
1211
 
File without changes
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ import itertools
4
+ import os
5
+ import re
6
+ from collections import defaultdict
7
+
8
+ import toml
9
+
10
+ from chgksuite.common import get_source_dirs
11
+ from chgksuite.composer.chgksuite_parser import parse_4s
12
+ from chgksuite.composer.composer_common import _parse_4s_elem, parseimg
13
+ from chgksuite.handouter.utils import read_file, write_file
14
+
15
+
16
+ def postprocess(s):
17
+ return s.replace("\\_", "_")
18
+
19
+
20
+ def generate_handouts_list(handouts, output_dir, base_name, parsed):
21
+ """Generate a human-readable file with question numbers that have handouts."""
22
+ question_numbers = sorted([int(h["for_question"]) for h in handouts])
23
+
24
+ content = "ВОПРОСЫ С РАЗДАТОЧНЫМ МАТЕРИАЛОМ:\n\n"
25
+ content += f"Сквозная нумерация:\n{', '.join(map(str, question_numbers))}\n\n"
26
+
27
+ content += "По турам:\n"
28
+ tour = 0
29
+ by_tour = {}
30
+ for tup in parsed:
31
+ if tup[0] == "section":
32
+ tour += 1
33
+ by_tour[tour] = []
34
+ if tup[0] == "Question":
35
+ if tour == 0:
36
+ tour = 1
37
+ by_tour[tour] = []
38
+ if tup[1]["number"] in question_numbers:
39
+ by_tour[tour].append(tup[1]["number"])
40
+
41
+ for tour in sorted(by_tour):
42
+ tour_handouts = by_tour[tour]
43
+ if tour_handouts:
44
+ content += f"Тур {tour}: {', '.join(map(str, tour_handouts))}\n"
45
+ else:
46
+ content += f"Тур {tour}: нет раздаток\n"
47
+
48
+ output_fn = os.path.join(output_dir, base_name + "_handouts_list.txt")
49
+ write_file(output_fn, content)
50
+ print(f"File with list of handouts: {output_fn}")
51
+ print(content)
52
+
53
+
54
+ def generate_handouts(args):
55
+ _, resourcedir = get_source_dirs()
56
+ labels = toml.loads(
57
+ read_file(os.path.join(resourcedir, f"labels_{args.lang}.toml"))
58
+ )
59
+ handout_re = re.compile(
60
+ "\\["
61
+ + labels["question_labels"]["handout_short"]
62
+ + ".+?:( |\n)(?P<handout_text>.+?)\\]",
63
+ flags=re.DOTALL,
64
+ )
65
+
66
+ cnt = read_file(args.filename)
67
+ parsed = parse_4s(cnt)
68
+
69
+ questions = [q[1] for q in parsed if q[0] == "Question"]
70
+ handouts = []
71
+ for q in questions:
72
+ if isinstance(q["question"], list):
73
+ question_text = "\n".join(itertools.chain.from_iterable(q["question"]))
74
+ else:
75
+ question_text = q["question"]
76
+ question_text_lower = question_text.lower()
77
+ srch = handout_re.search(question_text)
78
+ if srch:
79
+ text = postprocess(srch.group("handout_text"))
80
+ elems = _parse_4s_elem(text)
81
+ img = [el for el in elems if el[0] == "img"]
82
+ if img:
83
+ try:
84
+ parsed_img = parseimg(img[0][1])
85
+ except:
86
+ print(
87
+ f"Image file for question {q['number']} not found, add it by hand"
88
+ )
89
+ continue
90
+ else:
91
+ parsed_img = None
92
+ res = {"for_question": q["number"]}
93
+ if parsed_img:
94
+ res["image"] = parsed_img["imgfile"]
95
+ else:
96
+ res["text"] = text
97
+ handouts.append(res)
98
+ elif (
99
+ "раздат" in question_text_lower
100
+ or "роздан" in question_text_lower
101
+ or "(img" in question_text_lower
102
+ ):
103
+ print(f"probably badly formatted handout for question {q['number']}")
104
+ res = {"for_question": q["number"], "text": postprocess(question_text)}
105
+ handouts.append(res)
106
+ result = []
107
+ result_by_question = defaultdict(list)
108
+ for handout in handouts:
109
+ if "image" in handout:
110
+ key = "image"
111
+ prefix = "image: "
112
+ else:
113
+ key = "text"
114
+ prefix = ""
115
+ value = handout[key]
116
+ formatted = (
117
+ f"for_question: {handout['for_question']}\n" if not args.separate else ""
118
+ ) + f"columns: 3\n\n{prefix}{value}"
119
+ result.append(formatted)
120
+ result_by_question[handout["for_question"]].append(formatted)
121
+ output_dir = os.path.dirname(os.path.abspath(args.filename))
122
+ bn, _ = os.path.splitext(os.path.basename(args.filename))
123
+
124
+ if args.separate:
125
+ for k, v in result_by_question.items():
126
+ if len(v) > 1:
127
+ for i, cnt in enumerate(v):
128
+ output_fn = os.path.join(
129
+ output_dir, f"{bn}_q{k.zfill(2)}_{i + 1}.txt"
130
+ )
131
+ print(output_fn)
132
+ write_file(output_fn, cnt)
133
+ else:
134
+ output_fn = os.path.join(output_dir, f"{bn}_q{str(k).zfill(2)}.txt")
135
+ print(output_fn)
136
+ write_file(output_fn, v[0])
137
+ else:
138
+ output_fn = os.path.join(output_dir, bn + "_handouts.txt")
139
+ print(f"output filename: {output_fn}")
140
+ write_file(output_fn, "\n---\n".join(result))
141
+
142
+ if args.list_handouts:
143
+ generate_handouts_list(handouts, output_dir, bn, parsed)