Compare commits

..

No commits in common. "main" and "feature-template" have entirely different histories.

14 changed files with 217 additions and 331 deletions

View file

@ -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: 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:

View file

@ -35,6 +35,7 @@ options:
© Copyright 2020-2024 Hartmut Seichter © Copyright 2020-2024 Hartmut Seichter
# Licence # Licence
Coursebuilder is licensed under the terms of the MIT License. For details consult https://opensource.org/license/mit/ or the attached license file Coursebuilder is licensed under the terms of the MIT License. For details consult https://opensource.org/license/mit/ or the attached license file

View file

@ -3,7 +3,5 @@
* [ ] proper referencing of tables * [ ] proper referencing of tables
* [ ] custom python code in tables * [ ] custom python code in tables
* [x] fix overlong table cells (pandoc longtable only deals with overlong tables but not cells) * [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) * [ ] add a book mode for mixing input and headers (# Blah -m mod.cg.yaml)
* [~] table generator * [ ] table generator
* [ ] overlay of compulsory with other modes ...
* [ ] add template based generator

View file

@ -13,118 +13,207 @@ actual values are kept in YAML files in order to version them with git.
from argparse import ArgumentParser from argparse import ArgumentParser
import yaml import yaml
import string import string
import os,sys import os
from tablegenerator import TableGenerator from tablegenerator import TableGenerator
from markdowngenerator import MarkdownGenerator from markdowngenerator import MarkdownGenerator
from templategenerator import TemplateGenerator
from metagenerator import MetaGenerator
class CourseBuilder: class CourseBuilder:
@staticmethod def __init__(self) -> None:
def run(): self.__schema = None
# 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') def set_schema(self,schema = None):
parser.add_argument('-f','--fields',help="Fields to be used, the table will be build accordingly",action="extend", nargs="+", type=str) self.__schema = schema
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 get_template(self,field,lang='de'):
if 'template' in self.__schema[field]:
return self.__schema[field]['template'][lang]
else: 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__': if __name__ == '__main__':
CourseBuilder.run() main()

View file

@ -2,21 +2,13 @@
import textwrap,itertools 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: class MarkdownGenerator:
def __init__(self) -> None:
@staticmethod
def generate_tablerow() -> str:
pass pass
@staticmethod def generate_markdown(self,ti,pagebreak = False,title = False,header_level = 1) -> str:
def generate(ti,pagebreak = False,title = False,header_level = 1) -> str:
line_length = 128 line_length = 128
column_ratio= 0.28 column_ratio= 0.28
@ -24,9 +16,6 @@ class MarkdownGenerator:
h_len = int(line_length * column_ratio) h_len = int(line_length * column_ratio)
d_len = line_length-h_len d_len = line_length-h_len
#
# generate title (currently the first one)
#
if title: if title:
print('#' * header_level,ti[0][1],'\n') print('#' * header_level,ti[0][1],'\n')
@ -38,11 +27,8 @@ class MarkdownGenerator:
# this implements a Markdown Grid-Table # this implements a Markdown Grid-Table
# #
# test if this affected by a third item!
for k,v in ti: for k,v in ti:
#
if v == None: if v == None:
v = '' v = ''

View file

@ -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)

View file

@ -7,7 +7,7 @@ import os
class TableGenerator: 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: def __init__(self) -> None:
@ -22,7 +22,7 @@ class TableGenerator:
for token in data.split(','): for token in data.split(','):
t = tuple(token.split(':')[:2]) t = tuple(token.split(':')[:2])
row = [t[0]] row = [t[0]]
for k in list(self.__cols_map[lang].keys())[1:]: for k in list(self.__cols_map[lang].keys())[1:]:
if k in t[1]: if k in t[1]:
@ -33,10 +33,10 @@ class TableGenerator:
rows.append(' & '.join(row) + '\\\\') rows.append(' & '.join(row) + '\\\\')
self.run_template(rows=rows) self.run_template(rows=rows)
def run_template(self,rows = [],lang = 'de'):
def run_template(self,rows = [],lang = 'de') -> None:
t = string.Template(self.get_latex_template()) t = string.Template(self.get_latex_template())
with tempfile.NamedTemporaryFile('w',delete=False,prefix='cb-') as fp: with tempfile.NamedTemporaryFile('w',delete=False,prefix='cb-') as fp:
@ -45,7 +45,7 @@ class TableGenerator:
subprocess.run(["latex",fp.name]) subprocess.run(["latex",fp.name])
subprocess.run(["dvisvgm",os.path.basename(fp.name) + '.dvi']) subprocess.run(["dvisvgm",os.path.basename(fp.name) + '.dvi'])
# subprocess.run(["mv",os.path.basename(fp.name) + '.svg','.']) # subprocess.run(["mv",os.path.basename(fp.name) + '.svg','.'])
def get_latex_template(self,lang = 'de') -> str: def get_latex_template(self,lang = 'de') -> str:
@ -63,7 +63,7 @@ class TableGenerator:
r'\begin{table}[ht]' + r'\begin{table}[ht]' +
'\\begin{{tabular}} {{ {0} }}'.format(' '.join(layout)) + '\\begin{{tabular}} {{ {0} }}'.format(' '.join(layout)) +
r'\hline' r'\hline'
r' ${th}' r' ${th}'
r'\hline' r'\hline'
r' ${td}' + r' ${td}' +
r'\hline' r'\hline'
@ -71,7 +71,7 @@ class TableGenerator:
r'\end{table}' r'\end{table}'
r'\end{document}') r'\end{document}')
# #
# Kompetenz & Kennen & Wertung \\ # Kompetenz & Kennen & Wertung \\
@ -79,4 +79,4 @@ class TableGenerator:
# 2 & Latex & ++ \\ # 2 & Latex & ++ \\
# 3 & Writer & +- \\ # 3 & Writer & +- \\
# #
# latex image.tex;dvisvgm image.dvi # latex image.tex;dvisvgm image.dvi

View file

@ -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

View file

@ -1,30 +1,17 @@
coursebuilder := ../../coursebuilder
table.en.pdf: table.en.pdf:
@echo "creating English version ..." @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: table.de.pdf:
@echo "creating German version ..." @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 all: table.en.pdf table.de.pdf
clean: clean:
rm -f table.en.pdf table.de.pdf 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: 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

View file

@ -11,7 +11,7 @@ book:
- content - content
- form-of-instruction - form-of-instruction
- prerequisites - prerequisites
- teaching-material - media-of-instruction
- author-of-indenture - author-of-indenture
- used-in - used-in
- workload - workload
@ -28,10 +28,4 @@ book:
en: "## compulsory courses {.unnumbered}" en: "## compulsory courses {.unnumbered}"
- modules: - modules:
- mod.cg.yaml - mod.cg.yaml
- text:
de: "## Wahlbereich {.unnumbered}"
en: "## elective courses {.unnumbered}"
- modules:
- mod.interactsys.yaml

View file

@ -5,7 +5,7 @@ fields:
- content - content
- form-of-instruction - form-of-instruction
- prerequisites - prerequisites
- teaching-material - media-of-instruction
- author-of-indenture - author-of-indenture
- used-in - used-in
- workload - workload

View file

@ -74,7 +74,7 @@ content:
* Overview visualizations * Overview visualizations
* Graphical User Interfaces * Graphical User Interfaces
teaching-material: media-of-instruction:
de: | de: |
* H5P Lernmodule * H5P Lernmodule
* Lernforum * Lernforum

View file

@ -61,7 +61,7 @@ content:
* evaluation methods of interactive systems * evaluation methods of interactive systems
* statistical methods for UX design * statistical methods for UX design
teaching-material: media-of-instruction:
de: | de: |
H5P Lernmodule, Lernforum und Übungen am PC H5P Lernmodule, Lernforum und Übungen am PC
@ -101,9 +101,6 @@ workload:
form-of-exam: form-of-exam:
value: alternative value: alternative
spec:
de:
en:
frequency: frequency:
value: once_per_year value: once_per_year

View file

@ -142,7 +142,7 @@ prerequisites:
# #
# Wie können die Studierenden sich auf die Teilnahme an diesem Modul vorbereiten? # Wie können die Studierenden sich auf die Teilnahme an diesem Modul vorbereiten?
# #
teaching-material: media-of-instruction:
type: str type: str
label: { label: {
de: "Literatur und multimediale Lehr- und Lernprogramme", de: "Literatur und multimediale Lehr- und Lernprogramme",