def __init__(self, courseid, content, course_fs, task_factory, hook_manager): super(WebAppCourse, self).__init__(courseid, content, course_fs, task_factory, hook_manager) try: self._name = self._content['name'] except: raise Exception("Course has an invalid name: " + self.get_id()) if self._content.get('nofrontend', False): raise Exception("That course is not allowed to be displayed directly in the webapp") try: self._admins = self._content.get('admins', []) self._tutors = self._content.get('tutors', []) self._description = self._content.get('description', '') self._accessible = AccessibleTime(self._content.get("accessible", None)) self._registration = AccessibleTime(self._content.get("registration", None)) self._registration_password = self._content.get('registration_password', None) self._registration_ac = self._content.get('registration_ac', None) if self._registration_ac not in [None, "username", "binding", "email"]: raise Exception("Course has an invalid value for registration_ac: " + self.get_id()) self._registration_ac_list = self._content.get('registration_ac_list', []) self._groups_student_choice = self._content.get("groups_student_choice", False) self._use_classrooms = self._content.get('use_classrooms', True) self._allow_unregister = self._content.get('allow_unregister', True) self._allow_preview = self._content.get('allow_preview', False) self._is_lti = self._content.get('is_lti', False) self._lti_keys = self._content.get('lti_keys', {}) self._lti_send_back_grade = self._content.get('lti_send_back_grade', False) except: raise Exception("Course has an invalid YAML spec: " + self.get_id()) # Force some parameters if LTI is active if self.is_lti(): self._accessible = AccessibleTime(True) self._registration = AccessibleTime(False) self._registration_password = None self._registration_ac = None self._registration_ac_list = [] self._groups_student_choice = False self._use_classrooms = True self._allow_unregister = False else: self._lti_keys = {} self._lti_send_back_grade = False # Caches for tag lists self._all_tags_cache = None self._all_tags_cache_list = {} self._all_tags_cache_list_admin = {} self._organisational_tags_to_task = {} self.update_all_tags_cache()
def __init__(self, course, taskid, content, task_fs, hook_manager, task_problem_types): # We load the descriptor of the task here to allow plugins to modify settings of the task before it is read by the Task constructor if not id_checker(taskid): raise Exception("Task with invalid id: " + course.get_id() + "/" + taskid) super(WebAppTask, self).__init__(course, taskid, content, task_fs, hook_manager, task_problem_types) self._name = self._data.get('name', 'Task {}'.format(self.get_id())) self._context = self._data.get('context', "") # Authors if isinstance(self._data.get('author'), str): # verify if author is a string self._author = self._data['author'] else: self._author = "" # Submission storage self._stored_submissions = int(self._data.get("stored_submissions", 0)) # Default download self._evaluate = self._data.get("evaluate", "best") # Grade weight self._weight = float(self._data.get("weight", 1.0)) # _accessible self._accessible = AccessibleTime(self._data.get("accessible", None)) # Group task self._groups = bool(self._data.get("groups", False)) # Submission limits self._submission_limit = self._data.get("submission_limit", { "amount": -1, "period": -1 }) # Input random self._input_random = int(self._data.get("input_random", 0)) # Regenerate input random self._regenerate_input_random = bool( self._data.get("regenerate_input_random", False)) # Tags self._tags = Tag.create_tags_from_dict(self._data.get("tags", {}))
def course_accessibility(course, default_value, course_factory, database, user_manager): descriptor = course.get_descriptor() if descriptor.get("exam_active", False): # Check for SEB if not check_key(descriptor.get("seb_hash", "")): return AccessibleTime(False) # Check for exam finalization courseid = course.get_id() username = user_manager.session_username() if get_user_status(courseid, username, database, user_manager): return AccessibleTime(False) return default_value
def __init__(self, course, taskid, content, task_fs, translations_fs, hook_manager, task_problem_types): # We load the descriptor of the task here to allow plugins to modify settings of the task before it is read by the Task constructor if not id_checker(taskid): raise Exception("Task with invalid id: " + course.get_id() + "/" + taskid) super(WebAppTask, self).__init__(course, taskid, content, task_fs, translations_fs, hook_manager, task_problem_types) # Env type env_type_obj = get_env_type(self._environment_type) if env_type_obj is None: raise Exception(_("Environment type {0} is unknown").format(self._environment_type)) # Ensure that the content of the dictionary is ok self._environment_parameters = env_type_obj.check_task_environment_parameters(self._environment_parameters) # Name and context self._name = self._data.get('name', 'Task {}'.format(self.get_id())) self._context = self._data.get('context', "") # Authors if isinstance(self._data.get('author'), str): # verify if author is a string self._author = self._data['author'] else: self._author = "" if isinstance(self._data.get('contact_url'), str): self._contact_url = self._data['contact_url'] else: self._contact_url = "" # Submission storage self._stored_submissions = int(self._data.get("stored_submissions", 0)) # Default download self._evaluate = self._data.get("evaluate", "best") # Grade weight self._weight = float(self._data.get("weight", 1.0)) # _accessible self._accessible = AccessibleTime(self._data.get("accessible", None)) # Group task self._groups = bool(self._data.get("groups", False)) # Submission limits self._submission_limit = self._data.get("submission_limit", {"amount": -1, "period": -1}) # Input random self._input_random = int(self._data.get("input_random", 0)) # Regenerate input random self._regenerate_input_random = bool(self._data.get("regenerate_input_random", False)) # Category tags self._categories = self._data.get("categories", [])
def __init__(self, courseid, content, course_fs, task_factory, hook_manager): super(WebAppCourse, self).__init__(courseid, content, course_fs, task_factory, hook_manager) try: self._name = self._content['name'] except: raise Exception("Course has an invalid description: " + self.get_id()) if self._content.get('nofrontend', False): raise Exception("That course is not allowed to be displayed directly in the webapp") try: self._admins = self._content.get('admins', []) self._tutors = self._content.get('tutors', []) self._accessible = AccessibleTime(self._content.get("accessible", None)) self._registration = AccessibleTime(self._content.get("registration", None)) self._registration_password = self._content.get('registration_password', None) self._registration_ac = self._content.get('registration_ac', None) if self._registration_ac not in [None, "username", "binding", "email"]: raise Exception("Course has an invalid value for registration_ac: " + self.get_id()) self._registration_ac_list = self._content.get('registration_ac_list', []) self._groups_student_choice = self._content.get("groups_student_choice", False) self._use_classrooms = self._content.get('use_classrooms', True) self._allow_unregister = self._content.get('allow_unregister', True) self._allow_preview = self._content.get('allow_preview', False) self._is_lti = self._content.get('is_lti', False) self._lti_keys = self._content.get('lti_keys', {}) self._lti_send_back_grade = self._content.get('lti_send_back_grade', False) except: raise Exception("Course has an invalid description: " + self.get_id()) # Force some parameters if LTI is active if self.is_lti(): self._accessible = AccessibleTime(True) self._registration = AccessibleTime(False) self._registration_password = None self._registration_ac = None self._registration_ac_list = [] self._groups_student_choice = False self._use_classrooms = True self._allow_unregister = True self._allow_preview = False else: self._lti_keys = {} self._lti_send_back_grade = False # Caches for tag lists self._all_tags_cache = None self._all_tags_cache_list = {} self._all_tags_cache_list_admin = {} self.update_all_tags_cache()
def __init__(self, courseid, content, course_fs, task_factory, hook_manager): super(WebAppCourse, self).__init__(courseid, content, course_fs, task_factory, hook_manager) try: self._name = self._content['name'] except: raise Exception("Course has an invalid name: " + self.get_id()) if self._content.get('nofrontend', False): raise Exception("That course is not allowed to be displayed directly in the webapp") try: self._admins = self._content.get('admins', []) self._tutors = self._content.get('tutors', []) self._description = self._content.get('description', '') self._accessible = AccessibleTime(self._content.get("accessible", None)) self._registration = AccessibleTime(self._content.get("registration", None)) self._registration_password = self._content.get('registration_password', None) self._registration_ac = self._content.get('registration_ac', None) if self._registration_ac not in [None, "username", "binding", "email"]: raise Exception("Course has an invalid value for registration_ac: " + self.get_id()) self._registration_ac_list = self._content.get('registration_ac_list', []) self._groups_student_choice = self._content.get("groups_student_choice", False) self._allow_unregister = self._content.get('allow_unregister', True) self._allow_preview = self._content.get('allow_preview', False) self._is_lti = self._content.get('is_lti', False) self._lti_url = self._content.get('lti_url', '') self._lti_keys = self._content.get('lti_keys', {}) self._lti_send_back_grade = self._content.get('lti_send_back_grade', False) self._tags = {key: Tag(key, tag_dict, self.gettext) for key, tag_dict in self._content.get("tags", {}).items()} except: raise Exception("Course has an invalid YAML spec: " + self.get_id()) # Force some parameters if LTI is active if self.is_lti(): self._accessible = AccessibleTime(True) self._registration = AccessibleTime(False) self._registration_password = None self._registration_ac = None self._registration_ac_list = [] self._groups_student_choice = False self._allow_unregister = False else: self._lti_keys = {} self._lti_url = '' self._lti_send_back_grade = False self.course_page = "course" self.admin_page = "admin" self.has_student = True
class WebAppCourse(Course): """ A course with some modification for users """ def __init__(self, courseid, content, course_fs, task_factory, hook_manager): super(WebAppCourse, self).__init__(courseid, content, course_fs, task_factory, hook_manager) try: self._name = self._content['name'] except: raise Exception("Course has an invalid name: " + self.get_id()) if self._content.get('nofrontend', False): raise Exception("That course is not allowed to be displayed directly in the webapp") try: self._admins = self._content.get('admins', []) self._tutors = self._content.get('tutors', []) self._description = self._content.get('description', '') self._accessible = AccessibleTime(self._content.get("accessible", None)) self._registration = AccessibleTime(self._content.get("registration", None)) self._registration_password = self._content.get('registration_password', None) self._registration_ac = self._content.get('registration_ac', None) if self._registration_ac not in [None, "username", "binding", "email"]: raise Exception("Course has an invalid value for registration_ac: " + self.get_id()) self._registration_ac_list = self._content.get('registration_ac_list', []) self._groups_student_choice = self._content.get("groups_student_choice", False) self._allow_unregister = self._content.get('allow_unregister', True) self._allow_preview = self._content.get('allow_preview', False) self._is_lti = self._content.get('is_lti', False) self._lti_url = self._content.get('lti_url', '') self._lti_keys = self._content.get('lti_keys', {}) self._lti_send_back_grade = self._content.get('lti_send_back_grade', False) self._tags = {key: Tag(key, tag_dict, self.gettext) for key, tag_dict in self._content.get("tags", {}).items()} except: raise Exception("Course has an invalid YAML spec: " + self.get_id()) # Force some parameters if LTI is active if self.is_lti(): self._accessible = AccessibleTime(True) self._registration = AccessibleTime(False) self._registration_password = None self._registration_ac = None self._registration_ac_list = [] self._groups_student_choice = False self._allow_unregister = False else: self._lti_keys = {} self._lti_url = '' self._lti_send_back_grade = False self.course_page = "course" self.admin_page = "admin" self.has_student = True def get_staff(self): """ Returns a list containing the usernames of all the staff users """ return list(set(self.get_tutors() + self.get_admins())) def get_admins(self): """ Returns a list containing the usernames of the administrators of this course """ return self._admins def get_tutors(self): """ Returns a list containing the usernames of the tutors assigned to this course """ return self._tutors def is_open_to_non_staff(self): """ Returns true if the course is accessible by users that are not administrator of this course """ return self.get_accessibility().is_open() def is_registration_possible(self, user_info): """ Returns true if users can register for this course """ return self.get_accessibility().is_open() and self._registration.is_open() and self.is_user_accepted_by_access_control(user_info) def is_password_needed_for_registration(self): """ Returns true if a password is needed for registration """ return self._registration_password is not None def get_registration_password(self): """ Returns the password needed for registration (None if there is no password) """ return self._registration_password def get_accessibility(self, plugin_override=True): """ Return the AccessibleTime object associated with the accessibility of this course """ vals = self._hook_manager.call_hook('course_accessibility', course=self, default=self._accessible) return vals[0] if len(vals) and plugin_override else self._accessible def get_registration_accessibility(self): """ Return the AccessibleTime object associated with the registration """ return self._registration def get_tasks(self): return OrderedDict(sorted(list(Course.get_tasks(self).items()), key=lambda t: (t[1].get_order(), t[1].get_id()))) def get_access_control_method(self): """ Returns either None, "username", "binding", or "email", depending on the method used to verify that users can register to the course """ return self._registration_ac def get_access_control_list(self): """ Returns the list of all users allowed by the AC list """ return self._registration_ac_list def can_students_choose_group(self): """ Returns True if the students can choose their groups """ return self._groups_student_choice def is_lti(self): """ True if the current course is in LTI mode """ return self._is_lti def lti_keys(self): """ {name: key} for the LTI customers """ return self._lti_keys if self._is_lti else {} def lti_url(self): """ Returns the URL to the external platform the course is hosted on """ return self._lti_url def lti_send_back_grade(self): """ True if the current course should send back grade to the LTI Tool Consumer """ return self._is_lti and self._lti_send_back_grade def is_user_accepted_by_access_control(self, user_info): """ Returns True if the user is allowed by the ACL """ if self.get_access_control_method() is None: return True elif not user_info: return False elif self.get_access_control_method() == "username": return user_info["username"] in self.get_access_control_list() elif self.get_access_control_method() == "email": return user_info["email"] in self.get_access_control_list() elif self.get_access_control_method() == "binding": return set(user_info["bindings"].keys()).intersection(set(self.get_access_control_list())) return False def allow_preview(self): return self._allow_preview def allow_unregister(self, plugin_override=True): """ Returns True if students can unregister from course """ vals = self._hook_manager.call_hook('course_allow_unregister', course=self, default=self._allow_unregister) return vals[0] if len(vals) and plugin_override else self._allow_unregister def get_name(self, language): """ Return the name of this course """ return self.gettext(language, self._name) if self._name else "" def get_description(self, language): """Returns the course description """ description = self.gettext(language, self._description) if self._description else '' return ParsableText(description, "rst", translation=self.get_translation_obj(language)) def get_tags(self): return self._tags def get_admin_menu(self, plugin_manager, user_manager): """ Return the element to display in the admin menu of the course """ default_entries = [] if user_manager.has_admin_rights_on_course(self): default_entries += [("settings", "<i class='fa fa-cog fa-fw'></i> " + _("Course settings"))] default_entries += [("stats", "<i class='fa fa-area-chart fa-fw'></i> " + _("Stats")), ("students", "<i class='fa fa-user fa-fw'></i> " + _("Students")), ("audiences", "<i class='fa fa-group fa-fw'></i> " + _("Audiences"))] if not self.is_lti(): default_entries += [("groups", "<i class='fa fa-group fa-fw'></i> " + _("Groups"))] default_entries += [("tasks", "<i class='fa fa-tasks fa-fw'></i> " + _("Tasks")), ("tags", "<i class='fa fa-tags fa-fw'></i> " + _("Tags")), ("submissions", "<i class='fa fa-search fa-fw'></i> " + _("View submissions")), ("download", "<i class='fa fa-download fa-fw'></i> " + _("Download submissions"))] if user_manager.has_admin_rights_on_course(self): default_entries += [("replay", "<i class='fa fa-refresh fa-fw'></i> " + _("Replay submissions")), ("danger", "<i class='fa fa-bomb fa-fw'></i> " + _("Danger zone"))] # Hook should return a tuple (link,name) where link is the relative link from the index of the course administration. additional_entries = [entry for entry in plugin_manager.call_hook('course_admin_menu', course=self) if entry is not None] return default_entries + additional_entries
def POST_AUTH(self, courseid): # pylint: disable=arguments-differ """ POST request """ course, __ = self.get_course_and_check_rights(courseid, allow_all_staff=False) errors = [] course_content = {} try: data = web.input() course_content = self.course_factory.get_course_descriptor_content(courseid) course_content['name'] = data['name'] if course_content['name'] == "": errors.append(_('Invalid name')) course_content['description'] = data['description'] course_content['admins'] = list(map(str.strip, data['admins'].split(','))) if not self.user_manager.user_is_superadmin() and self.user_manager.session_username() not in course_content['admins']: errors.append(_('You cannot remove yourself from the administrators of this course')) course_content['tutors'] = list(map(str.strip, data['tutors'].split(','))) if len(course_content['tutors']) == 1 and course_content['tutors'][0].strip() == "": course_content['tutors'] = [] course_content['groups_student_choice'] = True if data["groups_student_choice"] == "true" else False if course_content.get('use_classrooms', True) != (data['use_classrooms'] == "true"): self.database.aggregations.delete_many({"courseid": course.get_id()}) course_content['use_classrooms'] = True if data["use_classrooms"] == "true" else False if data["accessible"] == "custom": course_content['accessible'] = "{}/{}".format(data["accessible_start"], data["accessible_end"]) elif data["accessible"] == "true": course_content['accessible'] = True else: course_content['accessible'] = False try: AccessibleTime(course_content['accessible']) except: errors.append(_('Invalid accessibility dates')) course_content['allow_unregister'] = True if data["allow_unregister"] == "true" else False course_content['allow_preview'] = True if data["allow_preview"] == "true" else False if data["registration"] == "custom": course_content['registration'] = "{}/{}".format(data["registration_start"], data["registration_end"]) elif data["registration"] == "true": course_content['registration'] = True else: course_content['registration'] = False try: AccessibleTime(course_content['registration']) except: errors.append(_('Invalid registration dates')) course_content['registration_password'] = data['registration_password'] if course_content['registration_password'] == "": course_content['registration_password'] = None course_content['registration_ac'] = data['registration_ac'] if course_content['registration_ac'] not in ["None", "username", "binding", "email"]: errors.append(_('Invalid ACL value')) if course_content['registration_ac'] == "None": course_content['registration_ac'] = None course_content['registration_ac_list'] = data['registration_ac_list'].split("\n") course_content['is_lti'] = 'lti' in data and data['lti'] == "true" course_content['lti_keys'] = dict([x.split(":") for x in data['lti_keys'].split("\n") if x]) for lti_key in course_content['lti_keys'].keys(): if not re.match("^[a-zA-Z0-9]*$", lti_key): errors.append(_("LTI keys must be alphanumerical.")) course_content['lti_send_back_grade'] = 'lti_send_back_grade' in data and data['lti_send_back_grade'] == "true" except: errors.append(_('User returned an invalid form.')) if len(errors) == 0: self.course_factory.update_course_descriptor_content(courseid, course_content) errors = None course, __ = self.get_course_and_check_rights(courseid, allow_all_staff=False) # don't forget to reload the modified course return self.page(course, errors, errors is None)
class WebAppCourse(Course): """ A course with some modification for users """ def __init__(self, courseid, content, course_fs, task_factory, hook_manager): super(WebAppCourse, self).__init__(courseid, content, course_fs, task_factory, hook_manager) try: self._name = self._content['name'] except: raise Exception("Course has an invalid name: " + self.get_id()) if self._content.get('nofrontend', False): raise Exception( "That course is not allowed to be displayed directly in the webapp" ) try: self._admins = self._content.get('admins', []) self._tutors = self._content.get('tutors', []) self._description = self._content.get('description', '') self._accessible = AccessibleTime( self._content.get("accessible", None)) self._registration = AccessibleTime( self._content.get("registration", None)) self._registration_password = self._content.get( 'registration_password', None) self._registration_ac = self._content.get('registration_ac', None) if self._registration_ac not in [ None, "username", "binding", "email" ]: raise Exception( "Course has an invalid value for registration_ac: " + self.get_id()) self._registration_ac_list = self._content.get( 'registration_ac_list', []) self._groups_student_choice = self._content.get( "groups_student_choice", False) self._use_classrooms = self._content.get('use_classrooms', True) self._allow_unregister = self._content.get('allow_unregister', True) self._allow_preview = self._content.get('allow_preview', False) self._is_lti = self._content.get('is_lti', False) self._lti_keys = self._content.get('lti_keys', {}) self._lti_send_back_grade = self._content.get( 'lti_send_back_grade', False) except: raise Exception("Course has an invalid YAML spec: " + self.get_id()) # Force some parameters if LTI is active if self.is_lti(): self._accessible = AccessibleTime(True) self._registration = AccessibleTime(False) self._registration_password = None self._registration_ac = None self._registration_ac_list = [] self._groups_student_choice = False self._use_classrooms = True self._allow_unregister = False else: self._lti_keys = {} self._lti_send_back_grade = False # Caches for tag lists self._all_tags_cache = None self._all_tags_cache_list = {} self._all_tags_cache_list_admin = {} self._organisational_tags_to_task = {} self.update_all_tags_cache() def get_staff(self): """ Returns a list containing the usernames of all the staff users """ return list(set(self.get_tutors() + self.get_admins())) def get_admins(self): """ Returns a list containing the usernames of the administrators of this course """ return self._admins def get_tutors(self): """ Returns a list containing the usernames of the tutors assigned to this course """ return self._tutors def is_open_to_non_staff(self): """ Returns true if the course is accessible by users that are not administrator of this course """ return self.get_accessibility().is_open() def is_registration_possible(self, user_info): """ Returns true if users can register for this course """ return self.get_accessibility().is_open( ) and self._registration.is_open( ) and self.is_user_accepted_by_access_control(user_info) def is_password_needed_for_registration(self): """ Returns true if a password is needed for registration """ return self._registration_password is not None def get_registration_password(self): """ Returns the password needed for registration (None if there is no password) """ return self._registration_password def get_accessibility(self, plugin_override=True): """ Return the AccessibleTime object associated with the accessibility of this course """ vals = self._hook_manager.call_hook('course_accessibility', course=self, default=self._accessible) return vals[0] if len(vals) and plugin_override else self._accessible def get_registration_accessibility(self): """ Return the AccessibleTime object associated with the registration """ return self._registration def get_tasks(self): return OrderedDict( sorted(list(Course.get_tasks(self).items()), key=lambda t: (t[1].get_order(), t[1].get_id()))) def get_access_control_method(self): """ Returns either None, "username", "binding", or "email", depending on the method used to verify that users can register to the course """ return self._registration_ac def get_access_control_list(self): """ Returns the list of all users allowed by the AC list """ return self._registration_ac_list def can_students_choose_group(self): """ Returns True if the students can choose their groups """ return self._groups_student_choice def use_classrooms(self): """ Returns True if classrooms are used """ return self._use_classrooms def is_lti(self): """ True if the current course is in LTI mode """ return self._is_lti def lti_keys(self): """ {name: key} for the LTI customers """ return self._lti_keys if self._is_lti else {} def lti_send_back_grade(self): """ True if the current course should send back grade to the LTI Tool Consumer """ return self._is_lti and self._lti_send_back_grade def is_user_accepted_by_access_control(self, user_info): """ Returns True if the user is allowed by the ACL """ if self.get_access_control_method() is None: return True elif not user_info: return False elif self.get_access_control_method() == "username": return user_info["username"] in self.get_access_control_list() elif self.get_access_control_method() == "email": return user_info["email"] in self.get_access_control_list() elif self.get_access_control_method() == "binding": return set(user_info["bindings"].keys()).intersection( set(self.get_access_control_list())) return False def allow_preview(self): return self._allow_preview def allow_unregister(self, plugin_override=True): """ Returns True if students can unregister from course """ vals = self._hook_manager.call_hook('course_allow_unregister', course=self, default=self._allow_unregister) return vals[0] if len( vals) and plugin_override else self._allow_unregister def get_name(self, language): """ Return the name of this course """ return self.gettext(language, self._name) if self._name else "" def get_description(self, language): """Returns the course description """ description = self.gettext( language, self._description) if self._description else '' return ParsableText( description, "rst", self._translations.get(language, gettext.NullTranslations())) def get_all_tags(self): """ Return a tuple of lists ([common_tags], [anti_tags], [organisational_tags]) all tags of all tasks of this course Since this is an heavy procedure, we use a cache to cache results. Cache should be updated when a task is modified. """ if self._all_tags_cache != None: return self._all_tags_cache tag_list_common = set() tag_list_misconception = set() tag_list_org = set() tasks = self.get_tasks() for id, task in tasks.items(): for tag in task.get_tags()[0]: tag_list_common.add(tag) for tag in task.get_tags()[1]: tag_list_misconception.add(tag) for tag in task.get_tags()[2]: tag_list_org.add(tag) tag_list_common = natsorted(tag_list_common, key=lambda y: y.get_name().lower()) tag_list_misconception = natsorted(tag_list_misconception, key=lambda y: y.get_name().lower()) tag_list_org = natsorted(tag_list_org, key=lambda y: y.get_name().lower()) self._all_tags_cache = (list(tag_list_common), list(tag_list_misconception), list(tag_list_org)) return self._all_tags_cache def get_all_tags_names_as_list(self, admin=False, language="en"): """ Computes and cache two list containing all tags name sorted by natural order on name """ if admin: if self._all_tags_cache_list_admin != {} and language in self._all_tags_cache_list_admin: return self._all_tags_cache_list_admin[language] #Cache hit else: if self._all_tags_cache_list != {} and language in self._all_tags_cache_list: return self._all_tags_cache_list[language] #Cache hit #Cache miss, computes everything s_stud = set() s_admin = set() (common, _, org) = self.get_all_tags() for tag in common + org: # Is tag_name_with_translation correct by doing that like that ? tag_name_with_translation = self.gettext( language, tag.get_name()) if tag.get_name() else "" s_admin.add(tag_name_with_translation) if tag.is_visible_for_student(): s_stud.add(tag_name_with_translation) self._all_tags_cache_list_admin[language] = natsorted( s_admin, key=lambda y: y.lower()) self._all_tags_cache_list[language] = natsorted( s_stud, key=lambda y: y.lower()) if admin: return self._all_tags_cache_list_admin[language] return self._all_tags_cache_list[language] def get_organisational_tags_to_task(self): """ This build a dict for fast retrive tasks id based on organisational tags. The form of the dict is: { 'org_tag_1': ['task_id', 'task_id', ...], 'org_tag_2' : ['task_id', 'task_id', ...], ... } """ if self._organisational_tags_to_task != {}: return self._organisational_tags_to_task for taskid, task in self.get_tasks().items(): for tag in task.get_tags()[2]: self._organisational_tags_to_task.setdefault( tag.get_name(), []).append(taskid) return self._organisational_tags_to_task def update_all_tags_cache(self): """ Force the cache refreshing """ self._all_tags_cache = None self._all_tags_cache_list = {} self._all_tags_cache_list_admin = {} self._organisational_tags_to_task = {} self.get_all_tags() self.get_all_tags_names_as_list() self.get_organisational_tags_to_task()
def __init__(self, courseid, content, course_fs, task_factory, plugin_manager): self._id = courseid self._content = content self._fs = course_fs self._task_factory = task_factory self._plugin_manager = plugin_manager self._translations = {} translations_fs = self._fs.from_subfolder("$i18n") if translations_fs.exists(): for f in translations_fs.list(folders=False, files=True, recursive=False): lang = f[0:len(f) - 3] if translations_fs.exists(lang + ".mo"): self._translations[lang] = gettext.GNUTranslations( translations_fs.get_fd(lang + ".mo")) else: self._translations[lang] = gettext.NullTranslations() try: self._name = self._content['name'] except: raise Exception("Course has an invalid name: " + self.get_id()) if self._content.get('nofrontend', False): raise Exception( "That course is not allowed to be displayed directly in the webapp" ) try: self._admins = self._content.get('admins', []) self._tutors = self._content.get('tutors', []) self._description = self._content.get('description', '') self._accessible = AccessibleTime( self._content.get("accessible", None)) self._registration = AccessibleTime( self._content.get("registration", None)) self._registration_password = self._content.get( 'registration_password', None) self._registration_ac = self._content.get('registration_ac', None) if self._registration_ac not in [ None, "username", "binding", "email" ]: raise Exception( "Course has an invalid value for registration_ac: " + self.get_id()) self._registration_ac_list = self._content.get( 'registration_ac_list', []) self._groups_student_choice = self._content.get( "groups_student_choice", False) self._allow_unregister = self._content.get('allow_unregister', True) self._allow_preview = self._content.get('allow_preview', False) self._is_lti = self._content.get('is_lti', False) self._lti_url = self._content.get('lti_url', '') self._lti_keys = self._content.get('lti_keys', {}) self._lti_send_back_grade = self._content.get( 'lti_send_back_grade', False) self._tags = { key: Tag(key, tag_dict, self.gettext) for key, tag_dict in self._content.get("tags", {}).items() } if 'toc' in self._content: self._toc = SectionsList(self._content['toc']) else: tasks = self._task_factory.get_all_tasks(self) ordered_task_list = OrderedDict( sorted(list(tasks.items()), key=lambda t: (t[1].get_old_order(), t[1].get_id()))) indexed_task_list = { taskid: rank for rank, taskid in enumerate(ordered_task_list.keys()) } self._toc = SectionsList([{ "id": "tasks-list", "title": _("List of exercises"), "rank": 0, "tasks_list": indexed_task_list }]) except: raise Exception("Course has an invalid YAML spec: " + self.get_id()) # Force some parameters if LTI is active if self.is_lti(): self._accessible = AccessibleTime(True) self._registration = AccessibleTime(False) self._registration_password = None self._registration_ac = None self._registration_ac_list = [] self._groups_student_choice = False self._allow_unregister = False else: self._lti_keys = {} self._lti_url = '' self._lti_send_back_grade = False
def __init__(self, courseid, content, course_fs, task_factory, plugin_manager, task_dispensers): self._id = courseid self._content = content self._fs = course_fs self._task_factory = task_factory self._plugin_manager = plugin_manager self._translations = {} translations_fs = self._fs.from_subfolder("$i18n") if translations_fs.exists(): for f in translations_fs.list(folders=False, files=True, recursive=False): lang = f[0:len(f) - 3] if translations_fs.exists(lang + ".mo"): self._translations[lang] = gettext.GNUTranslations(translations_fs.get_fd(lang + ".mo")) else: self._translations[lang] = gettext.NullTranslations() try: self._name = self._content['name'] except: raise Exception("Course has an invalid name: " + self.get_id()) if self._content.get('nofrontend', False): raise Exception("That course is not allowed to be displayed directly in the webapp") _migrate_from_v_0_6(content, self._task_factory.get_all_tasks(self)) try: self._admins = self._content.get('admins', []) self._tutors = self._content.get('tutors', []) self._description = self._content.get('description', '') self._accessible = AccessibleTime(self._content.get("accessible", None)) self._registration = AccessibleTime(self._content.get("registration", None)) self._registration_password = self._content.get('registration_password', None) self._registration_ac = self._content.get('registration_ac', None) if self._registration_ac not in [None, "username", "binding", "email"]: raise Exception("Course has an invalid value for registration_ac: " + self.get_id()) self._registration_ac_list = self._content.get('registration_ac_list', []) self._groups_student_choice = self._content.get("groups_student_choice", False) self._allow_unregister = self._content.get('allow_unregister', True) self._allow_preview = self._content.get('allow_preview', False) self._is_lti = self._content.get('is_lti', False) self._lti_url = self._content.get('lti_url', '') self._lti_keys = self._content.get('lti_keys', {}) self._lti_send_back_grade = self._content.get('lti_send_back_grade', False) self._tags = {key: Tag(key, tag_dict, self.gettext) for key, tag_dict in self._content.get("tags", {}).items()} task_dispenser_class = task_dispensers.get(self._content.get('task_dispenser', 'toc'), TableOfContents) # Here we use a lambda to encourage the task dispenser to pass by the task_factory to fetch course tasks # to avoid them to be cached along with the course object. Passing the task factory as argument # would require to pass the course too, and have a useless reference back. self._task_dispenser = task_dispenser_class(lambda: self._task_factory.get_all_tasks(self), self._content.get("dispenser_data", '')) except: raise Exception("Course has an invalid YAML spec: " + self.get_id()) # Force some parameters if LTI is active if self.is_lti(): self._accessible = AccessibleTime(True) self._registration = AccessibleTime(False) self._registration_password = None self._registration_ac = None self._registration_ac_list = [] self._groups_student_choice = False self._allow_unregister = False else: self._lti_keys = {} self._lti_url = '' self._lti_send_back_grade = False # Build the regex for the ACL, allowing for fast matching. Only used internally. self._registration_ac_regex = self._build_ac_regex(self._registration_ac_list)
class WebAppCourse(Course): """ A course with some modification for users """ def __init__(self, courseid, content, course_fs, task_factory, hook_manager): super(WebAppCourse, self).__init__(courseid, content, course_fs, task_factory, hook_manager) try: self._name = self._content['name'] except: raise Exception("Course has an invalid name: " + self.get_id()) if self._content.get('nofrontend', False): raise Exception( "That course is not allowed to be displayed directly in the webapp" ) try: self._admins = self._content.get('admins', []) self._tutors = self._content.get('tutors', []) self._description = self._content.get('description', '') self._accessible = AccessibleTime( self._content.get("accessible", None)) self._registration = AccessibleTime( self._content.get("registration", None)) self._registration_password = self._content.get( 'registration_password', None) self._registration_ac = self._content.get('registration_ac', None) if self._registration_ac not in [ None, "username", "binding", "email" ]: raise Exception( "Course has an invalid value for registration_ac: " + self.get_id()) self._registration_ac_list = self._content.get( 'registration_ac_list', []) self._groups_student_choice = self._content.get( "groups_student_choice", False) self._allow_unregister = self._content.get('allow_unregister', True) self._allow_preview = self._content.get('allow_preview', False) self._is_lti = self._content.get('is_lti', False) self._lti_keys = self._content.get('lti_keys', {}) self._lti_send_back_grade = self._content.get( 'lti_send_back_grade', False) self._tags = { key: Tag(key, tag_dict, self.gettext) for key, tag_dict in self._content.get("tags", {}).items() } except: raise Exception("Course has an invalid YAML spec: " + self.get_id()) # Force some parameters if LTI is active if self.is_lti(): self._accessible = AccessibleTime(True) self._registration = AccessibleTime(False) self._registration_password = None self._registration_ac = None self._registration_ac_list = [] self._groups_student_choice = False self._allow_unregister = False else: self._lti_keys = {} self._lti_send_back_grade = False def get_staff(self): """ Returns a list containing the usernames of all the staff users """ return list(set(self.get_tutors() + self.get_admins())) def get_admins(self): """ Returns a list containing the usernames of the administrators of this course """ return self._admins def get_tutors(self): """ Returns a list containing the usernames of the tutors assigned to this course """ return self._tutors def is_open_to_non_staff(self): """ Returns true if the course is accessible by users that are not administrator of this course """ return self.get_accessibility().is_open() def is_registration_possible(self, user_info): """ Returns true if users can register for this course """ return self.get_accessibility().is_open( ) and self._registration.is_open( ) and self.is_user_accepted_by_access_control(user_info) def is_password_needed_for_registration(self): """ Returns true if a password is needed for registration """ return self._registration_password is not None def get_registration_password(self): """ Returns the password needed for registration (None if there is no password) """ return self._registration_password def get_accessibility(self, plugin_override=True): """ Return the AccessibleTime object associated with the accessibility of this course """ vals = self._hook_manager.call_hook('course_accessibility', course=self, default=self._accessible) return vals[0] if len(vals) and plugin_override else self._accessible def get_registration_accessibility(self): """ Return the AccessibleTime object associated with the registration """ return self._registration def get_tasks(self): return OrderedDict( sorted(list(Course.get_tasks(self).items()), key=lambda t: (t[1].get_order(), t[1].get_id()))) def get_access_control_method(self): """ Returns either None, "username", "binding", or "email", depending on the method used to verify that users can register to the course """ return self._registration_ac def get_access_control_list(self): """ Returns the list of all users allowed by the AC list """ return self._registration_ac_list def can_students_choose_group(self): """ Returns True if the students can choose their groups """ return self._groups_student_choice def is_lti(self): """ True if the current course is in LTI mode """ return self._is_lti def lti_keys(self): """ {name: key} for the LTI customers """ return self._lti_keys if self._is_lti else {} def lti_send_back_grade(self): """ True if the current course should send back grade to the LTI Tool Consumer """ return self._is_lti and self._lti_send_back_grade def is_user_accepted_by_access_control(self, user_info): """ Returns True if the user is allowed by the ACL """ if self.get_access_control_method() is None: return True elif not user_info: return False elif self.get_access_control_method() == "username": return user_info["username"] in self.get_access_control_list() elif self.get_access_control_method() == "email": return user_info["email"] in self.get_access_control_list() elif self.get_access_control_method() == "binding": return set(user_info["bindings"].keys()).intersection( set(self.get_access_control_list())) return False def allow_preview(self): return self._allow_preview def allow_unregister(self, plugin_override=True): """ Returns True if students can unregister from course """ vals = self._hook_manager.call_hook('course_allow_unregister', course=self, default=self._allow_unregister) return vals[0] if len( vals) and plugin_override else self._allow_unregister def get_name(self, language): """ Return the name of this course """ return self.gettext(language, self._name) if self._name else "" def get_description(self, language): """Returns the course description """ description = self.gettext( language, self._description) if self._description else '' return ParsableText(description, "rst", translation=self.get_translation_obj(language)) def get_tags(self): return self._tags
def task_accessibility(course, task, default): # pylint: disable=unused-argument contest_data = get_contest_data(course) if contest_data['enabled']: return AccessibleTime(contest_data['start'] + '/') else: return default
class WebAppCourse(Course): """ A course with some modification for users """ def __init__(self, courseid, content, course_fs, task_factory, hook_manager): super(WebAppCourse, self).__init__(courseid, content, course_fs, task_factory, hook_manager) try: self._name = self._content['name'] except: raise Exception("Course has an invalid name: " + self.get_id()) if self._content.get('nofrontend', False): raise Exception("That course is not allowed to be displayed directly in the webapp") try: self._admins = self._content.get('admins', []) self._tutors = self._content.get('tutors', []) self._description = self._content.get('description', '') self._accessible = AccessibleTime(self._content.get("accessible", None)) self._registration = AccessibleTime(self._content.get("registration", None)) self._registration_password = self._content.get('registration_password', None) self._registration_ac = self._content.get('registration_ac', None) if self._registration_ac not in [None, "username", "binding", "email"]: raise Exception("Course has an invalid value for registration_ac: " + self.get_id()) self._registration_ac_list = self._content.get('registration_ac_list', []) self._groups_student_choice = self._content.get("groups_student_choice", False) self._use_classrooms = self._content.get('use_classrooms', True) self._allow_unregister = self._content.get('allow_unregister', True) self._allow_preview = self._content.get('allow_preview', False) self._is_lti = self._content.get('is_lti', False) self._lti_keys = self._content.get('lti_keys', {}) self._lti_send_back_grade = self._content.get('lti_send_back_grade', False) except: raise Exception("Course has an invalid YAML spec: " + self.get_id()) # Force some parameters if LTI is active if self.is_lti(): self._accessible = AccessibleTime(True) self._registration = AccessibleTime(False) self._registration_password = None self._registration_ac = None self._registration_ac_list = [] self._groups_student_choice = False self._use_classrooms = True self._allow_unregister = False else: self._lti_keys = {} self._lti_send_back_grade = False # Caches for tag lists self._all_tags_cache = None self._all_tags_cache_list = {} self._all_tags_cache_list_admin = {} self._organisational_tags_to_task = {} self.update_all_tags_cache() def get_staff(self): """ Returns a list containing the usernames of all the staff users """ return list(set(self.get_tutors() + self.get_admins())) def get_admins(self): """ Returns a list containing the usernames of the administrators of this course """ return self._admins def get_tutors(self): """ Returns a list containing the usernames of the tutors assigned to this course """ return self._tutors def is_open_to_non_staff(self): """ Returns true if the course is accessible by users that are not administrator of this course """ return self.get_accessibility().is_open() def is_registration_possible(self, user_info): """ Returns true if users can register for this course """ return self.get_accessibility().is_open() and self._registration.is_open() and self.is_user_accepted_by_access_control(user_info) def is_password_needed_for_registration(self): """ Returns true if a password is needed for registration """ return self._registration_password is not None def get_registration_password(self): """ Returns the password needed for registration (None if there is no password) """ return self._registration_password def get_accessibility(self, plugin_override=True): """ Return the AccessibleTime object associated with the accessibility of this course """ vals = self._hook_manager.call_hook('course_accessibility', course=self, default=self._accessible) return vals[0] if len(vals) and plugin_override else self._accessible def get_registration_accessibility(self): """ Return the AccessibleTime object associated with the registration """ return self._registration def get_tasks(self): return OrderedDict(sorted(list(Course.get_tasks(self).items()), key=lambda t: (t[1].get_order(), t[1].get_id()))) def get_access_control_method(self): """ Returns either None, "username", "binding", or "email", depending on the method used to verify that users can register to the course """ return self._registration_ac def get_access_control_list(self): """ Returns the list of all users allowed by the AC list """ return self._registration_ac_list def can_students_choose_group(self): """ Returns True if the students can choose their groups """ return self._groups_student_choice def use_classrooms(self): """ Returns True if classrooms are used """ return self._use_classrooms def is_lti(self): """ True if the current course is in LTI mode """ return self._is_lti def lti_keys(self): """ {name: key} for the LTI customers """ return self._lti_keys if self._is_lti else {} def lti_send_back_grade(self): """ True if the current course should send back grade to the LTI Tool Consumer """ return self._is_lti and self._lti_send_back_grade def is_user_accepted_by_access_control(self, user_info): """ Returns True if the user is allowed by the ACL """ if self.get_access_control_method() is None: return True elif not user_info: return False elif self.get_access_control_method() == "username": return user_info["username"] in self.get_access_control_list() elif self.get_access_control_method() == "email": return user_info["email"] in self.get_access_control_list() elif self.get_access_control_method() == "binding": return set(user_info["bindings"].keys()).intersection(set(self.get_access_control_list())) return False def allow_preview(self): return self._allow_preview def allow_unregister(self, plugin_override=True): """ Returns True if students can unregister from course """ vals = self._hook_manager.call_hook('course_allow_unregister', course=self, default=self._allow_unregister) return vals[0] if len(vals) and plugin_override else self._allow_unregister def get_name(self, language): """ Return the name of this course """ return self.gettext(language, self._name) if self._name else "" def get_description(self, language): """Returns the course description """ description = self.gettext(language, self._description) if self._description else '' return ParsableText(description, "rst", self._translations.get(language, gettext.NullTranslations())) def get_all_tags(self): """ Return a tuple of lists ([common_tags], [anti_tags], [organisational_tags]) all tags of all tasks of this course Since this is an heavy procedure, we use a cache to cache results. Cache should be updated when a task is modified. """ if self._all_tags_cache != None: return self._all_tags_cache tag_list_common = set() tag_list_misconception = set() tag_list_org = set() tasks = self.get_tasks() for id, task in tasks.items(): for tag in task.get_tags()[0]: tag_list_common.add(tag) for tag in task.get_tags()[1]: tag_list_misconception.add(tag) for tag in task.get_tags()[2]: tag_list_org.add(tag) tag_list_common = natsorted(tag_list_common, key=lambda y: y.get_name().lower()) tag_list_misconception = natsorted(tag_list_misconception, key=lambda y: y.get_name().lower()) tag_list_org = natsorted(tag_list_org, key=lambda y: y.get_name().lower()) self._all_tags_cache = (list(tag_list_common), list(tag_list_misconception), list(tag_list_org)) return self._all_tags_cache def get_all_tags_names_as_list(self, admin=False, language="en"): """ Computes and cache two list containing all tags name sorted by natural order on name """ if admin: if self._all_tags_cache_list_admin != {} and language in self._all_tags_cache_list_admin: return self._all_tags_cache_list_admin[language] #Cache hit else: if self._all_tags_cache_list != {} and language in self._all_tags_cache_list: return self._all_tags_cache_list[language] #Cache hit #Cache miss, computes everything s_stud = set() s_admin = set() (common, _, org) = self.get_all_tags() for tag in common + org: # Is tag_name_with_translation correct by doing that like that ? tag_name_with_translation = self.gettext(language, tag.get_name()) if tag.get_name() else "" s_admin.add(tag_name_with_translation) if tag.is_visible_for_student(): s_stud.add(tag_name_with_translation) self._all_tags_cache_list_admin[language] = natsorted(s_admin, key=lambda y: y.lower()) self._all_tags_cache_list[language] = natsorted(s_stud, key=lambda y: y.lower()) if admin: return self._all_tags_cache_list_admin[language] return self._all_tags_cache_list[language] def get_organisational_tags_to_task(self): """ This build a dict for fast retrive tasks id based on organisational tags. The form of the dict is: { 'org_tag_1': ['task_id', 'task_id', ...], 'org_tag_2' : ['task_id', 'task_id', ...], ... } """ if self._organisational_tags_to_task != {}: return self._organisational_tags_to_task for taskid, task in self.get_tasks().items(): for tag in task.get_tags()[2]: self._organisational_tags_to_task.setdefault(tag.get_name(), []).append(taskid) return self._organisational_tags_to_task def update_all_tags_cache(self): """ Force the cache refreshing """ self._all_tags_cache = None self._all_tags_cache_list = {} self._all_tags_cache_list_admin = {} self._organisational_tags_to_task = {} self.get_all_tags() self.get_all_tags_names_as_list() self.get_organisational_tags_to_task()
def __init__(self, course, taskid, content, filesystem, plugin_manager, task_problem_types): # We load the descriptor of the task here to allow plugins to modify settings of the task before it is read by the Task constructor if not id_checker(taskid): raise Exception("Task with invalid id: " + course.get_id() + "/" + taskid) content = _migrate_from_v_0_6(content) self._course = course self._taskid = taskid self._fs = filesystem self._plugin_manager = plugin_manager self._data = content if "problems" not in self._data: raise Exception("Tasks must have some problems descriptions") # i18n self._translations = {} self._course_fs = self._fs.from_subfolder(course.get_id()) self._course_fs.ensure_exists() self._task_fs = self._course_fs.from_subfolder(taskid) self._task_fs.ensure_exists() self._translations_fs = self._task_fs.from_subfolder("$i18n") if not self._translations_fs.exists(): self._translations_fs = self._task_fs.from_subfolder("student").from_subfolder("$i18n") if not self._translations_fs.exists(): self._translations_fs = self._course_fs.from_subfolder("$common").from_subfolder("$i18n") if not self._translations_fs.exists(): self._translations_fs = self._course_fs.from_subfolder("$common").from_subfolder( "student").from_subfolder("$i18n") if self._translations_fs.exists(): for f in self._translations_fs.list(folders=False, files=True, recursive=False): lang = f[0:len(f) - 3] if self._translations_fs.exists(lang + ".mo"): self._translations[lang] = gettext.GNUTranslations(self._translations_fs.get_fd(lang + ".mo")) else: self._translations[lang] = gettext.NullTranslations() # Check all problems self._problems = [] for problemid in self._data['problems']: self._problems.append( self._create_task_problem(problemid, self._data['problems'][problemid], task_problem_types)) # Env type self._environment_id = self._data.get('environment_id', 'default') self._environment_type = self._data.get('environment_type', 'unknown') self._environment_parameters = self._data.get("environment_parameters", {}) env_type_obj = get_env_type(self._environment_type) if env_type_obj is None: raise Exception(_("Environment type {0} is unknown").format(self._environment_type)) # Ensure that the content of the dictionary is ok self._environment_parameters = env_type_obj.check_task_environment_parameters(self._environment_parameters) # Name and context self._name = self._data.get('name', 'Task {}'.format(self.get_id())) self._context = self._data.get('context', "") # Authors if isinstance(self._data.get('author'), str): # verify if author is a string self._author = self._data['author'] else: self._author = "" if isinstance(self._data.get('contact_url'), str): self._contact_url = self._data['contact_url'] else: self._contact_url = "" # Submission storage self._stored_submissions = int(self._data.get("stored_submissions", 0)) # Default download self._evaluate = self._data.get("evaluate", "best") # Grade weight self._weight = float(self._data.get("weight", 1.0)) # _accessible self._accessible = AccessibleTime(self._data.get("accessible", None)) # Group task self._groups = bool(self._data.get("groups", False)) # Submission limits self._submission_limit = self._data.get("submission_limit", {"amount": -1, "period": -1}) # Input random self._input_random = int(self._data.get("input_random", 0)) # Regenerate input random self._regenerate_input_random = bool(self._data.get("regenerate_input_random", False)) # Category tags self._categories = self._data.get("categories", [])
class Course(object): """ A course with some modification for users """ def __init__(self, courseid, content, course_fs, task_factory, plugin_manager, task_dispensers): self._id = courseid self._content = content self._fs = course_fs self._task_factory = task_factory self._plugin_manager = plugin_manager self._translations = {} translations_fs = self._fs.from_subfolder("$i18n") if translations_fs.exists(): for f in translations_fs.list(folders=False, files=True, recursive=False): lang = f[0:len(f) - 3] if translations_fs.exists(lang + ".mo"): self._translations[lang] = gettext.GNUTranslations(translations_fs.get_fd(lang + ".mo")) else: self._translations[lang] = gettext.NullTranslations() try: self._name = self._content['name'] except: raise Exception("Course has an invalid name: " + self.get_id()) if self._content.get('nofrontend', False): raise Exception("That course is not allowed to be displayed directly in the webapp") _migrate_from_v_0_6(content, self._task_factory.get_all_tasks(self)) try: self._admins = self._content.get('admins', []) self._tutors = self._content.get('tutors', []) self._description = self._content.get('description', '') self._accessible = AccessibleTime(self._content.get("accessible", None)) self._registration = AccessibleTime(self._content.get("registration", None)) self._registration_password = self._content.get('registration_password', None) self._registration_ac = self._content.get('registration_ac', None) if self._registration_ac not in [None, "username", "binding", "email"]: raise Exception("Course has an invalid value for registration_ac: " + self.get_id()) self._registration_ac_list = self._content.get('registration_ac_list', []) self._groups_student_choice = self._content.get("groups_student_choice", False) self._allow_unregister = self._content.get('allow_unregister', True) self._allow_preview = self._content.get('allow_preview', False) self._is_lti = self._content.get('is_lti', False) self._lti_url = self._content.get('lti_url', '') self._lti_keys = self._content.get('lti_keys', {}) self._lti_send_back_grade = self._content.get('lti_send_back_grade', False) self._tags = {key: Tag(key, tag_dict, self.gettext) for key, tag_dict in self._content.get("tags", {}).items()} task_dispenser_class = task_dispensers.get(self._content.get('task_dispenser', 'toc'), TableOfContents) # Here we use a lambda to encourage the task dispenser to pass by the task_factory to fetch course tasks # to avoid them to be cached along with the course object. Passing the task factory as argument # would require to pass the course too, and have a useless reference back. self._task_dispenser = task_dispenser_class(lambda: self._task_factory.get_all_tasks(self), self._content.get("dispenser_data", '')) except: raise Exception("Course has an invalid YAML spec: " + self.get_id()) # Force some parameters if LTI is active if self.is_lti(): self._accessible = AccessibleTime(True) self._registration = AccessibleTime(False) self._registration_password = None self._registration_ac = None self._registration_ac_list = [] self._groups_student_choice = False self._allow_unregister = False else: self._lti_keys = {} self._lti_url = '' self._lti_send_back_grade = False # Build the regex for the ACL, allowing for fast matching. Only used internally. self._registration_ac_regex = self._build_ac_regex(self._registration_ac_list) def get_translation_obj(self, language): return self._translations.get(language, gettext.NullTranslations()) def gettext(self, language, *args, **kwargs): return self.get_translation_obj(language).gettext(*args, **kwargs) def get_id(self): """ Return the _id of this course """ return self._id def get_fs(self): """ Returns a FileSystemProvider which points to the folder of this course """ return self._fs def get_task(self, taskid): """ Returns a Task object """ return self._task_factory.get_task(self, taskid) def get_descriptor(self): """ Get (a copy) the description of the course """ return copy.deepcopy(self._content) def get_staff(self): """ Returns a list containing the usernames of all the staff users """ return list(set(self.get_tutors() + self.get_admins())) def get_admins(self): """ Returns a list containing the usernames of the administrators of this course """ return self._admins def get_tutors(self): """ Returns a list containing the usernames of the tutors assigned to this course """ return self._tutors def is_open_to_non_staff(self): """ Returns true if the course is accessible by users that are not administrator of this course """ return self.get_accessibility().is_open() def is_registration_possible(self, user_info: UserInfo): """ Returns true if users can register for this course """ return self.get_accessibility().is_open() and self._registration.is_open() and self.is_user_accepted_by_access_control(user_info) def is_password_needed_for_registration(self): """ Returns true if a password is needed for registration """ return self._registration_password is not None def get_registration_password(self): """ Returns the password needed for registration (None if there is no password) """ return self._registration_password def get_accessibility(self, plugin_override=True): """ Return the AccessibleTime object associated with the accessibility of this course """ vals = self._plugin_manager.call_hook('course_accessibility', course=self, default=self._accessible) return vals[0] if len(vals) and plugin_override else self._accessible def get_registration_accessibility(self): """ Return the AccessibleTime object associated with the registration """ return self._registration def get_tasks(self, ordered=False): return self._task_dispenser.get_ordered_tasks() if ordered else self._task_factory.get_all_tasks(self) def get_access_control_method(self): """ Returns either None, "username", "binding", or "email", depending on the method used to verify that users can register to the course """ return self._registration_ac def get_access_control_list(self) -> List[str]: """ Returns the list of all users/emails/binding methods/... (see get_access_control_method) allowed by the AC list """ return self._registration_ac_list def can_students_choose_group(self): """ Returns True if the students can choose their groups """ return self._groups_student_choice def is_lti(self): """ True if the current course is in LTI mode """ return self._is_lti def lti_keys(self): """ {name: key} for the LTI customers """ return self._lti_keys if self._is_lti else {} def lti_url(self): """ Returns the URL to the external platform the course is hosted on """ return self._lti_url def lti_send_back_grade(self): """ True if the current course should send back grade to the LTI Tool Consumer """ return self._is_lti and self._lti_send_back_grade def is_user_accepted_by_access_control(self, user_info: UserInfo): """ Returns True if the user is allowed by the ACL """ if self.get_access_control_method() is None: return True keys_per_access_control_method = { "username": (lambda: [user_info.username]), "email": (lambda: [user_info.email]), "binding": (lambda: user_info.bindings.keys()) } if not user_info or self.get_access_control_method() not in keys_per_access_control_method: return False # check that at least one key matches in the list keys = keys_per_access_control_method[self.get_access_control_method()]() return any(self._registration_ac_regex.match(key) for key in keys) def allow_preview(self): return self._allow_preview def allow_unregister(self, plugin_override=True): """ Returns True if students can unregister from course """ vals = self._plugin_manager.call_hook('course_allow_unregister', course=self, default=self._allow_unregister) return vals[0] if len(vals) and plugin_override else self._allow_unregister def get_name(self, language): """ Return the name of this course """ return self.gettext(language, self._name) if self._name else "" def get_description(self, language): """Returns the course description """ description = self.gettext(language, self._description) if self._description else '' return ParsableText(description, "rst", translation=self.get_translation_obj(language)) def get_tags(self): return self._tags def get_task_dispenser(self): """ :return: the structure of the course """ return self._task_dispenser def _build_ac_regex(self, list_ac): """ Build a regex for the AC list, allowing for fast matching. The regex is only used internally """ return re.compile('|'.join(re.escape(x).replace("\\*", ".*") for x in list_ac))
class WebAppTask(Task): """ A task that stores additional context information, specific to the web app """ def __init__(self, course, taskid, content, task_fs, hook_manager, task_problem_types): # We load the descriptor of the task here to allow plugins to modify settings of the task before it is read by the Task constructor if not id_checker(taskid): raise Exception("Task with invalid id: " + course.get_id() + "/" + taskid) super(WebAppTask, self).__init__(course, taskid, content, task_fs, hook_manager, task_problem_types) self._name = self._data.get('name', 'Task {}'.format(self.get_id())) self._context = self._data.get('context', "") # Authors if isinstance(self._data.get('author'), str): # verify if author is a string self._author = self._data['author'] else: self._author = "" # Submission storage self._stored_submissions = int(self._data.get("stored_submissions", 0)) # Default download self._evaluate = self._data.get("evaluate", "best") # Grade weight self._weight = float(self._data.get("weight", 1.0)) # _accessible self._accessible = AccessibleTime(self._data.get("accessible", None)) # Group task self._groups = bool(self._data.get("groups", False)) # Submission limits self._submission_limit = self._data.get("submission_limit", { "amount": -1, "period": -1 }) # Input random self._input_random = int(self._data.get("input_random", 0)) # Regenerate input random self._regenerate_input_random = bool( self._data.get("regenerate_input_random", False)) # Tags self._tags = Tag.create_tags_from_dict(self._data.get("tags", {})) def get_grading_weight(self): """ Get the relative weight of this task in the grading """ return self._weight def get_accessible_time(self, plugin_override=True): """ Get the accessible time of this task """ vals = self._hook_manager.call_hook('task_accessibility', course=self.get_course(), task=self, default=self._accessible) return vals[0] if len(vals) and plugin_override else self._accessible def is_visible_by_students(self): """ Returns true if the task is accessible by all students that are not administrator of the course """ return self.get_course().is_open_to_non_staff( ) and self.get_accessible_time().after_start() def get_deadline(self): """ Returns a string containing the deadline for this task """ if self.get_accessible_time().is_always_accessible(): return _("No deadline") elif self.get_accessible_time().is_never_accessible(): return _("It's too late") else: return self.get_accessible_time().get_end_date().strftime( "%d/%m/%Y %H:%M:%S") def is_group_task(self): """ Indicates if the task submission mode is per groups """ return self._groups def get_submission_limit(self): """ Returns the submission limits et for the task""" return self._submission_limit def get_name(self, language): """ Returns the name of this task """ return self.gettext(language, self._name) if self._name else "" def get_name_or_id(self, language=None): """ Return the name or id this task """ name_or_id = self.get_name(language) if name_or_id == "": name_or_id = self.get_id() return name_or_id def get_context(self, language): """ Get the context(description) of this task """ context = self.gettext(language, self._context) if self._context else "" vals = self._hook_manager.call_hook('task_context', course=self.get_course(), task=self, default=context) return ParsableText(vals[0], "rst", self._translations.get(language, gettext.NullTranslations())) if len(vals) else \ ParsableText(context, "rst", self._translations.get(language, gettext.NullTranslations())) def get_authors(self, language): """ Return the list of this task's authors """ return self.gettext(language, self._author) if self._author else "" def adapt_input_for_backend(self, input_data): """ Adapt the input from web.py for the inginious.backend """ for problem in self._problems: input_data = problem.adapt_input_for_backend(input_data) return input_data def get_stored_submissions(self): """ Indicates if only the last submission must be stored for the task """ return self._stored_submissions def get_evaluate(self): """ Indicates the default download for the task """ return self._evaluate def get_tags(self): """ Get the tuple of list of the task """ return self._tags def get_number_input_random(self): """ Return the number of random inputs """ return self._input_random def regenerate_input_random(self): """ Indicates if random inputs should be regenerated """ return self._regenerate_input_random def can_submit_after_deadline(self): return self._data.get("allow_late_submission", False) and self._accessible.after_deadline()
def POST_AUTH(self, courseid, taskid): # pylint: disable=arguments-differ """ Edit a task """ if not id_checker(taskid) or not id_checker(courseid): raise NotFound(description=_("Invalid course/task id")) course, __ = self.get_course_and_check_rights(courseid, allow_all_staff=False) data = flask.request.form.copy() data["task_file"] = flask.request.files.get("task_file") # Delete task ? if "delete" in data: toc = course.get_task_dispenser().get_dispenser_data() toc.remove_task(taskid) self.course_factory.update_course_descriptor_element( courseid, 'toc', toc.to_structure()) self.task_factory.delete_task(courseid, taskid) if data.get("wipe", False): self.wipe_task(courseid, taskid) return redirect(self.app.get_homepath() + "/admin/" + courseid + "/tasks") # Else, parse content try: try: task_zip = data.get("task_file").read() except: task_zip = None del data["task_file"] problems = dict_from_prefix("problem", data) environment_type = data.get("environment_type", "") environment_parameters = dict_from_prefix("envparams", data).get( environment_type, {}) environment_id = dict_from_prefix("environment_id", data).get(environment_type, "") data = { key: val for key, val in data.items() if not key.startswith("problem") and not key.startswith( "envparams") and not key.startswith("environment_id") and not key.startswith("/") and not key == "@action" } data[ "environment_id"] = environment_id # we do this after having removed all the environment_id[something] entries # Determines the task filetype if data["@filetype"] not in self.task_factory.get_available_task_file_extensions( ): return json.dumps({ "status": "error", "message": _("Invalid file type: {}").format(str(data["@filetype"])) }) file_ext = data["@filetype"] del data["@filetype"] # Parse and order the problems (also deletes @order from the result) if problems is None: data["problems"] = OrderedDict([]) else: data["problems"] = OrderedDict([ (key, self.parse_problem(val)) for key, val in sorted(iter(problems.items()), key=lambda x: int(x[1]['@order'])) ]) # Categories course_tags = course.get_tags() data['categories'] = [ cat for cat in map(str.strip, data['categories'].split(',')) if cat ] for category in data['categories']: if category not in course_tags: return json.dumps({ "status": "error", "message": _("Unknown category tag.") }) # Task environment parameters data["environment_parameters"] = environment_parameters # Weight try: data["weight"] = float(data["weight"]) except: return json.dumps({ "status": "error", "message": _("Grade weight must be a floating-point number") }) # Groups if "groups" in data: data["groups"] = True if data["groups"] == "true" else False # Submission storage if "store_all" in data: try: stored_submissions = data["stored_submissions"] data["stored_submissions"] = 0 if data[ "store_all"] == "true" else int(stored_submissions) except: return json.dumps({ "status": "error", "message": _("The number of stored submission must be positive!") }) if data["store_all"] == "false" and data[ "stored_submissions"] <= 0: return json.dumps({ "status": "error", "message": _("The number of stored submission must be positive!") }) del data['store_all'] # Submission limits if "submission_limit" in data: if data["submission_limit"] == "none": result = {"amount": -1, "period": -1} elif data["submission_limit"] == "hard": try: result = { "amount": int(data["submission_limit_hard"]), "period": -1 } except: return json.dumps({ "status": "error", "message": _("Invalid submission limit!") }) else: try: result = { "amount": int(data["submission_limit_soft_0"]), "period": int(data["submission_limit_soft_1"]) } except: return json.dumps({ "status": "error", "message": _("Invalid submission limit!") }) del data["submission_limit_hard"] del data["submission_limit_soft_0"] del data["submission_limit_soft_1"] data["submission_limit"] = result # Accessible if data["accessible"] == "custom": data["accessible"] = "{}/{}/{}".format( data["accessible_start"], data["accessible_soft_end"], data["accessible_end"]) elif data["accessible"] == "true": data["accessible"] = True else: data["accessible"] = False del data["accessible_start"] del data["accessible_end"] del data["accessible_soft_end"] try: AccessibleTime(data["accessible"]) except Exception as message: return json.dumps({ "status": "error", "message": _("Invalid task accessibility ({})").format(message) }) # Checkboxes if data.get("responseIsHTML"): data["responseIsHTML"] = True # Network grading data["network_grading"] = "network_grading" in data except Exception as message: return json.dumps({ "status": "error", "message": _("Your browser returned an invalid form ({})").format(message) }) # Get the course try: course = self.course_factory.get_course(courseid) except: return json.dumps({ "status": "error", "message": _("Error while reading course's informations") }) # Get original data try: orig_data = self.task_factory.get_task_descriptor_content( courseid, taskid) data["order"] = orig_data["order"] except: pass task_fs = self.task_factory.get_task_fs(courseid, taskid) task_fs.ensure_exists() # Call plugins and return the first error plugin_results = self.plugin_manager.call_hook('task_editor_submit', course=course, taskid=taskid, task_data=data, task_fs=task_fs) # Retrieve the first non-null element error = next(filter(None, plugin_results), None) if error is not None: return error try: Task(course, taskid, data, self.course_factory.get_fs(), self.plugin_manager, self.task_factory.get_problem_types()) except Exception as message: return json.dumps({ "status": "error", "message": _("Invalid data: {}").format(str(message)) }) if task_zip: try: zipfile = ZipFile(task_zip) except Exception: return json.dumps({ "status": "error", "message": _("Cannot read zip file. Files were not modified") }) with tempfile.TemporaryDirectory() as tmpdirname: try: zipfile.extractall(tmpdirname) except Exception: return json.dumps({ "status": "error", "message": _("There was a problem while extracting the zip archive. Some files may have been modified" ) }) task_fs.copy_to(tmpdirname) self.task_factory.delete_all_possible_task_files(courseid, taskid) self.task_factory.update_task_descriptor_content( courseid, taskid, data, force_extension=file_ext) return json.dumps({"status": "ok"})
class Course(object): """ A course with some modification for users """ def __init__(self, courseid, content, course_fs, task_factory, plugin_manager): self._id = courseid self._content = content self._fs = course_fs self._task_factory = task_factory self._plugin_manager = plugin_manager self._translations = {} translations_fs = self._fs.from_subfolder("$i18n") if translations_fs.exists(): for f in translations_fs.list(folders=False, files=True, recursive=False): lang = f[0:len(f) - 3] if translations_fs.exists(lang + ".mo"): self._translations[lang] = gettext.GNUTranslations( translations_fs.get_fd(lang + ".mo")) else: self._translations[lang] = gettext.NullTranslations() try: self._name = self._content['name'] except: raise Exception("Course has an invalid name: " + self.get_id()) if self._content.get('nofrontend', False): raise Exception( "That course is not allowed to be displayed directly in the webapp" ) try: self._admins = self._content.get('admins', []) self._tutors = self._content.get('tutors', []) self._description = self._content.get('description', '') self._accessible = AccessibleTime( self._content.get("accessible", None)) self._registration = AccessibleTime( self._content.get("registration", None)) self._registration_password = self._content.get( 'registration_password', None) self._registration_ac = self._content.get('registration_ac', None) if self._registration_ac not in [ None, "username", "binding", "email" ]: raise Exception( "Course has an invalid value for registration_ac: " + self.get_id()) self._registration_ac_list = self._content.get( 'registration_ac_list', []) self._groups_student_choice = self._content.get( "groups_student_choice", False) self._allow_unregister = self._content.get('allow_unregister', True) self._allow_preview = self._content.get('allow_preview', False) self._is_lti = self._content.get('is_lti', False) self._lti_url = self._content.get('lti_url', '') self._lti_keys = self._content.get('lti_keys', {}) self._lti_send_back_grade = self._content.get( 'lti_send_back_grade', False) self._tags = { key: Tag(key, tag_dict, self.gettext) for key, tag_dict in self._content.get("tags", {}).items() } if 'toc' in self._content: self._toc = SectionsList(self._content['toc']) else: tasks = self._task_factory.get_all_tasks(self) ordered_task_list = OrderedDict( sorted(list(tasks.items()), key=lambda t: (t[1].get_old_order(), t[1].get_id()))) indexed_task_list = { taskid: rank for rank, taskid in enumerate(ordered_task_list.keys()) } self._toc = SectionsList([{ "id": "tasks-list", "title": _("List of exercises"), "rank": 0, "tasks_list": indexed_task_list }]) except: raise Exception("Course has an invalid YAML spec: " + self.get_id()) # Force some parameters if LTI is active if self.is_lti(): self._accessible = AccessibleTime(True) self._registration = AccessibleTime(False) self._registration_password = None self._registration_ac = None self._registration_ac_list = [] self._groups_student_choice = False self._allow_unregister = False else: self._lti_keys = {} self._lti_url = '' self._lti_send_back_grade = False def get_translation_obj(self, language): return self._translations.get(language, gettext.NullTranslations()) def gettext(self, language, *args, **kwargs): return self.get_translation_obj(language).gettext(*args, **kwargs) def get_id(self): """ Return the _id of this course """ return self._id def get_fs(self): """ Returns a FileSystemProvider which points to the folder of this course """ return self._fs def get_task(self, taskid): """ Returns a Task object """ return self._task_factory.get_task(self, taskid) def get_descriptor(self): """ Get (a copy) the description of the course """ return copy.deepcopy(self._content) def get_staff(self): """ Returns a list containing the usernames of all the staff users """ return list(set(self.get_tutors() + self.get_admins())) def get_admins(self): """ Returns a list containing the usernames of the administrators of this course """ return self._admins def get_tutors(self): """ Returns a list containing the usernames of the tutors assigned to this course """ return self._tutors def is_open_to_non_staff(self): """ Returns true if the course is accessible by users that are not administrator of this course """ return self.get_accessibility().is_open() def is_registration_possible(self, user_info): """ Returns true if users can register for this course """ return self.get_accessibility().is_open( ) and self._registration.is_open( ) and self.is_user_accepted_by_access_control(user_info) def is_password_needed_for_registration(self): """ Returns true if a password is needed for registration """ return self._registration_password is not None def get_registration_password(self): """ Returns the password needed for registration (None if there is no password) """ return self._registration_password def get_accessibility(self, plugin_override=True): """ Return the AccessibleTime object associated with the accessibility of this course """ vals = self._plugin_manager.call_hook('course_accessibility', course=self, default=self._accessible) return vals[0] if len(vals) and plugin_override else self._accessible def get_registration_accessibility(self): """ Return the AccessibleTime object associated with the registration """ return self._registration def get_tasks(self): tasks = self._task_factory.get_all_tasks(self) return OrderedDict( sorted(list(tasks.items()), key=lambda t: (t[1].get_order(), t[1].get_id()))) def get_access_control_method(self): """ Returns either None, "username", "binding", or "email", depending on the method used to verify that users can register to the course """ return self._registration_ac def get_access_control_list(self): """ Returns the list of all users allowed by the AC list """ return self._registration_ac_list def can_students_choose_group(self): """ Returns True if the students can choose their groups """ return self._groups_student_choice def is_lti(self): """ True if the current course is in LTI mode """ return self._is_lti def lti_keys(self): """ {name: key} for the LTI customers """ return self._lti_keys if self._is_lti else {} def lti_url(self): """ Returns the URL to the external platform the course is hosted on """ return self._lti_url def lti_send_back_grade(self): """ True if the current course should send back grade to the LTI Tool Consumer """ return self._is_lti and self._lti_send_back_grade def is_user_accepted_by_access_control(self, user_info): """ Returns True if the user is allowed by the ACL """ if self.get_access_control_method() is None: return True elif not user_info: return False elif self.get_access_control_method() == "username": return user_info["username"] in self.get_access_control_list() elif self.get_access_control_method() == "email": return user_info["email"] in self.get_access_control_list() elif self.get_access_control_method() == "binding": return set(user_info["bindings"].keys()).intersection( set(self.get_access_control_list())) return False def allow_preview(self): return self._allow_preview def allow_unregister(self, plugin_override=True): """ Returns True if students can unregister from course """ vals = self._plugin_manager.call_hook('course_allow_unregister', course=self, default=self._allow_unregister) return vals[0] if len( vals) and plugin_override else self._allow_unregister def get_name(self, language): """ Return the name of this course """ return self.gettext(language, self._name) if self._name else "" def get_description(self, language): """Returns the course description """ description = self.gettext( language, self._description) if self._description else '' return ParsableText(description, "rst", translation=self.get_translation_obj(language)) def get_tags(self): return self._tags def get_toc(self): """ :return: the structure of the course """ return self._toc