From 2e1490d98f2e99c3aa9a5a3c1a9cc4fb70373852 Mon Sep 17 00:00:00 2001 From: Hartmut Seichter Date: Mon, 17 Mar 2025 20:47:27 +0100 Subject: [PATCH] some initial testdata again --- coursebuilder/app.py | 83 +++++-- coursebuilder/schema.py | 163 ++++++++----- test/fields.yaml | 38 +-- test/maacs/v2/3dcc/{de.yaml => lang.de.yaml} | 5 +- test/maacs/v2/3dcc/{en.yaml => lang.en.yaml} | 2 - test/maacs/v2/3dcc/mod.yaml | 11 +- test/maacs/v2/maacs.yaml | 11 + test/maacs/v2/schema.yaml | 231 +++++++++++++++++++ 8 files changed, 448 insertions(+), 96 deletions(-) rename test/maacs/v2/3dcc/{de.yaml => lang.de.yaml} (94%) rename test/maacs/v2/3dcc/{en.yaml => lang.en.yaml} (99%) create mode 100644 test/maacs/v2/schema.yaml diff --git a/coursebuilder/app.py b/coursebuilder/app.py index 144f8c5..1b1b57b 100644 --- a/coursebuilder/app.py +++ b/coursebuilder/app.py @@ -2,29 +2,67 @@ # coursebuilder # +from typing import Any from argparse import ArgumentParser from pathlib import Path import yaml +from coursebuilder.schema import Schema + + +class LanguageSelector: + context_lang: str + + class Course: - def + i18n_name: str = "lang.{}.yaml" + mod_name: str = "mod.yaml" + + def __init__(self, *, path: Path) -> None: + with open(path) as f: + # resolve rest of bundle + self.__data = yaml.load(f, Loader=yaml.Loader) + # load i18n overlays + self.__i18n = { + f"{str(p).split('.')[1]}": yaml.load(open(p), Loader=yaml.Loader) + for p in path.parent.glob(Course.i18n_name.format("*")) + } + + def validate(self, *, schema: Schema, lang: str) -> None: + print(self.__data) + pass + + def __getitem__(self, name: str, /) -> Any: + return ( + self.__data[name] + if name in self.__data.keys() + else self.__i18n[LanguageSelector.context_lang][name] + ) + + def __getattr__(self, name: str, /) -> Any: + return self.__data[name] + + def __str__(self): + return f"data:{self.__data}\ni18n:{self.__i18n}" class StudyCourse: - def __init__(self, data: dict | None, path: Path | None): - self.path: Path | None = path - self.data: dict | None = data + def __init__(self, *, path: Path) -> None: + self.path = path - courses: list[Course] = [] - - @staticmethod - def load(*, path: str): with open(path) as f: - data = yaml.load(f, Loader=yaml.Loader) - return StudyCourse(data=data, path=Path(path)) + self.__data = yaml.load(f, Loader=yaml.Loader) + + self.courses: dict[str, Course] = { + f"{c}": Course(path=self.path.parent / c / Course.mod_name) + for c in self.__data["courses"] + } + + def __getattr__(self, name: str, /) -> Any: + return self.__data[name] if self.__data else None def __str__(self): - return f"path: {self.path}\ndata: {self.data}" + return f"path: {self.path}\ndata: {self.__data}" def main(): @@ -36,16 +74,31 @@ def main(): "-i", "--input", type=str, - help="folder with project data", + help="course file with definition of the course", + ) + + parser.add_argument( + "-s", + "--schema", + type=str, + help="schema to validate against", ) # get arguments args = parser.parse_args() # just input - if args.input: - sc = StudyCourse.load(path=(Path(".") / args.input).absolute()) - print(sc) + if args.input and args.schema: + with open(args.schema) as f_schema: + schema = Schema(schema=yaml.load(f_schema, Loader=yaml.Loader)) + sc = StudyCourse(path=(Path(".") / args.input).absolute()) + + LanguageSelector.context_lang = "de" + + for k in schema.keys(): + print(k) + for shortcode, course in sc.courses.items(): + print(course[k]) # run as main diff --git a/coursebuilder/schema.py b/coursebuilder/schema.py index 9dfeda5..64e61d1 100644 --- a/coursebuilder/schema.py +++ b/coursebuilder/schema.py @@ -1,8 +1,5 @@ -import string - class Schema: - - def __init__(self,schema) -> None: + def __init__(self, *, schema: dict) -> None: self.__schema = schema def __getitem__(self, field): @@ -10,58 +7,83 @@ class Schema: def keys(self): return self.__schema.keys() - - def is_translatable(self,field): - if 'translatable' in self.__schema[field]: - return self.__schema[field]['translatable'] - else: - return True - def needs_spec(self,field): - if 'spec' in self.__schema[field]: + def is_translatable(self, field): + if "translatable" in self.__schema[field]: + return self.__schema[field]["translatable"] + else: + return True + + def needs_spec(self, field): + if "spec" in self.__schema[field]: return self.__schema[field] else: return False - - def get_value(self,meta,field,lang): + + def get_value(self, meta: dict, field: str, lang: str): """ - treats receiving the value like a variant, + treats receiving the value like a variant, returns values with their language specific representations """ - match self.__schema[field]['type']: - case 'str': return meta[field][lang] if self.is_translatable(field) else meta[field]['value'] - case 'enum' | 'int' | 'num' | 'multikey' : return meta[field]['value'] - case 'multinum': return meta[field]['value'] if hasattr(meta[field]['value'],'__iter__') else [meta[field]['value'],] # force list! - - def to_list_of_dict(self,meta,fields,lang): + match self.__schema[field]["type"]: + case "str": + return ( + meta[field][lang] + if self.is_translatable(field) + else meta[field]["value"] + ) + case "enum" | "int" | "num" | "multikey": + return meta[field]["value"] + case "multinum": + return ( + meta[field]["value"] + if hasattr(meta[field]["value"], "__iter__") + else [ + meta[field]["value"], + ] + ) # force list! + + def to_list_of_dict(self, meta, fields, lang): """ - generates a list of dict which can easily be converted + generates a list of dict which can easily be converted to a pandas dataframe """ # list comprehension for rows - return [{'field' : field, # field name - 'lang' : lang, # language shortcode - 'type' : self.__schema[field]['type'], # datatype - 'label' : self.__schema[field]['label'][lang], # label - 'value' : self.get_value(meta,field,lang), # actual value - 'template' : self.__schema[field]['template'][lang] if 'template' in self.__schema[field] else None, - # getting crazy with nested dict comprehension - 'enum_values' : { k:v[lang] for (k,v) in self.__schema[field]['values'].items()} if 'enum' in self.__schema[field]['type'] else None, - 'key_values' : { k:v[lang] for (k,v) in self.__schema[field]['keys'].items()} if 'multikey' in self.__schema[field]['type'] else None, - 'spec' : meta[field]['spec'][lang] if 'spec' in meta[field] else None - } - for field in fields] - + return [ + { + "field": field, # field name + "lang": lang, # language shortcode + "type": self.__schema[field]["type"], # datatype + "label": self.__schema[field]["label"][lang], # label + "value": self.get_value(meta, field, lang), # actual value + "template": self.__schema[field]["template"][lang] + if "template" in self.__schema[field] + else None, + # getting crazy with nested dict comprehension + "enum_values": { + k: v[lang] for (k, v) in self.__schema[field]["values"].items() + } + if "enum" in self.__schema[field]["type"] + else None, + "key_values": { + k: v[lang] for (k, v) in self.__schema[field]["keys"].items() + } + if "multikey" in self.__schema[field]["type"] + else None, + "spec": meta[field]["spec"][lang] if "spec" in meta[field] else None, + } + for field in fields + ] - def to_short_dict(self,meta,fields,lang): + def to_short_dict(self, meta, fields, lang): """ - generates a short version of dict which can easily be converted + generates a short version of dict which can easily be converted to a pandas dataframe """ # dict comprehension for whole meta part - return { field : self.get_value(meta,field,lang) for field in fields } - - def to_list_of_tuple(self,meta,fields,lang): + return {field: self.get_value(meta, field, lang) for field in fields} + + def to_list_of_tuple(self, meta, fields, lang): """ generates a list of tuples with a label and value (text) this is usually consumed by a Markdown generator @@ -69,19 +91,52 @@ class Schema: todo: needs deuglyfication of free standing loop, templates are possible for all """ list = [] - for r in self.to_list_of_dict(meta,fields,lang): - match r['type']: - case 'str' : - list.append( (r['label'],r['value']) ) - case 'int' | 'num' : - list.append( ( r['label'], r['template'].format(value=r['value'],spec=r['spec']) if r['template'] else r['value']) ) - case 'enum' : - list.append( ( r['label'], r['template'].format(value=r['enum_values'][r['value']],spec=r['spec']) - if r['template'] else r['enum_values'][r['value']] ) ) - case 'multikey' : - list.append( ( r['label'], ', '.join( [r['template'].format(key=r['key_values'][k],value=v) for k,v in r['value'].items()] ) ) ) - case 'multinum' : - list.append( (r['label'], ', '.join( r['template'].format(value=v) for v in r['value'])) ) + for r in self.to_list_of_dict(meta, fields, lang): + match r["type"]: + case "str": + list.append((r["label"], r["value"])) + case "int" | "num": + list.append( + ( + r["label"], + r["template"].format(value=r["value"], spec=r["spec"]) + if r["template"] + else r["value"], + ) + ) + case "enum": + list.append( + ( + r["label"], + r["template"].format( + value=r["enum_values"][r["value"]], spec=r["spec"] + ) + if r["template"] + else r["enum_values"][r["value"]], + ) + ) + case "multikey": + list.append( + ( + r["label"], + ", ".join( + [ + r["template"].format( + key=r["key_values"][k], value=v + ) + for k, v in r["value"].items() + ] + ), + ) + ) + case "multinum": + list.append( + ( + r["label"], + ", ".join( + r["template"].format(value=v) for v in r["value"] + ), + ) + ) return list - diff --git a/test/fields.yaml b/test/fields.yaml index 28aef14..4cdbd60 100644 --- a/test/fields.yaml +++ b/test/fields.yaml @@ -1,19 +1,19 @@ -fields: - - name - - instructor - - id - - goal - - content - - form-of-instruction - - prerequisites - - teaching-material - - author-of-indenture - - used-in - - workload - - credits - - form-of-exam - - term - - frequency - - duration - - kind - - remarks \ No newline at end of file +field-names: + - name + - instructor + - id + - goal + - content + - form-of-instruction + - prerequisites + - teaching-material + - author-of-indenture + - used-in + - workload + - credits + - form-of-exam + - term + - frequency + - duration + - kind + - remarks diff --git a/test/maacs/v2/3dcc/de.yaml b/test/maacs/v2/3dcc/lang.de.yaml similarity index 94% rename from test/maacs/v2/3dcc/de.yaml rename to test/maacs/v2/3dcc/lang.de.yaml index c85e6ac..af1ace2 100644 --- a/test/maacs/v2/3dcc/de.yaml +++ b/test/maacs/v2/3dcc/lang.de.yaml @@ -34,8 +34,7 @@ teaching-material: | prerequisites: | Formale Voraussetzung bestehen nicht. Für eine erfolgreiche Teilnahme sollte das Modul „Grundlagen der Computergrafik“ im Vorfeld belegt werden. -author-of-indenture: +form-of-exam: + spec: abzugebende Projektarbeit (70%) und mündliche Prüfung (30% ~20min) workload: "150h Insgesamt bestehend aus 60 Stunden Präsenzzeit, 60 Stunden Selbststudium, 30h Prüfung und Prüfungsvorbereitung" - -remarks: diff --git a/test/maacs/v2/3dcc/en.yaml b/test/maacs/v2/3dcc/lang.en.yaml similarity index 99% rename from test/maacs/v2/3dcc/en.yaml rename to test/maacs/v2/3dcc/lang.en.yaml index 6a81d1d..95fb9e9 100644 --- a/test/maacs/v2/3dcc/en.yaml +++ b/test/maacs/v2/3dcc/lang.en.yaml @@ -29,5 +29,3 @@ workload: "overall 150h comprising of 60h in-person training, 60h of self-study form-of-exam: spec: submitted project (70%) and oral exam (30% ~20min) - -remarks: diff --git a/test/maacs/v2/3dcc/mod.yaml b/test/maacs/v2/3dcc/mod.yaml index 88031b6..6f944ea 100644 --- a/test/maacs/v2/3dcc/mod.yaml +++ b/test/maacs/v2/3dcc/mod.yaml @@ -1,9 +1,9 @@ +id: 3DCC + name: 3D Content Creation instructor: Prof. Hartmut Seichter, PhD -id: 3DCC - form-of-instruction: - seminar: 2 - exersise: 2 @@ -12,7 +12,12 @@ credits: 5 form-of-exam: type: alternative - spec: abzugebende Projektarbeit (70%) und mündliche Prüfung (30% ~20min) duration: value: 1 + +author-of-indenture: + +kind: elective + +remarks: diff --git a/test/maacs/v2/maacs.yaml b/test/maacs/v2/maacs.yaml index 1ef9fac..adfb1cd 100644 --- a/test/maacs/v2/maacs.yaml +++ b/test/maacs/v2/maacs.yaml @@ -1,4 +1,15 @@ +# shortcode shortcode: maacs + +# name name: Applied Computer Science (Master of Science) + +# validation schema +schema: schema.yaml + +# languages (oder defines the order of overlay) +languages: [de, en] + +# courses courses: - 3dcc diff --git a/test/maacs/v2/schema.yaml b/test/maacs/v2/schema.yaml new file mode 100644 index 0000000..dbaf221 --- /dev/null +++ b/test/maacs/v2/schema.yaml @@ -0,0 +1,231 @@ +# fields in curricular description +# leaning on methods in OpenAPI 3.0 + +# +# Modulname +# +name: + type: str + label: + de: "Modulname" + en: "name of course" + +# +# Modulverantwortliche:r +# +instructor: + type: str + label: + de: "Modulverantwortlicher / Modulverantwortliche" + en: "module instructor" + +# +# Kürzel / ID +# +id: + type: str + translatable: false + label: { de: "Kürzel", en: "code" } + +# +# Qualifikationsziele +# + +# Welche fachbezogenen, methodischen, fachübergreifende Kompetenzen, +# Schlüsselqualifikationen - werden erzielt (erworben)? Diese sind +# an der zu definierenden Gesamtqualifikation (angestrebter Abschluss) auszurichten. +# +# Lernergebnisse sind Aussagen darüber, was ein Studierender nach Abschluss des Moduls weiß, +# versteht und in der Lage ist zu tun. Die Formulierung sollte sich am Qualifikationsrahmen +# für Deutsche Hochschulabschlüsse orientieren und Inhaltswiederholungen vermeiden. +# +# Des Weiteren finden Sie im QM-Portal die „Handreichung zur Beschreibung von Lernzielen“ +# als Formulierungshilfe. + +goal: + type: str + label: { de: "Qualifikationsziele", en: "educational goal" } + +# +# Modulinhalte +# + +# Welche fachlichen, methodischen, fachpraktischen und fächerübergreifenden +# Inhalte sollen vermittelt werden? +# +# Es ist ein stichpunktartiges Inhaltsverzeichnis zu erstellen. + +content: + type: str + label: { de: "Modulinhalte", en: "content" } + +# +# Lehrform +# + +# +# Welche Lehr- und Lernformen werden angewendet? +# (Vorlesungen, Übungen, Seminare, Praktika, +# Projektarbeit, Selbststudium) +# +# Es sind nur Werte aus der Prüfungsordung zugelassen +# +form-of-instruction: + type: multikey + label: { de: "Lehrform(en)", en: "form of instruction" } + keys: + { + "lecture": { de: "Vorlesung", en: "lecture" }, + "lecture_seminar": + { de: "Seminaristische Vorlesung", en: "lecture and seminar" }, + "seminar": { de: "Seminar", en: "seminar" }, + "exersise": { de: "Übung", en: "lab exersise" }, + "pc_lab": { de: "Rechnergestütztes Praktikum", en: "PC exersise" }, + "project": { de: "Project", en: "project" }, + } + template: + de: "{key} ({value}SWS)" + en: "{key} ({value}SWS)" + +# +# Voraussetzungen für die Teilnahme +# + +# Für jedes Modul sind die Voraussetzungen für die Teilnahme zu beschreiben. +# Welche Kenntnisse, Fähigkeiten und Fertigkeiten sind für eine +# erfolgreiche Teilnahme vorauszusetzen? +# +# Alternativ können die Module benannt werden welche für die erfolgreiche +# Teilnahme im Vorfeld zu belegen sind. + +prerequisites: + type: str + label: { de: "Voraussetzungen für die Teilnahme", en: "prerequisites" } + +# +# Literatur und multimediale Lehr- und Lernprogramme +# +# +# Wie können die Studierenden sich auf die Teilnahme an diesem Modul vorbereiten? +# +teaching-material: + type: str + label: + { + de: "Literatur und multimediale Lehr- und Lernprogramme", + en: "media of instruction", + } + +# +# Lehrbriefautor +# +author-of-indenture: + type: str + label: { de: "Lehrbriefautor", en: "author of indenture" } + +# +# Verwendung in (Studienprogramm) +# +# used-in: +# type: str +# label: { de: "Verwendung", en: "used in study programs" } + +# +# Arbeitsaufwand +# +workload: + type: str + label: { de: "Arbeitsaufwand / Gesamtworkload", en: "workload" } +# +# credits/ECTS +# +credits: + type: num + unit: ECTS + label: + { + en: "credits and weight of mark", + de: "Kreditpunkte und Gewichtung der Note in der Gesamtnote", + } + template: + de: "{value}CP, Gewichtung: {value}CP von 120CP " + en: "{value}CP, weight: {value} / 120 " + +# +# Leistungsnachweis +# +form-of-exam: + type: enum + label: { de: "Leistungsnachweis", en: "form of examination" } + values: + { + "written": { de: "Schriftliche Prüfung", en: "written exam" }, + "oral": { de: "Mündliche Prüfung", en: "oral exam" }, + "alternative": + { + de: "Alternative Prüfungunsleistung", + en: "alternative examination", + }, + } + spec: true + template: + de: "{value} ({spec})" + en: "{value} ({spec})" + +# +# Semester +# +# term: +# type: multinum +# label: { de: "Semester", en: "term" } +# template: +# de: "{value}\\. Semester" +# en: "{value}\\. semester" + +# +# Häufigkeit des Angebots +# +# frequency: +# type: enum +# label: { de: "Häufigkeit des Angebots", en: "frequency of Offer" } +# values: +# { +# "once_per_term": { de: "jedes Semester", en: "every semester" }, +# "once_per_year": +# { de: "einmal im Studienjahr", en: "once per study year" }, +# } + +# +# Dauer des Angebots +# +# duration: +# type: int +# label: +# de: Dauer +# en: duration +# template: +# de: "{value} Semester" +# en: "{value} term(s)" + +# +# Art der Veranstaltung +# +kind: + type: enum + label: + { + de: "Art der Veranstaltung (Pflicht, Wahl, etc.)", + en: "kind of module (compulsory, elective)", + } + values: + { + "compulsory": { de: "Pflicht", en: "compulsory" }, + "elective": { de: "Wahl/Wahlpflicht", en: "elective" }, + } + +# +# Freiform Bemerkungen +# +remarks: + type: str + label: { de: "Besonderes", en: "remarks" }