123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475 |
- import cherrypy
- import os
- import pystache
- import humanize
- import json
- import hashlib
- import urllib.parse
- from lxml.html import parse
- from lxml.html import tostring
- from io import StringIO
- from polyglot import aggregator
- from polyglot.backends.support import ResourceCategory, ResourcePlain, ResourceRawHTML, ResourceLink, Quiz, Forum, MultipleChoiceQuestion, FreeResponseQuestion, MultipartQuestion, FileUpload
- import sys
- backend = aggregator.load_backend(sys.argv[1])
- config = {
- "Email": sys.argv[2],
- "Username": sys.argv[2],
- "Password": sys.argv[3],
- "School": sys.argv[4]
- }
- aggregator.authenticate(backend, config)
- # TODO: FIXME
- quizzes = {}
- uploads = {}
- def render_grade_summary(s):
- return str(s * 100) + "%" if s is not None else "N/A"
- cor = backend.courses
- courses = [{"uuid": c.id, "name": c.title, "code": c.teacher, "term": render_grade_summary(c.grade_summary)} for c in cor]
- task_list = []
- for c in cor:
- task_list += [{"title": task.name, "class": c.title, "due": humanize.naturaltime(task.due_date), "cid": c.id} for task in c.tasks]
- def get_course(uuid):
- for c in cor:
- if c.id == uuid:
- return c
- return None
- #TODO
- def intl(text):
- return text
- def svg_icon(text):
- if text[0] == ":":
- text = text[1:]
- if text.endswith("_link_icon"):
- text = text[0:-len("_link_icon")]
- # TODO: Don't be vulnerable FIXME
- with open("images/svg-icons/svg_icon_" + text + ".svg") as f:
- return f.read()
- def template(templ, data):
- with open(templ, "r") as f:
- return pystache.render(f.read(), data)
- def make_course_card(c):
- h = hashlib.md5(c["uuid"].encode("utf-8")).digest()
- color = "hsl(" + str(float(h[0])) + ", " + str(25 + (50*float(h[1])/256.0)) + "%, " + str(25 + (50*float(h[2])/256.0)) + "%)"
- return {
- "nickname": c["name"],
- "courseCode": c["code"],
- "originalName": c["name"],
- "term": c["term"],
- "href": "/courses?id=" + c["uuid"],
- "background": color,
- "backgroundColor": color,
- "links": [
- {"class": "announcements", "iconClass": "icon-announcement"},
- {"class": "assignments", "iconClass": "icon-assignment"},
- {"class": "discussions", "iconClass": "icon-discussion"},
- ]
- }
- def make_dashboard(courses):
- return {
- "t": intl,
- "dashboard_courses": [make_course_card(c) for c in courses]
- }
- def make_todo(assignments):
- return {
- "t": intl,
- "any_assignments": len(assignments) > 0,
- "assignments": [
- {**a, **({"classist": a["class"] is not None, "href": "/task?task=" + urllib.parse.quote(a["title"]) + "&course=" + a["cid"]}) } for a in assignments
- ]
- }
- data_todo = make_todo(task_list)
- def outer_template(content, data_todo, groups, expand=False):
- data = {
- "t": intl,
- "svg_icon": svg_icon,
- "content": content,
- "groups": groups,
- "todo": data_todo,
- "expand": "ic-Layout-expand" if expand else ""
- }
- if len(groups) > 0:
- data["show-groups"] = True
- templated = template("views/layouts/application.mustache", data)
- # Right before it hits the browser, transform any emitted links
- # This will ensure the user does not accidentally click to e.g Google Docs
- tree = parse(StringIO(templated)).getroot()
- for link in tree.cssselect("a"):
- if "href" in link.attrib:
- link.attrib["href"] = aggregator.transform(link.attrib["href"])
- return tostring(tree).decode("utf-8")
- def inner_template(t, d, data_todo, groups, expand=False):
- return outer_template(lambda x: template(t, d), data_todo, groups, expand)
- def serialise_post(post, base):
- return {
- "canReply": False,
- "user": post.author,
- "message": post.body,
- "date": "TODO",
- "reply-href": base + "&reply=" + post.id,
- "replies": [serialise_post(r, base) for r in post.replies]
- }
- def dump_resource(rsrc, expand, base):
- out = ""
- if isinstance(rsrc, ResourceCategory):
- out += "<h1>" + rsrc.name + "</h1><ul>"
- for child in rsrc.children:
- URL = base + "/" + urllib.parse.quote(child.name)
- out += "<li><a href='" + URL + "'>" + child.name + "</a></li>"
- out += "</ul>"
- elif isinstance(rsrc, ResourceRawHTML):
- out += "<h1>" + rsrc.name + "</h1>" + rsrc.html
- elif isinstance(rsrc, ResourcePlain):
- out += "<h1>" + rsrc.name + "</h1>" + rsrc.text
- elif isinstance(rsrc, ResourceLink):
- out += "<p><a href='" + rsrc.url + "'>" + rsrc.name + "</a></p>"
- elif not expand and (isinstance(rsrc, Quiz) or isinstance(rsrc, Forum)):
- out += "<p><a href='" + base + "/" + rsrc.name + "&full=1'>" + rsrc.name + "</a></p>"
- elif expand and isinstance(rsrc, Quiz):
- questions = rsrc.questions()
- print(questions)
- def format_part(q):
- if isinstance(q, MultipleChoiceQuestion):
- return {
- "multiple_dropdowns_question": True,
- "question": q.prompt,
- "answers": [{"aid": 0, "text": text} for text in q.responses],
- "parts": []
- }
- elif isinstance(q, FreeResponseQuestion):
- return {
- "free_response": True,
- "long": True, # TODO
- "question": q.prompt,
- "parts": [],
- }
- elif isinstance(q, MultipartQuestion):
- return {
- "parts": [format_part(part) for part in q.parts]
- }
- data = {
- "t": intl,
- "title": rsrc.name,
- "id": rsrc.id,
- "action": "/submit_quiz",
- "description": None,
- "started_at": None,
- "previous_btn": False,
- "next_btn": False,
- "questions": [{"id": i, "name": intl("Question ") + str(i + 1), "question": format_part(q)} for i, q in enumerate(questions)]
- }
- quizzes[rsrc.id] = rsrc
- out += template("views/quizzes/quizzes/take_quiz.mustache", data)
- elif expand and isinstance(rsrc, Forum):
- data = serialise_post(rsrc.thread, base)
- data["t"] = intl
- if hasattr(rsrc, "title"):
- data["title"] = rsrc.title
- out += template("views/discussion_topics/show.mustache", data)
- elif isinstance(rsrc, FileUpload):
- uploads[rsrc.id] = rsrc
- data = {"id": rsrc.id, "name": rsrc.name}
- out += template("views/jst/re_upload_submissions_form.mustache", data)
- else:
- out += "<h1>" + rsrc.name + "</h1>Unknown element of type " + type(rsrc).__name__
- return out
- resource_cache = {}
- def path_serialise(path):
- return "/".join(map(str, path))
- def find_resource(root, path, ref):
- if len(path) == 0:
- return root
- if len(ref) == 0:
- try:
- almost = resource_cache[path_serialise(path[:-1])]
- return find_resource(almost, [path[-1]], path[:-1])
- except KeyError:
- pass
- if isinstance(root, ResourceCategory):
- for child in root.children:
- if child.name == path[0]:
- rsrc = find_resource(child, path[1:], ref + [path[0]])
- resource_cache[path_serialise(ref + path)] = rsrc
- return rsrc
- print("Can't find " + path[0])
- return
- print("Wrong rsrc type with " + ",".join(path))
- def find_task(lst, name):
- for task in lst:
- if task.name == name:
- return task
- def find_thread(lst, id):
- for msg in lst:
- if msg.id == id:
- return msg
- class PaletteServer(object):
- @cherrypy.expose
- def index(self):
- return self.dashboard()
- @cherrypy.expose
- def login(self):
- return inner_template("views/login/canvas/new_login_content.mustache", make_dashboard([]), [], [])
- @cherrypy.expose
- def do_login(self, email, password, school):
- print("Login ", email, password, school)
- return self.dashboard()
- @cherrypy.expose
- def submit_quiz(self, id, **kwargs):
- quiz = quizzes[id]
- questions = quiz.questions()
- print(id)
- print(kwargs)
- for name, value in kwargs.items():
- no = int(name[len("question_"):])
- q = questions[no]
- if isinstance(q, MultipleChoiceQuestion) or isinstance(q, FreeResponseQuestion):
- q.response = value
- elif isinstance(q, MultipartQuestion):
- for (part, response) in zip(q.parts, value):
- part.response = response
- else:
- print("Unknown question type")
- print(q)
- quiz.submit(questions)
- return self.dashboard()
- @cherrypy.expose
- def submit_file(self, id, submission):
- uploads[id].upload(submission)
- return self.dashboard()
- @cherrypy.expose
- def dashboard(self):
- return inner_template("views/users/user_dashboard.mustache", make_dashboard(courses), data_todo, [])
- @cherrypy.expose
- def grades(self):
- data_all_grades = {
- "t": intl,
- "courses": [{"name": c.title, "grade": render_grade_summary(c.grade_summary) } for c in cor]
- }
- return inner_template("views/users/grades.mustache", data_all_grades, data_todo, [])
- @cherrypy.expose
- def conversations(self, submit=False, compose=False, id=None, subject="", to="", course="", body=""):
- inbox = backend.inbox
- if submit:
- inbox.send(to, subject, body)
- if not compose:
- msgs = inbox.threads
- summaries = [{
- "read": "read",
- "date": thread.messages[0].date,
- "participants": thread.messages[0].sender,
- "subject": thread.messages[0].subject,
- "message_count": 0,
- "id": thread.id,
- "summary": "" # TODO: Fix
- } for thread in msgs]
- thread = find_thread(msgs, id)
- replies = [{
- "avatar": None,
- "author": msg.sender,
- "date": msg.date,
- "body": msg.body # TODO sanitize
- } for msg in thread.messages] if thread is not None else []
- subject = thread.messages[0].subject if thread else ""
- data = {
- "t": intl,
- "messages": summaries,
- "subject": subject,
- "replies": replies,
- "id": id
- }
- return inner_template("views/conversations/index_new.mustache", data, [], [], expand=True)
- else:
- data = {
- "t": intl,
- "id": id if id else "new"
- }
- return inner_template("views/jst/conversations/MessageFormDialog.mustache", data, [], [])
- @cherrypy.expose
- def task(self, course, task):
- t = find_task(get_course(course).tasks, task)
- o = "<h1>" + t.name + "</h1>" + "<p>" + humanize.naturaltime(t.due_date) + "</p>"
- rsrcs = t.resources
- if rsrcs is not None:
- for r in rsrcs:
- o += dump_resource(r, True, "/task?course=" + course + "&task=" + task)
- return outer_template(lambda _: o, data_todo, [])
- # Fetches an arbitrary URL in our context. Kind of a hack but shrug.
- @cherrypy.expose
- def wget(self, url):
- url = urllib.parse.unquote(url)
- print(url)
- # Set headers
- filename = urllib.parse.urlparse(url).path.split("/")[-1]
- cherrypy.response.headers["Content-Type"] = "application/octet-stream"
- cherrypy.response.headers["Content-Disposition"] = 'attachment; filename="' + filename + '"'
- # Fetch and return
- u = backend.session.get(aggregator.transform(url))
- return u.content
- @cherrypy.expose
- def courses(self, id, active="grades", data="", full=False, reply=None, do=None, args=""):
- course = get_course(id)
- # Filter TODO list for course
- course_todo = make_todo([t for t in task_list if t["cid"] == course.id])
- groups = [
- {"type": "grades", "href": "/courses?id=" + id + "&active=grades", "name": "Grades", "show": True},
- {"type": "modules", "href": "/courses?id=" + id + "&active=modules", "name": "Modules", "show": True}
- ]
- for g in groups:
- if g["type"] == active:
- g["active"] = "active"
- if active == "grades":
- report = course.grades
- lst = []
- for category in report:
- for grade in category.grades:
- grade = {
- "unread": False,
- "title": grade.name,
- "due": "N/A",
- "score": grade.grade,
- "possible": grade.possible,
- "category": category.name,
- "comment": grade.comment
- }
- lst += [grade]
- data_grades = {
- "t": intl,
- "grades": lst,
- "total": render_grade_summary(course.grade_summary)
- }
- return inner_template("views/gradebooks/grade_summary.mustache", data_grades, course_todo, groups)
- elif active == "modules":
- path = urllib.parse.unquote(data).split("/")
- rsrcs = ResourceCategory("Root", course.resources)
- rsrc = find_resource(rsrcs, path[1:-1] if full else path[1:], [])
- # Don't show TODO on special resources
- if isinstance(rsrc, Forum) or isinstance(rsrc, Quiz):
- course_todo = []
- if full or (do == "reply" and not hasattr(rsrc, "thread")):
- contents = rsrc.children
- for con in contents:
- if con.name == path[-1]:
- rsrc = con
- if do == "reply":
- thread = rsrc.thread
- rsrc.reply(rsrc.posts[reply], args)
- elif reply is not None:
- form = {
- "t": intl,
- "id": id,
- "active": "modules",
- "data": data,
- "action": "/courses",
- "reply": reply
- }
- return inner_template("views/jst/discussions/reply_form.mustache", form, course_todo, groups)
- return outer_template(lambda _: dump_resource(rsrc, True, "/courses?id=" + id + "&active=modules&data=" + urllib.parse.quote(data)), course_todo, groups)
- else:
- return inner_template("views/error.mustache", {}, [], [])
- conf = {
- '/': {
- 'tools.staticdir.root': os.path.abspath(os.getcwd())
- },
- '/stylesheets': {
- 'tools.staticdir.on': True,
- 'tools.staticdir.dir': './stylesheets'
- }
- }
- cherrypy.quickstart(PaletteServer(), '/', conf)
|