web-server.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475
  1. import cherrypy
  2. import os
  3. import pystache
  4. import humanize
  5. import json
  6. import hashlib
  7. import urllib.parse
  8. from lxml.html import parse
  9. from lxml.html import tostring
  10. from io import StringIO
  11. from polyglot import aggregator
  12. from polyglot.backends.support import ResourceCategory, ResourcePlain, ResourceRawHTML, ResourceLink, Quiz, Forum, MultipleChoiceQuestion, FreeResponseQuestion, MultipartQuestion, FileUpload
  13. import sys
  14. backend = aggregator.load_backend(sys.argv[1])
  15. config = {
  16. "Email": sys.argv[2],
  17. "Username": sys.argv[2],
  18. "Password": sys.argv[3],
  19. "School": sys.argv[4]
  20. }
  21. aggregator.authenticate(backend, config)
  22. # TODO: FIXME
  23. quizzes = {}
  24. uploads = {}
  25. def render_grade_summary(s):
  26. return str(s * 100) + "%" if s is not None else "N/A"
  27. cor = backend.courses
  28. courses = [{"uuid": c.id, "name": c.title, "code": c.teacher, "term": render_grade_summary(c.grade_summary)} for c in cor]
  29. task_list = []
  30. for c in cor:
  31. task_list += [{"title": task.name, "class": c.title, "due": humanize.naturaltime(task.due_date), "cid": c.id} for task in c.tasks]
  32. def get_course(uuid):
  33. for c in cor:
  34. if c.id == uuid:
  35. return c
  36. return None
  37. #TODO
  38. def intl(text):
  39. return text
  40. def svg_icon(text):
  41. if text[0] == ":":
  42. text = text[1:]
  43. if text.endswith("_link_icon"):
  44. text = text[0:-len("_link_icon")]
  45. # TODO: Don't be vulnerable FIXME
  46. with open("images/svg-icons/svg_icon_" + text + ".svg") as f:
  47. return f.read()
  48. def template(templ, data):
  49. with open(templ, "r") as f:
  50. return pystache.render(f.read(), data)
  51. def make_course_card(c):
  52. h = hashlib.md5(c["uuid"].encode("utf-8")).digest()
  53. color = "hsl(" + str(float(h[0])) + ", " + str(25 + (50*float(h[1])/256.0)) + "%, " + str(25 + (50*float(h[2])/256.0)) + "%)"
  54. return {
  55. "nickname": c["name"],
  56. "courseCode": c["code"],
  57. "originalName": c["name"],
  58. "term": c["term"],
  59. "href": "/courses?id=" + c["uuid"],
  60. "background": color,
  61. "backgroundColor": color,
  62. "links": [
  63. {"class": "announcements", "iconClass": "icon-announcement"},
  64. {"class": "assignments", "iconClass": "icon-assignment"},
  65. {"class": "discussions", "iconClass": "icon-discussion"},
  66. ]
  67. }
  68. def make_dashboard(courses):
  69. return {
  70. "t": intl,
  71. "dashboard_courses": [make_course_card(c) for c in courses]
  72. }
  73. def make_todo(assignments):
  74. return {
  75. "t": intl,
  76. "any_assignments": len(assignments) > 0,
  77. "assignments": [
  78. {**a, **({"classist": a["class"] is not None, "href": "/task?task=" + urllib.parse.quote(a["title"]) + "&course=" + a["cid"]}) } for a in assignments
  79. ]
  80. }
  81. data_todo = make_todo(task_list)
  82. def outer_template(content, data_todo, groups, expand=False):
  83. data = {
  84. "t": intl,
  85. "svg_icon": svg_icon,
  86. "content": content,
  87. "groups": groups,
  88. "todo": data_todo,
  89. "expand": "ic-Layout-expand" if expand else ""
  90. }
  91. if len(groups) > 0:
  92. data["show-groups"] = True
  93. templated = template("views/layouts/application.mustache", data)
  94. # Right before it hits the browser, transform any emitted links
  95. # This will ensure the user does not accidentally click to e.g Google Docs
  96. tree = parse(StringIO(templated)).getroot()
  97. for link in tree.cssselect("a"):
  98. if "href" in link.attrib:
  99. link.attrib["href"] = aggregator.transform(link.attrib["href"])
  100. return tostring(tree).decode("utf-8")
  101. def inner_template(t, d, data_todo, groups, expand=False):
  102. return outer_template(lambda x: template(t, d), data_todo, groups, expand)
  103. def serialise_post(post, base):
  104. return {
  105. "canReply": False,
  106. "user": post.author,
  107. "message": post.body,
  108. "date": "TODO",
  109. "reply-href": base + "&reply=" + post.id,
  110. "replies": [serialise_post(r, base) for r in post.replies]
  111. }
  112. def dump_resource(rsrc, expand, base):
  113. out = ""
  114. if isinstance(rsrc, ResourceCategory):
  115. out += "<h1>" + rsrc.name + "</h1><ul>"
  116. for child in rsrc.children:
  117. URL = base + "/" + urllib.parse.quote(child.name)
  118. out += "<li><a href='" + URL + "'>" + child.name + "</a></li>"
  119. out += "</ul>"
  120. elif isinstance(rsrc, ResourceRawHTML):
  121. out += "<h1>" + rsrc.name + "</h1>" + rsrc.html
  122. elif isinstance(rsrc, ResourcePlain):
  123. out += "<h1>" + rsrc.name + "</h1>" + rsrc.text
  124. elif isinstance(rsrc, ResourceLink):
  125. out += "<p><a href='" + rsrc.url + "'>" + rsrc.name + "</a></p>"
  126. elif not expand and (isinstance(rsrc, Quiz) or isinstance(rsrc, Forum)):
  127. out += "<p><a href='" + base + "/" + rsrc.name + "&full=1'>" + rsrc.name + "</a></p>"
  128. elif expand and isinstance(rsrc, Quiz):
  129. questions = rsrc.questions()
  130. print(questions)
  131. def format_part(q):
  132. if isinstance(q, MultipleChoiceQuestion):
  133. return {
  134. "multiple_dropdowns_question": True,
  135. "question": q.prompt,
  136. "answers": [{"aid": 0, "text": text} for text in q.responses],
  137. "parts": []
  138. }
  139. elif isinstance(q, FreeResponseQuestion):
  140. return {
  141. "free_response": True,
  142. "long": True, # TODO
  143. "question": q.prompt,
  144. "parts": [],
  145. }
  146. elif isinstance(q, MultipartQuestion):
  147. return {
  148. "parts": [format_part(part) for part in q.parts]
  149. }
  150. data = {
  151. "t": intl,
  152. "title": rsrc.name,
  153. "id": rsrc.id,
  154. "action": "/submit_quiz",
  155. "description": None,
  156. "started_at": None,
  157. "previous_btn": False,
  158. "next_btn": False,
  159. "questions": [{"id": i, "name": intl("Question ") + str(i + 1), "question": format_part(q)} for i, q in enumerate(questions)]
  160. }
  161. quizzes[rsrc.id] = rsrc
  162. out += template("views/quizzes/quizzes/take_quiz.mustache", data)
  163. elif expand and isinstance(rsrc, Forum):
  164. data = serialise_post(rsrc.thread, base)
  165. data["t"] = intl
  166. if hasattr(rsrc, "title"):
  167. data["title"] = rsrc.title
  168. out += template("views/discussion_topics/show.mustache", data)
  169. elif isinstance(rsrc, FileUpload):
  170. uploads[rsrc.id] = rsrc
  171. data = {"id": rsrc.id, "name": rsrc.name}
  172. out += template("views/jst/re_upload_submissions_form.mustache", data)
  173. else:
  174. out += "<h1>" + rsrc.name + "</h1>Unknown element of type " + type(rsrc).__name__
  175. return out
  176. resource_cache = {}
  177. def path_serialise(path):
  178. return "/".join(map(str, path))
  179. def find_resource(root, path, ref):
  180. if len(path) == 0:
  181. return root
  182. if len(ref) == 0:
  183. try:
  184. almost = resource_cache[path_serialise(path[:-1])]
  185. return find_resource(almost, [path[-1]], path[:-1])
  186. except KeyError:
  187. pass
  188. if isinstance(root, ResourceCategory):
  189. for child in root.children:
  190. if child.name == path[0]:
  191. rsrc = find_resource(child, path[1:], ref + [path[0]])
  192. resource_cache[path_serialise(ref + path)] = rsrc
  193. return rsrc
  194. print("Can't find " + path[0])
  195. return
  196. print("Wrong rsrc type with " + ",".join(path))
  197. def find_task(lst, name):
  198. for task in lst:
  199. if task.name == name:
  200. return task
  201. def find_thread(lst, id):
  202. for msg in lst:
  203. if msg.id == id:
  204. return msg
  205. class PaletteServer(object):
  206. @cherrypy.expose
  207. def index(self):
  208. return self.dashboard()
  209. @cherrypy.expose
  210. def login(self):
  211. return inner_template("views/login/canvas/new_login_content.mustache", make_dashboard([]), [], [])
  212. @cherrypy.expose
  213. def do_login(self, email, password, school):
  214. print("Login ", email, password, school)
  215. return self.dashboard()
  216. @cherrypy.expose
  217. def submit_quiz(self, id, **kwargs):
  218. quiz = quizzes[id]
  219. questions = quiz.questions()
  220. print(id)
  221. print(kwargs)
  222. for name, value in kwargs.items():
  223. no = int(name[len("question_"):])
  224. q = questions[no]
  225. if isinstance(q, MultipleChoiceQuestion) or isinstance(q, FreeResponseQuestion):
  226. q.response = value
  227. elif isinstance(q, MultipartQuestion):
  228. for (part, response) in zip(q.parts, value):
  229. part.response = response
  230. else:
  231. print("Unknown question type")
  232. print(q)
  233. quiz.submit(questions)
  234. return self.dashboard()
  235. @cherrypy.expose
  236. def submit_file(self, id, submission):
  237. uploads[id].upload(submission)
  238. return self.dashboard()
  239. @cherrypy.expose
  240. def dashboard(self):
  241. return inner_template("views/users/user_dashboard.mustache", make_dashboard(courses), data_todo, [])
  242. @cherrypy.expose
  243. def grades(self):
  244. data_all_grades = {
  245. "t": intl,
  246. "courses": [{"name": c.title, "grade": render_grade_summary(c.grade_summary) } for c in cor]
  247. }
  248. return inner_template("views/users/grades.mustache", data_all_grades, data_todo, [])
  249. @cherrypy.expose
  250. def conversations(self, submit=False, compose=False, id=None, subject="", to="", course="", body=""):
  251. inbox = backend.inbox
  252. if submit:
  253. inbox.send(to, subject, body)
  254. if not compose:
  255. msgs = inbox.threads
  256. summaries = [{
  257. "read": "read",
  258. "date": thread.messages[0].date,
  259. "participants": thread.messages[0].sender,
  260. "subject": thread.messages[0].subject,
  261. "message_count": 0,
  262. "id": thread.id,
  263. "summary": "" # TODO: Fix
  264. } for thread in msgs]
  265. thread = find_thread(msgs, id)
  266. replies = [{
  267. "avatar": None,
  268. "author": msg.sender,
  269. "date": msg.date,
  270. "body": msg.body # TODO sanitize
  271. } for msg in thread.messages] if thread is not None else []
  272. subject = thread.messages[0].subject if thread else ""
  273. data = {
  274. "t": intl,
  275. "messages": summaries,
  276. "subject": subject,
  277. "replies": replies,
  278. "id": id
  279. }
  280. return inner_template("views/conversations/index_new.mustache", data, [], [], expand=True)
  281. else:
  282. data = {
  283. "t": intl,
  284. "id": id if id else "new"
  285. }
  286. return inner_template("views/jst/conversations/MessageFormDialog.mustache", data, [], [])
  287. @cherrypy.expose
  288. def task(self, course, task):
  289. t = find_task(get_course(course).tasks, task)
  290. o = "<h1>" + t.name + "</h1>" + "<p>" + humanize.naturaltime(t.due_date) + "</p>"
  291. rsrcs = t.resources
  292. if rsrcs is not None:
  293. for r in rsrcs:
  294. o += dump_resource(r, True, "/task?course=" + course + "&task=" + task)
  295. return outer_template(lambda _: o, data_todo, [])
  296. # Fetches an arbitrary URL in our context. Kind of a hack but shrug.
  297. @cherrypy.expose
  298. def wget(self, url):
  299. url = urllib.parse.unquote(url)
  300. print(url)
  301. # Set headers
  302. filename = urllib.parse.urlparse(url).path.split("/")[-1]
  303. cherrypy.response.headers["Content-Type"] = "application/octet-stream"
  304. cherrypy.response.headers["Content-Disposition"] = 'attachment; filename="' + filename + '"'
  305. # Fetch and return
  306. u = backend.session.get(aggregator.transform(url))
  307. return u.content
  308. @cherrypy.expose
  309. def courses(self, id, active="grades", data="", full=False, reply=None, do=None, args=""):
  310. course = get_course(id)
  311. # Filter TODO list for course
  312. course_todo = make_todo([t for t in task_list if t["cid"] == course.id])
  313. groups = [
  314. {"type": "grades", "href": "/courses?id=" + id + "&active=grades", "name": "Grades", "show": True},
  315. {"type": "modules", "href": "/courses?id=" + id + "&active=modules", "name": "Modules", "show": True}
  316. ]
  317. for g in groups:
  318. if g["type"] == active:
  319. g["active"] = "active"
  320. if active == "grades":
  321. report = course.grades
  322. lst = []
  323. for category in report:
  324. for grade in category.grades:
  325. grade = {
  326. "unread": False,
  327. "title": grade.name,
  328. "due": "N/A",
  329. "score": grade.grade,
  330. "possible": grade.possible,
  331. "category": category.name,
  332. "comment": grade.comment
  333. }
  334. lst += [grade]
  335. data_grades = {
  336. "t": intl,
  337. "grades": lst,
  338. "total": render_grade_summary(course.grade_summary)
  339. }
  340. return inner_template("views/gradebooks/grade_summary.mustache", data_grades, course_todo, groups)
  341. elif active == "modules":
  342. path = urllib.parse.unquote(data).split("/")
  343. rsrcs = ResourceCategory("Root", course.resources)
  344. rsrc = find_resource(rsrcs, path[1:-1] if full else path[1:], [])
  345. # Don't show TODO on special resources
  346. if isinstance(rsrc, Forum) or isinstance(rsrc, Quiz):
  347. course_todo = []
  348. if full or (do == "reply" and not hasattr(rsrc, "thread")):
  349. contents = rsrc.children
  350. for con in contents:
  351. if con.name == path[-1]:
  352. rsrc = con
  353. if do == "reply":
  354. thread = rsrc.thread
  355. rsrc.reply(rsrc.posts[reply], args)
  356. elif reply is not None:
  357. form = {
  358. "t": intl,
  359. "id": id,
  360. "active": "modules",
  361. "data": data,
  362. "action": "/courses",
  363. "reply": reply
  364. }
  365. return inner_template("views/jst/discussions/reply_form.mustache", form, course_todo, groups)
  366. return outer_template(lambda _: dump_resource(rsrc, True, "/courses?id=" + id + "&active=modules&data=" + urllib.parse.quote(data)), course_todo, groups)
  367. else:
  368. return inner_template("views/error.mustache", {}, [], [])
  369. conf = {
  370. '/': {
  371. 'tools.staticdir.root': os.path.abspath(os.getcwd())
  372. },
  373. '/stylesheets': {
  374. 'tools.staticdir.on': True,
  375. 'tools.staticdir.dir': './stylesheets'
  376. }
  377. }
  378. cherrypy.quickstart(PaletteServer(), '/', conf)