Skip to content

Commit

Permalink
[frontend] Moving the concept of weight into the Task Dispenser (#849)
Browse files Browse the repository at this point in the history
Warning: this doesn't ensure retrocompatibilty with previous weighing system.
  • Loading branch information
maleclercq committed Jul 26, 2022
1 parent c1f47ac commit fc5bde4
Show file tree
Hide file tree
Showing 18 changed files with 154 additions and 68 deletions.
2 changes: 0 additions & 2 deletions inginious/frontend/pages/api/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ def API_GET(self, courseid, taskid): # pylint: disable=arguments-differ
"deadline": "",
"status": "success" # can be "succeeded", "failed" or "notattempted"
"grade": 0.0,
"grade_weight": 0.0,
"context": "" # context of the task, in RST
"problems": # dict of the subproblems
{
Expand Down Expand Up @@ -89,7 +88,6 @@ def API_GET(self, courseid, taskid): # pylint: disable=arguments-differ
"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,
"grade_weight": task.get_grading_weight(),
"context": task.get_context(self.user_manager.session_language()).original_content(),
"problems": []
}
Expand Down
10 changes: 1 addition & 9 deletions inginious/frontend/pages/course.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,20 +73,12 @@ def show_page(self, course):
# Compute course/tasks scores
tasks_data = {taskid: {"succeeded": False, "grade": 0.0} for taskid in user_task_list}
user_tasks = self.database.user_tasks.find({"username": username, "courseid": course.get_id(), "taskid": {"$in": user_task_list}})
is_admin = self.user_manager.has_staff_rights_on_course(course, username)
tasks_score = [0.0, 0.0]

for taskid in user_task_list:
tasks_score[1] += tasks[taskid].get_grading_weight()

for user_task in user_tasks:
tasks_data[user_task["taskid"]]["succeeded"] = user_task["succeeded"]
tasks_data[user_task["taskid"]]["grade"] = user_task["grade"]

weighted_score = user_task["grade"]*tasks[user_task["taskid"]].get_grading_weight()
tasks_score[0] += weighted_score

course_grade = round(tasks_score[0]/tasks_score[1]) if tasks_score[1] > 0 else 0
course_grade = course.get_task_dispenser().get_course_grade(username)

# Get tag list
tag_list = course.get_tags()
Expand Down
8 changes: 0 additions & 8 deletions inginious/frontend/pages/course_admin/task_edit.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,14 +159,6 @@ def POST_AUTH(self, courseid, taskid): # pylint: disable=arguments-differ
# 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")})
if data["weight"] < 0:
return json.dumps({"status": "error", "message": _("Grade weight must be positive!")})

# Groups
if "groups" in data:
data["groups"] = True if data["groups"] == "true" else False
Expand Down
1 change: 0 additions & 1 deletion inginious/frontend/pages/course_admin/task_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ def POST_AUTH(self, courseid): # pylint: disable=arguments-differ

# don't forget to reload the modified course
course, __ = self.get_course_and_check_rights(courseid, allow_all_staff=False)

return self.page(course, errors, not errors)

def submission_url_generator(self, taskid):
Expand Down
21 changes: 1 addition & 20 deletions inginious/frontend/static/js/all-minified.js

Large diffs are not rendered by default.

23 changes: 22 additions & 1 deletion inginious/frontend/static/js/task_dispensers.js
Original file line number Diff line number Diff line change
Expand Up @@ -363,10 +363,17 @@ function dispenser_util_get_sections_list(element) {

const content = $(this).children(".content");
if ($(this).hasClass("tasks_list")) {
structure["tasks_list"] = dispenser_util_get_tasks_list(content);
tasks_id = dispenser_util_get_tasks_list(content);
structure["tasks_list"] = tasks_id;

var weights = dispenser_util_get_weights(tasks_id);
if(Object.keys(weights).length > 0){
structure["weights"] = weights;
}
} else if ($(this).hasClass("sections_list")) {
structure["sections_list"] = dispenser_util_get_sections_list(content);
}

return structure;
}).get();
}
Expand All @@ -382,6 +389,20 @@ function dispenser_util_get_section_config(element) {
return config_list;
}

function dispenser_util_get_weights(tasks_id) {
const weight_list = {};
$(".weight").each(function(){
if(this.id in tasks_id){
if(this.value === ""){
weight_list[this.id] = 1;
}else{
weight_list[this.id] = parseFloat(this.value);
}
}
});
return weight_list;
}

function dispenser_util_get_tasks_list(element) {
const tasks_list = {};
element.children(".task").each(function (index) {
Expand Down
6 changes: 6 additions & 0 deletions inginious/frontend/task_dispensers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from abc import ABCMeta, abstractmethod

class TaskDispenser(metaclass=ABCMeta):

def __init__(self, task_list_func, dispenser_data, database, course_id):
"""
Instantiate a new TaskDispenser
Expand All @@ -11,6 +12,11 @@ def __init__(self, task_list_func, dispenser_data, database, course_id):
"""
pass

@abstractmethod
def get_course_grade(self, username):
"""Returns the current grade of the course for a specific user"""
pass

@classmethod
@abstractmethod
def get_id(cls):
Expand Down
20 changes: 20 additions & 0 deletions inginious/frontend/task_dispensers/combinatory_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ class CombinatoryTest(TaskDispenser):
def __init__(self, task_list_func, dispenser_data, database, course_id):
self._task_list_func = task_list_func
self._data = SectionsList(dispenser_data)
self._database = database
self._course_id = course_id

@classmethod
def get_id(cls):
Expand All @@ -20,6 +22,24 @@ def get_id(cls):
def get_name(cls, language):
return _("Combinatory test")

def get_weight(self, taskid):
""" Returns the weight of taskid """
try:
struct = self._data.to_structure()
for elem in struct:
weight = self._data.get_value_rec(taskid,elem,"weights")
if weight is not None:
return weight
return 1
except:
return 1

def get_course_grade(self, username):
""" Returns the grade of a user for the current course"""
task_list = self.get_user_task_list([username])[username]
user_tasks = self._database.user_tasks.find({"username": username, "courseid": self._course_id, "taskid": {"$in": task_list}})
return self._data.get_course_grade_weighted_sum(user_tasks, task_list, self.get_weight)

def get_dispenser_data(self):
return ""

Expand Down
20 changes: 20 additions & 0 deletions inginious/frontend/task_dispensers/toc.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ class TableOfContents(TaskDispenser):
def __init__(self, task_list_func, dispenser_data, database, course_id):
self._task_list_func = task_list_func
self._toc = SectionsList(dispenser_data)
self._database = database
self._course_id = course_id

@classmethod
def get_id(cls):
Expand All @@ -26,6 +28,24 @@ def get_name(cls, language):
""" Returns the localized task dispenser name """
return _("Table of contents")

def get_weight(self, taskid):
""" Returns the weight of taskid """
try:
struct = self._toc.to_structure()
for elem in struct:
weight = self._toc.get_value_rec(taskid,elem,"weights")
if weight is not None:
return weight
return 1
except:
return 1

def get_course_grade(self, username):
""" Returns the grade of a user for the current course"""
task_list = self.get_user_task_list([username])[username]
user_tasks = self._database.user_tasks.find({"username": username, "courseid": self._course_id, "taskid": {"$in": task_list}})
return self._toc.get_course_grade_weighted_sum(user_tasks, task_list, self.get_weight)

def get_dispenser_data(self):
""" Returns the task dispenser data structure """
return self._toc
Expand Down
53 changes: 52 additions & 1 deletion inginious/frontend/task_dispensers/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,47 @@ def remove_task(self, taskid):
for section in self._sections:
section.remove_task(taskid)

def get_value_rec(self,taskid,structure,key):
"""
Returns the value of key for the taskid in the structure if any or None
The structure can have mutliples sections_list that countains either sections_list or one tasks_list
The key should be inside one of the tasks_list
"""
if "sections_list" in structure:
for section in structure["sections_list"]:
weight = self.get_value_rec(taskid,section, key)
if weight is not None:
return weight
elif "tasks_list" in structure:
if taskid in structure["tasks_list"]:
return structure[key].get(taskid, None)
return None

def get_course_grade_weighted_sum(self, user_tasks, task_list, get_weight):
"""
Returns the course grade following a weighted sum
:param user_tasks: the user tasks as in the database
:param task_list: the list of tasks for a user
:param get_weight: a function that take a taskid as input and returns the weight for that taskid
:returns: the value of the grade
"""
tasks_data = {taskid: {"succeeded": False, "grade": 0.0} for taskid in task_list}
tasks_score = [0.0, 0.0]

for taskid in task_list:
tasks_score[1] += get_weight(taskid)

for user_task in user_tasks:
tasks_data[user_task["taskid"]]["succeeded"] = user_task["succeeded"]
tasks_data[user_task["taskid"]]["grade"] = user_task["grade"]

weighted_score = user_task["grade"]*get_weight(user_task["taskid"])
tasks_score[0] += weighted_score

course_grade = round(tasks_score[0]/tasks_score[1]) if tasks_score[1] > 0 else 0
return course_grade

def to_structure(self):
"""
:return: The structure in YAML format
Expand Down Expand Up @@ -160,6 +201,16 @@ def __init__(self, structure):
if not all(id_checker(id) for id in structure["tasks_list"]):
raise InvalidTocException(_("One task id contains non alphanumerical characters"))
self._task_list = [task for task, _ in sorted(structure["tasks_list"].items(), key=lambda x: x[1])]
self._weights = {}
if "weights" in structure:
for taskid,weight in structure["weights"].items():
if not (type(weight) == float or type(weight) == int):
raise InvalidTocException( ("The weight value must be a numeric >= 0 for the task: " + str(taskid)) )
elif weight >= 0:
if taskid in structure['tasks_list']:
self._weights[taskid] = weight
else:
raise InvalidTocException( ("The weight value must be a numeric >= 0 for the task: " + str(taskid)) )

def is_terminal(self):
return True
Expand Down Expand Up @@ -203,7 +254,7 @@ def to_structure(self, rank):
:return: The structure in YAML format
"""
return {"id": self._id, "rank": rank, "title": self._title,
"tasks_list": {taskid: rank for rank, taskid in enumerate(self._task_list)}}
"tasks_list": {taskid: rank for rank, taskid in enumerate(self._task_list)}, "weights": self._weights}


def check_toc(toc):
Expand Down
7 changes: 0 additions & 7 deletions inginious/frontend/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,6 @@ def __init__(self, course, taskid, content, filesystem, plugin_manager, task_pro
# 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))

Expand Down Expand Up @@ -201,10 +198,6 @@ def _create_task_problem(self, problemid, problem_content, task_problem_types):

return task_problem_types.get(problem_content.get('type', ""))(problemid, problem_content, self._translations, self._task_fs)

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._plugin_manager.call_hook('task_accessibility', course=self.get_course(), task=self, default=self._accessible)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,6 @@
value="{{ task_data.get('categories', []) | join(',') }}">
</div>
</div>
<div class="form-group row">
<label for="author" class="col-sm-2 control-label">{{_("Grade weight (in comparison to other tasks)")}}</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="weight" name="weight" placeholder="1.00" value="{{task_data.get('weight',1.00)}}">
</div>
</div>
<div class="form-group row">
<label for="groups" class="col-sm-2 control-label">{{_("Submission mode")}}</label>
<div class="col-sm-10">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
{# more information about the licensing of this file. #}

<div class="ml-auto btn-group btn-group-sm" role="group">
<button class="btn btn-primary" data-toggle="modal" data-target="#weightModal_{{taskid}}">
<i class="fa fa-cogs" aria-hidden="true"></i>
</button>
<a class="btn btn-success" href="{{get_homepath()}}/course/{{course.get_id()}}/{{taskid}}"
title="{{ _('View task') }}" data-toggle="tooltip" data-placement="bottom">
<i class="fa fa-eye"></i>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@
{% include "course_admin/task_dispensers/util_delete_modal.html" %}
{% endwith %}

{% for taskid in tasks %}
{% include "course_admin/task_dispensers/util_weight_modal.html" %}
{% endfor %}

<div class="modal fade" id="addTaskModal" role="dialog">
<div class="modal-dialog modal-lg">
<div class="modal-content">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{# This file is part of INGInious. See the LICENSE and the COPYRIGHTS files for #}
{# more information about the licensing of this file. #}

<div class="modal fade" id="weightModal_{{taskid}}" tabindex="-1" role="dialog" aria-labelledby="LabelModel">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel_{{taskid}}">{{_("Grade weight (in comparison to other tasks)")}}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
{% if course.get_task_dispenser().get_weight(taskid) >= 0 %}
<input type="text" value='{{course.get_task_dispenser().get_weight(taskid)}}' placeholder="1" id="{{taskid}}" class="weight"/>
{% else %}
<input type="text" placeholder="1" id="{{taskid}}" class="weight"/>
{% endif %}
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
6 changes: 0 additions & 6 deletions inginious/frontend/templates/task.html
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,6 @@ <h3>{{ _("Information") }}</h3>
<span id="task_grade">{{ user_task.get('grade', 0) }}</span>%
</td>
</tr>
{% if not is_lti() %}
<tr>
<td>{{ _("Grading weight") }}</td>
<td>{{ task.get_grading_weight() }}</td>
</tr>
{% endif %}
<tr>
<td>{{ _("Attempts") }}</td>
<td id="task_tries">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
</div>
<div class="col-xs-12 col-md-5">
{% if registered %}
<div type="button" class="fa fa-info" style="float: left; margin-top: 2px; margin-right: 5px;" data-toggle="tooltip" data-placement="bottom" title='{{_("Weight : ")}}{{course.get_task_dispenser().get_weight(taskid)}}'></div>
<div class="progress">
<div class="progress-bar bg-success" aria-valuenow="{{ completion | int }}" aria-valuemin="0" aria-valuemax="100" style="width: {{ completion | int }}%">
{% if completion.is_integer() %}{{ completion | int }}{% else %}{{ completion }}{% endif %} %
Expand Down
8 changes: 1 addition & 7 deletions inginious/frontend/user_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -617,13 +617,7 @@ def get_course_caches(self, usernames, course):
result["task_grades"] = {dg["taskid"]: dg["grade"] for dg in result["task_grades"] if
dg["taskid"] in visible_tasks}

total_weight = 0
grade = 0
for task_id in visible_tasks:
total_weight += tasks[task_id].get_grading_weight()
grade += result["task_grades"].get(task_id, 0.0) * tasks[task_id].get_grading_weight()

result["grade"] = round(grade / total_weight) if total_weight > 0 else 0
result["grade"] = course.get_task_dispenser().get_course_grade(username)
retval[username] = result

return retval
Expand Down

0 comments on commit fc5bde4

Please sign in to comment.