Spaces:
Sleeping
Sleeping
| #!/usr/bin/env python3 | |
| """Generate LaTeX content files for an ITU-T Question report. | |
| Usage: | |
| python generate_question_report.py <config.json> | |
| Reads a JSON configuration file, fetches data from the ITU website, | |
| and generates LaTeX snippet files in ../question_doc_template/chapters/results/. | |
| These snippets are included by the .tex templates in ../question_doc_template/chapters/. | |
| """ | |
| import sys | |
| import os | |
| # Add script directory to path for imports | |
| sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) | |
| from common.config import load_question_config | |
| from common.itu_api import get_documents, get_question, get_study_group, get_work_programme, get_work_item_editors | |
| from common.utils import ( | |
| comma_separated_list, find_td_by_name, find_td_by_number, | |
| find_question_name_td_and_a5, stripped_starts_with, | |
| is_new_work_item, get_rapporteurs, get_associate_rapporteurs, | |
| get_meeting_reports, extract_alt_name, extract_new_work_item_info, | |
| detect_outgoing_liaisons, detect_processed_work_items, print_work_programme_summary, | |
| parse_timing, | |
| ) | |
| from common.latex import ( | |
| URL, escape_latex, make_href, td_href, write_result, table_row_str, seqsplit_text, | |
| ) | |
| from common.models import split_title | |
| SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) | |
| PROJECT_DIR = os.path.dirname(SCRIPT_DIR) | |
| RESULTS_DIR = os.path.join(PROJECT_DIR, 'question_doc_template', 'chapters', 'variables') | |
| def _format_date_range(start_dt, end_dt): | |
| """Format date range as 'D - D Month YYYY' or 'D Month - D Month YYYY'.""" | |
| if end_dt is None: | |
| return f"{start_dt.day} {start_dt.strftime('%B')} {start_dt.year}" | |
| if start_dt.month == end_dt.month and start_dt.year == end_dt.year: | |
| return (f"{start_dt.day} - {end_dt.day} " | |
| f"{end_dt.strftime('%B')} {end_dt.year}") | |
| return (f"{start_dt.day} {start_dt.strftime('%B')} - " | |
| f"{end_dt.day} {end_dt.strftime('%B')} {end_dt.year}") | |
| def main(): | |
| if len(sys.argv) < 2: | |
| print(f"Usage: {sys.argv[0]} <config.json>") | |
| sys.exit(1) | |
| config = load_question_config(sys.argv[1]) | |
| group = config['group'] | |
| question = config['question'] | |
| place = config['place'] | |
| start = config['start'] | |
| end = config['end'] | |
| start_string = config['startString'] | |
| start_date = config['startDate'] | |
| next_meeting = config.get('next_meeting') | |
| # --------------------------------------------------------------- | |
| # Fetch data from ITU website | |
| # --------------------------------------------------------------- | |
| print("Fetching study group structure...") | |
| sg_details = get_study_group(group=group, start=start_string) | |
| # Find which WP this question belongs to | |
| wp_number = '' | |
| for wp in sg_details.workingParties: | |
| for q in wp.questions: | |
| if q.number == question: | |
| wp_number = wp.number | |
| break | |
| if not wp_number: | |
| print(f"Question {question} not found in study group {group}") | |
| sys.exit(1) | |
| print("Fetching question details...") | |
| question_details = get_question(group=group, question=question, start=start_date) | |
| rapporteurs = get_rapporteurs(question_details) | |
| associate_rapporteurs = get_associate_rapporteurs(question_details) | |
| print("Fetching documents...") | |
| c_rows = get_documents(document_type="C", group=group, working_party=wp_number, | |
| questions=question, start=start_date) | |
| plen_rows = get_documents(document_type="PLEN", group=group, working_party=wp_number, | |
| questions=question, start=start_date) | |
| all_plen_rows = get_documents(document_type="PLEN", group=group, working_party=wp_number, | |
| questions="QALL", start=start_date) | |
| gen_rows = get_documents(document_type="GEN", group=group, working_party=wp_number, | |
| questions=question, start=start_date) | |
| all_gen_rows = get_documents(document_type="GEN", group=group, working_party=wp_number, | |
| questions="QALL", start=start_date) | |
| wp_rows = get_documents(document_type="WP", group=group, working_party=wp_number, | |
| questions=question, start=start_date) | |
| all_wp_rows = get_documents(document_type="WP", group=group, working_party=wp_number, | |
| questions="QALL", start=start_date) | |
| # Display fetched TDs | |
| _print_td_summary("Contributions (C)", c_rows) | |
| _print_td_summary("TD/PLEN (Q-specific)", plen_rows) | |
| _print_td_summary("TD/PLEN (QALL)", all_plen_rows) | |
| _print_td_summary("TD/GEN (Q-specific)", gen_rows) | |
| _print_td_summary("TD/GEN (QALL)", all_gen_rows) | |
| _print_td_summary(f"TD/WP{wp_number} (Q-specific)", wp_rows) | |
| _print_td_summary(f"TD/WP{wp_number} (QALL)", all_wp_rows) | |
| # --------------------------------------------------------------- | |
| # Process work programme (scraped from ITU website) | |
| # --------------------------------------------------------------- | |
| print("Fetching work programme...") | |
| work_item_details = get_work_programme( | |
| group=group, question=question, | |
| working_party=wp_number, start=start_string) | |
| work_items = [wi.workItem for wi in work_item_details] | |
| # Display work programme summary | |
| print_work_programme_summary(work_item_details) | |
| print("Fetching work item editors...") | |
| editors = get_work_item_editors(work_item_details) | |
| approval = [] | |
| determination = [] | |
| consent = [] | |
| non_normative = [] | |
| new_work_items = [] | |
| outgoing_ls = [] | |
| # Auto-detect approval/consent/determination/agreement from TD titles only | |
| # (e.g., "Approval - X.1234: Title", "Determination - X.5678: Title") | |
| # Also build td_to_work_item mapping by matching work item name to work programme | |
| td_to_work_item = {} | |
| for row in wp_rows: | |
| val = row.number.value.strip() | |
| if row.documentType == 'Approval': | |
| if val not in approval: | |
| approval.append(val) | |
| elif row.documentType == 'Determination': | |
| if val not in determination: | |
| determination.append(val) | |
| elif row.documentType == 'Consent': | |
| if val not in consent: | |
| consent.append(val) | |
| elif row.documentType == 'Agreement': | |
| if val not in non_normative: | |
| non_normative.append(val) | |
| # Match work item name from TD to work programme data | |
| work_item_name = row.recommendation or row.acronym | |
| if work_item_name: | |
| for wi in work_item_details: | |
| # Match by work item name (e.g., "X.1234" or "X.abc") | |
| wi_name = wi.workItem or "" | |
| # Normalize for comparison (remove spaces, handle "ex" aliases) | |
| if wi_name and (work_item_name in wi_name or wi_name in work_item_name or | |
| work_item_name.split()[0] == wi_name.split()[0] if ' ' in work_item_name else False): | |
| td_to_work_item[val] = wi | |
| print(f" Matched TD {val} ({work_item_name}) -> WP: {wi_name}, version={wi.version}") | |
| break | |
| # Detect processed work items (under study items with TDs) | |
| processed_work_items = detect_processed_work_items(work_item_details, wp_rows) | |
| # Auto-detect candidate work items for next SG meeting | |
| # Criteria: "Under study" items with timing between end of current meeting and next_meeting | |
| from datetime import timedelta | |
| candidate_next = [] | |
| if next_meeting: | |
| next_meeting_date = next_meeting | |
| else: | |
| # Default to 6 months after start if next_meeting not specified | |
| next_meeting_date = start + timedelta(days=180) | |
| for wi in work_item_details: | |
| status = (wi.status or '').strip() | |
| if not status.startswith('Under study'): | |
| continue | |
| timing = (wi.timing or '').strip() | |
| if not timing: | |
| continue | |
| timing_date = parse_timing(timing) | |
| # Candidate if timing is between end of current meeting and next meeting | |
| if timing_date and end and timing_date > end and timing_date <= next_meeting_date: | |
| name = wi.workItem or "" | |
| if name and name not in candidate_next: | |
| candidate_next.append(name) | |
| if candidate_next: | |
| print(f" Found {len(candidate_next)} candidate work items for next meeting") | |
| # Auto-detect outgoing liaisons from GEN TDs | |
| outgoing_ls = detect_outgoing_liaisons(gen_rows) | |
| # Find agenda and report TDs | |
| agenda_number = "" | |
| agenda_url = "" | |
| # Match patterns like "agenda of Q10/17" or "agenda of Question 10/17" | |
| agenda_patterns = [ | |
| f"agenda of q{question}/{group}", | |
| f"agenda of question {question}/{group}", | |
| ] | |
| for row in wp_rows: | |
| title_lower = row.title.lower() | |
| for pattern in agenda_patterns: | |
| if pattern in title_lower: | |
| agenda_number = row.number.value.replace(' ', '') | |
| agenda_url = URL + row.number.link | |
| print(f" Found agenda TD: {agenda_number} - {row.title}") | |
| break | |
| if agenda_number: | |
| break | |
| if not agenda_number: | |
| print(f" WARNING: No agenda TD found for Question {question}/{group}") | |
| report_number = '' | |
| report_title_prefix = f"Report of Question {question}/{group}" | |
| interim_report_prefix = f"Report of ITU-T Q{question}/{group}" | |
| interim_reports = [] | |
| for row in wp_rows: | |
| if stripped_starts_with(report_title_prefix, row.title): | |
| report_number = row.number.value.replace(' ', '') | |
| elif stripped_starts_with(interim_report_prefix, row.title): | |
| interim_reports.append( | |
| make_href(URL + row.number.link, | |
| f"TD{row.number.value}/{wp_number}{row.lastRev}") | |
| ) | |
| year = int(start_date[2:4]) | |
| period = str(int(year / 4) * 4 + 1) | |
| # --------------------------------------------------------------- | |
| # Generate LaTeX snippets | |
| # --------------------------------------------------------------- | |
| os.makedirs(RESULTS_DIR, exist_ok=True) | |
| print(f"\nGenerating LaTeX snippets to {RESULTS_DIR}/") | |
| # --- Variables (includes \contact macro) --- | |
| _generate_variables(group, question, wp_number, place, start, end, | |
| start_date, period, question_details, report_number) | |
| # --- Introduction variables --- | |
| _gen_introduction(question_details, rapporteurs, associate_rapporteurs, agenda_url, agenda_number, wp_number) | |
| _generate_report( | |
| group, question, wp_number, start_date, period, | |
| c_rows, plen_rows, gen_rows, wp_rows, | |
| all_plen_rows, all_gen_rows, all_wp_rows, | |
| approval, determination, consent, non_normative, | |
| work_items, work_item_details, new_work_items, | |
| outgoing_ls, | |
| td_to_work_item, | |
| editors, | |
| processed_work_items, | |
| candidate_next, | |
| ) | |
| print("\nDone.") | |
| # =================================================================== | |
| # Variables file (used by LaTeX templates as macros) | |
| # =================================================================== | |
| def _generate_variables(group, question, wp_number, place, start, end, | |
| start_date, period, question_details, doc_number): | |
| """Generate results/00-variables.tex with LaTeX macro definitions. | |
| Produces \\newcommand entries for all document-level variables, | |
| including a \\contact command with the contact longtable. | |
| """ | |
| year = int(start_date[0:4]) | |
| first_year = int(year / 4) * 4 + 1 | |
| last_year = first_year + 3 | |
| occurence = _format_date_range(start, end) | |
| lines = [ | |
| f"\\newcommand{{\\studyGroup}}{{{group}}}", | |
| f"\\newcommand{{\\workingParty}}{{{wp_number}}}", | |
| f"\\newcommand{{\\questions}}{{{question}}}", | |
| f"\\newcommand{{\\place}}{{{place}}}", | |
| f"\\newcommand{{\\occurence}}{{{occurence}}}", | |
| f"\\newcommand{{\\startDate}}{{{start_date}}}", | |
| f"\\newcommand{{\\studyPeriod}}{{{first_year}-{last_year}}}", | |
| f"\\newcommand{{\\period}}{{{period}}}", | |
| f"\\newcommand{{\\reportNumber}}{{{doc_number}}}" | |
| f"\\newcommand{{\\abstr}}{{This TD contains the report for Question {question}/{group} meeting.}}", | |
| ] | |
| # Contact command — embeds all contacts in a single \contact macro | |
| contact_lines = [] | |
| for role in question_details.roles: | |
| role_label = role.roleName or "Contact" | |
| name = f"{role.firstName} {role.lastName}".strip() | |
| country = escape_latex(role.address) | |
| title_line = f"Q{question}/{group} {role_label}" | |
| contact_info = [] | |
| if role.tel: | |
| contact_info.append(f"Tel: {escape_latex(role.tel)}") | |
| if role.email: | |
| contact_info.append(f"Email: \\ul{{{escape_latex(role.email)}}}") | |
| contact_lines.append("\\begin{longtable}{p{0.15\\linewidth} p{0.30\\linewidth} p{0.50\\linewidth}}") | |
| contact_lines.append("\\midrule") | |
| newline_sep = ' \\newline ' | |
| contact_str = newline_sep.join(contact_info) | |
| contact_lines.append( | |
| f"\\textbf{{Contact:}} &\n" | |
| f"{escape_latex(name)}\\newline\n" | |
| f"{country}\\newline\n" | |
| f"{escape_latex(title_line)} &\n" | |
| f"{contact_str} \\\\") | |
| contact_lines.append("\\midrule") | |
| contact_lines.append("\\end{longtable}") | |
| lines.append("") | |
| lines.append("\\newcommand{\\contact}{") | |
| lines.extend(contact_lines) | |
| lines.append("}") | |
| write_result(RESULTS_DIR, "00-metadata.tex", "\n".join(lines) + "\n") | |
| # =================================================================== | |
| # Agenda generation | |
| # =================================================================== | |
| def _generate_report(group, question, wp_number, start_date, | |
| period, c_rows, plen_rows, gen_rows, wp_rows, | |
| all_plen_rows, all_gen_rows, all_wp_rows, | |
| approval, determination, consent, non_normative, | |
| work_items, work_item_details, new_work_items, | |
| outgoing_ls, | |
| td_to_work_item=None, editors=None, processed_work_items=None, | |
| candidate_next=None): | |
| """Generate all LaTeX snippet files for a Question report.""" | |
| _gen_executive_summary(group, question, c_rows, approval, determination, | |
| consent, non_normative, work_items, new_work_items, | |
| outgoing_ls, | |
| wp_rows, gen_rows, processed_work_items) | |
| _gen_documentation(group, question, wp_number, start_date, period, | |
| c_rows, plen_rows, gen_rows, wp_rows) | |
| _gen_interim_reports(group, question, wp_number, wp_rows) | |
| _gen_discussions(group, question, wp_number, work_items, c_rows, gen_rows, | |
| wp_rows, processed_work_items) | |
| _gen_draft_recommendations(group, question, wp_number, wp_rows, | |
| approval, determination, consent, non_normative, | |
| td_to_work_item) | |
| _gen_outgoing_liaisons(outgoing_ls, gen_rows) | |
| _gen_work_programme(group, question, wp_number, work_item_details, | |
| new_work_items, wp_rows, editors) | |
| _gen_candidate_work_items(group, wp_number, candidate_next, wp_rows, | |
| work_item_details, editors) | |
| _gen_annex_a(group, question, wp_number, c_rows, gen_rows, plen_rows, wp_rows, | |
| all_gen_rows, all_plen_rows, all_wp_rows) | |
| _gen_annex_c(wp_number, wp_rows) | |
| # --- Section generators --- | |
| def _gen_introduction(question_details, rapporteurs, associate_rapporteurs, agenda_url, agenda_number, wp_number): | |
| """01.tex — introduction variables (questionTitle, leadership, assistedBy, agendaRef).""" | |
| leadership = " and ".join(escape_latex(r) for r in rapporteurs) if rapporteurs else "" | |
| title = escape_latex(question_details.title or '') | |
| # Format "assisted by [Associate Rapporteurs]" if any exist | |
| if associate_rapporteurs: | |
| assisted_by = ", assisted by " + ", ".join(escape_latex(ar) for ar in associate_rapporteurs) | |
| else: | |
| assisted_by = "" | |
| # Format agenda as hyperlink with TD number (e.g., "TD 123/WP1" as clickable link) | |
| if agenda_number and agenda_url: | |
| agenda_ref = make_href(agenda_url, f"TD {agenda_number}/{wp_number}") | |
| else: | |
| agenda_ref = "" | |
| lines = [ | |
| f"\\newcommand{{\\questionTitle}}{{{title}}}", | |
| f"\\newcommand{{\\leadership}}{{{leadership}}}", | |
| f"\\newcommand{{\\assistedBy}}{{{assisted_by}}}", | |
| f"\\newcommand{{\\agendaRef}}{{{agenda_ref}}}", | |
| ] | |
| write_result(RESULTS_DIR, "01-introduction.tex", "\n".join(lines) + "\n") | |
| def _gen_executive_summary(group, question, c_rows, approval, determination, | |
| consent, non_normative, work_items, new_work_items, | |
| outgoing_ls, | |
| wp_rows, gen_rows=None, processed_work_items=None): | |
| """02.tex — executive summary variables (nbContributions, listQuestionContributions).""" | |
| gen_rows = gen_rows or [] | |
| processed_work_items = processed_work_items or [] | |
| def _make_td_links(td_numbers, rows, suffix=""): | |
| """Create hyperlinks for TD numbers.""" | |
| links = [] | |
| for n in td_numbers: | |
| _, td = find_td_by_number(rows, n) | |
| if td: | |
| links.append(make_href(URL + td.number.link, f"TD {n}{suffix}")) | |
| else: | |
| links.append(f"TD {n}{suffix}") | |
| return links | |
| # Build the itemize list | |
| items = [] | |
| # Approval | |
| if approval: | |
| links = _make_td_links(approval, wp_rows) | |
| items.append(f" \\item Recommendations finalized and proposed for TAP Approval: " | |
| f"{comma_separated_list(links)}") | |
| else: | |
| items.append(f" \\item Recommendations finalized and proposed for TAP Approval: None") | |
| # Consent | |
| if consent: | |
| links = _make_td_links(consent, wp_rows) | |
| items.append(f" \\item Recommendations finalized and proposed for TAP Consent: " | |
| f"{comma_separated_list(links)}") | |
| else: | |
| items.append(f" \\item Recommendations finalized and proposed for TAP Consent: None") | |
| # Determination | |
| if determination: | |
| links = _make_td_links(determination, wp_rows) | |
| items.append(f" \\item Recommendations finalized and proposed for TAP Determination: " | |
| f"{comma_separated_list(links)}") | |
| else: | |
| items.append(f" \\item Recommendations finalized and proposed for TAP Determination: None") | |
| # Agreement | |
| if non_normative: | |
| links = _make_td_links(non_normative, wp_rows) | |
| items.append(f" \\item Recommendations finalized and proposed for TAP Agreement: " | |
| f"{comma_separated_list(links)}") | |
| else: | |
| items.append(f" \\item Recommendations finalized and proposed for TAP Agreement: None") | |
| # Work items progressed | |
| if processed_work_items: | |
| td_links = [make_href(URL + td.number.link, f"TD {td_num}") | |
| for name, td_num, td in processed_work_items] | |
| items.append(f" \\item Work items progressed: {comma_separated_list(td_links)}") | |
| elif work_items: | |
| items.append(f" \\item Work items progressed: {comma_separated_list(work_items)}") | |
| else: | |
| items.append(f" \\item Work items progressed: None") | |
| # Outgoing liaison statements (from gen_rows) | |
| if outgoing_ls: | |
| links = _make_td_links(outgoing_ls, gen_rows, "/G") | |
| items.append(f" \\item Outgoing liaison statements agreed: {comma_separated_list(links)}") | |
| else: | |
| items.append(f" \\item Outgoing liaison statements agreed: None") | |
| # Build the two macros | |
| lines = [ | |
| f"\\newcommand{{\\nbContributions}}{{{len(c_rows)}}}", | |
| "", | |
| "\\newcommand{\\listQuestionContributions}{", | |
| "\\begin{itemize}", | |
| ] | |
| lines.extend(items) | |
| lines.append("\\end{itemize}") | |
| lines.append("}") | |
| write_result(RESULTS_DIR, "02-executive-summary.tex", "\n".join(lines) + "\n") | |
| def _gen_documentation(group, question, wp_number, start_date, period, | |
| c_rows, plen_rows, gen_rows, wp_rows): | |
| """03.tex — documentation variables (documentation, reflectorUrl, subUrl, ifaUrl).""" | |
| # \documentation — itemize list of document links | |
| doc_lines = [] | |
| doc_lines.append("\\begin{itemize}") | |
| c_links = ", ".join(td_href(r, "C") for r in reversed(c_rows)) | |
| doc_lines.append(f" \\item Contributions: {c_links}") | |
| p_links = ", ".join( | |
| make_href(URL + r.number.link, f"{r.number.value}{r.lastRev}") | |
| for r in reversed(plen_rows) | |
| ) | |
| doc_lines.append(f" \\item TD/P: {p_links}") | |
| g_links = ", ".join( | |
| make_href(URL + r.number.link, f"{r.number.value}{r.lastRev}") | |
| for r in reversed(gen_rows) | |
| ) | |
| doc_lines.append(f" \\item TD/G: {g_links}") | |
| w_links = ", ".join( | |
| make_href(URL + r.number.link, f"{r.number.value}{r.lastRev}") | |
| for r in reversed(wp_rows) | |
| ) | |
| doc_lines.append(f" \\item TD/{wp_number}: {w_links}") | |
| doc_lines.append("\\end{itemize}") | |
| # Email URLs - format: t25sg17qXX@lists.itu.int (lowercase q, two-digit question) | |
| reflector = f"t{period}sg{group}q{int(question):02d}@lists.itu.int" | |
| year = int(start_date[0:4]) | |
| first_year = int(year / 4) * 4 + 1 | |
| last_year = first_year + 3 | |
| sub_url = f"{URL}/net4/iwm?p0=0&p11=ITU&p12=ITU-SEP-ITU-T-SEP-SP\\%2017-SEP-Study\\%20Group\\%2017&p21=ITU&p22=ITU" | |
| ifa_url = f"{URL}/en/ITU-T/studygroups/{first_year}-{last_year}/{group}/Pages/ifa-structure.aspx" | |
| lines = [ | |
| "\\newcommand{\\documentation}{", | |
| "\n".join(doc_lines), | |
| "}", | |
| "", | |
| f"\\newcommand{{\\reflectorUrl}}{{\\ul{{{reflector}}}}}", | |
| "", | |
| f"\\newcommand{{\\subUrl}}{{\\href{{{sub_url}}}{{subscription webpage}}}}", | |
| "", | |
| f"\\newcommand{{\\ifaUrl}}{{\\href{{{ifa_url}}}{{webpage}}}}", | |
| ] | |
| write_result(RESULTS_DIR, "03-documentation.tex", "\n".join(lines) + "\n") | |
| def _gen_interim_reports(group, question, wp_number, wp_rows): | |
| """04.tex — interim reports variable (interimReports).""" | |
| meeting_reports = get_meeting_reports(wp_rows, question, group) | |
| content_lines = [] | |
| if meeting_reports: | |
| suffix = "meeting" if len(meeting_reports) == 1 else "meetings" | |
| content_lines.append(f"Since the last SG{group} meeting, Question {question}/{group} " | |
| f"held the following Rapporteur {suffix}") | |
| content_lines.append("\\begin{itemize}") | |
| for report in meeting_reports: | |
| # Extract place and date from title like "(Paris, 6 December 2024)" | |
| place = "" | |
| date = "" | |
| idx1 = report.title.rfind('(') | |
| if idx1 >= 0: | |
| idx2 = report.title.find(')', idx1) | |
| if idx2 >= 0: | |
| content = report.title[idx1 + 1:idx2] | |
| # Split by comma: "Paris, 6 December 2024" -> ["Paris", "6 December 2024"] | |
| parts = content.split(',', 1) | |
| if len(parts) == 2: | |
| place = parts[0].strip() | |
| date = parts[1].strip() | |
| else: | |
| # No comma, might be just place or date | |
| place = content.strip() | |
| td_number = report.number.value.replace(' ', '') | |
| td_link = make_href(URL + report.number.link, f"TD {td_number}/{wp_number}") | |
| content_lines.append( | |
| f" \\item The report of the Rapporteur meeting from {date} ({place}), " | |
| f"can be found in {td_link}" | |
| ) | |
| content_lines.append("\\end{itemize}") | |
| else: | |
| content_lines.append("\\textit{No interim rapporteur meetings were held.}") | |
| lines = [ | |
| "\\newcommand{\\interimReports}{", | |
| "\n".join(content_lines), | |
| "}", | |
| ] | |
| write_result(RESULTS_DIR, "04-interim-reports.tex", "\n".join(lines) + "\n") | |
| def _gen_discussions(group, question, wp_number, work_items, c_rows, gen_rows, | |
| wp_rows, processed_work_items=None): | |
| """05-discussions subsection content files.""" | |
| processed_work_items = processed_work_items or [] | |
| selected_rows = [] | |
| # 05-01: Ongoing work items (from processed_work_items in part 2) | |
| lines = [] | |
| if processed_work_items: | |
| for i, (name, td_num, td) in enumerate(processed_work_items): | |
| td_link = make_href(URL + td.number.link, f"TD {td_num}") | |
| lines.append(f"\\subsubsection{{Work Item {i + 1}: ({escape_latex(name)}):}}\n") | |
| lines.append(f"{td_link}\n") | |
| # Find related contributions | |
| search_term = name.split(' ')[0] if ' ' in name else name | |
| for row in c_rows: | |
| if search_term.lower() in row.title.lower(): | |
| selected_rows.append(row) | |
| lines.append(f"{td_href(row, 'C')}\n") | |
| lines.append(f"\\textit{{TODO: write the observation here}}\n") | |
| else: | |
| # Fallback to old behavior if no processed_work_items | |
| for i, wi in enumerate(work_items): | |
| lines.append(f"\\subsubsection{{Work Item {i + 1}: ({wi}):}}\n") | |
| idx = wi.find(' ') | |
| search_term = wi[:idx] if idx > 0 else wi | |
| for row in c_rows: | |
| if search_term.lower() in row.title.lower(): | |
| selected_rows.append(row) | |
| lines.append(f"{td_href(row, 'C')}\n") | |
| lines.append(f"\\textit{{TODO: write the observation here}}\n") | |
| write_result(RESULTS_DIR, "05-01-work-items.tex", "\n".join(lines)) | |
| # 05-02: New proposed work items (search in wp_rows for "Proposal", "NWI", or "new work item") | |
| lines = [] | |
| num = 0 | |
| for row in wp_rows: | |
| if is_new_work_item(row.title): | |
| num += 1 | |
| td_link = make_href(URL + row.number.link, f"TD {row.number.value.replace(' ', '')}/{wp_number}") | |
| lines.append(f"\\subsubsection{{New Work Item {num}: ({escape_latex(row.title)}):}}\n") | |
| lines.append(f"{td_link}\n") | |
| lines.append(f"\\textit{{TODO: write the observation here}}\n") | |
| write_result(RESULTS_DIR, "05-02-new-work-items.tex", "\n".join(lines)) | |
| # 05-03: Other contributions | |
| lines = [] | |
| for row in c_rows: | |
| if row not in selected_rows: | |
| lines.append(f"{td_href(row, 'C')}: {escape_latex(row.title)}\n") | |
| write_result(RESULTS_DIR, "05-03-other-contributions.tex", "\n".join(lines)) | |
| # 05-04: Incoming liaison statements | |
| selected_gen = [] | |
| lines = [] | |
| for row in gen_rows: | |
| if row.title.startswith("LS/i"): | |
| selected_gen.append(row) | |
| source_link = make_href(URL + row.source.link, row.source.name) | |
| lines.append( | |
| f"{make_href(URL + row.number.link, f'TD{row.number.value}{row.lastRev}/G')}: " | |
| f"{escape_latex(row.title)} [from {source_link}]\n" | |
| ) | |
| lines.append(f"\\textit{{TODO: write the observation here}}\n") | |
| write_result(RESULTS_DIR, "05-04-incoming-liaisons.tex", "\n".join(lines)) | |
| def _gen_recommendation_table_rows(wp_rows, items, group, question, wp_number, has_a5=True, td_wi=None): | |
| """Generate table rows for recommendation tables (approval/determination/consent).""" | |
| td_wi = td_wi or {} | |
| lines = [] | |
| for num, element in enumerate(items, 1): | |
| q_name, td, a5 = find_question_name_td_and_a5(wp_rows, element) | |
| if td is not None: | |
| final_text = make_href(URL + td.number.link, | |
| f"TD {td.number.value}{td.lastRev}/{wp_number}") | |
| else: | |
| final_text = "no update" | |
| a5_text = "" | |
| if a5 is not None: | |
| a5_text = make_href(URL + a5.number.link, | |
| f"TD {a5.number.value}{a5.lastRev}/{wp_number}") | |
| # Extract work item name: find part starting with X., stop at first space | |
| work_item = "" | |
| text_title = "" | |
| if td is not None: | |
| source = td.recommendation or td.acronym or "" | |
| for part in source.split(): | |
| if part.startswith('X.') or part.startswith('XSTR.'): | |
| work_item = part | |
| break | |
| text_title = escape_latex(td.textTitle) | |
| # Fill version and equiv. num. from work programme data | |
| wp_wi = td_wi.get(element) | |
| version = escape_latex(wp_wi.version) if wp_wi and wp_wi.version else "" | |
| equiv_num = escape_latex(wp_wi.equivNum) if wp_wi and wp_wi.equivNum else "" | |
| # If no work_item found from TD, try to get it from work programme | |
| if not work_item and wp_wi: | |
| work_item = wp_wi.workItem or "" | |
| if not text_title and wp_wi: | |
| text_title = escape_latex(wp_wi.title or "") | |
| # Removed # and Question columns | |
| if has_a5: | |
| lines.append(table_row_str([ | |
| seqsplit_text(work_item), version, text_title, | |
| final_text, a5_text, equiv_num | |
| ])) | |
| else: | |
| lines.append(table_row_str([ | |
| seqsplit_text(work_item), version, text_title, final_text | |
| ])) | |
| return "".join(lines) | |
| def _gen_draft_recommendations(group, question, wp_number, wp_rows, | |
| approval, determination, consent, non_normative, | |
| td_to_work_item=None): | |
| """06-draft-recommendations table rows.""" | |
| td_wi = td_to_work_item or {} | |
| # TAP approval | |
| rows = _gen_recommendation_table_rows(wp_rows, approval, group, question, wp_number, td_wi=td_wi) | |
| has_approval = "true" if approval else "false" | |
| write_result(RESULTS_DIR, "06-approval.tex", | |
| f"\\newcommand{{\\hasApproval}}{{{has_approval}}}\n" | |
| f"\\newcommand{{\\approval}}{{\n{rows}}}\n") | |
| # TAP determination | |
| rows = _gen_recommendation_table_rows(wp_rows, determination, group, question, wp_number, td_wi=td_wi) | |
| has_determination = "true" if determination else "false" | |
| write_result(RESULTS_DIR, "06-determination.tex", | |
| f"\\newcommand{{\\hasDetermination}}{{{has_determination}}}\n" | |
| f"\\newcommand{{\\determination}}{{\n{rows}}}\n") | |
| # AAP consent | |
| rows = _gen_recommendation_table_rows(wp_rows, consent, group, question, wp_number, td_wi=td_wi) | |
| has_consent = "true" if consent else "false" | |
| write_result(RESULTS_DIR, "06-consent.tex", | |
| f"\\newcommand{{\\hasConsent}}{{{has_consent}}}\n" | |
| f"\\newcommand{{\\consent}}{{\n{rows}}}\n") | |
| # Non-normative / agreement | |
| lines = [] | |
| for num, element in enumerate(non_normative, 1): | |
| q_name, td, a5 = find_question_name_td_and_a5(wp_rows, element) | |
| if td is None: | |
| print(f"WARNING: Could not find TD for agreement number '{element}', skipping row") | |
| continue | |
| final_text = make_href(URL + td.number.link, | |
| f"TD {td.number.value}{td.lastRev}/{wp_number}") | |
| # Extract work item name: find part starting with X./XSTR., stop at first space | |
| work_item = "" | |
| source = td.acronym or td.recommendation or "" | |
| for part in source.split(): | |
| if part.startswith('X.') or part.startswith('XSTR.'): | |
| work_item = part | |
| break | |
| # Use work programme data for version if available | |
| wp_wi = td_wi.get(element) | |
| version = escape_latex(wp_wi.version) if wp_wi and wp_wi.version else "" | |
| text_title = escape_latex(split_title(td.title)[3]) | |
| # Removed # and Question columns | |
| lines.append(table_row_str([ | |
| seqsplit_text(work_item), version, text_title, final_text | |
| ])) | |
| rows = "".join(lines) | |
| has_agreement = "true" if non_normative else "false" | |
| write_result(RESULTS_DIR, "07-agreement.tex", | |
| f"\\newcommand{{\\hasAgreement}}{{{has_agreement}}}\n" | |
| f"\\newcommand{{\\agreement}}{{\n{rows}}}\n") | |
| def _gen_outgoing_liaisons(outgoing_ls, gen_rows): | |
| """09-outgoing-liaison-statements table rows.""" | |
| lines = [] | |
| for num, element in enumerate(outgoing_ls, 1): | |
| q_name, td = find_td_by_number(gen_rows, element) | |
| title = "" | |
| td_name = "no update" | |
| action_to = "" | |
| info_to = "" | |
| if td: | |
| title = td.title | |
| action_to, info_to = _parse_liaison_destinations(title) | |
| td_name = make_href(URL + td.number.link, | |
| f"TD {element}{td.lastRev}/G") | |
| # Combine action_to and info_to in single column (without prefixes) | |
| combined_dest = [] | |
| if action_to: | |
| combined_dest.append(escape_latex(action_to)) | |
| if info_to: | |
| combined_dest.append(escape_latex(info_to)) | |
| destination = " / ".join(combined_dest) if combined_dest else "\\textit{(manual entry)}" | |
| # Columns: Title | For information / For action to | TD number | |
| lines.append(table_row_str([ | |
| escape_latex(title), | |
| destination, | |
| td_name | |
| ])) | |
| rows = "".join(lines) | |
| has_outgoing_liaisons = "true" if outgoing_ls else "false" | |
| write_result(RESULTS_DIR, "08-outgoing-liaisons.tex", | |
| f"\\newcommand{{\\hasOutgoingLiaisons}}{{{has_outgoing_liaisons}}}\n" | |
| f"\\newcommand{{\\outgoingLiaisons}}{{\n{rows}}}\n") | |
| def _parse_liaison_destinations(title): | |
| """Parse '[to X]' and '[for info to Y]' from a liaison statement title. | |
| Returns (action_to, info_to) strings. | |
| """ | |
| action_to = "" | |
| info_to = "" | |
| # Find all bracketed sections | |
| idx = 0 | |
| while idx < len(title): | |
| start = title.find('[', idx) | |
| if start < 0: | |
| break | |
| end = title.find(']', start) | |
| if end < 0: | |
| break | |
| bracket = title[start + 1:end].strip() | |
| if bracket.lower().startswith('for info'): | |
| # "[for info to X, Y]" → extract after "for info to " | |
| prefix = 'for info to ' | |
| pos = bracket.lower().find(prefix) | |
| if pos >= 0: | |
| info_to = bracket[pos + len(prefix):] | |
| elif bracket.lower().startswith('to '): | |
| action_to = bracket[3:] # skip "to " | |
| idx = end + 1 | |
| return action_to, info_to | |
| def _gen_work_programme(group, question, wp_number, work_item_details, | |
| new_work_items, wp_rows, editors=None): | |
| """10-work-programme table rows (new, deleted, ongoing).""" | |
| editors = editors or {} | |
| # New work items (from wp_rows, same as part 5.2) | |
| lines = [] | |
| for row in wp_rows: | |
| if is_new_work_item(row.title): | |
| work_item, text_title = extract_new_work_item_info(row.title, wp_rows) | |
| # Skip entries without a valid work item name (X. or XSTR.) | |
| if not work_item: | |
| continue | |
| td_link = make_href(URL + row.number.link, f"TD {row.number.value.replace(' ', '')}/{wp_number}") | |
| # Columns: Work Item | Status | Title | Editor | Base Text | Equivalent | Approval process | |
| lines.append(table_row_str([ | |
| seqsplit_text(work_item), "New", | |
| escape_latex(text_title), "(manual entry)", td_link, "", "" | |
| ])) | |
| rows = "".join(lines) | |
| has_new_work_items = "true" if lines else "false" | |
| write_result(RESULTS_DIR, "09-wp-new.tex", | |
| f"\\newcommand{{\\hasNewWorkItems}}{{{has_new_work_items}}}\n" | |
| f"\\newcommand{{\\newWorkItems}}{{\n{rows}}}\n") | |
| # Ongoing work items — only "Under study" status | |
| lines = [] | |
| for wi in work_item_details: | |
| name = wi.workItem or "" | |
| title = wi.title or "" | |
| status = wi.status or "" | |
| equiv = wi.equivNum or "" | |
| timing = wi.timing or "" | |
| aap = wi.approvalProcess or "" | |
| if "under study" not in status.lower(): | |
| continue | |
| # Try to find matching WP TD for the base text link | |
| td_name = "no update" | |
| _, td = find_td_by_name(wp_rows, name) | |
| if td is None: | |
| alt = extract_alt_name(name) | |
| if alt: | |
| _, td = find_td_by_name(wp_rows, alt) | |
| if td: | |
| td_name = make_href(URL + td.number.link, | |
| f"TD{td.number.value}{td.lastRev}/{wp_number}") | |
| if not title: | |
| title = td.textTitle | |
| # New columns (removed # and Question): Work Item | Status | Title | Editor | Base Text | Equivalent | Target Date | Summary updated | AAP | |
| lines.append(table_row_str([ | |
| seqsplit_text(name), escape_latex(status), | |
| escape_latex(title), "(manual entry)", td_name, escape_latex(equiv), | |
| escape_latex(timing), "", escape_latex(aap) | |
| ])) | |
| rows = "".join(lines) | |
| has_ongoing_work_items = "true" if lines else "false" | |
| write_result(RESULTS_DIR, "09-wp-ongoing.tex", | |
| f"\\newcommand{{\\hasOngoingWorkItems}}{{{has_ongoing_work_items}}}\n" | |
| f"\\newcommand{{\\ongoingWorkItems}}{{\n{rows}}}\n") | |
| def _gen_candidate_work_items(group, wp_number, candidate_next, wp_rows, | |
| work_item_details, editors=None): | |
| """10-candidate-work-items.tex — candidate work items for next SG meeting.""" | |
| editors = editors or {} | |
| candidate_next = candidate_next or [] | |
| lines = [] | |
| for element in candidate_next: | |
| # Find matching work item in work programme | |
| wi_match = None | |
| for wi in work_item_details: | |
| wi_name = wi.workItem or "" | |
| wi_alt = extract_alt_name(wi_name) | |
| if (wi_name == element or wi_alt == element | |
| or element in wi_name or (wi_alt and element in wi_alt)): | |
| wi_match = wi | |
| break | |
| name = element | |
| status = "" | |
| title = "" | |
| equiv = "" | |
| if wi_match: | |
| status = wi_match.status or "" | |
| title = wi_match.title or "" | |
| equiv = wi_match.equivNum or "" | |
| # Try to find matching WP TD for the base text link | |
| # Look for TDs containing "baseline text" in the title AND the work item name | |
| td_name = "no update" | |
| td = None | |
| element_lower = element.lower() | |
| alt = extract_alt_name(element) | |
| alt_lower = alt.lower() if alt else "" | |
| for row in wp_rows: | |
| row_title = (row.title or "").lower() | |
| # Check if this is a baseline text TD for this work item | |
| if "baseline text" in row_title: | |
| # Check if work item name appears in the title | |
| if element_lower in row_title or (alt_lower and alt_lower in row_title): | |
| td = row | |
| break | |
| if td: | |
| td_name = make_href(URL + td.number.link, | |
| f"TD{td.number.value}{td.lastRev}/{wp_number}") | |
| if not title: | |
| title = td.textTitle | |
| # Columns: Work Item | Status | Title | Editor | Base Text | A.5 justification | Equivalent | |
| lines.append(table_row_str([ | |
| seqsplit_text(name), escape_latex(status), | |
| escape_latex(title), "(manual entry)", td_name, "", escape_latex(equiv) | |
| ])) | |
| rows = "".join(lines) | |
| has_candidate = "true" if candidate_next else "false" | |
| write_result(RESULTS_DIR, "10-candidate-work-items.tex", | |
| f"\\newcommand{{\\hasCandidateWorkItems}}{{{has_candidate}}}\n" | |
| f"\\newcommand{{\\candidateWorkItems}}{{\n{rows}}}\n") | |
| def _format_source(source_value, group): | |
| """Format source value, preserving full text with normal spaces. | |
| Allows text to wrap in table cells when the source name is long. | |
| Note: Do NOT escape here - make_href() already calls escape_latex(). | |
| """ | |
| if not source_value: | |
| return source_value | |
| return source_value | |
| def _gen_annex_c(wp_number, wp_rows): | |
| """annex-c-new-work-items.tex — list of new work items for Annex C.""" | |
| lines = [] | |
| for row in wp_rows: | |
| if is_new_work_item(row.title): | |
| work_item, text_title = extract_new_work_item_info(row.title, wp_rows) | |
| if not work_item: | |
| continue | |
| td_link = make_href(URL + row.number.link, | |
| f"TD {row.number.value.replace(' ', '')}/{wp_number}") | |
| lines.append(f" \\item {escape_latex(work_item)}: {escape_latex(text_title)} ({td_link})") | |
| if lines: | |
| content = f"\\newcommand{{\\annexNewWorkItems}}{{\n" + "\n".join(lines) + "\n}\n" | |
| else: | |
| content = "% No new work items\n" | |
| write_result(RESULTS_DIR, "annex-c-new-work-items.tex", content) | |
| def _gen_annex_a(group, question, wp_number, c_rows, gen_rows, plen_rows, wp_rows, | |
| all_gen_rows=None, all_plen_rows=None, all_wp_rows=None): | |
| """annex-a table rows (contributions, GEN, PLEN, WP) including QALL documents.""" | |
| q_str = f"Q{question}/{group}" | |
| qall_str = f"QALL/{group}" | |
| all_gen_rows = all_gen_rows or [] | |
| all_plen_rows = all_plen_rows or [] | |
| all_wp_rows = all_wp_rows or [] | |
| # Contributions | |
| lines = [] | |
| for row in c_rows: | |
| name = make_href(URL + row.number.link, f"C{row.number.value}{row.lastRev}") | |
| source_text = _format_source(row.source.name, group) | |
| source = make_href(URL + row.source.link, source_text) | |
| lines.append(table_row_str([name, source, escape_latex(row.title), q_str])) | |
| rows = "".join(lines) if lines else "NONE" | |
| write_result(RESULTS_DIR, "annex-a-contributions.tex", | |
| f"\\newcommand{{\\annexContributions}}{{\n{rows}}}\n") | |
| # GEN TDs (Q-specific + QALL) | |
| lines = [] | |
| for row in gen_rows: | |
| name = make_href(URL + row.number.link, f"TD{row.number.value}{row.lastRev}/G") | |
| source_text = _format_source(row.source.name, group) | |
| source = make_href(URL + row.source.link, source_text) | |
| lines.append(table_row_str([name, source, escape_latex(row.title), q_str])) | |
| for row in all_gen_rows: | |
| name = make_href(URL + row.number.link, f"TD{row.number.value}{row.lastRev}/G") | |
| source_text = _format_source(row.source.name, group) | |
| source = make_href(URL + row.source.link, source_text) | |
| lines.append(table_row_str([name, source, escape_latex(row.title), qall_str])) | |
| rows = "".join(lines) if lines else "NONE" | |
| write_result(RESULTS_DIR, "annex-a-gen.tex", | |
| f"\\newcommand{{\\annexGen}}{{\n{rows}}}\n") | |
| # PLEN TDs (Q-specific + QALL) | |
| lines = [] | |
| for row in plen_rows: | |
| name = make_href(URL + row.number.link, f"TD{row.number.value}{row.lastRev}/P") | |
| source_text = _format_source(row.source.name, group) | |
| source = make_href(URL + row.source.link, source_text) | |
| lines.append(table_row_str([name, source, escape_latex(row.title), q_str])) | |
| for row in all_plen_rows: | |
| name = make_href(URL + row.number.link, f"TD{row.number.value}{row.lastRev}/P") | |
| source_text = _format_source(row.source.name, group) | |
| source = make_href(URL + row.source.link, source_text) | |
| lines.append(table_row_str([name, source, escape_latex(row.title), qall_str])) | |
| rows = "".join(lines) if lines else "NONE" | |
| write_result(RESULTS_DIR, "annex-a-plen.tex", | |
| f"\\newcommand{{\\annexPlen}}{{\n{rows}}}\n") | |
| # WP TDs (Q-specific + QALL) | |
| lines = [] | |
| for row in wp_rows: | |
| name = make_href(URL + row.number.link, | |
| f"TD{row.number.value}{row.lastRev}/{wp_number}") | |
| source_text = _format_source(row.source.name, group) | |
| source = make_href(URL + row.source.link, source_text) | |
| lines.append(table_row_str([name, source, escape_latex(row.title), q_str])) | |
| for row in all_wp_rows: | |
| name = make_href(URL + row.number.link, | |
| f"TD{row.number.value}{row.lastRev}/{wp_number}") | |
| source_text = _format_source(row.source.name, group) | |
| source = make_href(URL + row.source.link, source_text) | |
| lines.append(table_row_str([name, source, escape_latex(row.title), qall_str])) | |
| rows = "".join(lines) if lines else "NONE" | |
| write_result(RESULTS_DIR, "annex-a-wp.tex", | |
| f"\\newcommand{{\\annexWp}}{{\n{rows}}}\n") | |
| def _print_td_summary(label, rows): | |
| """Print a summary of fetched TD rows.""" | |
| print(f"\n {label}: {len(rows)} document(s)") | |
| for row in rows: | |
| td_num = row.number.value.strip() if row.number else "?" | |
| rev = row.lastRev if row.lastRev else "" | |
| title = row.title or "" | |
| if len(title) > 80: | |
| title = title[:77] + "..." | |
| print(f" TD {td_num}{rev} — {title}") | |
| if __name__ == "__main__": | |
| main() | |