def cmd_md(db, args): if not args.components: args.components = ["syllabus.md", "assignment_groups", "assignments", "files", "pages", "quizzes", "modules"] for component_filepath in args.components: if component_filepath.endswith("*"): component_filepath = component_filepath[:-1] if component_filepath.endswith("/"): component_filepath = component_filepath[:-1] if os.path.isdir(component_filepath) and not component_filepath.startswith("files"): if component_filepath not in helpers.DIRS: logging.error("Invalid directory: "+component_filepath) continue for child_path in os.listdir(component_filepath): full_child_path = component_filepath + '/' + child_path component = helpers_yaml.read(full_child_path) print(component.md()) else: if component_filepath == "syllabus.md": f = open("syllabus.md") print(helpers.md2html(f.read())) f.close() else: component = helpers_yaml.read(component_filepath) if isinstance(component, list): for obj in component: print("\n\n*****\n", obj.md()) else: print(component.md())
def load_questions_file(filename): # I don't want to require users to put a single question in a list so we # detect if it's a list and if not, put it in a list contents = helpers_yaml.read(filename) if not isinstance(contents, list): contents = [contents] return contents
def push(db, course_, dry_run): remote = NavigationTabs().pull(db, course_, dry_run) local = helpers_yaml.read(FILENAME) hidden = [] for remote_tab in remote.tabs: # Per Canvas docs: "Home and Settings tabs are not manageable, and # can't be hidden or moved" if remote_tab.id in ['home', 'settings']: continue found = False for local_tab in local.tabs: # we want to update a tab if 1. we have it locally in a different # position than what's currently in canvas, OR 2. the remote tab is # hidden but it's specified locally (i.e., we want it not hidden) if (remote_tab.label == local_tab.label and (remote_tab.position != local_tab.position or remote_tab.hidden != local_tab.hidden)): # local tabs are from the yaml and only have label, position, # and hidden fields, but we need the id local_tab.id = remote_tab.id local_tab.push(db, course_, dry_run) found = True break if not found: hidden.append(remote_tab) print("The following navigation tabs are or will be made hidden:") for tab in hidden: print("\t-", tab.label) # we also want to push a tab if it's not hidden in Canvas but it wasn't # included in the local yaml if not tab.hidden: tab.hidden = True tab.push(db, course_, dry_run)
def cmd_remove(db, args): if not args.components: # remove everything args.components = ["modules", "assignments", "files", "pages", "quizzes", "assignment_groups"] if not args.course: args.course = course.find_all(db) else: args.course = course.match_courses(db, args.course) for component_filepath in args.components: if component_filepath.endswith("*"): component_filepath = component_filepath[:-1] if component_filepath.endswith("/"): component_filepath = component_filepath[:-1] if os.path.isdir(component_filepath) and not component_filepath.startswith("files"): if component_filepath not in helpers.DIRS: logging.error("Invalid directory: "+component_filepath) continue for child_path in os.listdir(component_filepath): full_child_path = component_filepath + '/' + child_path component = helpers_yaml.read(full_child_path) if component and not isinstance(component, str): component.filename = full_child_path for course_ in args.course: print(f"removing {component} from {course_.name} ({course_.canvas_id})") component.remove(db, course_, args.dry_run) else: for course_ in args.course: if component_filepath == "syllabus.md": logging.error("Don't remove your syllabus!") else: component = helpers_yaml.read(component_filepath) if component and not isinstance(component, str): component.filename = component_filepath print(f"removing {component} from {course_.name} ({course_.canvas_id})") component.remove(db, course_, args.dry_run) else: # not a yaml file so assume it's a file/dir to remove files.remove(db, course_, component_filepath, args.dry_run)
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 preprocess(self, db, course_id, dry_run): item = helpers_yaml.read(self.item) item.filename = self.item if not self.title: self.title = getattr(item, "name", getattr(item, "title", self.item)) type_ = type(item).__name__ if type_ not in VALID_TYPES: raise ValueError(f"Cannot add an item of type {type_} to a module." " Can be one of {VALID_TYPES}") self.type = type_ cid = item.get_canvas_id(db, course_id) if self.type == "Page": self.page_url = cid else: self.content_id = cid
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 preprocess(self, db, course_, dry_run): if not self.item: # it should be a url or SubHeader so we only have to set the type # and filename if self.external_url: self.type = "ExternalUrl" else: self.type = "SubHeader" return cid = canvas_id.CanvasID(self.item, course_.canvas_id) cid.find_id(db) if self.item.startswith("files"): # TODO: need a better way to detect that this is a literal File to # link to and not just a yaml file containing item info such as for # a Page or Assignment self.type = "File" if not cid.canvas_id: # the file probably doesn't exist in the course so we need to # push it first files.push(db, course_, self.item, True, dry_run) return item = helpers_yaml.read(self.item) item.filename = self.item if not cid.canvas_id: # the component probably doesn't exist in the course so we need to # push it first item.push(db, course_, dry_run) if not self.title: self.title = getattr(item, "name", getattr(item, "title", self.item)) type_ = type(item).__name__ if type_ not in VALID_TYPES: raise ValueError(f"Cannot add an item of type {type_} to a module." " Can be one of {VALID_TYPES}") self.type = type_ cid = item.get_canvas_id(db, course_.canvas_id) if self.type == "Page": self.page_url = cid else: self.content_id = cid
def load_module(self, db, course_id): if self.filename: module_filename = self.filename.split('--')[0] m = helpers_yaml.read(module_filename) m.filename = module_filename return m
def cmd_push(db, args): if not args.components: # push everything args.components = ["syllabus.md", "grading_scheme.yaml", "navigation.yaml", "assignment_groups", "assignments", "files", "pages", "quizzes", "modules"] if not args.course: args.course = course.find_all(db) else: args.course = course.match_courses(db, args.course) for component_filepath in args.components: if component_filepath.endswith("*"): component_filepath = component_filepath[:-1] if component_filepath.endswith("/"): component_filepath = component_filepath[:-1] if os.path.isdir(component_filepath) and not component_filepath.startswith("files"): if component_filepath not in helpers.DIRS: logging.error("Invalid directory: "+component_filepath) continue for child_path in os.listdir(component_filepath): full_child_path = component_filepath + '/' + child_path component = helpers_yaml.read(full_child_path) if component and not isinstance(component, str): component.filename = full_child_path for course_ in args.course: print(f"pushing {component} to {course_.name} ({course_.canvas_id})") component.push(db, course_, args.dry_run) elif component_filepath.startswith("files"): for course_ in args.course: files.push(db, course_, component_filepath, args.hidden, args.dry_run) else: if not os.path.isfile(component_filepath): logging.error("Cannot find file: " + component_filepath) continue for course_ in args.course: if component_filepath == "syllabus.md": print(f"pushing syllabus to {course_.name} ({course_.canvas_id})") course.push_syllabus(db, course_.canvas_id, args.dry_run) elif component_filepath == "grading_scheme.yaml": print(f"pushing grading scheme to {course_.name} ({course_.canvas_id})") cid = canvas_id.CanvasID(component_filepath, course_.canvas_id) cid.find_id(db) if cid.canvas_id == "": # Don't try to create a scheme if it doesn't already # exist. Ideally, we update the scheme but Canvas # apparently doesn't allow for that. component = helpers_yaml.read(component_filepath) component.push(db, course_, args.dry_run) cid.find_id(db) course.update_grading_scheme(db, course_.canvas_id, cid.canvas_id, args.dry_run) elif component_filepath == "navigation.yaml": print(f"pushing navigation tabs to {course_.name} ({course_.canvas_id})") navigation_tab.push(db, course_, args.dry_run) else: component = helpers_yaml.read(component_filepath) if component and not isinstance(component, str): component.filename = component_filepath print(f"pushing {component} to {course_.name} ({course_.canvas_id})") component.push(db, course_, args.dry_run) else: # not a yaml file so assume it's a file/dir to upload files.push(db, course_, component_filepath, args.hidden, args.dry_run)
def cmd_pull(db, args): if not args.components: # pull everything args.components = ["assignment_groups", "assignments", "files", "pages", "quizzes", "modules"] if not args.course: args.course = course.find_all(db) else: args.course = course.match_courses(db, args.course) for component_filepath in args.components: local = {} remote = {} if component_filepath.endswith("*"): component_filepath = component_filepath[:-1] if component_filepath.endswith("/"): component_filepath = component_filepath[:-1] if os.path.isdir(component_filepath): if component_filepath not in helpers.DIRS: logging.error("Invalid directory: "+component_filepath) break # local versions for child_path in os.listdir(component_filepath): if not component_filepath.startswith("files"): component = helpers_yaml.read(component_filepath + '/' + child_path) local[component.filename] = component # request remote versions for course_ in args.course: m = importlib.import_module("easel."+helpers.DIRS[component_filepath]) print(f"pulling all {component_filepath} from {course_.name} ({course_.canvas_id})") for remote_comp in m.pull_all(db, course_, args.dry_run): if remote_comp.filename in remote: remote[remote_comp.filename].append(remote_comp) else: remote[remote_comp.filename] = [remote_comp] elif os.path.isfile(component_filepath): # local version component = helpers_yaml.read(component_filepath) component.filename = component_filepath local[component.filename] = component # request remote version(s) for course_ in args.course: print(f"pulling {component} from {course_.name} ({course_.canvas_id})") remote_comp = component.pull(db, course_, args.dry_run) if remote_comp.filename in remote: remote[remote_comp.filename].append(remote_comp) else: remote[remote_comp.filename] = [remote_comp] elif component_filepath in ["navigation", "navigation.yaml"]: # We don't have a local version at this point so only request # remote version(s). If we have a local version, the previous if # clause will take care of it. for course_ in args.course: print(f"pulling navigation tabs from {course_.name} ({course_.canvas_id})") component = navigation_tab.NavigationTabs() remote_comp = component.pull(db, course_, args.dry_run) if remote_comp.filename in remote: remote[remote_comp.filename].append(remote_comp) else: remote[remote_comp.filename] = [remote_comp] else: logging.error("Cannot find file: " + component_filepath) # TODO: merge remote into local for remote_comp in remote: components = remote[remote_comp] if remote_comp in local: logging.warn("Overwriting local copy of " "{components[0].filename}. In the future, we'll " "implement a merge workflow") if len(remote[remote_comp]) == 1: print(f"writing {components[0]} to {components[0].filename}") helpers_yaml.write(components[0].filename, components[0]) else: logging.error("Too many remote options to handle right now..." "Please manually specify a single course to pull from " "with the -c flag and later we'll implement a merge " "workflow")