Skip to content

Commit

Permalink
Merge pull request #908 from UCL-INGI/task_disp_refactor
Browse files Browse the repository at this point in the history
Refactor task dispensers to handle course-specific parameters
  • Loading branch information
Drumor committed Mar 23, 2023
2 parents 31a8b8e + 3de072e commit 2b054c4
Show file tree
Hide file tree
Showing 42 changed files with 831 additions and 839 deletions.
8 changes: 0 additions & 8 deletions doc/dev_doc/extensions_doc/plugins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -186,14 +186,6 @@ Each hook available in INGInious is described here, starting with its name and p
``default`` : Default value as specified in the configuration

Overrides the course accessibility.
``task_accessibility`` (``course``, ``taskid``, ``default``)
Returns: inginious.frontend.accessible_time.AccessibleTime

``course`` : inginious.frontend.courses.Course

``task`` : inginious.frontend.tasks.Task

``default`` : Default value as specified in the configuration

Overrides the task accessibility
``task_limits`` (``course``, ``taskid``, ``default``)
Expand Down
8 changes: 4 additions & 4 deletions inginious/frontend/courses.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,17 @@
from inginious.frontend.user_manager import UserInfo
from inginious.frontend.task_dispensers.toc import TableOfContents


def _migrate_from_v_0_6(content, task_list):
if 'task_dispenser' not in content:
content["task_dispenser"] = "toc"
if 'toc' in content:
content['dispenser_data'] = content["toc"]
content['dispenser_data'] = {"toc": content["toc"]}
else:
ordered_tasks = OrderedDict(sorted(list(task_list.items()),
key=lambda t: (int(t[1]._data.get('order', -1)), t[1].get_id())))
indexed_task_list = {taskid: rank for rank, taskid in enumerate(ordered_tasks.keys())}
content['dispenser_data'] = [{"id": "tasks-list", "title": _("List of exercises"),
"rank": 0, "tasks_list": indexed_task_list}]
content['dispenser_data'] = {"toc": [{"id": "tasks-list", "title": _("List of exercises"),
"rank": 0, "tasks_list": list(ordered_tasks.keys())}], "config": {}}


class Course(object):
Expand Down
1 change: 0 additions & 1 deletion inginious/frontend/pages/api/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@ def API_GET(self, courseid, taskid): # pylint: disable=arguments-differ
"name": task.get_name(self.user_manager.session_language()),
"authors": task.get_authors(self.user_manager.session_language()),
"contact_url": task.get_contact_url(self.user_manager.session_language()),
"deadline": task.get_deadline(),
"status": "notviewed" if task_cache is None else "notattempted" if task_cache["tried"] == 0 else "succeeded" if task_cache["succeeded"] else "failed",
"grade": task_cache.get("grade", 0.0) if task_cache is not None else 0.0,
"context": task.get_context(self.user_manager.session_language()).original_content(),
Expand Down
2 changes: 1 addition & 1 deletion inginious/frontend/pages/course.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ def show_page(self, course):
course_grade = course.get_task_dispenser().get_course_grade(username)

# Get tag list
categories = set(course.get_task_dispenser().get_all_categories())
categories = course.get_task_dispenser().get_all_categories()

# Get user info
user_info = self.user_manager.get_user_info(username)
Expand Down
73 changes: 2 additions & 71 deletions inginious/frontend/pages/course_admin/task_edit.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
from werkzeug.exceptions import NotFound

from inginious.frontend.tasks import _migrate_from_v_0_6
from inginious.frontend.accessible_time import AccessibleTime
from inginious.frontend.pages.course_admin.utils import INGIniousAdminPage

from inginious.common.base import dict_from_prefix, id_checker
Expand Down Expand Up @@ -63,7 +62,7 @@ def GET_AUTH(self, courseid, taskid): # pylint: disable=arguments-differ
problemdata=json.dumps(task_data.get('problems', {})),
contains_is_html=self.contains_is_html(task_data),
current_filetype=current_filetype,
available_filetypes=available_filetypes, AccessibleTime=AccessibleTime,
available_filetypes=available_filetypes,
file_list=CourseTaskFiles.get_task_filelist(self.task_factory, courseid, taskid),
additional_tabs=additional_tabs)

Expand All @@ -82,38 +81,15 @@ def parse_problem(self, problem_content):
del problem_content["@order"]
return self.task_factory.get_problem_types().get(problem_content["type"]).parse_problem(problem_content)

def wipe_task(self, courseid, taskid):
""" Wipe the data associated to the taskid from DB"""
submissions = self.database.submissions.find({"courseid": courseid, "taskid": taskid})
for submission in submissions:
for key in ["input", "archive"]:
if key in submission and type(submission[key]) == bson.objectid.ObjectId:
self.submission_manager.get_gridfs().delete(submission[key])

self.database.user_tasks.delete_many({"courseid": courseid, "taskid": taskid})
self.database.submissions.delete_many({"courseid": courseid, "taskid": taskid})

self._logger.info("Task %s/%s wiped.", courseid, taskid)

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)
__, __ = 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:
Expand Down Expand Up @@ -152,51 +128,6 @@ def POST_AUTH(self, courseid, taskid): # pylint: disable=arguments-differ
# Task environment parameters
data["environment_parameters"] = environment_parameters

# Groups
if "groups" in data:
data["groups"] = True if data["groups"] == "true" else False

# 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"])}
if result['period'] < 0:
return json.dumps({"status": "error", "message": _("The soft limit period must be positive!")})
except:
return json.dumps({"status": "error", "message": _("Invalid submission limit!")})

if data['submission_limit'] != 'none' and result['amount'] < 0:
return json.dumps({"status": "error", "message": _("The submission limit must be positive!")})

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)})

# Random inputs
try:
data['input_random'] = int(data['input_random'])
Expand Down
20 changes: 10 additions & 10 deletions inginious/frontend/pages/course_admin/task_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import logging
import flask
from collections import OrderedDict
from natsort import natsorted

from inginious.frontend.pages.course_admin.utils import INGIniousAdminPage

Expand All @@ -30,7 +31,7 @@ def POST_AUTH(self, courseid): # pylint: disable=arguments-differ
task_dispenser_class = self.course_factory.get_task_dispensers().get(selected_task_dispenser, None)
if task_dispenser_class:
self.course_factory.update_course_descriptor_element(courseid, 'task_dispenser', task_dispenser_class.get_id())
self.course_factory.update_course_descriptor_element(courseid, 'dispenser_data', "")
self.course_factory.update_course_descriptor_element(courseid, 'dispenser_data', {})
else:
errors.append(_("Invalid task dispenser"))
else:
Expand Down Expand Up @@ -89,20 +90,19 @@ def page(self, course, errors=None, validated=False):
# Load tasks and verify exceptions
files = self.task_factory.get_readable_tasks(course)

output = {}
tasks = {}
if errors is None:
errors = []
for task in files:
for taskid in files:
try:
output[task] = course.get_task(task)
tasks[taskid] = course.get_task(taskid)
except Exception as inst:
errors.append({"taskid": task, "error": str(inst)})
tasks = OrderedDict(sorted(list(output.items()), key=lambda t: (course.get_task_dispenser().get_task_order(t[1].get_id()), t[1].get_id())))
errors.append({"taskid": taskid, "error": str(inst)})

tasks_data = OrderedDict()
for taskid in tasks:
tasks_data[taskid] = {"name": tasks[taskid].get_name(self.user_manager.session_language()),
"url": self.submission_url_generator(taskid)}
tasks_data = natsorted([(taskid, {"name": tasks[taskid].get_name(self.user_manager.session_language()),
"url": self.submission_url_generator(taskid)}) for taskid in tasks],
key=lambda x: x[1]["name"])
tasks_data = OrderedDict(tasks_data)

task_dispensers = self.course_factory.get_task_dispensers()

Expand Down
2 changes: 1 addition & 1 deletion inginious/frontend/pages/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ def GET(self, courseid, taskid, is_LTI):
eval_submission = self.database.submissions.find_one({'_id': ObjectId(submissionid)}) if submissionid else None

students = [self.user_manager.session_username()]
if task.is_group_task() and not self.user_manager.has_admin_rights_on_course(course, username):
if course.get_task_dispenser().get_group_submission(taskid) and not self.user_manager.has_admin_rights_on_course(course, username):
group = self.database.groups.find_one({"courseid": task.get_course_id(),
"students": self.user_manager.session_username()})
if group is not None:
Expand Down
84 changes: 61 additions & 23 deletions inginious/frontend/plugins/contests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,54 @@
from inginious.frontend.accessible_time import AccessibleTime
from inginious.frontend.pages.course_admin.utils import INGIniousAdminPage
from inginious.frontend.pages.utils import INGIniousAuthPage
from inginious.frontend.task_dispensers.toc import TableOfContents


def add_admin_menu(course): # pylint: disable=unused-argument
""" Add a menu for the contest settings in the administration """
return ('contest', '<i class="fa fa-trophy fa-fw"></i>&nbsp; Contest')
task_dispenser = course.get_task_dispenser()
if task_dispenser.get_id() == Contest.get_id():
return ('contest', '<i class="fa fa-trophy fa-fw"></i>&nbsp; Contest')
else:
return None


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 Contest(TableOfContents):

def __init__(self, task_list_func, dispenser_data, database, course_id):
TableOfContents.__init__(self, task_list_func, dispenser_data.get("toc_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"),
"blackout": 0,
"penalty": 20}
)

@classmethod
def get_id(cls):
return "contest"

@classmethod
def get_name(cls, language):
return "Contest"

def check_dispenser_data(self, dispenser_data):
""" Checks the dispenser data as formatted by the form from render_edit function """
data, errors = TableOfContents.check_dispenser_data(self, dispenser_data)
return {"toc_data": data, "contest_settings": self._contest_settings} if data else None, errors

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}
else:
return TableOfContents.get_accessibilities(self, taskids, usernames)

def get_contest_data(self):
""" Returns the settings of the contest for this course """
return self._contest_settings


def additional_headers():
Expand All @@ -39,19 +74,13 @@ def additional_headers():
'<script src="' + flask.request.url_root + '/static/plugins/contests/contests.js"></script>'


def get_contest_data(course):
""" Returns the settings of the contest for this course """
return course.get_descriptor().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"),
"blackout": 0,
"penalty": 20})


def course_menu(course, template_helper):
""" Displays some informations about the contest on the course page"""
contest_data = get_contest_data(course)
task_dispenser = course.get_task_dispenser()
if not task_dispenser.get_id() == Contest.get_id():
return None

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")
Expand All @@ -67,7 +96,10 @@ class ContestScoreboard(INGIniousAuthPage):

def GET_AUTH(self, courseid): # pylint: disable=arguments-differ
course = self.course_factory.get_course(courseid)
contest_data = get_contest_data(course)
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")
Expand Down Expand Up @@ -160,20 +192,26 @@ class ContestAdmin(INGIniousAdminPage):
def save_contest_data(self, course, contest_data):
""" Saves updated contest data for the course """
course_content = self.course_factory.get_course_descriptor_content(course.get_id())
course_content["contest_settings"] = contest_data
course_content["dispenser_data"]["contest_settings"] = contest_data
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 """
course, __ = self.get_course_and_check_rights(courseid, allow_all_staff=False)
contest_data = get_contest_data(course)
task_dispenser = course.get_task_dispenser()
if not task_dispenser.get_id() == Contest.get_id():
raise NotFound()
contest_data = task_dispenser.get_contest_data()
return self.template_helper.render("admin.html", template_folder="frontend/plugins/contests", course=course,
data=contest_data, errors=None, saved=False)

def POST_AUTH(self, courseid): # pylint: disable=arguments-differ
""" POST request: update the settings """
course, __ = self.get_course_and_check_rights(courseid, allow_all_staff=False)
contest_data = get_contest_data(course)
task_dispenser = course.get_task_dispenser()
if not task_dispenser.get_id() == Contest.get_id():
raise NotFound()
contest_data = task_dispenser.get_contest_data()

new_data = flask.request.form
errors = []
Expand Down Expand Up @@ -235,6 +273,6 @@ def init(plugin_manager, course_factory, client, config): # pylint: disable=unu
plugin_manager.add_page('/contest/<courseid>', ContestScoreboard.as_view('contestscoreboard'))
plugin_manager.add_page('/admin/<courseid>/contest', ContestAdmin.as_view('contestadmin'))
plugin_manager.add_hook('course_admin_menu', add_admin_menu)
plugin_manager.add_hook('task_accessibility', task_accessibility)
plugin_manager.add_hook('header_html', additional_headers)
plugin_manager.add_hook('course_menu', course_menu)
course_factory.add_task_dispenser(Contest)

0 comments on commit 2b054c4

Please sign in to comment.