aboutsummaryrefslogtreecommitdiff
path: root/scripts/gen_map/gen_map.py
diff options
context:
space:
mode:
authorFlorian Fischer <florian.fischer@muhq.space>2023-08-22 15:27:14 +0200
committerFlorian Fischer <florian.fischer@muhq.space>2023-08-22 15:27:14 +0200
commit515ca7fadd11276421ba9d9be217a07b2e6b1652 (patch)
tree58b53b87813a33a04a918741d524d839344913d9 /scripts/gen_map/gen_map.py
parentf44dbadbf41ec784a46a7fb7818d2ff822d1255a (diff)
downloadunterricht-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-xscripts/gen_map/gen_map.py298
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()