Skip to content

Commit d5fec19

Browse files
Add submision conversion feature for individual submission and by module
conversion EDIT-767
1 parent 8245ca6 commit d5fec19

13 files changed

Lines changed: 355 additions & 4 deletions

File tree

deviations/forms.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,11 @@ class DeadlineRuleDeviationForm(BaseDeviationForm):
8989
initial=True,
9090
label=_('LABEL_WITHOUT_LATE_PENALTY'),
9191
)
92+
without_late_submission_approval = forms.BooleanField(
93+
required=False,
94+
initial=False,
95+
label=_('LABEL_WITHOUT_LATE_APPROVAL'),
96+
)
9297

9398
def __init__(self, *args: Any, **kwargs: Any) -> None:
9499
super().__init__(*args, **kwargs)
@@ -114,6 +119,11 @@ class MaxSubmissionRuleDeviationForm(BaseDeviationForm):
114119
label=_('LABEL_EXTRA_SUBMISSIONS'),
115120
)
116121

122+
without_unofficial_submission_approval = forms.BooleanField(
123+
required=False,
124+
initial=False,
125+
label=_('LABEL_WITHOUT_UNOFFICIAL_APPROVAL'),
126+
)
117127
def __init__(self, *args, **kwargs) -> None:
118128
super().__init__(*args, **kwargs)
119129
self.fields['module'].help_text = _('DEVIATION_MODULE_ADD_HELPTEXT')

deviations/viewbase.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from itertools import groupby
2+
from lib2to3.pytree import convert
23
from typing import Any, Dict, Iterable, List, Optional, Tuple, Type
34

45
from django.db import models
@@ -8,6 +9,10 @@
89
from django.shortcuts import get_object_or_404
910
from django.utils.text import format_lazy
1011
from django.utils.translation import ugettext_lazy as _, ngettext
12+
from django.utils import timezone
13+
from django.utils.dateparse import parse_datetime
14+
import datetime
15+
1116

1217
from course.models import CourseModule, UserTag
1318
from course.viewbase import CourseInstanceMixin, CourseInstanceBaseView
@@ -16,6 +21,9 @@
1621
from authorization.permissions import ACCESS
1722
from exercise.models import BaseExercise
1823
from userprofile.models import UserProfile
24+
from exercise.submission_models import Submission
25+
26+
from .forms import DeadlineRuleDeviationForm, MaxSubmissionRuleDeviationForm
1927

2028

2129
class ListDeviationsView(CourseInstanceBaseView):
@@ -48,7 +56,12 @@ def form_valid(self, form: forms.BaseForm) -> HttpResponse:
4856
exercise__in=exercises,
4957
submitter__in=submitters,
5058
)
59+
if isinstance(form, DeadlineRuleDeviationForm ) and not form.cleaned_data.get("without_late_submission_approval"):
60+
approve_late_submissions(submitters, exercises, form.cleaned_data)
5161

62+
elif isinstance(form, MaxSubmissionRuleDeviationForm ) and not form.cleaned_data.get("without_unofficial_submission_approval"):
63+
approve_unofficial_submissions(submitters, exercises, form.cleaned_data)
64+
5265
if existing_deviations:
5366
# Some deviations already existed. Use OverrideDeviationsView to
5467
# confirm which ones the user wants to override. Store the form
@@ -297,3 +310,36 @@ def get_submitters(form_data: Dict[str, Any]) -> models.QuerySet[UserProfile]:
297310
models.Q(id__in=form_data.get('submitter', []))
298311
| models.Q(taggings__tag__in=form_data.get('submitter_tag', []))
299312
)
313+
314+
def approve_late_submissions(submitters, exercises, form_data):
315+
minutes = form_data.get('minutes')
316+
new_date = form_data.get('new_date')
317+
for submitter in submitters:
318+
for exercise in exercises:
319+
submissions = exercise.get_submissions_for_student(submitter, exclude_errors=True)
320+
for submission in submissions:
321+
new_deadline = None
322+
if new_date:
323+
string_date = str(new_date)[:16]
324+
new_deadline = timezone.make_aware(
325+
parse_datetime(string_date),
326+
timezone.get_current_timezone())
327+
else:
328+
new_deadline = submission.submission_time + datetime.timedelta(minutes=minutes)
329+
330+
if submission.late_penalty_applied is not None and submission.submission_time <= new_deadline:
331+
submission.convert_penalized_submission()
332+
submission.save()
333+
334+
def approve_unofficial_submissions(submitters, exercises, form_data):
335+
extra_submissions = form_data.get('extra_submissions')
336+
for submitter in submitters:
337+
for exercise in exercises:
338+
converted_counter = extra_submissions
339+
submissions_unordered = exercise.get_submissions_for_student(submitter, exclude_errors=True)
340+
submissions = [submission for submission in reversed(submissions_unordered)] #reverse the order to get older submission first
341+
for submission in submissions:
342+
if submission.status == Submission.STATUS.UNOFFICIAL and converted_counter > 0:
343+
submission.convert_penalized_submission()
344+
submission.save()
345+
converted_counter -= 1

exercise/staff_views.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,14 @@
1919
from authorization.permissions import ACCESS
2020
from course.viewbase import CourseInstanceBaseView, CourseInstanceMixin
2121
from course.models import (
22+
CourseModule,
2223
Enrollment,
2324
USERTAG_EXTERNAL,
2425
USERTAG_INTERNAL,
2526
)
2627
from deviations.models import MaxSubmissionsRuleDeviation
2728
from exercise.cache.points import CachedPoints
29+
from exercise.exercise_models import BaseExercise
2830
from lib.helpers import settings_text, extract_form_errors
2931
from lib.viewbase import BaseRedirectView, BaseFormView, BaseView
3032
from notification.models import Notification
@@ -530,3 +532,73 @@ def form_valid(self, form):
530532
def form_invalid(self, form):
531533
messages.error(self.request, _('FAILURE_SAVING_CHANGES'))
532534
return super().form_invalid(form)
535+
536+
class SubmissionConversionView(SubmissionMixin, BaseRedirectView):
537+
"""
538+
A POST-only view that updates a student's late or unofficial submission
539+
to normal submission. Changed the status and remove the penalty
540+
"""
541+
def post(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
542+
self.submission = self.get_submission_object()
543+
self.submission.convert_penalized_submission()
544+
self.submission.save()
545+
return self.redirect(self.submission.get_inspect_url())
546+
547+
548+
class SubmissionConversionByModuleView(CourseInstanceMixin, BaseRedirectView):
549+
"""
550+
A POST-only view that by module bulks updates a student's late or unofficial submission
551+
to normal submission. Changed the status and remove the penalty
552+
"""
553+
554+
user_kw = 'user_id'
555+
module_kw = 'module_id'
556+
access_mode = ACCESS.ASSISTANT
557+
558+
def get_course_module_object(self):
559+
return get_object_or_404(
560+
CourseModule,
561+
id=self.kwargs[self.module_kw],
562+
course_instance=self.instance
563+
)
564+
565+
def get_resource_objects(self):
566+
self.kwargs[self.user_kw] = self.request.POST[self.user_kw],
567+
self.kwargs[self.user_kw] = self.kwargs[self.user_kw][0]
568+
self.kwargs[self.module_kw] = self.request.POST[self.module_kw]
569+
super().get_resource_objects()
570+
571+
#getting module and user by id.
572+
self.module = get_object_or_404(
573+
CourseModule,
574+
id=self.kwargs[self.module_kw])
575+
576+
self.student = get_object_or_404(
577+
User,
578+
id=self.kwargs[self.user_kw],
579+
)
580+
581+
def post(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
582+
approve_scope = self.request.POST["approve-scope"]
583+
approve_type = self.request.POST["approve-type"]
584+
is_scope_exercise = approve_scope == "single-exercise"
585+
exercise_id = self.request.POST["exercise_id"]
586+
is_late = False if approve_type == "isUnofficial" else True
587+
is_unofficial = False if approve_type == "isLate" else True
588+
589+
profile = self.student.userprofile
590+
exercises = []
591+
if is_scope_exercise:
592+
exercises = BaseExercise.objects.filter(id=exercise_id)
593+
else:
594+
exercises = BaseExercise.objects.filter(course_module=self.module)
595+
596+
for exercise in exercises:
597+
submissions = exercise.get_submissions_for_student(self.student.userprofile, exclude_errors=True)
598+
for submission in submissions:
599+
if ((is_unofficial and submission.status == Submission.STATUS.UNOFFICIAL) or (is_late and submission.late_penalty_applied is not None)):
600+
submission.convert_penalized_submission()
601+
submission.save()
602+
603+
link = reverse('user-results', kwargs={'user_id': profile.id, **self.instance.get_url_kwargs()})
604+
return self.redirect(link)

exercise/submission_models.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,16 @@ def clean_post_parameters(self):
406406
del self._files
407407
del self._data
408408

409+
def convert_penalized_submission(self):
410+
"""
411+
Removes penalty and Sets the points for this submission object based on original score.
412+
Then this will set the status of submission to ready
413+
414+
The method is used to convert late or unofficial submission to fully graded one.
415+
"""
416+
self.set_points(self.service_points, self.service_max_points, no_penalties=True)
417+
self.set_ready(convert_operation=True)
418+
409419
def set_points(self, points, max_points, no_penalties=False):
410420
"""
411421
Sets the points and maximum points for this submissions. If the given
@@ -467,9 +477,9 @@ def scale_grade_to(self, percentage):
467477
def set_waiting(self):
468478
self.status = self.STATUS.WAITING
469479

470-
def set_ready(self):
480+
def set_ready(self, convert_operation=False):
471481
self.grading_time = timezone.now()
472-
if self.status != self.STATUS.UNOFFICIAL or self.force_exercise_points:
482+
if (self.status != self.STATUS.UNOFFICIAL or self.force_exercise_points) or convert_operation:
473483
self.status = self.STATUS.READY
474484

475485
# Fire set hooks.

exercise/templates/exercise/_user_results.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,11 @@ <h3 class="panel-title">
8585

8686
{% points_progress module %}
8787
{{ module.introduction|safe }}
88+
89+
{% if module.late_allowed and module.late_percent > 0 %}
90+
91+
{% endif %}
92+
8893
</div>
8994
{% if not exercise_accessible and not is_course_staff %}
9095
<div class="alert alert-warning clearfix site-message">

exercise/templates/exercise/staff/_assessment_panel.html

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,24 @@
1919
{% endif %}
2020
</div>
2121
<div>
22-
<button
22+
<form method="post" style="display: inline-block">
23+
{% csrf_token %}
24+
<button
25+
data-toggle="modal"
26+
data-target="#submission-approval-modal"
27+
class="aplus-button--secondary aplus-button--sm aplus-button--left"
28+
type="button"
29+
>
30+
{% translate 'APPROVE_SUBMISSION' %}
31+
</button><button
32+
data-toggle="modal"
33+
data-target="#approval-help-modal"
34+
class="aplus-button--secondary aplus-button--sm aplus-button--right"
35+
type="button"
36+
title="{% translate 'HELP' %}"
37+
>?</button>
38+
</form>
39+
<button
2340
data-toggle="modal"
2441
data-target="#details-modal"
2542
class="aplus-button--secondary aplus-button--sm"
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{% load i18n %}
2+
{% load course %}
3+
4+
<div class="modal" id="approval-help-modal" tabindex="-1" role="dialog" aria-labelledby="approval-help-modal-label">
5+
<div class="modal-dialog" role="document">
6+
<div class="modal-content">
7+
<div class="modal-header">
8+
<button type="button" class="close" data-dismiss="modal" aria-label="{% translate "CLOSE" %}">
9+
<span aria-hidden="true">&times;</span>
10+
</button>
11+
<h4 class="modal-title" id="approval-help-modal-label">
12+
{% translate "SUBMISSION_APPROVAL_HELP" %}
13+
</h4>
14+
</div>
15+
16+
<div class="modal-body">
17+
{% translate "SUBMISSION_APPROVAL_DESCRIPTION" %}
18+
</div>
19+
</div>
20+
</div>
21+
</div>
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
{% load i18n %}
2+
{% load course %}
3+
4+
<div class="modal" id="submission-approval-modal" tabindex="-1" role="dialog" aria-labelledby="resubmit-modal-label">
5+
<div class="modal-dialog" role="document">
6+
<div class="modal-content">
7+
<div class="modal-header">
8+
<button type="button" class="close" data-dismiss="modal" aria-label="{% translate "CLOSE" %}">
9+
<span aria-hidden="true">&times;</span>
10+
</button>
11+
<h4 class="modal-title" id="resubmit-modal-label">
12+
{% translate 'LATE_SUBMISSION_APPROVAL_CONFIRMATION_HEADER' %}
13+
</h4>
14+
</div>
15+
16+
<div class="modal-body">
17+
<p>{% translate 'LATE_SUBMISSION_APPROVAL_CONFIRMATION_TEXT' %}</p>
18+
<p>{{submission.submitters.all.0.name_with_student_id}}</p>
19+
<dl>
20+
<dt>{% translate 'LATE_SUBMISSION_APPROVAL_CONFIRMATION_TEXT_DETAIL' %}</dt>
21+
<dd>Module: {{ module.name|parse_localization }}</dd>
22+
<dd>Exercise: {{ exercise.name|parse_localization }}</dd>
23+
<dd>Submission ID & Time: {{ submission.id }} - {{ submission.submission_time }}</dd>
24+
</dl>
25+
<br>
26+
<button onclick="OpenMultipleConversionform();" name = "approve-multiple" class="aplus-button--secondary aplus-button--xs">{% translate 'LATE_SUBMISSION_APPROVAL_APPROVE_MULTIPLE_BUTTON' %}</button>
27+
<br>
28+
<br>
29+
<form method="post" id="convert-module-form" action="{% url 'submission-conversion-module' course_slug=course.url instance_slug=instance.url %}" style="display: none">
30+
{% csrf_token %}
31+
<input type="hidden" id="module_id" name="module_id" value="{{ module.id }}">
32+
<input type="hidden" id="user_id" name="user_id" value="{{ submission.submitters.all.0.id }}">
33+
<input type="hidden" id="exercise_id" name="exercise_id" value="{{ exercise.id }}">
34+
<p>{% translate 'LATE_SUBMISSION_APPROVAL_APPROVE_MULTIPLE_SELECT_SCOPE' %}</p>
35+
<input type="radio" id="single-exercise" name="approve-scope" value="single-exercise">
36+
<label for="single-exercise">{% translate 'LATE_SUBMISSION_APPROVAL_APPROVE_MULTIPLE_SCOPE_EXERCISE' %}</label><br>
37+
<input type="radio" id="whole-module" name="approve-scope" value="whole-module">
38+
<label for="whole-module">{% translate 'LATE_SUBMISSION_APPROVAL_APPROVE_MULTIPLE_SCOPE_MODULE' %}</label><br>
39+
40+
<br><p>{% translate 'LATE_SUBMISSION_APPROVAL_APPROVE_MULTIPLE_SELECT_TYPE' %}</p>
41+
<input type="radio" id="isLate" name="approve-type" value="isLate">
42+
<label for="isLate"> {% translate 'LATE_SUBMISSION_APPROVAL_APPROVE_MULTIPLE_TYPE_LATE' %}</label><br>
43+
<input type="radio" id="isUnofficial" name="approve-type" value="isUnofficial">
44+
<label for="isUnofficial"> {% translate 'LATE_SUBMISSION_APPROVAL_APPROVE_MULTIPLE_TYPE_UNOFFICIAL' %}</label><br>
45+
<input type="radio" id="isAll" name="approve-type" value="isAll">
46+
<label for="isAll"> {% translate 'LATE_SUBMISSION_APPROVAL_APPROVE_MULTIPLE_TYPE_ALL' %}</label><br>
47+
<button
48+
class="aplus-button--default aplus-button--sm"
49+
type="submit"
50+
>
51+
{% translate 'APPROVE_MULTIPLE_SUBMISSIONS' %}
52+
</button>
53+
</form>
54+
<form method="post" id="convert-singular-form" action="{{ submission|url:'submission-conversion' }}" style="display: inline-block">
55+
{% csrf_token %}
56+
<button
57+
class="aplus-button--default aplus-button--sm"
58+
type="submit"
59+
>
60+
{% translate 'APPROVE_SUBMISSION' %}
61+
</button>
62+
</form>
63+
</div>
64+
</div>
65+
</div>
66+
</div>
67+
<script>
68+
function OpenMultipleConversionform(){
69+
moduleform = document.getElementById('convert-module-form')
70+
singularform = document.getElementById('convert-singular-form')
71+
if (moduleform.style.display === "none") {
72+
document.getElementById('convert-module-form').style.display = 'inline-block';
73+
document.getElementById('convert-singular-form').style.display = 'none';
74+
} else {
75+
document.getElementById('convert-module-form').style.display = 'none';
76+
document.getElementById('convert-singular-form').style.display = 'inline-block';
77+
}
78+
}
79+
</script>

exercise/templates/exercise/staff/inspect_submission.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@
9797
</div>
9898
{% include "exercise/staff/_submission_data_modal.html" %}
9999
{% include "exercise/staff/_resubmit_modal.html" %}
100+
{% include "exercise/staff/_late_submission_approval_modal.html" %}
101+
{% include "exercise/staff/_late_submission_approval_help_modal.html" %}
100102
{% endblock %}
101103

102104
{% block scripts %}

exercise/tests.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,13 @@ def test_submission_late_penalty_applied(self):
571571
deviation.save()
572572
self.late_late_submission_when_late_allowed.set_points(5, 10)
573573
self.assertAlmostEqual(self.late_late_submission_when_late_allowed.late_penalty_applied, 0.2)
574+
575+
def test_submission_late_conversion(self):
576+
convert_submission_url = self.late_submission.get_url('submission-conversion')
577+
response = self.client.get(convert_submission_url)
578+
self.assertEqual(response.status_code, 302)
579+
self.assertTrue(self.late_submission.late_penalty_applied is None)
580+
574581

575582
def test_early_submission(self):
576583
self.course_module_with_late_submissions_allowed.opening_time = self.tomorrow

0 commit comments

Comments
 (0)