Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[frontend] Changing accessibility and registration data structure #985

Open
wants to merge 49 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
2a24526
[frontend/tasksets] Changing course accessibility init structure
AlexandreDoneux Nov 14, 2023
78449e8
[frontend/accessible_time] Changing AccessibleTime to new course/task…
AlexandreDoneux Nov 14, 2023
d8e22f8
[frontend/course_admin] Adapting course settings form to new course a…
AlexandreDoneux Nov 14, 2023
ae87e9d
[frontend/accessibility] Changing task accessibility handling for tas…
AlexandreDoneux Nov 14, 2023
5f95381
[frontend/task_list] processing accessibility_period before updating …
AlexandreDoneux Nov 14, 2023
3437e2e
[frontend] Changing task accessibility data structure
AlexandreDoneux Nov 14, 2023
98e8396
[frontend] changing course acces and registration structure in yaml a…
AlexandreDoneux Nov 14, 2023
18a8daf
[frontend/toc] fixing dates transformation before rendering toc.html
AlexandreDoneux Nov 16, 2023
7ae3a78
[frontend] Storing dates in yaml files as datetime objects
AlexandreDoneux Nov 20, 2023
0340138
[frontend/accessible_time] Changing AccessibleTime init structure
AlexandreDoneux Nov 22, 2023
96231dd
[frontend] Storing datetime to string and the opposite in util files
AlexandreDoneux Nov 23, 2023
fa0750a
[frontend] Adapting combinatory test to new AccessibleTime structure
AlexandreDoneux Nov 23, 2023
f9aee73
[frontend] Changing datetime to str and opposite
AlexandreDoneux Nov 28, 2023
9c0473d
[frontend/task_dispenser_admin] Allowing string dates in AccessibleTi…
AlexandreDoneux Nov 30, 2023
0df58a4
[frontend/task_dispenser] Adapting Accessibility.get_value() to legac…
AlexandreDoneux Nov 30, 2023
1c7ba27
Â[frontend] Remove boolean from access data structure
AlexandreDoneux Dec 6, 2023
2340aea
[common/custom_yaml] Changing timestamp representer to correctly tran…
AlexandreDoneux Dec 6, 2023
fd25ba3
[frontend/accessibility.html] Fix error occuring when changing date o…
AlexandreDoneux Dec 6, 2023
ed56453
[frontend] Fixing max date milliseconds not written in yaml and DB
AlexandreDoneux Dec 8, 2023
19c0500
[frontend/accessibility] Fixing grouped actions task edit
AlexandreDoneux Dec 8, 2023
a789634
[frontend] Changing accessible and registration structure when import…
AlexandreDoneux Dec 12, 2023
6ea6b02
[frontend] Changing task access structure during imports in task_disp…
AlexandreDoneux Dec 13, 2023
0fe5ec1
[plugins/contest] fixing method name change from TasksetFactory
AlexandreDoneux Dec 13, 2023
1d50870
[plugins/contests] Fixing contest edits saved in taskset.yaml instead…
AlexandreDoneux Dec 13, 2023
7a7d03f
[plugins/contests] Adapting contest plugin to new access structure
AlexandreDoneux Dec 13, 2023
7a6b750
[frontend/accessible_time] Refactoring parameter check for Accessible…
AlexandreDoneux Dec 13, 2023
39ce930
cleaning for PR
AlexandreDoneux Dec 14, 2023
0998b0b
[frontend/accessibility] Fixing modal feed to all modal instead of sp…
AlexandreDoneux Jan 4, 2024
96b2446
[frontend/accessibility] Fixing datetime picker cleaning during modal…
AlexandreDoneux Jan 5, 2024
d25e5a0
[frontend/contest] Fixing constest plugin with new database date format
AlexandreDoneux Jan 25, 2024
ea4df4e
[frontend/constest] Fixing no soft_end given in contest .get_accessib…
AlexandreDoneux Jan 26, 2024
c6685c3
[frontend] Indicating a four digit year when transforming datetimes i…
AlexandreDoneux Apr 16, 2024
9b5416d
[custom_yaml] refactoring formatting for datetime yaml representer
AlexandreDoneux Apr 16, 2024
9395f96
[frontend] Remove min and max hardcoded dates + storing without micro…
AlexandreDoneux Apr 17, 2024
26867c7
[frontend/accessibleTime] Remove method adapting database time
AlexandreDoneux Apr 17, 2024
085fef4
[frontaned/accessible_time] Adding support for legacy time format
AlexandreDoneux Apr 18, 2024
f1a3f4c
[frontend/accessible_time] Fixing legacy structure support
AlexandreDoneux Apr 26, 2024
96b0d45
[frontend/accessible_time] Adapting course accessibility when no regi…
AlexandreDoneux Apr 29, 2024
d56d8fd
[frontend/settings] Changing course accessibility structure when upda…
AlexandreDoneux Apr 29, 2024
4f7f40a
[frontend/task_list] Moving task access structure update to legacy up…
AlexandreDoneux Apr 29, 2024
b615b92
Revert "[frontend/task_list] Moving task access structure update to l…
AlexandreDoneux May 6, 2024
26e0658
[frontend/task_dispenser] Updating task accessibility structure when …
AlexandreDoneux May 6, 2024
5776d6e
Fixing Codacy warnings
AlexandreDoneux May 7, 2024
ca8e595
[frontend/accessible_time] Fix legacy accessibility string structure …
AlexandreDoneux May 7, 2024
dafa720
Fix codacy warning
AlexandreDoneux May 7, 2024
1a88ba2
[frontend/accessible_time] Using self.min and max in legacy_string_st…
AlexandreDoneux May 16, 2024
42811b0
[frontend] Fixing forgotten code that needed to be removed
AlexandreDoneux May 16, 2024
fed730d
[frontend/util] Removing util.py file with unused method
AlexandreDoneux May 16, 2024
05daaf2
[frontend/accessible_time] Fixing soft_end set to end when soft_end n…
AlexandreDoneux May 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions inginious/common/custom_yaml.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down
172 changes: 106 additions & 66 deletions inginious/frontend/accessible_time.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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 """
Expand All @@ -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)
}
1 change: 1 addition & 0 deletions inginious/frontend/course_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
13 changes: 7 additions & 6 deletions inginious/frontend/courses.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 = []
Expand Down
4 changes: 2 additions & 2 deletions inginious/frontend/pages/course_admin/danger_zone.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
28 changes: 21 additions & 7 deletions inginious/frontend/pages/course_admin/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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'])
Expand All @@ -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']
Expand Down
Loading
Loading