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

Personalized points goal #1387

Merged
merged 15 commits into from
Oct 9, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
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
2 changes: 1 addition & 1 deletion assets/css/main.css

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions assets/sass/legacy/_main.scss
Original file line number Diff line number Diff line change
Expand Up @@ -234,8 +234,8 @@ file for all the components */
}
.progress .required-points {
position: absolute;
height: 20px;
border-left: 1px solid black;
height: 100%;
border-left: 2px solid black;
}
.glyphicon.red {
color: #ff7070;
Expand Down
48 changes: 48 additions & 0 deletions course/migrations/0060_studentmodulegoal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Generated by Django 4.2.11 on 2024-08-07 12:27

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
("userprofile", "0008_delete_studentmodulegoal"),
("course", "0059_submissiontag"),
]

operations = [
migrations.CreateModel(
name="StudentModuleGoal",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"personalized_points_goal_percentage",
models.FloatField(default=100.0),
),
("personalized_points_goal_points", models.IntegerField(default=0)),
(
"module",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="course.coursemodule",
),
),
(
"student",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="userprofile.userprofile",
),
),
],
),
]
6 changes: 6 additions & 0 deletions course/models.py
mikaelGusse marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -1482,6 +1482,12 @@ def number_of_submitters(self):
.filter(submissions__exercise__course_module=self).distinct().count()


class StudentModuleGoal(models.Model):
student = models.ForeignKey(UserProfile, on_delete=models.CASCADE)
module = models.ForeignKey(CourseModule, on_delete=models.CASCADE)
personalized_points_goal_percentage = models.FloatField(default=100.0)
mikaelGusse marked this conversation as resolved.
Show resolved Hide resolved
personalized_points_goal_points = models.IntegerField(default=0)

class LearningObjectCategory(models.Model):
"""
Learning objects may be grouped to different categories.
Expand Down
41 changes: 41 additions & 0 deletions e2e_tests/test_points_goal_set.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from playwright.sync_api import Page, expect

from e2e_tests.helpers import login


def test_points_goal_set(page: Page) -> None:
login(page, "student", "student")

page.get_by_role("link", name="Def. Course Current DEF000 1.").click()
page.get_by_role("link", name="Your points").click()
page.locator("#progress-questionnaires").get_by_role("button", name="Points goal").click()
page.get_by_label("Input personalized goal as").fill("50")
page.get_by_label("Input personalized goal as").press("Tab")
page.get_by_role("button", name="Save").click()
expect(page.locator("#success-alert")).to_contain_text("Succesfully set personalized points goal")
page.get_by_role("button", name="Close", exact=True).click()
expect(page.get_by_text("Points goal: 50"))
expect(page.locator("#goal-points"))


def test_points_goal_reached(page: Page) -> None:
login(page, "student", "student")
page.get_by_role("link", name="Def. Course Current DEF000 1.").click()
page.get_by_role("link", name="Creating questionnaire exercises").click()
page.locator("label").filter(has_text="2").first.click()
page.locator("label").filter(has_text="an integer").first.click()
page.locator("div:nth-child(4) > div:nth-child(6) > label").click()
page.locator("label").filter(has_text="-").nth(1).click()
page.locator("label").filter(has_text="an integer").nth(1).click()
page.locator("div:nth-child(5) > div:nth-child(6) > label").click()
page.locator("label").filter(has_text="-").nth(3).click()
page.locator("#chapter-exercise-1").get_by_role("button", name="Submit").click()
expect(page.get_by_label("5.1.1 Single-choice and")).to_contain_text("30 / 40")
page.get_by_role("link", name="Your points").click()
page.locator("#progress-questionnaires").get_by_role("button", name="Points goal").click()
page.get_by_label("Input personalized goal as").fill("30")
page.get_by_role("button", name="Save").click()
expect(page.locator("#success-alert")).to_contain_text("Succesfully set personalized points goal")
page.get_by_role("button", name="Close", exact=True).click()
progress_bar_locator = page.locator("#progress-questionnaires > .progress > .progress-bar")
expect(progress_bar_locator).to_have_class("progress-bar progress-bar-striped progress-bar-primary")
15 changes: 12 additions & 3 deletions exercise/cache/points.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from django.db.models.signals import post_save, post_delete, pre_delete, m2m_changed
from django.utils import timezone

from course.models import CourseInstance, CourseModule, StudentGroup
from course.models import CourseInstance, CourseModule, StudentGroup, StudentModuleGoal
from deviations.models import DeadlineRuleDeviation, MaxSubmissionsRuleDeviation
from lib.cache.cached import DBDataManager, Dependencies, ProxyManager, resolve_proxies
from lib.helpers import format_points
Expand Down Expand Up @@ -865,6 +865,8 @@ class ModulePoints(DifficultyStats, ModuleEntryBase[LearningObjectPoints]):
_children_unconfirmed: bool
is_model_answer_revealed: bool
confirmable_children: bool
personalized_points_module_goal: Optional[int]
personalized_points_module_goal_points: Optional[int]

children_unconfirmed = RevealableAttribute[bool]()

Expand Down Expand Up @@ -919,7 +921,8 @@ def _generate_data(
self._points_by_difficulty = {}
self._true_unconfirmed_points_by_difficulty = {}
self._unconfirmed_points_by_difficulty = {}

self.personalized_points_module_goal = None
self.personalized_points_module_goal_points = None
self.instance = precreated.get_or_create_proxy(
CachedPointsData, *self.instance._params, user_id, modifiers=self._modifiers
)
Expand All @@ -940,6 +943,13 @@ def _generate_data(
elif entry.submission_count > 0:
self.confirmable_children = True

try:
student_module_goal = StudentModuleGoal.objects.get(module_id=module_id, student_id=user_id)
self.personalized_points_module_goal = student_module_goal.personalized_points_goal_percentage
self.personalized_points_module_goal_points = student_module_goal.personalized_points_goal_points
except StudentModuleGoal.DoesNotExist:
pass

def add_points(children):
for entry in children:
if not entry.confirm_the_level and isinstance(entry, ExercisePoints) and entry.is_visible():
Expand All @@ -948,7 +958,6 @@ def add_points(children):
add_points(entry.children)

add_points(self.children)

self._true_passed = self._true_passed and self._true_points >= self.points_to_pass
self._passed = self._passed and self._points >= self.points_to_pass

Expand Down
12 changes: 12 additions & 0 deletions exercise/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,3 +179,15 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
class Meta:
model = Submission
fields = ['submitters']

class StudentModuleGoalForm(forms.Form):
personalized_points_goal_input = forms.CharField(
label=_('LABEL_POINTS_GOAL_INPUT'),
max_length=10,
widget=forms.TextInput(attrs={'style': 'width: 66px;', 'class': 'form-control'}),
)

class Meta():
fields = [
'personalized_points_goal_input',
]
6 changes: 6 additions & 0 deletions exercise/static/exercise/css/goal_points.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.progress .goal-points {
position: absolute;
height: 100%;
width: 3px;
background: repeating-linear-gradient(#000000, #000000 2px, transparent 2px, transparent 4px);
}
149 changes: 149 additions & 0 deletions exercise/static/exercise/personalized_points_goal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
$(document).ready(function() {
const $pointsGoalForm = $('#pointsGoalForm');
const $inputField = $('#id_personalized_points_goal_input');
// If points-goal is a number then input it into the field
if (typeof $pointsGoalForm.data('points-goal') === 'number') {
$inputField.val($pointsGoalForm.data('points-goal'))};
$inputField.focus();
$pointsGoalForm.on('submit', function(event) {
event.preventDefault();

// Validate input
const inputValue = $inputField.val().trim();

const isNumber = !isNaN(inputValue) && inputValue !== '';
const isPercentage = inputValue.endsWith('%') && !isNaN(inputValue.slice(0, -1));

if (!isNumber && !isPercentage) {
$('#validation-alert').show();
setTimeout(function() {
$('#validation-alert').hide();
}, 5000);
return;
}

$.ajax({
type: 'POST',
url: $pointsGoalForm.attr('action'),
data: $pointsGoalForm.serialize(),
success: function(response) {
// Update page dynamically
const $progressElement = $('#progress-' + $pointsGoalForm.data('module-url'));
const $progressDiv = $progressElement.find('.progress');

// Update goal indicator
let $goalPointsElement = $progressElement.find('.goal-points');
if ($goalPointsElement.length === 0) {
// Create the goal points bar if it does not exist
$goalPointsElement = $('<div>', {
id: 'goal-points',
class: 'goal-points',
css: {
left: response.personalized_points_goal_percentage + '%'
}
});
$progressDiv.append($goalPointsElement);
} else {
// Update the existing element
$goalPointsElement.css('left', response.personalized_points_goal_percentage + '%');
}

// Update tooltip
if ($progressDiv.length) {
const tooltipTitle = $progressDiv.attr('data-original-title');
const parser = new DOMParser();
const doc = parser.parseFromString(tooltipTitle, 'text/html');
Fixed Show fixed Hide fixed
Fixed Show fixed Hide fixed

let spanElement = doc.querySelector('span.personalized-points-text');
// If the span element does not exist, create it
if (spanElement == null) {
spanElement = doc.createElement('span');
spanElement.className = 'personalized-points-full-text text-nowrap';
doc.body.appendChild(spanElement);
spanElement.innerHTML = "<br>" + $pointsGoalForm.data('personalized-points-goal-tooltip-text') + ": " + "<span class=\"personalized-points-text text-nowrap\">" + response.personalized_points_goal_points + "</span>";
}
else {
spanElement.textContent = response.personalized_points_goal_points;
}

const updatedTooltipTitle = doc.body.innerHTML;
$progressDiv.attr('data-original-title', updatedTooltipTitle);

// Update progress-bar style
if (response.personalized_points_goal_points <= $pointsGoalForm.data('points')) {
$progressDiv.find('.progress-bar').removeClass('progress-bar-warning');
$progressDiv.find('.progress-bar').addClass('progress-bar-primary');
}
else {
$progressDiv.find('.progress-bar').removeClass('progress-bar-primary');
$progressDiv.find('.progress-bar').addClass('progress-bar-warning');
}
// Show the success alert
$('#success-alert').show();
setTimeout(function() {
$('#success-alert').hide();
}, 5000);
}
},
error: function(xhr, status, error) {
if (xhr.responseJSON.error === 'less_than_required') {
$('#danger-alert').show();
setTimeout(function() {
$('#danger-alert').hide();
}, 5000);
}
else {
$('#warning-alert').show();
setTimeout(function() {
$('#warning-alert').hide();
}, 5000);
}
}
});
});
$('#deletePointsGoalForm').on('submit', function(event) {
event.preventDefault();

$.ajax({
type: 'POST',
url: $(this).attr('action'),
data: $(this).serialize() + '&delete=true',
success: function(response) {
// Update page dynamically
const $progressElement = $('#progress-' + $pointsGoalForm.data('module-url'));
const $progressDiv = $progressElement.find('.progress');

// Remove goal indicator
let $goalPointsElement = $progressElement.find('.goal-points');
$goalPointsElement.removeClass('goal-points');

// Update tooltip
const tooltipTitle = $progressDiv.attr('data-original-title');
const parser = new DOMParser();
const doc = parser.parseFromString(tooltipTitle, 'text/html');
Fixed Show fixed Hide fixed

let spanElement = doc.querySelector('span.personalized-points-full-text');
spanElement.remove();

const updatedTooltipTitle = doc.body.innerHTML;
$progressDiv.attr('data-original-title', updatedTooltipTitle);

// Update progress-bar style
$progressDiv.find('.progress-bar').removeClass('progress-bar-primary');

$('#remove-success-alert').show();
setTimeout(function() {
$('#remove-success-alert').hide();
}, 5000);

},
error: function(xhr, status, error) {
// Handle error response
$('#remove-warning-alert').show();
setTimeout(function() {
$('#remove-warning-alert').hide();
}, 5000);
}
});
});
});
12 changes: 10 additions & 2 deletions exercise/templates/exercise/_points_progress.html
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
{% load i18n %}
{% load static %}
{% if not feedback_revealed %}
<p class="small">
<i class="glyphicon glyphicon-info-sign"></i>
{% translate "RESULTS_OF_SOME_ASSIGNMENTS_ARE_CURRENTLY_HIDDEN" %}
</p>
{% endif %}
<div class="progress" data-toggle="tooltip" data-html="true" data-placement="bottom"
title="{% translate 'POINTS' %}: &lt;span class='text-nowrap'&gt;{{ formatted_points }} / {{ max }}&lt;/span&gt;{% if required %} {% translate 'POINTS_TO_PASS' %}: &lt;span class='text-nowrap'&gt;{{ required }}&lt;/span&gt;{% endif %}">
<div class="progress-bar progress-bar-striped progress-bar-{% if full_score %}success{% elif passed %}warning{% else %}danger{% endif %}"
title="{% translate 'POINTS' %}: &lt;span class='text-nowrap'&gt;{{ formatted_points }} / {{ max }}&lt;/span&gt;
{% if required %} &lt;br&gt; {% translate 'POINTS_TO_PASS' %}: &lt;span class='text-nowrap'&gt;{{ required }}&lt;/span&gt; {% endif %}
{% if personalized_points_module_goal %} &lt;span class='personalized-points-full-text text-nowrap'&gt; &lt;br&gt;{% translate 'PERSONALIZED_POINTS_GOAL' %}: &lt;span class='personalized-points-text text-nowrap'&gt;{{ personalized_points_module_goal_points|floatformat:"0" }}&lt;/span&gt;&lt;/span&gt;{% endif %}"
>
mikaelGusse marked this conversation as resolved.
Show resolved Hide resolved
<div class="progress-bar progress-bar-striped progress-bar-{% if full_score %}success{% elif percentage >= personalized_points_module_goal %}primary{% elif passed %}warning{% else %}danger{% endif %}"
rel="progressbar" aria-valuenow="{{ points }}" aria-valuemin="0" aria-valuemax="{{ max }}"
style="width:{{ percentage }}%;"></div>
<link rel="stylesheet" href="{% static 'exercise/css/goal_points.css' %}" />
{% if required_percentage %}
<div class="required-points" style="left:{{ required_percentage }}%"></div>
{% endif %}
{% if personalized_points_module_goal %}
<div id="goal-points" class="goal-points" style="left:{{ personalized_points_module_goal|floatformat:0 }}%"></div>
{% endif %}
</div>
16 changes: 14 additions & 2 deletions exercise/templates/exercise/_user_results.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
{% load static %}
{% load course %}
{% load exercise %}

<script src="{% static 'exercise/user_results.js' %}"></script>

{% if categories|len_listed > 1 %}
Expand Down Expand Up @@ -103,8 +102,21 @@ <h3 class="panel-title">
{% endblocktranslate %}
{% endif %}
</p>

<div id="progress-{{module.url|lower}}" style="width: 100%;">
{% csrf_token %}
{% if user.id %}
{% get_max_module_points module.id user.id as max_points %}
{% get_points_goal module.id user.id as points_goal %}
{% if max_points > 0 and user.id %}
<a class="page-modal aplus-button--secondary aplus-button--xs" style="float: right; margin-left: 5px;" role="button" href="{% url 'save_points_goal_form_view' course_slug=course.url instance_slug=instance.url module_slug=module.url %}" data-module-id="{{ module.id }}" title="{% translate 'POINTS_GOAL_TOOLTIP' %}" data-toggle="tooltip">
<span class="glyphicon glyphicon-screenshot" aria-hidden="true"></span> {% translate 'POINTS_GOAL' %}
</a>
{% endif %}
{% endif %}
{% points_progress module %}
</div>

{% points_progress module %}
{{ module.introduction|safe }}
{% if student %}
{% adddeviationsbutton instance module submitters=student %}
Expand Down
Loading
Loading