Commit 57e43ab2 authored by Carsten Eie Frigaard's avatar Carsten Eie Frigaard
Browse files

update

parent 4c419aa3
#!/usr/bin/env python3
from Utils.dbg import ERR, DiagStdErr, PrettyPrintTracebackDiagnostics
from sys import stdout, stderr
from html import escape, unescape
from urllib.parse import quote
def Str(t, checknonempty=True):
assert isinstance(t, str), f"exected string but got type='{type(t)}'"
......@@ -34,6 +37,8 @@ def Check(expr, msg):
Str(msg)
if not Bool(expr):
ERR("EXPRESSION NOT FULLFILLED: " + msg)
return False
return True
def Trim(s, checknonempty=True):
s = Str(s, Bool(checknonempty)).replace("\t"," ")
......@@ -69,6 +74,22 @@ def LoadText(filename, timeout=4000, split=True):
c = c.split("\n")
return c
def HtmlEncode(s):
# for text in html page
return escape(Str(s, False))
def HtmlDecode(s):
return unescape(Str(s, False))
def UrlQuote(s):
# for file names in urls
s = quote(Str(s, False), safe='')
assert s.find("'") < 0
assert s.find('"') < 0
assert s.find(" ") < 0
return s
def MkHtmlPage(htmlcontent):
assert Str(htmlcontent).find("DOCTYPE")<0 and htmlcontent.find("<html>")<=0 and htmlcontent.find("<body>")<=0
......
......@@ -6,7 +6,6 @@ from Utils.mkutils import *
from sys import argv
from argparse import ArgumentParser
from html import escape, unescape
if __name__ == '__main__':
......@@ -16,20 +15,12 @@ if __name__ == '__main__':
LEFT = '<'
RIGHT= '>'
def HtmlEncode(s):
return escape(Str(s, False))
def HtmlDecode(s):
return unescape(Str(s, False))
def _mkHtml(tag, style=""):
if len(Str(style, False)) > 0:
if style[0]!=" ":
style = " " + style
if style.find("'") >= 0:
ERR(f"no plings in style, please, got style = '{style}'")
if style.find('"') >= 0:
ERR(f"no quotes in style, please, got style = '{style}'")
Check(style.find("'") < 0, f"no plings in style, please, got style = '{style}'")
Check(style.find('"') < 0, f"no quotes in style, please, got style = '{style}'")
return f"{LEFT}{Str(tag)}{style}{RIGHT}"
......@@ -84,8 +75,7 @@ if __name__ == '__main__':
arg0 = Trim(args[0], checknonempty0)
arg1 = Trim(args[1], checknonempty1)
if arg0.find('"')>=0 or arg0.find("'")>=0:
ERR(f"do not use pligs (') or quotes (\") in style command, style='{arg0}'")
Check(arg0.find('"') < 0 and arg0.find("'") < 0, f"do not use pligs (') or quotes (\") in style command, style='{arg0}'")
return arg0, arg1
......@@ -104,18 +94,14 @@ if __name__ == '__main__':
arg0 = Str(args[0], False)
arg1 = Trim(args[1])
if len(arg1)==0:
ERR(f"need second link argument or nonempty first argument, currently empty for arguments '{a}'")
Check( len(arg1) > 0, f"need second link argument or nonempty first argument, currently empty for arguments '{a}'")
n_http = arg1.find("http://")
n_https = arg1.find("https://")
m = 7 + (1 if n_https==0 else 0)
if not (n_http==0 or n_https==0):
ERR(f"link '{arg1}' does not begin with 'http://' or 'https://' as it should")
if arg1[m]=="/":
ERR(f"link with triple '///', for '{arg1}'")
Check(n_http==0 or n_https==0, f"link '{arg1}' does not begin with 'http://' or 'https://' as it should")
Check(arg1[m]!="/", f"link with triple '///', for '{arg1}'")
if len(arg0)==0:
#n = arg1.rfind('/')
......@@ -129,14 +115,11 @@ if __name__ == '__main__':
if uselinkfilename:
k = arg0.rfind("/")
if arg0[-1]=="/":
ERR(f"can not generate file name, when link argument '{arg0}' ends in a slash '/'")
if k<=0:
ERR("could not generate filename from link '{arg0}', need at least one slash '/'")
Check(arg0[-1]!="/", f"can not generate file name, when link argument '{arg0}' ends in a slash '/'")
Check( k > 0, f"could not generate filename from link '{arg0}', need at least one slash '/'")
arg0 = arg0[k+1:]
if len(arg0)==0:
ERR("link arg0 ends-up empty, this was not the way it should be")
Check(len(arg0)>0, "link arg0 ends-up empty, this was not the way it should be")
exlink = True
if arg1.find("au.dk")>0:
......@@ -251,7 +234,7 @@ if __name__ == '__main__':
head = "<br>\n<br>\n"
tail = "<br>\n<br>\n"
else:
ERR(f"that odd, an unhandled command '{c}' that seem to be present in isCmd() list")
Check(False, f"that odd, an unhandled command '{c}' that seem to be present in isCmd() list")
closetags = f"{left}/{c}{right}{tail}" if closing else ""
v = f"{head}{left}{c}{style}{right}{a}{closetags}"
......@@ -264,7 +247,7 @@ if __name__ == '__main__':
elif c=="img":
v = Cmd.__MkImg(a, left, right)
else:
ERR(f"unknown command '{c}' with argument(s) '{a}'")
Check(False, f"unknown command '{c}' with argument(s) '{a}'")
self.__txt += v
......@@ -339,11 +322,8 @@ if __name__ == '__main__':
curr.Parse("\n")
if curr.State() != 0:
ERR(f"still in command parsing state={curr.State()}")
if len(st) != 0:
ERR(f"still {len(st)} elements on stack, expected zero")
Check(curr.State()==0, f"still in command parsing state={curr.State()}")
Check(len(st)==0, f"still {len(st)} elements on stack, expected zero")
txt = curr.Text()
Dbg(verbose, f"{Col('CYAN')}{Str(txt)}{ColEnd()}", 3)
......@@ -356,16 +336,13 @@ if __name__ == '__main__':
if len(Trim(i, False))>0:
n = Str(i).find(']')
if n<=0 or i[0]!='[':
ERR(f"refs need to be of the form '[key] value', got='{i}'")
Check(not(n<=0 or i[0]!='['), f"refs need to be of the form '[key] value', got='{i}'")
key = i[0:n+1].strip()
val = i[n+1:].strip()
if len(key)==0:
ERR(f"empty key in ref element='{i}'")
if len(val)==0:
ERR(f"empty value in ref element='{i}'")
Check(len(key) > 0, f"empty key in ref element='{i}'")
Check(len(val) > 0, f"empty value in ref element='{i}'")
Dbg(verbose, f" ParseRef(): found '{key}' => '{val}'", 2)
assert not r.get(key)
......@@ -520,13 +497,14 @@ if __name__ == '__main__':
verbose = Int(args.v)
coursefile = Str(args.c)
ouptputfiledir = Str(args.o)
Dbg(verbose, f"{Col('PURPLE')}GENERATING html course from file '{coursefile}'..{ColEnd()}")
htmlencoded = [HtmlEncode(i) for i in LoadCourseFile(coursefile)]
htmlstructure = ParseStructure(htmlencoded)
MkHtml(htmlstructure, not Bool(args.t), Str(args.o))
MkHtml(htmlstructure, not Bool(args.t), outputfiledir)
Dbg(verbose, f"{Col('PURPLE')}DONE{ColEnd()}")
......
#!/usr/bin/env python3
from Utils.dbg import ERR, WARN, isBool, isStr, isInt, isNatural, isTuple, isList, isDict
from Utils.colors import Col, ColEnd
from Utils.mkutils import *
......@@ -15,14 +14,17 @@ if __name__ == '__main__':
outputstr = ""
def CheckTuple(t, secondisstr=False):
Check(len(Tuple(t)) ==2, "not a tuple with len=2")
Check(len(Str(t[0], False))>=0, "tuple first not of string type")
Check(isinstance(t[1], str) or (not secondisstr and isinstance(t[1], dict)), f"tuple second not of expected type(s), type={type(t[1])}")
return True
def Print(msg, outputfile):
global outputstr
assert isStr(msg)
outputstr += msg + "\n"
outputstr += Str(msg) + "\n"
def Find(root, excludepat):
assert isList(excludepat)
tree = {}
tree['trees'] = []
tree['files'] = []
......@@ -34,7 +36,7 @@ if __name__ == '__main__':
d = d[2:]
skip = False
for j in excludepat:
for j in List(excludepat):
if d.find(j)>=0:
skip = True
break
......@@ -48,17 +50,9 @@ if __name__ == '__main__':
def PrintItem(i, level, isDir):
def Tab(level, tab):
assert isNatural(level)
assert isStr(tab)
assert isBool(isDir)
#if htmlmode:
# return "", ""
# #return f"<p style='padding-left: {(level+1)*40}px;'>", "</p>"
t = ""
for n in range(level):
t += tab
for n in range(Int(level)):
t += Str(tab)
return t, ""
......@@ -68,8 +62,7 @@ if __name__ == '__main__':
def Link(i, isDir):
def CheckUrl(linkurl):
assert Str(linkurl).find("'") < 0
if testurls:
if Bool(testurls):
sfx = linkurl.split(".")[-1]
if sfx!="py":
#print(f"WEB GET test {linkurl}..")
......@@ -78,42 +71,31 @@ if __name__ == '__main__':
urlretrieve(linkurl, filename=None)
except Exception as ex:
#PrettyPrintTracebackDiagnostics(ex)
print(f"{Col('LRED UNDERLINE')}EXCEPTION:{ColEnd()}{Col('LRED')} {ex}{ColEnd()}", stderr)
WARN(f"ignore test on link {linkurl}")
Warn(f"{Col('LRED UNDERLINE')}EXCEPTION:{ColEnd()}{Col('LRED')} {ex}{ColEnd()}")
Warn(f"ignore test on link {linkurl}")
return linkurl
assert isStr(url)
assert isBool(isDir)
assert isTuple(i)
assert isBool(bsfileidmode)
CheckTuple(i)
if isDir:
return i[0]
if Bool(isDir):
return Str(i[0])
assert isStr(i[0])
assert isStr(i[1])
if Bool(bsfileidmode):
filename = UrlQuote(Str(i[1]))
if bsfileidmode:
filename = Str(i[1].replace("/","%2f").replace(" ","%20"))
assert filename.find("'") < 0
assert filename.find('"') < 0
# <a href="/d2l/common/dialogs/quickLink/quickLink.d2l?ou={orgUnitId}&amp;type=coursefile&amp;fileId=Kursusfiler%2fEksamen%2fScreenshot_mathcad_navn-og-studienummer.jpg" target="_self">dsf</a>
orgUnitId = Str(str(ouid) if Int(ouid>0) else "{orgUnitId}")
orgUnitId = "{orgUnitId}"
if ouid > 0:
orgUnitId = str(ouid)
assert isStr(orgUnitId)
# From BS: <a href="/d2l/common/dialogs/quickLink/quickLink.d2l?ou={orgUnitId}&amp;type=coursefile&amp;fileId=Kursusfiler%2fEksamen%2fScreenshot_mathcad_navn-og-studienummer.jpg" target="_self">dsf</a>
r = '<a href="/d2l/common/dialogs/quickLink/quickLink.d2l?ou=' + orgUnitId + '&amp;type=coursefile&amp;fileId='+filename+'" target="_self">'+i[0]+'</a>'
else:
linkurl = CheckUrl(url + "/" + i[1])
linkurl = CheckUrl(Str(url) + "/" + i[1])
assert linkurl.find("'") < 0
r = f"<a href='{linkurl}'>{i[0]}</a>"
return r
assert isTuple(i)
assert isStr(i[0])
CheckTuple(i)
nbsp = "&nbsp;"
tab = Tab(4*level, nbsp if htmlmode else " ")
......@@ -123,36 +105,28 @@ if __name__ == '__main__':
pre = f"<span style=\"font-family: 'courier new', courier, sans-serif\">" if htmlmode else ""
post = "</span>" if htmlmode else ""
assert isTuple(tab)
CheckTuple(tab, True)
r = pre + tab[0] + link + tab[1] + Newline() + post
Print(r, outputfile)
return r
def PrintTree(tree, level=0):
assert isDict(tree)
assert isStr(url)
assert isBool(htmlmode)
files=0
dirs=0
for j in ['trees', 'files']:
t = tree[j]
assert isList(t)
d = sorted(tree[j])
assert isList(d)
t = List(Dict(tree)[j])
d = List(sorted(tree[j]))
for i in d:
CheckTuple(i, False)
isdict = isinstance(i[1], dict)
assert isTuple(i)
assert isdict == (j=='trees')
PrintItem(i, level, isdict)
files += 1
if j=='trees':
r = (PrintTree(i[1], level+1))
assert isTuple(r)
r = Tuple((PrintTree(i[1], level+1)))
files += r[0]
dirs += 1 + r[1]
......@@ -188,27 +162,21 @@ if __name__ == '__main__':
verbose = Int(args.v)
testurls = Bool(args.t)
if testurls:
WARN("test urls not implemented yet")
Check(not testurls, "test urls not implemented yet")
htmlmode = not Bool(args.plain)
header = Str(args.header, False)
url = Str(args.url)
if url[-1]=='/':
ERR("no trailing '/' in url, please remove")
Check(url[-1]!='/',"no trailing '/' in url, please remove")
outputfile = Str(args.o)
bsfileidmode = Bool(args.bsfileidmode)
ouid = Str(args.ouid, False)
if len(ouid)>0:
ouid = int(ouid)
else:
ouid = -1
assert isInt(ouid)
ouid = Int(Int(int(ouid)) if len(ouid)>0 else -1, -1)
excludepath += "," + Str(args.excludepath)
assert excludepath.find(" ") < 0
......@@ -218,8 +186,7 @@ if __name__ == '__main__':
#if bsfileidmode and len(url)>0:
# ERR("cannot specify -url and -bsfileidmode at the same time")
if ouid>0 and not bsfileidmode:
ERR("canot specify -ouid without -bsfileidmode")
Check( not( ouid>0 and not bsfileidmode), "canot specify -ouid without -bsfileidmode")
root = "./"
Dbg(verbose, f"{Col('PURPLE')}GENERATING html file tree from root '{root}'" + (("" if bsfileidmode else f", (url='{url}')") if verbose > 0 else "") + f"..{ColEnd()}")
......
#!/usr/bin/env python3
from Utils.dbg import ERR
from Utils.colors import Col, ColEnd
from Utils.mkutils import *
from sys import argv
from argparse import ArgumentParser
from html import escape
if __name__ == '__main__':
def ParseStructure(planlist):
def ParseWidths(headers):
......@@ -37,10 +34,8 @@ if __name__ == '__main__':
N = len(s)
if N != expectedlen:
if expectedlen < 0 and N < 2:
ERR(f"expected more than one column in header '{h}', but got {N} column(s)")
elif expectedlen >= 0:
ERR(f"expected exaclty {expectedlen} column(s) in line '{h}', but got {N} column(s)")
if not Check(not(expectedlen < 0 and N < 2), f"expected more than one column in header '{h}', but got {N} column(s)"):
Check(expectedlen <= 0, f"expected exactly {expectedlen} column(s) in line '{h}', but got {N} column(s)")
r = []
o = ""
......@@ -58,15 +53,13 @@ if __name__ == '__main__':
Check(planlist[i]==expected, f"planlist missing tag '{expected}' at line {i}, got='{planlist[i]}'")
def CheckContent(elem):
if Str(elem)!=elem.lower():
ERR(f"elem '{elem}' must be all-lower-case")
Check(Str(elem)==elem.lower(), f"elem '{elem}' must be all-lower-case")
t = s.get(Str(elem))
Check(isinstance(t, list), f"planlist missing {elem.upper()} structure")
N = len(List(planlist))
if N < 7:
ERR("planlist too short, expected at least four lines with COUSEPLAN/HEAD/CONTENT/[REFS]/END")
Check(N >= 7, "planlist too short, expected at least four lines with COUSEPLAN/HEAD/CONTENT/[REFS]/END")
CheckLine(0, "COUSEPLAN")
CheckLine(1, "HEAD")
......@@ -119,8 +112,6 @@ if __name__ == '__main__':
return s
def GenerateHtml(headers, widths, parsed, notes, fullhtmldoc):
def HtmlEncode(s):
return escape(Str(s, False))
def StyleSheet():
return """
......
......@@ -58,12 +58,25 @@
<span style="font-family: 'courier new', courier, sans-serif">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Dir2<br></span>
<span style="font-family: 'courier new', courier, sans-serif">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Dir3<br></span>
<span style="font-family: 'courier new', courier, sans-serif">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='https://itundervisning.ase.au.dk/ITMAL_E21/Etc/CourseBuilder/Test/Dir2/Dir3/dummy.txt'>dummy.txt</a><br></span>
<span style="font-family: 'courier new', courier, sans-serif">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='https://itundervisning.ase.au.dk/ITMAL_E21/Etc/CourseBuilder/Test/L00.html'>L00.html</a><br></span>
<span style="font-family: 'courier new', courier, sans-serif">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='https://itundervisning.ase.au.dk/ITMAL_E21/Etc/CourseBuilder/Test/L01.html'>L01.html</a><br></span>
<span style="font-family: 'courier new', courier, sans-serif">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='https://itundervisning.ase.au.dk/ITMAL_E21/Etc/CourseBuilder/Test/course.tex'>course.tex</a><br></span>
<span style="font-family: 'courier new', courier, sans-serif">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='https://itundervisning.ase.au.dk/ITMAL_E21/Etc/CourseBuilder/Test/plan.txt'>plan.txt</a><br></span>
<span style="font-family: 'courier new', courier, sans-serif">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Utils<br></span>
<span style="font-family: 'courier new', courier, sans-serif">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='https://itundervisning.ase.au.dk/ITMAL_E21/Etc/CourseBuilder/Utils/colors.py'>colors.py</a><br></span>
<span style="font-family: 'courier new', courier, sans-serif">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='https://itundervisning.ase.au.dk/ITMAL_E21/Etc/CourseBuilder/Utils/dbg.py'>dbg.py</a><br></span>
<span style="font-family: 'courier new', courier, sans-serif">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='https://itundervisning.ase.au.dk/ITMAL_E21/Etc/CourseBuilder/Utils/mkutils.py'>mkutils.py</a><br></span>
<span style="font-family: 'courier new', courier, sans-serif">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='https://itundervisning.ase.au.dk/ITMAL_E21/Etc/CourseBuilder/Dokumentation_og_links.html'>Dokumentation_og_links.html</a><br></span>
<span style="font-family: 'courier new', courier, sans-serif">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='https://itundervisning.ase.au.dk/ITMAL_E21/Etc/CourseBuilder/GPU_Cluster.html'>GPU_Cluster.html</a><br></span>
<span style="font-family: 'courier new', courier, sans-serif">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='https://itundervisning.ase.au.dk/ITMAL_E21/Etc/CourseBuilder/Journal_afleveringsformat.html'>Journal_afleveringsformat.html</a><br></span>
<span style="font-family: 'courier new', courier, sans-serif">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='https://itundervisning.ase.au.dk/ITMAL_E21/Etc/CourseBuilder/Kriterier_for_O4.html'>Kriterier_for_O4.html</a><br></span>
<span style="font-family: 'courier new', courier, sans-serif">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='https://itundervisning.ase.au.dk/ITMAL_E21/Etc/CourseBuilder/Kursusforkortelser.html'>Kursusforkortelser.html</a><br></span>
<span style="font-family: 'courier new', courier, sans-serif">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='https://itundervisning.ase.au.dk/ITMAL_E21/Etc/CourseBuilder/L00.html'>L00.html</a><br></span>
<span style="font-family: 'courier new', courier, sans-serif">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='https://itundervisning.ase.au.dk/ITMAL_E21/Etc/CourseBuilder/L01.html'>L01.html</a><br></span>
<span style="font-family: 'courier new', courier, sans-serif">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='https://itundervisning.ase.au.dk/ITMAL_E21/Etc/CourseBuilder/L02.html'>L02.html</a><br></span>
<span style="font-family: 'courier new', courier, sans-serif">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='https://itundervisning.ase.au.dk/ITMAL_E21/Etc/CourseBuilder/L03.html'>L03.html</a><br></span>
<span style="font-family: 'courier new', courier, sans-serif">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='https://itundervisning.ase.au.dk/ITMAL_E21/Etc/CourseBuilder/L04.html'>L04.html</a><br></span>
<span style="font-family: 'courier new', courier, sans-serif">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='https://itundervisning.ase.au.dk/ITMAL_E21/Etc/CourseBuilder/Litteratur.html'>Litteratur.html</a><br></span>
<span style="font-family: 'courier new', courier, sans-serif">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='https://itundervisning.ase.au.dk/ITMAL_E21/Etc/CourseBuilder/Makefile'>Makefile</a><br></span>
<span style="font-family: 'courier new', courier, sans-serif">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='https://itundervisning.ase.au.dk/ITMAL_E21/Etc/CourseBuilder/course.tex'>course.tex</a><br></span>
<span style="font-family: 'courier new', courier, sans-serif">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='https://itundervisning.ase.au.dk/ITMAL_E21/Etc/CourseBuilder/mk_course.py'>mk_course.py</a><br></span>
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment