diff --git a/LICENSE.md b/LICENSE.md index a480400..a749f99 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,4 +1,4 @@ -Copyright 2020-2024 Hartmut Seichter +Copyright 2020-2023 Hartmut Seichter Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/README.md b/README.md index 368648b..ec5f847 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ options: © Copyright 2020-2024 Hartmut Seichter + # Licence Coursebuilder is licensed under the terms of the MIT License. For details consult https://opensource.org/license/mit/ or the attached license file diff --git a/TODO.md b/TODO.md index c35be4a..484edcc 100644 --- a/TODO.md +++ b/TODO.md @@ -3,7 +3,5 @@ * [ ] proper referencing of tables * [ ] custom python code in tables * [x] fix overlong table cells (pandoc longtable only deals with overlong tables but not cells) -* [x] add a book mode for mixing input and headers (# Blah -m mod.cg.yaml) -* [~] table generator -* [ ] overlay of compulsory with other modes ... -* [ ] add template based generator \ No newline at end of file +* [ ] add a book mode for mixing input and headers (# Blah -m mod.cg.yaml) +* [ ] table generator diff --git a/coursebuilder/__main__.py b/coursebuilder/__main__.py index 996e426..2ce0fc5 100644 --- a/coursebuilder/__main__.py +++ b/coursebuilder/__main__.py @@ -13,118 +13,207 @@ actual values are kept in YAML files in order to version them with git. from argparse import ArgumentParser import yaml import string -import os,sys +import os from tablegenerator import TableGenerator from markdowngenerator import MarkdownGenerator -from templategenerator import TemplateGenerator -from metagenerator import MetaGenerator class CourseBuilder: - @staticmethod - def run(): - - # arguments - parser = ArgumentParser(description='versatile curricula generator') + def __init__(self) -> None: + self.__schema = None - parser.add_argument('-m','--meta',action="extend", nargs="+", type=str,help="course description(s) as YAML file(s)") - parser.add_argument('-l','--lang',help="Language to parse from meta file (use de or en)",default='de') - parser.add_argument('-f','--fields',help="Fields to be used, the table will be build accordingly",action="extend", nargs="+", type=str) - parser.add_argument('-s','--schema',help="using provided schema") - parser.add_argument('-p','--pagebreak',action="store_true",help="add a pagebreak after each module") - parser.add_argument('-t','--title',action="store_true",help="take first value in list as title") - parser.add_argument('-b','--book',type=str,help="process a whole curriculum book with sections") - parser.add_argument('--level',type=int,default=1,help="level of header tags") - parser.add_argument('--table-gen',type=str,default=None,help='runs table generator') - parser.add_argument('--template',type=str,default=None,help='defines a template to be used with fields') - - # get arguments - args = parser.parse_args() - - if args.table_gen: - - tg = TableGenerator() - - tg.generate_table(args.table_gen) - - return - - - # book mode with predefined setting from a book file - if args.book and args.schema: - - generator = MetaGenerator() - - with open(args.schema) as sf: - generator.set_schema(yaml.load(sf,Loader=yaml.Loader)) - - with open(args.book) as bf: - - actual_fields = [] - - book = yaml.load(bf,Loader=yaml.Loader) - book_path = os.path.abspath(args.book) - - for bi in book['book']: - if 'fields' in bi: - actual_fields = bi['fields'] - if 'sections' in bi: - for section in bi['sections']: - if 'text' in section: - print(section['text'][args.lang]) - if 'modules' in section: - for m in section['modules']: - mod_path = os.path.join(os.path.dirname(book_path),m) - - with open(mod_path) as fm: - try: - - table_items = generator.process(yaml.load(fm,Loader=yaml.Loader),fields=actual_fields,lang=args.lang,pagebreak=args.pagebreak,createTitle=args.title,header_level=args.level) - - MarkdownGenerator.generate(table_items,pagebreak=args.pagebreak,title=args.title,header_level=args.level) - - except Exception as exc: - print(f'{type(exc).__name__} in {mod_path}: {exc}',file=sys.stderr) - - - # verbose command line mode - elif args.schema and args.meta and len(args.fields) > 0: - - # get actual fields - actual_fields = [] - - if os.path.isfile(args.fields[0]): - with open(args.fields[0]) as ff: - actual_fields = yaml.load(ff,Loader=yaml.Loader)['fields'] - else: - actual_fields = args.fields - - - # get schema - actual_schema = None - with open(args.schema) as f: - actual_schema = yaml.load(f,Loader=yaml.Loader) - - # iterate through meta files - for m in args.meta: - with open(m) as fm: - - generator = MetaGenerator() - generator.set_schema(actual_schema) - - table_items = generator.process(yaml.load(fm,Loader=yaml.Loader),fields=actual_fields,lang=args.lang,pagebreak=args.pagebreak,createTitle=args.title,header_level=args.level,template=args.template) - - if args.template: - TemplateGenerator.generate(table_items) - else: - MarkdownGenerator.generate(table_items,pagebreak=args.pagebreak,title=args.title,header_level=args.level) - - # print(table_items) + + def set_schema(self,schema = None): + self.__schema = schema + def get_template(self,field,lang='de'): + if 'template' in self.__schema[field]: + return self.__schema[field]['template'][lang] else: - parser.print_help() + return "$value" + + 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 process_label(self,field,lang='de'): + # processes the label of a field item + return self.__schema[field]['label'][lang] + + def process_str(self,meta,field,lang='de'): + if self.is_translatable(field): + + return [self.process_label(field,lang),meta[field][lang]] + else: + if not 'value' in meta[field]: + raise AssertionError(field,'incomplete') + + return [self.process_label(field,lang),meta[field]['value']] + + def process_enum(self,meta,field,lang='de'): + """ + enum have a specification 'specs' option + that can be forced by the scheme + """ + vv = meta[field]['value'] + enum_val = self.__schema[field]['values'][vv][lang] + + if self.needs_spec(field): + + t = string.Template(self.get_template(field=field,lang=lang)) + + spec = meta[field]['spec'][lang] + + return [self.process_label(field,lang),t.substitute({'value': enum_val,'spec': spec})] + else: + return [self.process_label(field,lang),enum_val] + + def process_num(self,meta,field,lang='de'): + v = meta[field]['value'] + t = string.Template(self.get_template(field,lang)) + return [self.process_label(field,lang),t.substitute({'value' : v})] + + def process_multinum(self,meta,field,lang='de'): + v = meta[field]['value'] + t = string.Template(self.get_template(field,lang)) + if hasattr(v, "__len__"): + vv = [t.substitute({'value' : ev}) for ev in v] + return [self.process_label(field,lang),', '.join(vv)] + else: + return self.process_num(meta=meta,field=field,lang=lang) + + + def process_multikey(self,meta,field,lang='de'): + """ + multikey need to assign a numeric value to a key + """ + vs = meta[field]['value'] + t = string.Template(self.get_template(field,lang)) + + k = self.process_label(field,lang) + + parts = [] + + for e in vs: + kk = self.__schema[field]['keys'][e][lang] + parts.append(t.substitute({'key': kk, 'value' : vs[e]})) + + return [k,', '.join(parts)] + + + def process(self,meta,fields = [],lang = 'de',pagebreak = False,createTitle=False,header_level=1): + + table_items = [] + + for field in fields: + match self.__schema[field]['type']: + case 'str': table_items.append(self.process_str(meta,field,lang)) + case 'enum': table_items.append(self.process_enum(meta,field,lang)) + case 'int' | 'num' : table_items.append(self.process_num(meta,field,lang)) + case 'multinum' : table_items.append(self.process_multinum(meta,field,lang)) + case 'multikey': table_items.append(self.process_multikey(meta,field,lang)) + + mdg = MarkdownGenerator() + mdg.generate_markdown(table_items,pagebreak,createTitle,header_level=header_level) + + def process_book_section(self,section,lang='de'): + pass + + def process_book(self,book,bookpath,create_title,pagebreak,lang='de',header_level=2): + + actual_fields = [] + + for bi in book['book']: + if 'fields' in bi: + actual_fields = bi['fields'] + if 'sections' in bi: + for section in bi['sections']: + if 'text' in section: + print(section['text'][lang]) + if 'modules' in section: + for m in section['modules']: + mod_path = os.path.join(os.path.dirname(bookpath),m) + + with open(mod_path) as fm: + self.process(yaml.load(fm,Loader=yaml.Loader),fields=actual_fields,lang=lang,pagebreak=pagebreak,createTitle=create_title,header_level=header_level) + + + + + +def main(): + + # arguments + parser = ArgumentParser(description='versatile curricula generator') + + parser.add_argument('-m','--meta',action="extend", nargs="+", type=str,help="course description(s) as YAML file(s)") + parser.add_argument('-l','--lang',help="Language to parse from meta file (use de or en)",default='de') + parser.add_argument('-f','--fields',help="Fields to be used, the table will be build accordingly",action="extend", nargs="+", type=str) + parser.add_argument('-s','--schema',help="using provided schema") + parser.add_argument('-p','--pagebreak',action="store_true",help="add a pagebreak after each module") + parser.add_argument('-t','--title',action="store_true",help="take first value in list as title") + parser.add_argument('-b','--book',type=str,help="process a whole curriculum book with sections") + parser.add_argument('--level',type=int,default=1,help="level of header tags") + parser.add_argument('--table-gen',type=str,default=None,help='runs table generator') + + + # get arguments + args = parser.parse_args() + + if args.table_gen: + + tg = TableGenerator() + + tg.generate_table(args.table_gen) + + return + + + + # book mode with predefined setting from a book file + if args.book and args.schema: + + cb = CourseBuilder() + + with open(args.schema) as sf: + cb.set_schema(yaml.load(sf,Loader=yaml.Loader)) + + with open(args.book) as bf: + cb.process_book(yaml.load(bf,Loader=yaml.Loader),os.path.abspath(args.book),lang=args.lang,pagebreak=args.pagebreak,create_title=args.title,header_level=args.level) + + + # verbose command line mode + elif args.schema and args.meta and len(args.fields) > 0: + + cb = CourseBuilder() + + actual_fields = [] + + if os.path.isfile(args.fields[0]): + with open(args.fields[0]) as ff: + actual_fields = yaml.load(ff,Loader=yaml.Loader)['fields'] + else: + actual_fields = args.fields + + + with open(args.schema) as f: + cb.set_schema(yaml.load(f,Loader=yaml.Loader)) + + for m in args.meta: + with open(m) as fm: + cb.process(yaml.load(fm,Loader=yaml.Loader),fields=actual_fields,lang=args.lang,pagebreak=args.pagebreak,createTitle=args.title,header_level=args.level) + else: + parser.print_help() if __name__ == '__main__': - CourseBuilder.run() + main() diff --git a/coursebuilder/markdowngenerator.py b/coursebuilder/markdowngenerator.py index 59f4323..f7ff98a 100644 --- a/coursebuilder/markdowngenerator.py +++ b/coursebuilder/markdowngenerator.py @@ -2,21 +2,13 @@ import textwrap,itertools -# we need raw value maybe add a third item - tuple the input? - -# alternative use a dictionary -# { name: XYZ } - -# or make it the class class MarkdownGenerator: - - @staticmethod - def generate_tablerow() -> str: + def __init__(self) -> None: pass + - @staticmethod - def generate(ti,pagebreak = False,title = False,header_level = 1) -> str: + def generate_markdown(self,ti,pagebreak = False,title = False,header_level = 1) -> str: line_length = 128 column_ratio= 0.28 @@ -24,9 +16,6 @@ class MarkdownGenerator: h_len = int(line_length * column_ratio) d_len = line_length-h_len - # - # generate title (currently the first one) - # if title: print('#' * header_level,ti[0][1],'\n') @@ -38,11 +27,8 @@ class MarkdownGenerator: # this implements a Markdown Grid-Table # - # test if this affected by a third item! - for k,v in ti: - # if v == None: v = '' diff --git a/coursebuilder/metagenerator.py b/coursebuilder/metagenerator.py deleted file mode 100644 index fe70f33..0000000 --- a/coursebuilder/metagenerator.py +++ /dev/null @@ -1,155 +0,0 @@ -import os,string,sys -import yaml - -class MetaGenerator: - - def __init__(self) -> None: - self.__schema = None - - def set_schema(self,schema = None): - self.__schema = schema - - def get_template(self,field,lang='de'): - if 'template' in self.__schema[field]: - return self.__schema[field]['template'][lang] - else: - return "$value" - - 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 process_label(self,field,lang='de'): - # processes the label of a field item - return self.__schema[field]['label'][lang] - - def process_str(self,meta,field,lang='de'): - if self.is_translatable(field): - - return [self.process_label(field,lang),meta[field][lang]] - else: - if not 'value' in meta[field]: - raise AssertionError(field,'incomplete') - - return [self.process_label(field,lang),meta[field]['value']] - - def process_enum(self,meta,field,lang='de'): - """ - enum have a specification 'specs' option - that can be forced by the scheme - """ - vv = meta[field]['value'] - enum_val = self.__schema[field]['values'][vv][lang] - - if self.needs_spec(field): - - t = string.Template(self.get_template(field=field,lang=lang)) - - spec = meta[field]['spec'][lang] - - return [self.process_label(field,lang),t.substitute({'value': enum_val,'spec': spec})] - else: - return [self.process_label(field,lang),enum_val] - - def process_num(self,meta,field,lang='de'): - v = meta[field]['value'] - t = string.Template(self.get_template(field,lang)) - return [self.process_label(field,lang),t.substitute({'value' : v})] - - def process_multinum(self,meta,field,lang='de'): - v = meta[field]['value'] - t = string.Template(self.get_template(field,lang)) - if hasattr(v, "__len__"): - vv = [t.substitute({'value' : ev}) for ev in v] - return [self.process_label(field,lang),', '.join(vv)] - else: - return self.process_num(meta=meta,field=field,lang=lang) - - - def process_multikey(self,meta,field,lang='de'): - """ - multikey need to assign a numeric value to a key - """ - vs = meta[field]['value'] - t = string.Template(self.get_template(field,lang)) - - k = self.process_label(field,lang) - - parts = [] - - for e in vs: - kk = self.__schema[field]['keys'][e][lang] - parts.append(t.substitute({'key': kk, 'value' : vs[e]})) - - return [k,', '.join(parts)] - - - def process(self,meta,fields = [],lang = 'de',pagebreak = False,createTitle=False,header_level=1,template=None): - - table_items = [] - - # iterate over requested fields - for field in fields: - try: - # correlate with schema and append - match self.__schema[field]['type']: - case 'str': table_items.append(self.process_str(meta,field,lang)) - case 'enum': table_items.append(self.process_enum(meta,field,lang)) - case 'int' | 'num' : table_items.append(self.process_num(meta,field,lang)) - case 'multinum' : table_items.append(self.process_multinum(meta,field,lang)) - case 'multikey': table_items.append(self.process_multikey(meta,field,lang)) - except Exception as exp: - print(field,' not resolvable in ',self.__schema,exp) - - # maybe return tableitems as np.Dataframe? - return table_items - - - - # if template != None: - # # use template generator - # TemplateGenerator.generate(table_items,pagebreak,createTitle,header_level=header_level) - # pass - # else: - # # conventional MD mode - # MarkdownGenerator.generate(table_items,pagebreak,createTitle,header_level=header_level) - - # def process_book_section(self,section,lang='de'): - # pass - - # book mode - # def process_book(self,book,bookpath,create_title,pagebreak,lang='de',header_level=2): - - # actual_fields = [] - - # for bi in book['book']: - # if 'fields' in bi: - # actual_fields = bi['fields'] - # if 'sections' in bi: - # for section in bi['sections']: - # if 'text' in section: - # print(section['text'][lang]) - # if 'modules' in section: - # for m in section['modules']: - # mod_path = os.path.join(os.path.dirname(bookpath),m) - - # with open(mod_path) as fm: - # try: - # table_items = self.process(yaml.load(fm,Loader=yaml.Loader),fields=actual_fields,lang=lang,pagebreak=pagebreak,createTitle=create_title,header_level=header_level) - - # print(table_items) - - # except Exception as exc: - # print(f'{type(exc).__name__} in {mod_path}: {exc}',file=sys.stderr) - - - - diff --git a/coursebuilder/tablegenerator.py b/coursebuilder/tablegenerator.py index 40b8afd..3bdc6f5 100644 --- a/coursebuilder/tablegenerator.py +++ b/coursebuilder/tablegenerator.py @@ -7,7 +7,7 @@ import os class TableGenerator: """ - Really hacky method to create latex > dvi > SVG to create images to include + Really hacky method to create latex > dvi > SVG to create images to include """ def __init__(self) -> None: @@ -22,7 +22,7 @@ class TableGenerator: for token in data.split(','): - t = tuple(token.split(':')[:2]) + t = tuple(token.split(':')[:2]) row = [t[0]] for k in list(self.__cols_map[lang].keys())[1:]: if k in t[1]: @@ -33,10 +33,10 @@ class TableGenerator: rows.append(' & '.join(row) + '\\\\') self.run_template(rows=rows) + - - def run_template(self,rows = [],lang = 'de') -> None: - + def run_template(self,rows = [],lang = 'de'): + t = string.Template(self.get_latex_template()) with tempfile.NamedTemporaryFile('w',delete=False,prefix='cb-') as fp: @@ -45,7 +45,7 @@ class TableGenerator: subprocess.run(["latex",fp.name]) subprocess.run(["dvisvgm",os.path.basename(fp.name) + '.dvi']) # subprocess.run(["mv",os.path.basename(fp.name) + '.svg','.']) - + def get_latex_template(self,lang = 'de') -> str: @@ -63,7 +63,7 @@ class TableGenerator: r'\begin{table}[ht]' + '\\begin{{tabular}} {{ {0} }}'.format(' '.join(layout)) + r'\hline' - r' ${th}' + r' ${th}' r'\hline' r' ${td}' + r'\hline' @@ -71,7 +71,7 @@ class TableGenerator: r'\end{table}' r'\end{document}') - + # # Kompetenz & Kennen & Wertung \\ @@ -79,4 +79,4 @@ class TableGenerator: # 2 & Latex & ++ \\ # 3 & Writer & +- \\ # -# latex image.tex;dvisvgm image.dvi +# latex image.tex;dvisvgm image.dvi \ No newline at end of file diff --git a/coursebuilder/templategenerator.py b/coursebuilder/templategenerator.py deleted file mode 100644 index 0e7a23a..0000000 --- a/coursebuilder/templategenerator.py +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env python - -import textwrap,itertools - - -class TemplateGenerator: - - @staticmethod - def generate(ti,pagebreak = False,title = False,header_level = 1) -> str: - print(ti) - pass \ No newline at end of file diff --git a/test/simple/Makefile b/test/simple/Makefile index 76e1d83..8ea5c11 100644 --- a/test/simple/Makefile +++ b/test/simple/Makefile @@ -1,30 +1,17 @@ - -coursebuilder := ../../coursebuilder - table.en.pdf: @echo "creating English version ..." - python ${coursebuilder} -s schema.yaml -m mod.cg.yaml -l en -f fields.yaml | pandoc --template pandoc-template/eisvogel.latex -o table.en.pdf + python ../../coursebuilder -s schema.yaml -m mod.cg.yaml -l en -f fields.yaml | pandoc --template pandoc-template/eisvogel.latex -o table.en.pdf table.de.pdf: @echo "creating German version ..." - python ${coursebuilder} -s schema.yaml -m mod.cg.yaml -l de -f fields.yaml | pandoc --template pandoc-template/eisvogel.latex -o table.de.pdf + python ../../coursebuilder -s schema.yaml -m mod.cg.yaml -l de -f fields.yaml | pandoc --template pandoc-template/eisvogel.latex -o table.de.pdf all: table.en.pdf table.de.pdf -clean: +clean: rm -f table.en.pdf table.de.pdf -debug-template: - python ${coursebuilder} -s schema.yaml -m mod.cg.yaml mod.interactsys.yaml -l de -f name credits --template "$$name | $$credits" - -debug-markdown: - python ${coursebuilder} -s schema.yaml -m mod.cg.yaml mod.interactsys.yaml -l de -f name credits - -debug-book: - python ${coursebuilder} -s schema.yaml -b book.yaml -l de debug: - python ${coursebuilder} -s schema.yaml -m mod.cg.yaml -l de -f fields.yaml + python ../../coursebuilder -s schema.yaml -m mod.cg.yaml -l de -f fields.yaml -table: - python ${coursebuilder} -s schema.yaml -b book.yaml -l de -f fields.yaml diff --git a/test/simple/book.yaml b/test/simple/book.yaml index 5ec66d5..6cd1b46 100644 --- a/test/simple/book.yaml +++ b/test/simple/book.yaml @@ -11,7 +11,7 @@ book: - content - form-of-instruction - prerequisites - - teaching-material + - media-of-instruction - author-of-indenture - used-in - workload @@ -28,10 +28,4 @@ book: en: "## compulsory courses {.unnumbered}" - modules: - mod.cg.yaml - - text: - de: "## Wahlbereich {.unnumbered}" - en: "## elective courses {.unnumbered}" - - modules: - - mod.interactsys.yaml - diff --git a/test/simple/fields.yaml b/test/simple/fields.yaml index bede013..92a0a08 100644 --- a/test/simple/fields.yaml +++ b/test/simple/fields.yaml @@ -5,7 +5,7 @@ fields: - content - form-of-instruction - prerequisites - - teaching-material + - media-of-instruction - author-of-indenture - used-in - workload diff --git a/test/simple/mod.cg.yaml b/test/simple/mod.cg.yaml index e5b4201..55d76c4 100644 --- a/test/simple/mod.cg.yaml +++ b/test/simple/mod.cg.yaml @@ -74,7 +74,7 @@ content: * Overview visualizations * Graphical User Interfaces -teaching-material: +media-of-instruction: de: | * H5P Lernmodule * Lernforum diff --git a/test/simple/mod.interactsys.yaml b/test/simple/mod.interactsys.yaml index c20086e..d8efd55 100644 --- a/test/simple/mod.interactsys.yaml +++ b/test/simple/mod.interactsys.yaml @@ -61,7 +61,7 @@ content: * evaluation methods of interactive systems * statistical methods for UX design -teaching-material: +media-of-instruction: de: | H5P Lernmodule, Lernforum und Übungen am PC @@ -101,9 +101,6 @@ workload: form-of-exam: value: alternative - spec: - de: - en: frequency: value: once_per_year diff --git a/test/simple/schema.yaml b/test/simple/schema.yaml index 8d54fab..9a46893 100644 --- a/test/simple/schema.yaml +++ b/test/simple/schema.yaml @@ -142,7 +142,7 @@ prerequisites: # # Wie können die Studierenden sich auf die Teilnahme an diesem Modul vorbereiten? # -teaching-material: +media-of-instruction: type: str label: { de: "Literatur und multimediale Lehr- und Lernprogramme",