diff options
| author | Florian Fischer <florian.fischer@muhq.space> | 2023-08-22 15:27:14 +0200 |
|---|---|---|
| committer | Florian Fischer <florian.fischer@muhq.space> | 2023-08-22 15:27:14 +0200 |
| commit | 515ca7fadd11276421ba9d9be217a07b2e6b1652 (patch) | |
| tree | 58b53b87813a33a04a918741d524d839344913d9 /scripts/gen_map/gen_map.py | |
| parent | f44dbadbf41ec784a46a7fb7818d2ff822d1255a (diff) | |
| download | unterricht-515ca7fadd11276421ba9d9be217a07b2e6b1652.tar.gz unterricht-515ca7fadd11276421ba9d9be217a07b2e6b1652.zip | |
add script to create rough map for the school year
school hoildays are retrieved from schulferien.eu and the lehrplan
is scraped from lehrplanplus.bayern.de.
Diffstat (limited to 'scripts/gen_map/gen_map.py')
| -rwxr-xr-x | scripts/gen_map/gen_map.py | 298 |
1 files changed, 298 insertions, 0 deletions
diff --git a/scripts/gen_map/gen_map.py b/scripts/gen_map/gen_map.py new file mode 100755 index 00000000..2f1089de --- /dev/null +++ b/scripts/gen_map/gen_map.py @@ -0,0 +1,298 @@ +#!/usr/bin/env python3 + +import argparse +from datetime import datetime, timedelta +from pathlib import Path +import os +import re +import sys +import urllib + +from bs4 import BeautifulSoup +import icalevents +from icalevents.icalevents import events +import requests + +BAYERN = 1 +ICAL_BASE_URL = 'https://www.schulferien.eu/downloads/ical4.php' + +TEMPLATE_DIR = Path( + os.environ['HOME'] +) / 'Documents/MyDocuments/Work/Schule/Unterricht/templates' +TEMPLATE_FILE = 'map.md' + +LPP_BASE_URL = 'https://www.lehrplanplus.bayern.de/fachlehrplan/gymnasium' + +WEEKLY_HOURS = { + 'mathematik': { + 5: 4, + 6: 4, + 7: 4, + 8: 3, + 9: 4, + 10: 3, + 11: 4 + }, + 'informatik': { + 9: 2, + 10: 2, + 11: 2 + }, + 'nt_gym': { + 6: 2, + 7: 2 + } +} + + +def default_year() -> int: + """return the default year""" + now = datetime.now() + return now.year if now.month < 8 else now.year + 1 + + +def get_holidays(land: int, year: int) -> list[icalevents.icalparser.Event]: + """Get list of all holidays in a school year""" + start_year = datetime(year=year - 1, month=1, day=1) + end_year = datetime(year=year + 1, month=12, day=31) + + params = {'land': land, 'year': year, 'type': 1} + uri_first_year = ICAL_BASE_URL + '?' + urllib.parse.urlencode(params) + + first_holidays = events(url=uri_first_year, start=start_year, end=end_year) + + params['year'] = year + 1 + uri_second_year = ICAL_BASE_URL + '?' + urllib.parse.urlencode(params) + + second_holidays = events(url=uri_second_year, + start=start_year, + end=end_year) + + first_summer = next( + filter(lambda e: "Sommerferien" in e.summary, first_holidays)) + + second_summer = next( + filter(lambda e: "Sommerferien" in e.summary, second_holidays)) + + return [ + h for h in first_holidays + second_holidays + if h.start >= first_summer.start and h.start <= second_summer.start + ] + + +def fmt_holiday(holiday: icalevents.icalparser.Event) -> str: + """Return a formatted holiday description""" + h_start = f'{holiday.start.date().strftime("%d.%m")}' + h_end = f'{holiday.end.date().strftime("%d.%m")}' + return f'{h_start}-{h_end} {holiday.summary}' + + +def get_public_holidays(land: int, start: datetime, + end: datetime) -> list[icalevents.icalparser.Event]: + """Get list of all public holidays in the school year""" + + params = {'land': land, 'year': start.year, 'type': 0} + uri_first_year = ICAL_BASE_URL + '?' + urllib.parse.urlencode(params) + + params['year'] = end.year + uri_second_year = ICAL_BASE_URL + '?' + urllib.parse.urlencode(params) + + all_public_holidays = events(url=uri_first_year, start=start, + end=end) + events( + url=uri_second_year, start=start, end=end) + + return [ + e for e in all_public_holidays if e.start >= start and e.start <= end + ] + + +class LehrplanSection: + + def __init__(self, num, summary, duration, subsections=None): + self.num = num + self.summary = summary + self.duration = duration + self.subsections = subsections or [] + + def has_subsections(self) -> bool: + return bool(self.subsections) + + def __str__(self): + return f'{self.num}: {self.summary} ({self.duration}h) {self.subsections}' + + def __repr__(self): + return f'LehrplanSection({self.num}, {self.summary}, {self.duration}h, {self.subsections})' + + +Lehrplan = list[LehrplanSection] + + +def get_lehrplan(subject: str, form: str) -> Lehrplan: + """Retrieve the sections and expected duration from lehrplanplus""" + + lpp_url = '/'.join([LPP_BASE_URL, form, subject]) + resp = requests.get(lpp_url, timeout=10) + html = BeautifulSoup(resp.text) + + summary_re = re.compile(r'(.*) \(ca\. ([1-9][0-9]*) Std\.\)') + + lehrplan: Lehrplan = [] + top_level_sections = html.find_all(**{'class': 'headline_lvl1'}) + for section in top_level_sections: + num = list(section.find(**{ + 'class': 'head-absatz-num' + }).children)[2].strip() + + summary = section.find(**{ + 'class': 'head-absatz-title-short' + }).string.strip() + summary = summary.replace('\xa0', ' ') + + m = summary_re.match(summary) + summary = m.group(1) + duration = int(m.group(2)) + lehrplan.append(LehrplanSection(num, summary, duration)) + + second_level_sections = html.find_all(**{'class': 'headline_lvl2'}) + for section in second_level_sections: + summary = section.find(**{ + 'class': 'head-absatz-title-long' + }).string.strip() + summary = summary.replace('\xa0', ' ') + + m = summary_re.match(summary) + summary = m.group(1) + duration = int(m.group(2)) + num = list(section.find(**{ + 'class': 'head-absatz-num' + }).children)[2].strip() + lehrplan[int(num.split('.')[0]) - 1].subsections.append( + LehrplanSection(num, summary, duration)) + + return lehrplan + + +def gen_section_iter(lehrplan): + sections = [] + for s in lehrplan: + if not s.has_subsections(): + sections.append(s) + else: + sections.extend(s.subsections) + + next_section_in = sections[0].duration + cur = 0 + + def _section_iter(weekly_hours, total_hours) -> list[LehrplanSection]: + nonlocal cur + if cur == len(sections) - 1: + return [sections[-1]] + + cur_sections = [] + + nonlocal next_section_in + # advance section + if total_hours > next_section_in: + if total_hours - weekly_hours < next_section_in: + cur_sections.append(sections[cur]) + + cur += 1 + next_section_in += sections[cur].duration + + cur_sections.append(sections[cur]) + + return cur_sections + + return _section_iter + + +def gen_latex_table(weekly_hours: int, lehrplan: Lehrplan, school_holidays, + public_holidays) -> list[str]: + """generate a latex table matching lehrplan section on kalendarweeks""" + + rows: list[str] = [] + + lehrplan.append(LehrplanSection('', 'Puffer', 0)) + + start = school_holidays[0].end + end = school_holidays[-1].start + + holiday_iter = iter(school_holidays[1:]) + next_holiday = next(holiday_iter) + + section_iter = gen_section_iter(lehrplan) + total_hours = 0 + + week_iter = start + timedelta(days=1) + while week_iter < end: + if week_iter > next_holiday.start: + rows.append(( + f'\\rowcolor{{blue!30}}{week_iter.isocalendar().week} & ' + f' \\multicolumn{{2}}{{c}}{{{fmt_holiday(next_holiday)}}} \\\\' + )) + week_iter = next_holiday.end + timedelta(days=1) + next_holiday = next(holiday_iter) + continue + + lp_sections = section_iter(weekly_hours, total_hours) + + lp_level = ' \\& '.join([s.num for s in lp_sections]) + lp_summary = ' \\& '.join([s.summary for s in lp_sections]) + + rows.append( + f'{week_iter.isocalendar().week} & {lp_level} & {lp_summary} \\\\') + + week_iter += timedelta(days=7) + total_hours += weekly_hours + + return rows + + +def print_map(rows: list[str], template: Path, outfile): + with open(template, 'r', encoding='utf-8') as template_file: + template = template_file.readlines() + + template_prefix = template[:-6] + template_suffix = template[-2:] + + print(''.join(template_prefix), file=outfile) + print('\n'.join(rows), file=outfile) + print(''.join(template_suffix), file=outfile) + + +def main(): + """Create a rough map template for a certain school year""" + parser = argparse.ArgumentParser() + parser.add_argument('subject', type=str) + parser.add_argument('form', type=int) + parser.add_argument('--template-dir') + parser.add_argument('--year', default=default_year()) + parser.add_argument('--land', default=BAYERN) + parser.add_argument('-o', '--output', default=sys.stdout) + + args = parser.parse_args() + + school_holidays = get_holidays(args.land, args.year) + public_holidays = get_public_holidays(args.land, school_holidays[0].end, + school_holidays[-1].start) + + weekly_hours = WEEKLY_HOURS[args.subject][args.form] + lehrplan = get_lehrplan(args.subject, str(args.form)) + + rows = gen_latex_table(weekly_hours, lehrplan, school_holidays, + public_holidays) + + if args.template_dir: + template_path = Path(args.template_dir) / TEMPLATE_FILE + else: + template_path = TEMPLATE_DIR / TEMPLATE_FILE + + if args.output != sys.stdout: + with open(args.output, 'w', encoding='utf-8') as outfile: + print_map(rows, template_path, outfile) + else: + print_map(rows, template_path, args.output) + + +if __name__ == '__main__': + main() |
