|
1 | | -import json |
2 | 1 | import logging |
3 | | -import time |
4 | 2 | from typing import Any, Dict |
5 | 3 |
|
6 | 4 | from django.contrib import messages |
7 | 5 | from django.contrib.auth.models import User |
8 | 6 | from django.core.exceptions import PermissionDenied, ValidationError |
9 | 7 | from django.core.validators import URLValidator |
10 | | -from django.db.models import Count, F, Max, Prefetch, Q |
| 8 | +from django.db.models import Count, Max, Prefetch, Q |
11 | 9 | from django.http.request import HttpRequest |
12 | 10 | from django.http.response import HttpResponse, JsonResponse, Http404 |
13 | 11 | from django.shortcuts import get_object_or_404 |
14 | 12 | from django.urls.base import reverse |
15 | 13 | from django.utils import timezone |
16 | 14 | from django.utils.text import format_lazy |
17 | | -from django.utils.translation import gettext_lazy as _ |
| 15 | +from django.utils.translation import gettext_lazy as _, ngettext |
18 | 16 |
|
19 | 17 | from authorization.permissions import ACCESS |
20 | 18 | from course.viewbase import CourseInstanceBaseView, CourseInstanceMixin |
21 | 19 | from course.models import ( |
| 20 | + CourseModule, |
22 | 21 | Enrollment, |
23 | 22 | USERTAG_EXTERNAL, |
24 | 23 | USERTAG_INTERNAL, |
|
29 | 28 | from lib.viewbase import BaseRedirectView, BaseFormView, BaseView |
30 | 29 | from notification.models import Notification |
31 | 30 | from userprofile.models import UserProfile |
32 | | -from .models import LearningObject |
| 31 | +from .models import BaseExercise, ExerciseTask, LearningObject, Submission |
33 | 32 | from .forms import ( |
34 | 33 | SubmissionReviewForm, |
35 | 34 | SubmissionCreateAndReviewForm, |
36 | 35 | EditSubmittersForm, |
37 | 36 | ) |
38 | 37 | from .tasks import regrade_exercises |
39 | | -from .submission_models import Submission |
40 | | -from .exercise_models import ExerciseTask |
41 | 38 | from .viewbase import ( |
42 | 39 | ExerciseBaseView, |
43 | 40 | SubmissionBaseView, |
44 | 41 | SubmissionMixin, |
45 | 42 | ExerciseMixin, |
46 | 43 | ExerciseListBaseView, |
47 | 44 | ) |
48 | | -from .exercise_models import BaseExercise |
49 | 45 | from lib.logging import SecurityLog |
50 | 46 |
|
51 | 47 |
|
@@ -526,3 +522,102 @@ def form_valid(self, form): |
526 | 522 | def form_invalid(self, form): |
527 | 523 | messages.error(self.request, _('FAILURE_SAVING_CHANGES')) |
528 | 524 | return super().form_invalid(form) |
| 525 | + |
| 526 | + |
| 527 | +class SubmissionApprovalView(SubmissionMixin, BaseRedirectView): |
| 528 | + """ |
| 529 | + A POST-only view that approves a student's late or unofficial submission |
| 530 | + as a normal, graded submission. The late penalty of the submission is |
| 531 | + removed and the submission status is changed to ready. |
| 532 | + """ |
| 533 | + access_mode = ACCESS.GRADING |
| 534 | + |
| 535 | + def post(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse: |
| 536 | + self.submission.approve_penalized_submission() |
| 537 | + self.submission.save() |
| 538 | + messages.success(self.request, format_lazy( |
| 539 | + _('SUBMISSION_APPROVAL_SUCCESS -- {points}, {max_points}'), |
| 540 | + points=self.submission.grade, |
| 541 | + max_points=self.submission.exercise.max_points, |
| 542 | + )) |
| 543 | + return self.redirect(self.submission.get_inspect_url()) |
| 544 | + |
| 545 | + |
| 546 | +class SubmissionApprovalByModuleView(CourseInstanceMixin, BaseRedirectView): |
| 547 | + """ |
| 548 | + A POST-only view that approves a student's late or unofficial submissions |
| 549 | + as normal, graded submissions in a whole module or exercise. |
| 550 | + """ |
| 551 | + user_kw = 'user_id' |
| 552 | + submission_kw = 'submission_id' |
| 553 | + access_mode = ACCESS.ASSISTANT |
| 554 | + |
| 555 | + def get_resource_objects(self): |
| 556 | + super().get_resource_objects() |
| 557 | + |
| 558 | + self.student = get_object_or_404( |
| 559 | + User, |
| 560 | + id=self.request.POST.get(self.user_kw), |
| 561 | + ) |
| 562 | + self.submission = get_object_or_404( |
| 563 | + Submission, |
| 564 | + id=self.request.POST.get(self.submission_kw), |
| 565 | + ) |
| 566 | + self.exercise = self.submission.exercise |
| 567 | + self.module = self.exercise.course_module |
| 568 | + |
| 569 | + def post(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse: |
| 570 | + approve_scope = self.request.POST.get('approve-scope') |
| 571 | + approve_type = self.request.POST.get('approve-type') |
| 572 | + |
| 573 | + if approve_scope == 'single-exercise': |
| 574 | + exercise_filter = {'exercise': self.exercise} |
| 575 | + disallow_assistant_grading = not self.exercise.allow_assistant_grading |
| 576 | + else: |
| 577 | + exercise_filter = {'exercise__course_module': self.module} |
| 578 | + disallow_assistant_grading = (BaseExercise.objects |
| 579 | + .filter( |
| 580 | + course_module=self.module, |
| 581 | + allow_assistant_grading=False, |
| 582 | + ) |
| 583 | + .exists()) |
| 584 | + |
| 585 | + if self.is_assistant and disallow_assistant_grading: |
| 586 | + return self.permission_denied( |
| 587 | + message=_('SUBMISSION_APPROVAL_ASSISTANT_PERMISSION_DENIED_MSG'), |
| 588 | + ) |
| 589 | + |
| 590 | + submissions = (self.student.userprofile.submissions |
| 591 | + .exclude_errors() |
| 592 | + .defer_text_fields() |
| 593 | + .filter(**exercise_filter) |
| 594 | + ) |
| 595 | + if approve_type == 'only-late': |
| 596 | + submissions = submissions.filter( |
| 597 | + late_penalty_applied__isnull=False, |
| 598 | + ).exclude( |
| 599 | + status=Submission.STATUS.UNOFFICIAL, |
| 600 | + ) |
| 601 | + elif approve_type == 'only-unofficial': |
| 602 | + submissions = submissions.filter(status=Submission.STATUS.UNOFFICIAL) |
| 603 | + else: |
| 604 | + # Both late and unofficial submissions. |
| 605 | + # Exclude normal submissions since there are usually many of those. |
| 606 | + submissions = submissions.filter( |
| 607 | + Q(late_penalty_applied__isnull=False) |
| 608 | + | Q(status=Submission.STATUS.UNOFFICIAL), |
| 609 | + ) |
| 610 | + |
| 611 | + count = 0 |
| 612 | + for submission in submissions: |
| 613 | + submission.approve_penalized_submission() |
| 614 | + submission.save() |
| 615 | + count += 1 |
| 616 | + |
| 617 | + messages.success(self.request, ngettext( |
| 618 | + 'SUBMISSION_APPROVAL_MULTIPLE_SUCCESS -- {count}', |
| 619 | + 'SUBMISSION_APPROVAL_MULTIPLE_SUCCESS_PLURAL -- {count}', |
| 620 | + count, |
| 621 | + ).format(count=count), |
| 622 | + ) |
| 623 | + return self.redirect(self.submission.get_inspect_url()) |
0 commit comments