diff --git a/.gitignore b/.gitignore index a75a36b..a970621 100644 --- a/.gitignore +++ b/.gitignore @@ -166,4 +166,5 @@ backend/schools/private_data creds.json proxies.json -/test_scripts \ No newline at end of file +/test_scripts +/downloaded_files \ No newline at end of file diff --git a/backend/cache.py b/backend/cache.py index 02421a6..f98fdd7 100644 --- a/backend/cache.py +++ b/backend/cache.py @@ -34,24 +34,6 @@ def remove_plan_file(self, day: datetime.date, timestamp: datetime.datetime | st path = self.get_plan_path(day, timestamp) / filename path.unlink(missing_ok=True) - def store_plan_file_link( - self, - day: datetime.date, - timestamp: datetime.datetime | str, - filename: str, - to_timestamp: datetime.datetime | str, - to_filename: str - ): - """Create a symlink to a plan file in the cache.""" - - path = self.get_plan_path(day, timestamp) / filename - to_path = self.get_plan_path(day, to_timestamp) / to_filename - - path.parent.mkdir(parents=True, exist_ok=True) - - path.unlink(missing_ok=True) - path.symlink_to(to_path) - def get_plan_file(self, day: datetime.date, timestamp: datetime.datetime | str, diff --git a/backend/default_plan.py b/backend/default_plan.py new file mode 100644 index 0000000..7fb71a5 --- /dev/null +++ b/backend/default_plan.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +import copy +import dataclasses +import typing + +from . import models + + +@dataclasses.dataclass +class DefaultPlanInfo: + unchanged_lessons: list[models.Lesson] = dataclasses.field(default_factory=list) + week: int | None = None + + @classmethod + def from_lessons(cls, lessons: typing.Iterable[models.Lesson]) -> DefaultPlanInfo: + out = cls() + + for lesson in lessons: + if len(lesson.parsed_info.paragraphs) != 0: + continue + + if not lesson._is_scheduled or lesson.is_internal or lesson.subject_changed: + continue + + lesson = copy.deepcopy(lesson) + + if lesson.teacher_changed: + lesson.teachers = None + + if lesson.room_changed: + lesson.rooms = None + + if lesson.forms_changed: + lesson.forms = None + assert False, f"{cls.__name__}.from_lessons() only supports lessons from form plans." + + out.unchanged_lessons.append(lesson) + + return out + + def serialize(self): + return { + "unchanged_lessons": [lesson.serialize() for lesson in self.unchanged_lessons], + "week": self.week + } + + @classmethod + def deserialize(cls, data): + return cls( + unchanged_lessons=[models.Lesson.deserialize(lesson) for lesson in data["unchanged_lessons"]], + week=data["week"] + ) diff --git a/backend/lesson_info.py b/backend/lesson_info.py index 3f512c7..34b9649 100644 --- a/backend/lesson_info.py +++ b/backend/lesson_info.py @@ -19,14 +19,16 @@ class _InfoParsers: - _teacher_name = (r"[A-ZÄÖÜ][a-zäöüß]+(?: [A-ZÄÖÜ][a-zäöüß]+(?:-[A-ZÄÖÜ][a-zäöüß]+)?\.?)*" - r"(?: [A-ZÄÖÜ][a-zäöüß]+(?:-[A-ZÄÖÜ][a-zäöüß]+)?)") + _teacher_name = (r"[A-ZÄÖÜ][a-zäöüß]+" + r"(?: (?:[A-ZÄÖÜ]')?[A-ZÄÖÜ][a-zäöüß]+(?:-[A-ZÄÖÜ][a-zäöüß]+)*\.?)*" + r"(?: van)?" + r"(?: (?:[A-ZÄÖÜ]')?[A-ZÄÖÜ][a-zäöüß]+(?:-[A-ZÄÖÜ][a-zäöüß]+)*)") _teacher_abbreviation = r"[A-ZÄÖÜ][A-ZÄÖÜa-zäöüß]{2,}" _teacher = fr"(?:{_teacher_name})|(?:{_teacher_abbreviation})" # teacher a,teacher b _teachers = fr"{_teacher}(?:, ?{_teacher})*" - _course = r"([A-Za-z0-9ÄÖÜäöüß\/-]{2,8})" # maybe be more strict? + _course = r"([A-Za-z0-9ÄÖÜäöüß\/-_]{2,8})" # maybe be more strict? _period = r"St\.(?P(?P\d{1,2})(?:-(?P\d{1,2}))?)" _form = _parse_form_pattern.pattern @@ -71,7 +73,7 @@ class _InfoParsers: # selbst. (v), Aufgaben stehen im LernSax, bitte zu Hause bearbeiten # selbst. (v), Aufgaben stehen im LernSax # selbst. (v), Aufgaben wurden erteilt, bitte zu Hause erledigen - independent = re.compile(rf'selbst\. \(.\)') + independent = re.compile(rf'selbst\.(?: \(.\))?') tasks_in_lernsax = re.compile(rf'Aufgaben stehen im LernSax') tasks_were_given = re.compile(rf'Aufgaben wurden erteilt') @@ -507,6 +509,7 @@ def _parse_message(info: str, lesson: models.Lesson, plan_type: typing.Literal[" ) -> ParsedLessonInfoMessage: info = info.strip() info = re.sub(r"(?<=\w)/ ", "/", info) # remove spaces after slashes like in G/ R/ W + info = re.sub(r"\b[´`]\b", "'", info) if plan_type == "forms": parsed_info, match = _parse_form_plan_message(info, lesson) @@ -699,8 +702,8 @@ def extract_teachers(lesson: models.Lesson, classes: dict[str, models.Class], *, surname = next(iter(message.parsed._teachers)) course = message.parsed.course + # TODO: Better discrimination criteria between teacher surname and abbreviation if len(surname.split()) == 1: - logger.debug(f"Skipping teacher \"surname\" {surname!r}.") continue _class: dict[str, models.Class] = { @@ -764,7 +767,8 @@ def process_additional_info_line(text: str, parsed_existing_forms: list[ParsedFo return [] # TODO: Dates, Rooms # remove spaces after slashes like in 5/ 3 - text = re.sub(r"(?<=\w)/ {1,3}", "/", text.strip()) + text = re.sub(r"\b/ {1,3}\b", "/", text.strip()) + text = re.sub(r"\b {1,3}\b", " ", text.strip()) funcs = ( lambda s: add_fuzzy_teacher_links(s, teacher_abbreviation_by_surname, date), @@ -829,7 +833,12 @@ def validator(match: re.Match) -> list[LessonInfoTextSegment] | None: form_match = existing_form break elif MajorMinorParsedForm == type(parsed_form) == type(existing_form): - if existing_form[0].lower() == parsed_form[0].lower() and existing_form[2] == parsed_form[2]: + try: + is_match = int(existing_form[0]) == int(parsed_form[0].lower()) + except ValueError: + is_match = existing_form[0].lower() == parsed_form[0].lower() + + if is_match and existing_form[2] == parsed_form[2]: form_match = existing_form break diff --git a/backend/load_plans.py b/backend/load_plans.py index 564f3f0..26b777b 100644 --- a/backend/load_plans.py +++ b/backend/load_plans.py @@ -83,7 +83,7 @@ async def get_clients(session: aiohttp.ClientSession | None = None, # create crawler p = PlanCrawler(plan_downloader, plan_processor) - clients |= {school_name: p} + clients[school_name] = p return clients diff --git a/backend/models.py b/backend/models.py index fae798d..ac242f1 100644 --- a/backend/models.py +++ b/backend/models.py @@ -86,24 +86,45 @@ def create_internal(cls, date: datetime.date, plan_type: typing.Literal["forms", ) def serialize(self) -> dict: + assert len(self.parsed_info.paragraphs) == 0 return { "periods": sorted(self.periods), + "begin": self.begin.strftime("%H:%M") if self.begin else None, + "end": self.end.strftime("%H:%M") if self.end else None, "forms": sorted(self.forms), "teachers": sorted(self.teachers) if self.teachers is not None else None, "rooms": sorted(self.rooms) if self.rooms is not None else None, "course": self.course, - "begin": self.begin.strftime("%H:%M") if self.begin else None, - "end": self.end.strftime("%H:%M") if self.end else None, "subject_changed": self.subject_changed, "teacher_changed": self.teacher_changed, "room_changed": self.room_changed, + "forms_changed": self.forms_changed, "takes_place": self.takes_place, "is_internal": self.is_internal, - "class_data": repr(self.class_), - "info": self.parsed_info.serialize(self._lesson_date), - "origin_plan_type": self._origin_plan_type, + "_origin_plan_lesson_id": self._origin_plan_lesson_id, } + @classmethod + def deserialize(cls, data): + return cls( + periods=set(data["periods"]), + begin=datetime.datetime.strptime(data["begin"], "%H:%M").time() if data["begin"] else None, + end=datetime.datetime.strptime(data["end"], "%H:%M").time() if data["end"] else None, + forms=set(data["forms"]) if data["forms"] is not None else None, + teachers=set(data["teachers"]) if data["teachers"] is not None else None, + rooms=set(data["rooms"]) if data["rooms"] is not None else None, + course=data["course"], + parsed_info=ParsedLessonInfo([]), + class_=None, + subject_changed=data["subject_changed"], + teacher_changed=data["teacher_changed"], + room_changed=data["room_changed"], + forms_changed=data["forms_changed"], + takes_place=data["takes_place"], + is_internal=data["is_internal"], + _origin_plan_lesson_id=data["_origin_plan_lesson_id"], + ) + @dataclasses.dataclass class ClassData(indiware_mobil.Class): @@ -300,28 +321,53 @@ def create(cls, taking_place_lesson: Lesson | None, not_taking_place_lesson: Les @dataclasses.dataclass class Lessons: - lessons: list[Lesson] + lessons: list[Lesson] = dataclasses.field(default_factory=list) + + _T = typing.TypeVar("_T") - def group_by(self, *attributes: str, include_none: bool = False) -> dict[str, Lessons]: - grouped_i = defaultdict(set) + def group_by_key(self, key: typing.Callable[[Lesson], typing.Iterable[_T]]) -> dict[_T, Lessons]: + lesson_i_by_category = defaultdict(set) for lesson_i, lesson in enumerate(self.lessons): + for category in key(lesson): + lesson_i_by_category[category].add(lesson_i) + + return {category: Lessons([self.lessons[i] for i in indices]) + for category, indices in lesson_i_by_category.items()} + + @typing.overload + def group_by(self, *attributes: tuple[str, ...], include_none: bool = False) -> dict[tuple[typing.Any, ...], Lessons]: + ... + + def group_by(self, *attributes: str, include_none: bool = False) -> dict[typing.Any, Lessons]: + def key(lesson: Lesson) -> list: + out = [] for attribute in attributes: - value = getattr(lesson, attribute) + if isinstance(attribute, tuple): + category = [] + for attr in attribute: + value = getattr(lesson, attr) + + if isinstance(value, (list, set)): + value = tuple(value) + + category.append(value) + + out.append(tuple(category)) + else: + value = getattr(lesson, attribute) - if not include_none and value is None: - continue + if not include_none and value is None: + continue - if not isinstance(value, (list, set)): - value = [value] + if not isinstance(value, (list, set)): + value = [value] - for element in value: - grouped_i[element].add(lesson_i) + out += value - grouped = {attribute: [self.lessons[i] for i in indices] for attribute, indices in grouped_i.items()} + return out - return {attribute: Lessons(sorted(lessons, key=lambda x: list(x.periods)[0])) - for attribute, lessons in grouped.items()} + return self.group_by_key(key=key) @staticmethod def _to_plan_lessons(lessons: list[Lesson], plan_type: typing.Literal["forms", "teachers", "rooms"], @@ -349,6 +395,7 @@ def _to_plan_lessons(lessons: list[Lesson], plan_type: typing.Literal["forms", " l.course if l.course else "", )) + # TODO: potentially multiple NTP lessons per TPL lesson bc they get grouped for taking_place_lesson in lessons: if not taking_place_lesson.takes_place: continue @@ -365,9 +412,9 @@ def _to_plan_lessons(lessons: list[Lesson], plan_type: typing.Literal["forms", " # @formatter:off is_match = ( (taking_place_lesson.course != not_taking_place_lesson.course) in ((True, False) if taking_place_lesson._origin_plan_type != plan_type and taking_place_lesson.subject_changed else (taking_place_lesson.subject_changed,)) - and (True if taking_place_lesson._origin_plan_type != plan_type and taking_place_lesson.teacher_changed else (not taking_place_lesson.teachers.issuperset(not_taking_place_lesson.teachers or set())) == taking_place_lesson.teacher_changed) - and (True if taking_place_lesson._origin_plan_type != plan_type and taking_place_lesson.room_changed else (not taking_place_lesson.rooms.issuperset(not_taking_place_lesson.rooms or set())) == taking_place_lesson.room_changed) - and (True if taking_place_lesson._origin_plan_type != plan_type and taking_place_lesson.forms_changed else (not taking_place_lesson.forms.issuperset(not_taking_place_lesson.forms or set())) == taking_place_lesson.forms_changed) + and (True if taking_place_lesson.teachers is None or taking_place_lesson._origin_plan_type != plan_type and taking_place_lesson.teacher_changed else (not taking_place_lesson.teachers.issuperset(not_taking_place_lesson.teachers or set())) == taking_place_lesson.teacher_changed) + and (True if taking_place_lesson.rooms is None or taking_place_lesson._origin_plan_type != plan_type and taking_place_lesson.room_changed else (not taking_place_lesson.rooms.issuperset(not_taking_place_lesson.rooms or set())) == taking_place_lesson.room_changed) + and (True if taking_place_lesson.forms is None or taking_place_lesson._origin_plan_type != plan_type and taking_place_lesson.forms_changed else (not taking_place_lesson.forms.issuperset(not_taking_place_lesson.forms or set())) == taking_place_lesson.forms_changed) ) # @formatter:on @@ -397,7 +444,6 @@ def _to_plan_lessons(lessons: list[Lesson], plan_type: typing.Literal["forms", " def to_plan_lessons(self, plan_type: typing.Literal["forms", "teachers", "rooms"], plan_value: set[str] ) -> list[PlanLesson]: - lessons_by_periods: dict[frozenset[int], list[Lesson]] = defaultdict(list) for lesson in self: lessons_by_periods[frozenset(lesson.periods)].append(lesson) @@ -477,42 +523,41 @@ def _group_lesson_info( out.sort_original() return out - def group_blocks_and_lesson_info(self, plan_type: typing.Literal["forms", "teachers", "rooms"]) -> Lessons: + def group_blocks_and_lesson_info(self, origin_plan_type: typing.Literal["forms", "teachers", "rooms"]) -> Lessons: assert all(len(x.periods) <= 1 for x in self.lessons), \ "Lessons must be ungrouped. (Must only have one period.)" - if plan_type == "forms": + if origin_plan_type == "forms": sort_key = lambda x: ( x.takes_place, - x.course or "", - x.teachers or set(), - x.rooms or set(), + x.course or x.class_opt.group or "", + tuple(x.rooms or set()), + tuple(x.teachers or set()), x.parsed_info.lesson_group_sort_key(), - x.class_opt.group or "", - x.forms or set(), - x.periods or set(), + tuple(x.forms or set()), + tuple(x.periods or set()), ) - elif plan_type == "teachers": + elif origin_plan_type == "teachers": sort_key = lambda x: ( x.takes_place, x.course or "", - x.rooms or set(), - x.forms or set(), + tuple(x.rooms or set()), + tuple(x.forms or set()), x.parsed_info.lesson_group_sort_key(), - x.teachers or set(), - x.periods or set(), + tuple(x.teachers or set()), + tuple(x.periods or set()), ) - elif plan_type == "rooms": + elif origin_plan_type == "rooms": sort_key = lambda x: ( x.takes_place, x.course or "", - x.teachers or set(), - x.forms or set(), + tuple(x.teachers or set()), + tuple(x.forms or set()), x.parsed_info.lesson_group_sort_key(), - x.rooms or set(), + tuple(x.rooms or set()), x.periods or set(), ) else: @@ -523,13 +568,15 @@ def group_blocks_and_lesson_info(self, plan_type: typing.Literal["forms", "teach grouped: list[Lesson] = [] for lesson in sorted_lessons: + assert lesson._origin_plan_type == origin_plan_type + for previous_lesson in grouped[-1:-4:-1]: can_get_grouped = ( lesson.course == previous_lesson.course and lesson.takes_place == previous_lesson.takes_place ) - for remaining_plan_value in {"forms", "teachers", "rooms"} - {plan_type}: + for remaining_plan_value in {"forms", "teachers", "rooms"} - {origin_plan_type}: can_get_grouped &= ( getattr(lesson, remaining_plan_value) == getattr(previous_lesson, remaining_plan_value) ) @@ -544,14 +591,15 @@ def group_blocks_and_lesson_info(self, plan_type: typing.Literal["forms", "teach can_get_grouped &= previous_lesson_block == current_lesson_block if can_get_grouped: - if plan_type == "forms": + if origin_plan_type == "forms": previous_lesson.forms |= lesson.forms - elif plan_type == "teachers": + elif origin_plan_type == "teachers": previous_lesson.teachers |= lesson.teachers - elif plan_type == "rooms": + elif origin_plan_type == "rooms": previous_lesson.rooms |= lesson.rooms else: raise NotImplementedError + previous_lesson.parsed_info = grouped_additional_info previous_lesson.periods |= lesson.periods previous_lesson.begin = min(filter(lambda x: x, (previous_lesson.begin, lesson.begin)), @@ -655,7 +703,7 @@ def from_form_plan(cls, form_plan: indiware_mobil.IndiwareMobilPlan) -> Plan: for paragraph in current_lesson.parsed_info.paragraphs: for moved_to_message in paragraph.messages: - if isinstance(moved_to_message.parsed, lesson_info.MovedTo): + if isinstance(moved_to_message.parsed, (lesson_info.MovedTo, lesson_info.MovedFrom)): # if lessons are moved, class data no longer represents the scheduled lesson class_data = None current_lesson.class_ = None diff --git a/backend/plan_extractor.py b/backend/plan_extractor.py index d814b0b..0a1b350 100644 --- a/backend/plan_extractor.py +++ b/backend/plan_extractor.py @@ -8,7 +8,7 @@ from stundenplan24_py import indiware_mobil, substitution_plan -from . import lesson_info +from . import lesson_info, default_plan from .lesson_info import process_additional_info from .teacher import Teacher, Teachers from .models import Lesson, Lessons, Plan @@ -147,7 +147,7 @@ def add_lessons_for_unavailable_from_subst_plan(self): info = f"{teacher_name}{' den ganzen Tag' if not periods else ''} abwesend laut Vertretungsplan" lesson = Lesson.create_internal(self.plan.indiware_plan.date) lesson.periods = {period} - lesson.current_teachers = {teacher_abbreviation} + lesson.teachers = {teacher_abbreviation} lesson.info = info lesson.parsed_info = lesson_info.create_literal_parsed_info(info) self.plan.lessons.lessons.append(lesson) @@ -159,8 +159,8 @@ def add_lessons_for_unavailable_from_subst_plan(self): info = f"Raum {room}{' den ganzen Tag' if not periods else ''} nicht verfügbar laut Vertretungsplan" lesson = Lesson.create_internal(self.plan.indiware_plan.date) lesson.periods = {period} - lesson.current_rooms = {room_str} - lesson.current_course = "Belegt" + lesson.rooms = {room_str} + lesson.course = "Belegt" lesson.info = info lesson.parsed_info = lesson_info.create_literal_parsed_info(info) self.plan.lessons.lessons.append(lesson) @@ -172,12 +172,15 @@ def add_lessons_for_unavailable_from_subst_plan(self): info = f"Klasse {form}{' den ganzen Tag' if not periods else ''} abwesend laut Vertretungsplan" lesson = Lesson.create_internal(self.plan.indiware_plan.date) lesson.periods = {period} - lesson.current_forms = {form} + lesson.forms = {form} lesson.info = info lesson.parsed_info = lesson_info.create_literal_parsed_info(info) self.plan.lessons.lessons.append(lesson) + def default_plan(self) -> default_plan.DefaultPlanInfo: + return default_plan.DefaultPlanInfo.from_lessons(self.plan.lessons) + class SubPlanExtractor: def __init__(self, forms_plan: Plan, plan_type: typing.Literal["forms", "rooms", "teachers"], @@ -187,14 +190,13 @@ def __init__(self, forms_plan: Plan, plan_type: typing.Literal["forms", "rooms", self.forms_lessons_grouped = ( forms_plan.lessons .filter_plan_type_messages(plan_type) - .group_blocks_and_lesson_info("forms") + .group_blocks_and_lesson_info(origin_plan_type="forms") ) if self.plan_type in ("rooms", "teachers"): self.forms_lessons_grouped = self.forms_lessons_grouped.filter(lambda l: not l.is_internal) self.resolve_teachers_in_lesson_info(teacher_abbreviation_by_surname) - self.extrapolate_lesson_times(self.forms_lessons_grouped) def resolve_teachers_in_lesson_info(self, teacher_abbreviation_by_surname: dict[str, str]): diff --git a/backend/plan_processor.py b/backend/plan_processor.py index 04a3e0e..32c87d0 100644 --- a/backend/plan_processor.py +++ b/backend/plan_processor.py @@ -15,7 +15,7 @@ class PlanProcessor: - VERSION = "91" + VERSION = "98" def __init__(self, cache: Cache, school_number: str, *, logger: logging.Logger): self._logger = logger @@ -85,6 +85,12 @@ def compute_plans(self, date: datetime.date, timestamp: datetime.datetime): }, default=PlanLesson.serialize), "plans.json" ) + self.cache.store_plan_file( + date, timestamp, + json.dumps(students_plan_extractor.default_plan().serialize()), + "_default_plan.json" + ) + self.cache.store_plan_file( date, timestamp, json.dumps(students_plan_extractor.form_plan_extractor.grouped_form_plans(), diff --git a/backend/schools/school_utils.py b/backend/schools/school_utils.py new file mode 100644 index 0000000..031dbb9 --- /dev/null +++ b/backend/schools/school_utils.py @@ -0,0 +1,41 @@ +def is_kuerzel_in_name(name, kuerzel): + name = name.lower() + abbreviation = kuerzel.lower() + if abbreviation[0] != name[0]: + return False + name_index = 0 + abbreviation_index = 0 + while abbreviation_index < len(abbreviation): + char = abbreviation[abbreviation_index] + char_index = name.find(char, name_index) + if char_index == -1: + return False + name_index = char_index + 1 + abbreviation_index += 1 + return True + + +def names_kuerzel(kuerzels, names): + names_match = {} + for name in names: + names_match[name] = [] + for kuerzel in kuerzels: + if is_kuerzel_in_name(name.split(" ")[-1], kuerzel): + names_match[name].append(kuerzel) + # problem: the kuerzel seem very indefinitive... + """# error detection should be implemented here (e.g. 2 names that both only have 1 valid kuerzel) + definite_kuerzel = [] + for name, values in names_match.items(): + if len(values) == 0: + names_match[name] = None + if len(values) == 1: + if values[0] not in definite_kuerzel: + definite_kuerzel.append(values[0]) + else: + print(f"conflict for kuerzel {values[0]}") + names_match[name] = values[0] + # for name, values in names_match.items(): + # if type(values) == list: + # names_match[name] = [value for value in values if value not in definite_kuerzel] + """ + return names_match diff --git a/backend/vplan_utils.py b/backend/vplan_utils.py index 7927b71..3591dd8 100644 --- a/backend/vplan_utils.py +++ b/backend/vplan_utils.py @@ -93,13 +93,17 @@ def form_sort_key(major: str | None): return float("inf"), 0, major -def group_forms(forms: list[str]) -> dict[str, list[str]]: +def group_forms(forms: list[str]) -> dict[str | None, list[str]]: groups: dict[str | None, list[str]] = defaultdict(list) - for form in forms: - group_name, *_ = ParsedForm.from_str(form) + for form_str in forms: + form = ParsedForm.from_str(form_str) + if isinstance(form, MajorMinorParsedForm): + group_name = form.major + else: + group_name = None - groups[group_name].append(form) + groups[group_name].append(form_str) return {k: v for k, v in sorted(groups.items(), key=lambda x: form_sort_key(x[0]))} diff --git a/client/src/App.svelte b/client/src/App.svelte index bbf8c4d..7e4c3c8 100644 --- a/client/src/App.svelte +++ b/client/src/App.svelte @@ -6,17 +6,17 @@ import Navbar from "./components/Navbar.svelte"; import Settings from "./components/Settings.svelte"; import AboutUs from "./components/AboutUs.svelte"; + import Favourites from "./components/Favourites.svelte"; import SveltyPicker from 'svelty-picker'; - import {get_settings, group_rooms, update_colors, navigate_page, init_indexed_db, clear_plan_cache} from "./utils.js"; + import {get_settings, group_rooms, update_colors, navigate_page, init_indexed_db, clear_plan_cache, get_favourites} from "./utils.js"; import {notifications} from './notifications.js'; - import {logged_in, title, current_page, preferences, settings, active_modal, pwa_prompt, indexed_db} from './stores.js' - import {customFetch, format_revision_date} from "./utils.js"; + import {logged_in, title, current_page, settings, active_modal, pwa_prompt, indexed_db, selected_favourite, favourites} from './stores.js' import SchoolManager from "./components/SchoolManager.svelte"; - import Preferences from "./components/Preferences.svelte"; import Changelog from "./components/Changelog.svelte"; import Select from "./base_components/Select.svelte"; import Contact from "./components/Contact.svelte"; import Impressum from "./components/Impressum.svelte"; + import {customFetch, format_revision_date, load_meta} from "./utils.js"; import {de} from 'svelty-picker/i18n'; import PwaInstallHelper from "./components/PWAInstallHelper.svelte"; import Dropdown from "./base_components/Dropdown.svelte"; @@ -83,43 +83,22 @@ revision_arr = []; } - function get_meta() { - let data_from_cache = false; - let data = localStorage.getItem(`${school_num}_meta`); - if (data !== "undefined" && data) { - data = JSON.parse(data); - meta = data; - all_rooms = data.rooms; - teacher_list = Object.keys(data.teachers); - grouped_forms = data.forms.grouped_forms; - enabled_dates = Object.keys(data.dates); - if(!date) { - date = data.date; - } - course_lists = data.forms.forms; - data_from_cache = true; - } - customFetch(`${api_base}/meta`) + function get_meta(tmp_school_num) { + load_meta(tmp_school_num) .then(data => { - // console.log("Meta geladen"); - try { - localStorage.setItem(`${school_num}_meta`, JSON.stringify(data)); - } catch (error) { - if (error.name === 'QuotaExceededError' ) { - notifications.danger("Die Schulmetadaten konnten nicht gecached werden.") - } else { - throw error; - } + if (!data) { + return } - meta = data; - all_rooms = data.rooms; - teacher_list = Object.keys(data.teachers); - grouped_forms = data.forms.grouped_forms; - enabled_dates = Object.keys(data.dates); + meta = data[0]; + all_rooms = meta.rooms; + teacher_list = Object.keys(meta.teachers); + grouped_forms = meta.forms.grouped_forms; + enabled_dates = Object.keys(meta.dates); if(!date) { - date = data.date; + date = meta.date; } - course_lists = data.forms.forms; + course_lists = meta.forms.forms; + //data_from_cache = data[1]; }) .catch(error => { if (data_from_cache) { @@ -146,16 +125,6 @@ ); } - function get_preferences() { - customFetch(`${api_base}/preferences`) - .then(data => { - preferences.set(data); - }) - .catch(error => { - console.error("Preferences konnten nicht geladen werden."); - }) - } - function choose(choices) { var index = Math.floor(Math.random() * choices.length); return choices[index]; @@ -229,7 +198,7 @@ for(let form of forms) { converted_forms.push({"id": form, "display_name": form}); } - form_arr.push([form_group, converted_forms]); + form_arr.push([form_group !== "null" ? form_group : "Sonstige", converted_forms]); } } @@ -254,7 +223,7 @@ function gen_revision_arr(all_revisions) { revision_arr = []; for(const [index, revision] of Object.entries(all_revisions)) { - if(index == 1) {continue;} + if (index == 1) {continue;} revision_arr.push({ "id": revision, "display_name": format_revision_date(revision, all_revisions[1]) @@ -293,25 +262,60 @@ plan_type = decodeURI(tmp_variables[3]); plan_value = decodeURI(tmp_variables[4]); } - } + } + + function select_plan(favourites, selected_favourite) { + // check if selected_favourite is in favourites (selected_favourite is the index) + if (selected_favourite !== -1 && favourites[selected_favourite]) { + selected_favourite = favourites[selected_favourite]; + school_num = selected_favourite.school_num; + localStorage.setItem('school_num', school_num); + plan_type = selected_favourite.plan_type; + plan_value = selected_favourite.plan_value; + selected_form = null; + selected_teacher = null; + selected_room = null; + } + } + + $: select_plan($favourites, $selected_favourite); + function reset_favourite() { + selected_favourite.set(-1); + } + // reset favourite when selecting new thing + $: selected_form && reset_favourite(); + $: selected_teacher && reset_favourite(); + $: selected_room && reset_favourite(); + $: if ($selected_favourite !== -1) { + // check if selected_favourite is in favourites + if ($favourites.length <= $selected_favourite) { + selected_favourite.set(-1); + } + } + // CHANGE THIS FOR SETTING IF FIRST FAVOURITE SHOULD BE SELECTED + /*$: if ($favourites.length !== 0 && $selected_favourite === -1) { + selected_favourite.set(0); + }*/ + $logged_in = localStorage.getItem('logged_in') === 'true'; init_vars(); check_login_status(); refresh_plan_vars(); + get_favourites(); $: $logged_in && init_indexed_db(); $: !$logged_in && logout(); $: school_num && (api_base = `/api/v69.420/${school_num}`); - $: school_num && get_meta(); + $: get_meta(school_num); $: all_revisions = [".newest"].concat((meta?.dates || {})[date] || []); - $: school_num && get_preferences(); + //$: school_num && get_preferences(); $: all_rooms && (grouped_rooms = group_rooms(all_rooms)); $: $logged_in && get_settings(); $: (Object.keys($settings).length !== 0) && localStorage.setItem("settings", `${JSON.stringify($settings)}`); $: update_colors($settings); $: $logged_in && get_greeting(); - + $: selected_form && set_plan("forms", selected_form); $: gen_form_arr(grouped_forms); $: selected_teacher && set_plan("teachers", selected_teacher); @@ -340,7 +344,7 @@ } if(new_location === "") {new_location = "plan";} navigate_page(new_location); - if(new_location.startsWith("plan")) { + if (new_location.startsWith("plan")) { refresh_plan_vars(); } }); @@ -362,7 +366,6 @@ -
@@ -372,9 +375,15 @@ {:else if $current_page === "impressum"} + {:else if $current_page === "favourites"} + {:else if $logged_in} {#if $current_page.substring(0, 4) === "plan" || $current_page === "weekplan"}

{emoji} {greeting}

+ {#if $selected_favourite !== -1 && $favourites[$selected_favourite]} + Gewählter Favorit: {$favourites[$selected_favourite].name} +
+ {/if}
@@ -410,7 +419,11 @@
diff --git a/client/src/components/Favourites.svelte b/client/src/components/Favourites.svelte new file mode 100644 index 0000000..91fd773 --- /dev/null +++ b/client/src/components/Favourites.svelte @@ -0,0 +1,296 @@ + + + + +




+{#each cur_favourites as _, favourite} +

+ + + + + + {#if cur_favourites[favourite].plan_type === "forms"} + + + {#each Object.entries( + get_subjects(favourite, all_meta) + ).sort(([subj1, _], [subj2, __]) => subj1.localeCompare(subj2)).sort(([_, courses1], [__, courses2]) => courses2.length - courses1.length) as [subject, courses]} + {#if courses.length === 1} +

  • {subject}: + {courses[0].class_number} + {courses[0].teacher} | + {courses[0].subject} + {#if courses[0].group != null} + ({courses[0].group}) + {/if} +
  • + {:else} +
  • + {subject} + {#if courses.length > 2} + + + {/if} +
  • +
      + {#each courses as course} +
    • + + {course.class_number} + {course.teacher} | + {course.subject} + {#if course.group != null} + ({course.group}) + {/if} +
    • + {/each} +
    + {/if} + {/each} + + + + {:else if cur_favourites[favourite].plan_type !== "room_overview"} + + {/if} + +

    +{/each} + + + \ No newline at end of file diff --git a/client/src/components/Navbar.svelte b/client/src/components/Navbar.svelte index f6e8408..85046b1 100644 --- a/client/src/components/Navbar.svelte +++ b/client/src/components/Navbar.svelte @@ -4,6 +4,7 @@ import Dropdown from '../base_components/Dropdown.svelte'; import { fly } from 'svelte/transition'; import {customFetch, navigate_page} from "../utils.js"; + import {selected_favourite, favourites} from "../stores.js"; function logout() { customFetch('/auth/logout') @@ -17,11 +18,34 @@
    @@ -394,11 +405,11 @@
    \ No newline at end of file diff --git a/client/src/components/SchoolManager.svelte b/client/src/components/SchoolManager.svelte index e129373..3e55927 100644 --- a/client/src/components/SchoolManager.svelte +++ b/client/src/components/SchoolManager.svelte @@ -13,6 +13,7 @@ export let plan_value; let authorize_school_id; + let authorize_school_data = {}; let username = "schueler"; let password = ""; let schools = []; @@ -25,6 +26,12 @@ let school_id_arr = []; let password_visible = false; + let school_add_visible = false; + let add_school_name = ""; + let add_school_num = ""; + let add_school_username = ""; + let add_school_password = ""; + function isObjectInList(object, list) { return list.some(item => item.toString() === object.toString()); } @@ -68,18 +75,22 @@ function authorize_school() { get_authorized_schools(); - console.log(username, password); if (!isObjectInList(authorize_school_id, school_id_arr)) { notifications.danger("Schule unbekannt (kontaktiere uns, um deine Schule hinzuzufügen)") return } - if (username === "") { - notifications.danger("Bitte gib einen Nutzernamen an"); - return - } - if (password === "") { - notifications.danger("Bitte gib ein Passwort an"); - return + if (authorize_school_data.creds_needed) { + if (username === "") { + notifications.danger("Bitte gib einen Nutzernamen an"); + return + } + if (password === "") { + notifications.danger("Bitte gib ein Passwort an"); + return + } + } else { + username = ""; + password = ""; } let formData = new FormData(); formData.append('username', username); @@ -107,13 +118,17 @@ ); } - function get_school_name_by_id(school_id) { + function get_school_by_id(school_id) { + if (!school_id) { + return {}; + } for (let school of schools) { + console.log(school); if (school.id === school_id.toString()) { - return school.display_name; + return school } } - return ""; + return {}; } get_schools(); @@ -132,10 +147,37 @@ ["Autorisiert", authorized_schools], ["Unautorisiert", unauthorized_schools] ]; + $: authorize_school_data = get_school_by_id(authorize_school_id); + + function add_school() { + console.log(add_school_name, add_school_num, add_school_username, add_school_password); + + let formData = new FormData(); + formData.append('display_name', add_school_name); + formData.append('school_num', add_school_num); + formData.append('username', add_school_username); + formData.append('pw', add_school_password); + let tmp_api_base = `/api/v69.420/${authorize_school_id}`; + customFetch(`/api/v69.420/add_school`, { + method: 'POST', + body: formData + }) + .then(data => { + notifications.success(data); + school_add_visible = false; + add_school_name = ""; + add_school_num = ""; + add_school_username = ""; + add_school_password = ""; + }) + .catch(error => { + notifications.danger(error.message); + }); + }
    - {#if !school_auth_visible} + {#if !school_auth_visible && !school_add_visible}
    { if (authorize_school_id) { if(isObjectInList(authorize_school_id, authorized_school_ids) || is_admin) { @@ -168,29 +210,45 @@ }}>Schule autorisieren login {/if}
    - {:else} + {:else if !school_add_visible}
    -

    {authorize_school_id ? get_school_name_by_id(authorize_school_id) : "Schul"}-Login

    - Trage hier die Zugangsdaten für deine Schule ein, nicht die deines Accounts.
    (dieselben wie in der
    VpMobil24-App
    )
    - -
    - User Icon - -
    - -
    - Lock Icon - - {password = event.target.value}} type={password_visible ? "text" : "password"} required class="textfield" placeholder="Schul-Passwort"/> -
    +

    {authorize_school_id ? authorize_school_data.display_name : "Schul"}-Login

    + {#if authorize_school_data.creds_needed} + Trage hier die Zugangsdaten für deine Schule ein, nicht die deines Accounts.
    (dieselben wie in der
    VpMobil24-App
    )
    + +
    + User Icon + +
    + +
    + Lock Icon + + {password = event.target.value}} type={password_visible ? "text" : "password"} required class="textfield" placeholder="Schul-Passwort"/> +
    + {:else} + (Für diese Schule benötigst du keine Zugangsdaten) + {/if}
    + {:else} +
    + + + + + + +
    {/if} +