def pull(db, course_, quiz_, dry_run): course_id = course_.canvas_id quiz_id = quiz_.get('id') if not quiz_id: logging.error(f"Quiz {quiz_id} does not exist for course {course_id}") return None, None cid = canvas_id.find_by_id(db, course_id, quiz_id) if cid: quiz_['filename'] = cid.filename else: quiz_['filename'] = component.gen_filename(QUIZZES_DIR, quiz_.get('title', '')) cid = canvas_id.CanvasID(quiz_['filename'], course_id) cid.canvas_id = quiz_.get('id') cid.save(db) # check assignment_group_id to fill in assignment_group by name agid = quiz_.get('assignment_group_id') if agid: # first check if we have a cid for the assignment group agcid = canvas_id.find_by_id(db, course_id, agid) if agcid: ag = helpers_yaml.read(agcid.filename) if ag: quiz_['assignment_group'] = ag.name else: logging.error("failed to find the assignment group for " f"the assignment group with id {agid}. Your " ".easeldb may be out of sync") else: # we could look at all the local assignment group files if we # don't have a cid for it but chances are there isn't a file. # so might as well just go back to canvas and ask for it agpath = assignment_group.ASSIGN_GROUP_PATH.format(course_id, agid) r = helpers.get(agpath, dry_run=dry_run) if 'name' in r: quiz_['assignment_group'] = r['name'] else: logging.error("TODO: invalid response from canvas for " "the assignment group: " + json.dumps(r, indent=4)) # quiz questions quiz_questions_path = QUIZ_PATH.format(course_.canvas_id, quiz_id) + "/questions" quiz_questions = helpers.get(quiz_questions_path) quiz_['quiz_questions'] = quiz_questions return Quiz.build(quiz_), cid
def pull(self, db, course_, dry_run): path = NAV_TABS_PATH.format(course_.canvas_id) resp = helpers.get(path, dry_run=dry_run) nav_tabs = [] for nav_tab in resp: nav_tabs.append(NavigationTab(**nav_tab)) return NavigationTabs(nav_tabs)
def pull(self, db, course_, dry_run): cid = canvas_id.CanvasID(self.filename, course_.canvas_id) cid.find_id(db) path = self.format_update_path(db, course_.canvas_id, cid.canvas_id) resp = helpers.get(path, dry_run=dry_run) remote = self.__class__.build(resp) remote.filename = self.filename return remote
def pull_all(db, course_, dry_run): r = helpers.get(PAGES_PATH.format(course_.canvas_id), dry_run=dry_run) pages = [] print("pulling page contents") for p in tqdm(r): page_, _ = pull_page(db, course_.canvas_id, p.get('url'), dry_run) pages.append(page_) return pages
def pull_all(db, course_, dry_run): r = helpers.get(QUIZZES_PATH.format(course_.canvas_id), dry_run=dry_run) quizzes = [] print("pulling quiz questions") for quiz_json in tqdm(r): quiz_, _ = pull(db, course_, quiz_json, dry_run) if quiz_: quizzes.append(quiz_) return quizzes
def pull(db, course_, assignment_id, dry_run): course_id = course_.canvas_id a = helpers.get(ASSIGNMENT_PATH.format(course_id, assignment_id), dry_run=dry_run) if not a.get('id'): logging.error(f"Assignment {assignment_id} does not exist for course {course_id}") return None, None cid = canvas_id.find_by_id(db, course_id, a.get('id')) if cid: a['filename'] = cid.filename else: a['filename'] = component.gen_filename(ASSIGNMENTS_DIR, a.get('name','')) cid = canvas_id.CanvasID(a['filename'], course_id) cid.canvas_id = a.get('id') cid.save(db) # check assignment_group_id to fill in assignment_group by name agid = a.get('assignment_group_id') if agid: # first check if we have a cid for the assignment group agcid = canvas_id.find_by_id(db, course_id, agid) if agcid: ag = helpers_yaml.read(agcid.filename) if ag: a['assignment_group'] = ag.name else: logging.error("failed to find the assignment group for " f"the assignment group with id {agid}. Your " ".easeldb may be out of sync") else: # we could look at all the local assignment group files if we # don't have a cid for it but chances are there isn't a file. # so might as well just go back to canvas and ask for it agpath = assignment_group.ASSIGN_GROUP_PATH.format(course_id, agid) r = helpers.get(agpath, dry_run=dry_run) if 'name' in r: a['assignment_group'] = r['name'] else: logging.error("TODO: invalid response from canvas for " "the assignment group: " + json.dumps(r, indent=4)) return Assignment.build(a), cid
def pull_page(db, course_id, page_url, dry_run): page_ = helpers.get(PAGE_PATH.format(course_id, page_url), dry_run=dry_run) cid = canvas_id.find_by_id(db, course_id, page_.get('url')) if cid: page_['filename'] = cid.filename else: page_['filename'] = component.gen_filename(PAGES_DIR, page_.get('title', '')) cid = canvas_id.CanvasID(page_['filename'], course_id) cid.canvas_id = page_.get('url') cid.save(db) return Page.build(page_), cid
def pull_all(db, course_, dry_run): r = helpers.get(ASSIGNMENTS_PATH.format(course_.canvas_id), dry_run=dry_run) assignments = [] print("pulling assignment contents") for assignment in tqdm(r): # skip quizzes (handle in quiz.py) if assignment.get("is_quiz_assignment"): continue assignment_, _ = pull(db, course_, assignment.get('id'), dry_run) if assignment_: assignments.append(assignment_) return assignments
def pull_all(db, course_, dry_run): r = helpers.get(COURSE_FOLDERS_PATH.format(course_.canvas_id), dry_run=dry_run) for folder in r: path = folder['full_name'][len("course "):] print(f"pulling list of files in {path}") os.makedirs(path, exist_ok=True) files_path = urllib.parse.urlparse(folder['files_url']).path r = helpers.get(files_path, dry_run=dry_run) print(f"downloading files in {path}") for file_ in tqdm(r): if 'filename' not in file_ or 'url' not in file_ or 'id' not in file_: logging.error("Invalid file response from canvas:") print(file_) continue filepath = path + '/' + file_['filename'] pull_file(db, course_.canvas_id, file_['url'], file_['id'], filepath) # normally we'd return a list of file objects for diffing the local ones, # but since we don't track files in the db we won't be able to diff # anything (in the the way it's done in commands.py) # TODO: but it might be worth returning the contents of the files, although # we'd want to optimize for memory usage in case of large files return []
def pull_all(db, course_, dry_run): r = helpers.get(ASSIGN_GROUPS_PATH.format(course_.canvas_id), dry_run=dry_run) ags = [] for ag in tqdm(r): cid = canvas_id.find_by_id(db, course_.canvas_id, ag.get('id')) if cid: ag['filename'] = cid.filename else: ag['filename'] = component.gen_filename(ASSIGN_GROUPS_DIR, ag.get('name', '')) cid = canvas_id.CanvasID(ag['filename'], course_.canvas_id) cid.canvas_id = ag.get('id') cid.save(db) ags.append(AssignmentGroup.build(ag)) return ags
def cmd_login(db, args): hostname = args.hostname token = args.token protocol = "https://" if hostname.startswith(protocol): hostname = hostname[len(protocol):] if hostname.endswith("/"): hostname = hostname[:len(hostname)-1] if not helpers.write_config(hostname, token, args.dry_run): return # confirm user logged in correctly resp = helpers.get("/api/v1/users/self") if resp and isinstance(resp, dict) and "name" in resp and resp["name"]: print("Found user:"******"There was an issue logging you in. Check the hostname " "of your canvas instance as well as your token.") logging.debug(resp)
def postprocess(self, db, course_, dry_run): course_id = course_.canvas_id cid = canvas_id.CanvasID(self.filename, course_id) cid.find_id(db) if not cid.canvas_id: print(f"failed to add QuizQuestions to {self}, we don't " "have a canvas id for it") print("make sure the quiz has first been pushed to Canvas") return # delete any existing questions on the quiz # we'll use the local file as the source of truth and always update # canvas to match it quiz_path = QUIZ_PATH.format(course_id, cid.canvas_id) questions_path = quiz_path + "/questions" quiz_questions = helpers.get(questions_path, dry_run) for question in quiz_questions: if 'id' in question: path = questions_path + "/{}".format(question['id']) helpers.delete(path) # prepare actual QuizQuestion objects to be pushed to canvas questions = build_questions(self.quiz_questions) # push the questions for question in questions: print(f"\tpushing {question} to Quiz {self}") question.push(db, course_, dry_run, parent_component=self) # once I push the questions, canvas doesn't seem to update the # quiz's points possible until I save the entire quiz again... # https://community.canvaslms.com/t5/Question-Forum/Saving-Quizzes-w-API/td-p/226406 # turns out that canvas won't do this if the quiz is unpublished when # you create the questions. so I'm hackily unpublishing and then # publishing (if the user wants to publish it) if self.remember_published: helpers.put(quiz_path, {"quiz": {"published": True}})
def pull(db, course_id): params = {"include[]": "syllabus_body"} response = helpers.get(COURSE_PATH.format(course_id), params=params) response["canvas_id"] = response["id"] return Course(response["id"], response["name"], response["course_code"], response["workflow_state"], response["syllabus_body"])
def pull_all(db, course_, dry_run): modules = [] m = helpers.get(MODULES_PATH.format(course_.canvas_id), params={'include': ['items']}, dry_run=dry_run) for module_ in m: if 'items' not in module_ or not module_['items']: items_path = urllib.parse.urlparse(module_['items_url']).path module_['items'] = helpers.get(items_path, dry_run=dry_run) cid = canvas_id.find_by_id(db, course_.canvas_id, module_.get('id')) if cid: module_['filename'] = cid.filename else: module_['filename'] = component.gen_filename(MODULES_DIR, str(module_.get('position')) + '-' + module_.get('name','')) cid = canvas_id.CanvasID(module_['filename'], course_.canvas_id) cid.canvas_id = module_.get('id') cid.save(db) items = {} for item in module_['items']: item_id = 0 if item['type'] == "Page": item_id = item['page_url'] elif item['type'] in ['File', 'Assignment', 'Quiz']: item_id = item['content_id'] # SubHeaders and ExternalUrls won't be in the db anyway icid = canvas_id.find_by_id(db, course_.canvas_id, item_id) if item_id == 0 or not icid: # we don't have the item locally so try to pull it if item['type'] == "Page": _, icid = page.pull_page(db, course_.canvas_id, item['page_url'], dry_run) elif item['type'] == "File": # get file info file_path = urllib.parse.urlparse(item['url']).path file_ = helpers.get(file_path, dry_run=dry_run) url = file_['url'] # get parent folder info folder_path = files.COURSE_FOLDERS_PATH.format(course_.canvas_id)+"/"+str(file_['folder_id']) folder = helpers.get(folder_path, dry_run=dry_run) path = folder['full_name'][len("course "):] filepath = path + file_['filename'] # download the file icid = files.pull_file(db, course_.canvas_id, url, file_['id'], filepath) elif item['type'] == 'Assignment': _, icid = assignment.pull(db, course_, item_id, dry_run) elif item['type'] == 'Quiz': _, icid = quiz.pull(db, course_, item, dry_run) elif item['type'] == 'SubHeader': built_item = {'title': item['title']} if not item['published']: built_item['published'] = False elif item['type'] == 'ExternalUrl': built_item = { 'title': item.get('title', ''), 'external_url': item['external_url']} if not item['published']: built_item['published'] = False else: logging.warn("I can't find the module item " f"'{item['title']}' locally and I couldn't figure " "out how to pull it because it is not an " "easel-supported type: " + item['type']) continue position = item.get('position', 0) if position not in items: items[position] = [] if icid: built_item = {"item": icid.filename, "indent": item.get('indent', 0)} if item['type'] == 'File' and 'title' in item: built_item['title'] = item['title'] if 'indent' in built_item and built_item['indent'] == 0: del built_item['indent'] items[position].append(built_item) # order items by position all_items = [] for position in sorted(items.keys()): if position == 0: # we used 0 as default above so skip that for now continue all_items += items[position] # add positionless items to the end module_['items'] = all_items + items.get(0, []) modules.append(Module.build(module_)) return modules