diff --git a/inginious/common/custom_yaml.py b/inginious/common/custom_yaml.py index f86d0584b..ff3f64440 100644 --- a/inginious/common/custom_yaml.py +++ b/inginious/common/custom_yaml.py @@ -5,6 +5,8 @@ """ A custom YAML based on PyYAML, that provides Ordered Dicts """ # Most ideas for this implementation comes from http://stackoverflow.com/questions/5121931/in-python-how-can-you-load-yaml-mappings-as-ordereddicts +from datetime import datetime + from collections import OrderedDict import yaml as original_yaml @@ -74,10 +76,16 @@ def _long_str_representer(dumper, data): def _default_representer(dumper, data): return _long_str_representer(dumper, str(data)) + def _timestamp_representer(dumper, data): + date = data.strftime("%4Y-%m-%dT%H:%M:%SZ") + return dumper.represent_scalar('tag:yaml.org,2002:timestamp', date) + + OrderedDumper.add_representer(str, _long_str_representer) OrderedDumper.add_representer(str, _long_str_representer) OrderedDumper.add_representer(OrderedDict, _dict_representer) OrderedDumper.add_representer(None, _default_representer) + OrderedDumper.add_representer(datetime, _timestamp_representer) s = original_yaml.dump(data, stream, OrderedDumper, encoding='utf-8', allow_unicode=True, default_flow_style=False, indent=4, **kwds) diff --git a/inginious/frontend/accessible_time.py b/inginious/frontend/accessible_time.py index 4da1a370b..714674801 100644 --- a/inginious/frontend/accessible_time.py +++ b/inginious/frontend/accessible_time.py @@ -9,15 +9,25 @@ def parse_date(date, default=None): - """ Parse a valid date """ + """ + Parse a valid date + :param date: string, date to parse + :param default: datetime object, optionnal, default value to return if date is empty + :return: datetime object of the parsed date + """ if date == "": if default is not None: return default else: - raise Exception("Unknown format for " + date) + raise Exception("Empty date given to AccessibleTime") - for format_type in ["%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M", "%Y-%m-%d %H", "%Y-%m-%d", "%d/%m/%Y %H:%M:%S", "%d/%m/%Y %H:%M", "%d/%m/%Y %H", - "%d/%m/%Y"]: + if date == "0001-01-01 00:00:00": + return datetime.min + if date == "9999-12-31 23:59:59": + return datetime.max.replace(microsecond=0) + + for format_type in ["%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M", "%Y-%m-%d %H", "%Y-%m-%d", "%d/%m/%Y %H:%M:%S", + "%d/%m/%Y %H:%M", "%d/%m/%Y %H", "%d/%m/%Y"]: try: return datetime.strptime(date, format_type) except ValueError: @@ -28,55 +38,78 @@ def parse_date(date, default=None): class AccessibleTime(object): """ represents the period of time when a course/task is accessible """ - def __init__(self, val=None): + def __init__(self, period): """ - Parse a string/a boolean to get the correct time period. - Correct values for val: - True (task always open) - False (task always closed) - 2014-07-16 11:24:00 (task is open from 2014-07-16 at 11:24:00) - 2014-07-16 (task is open from 2014-07-16) - / 2014-07-16 11:24:00 (task is only open before the 2014-07-16 at 11:24:00) - / 2014-07-16 (task is only open before the 2014-07-16) - 2014-07-16 11:24:00 / 2014-07-20 11:24:00 (task is open from 2014-07-16 11:24:00 and will be closed the 2014-07-20 at 11:24:00) - 2014-07-16 / 2014-07-20 11:24:00 (...) - 2014-07-16 11:24:00 / 2014-07-20 (...) - 2014-07-16 / 2014-07-20 (...) - 2014-07-16 11:24:00 / 2014-07-20 11:24:00 / 2014-07-20 12:24:00 (task is open from 2014-07-16 11:24:00, has a soft deadline set at 2014-07-20 11:24:00 and will be closed the 2014-07-20 at 11:24:00) - 2014-07-16 / 2014-07-20 11:24:00 / 2014-07-21 (...) - 2014-07-16 / 2014-07-20 / 2014-07-21 (...) + Used to represent the period of time when a course/task is accessible. + :param period : dict, contains start, end and optionally soft_end as datetime objects or strings + (for frontend use through templates). + Can be a boolean, None or string if using the legacy format "start/soft_end/end" """ - if val is None or val == "" or val is True: - self._val = [datetime.min, datetime.max] - self._soft_end = datetime.max - elif val == False: - self._val = [datetime.max, datetime.max] - self._soft_end = datetime.max - else: # str - values = val.split("/") - if len(values) == 1: - self._val = [parse_date(values[0].strip(), datetime.min), datetime.max] - self._soft_end = datetime.max - elif len(values) == 2: - # Has start time and hard deadline - self._val = [parse_date(values[0].strip(), datetime.min), parse_date(values[1].strip(), datetime.max)] - self._soft_end = self._val[1] - else: - # Has start time, soft deadline and hard deadline - self._val = [parse_date(values[0].strip(), datetime.min), parse_date(values[2].strip(), datetime.max)] - self._soft_end = parse_date(values[1].strip(), datetime.max) - # Having a soft deadline after the hard one does not make sense, make soft-deadline same as hard-deadline - if self._soft_end > self._val[1]: - self._soft_end = self._val[1] + self.max = datetime.max.replace(microsecond=0) + self.min = datetime.min + if not isinstance(period, (dict, str, bool, type(None))): # add None check + raise Exception("Wrong period given to AccessibleTime") + + # if legacy format (start/soft_end/end string, empty string, bool) + if isinstance(period, str): + period = self.legacy_string_structure_to_dict(period) + if isinstance(period, (bool, type(None))): + if period is (True or None): + period = {"start": self.min, "soft_end": self.max, "end": self.max} + else: + period = {"start": self.max, "soft_end": self.max, "end": self.max} + + # transforming strings into datetimes in case AccessibleTime is used in html files, where datetime objects are not supported + for key, date in period.items(): + if not isinstance(date, (datetime, str)): + raise Exception("Wrong period given to AccessibleTime") + if isinstance(date, str): + period[key] = parse_date(date) + + self._start = period["start"] + self._end = period["end"] + if "soft_end" in period: + if period["soft_end"] == self.max: + self._soft_end = self.max + else: + soft_end = min(period["soft_end"], period["end"]) + self._soft_end = soft_end + + def legacy_string_structure_to_dict(self, legacy_date): + """ + Convert the legacy string structure to a dictionary. The legacy structure follows "start/soft_end/end" for + tasks or "start/end" for courses with some of the values being optional. Sometimes only a start date is + given as a string (ex: "start//end", "start//", "//end", "start/end", "start", "/end", ...). + :param legacy_date: string, legacy date structure + :return period: dict, containing the start, soft_end and end as strings + """ + period = {} + + values = legacy_date.split("/") + if len(values) == 1: + period["start"] = parse_date(values[0].strip(), self.min) + period["soft_end"] = self.max + period["end"] = self.max + elif len(values) == 2: + # Has start time and hard deadline + period["start"] = parse_date(values[0].strip(), self.min) + period["end"] = parse_date(values[1].strip(), self.max) + period["soft_end"] = period["end"] + else: + # Has start time, soft deadline and hard deadline + period["start"] = parse_date(values[0].strip(), self.min) + period["soft_end"] = parse_date(values[1].strip(), self.max) + period["end"] = parse_date(values[2].strip(), self.max) + return period def before_start(self, when=None): """ Returns True if the task/course is not yet accessible """ if when is None: when = datetime.now() - return self._val[0] > when + return self._start > when def after_start(self, when=None): """ Returns True if the task/course is or have been accessible in the past """ @@ -87,54 +120,61 @@ def is_open(self, when=None): if when is None: when = datetime.now() - return self._val[0] <= when and when <= self._val[1] + return self._start <= when <= self._end def is_open_with_soft_deadline(self, when=None): """ Returns True if the course/task is still open with the soft deadline """ if when is None: when = datetime.now() - return self._val[0] <= when and when <= self._soft_end + return self._start <= when <= self._soft_end def is_always_accessible(self): """ Returns true if the course/task is always accessible """ - return self._val[0] == datetime.min and self._val[1] == datetime.max + return self._start == self.min and self._end == self.max def is_never_accessible(self): """ Returns true if the course/task is never accessible """ - return self._val[0] == datetime.max and self._val[1] == datetime.max + return self._start == self.max and self._end == self.max def get_std_start_date(self): - """ If the date is custom, return the start datetime with the format %Y-%m-%d %H:%M:%S. Else, returns "". """ - first, _ = self._val - if first != datetime.min and first != datetime.max: - return first.strftime("%Y-%m-%d %H:%M:%S") - else: - return "" + """ If the date is custom, return the start datetime with the format %4Y-%m-%d %H:%M:%S. Else, returns "". """ + if self._start not in [self.min, self.max]: + return self._start.strftime("%4Y-%m-%d %H:%M:%S") + return "" def get_std_end_date(self): - """ If the date is custom, return the end datetime with the format %Y-%m-%d %H:%M:%S. Else, returns "". """ - _, second = self._val - if second != datetime.max: - return second.strftime("%Y-%m-%d %H:%M:%S") - else: - return "" + """ If the date is custom, return the end datetime with the format %4Y-%m-%d %H:%M:%S. Else, returns "". """ + if self._end != self.max: + return self._end.strftime("%4Y-%m-%d %H:%M:%S") + return "" def get_std_soft_end_date(self): - """ If the date is custom, return the soft datetime with the format %Y-%m-%d %H:%M:%S. Else, returns "". """ - if self._soft_end != datetime.max: - return self._soft_end.strftime("%Y-%m-%d %H:%M:%S") - else: - return "" + """ If the date is custom, return the soft datetime with the format %4Y-%m-%d %H:%M:%S. Else, returns "". """ + if self._soft_end != self.max: + return self._soft_end.strftime("%4Y-%m-%d %H:%M:%S") + return "" def get_start_date(self): """ Return a datetime object, representing the date when the task/course become accessible """ - return self._val[0] + return self._start def get_end_date(self): """ Return a datetime object, representing the deadline for accessibility """ - return self._val[1] + return self._end def get_soft_end_date(self): """ Return a datetime object, representing the soft deadline for accessibility """ return self._soft_end + + def string_date(self, date): + """ Returns the date as a string """ + return date.strftime("%4Y-%m-%d %H:%M:%S") + + def get_string_dict(self): + """ Returns a dictionary with the start, end and soft_end as strings """ + return { + "start": self.string_date(self._start), + "soft_end": self.string_date(self._soft_end), + "end": self.string_date(self._end) + } diff --git a/inginious/frontend/course_factory.py b/inginious/frontend/course_factory.py index 05e66d329..c56ef32c1 100644 --- a/inginious/frontend/course_factory.py +++ b/inginious/frontend/course_factory.py @@ -113,6 +113,7 @@ def _migrate_legacy_courses(self): cleaned_taskset_descriptor["dispenser_data"] = taskset_descriptor.get("dispenser_data", {}) taskset_descriptor["tasksetid"] = courseid taskset_descriptor["admins"] = taskset_descriptor.get("admins", []) + taskset_descriptor.get("tutors", []) + self._database.courses.update_one({"_id": courseid}, {"$set": taskset_descriptor}, upsert=True) self._taskset_factory.update_taskset_descriptor_content(courseid, cleaned_taskset_descriptor) except TasksetNotFoundException as e: diff --git a/inginious/frontend/courses.py b/inginious/frontend/courses.py index 74fc6e1cc..703b4dabb 100644 --- a/inginious/frontend/courses.py +++ b/inginious/frontend/courses.py @@ -9,6 +9,7 @@ import gettext import re from typing import List +from datetime import datetime from inginious.frontend.user_settings.course_user_setting import CourseUserSetting from inginious.common.tags import Tag @@ -41,10 +42,10 @@ def __init__(self, courseid, content, taskset_factory, task_factory, plugin_mana self._admins = self._content.get('admins', []) 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) + self._accessible = AccessibleTime(self._content.get('accessible')) + self._registration = AccessibleTime(self._content.get('registration')) + self._registration_password = self._content.get('registration_password') + self._registration_ac = self._content.get('registration_ac') 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_accept = self._content.get('registration_ac_accept', True) @@ -74,8 +75,8 @@ def __init__(self, courseid, content, taskset_factory, task_factory, plugin_mana # Force some parameters if LTI is active if self.is_lti(): - self._accessible = AccessibleTime(True) - self._registration = AccessibleTime(False) + self._accessible = AccessibleTime({"start": datetime.min, "end": datetime.max.replace(microsecond=0)}) + self._registration = AccessibleTime({"start": datetime.max.replace(microsecond=0), "end": datetime.max.replace(microsecond=0)}) self._registration_password = None self._registration_ac = None self._registration_ac_list = [] diff --git a/inginious/frontend/pages/course_admin/danger_zone.py b/inginious/frontend/pages/course_admin/danger_zone.py index 51b63dcb6..99bfb8d97 100644 --- a/inginious/frontend/pages/course_admin/danger_zone.py +++ b/inginious/frontend/pages/course_admin/danger_zone.py @@ -182,7 +182,7 @@ def POST_AUTH(self, courseid): # pylint: disable=arguments-differ try: dt = datetime.datetime.strptime(data["backupdate"], "%Y%m%d.%H%M%S") self.restore_course(courseid, data["backupdate"]) - msg = _("Course restored to date : {}.").format(dt.strftime("%Y-%m-%d %H:%M:%S")) + msg = _("Course restored to date : {}.").format(dt.strftime("%4Y-%m-%d %H:%M:%S")) except Exception as ex: msg = _("An error occurred while restoring backup: {}").format(repr(ex)) error = True @@ -208,7 +208,7 @@ def get_backup_list(self, course): for backup in glob.glob(os.path.join(filepath, '*.zip')): try: basename = os.path.basename(backup)[0:-4] - dt = datetime.datetime.strptime(basename, "%Y%m%d.%H%M%S").strftime("%Y-%m-%d %H:%M:%S") + dt = datetime.datetime.strptime(basename, "%Y%m%d.%H%M%S").strftime("%4Y-%m-%d %H:%M:%S") backups.append({"file": basename, "date": dt}) except: # Wrong format pass diff --git a/inginious/frontend/pages/course_admin/settings.py b/inginious/frontend/pages/course_admin/settings.py index fa230129f..c12e573e5 100644 --- a/inginious/frontend/pages/course_admin/settings.py +++ b/inginious/frontend/pages/course_admin/settings.py @@ -3,6 +3,7 @@ # This file is part of INGInious. See the LICENSE and the COPYRIGHTS files for # more information about the licensing of this file. +from datetime import datetime import re import flask @@ -39,12 +40,22 @@ def POST_AUTH(self, courseid): # pylint: disable=arguments-differ course_content['groups_student_choice'] = True if data["groups_student_choice"] == "true" else False + if isinstance(data["accessible"], (str, bool)): + course_content["accessible"] = {} + if isinstance(data["registration"], (str, bool)): + course_content["registration"] = {} + + course_accessibility = course.get_accessibility() + if data["accessible"] == "custom": - course_content['accessible'] = "{}/{}".format(data["accessible_start"], data["accessible_end"]) + course_content['accessible']["start"] = datetime.strptime(data["accessible_start"], '%Y-%m-%d %H:%M:%S') if data["accessible_start"] != "" else course_accessibility.min + course_content['accessible']["end"] = datetime.strptime(data["accessible_end"], '%Y-%m-%d %H:%M:%S') if data["accessible_end"] != "" else course_accessibility.max elif data["accessible"] == "true": - course_content['accessible'] = True + course_content['accessible']["start"] = course_accessibility.min + course_content['accessible']["end"] = course_accessibility.max else: - course_content['accessible'] = False + course_content['accessible']["start"] = course_accessibility.max + course_content['accessible']["end"] = course_accessibility.max try: AccessibleTime(course_content['accessible']) @@ -55,15 +66,18 @@ def POST_AUTH(self, courseid): # pylint: disable=arguments-differ 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"]) + course_content['registration']["start"] = datetime.strptime(data["registration_start"],'%Y-%m-%d %H:%M:%S') if data["registration_start"] != "" else course_accessibility.min + course_content['registration']["end"] = datetime.strptime(data["registration_end"], '%Y-%m-%d %H:%M:%S') if data["registration_end"] != "" else course_accessibility.max elif data["registration"] == "true": - course_content['registration'] = True + course_content['registration']["start"] = course_accessibility.min + course_content['registration']["end"] = course_accessibility.max else: - course_content['registration'] = False + course_content['registration']["start"] = course_accessibility.max + course_content['registration']["end"] = course_accessibility.max try: AccessibleTime(course_content['registration']) - except: + except Exception: errors.append(_('Invalid registration dates')) course_content['registration_password'] = data['registration_password'] diff --git a/inginious/frontend/pages/course_admin/statistics.py b/inginious/frontend/pages/course_admin/statistics.py index dd9e8e68b..43b9841ef 100644 --- a/inginious/frontend/pages/course_admin/statistics.py +++ b/inginious/frontend/pages/course_admin/statistics.py @@ -195,8 +195,8 @@ def page(self, course, params): now = datetime.now().replace(minute=0, second=0, microsecond=0) daterange = [now - timedelta(days=14), now] - params["date_before"] = daterange[1].strftime("%Y-%m-%d %H:%M:%S") - params["date_after"] = daterange[0].strftime("%Y-%m-%d %H:%M:%S") + params["date_before"] = daterange[1].strftime("%4Y-%m-%d %H:%M:%S") + params["date_after"] = daterange[0].strftime("%4Y-%m-%d %H:%M:%S") display_hours = (daterange[1] - daterange[0]).days < 4 users, tutored_users, audiences, tutored_audiences, tasks, limit = self.get_course_params(course, params) @@ -209,7 +209,7 @@ def page(self, course, params): float(params["grade_min"]) if params.get('grade_min', '') else None, float(params["grade_max"]) if params.get('grade_max', '') else None ], - submit_time_between=[x.strftime("%Y-%m-%d %H:%M:%S") for x in daterange], + submit_time_between=[x.strftime("%4Y-%m-%d %H:%M:%S") for x in daterange], keep_only_crashes="crashes_only" in params) stats_tasks = self._tasks_stats(tasks, filter, limit) diff --git a/inginious/frontend/pages/course_admin/task_list.py b/inginious/frontend/pages/course_admin/task_list.py index 71e85acaa..e06f462ca 100644 --- a/inginious/frontend/pages/course_admin/task_list.py +++ b/inginious/frontend/pages/course_admin/task_list.py @@ -11,7 +11,6 @@ from inginious.frontend.pages.course_admin.utils import INGIniousAdminPage - class CourseTaskListPage(INGIniousAdminPage): """ List informations about all tasks """ diff --git a/inginious/frontend/pages/marketplace.py b/inginious/frontend/pages/marketplace.py index a92640a80..5c0809f48 100644 --- a/inginious/frontend/pages/marketplace.py +++ b/inginious/frontend/pages/marketplace.py @@ -5,6 +5,7 @@ """ Course page """ import sys +from datetime import datetime import flask from flask import redirect from werkzeug.exceptions import Forbidden @@ -82,7 +83,7 @@ def import_taskset(taskset, new_tasksetid, username, taskset_factory): try: new_descriptor = {"description": old_descriptor.get("description", ""), 'admins': [username], - "accessible": False, + "accessible": {"start": datetime.max.replace(microsecond=0), "end": datetime.max.replace(microsecond=0)}, "tags": old_descriptor.get("tags", {})} if "name" in old_descriptor: new_descriptor["name"] = old_descriptor["name"] + " - " + new_tasksetid diff --git a/inginious/frontend/pages/tasks.py b/inginious/frontend/pages/tasks.py index 6515465c7..85d68dfd3 100644 --- a/inginious/frontend/pages/tasks.py +++ b/inginious/frontend/pages/tasks.py @@ -341,7 +341,7 @@ def submission_to_json(self, task, data, debug, reloading=False, replace=False, tojson["title"] = _("An internal error occurred. Please retry later. " "If the error persists, send an email to the course administrator.") - tojson["title"] += " " + _("[Submission #{submissionid} ({submissionDate})]").format(submissionid=data["_id"], submissionDate=data["submitted_on"].strftime("%Y-%m-%d %H:%M:%S")) + tojson["title"] += " " + _("[Submission #{submissionid} ({submissionDate})]").format(submissionid=data["_id"], submissionDate=data["submitted_on"].strftime("%4Y-%m-%d %H:%M:%S")) tojson["title"] = self.plugin_manager.call_hook_recursive("feedback_title", task=task, submission=data, title=tojson["title"])["title"] tojson["text"] = data.get("text", "") diff --git a/inginious/frontend/pages/tasksets.py b/inginious/frontend/pages/tasksets.py index 27a03a80a..e1e699657 100644 --- a/inginious/frontend/pages/tasksets.py +++ b/inginious/frontend/pages/tasksets.py @@ -4,6 +4,7 @@ # more information about the licensing of this file. """ Index page """ +from datetime import datetime import flask from collections import OrderedDict @@ -35,7 +36,8 @@ def POST_AUTH(self): # pylint: disable=arguments-differ if self.user_manager.session_username() in taskset.get_admins() or taskset.is_public() or self.user_manager.user_is_superadmin(): task_dispenser = taskset.get_task_dispenser() self.course_factory.create_course(courseid, { - "name": courseid, "accessible": False, "tasksetid": taskset.get_id(), + "name": courseid, "accessible": {"start": datetime.max.replace(microsecond=0), "end": datetime.max.replace(microsecond=0)}, + "registration": {"start": datetime.max.replace(microsecond=0), "end": datetime.max.replace(microsecond=0)}, "tasksetid": taskset.get_id(), "admins": [self.user_manager.session_username()], "students": [], "task_dispenser": task_dispenser.get_id(), "dispenser_data": task_dispenser.get_dispenser_data() }) diff --git a/inginious/frontend/plugins/contests/__init__.py b/inginious/frontend/plugins/contests/__init__.py index 8fe079c1a..ef21770f9 100644 --- a/inginious/frontend/plugins/contests/__init__.py +++ b/inginious/frontend/plugins/contests/__init__.py @@ -35,8 +35,8 @@ def __init__(self, task_list_func, dispenser_data, database, course_id): self._contest_settings = dispenser_data.get( 'contest_settings', {"enabled": False, - "start": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), - "end": (datetime.now() + timedelta(hours=1)).strftime("%Y-%m-%d %H:%M:%S"), + "start": datetime.now(), + "end": datetime.now() + timedelta(hours=1), "blackout": 0, "penalty": 20} ) @@ -57,7 +57,8 @@ def check_dispenser_data(self, dispenser_data): def get_accessibilities(self, taskids, usernames): # pylint: disable=unused-argument contest_data = self.get_contest_data() if contest_data['enabled']: - return {username: {taskid: AccessibleTime(contest_data['start'] + '/') for taskid in taskids} for username in usernames} + accessibility = {"start": contest_data['start'], "soft_end": contest_data['end'], "end": contest_data['end']} + return {username: {taskid: AccessibleTime(accessibility) for taskid in taskids} for username in usernames} else: return TableOfContents.get_accessibilities(self, taskids, usernames) @@ -82,8 +83,8 @@ def course_menu(course, template_helper): contest_data = task_dispenser.get_contest_data() if contest_data['enabled']: - start = datetime.strptime(contest_data['start'], "%Y-%m-%d %H:%M:%S") - end = datetime.strptime(contest_data['end'], "%Y-%m-%d %H:%M:%S") + start = contest_data['start'] + end = contest_data['end'] blackout = end - timedelta(hours=contest_data['blackout']) return template_helper.render("course_menu.html", template_folder="frontend/plugins/contests", course=course, start=start, end=end, blackout=blackout) @@ -95,15 +96,15 @@ class ContestScoreboard(INGIniousAuthPage): """ Displays the scoreboard of the contest """ def GET_AUTH(self, courseid): # pylint: disable=arguments-differ - course = self.taskset_factory.get_course(courseid) + course = self.course_factory.get_course(courseid) task_dispenser = course.get_task_dispenser() if not task_dispenser.get_id() == Contest.get_id(): raise NotFound() contest_data = task_dispenser.get_contest_data() if not contest_data['enabled']: raise NotFound() - start = datetime.strptime(contest_data['start'], "%Y-%m-%d %H:%M:%S") - end = datetime.strptime(contest_data['end'], "%Y-%m-%d %H:%M:%S") + start = contest_data['start'] + end = contest_data['end'] blackout = end - timedelta(hours=contest_data['blackout']) users = self.user_manager.get_course_registered_users(course) @@ -191,9 +192,9 @@ class ContestAdmin(INGIniousAdminPage): def save_contest_data(self, course, contest_data): """ Saves updated contest data for the course """ - course_content = self.taskset_factory.get_course_descriptor_content(course.get_id()) + course_content = self.course_factory.get_course_descriptor_content(course.get_id()) course_content["dispenser_data"]["contest_settings"] = contest_data - self.taskset_factory.update_course_descriptor_content(course.get_id(), course_content) + self.course_factory.update_course_descriptor_content(course.get_id(), course_content) def GET_AUTH(self, courseid): # pylint: disable=arguments-differ """ GET request: simply display the form """ @@ -221,17 +222,17 @@ def POST_AUTH(self, courseid): # pylint: disable=arguments-differ contest_data['end'] = new_data["end"] try: - start = datetime.strptime(contest_data['start'], "%Y-%m-%d %H:%M:%S") + contest_data['start'] = datetime.strptime(contest_data['start'], "%Y-%m-%d %H:%M:%S") except: errors.append('Invalid start date') try: - end = datetime.strptime(contest_data['end'], "%Y-%m-%d %H:%M:%S") + contest_data['end'] = datetime.strptime(contest_data['end'], "%Y-%m-%d %H:%M:%S") except: errors.append('Invalid end date') if len(errors) == 0: - if start >= end: + if contest_data['start'] >= contest_data['end']: errors.append('Start date should be before end date') try: diff --git a/inginious/frontend/plugins/contests/course_menu.html b/inginious/frontend/plugins/contests/course_menu.html index e7e08bd85..d9b186b71 100644 --- a/inginious/frontend/plugins/contests/course_menu.html +++ b/inginious/frontend/plugins/contests/course_menu.html @@ -21,11 +21,11 @@

Contest

{% if start > start.now() %} - + {% else %} - + {% if blackout != end %} -
+
{% endif %} {% endif %}
diff --git a/inginious/frontend/plugins/contests/scoreboard.html b/inginious/frontend/plugins/contests/scoreboard.html index 9610502c7..340cf3d1d 100644 --- a/inginious/frontend/plugins/contests/scoreboard.html +++ b/inginious/frontend/plugins/contests/scoreboard.html @@ -32,11 +32,11 @@

{{course.get_name(user_manager.session_language())}} - Scoreboard

{% if start > start.now() %} - + {% else %} - + {% if blackout != end %} -
+
{% endif %} {% endif %}
diff --git a/inginious/frontend/task_dispensers/combinatory_test.py b/inginious/frontend/task_dispensers/combinatory_test.py index 0cae80bbf..82ebd9b5d 100644 --- a/inginious/frontend/task_dispensers/combinatory_test.py +++ b/inginious/frontend/task_dispensers/combinatory_test.py @@ -4,6 +4,8 @@ # more information about the licensing of this file. from random import Random +from datetime import datetime + import inginious from inginious.frontend.task_dispensers.toc import TableOfContents from inginious.frontend.task_dispensers.util import SectionConfigItem, Weight, SubmissionStorage, EvaluationMode, \ @@ -27,7 +29,8 @@ def get_group_submission(self, taskid): return False def get_accessibilities(self, taskids, usernames): - result = {username: {taskid: AccessibleTime(False) for taskid in taskids} for username in usernames} + result = {username: {taskid: AccessibleTime({"start": datetime.min, "soft_end": datetime.max.replace(microsecond=0), "end": datetime.max.replace(microsecond=0)}) + for taskid in taskids} for username in usernames} for index, section in enumerate(self._toc): task_list = [taskid for taskid in section.get_tasks() if AccessibleTime(Accessibility.get_value(self._task_config.get(taskid, {}))).after_start()] @@ -37,7 +40,8 @@ def get_accessibilities(self, taskids, usernames): random_order_choices = task_list.copy() rand.shuffle(random_order_choices) for taskid in random_order_choices[0:amount_questions]: - result[username][taskid] = AccessibleTime(Accessibility.get_value(self._task_config.get(taskid, {}))) + accessibility_period = Accessibility.get_value(self._task_config.get(taskid, {})) + result[username][taskid] = AccessibleTime(accessibility_period) return result @@ -50,9 +54,8 @@ def render_edit(self, template_helper, element, task_data, task_errors): taskset = element if isinstance(element, inginious.frontend.tasksets.Taskset) else None course = element if isinstance(element, inginious.frontend.courses.Course) else None - return template_helper.render("task_dispensers_admin/combinatory_test.html", element=element, course=course, - taskset=taskset, dispenser_structure=self._toc, dispenser_config=self._task_config, - tasks=task_data, task_errors=task_errors, config_fields=config_fields) + return template_helper.render("task_dispensers_admin/combinatory_test.html", element=element, course=course, taskset=taskset, + dispenser_structure=self._toc, dispenser_config=self._task_config, tasks=task_data, task_errors=task_errors, config_fields=config_fields) def render(self, template_helper, course, tasks_data, tag_list, username): """ Returns the formatted task list""" diff --git a/inginious/frontend/task_dispensers/toc.py b/inginious/frontend/task_dispensers/toc.py index 98374102c..303e15852 100644 --- a/inginious/frontend/task_dispensers/toc.py +++ b/inginious/frontend/task_dispensers/toc.py @@ -37,6 +37,7 @@ def __init__(self, task_list_func, dispenser_data, database, element_id): self._toc = SectionsList(dispenser_data.get("toc", {})) self._task_config = dispenser_data.get("config", {}) parse_tasks_config(self._task_list_func().keys(), self.config_items, self._task_config) + self._task_config = self.adapt_accessibilities(self._task_config) @classmethod def get_id(cls): @@ -161,3 +162,10 @@ def import_legacy_tasks(self): raise Exception(f"In task {taskid} : {e}") dispenser_data["imported"] = True return dispenser_data + + def adapt_accessibilities(self, task_config): # better name ? -> function to pass data through all potential objects + """ Adapts the task accessibilities to the new format by passing them through the AccessibleTime object""" + for task in task_config: + accessibility = AccessibleTime(Accessibility.get_value(self._task_config.get(task, {}))) # reproduces .get_accessibilities from TableOfContent + task_config[task]["accessibility"] = accessibility.get_string_dict() + return task_config diff --git a/inginious/frontend/task_dispensers/util.py b/inginious/frontend/task_dispensers/util.py index 9af674bd6..b85e1c38d 100644 --- a/inginious/frontend/task_dispensers/util.py +++ b/inginious/frontend/task_dispensers/util.py @@ -1,7 +1,12 @@ +""" +Contains de task config item classes and the sections classes. +""" # -*- coding: utf-8 -*- # # This file is part of INGInious. See the LICENSE and the COPYRIGHTS files for # more information about the licensing of this file. +from datetime import datetime + from abc import ABCMeta, abstractmethod from collections import namedtuple from inginious.common.base import id_checker @@ -178,7 +183,7 @@ def get_value(cls, task_config): class Accessibility(TaskConfigItem): - default = False + default = {"start": datetime.min, "soft_end": datetime.max.replace(microsecond=0), "end": datetime.max.replace(microsecond=0)} @classmethod def get_template(cls): @@ -195,10 +200,11 @@ def get_name(cls): @classmethod def get_value(cls, task_config): accessibility = task_config.get(cls.get_id(), cls.default) - try: - AccessibleTime(accessibility) - except Exception as message: - raise InvalidTocException("Invalid task accessibility : {}".format(message)) + if isinstance(accessibility, dict): + try: + AccessibleTime(accessibility) + except Exception as message: + raise InvalidTocException("Invalid task accessibility : {}".format(message)) return accessibility @@ -371,7 +377,6 @@ def parse_tasks_config(task_list, config_items, data): def check_task_config(task_list, config_items, data): """ - :param data: the raw content of the task settings :return: (True, '') if the settings are valid or (False, The error message) otherwise """ @@ -379,4 +384,4 @@ def check_task_config(task_list, config_items, data): parse_tasks_config(task_list, config_items, data) return True, '' except Exception as ex: - return False, str(ex) \ No newline at end of file + return False, str(ex) diff --git a/inginious/frontend/tasks.py b/inginious/frontend/tasks.py index 4404d8595..ea46f0c68 100644 --- a/inginious/frontend/tasks.py +++ b/inginious/frontend/tasks.py @@ -102,9 +102,6 @@ def __init__(self, taskset, taskid, content, plugin_manager, task_problem_types) else: self._contact_url = "" - # _accessible - self._accessible = AccessibleTime(self._data.get("accessible", None)) - # Input random self._input_random = int(self._data.get("input_random", 0)) diff --git a/inginious/frontend/templates/task_dispensers_admin/config_items/accessibility.html b/inginious/frontend/templates/task_dispensers_admin/config_items/accessibility.html index bb2032989..a661bbe05 100644 --- a/inginious/frontend/templates/task_dispensers_admin/config_items/accessibility.html +++ b/inginious/frontend/templates/task_dispensers_admin/config_items/accessibility.html @@ -1,8 +1,8 @@ {# This file is part of INGInious. See the LICENSE and the COPYRIGHTS files for #} {# more information about the licensing of this file. #} +{% set at = element.get_task_dispenser().get_accessibility(taskid, user_manager.session_username()) %} {% if short %} - {% set at = element.get_task_dispenser().get_accessibility(taskid, user_manager.session_username()) %} {{_("Accessibility")}} : @@ -83,21 +83,33 @@ $("#edit_task_modal").on('show.bs.modal', function (event) { var button = $(event.relatedTarget); var taskid = button.data('taskid'); + if (!("accessibility" in dispenser_config[taskid])) return; var accessibility = dispenser_config[taskid]["accessibility"]; var value; - if (accessibility === true) { - value = 'true'; - } else if (accessibility === false) { - value = 'false'; - } else { + special_dates = ["{{ at.min }}", "{{ at.max }}"] + function isSpecialDate(date) { + return special_dates.includes(date); + } + + if (Object.values(accessibility).some(date => !isSpecialDate(date))) { value = 'custom'; - var splitted_values = accessibility.split("/"); - $(this).find("#accessibility_start").val(splitted_values[0]); - $(this).find("#accessibility_soft_end").val(splitted_values[1]); - $(this).find("#accessibility_end").val(splitted_values[2]); + + $(this).find('#accessibility_start_picker').datetimepicker('date', isSpecialDate(accessibility["start"]) ? null : accessibility["start"]); + $(this).find('#accessibility_soft_end_picker').datetimepicker('date', isSpecialDate(accessibility["soft_end"]) ? null : accessibility["soft_end"]); + $(this).find('#accessibility_end_picker').datetimepicker('date', isSpecialDate(accessibility["end"]) ? null : accessibility["end"]); + } else { + if (Object.values(accessibility).some(date => date == "{{ at.min }}")) { + value = 'true'; + } else { + value = 'false'; + } + + $(this).find('#accessibility_start_picker').datetimepicker('date', null); + $(this).find('#accessibility_soft_end_picker').datetimepicker('date', null); + $(this).find('#accessibility_end_picker').datetimepicker('date', null); } var field = $(this).find(".accessibility input[value=" + value + "]"); @@ -114,62 +126,68 @@ $("#task_" + taskid + " .accessibility").hide(); if($(this).val() == "true") { $("#task_" + taskid + " .accessibility-always").show(); - dispenser_config[taskid]["accessibility"] = true; + dispenser_config[taskid]["accessibility"]["start"] = "{{ at.min }}"; + dispenser_config[taskid]["accessibility"]["soft_end"] = "{{ at.max }}"; + dispenser_config[taskid]["accessibility"]["end"] = "{{ at.max }}"; } else if($(this).val() == "false") { $("#task_" + taskid + " .accessibility-never").show(); - dispenser_config[taskid]["accessibility"] = false; + dispenser_config[taskid]["accessibility"]["start"] = "{{ at.max }}"; + dispenser_config[taskid]["accessibility"]["soft_end"] = "{{ at.max }}"; + dispenser_config[taskid]["accessibility"]["end"] = "{{ at.max }}"; } else { $("#task_" + taskid + " .accessibility-custom").show(); var start = $("#edit_task_modal").find("#accessibility_start").val(); var soft_end = $("#edit_task_modal").find("#accessibility_soft_end").val(); var end = $("#edit_task_modal").find("#accessibility_end").val(); - dispenser_config[taskid]["accessibility"] = start + "/" + soft_end + "/" + end; + dispenser_config[taskid]["accessibility"]["start"] = start == "" ? "{{ at.min }}" : start; + dispenser_config[taskid]["accessibility"]["soft_end"] = soft_end == "" ? "{{ at.max }}" : soft_end; + dispenser_config[taskid]["accessibility"]["end"] = end == "" ? "{{ at.max }}" : end; } } else { let id = $(this).attr("id"); - if (id == "accessibility_start") - $("#task_" + taskid + " .accessibility-custom-start").text($(this).val()); - else if(id == "accessibility_end") - $("#task_" + taskid + " .accessibility-custom-end").text($(this).val()); - else - $("#task_" + taskid + " .accessibility-custom-soft-end").text($(this).val()); - - var start = $("#edit_task_modal").find("#accessibility_start").val(); - var soft_end = $("#edit_task_modal").find("#accessibility_soft_end").val(); - var end = $("#edit_task_modal").find("#accessibility_end").val(); - dispenser_config[taskid]["accessibility"] = start + "/" + soft_end + "/" + end; + + if (id == "accessibility_start") { + var start = $("#task_" + taskid + " .accessibility-custom-start").text($(this).val()); + dispenser_config[taskid]["accessibility"]["start"] = start == "" ? "{{ at.min }}" : start; + } else if(id == "accessibility_end") { + var end = $("#task_" + taskid + " .accessibility-custom-end").text($(this).val()); + dispenser_config[taskid]["accessibility"]["end"] = end == "" ? "{{ at.max }}" : end; + } else { + var soft_end = $("#task_" + taskid + " .accessibility-custom-soft-end").text($(this).val()); + dispenser_config[taskid]["accessibility"]["soft_end"] = soft_end == "" ? "{{ at.max }}" : soft_end; + } } }); $('#accessibility_start_picker').on("change.datetimepicker", function () { + if( $("input[name='accessibility']:checked").val() != "custom") + return; var taskid = $("#edit_task_modal").data("taskid"); let val = $("#edit_task_modal #accessibility_start").val(); $("#task_" + taskid + " .accessibility-custom-start").text(val); var start = $("#edit_task_modal").find("#accessibility_start").val(); - var soft_end = $("#edit_task_modal").find("#accessibility_soft_end").val(); - var end = $("#edit_task_modal").find("#accessibility_end").val(); - dispenser_config[taskid]["accessibility"] = start + "/" + soft_end + "/" + end; + dispenser_config[taskid]["accessibility"]["start"] = start == "" ? "{{ at.min }}" : start; }); $('#accessibility_end_picker').on("change.datetimepicker", function () { + if( $("input[name='accessibility']:checked").val() != "custom") + return; var taskid = $("#edit_task_modal").data("taskid"); let val = $("#edit_task_modal #accessibility_end").val(); $("#task_" + taskid + " .accessibility-custom-end").text(val); - var start = $("#edit_task_modal").find("#accessibility_start").val(); - var soft_end = $("#edit_task_modal").find("#accessibility_soft_end").val(); var end = $("#edit_task_modal").find("#accessibility_end").val(); - dispenser_config[taskid]["accessibility"] = start + "/" + soft_end + "/" + end; + dispenser_config[taskid]["accessibility"]["end"] = end == "" ? "{{ at.max }}" : end; }); $('#accessibility_soft_end_picker').on("change.datetimepicker", function () { + if( $("input[name='accessibility']:checked").val() != "custom") + return; var taskid = $("#edit_task_modal").data("taskid"); let val = $("#edit_task_modal #accessibility_soft_end").val(); $("#task_" + taskid + " .accessibility-custom-soft-end").text(val); - var start = $("#edit_task_modal").find("#accessibility_start").val(); var soft_end = $("#edit_task_modal").find("#accessibility_soft_end").val(); - var end = $("#edit_task_modal").find("#accessibility_end").val(); - dispenser_config[taskid]["accessibility"] = start + "/" + soft_end + "/" + end; + dispenser_config[taskid]["accessibility"]["soft_end"] = soft_end == "" ? "{{ at.max }}" : soft_end; }); // Catch grouped action changes @@ -184,17 +202,23 @@ $("#task_" + taskid + " .accessibility").hide(); if(value == "true") { $("#task_" + taskid + " .accessibility-always").show(); - dispenser_config[taskid]["accessibility"] = true; + dispenser_config[taskid]["accessibility"]["start"] = "{{ at.min }}"; + dispenser_config[taskid]["accessibility"]["soft_end"] = "{{ at.max }}"; + dispenser_config[taskid]["accessibility"]["end"] = "{{ at.max }}"; } else if(value== "false") { $("#task_" + taskid + " .accessibility-never").show(); - dispenser_config[taskid]["accessibility"] = false; + dispenser_config[taskid]["accessibility"]["start"] = "{{ at.max }}"; + dispenser_config[taskid]["accessibility"]["soft_end"] = "{{ at.max }}"; + dispenser_config[taskid]["accessibility"]["end"] = "{{ at.max }}"; } else { $("#task_" + taskid + " .accessibility-custom").show(); var start = $("#grouped-actions-edit #accessibility_start").val(); var soft_end = $("#grouped-actions-edit #accessibility_soft_end").val(); var end = $("#grouped-actions-edit #accessibility_end").val(); - dispenser_config[taskid]["accessibility"] = start + "/" + soft_end + "/" + end; + dispenser_config[taskid]["accessibility"]["start"] = start == "" ? "{{ at.min }}" : start; + dispenser_config[taskid]["accessibility"]["soft_end"] = soft_end == "" ? "{{ at.max }}" : soft_end; + dispenser_config[taskid]["accessibility"]["end"] = end == "" ? "{{ at.max }}" : end; } } else { if (id == "accessibility_start") @@ -207,7 +231,10 @@ var start = $("#grouped-actions-edit #accessibility_start").val(); var soft_end = $("#grouped-actions-edit #accessibility_soft_end").val(); var end = $("#grouped-actions-edit #accessibility_end").val(); - dispenser_config[taskid]["accessibility"] = start + "/" + soft_end + "/" + end; + + dispenser_config[taskid]["accessibility"]["start"] = start == "" ? "{{ at.min }}" : start; + dispenser_config[taskid]["accessibility"]["soft_end"] = soft_end == "" ? "{{ at.max }}" : soft_end; + dispenser_config[taskid]["accessibility"]["end"] = end == "" ? "{{ at.max }}" : end; } }); }); @@ -217,29 +244,23 @@ $(".grouped-actions-task:checked").each(function () { $("#task_" + $(this).data("taskid") + " .accessibility-custom-start").text(val); var start = $("#grouped-actions-edit #accessibility_start").val(); - var soft_end = $("#grouped-actions-edit #accessibility_soft_end").val(); - var end = $("#grouped-actions-edit #accessibility_end").val(); - dispenser_config[$(this).data("taskid")]["accessibility"] = start + "/" + soft_end + "/" + end; + dispenser_config[$(this).data("taskid")]["accessibility"]["start"] = start == "" ? "{{ at.min }}" : start; }); }); $('#accessibility_end_picker_grouped').on("change.datetimepicker", function () { let val = $("#grouped-actions-edit #accessibility_end").val(); $(".grouped-actions-task:checked").each(function () { $("#task_" + $(this).data("taskid") + " .accessibility-custom-end").text(val); - var start = $("#grouped-actions-edit #accessibility_start").val(); - var soft_end = $("#grouped-actions-edit #accessibility_soft_end").val(); var end = $("#grouped-actions-edit #accessibility_end").val(); - dispenser_config[$(this).data("taskid")]["accessibility"] = start + "/" + soft_end + "/" + end; + dispenser_config[$(this).data("taskid")]["accessibility"]["end"] = end == "" ? "{{ at.max }}" : end; }); }); $('#accessibility_soft_end_picker_grouped').on("change.datetimepicker", function () { let val = $("#grouped-actions-edit #accessibility_soft_end").val(); $(".grouped-actions-task:checked").each(function () { $("#task_" + $(this).data("taskid") + " .accessibility-custom-soft-end").text(val); - var start = $("#grouped-actions-edit #accessibility_start").val(); var soft_end = $("#grouped-actions-edit #accessibility_soft_end").val(); - var end = $("#grouped-actions-edit #accessibility_end").val(); - dispenser_config[$(this).data("taskid")]["accessibility"] = start + "/" + soft_end + "/" + end; + dispenser_config[$(this).data("taskid")]["accessibility"]["soft_end"] = soft_end == "" ? "{{ at.max }}" : soft_end; }); }); diff --git a/inginious/frontend/templates/task_dispensers_admin/toc.html b/inginious/frontend/templates/task_dispensers_admin/toc.html index 43226a777..9364e48fc 100644 --- a/inginious/frontend/templates/task_dispensers_admin/toc.html +++ b/inginious/frontend/templates/task_dispensers_admin/toc.html @@ -1,6 +1,10 @@ {# This file is part of INGInious. See the LICENSE and the COPYRIGHTS files for #} {# more information about the licensing of this file. #} + + {% include "task_dispensers_admin/util.html" %}
@@ -24,6 +28,3 @@ {% include "task_dispensers_admin/empty_section.html" %} {% endwith %} -