diff --git a/assets/js/submissions.js b/assets/js/submissions.js new file mode 100644 index 000000000..9da86fae4 --- /dev/null +++ b/assets/js/submissions.js @@ -0,0 +1,56 @@ +function submissions_list() { + var checkedButtons = []; // Array to store data-slug of checked buttons + + $('button').on('click', function(event) { + event.preventDefault(); + var icon = $(this).find('.glyphicon'); + + if (icon.hasClass('glyphicon-unchecked')) { + icon.removeClass('glyphicon-unchecked').addClass('glyphicon-check'); + $(this).attr("data-status", "checked"); // set data-status to 'checked' + checkedButtons.push($(this).attr("data-tagslug")); // add data-slug of the button to the array + } else { + icon.removeClass('glyphicon-check').addClass('glyphicon-unchecked'); + $(this).attr("data-status", "unchecked"); // set data-status to 'unchecked' + var index = checkedButtons.indexOf($(this).attr("data-tagslug")); // get the index of the button in the array + if (index !== -1) { + checkedButtons.splice(index, 1); // remove it from the array if it exists + } + } + + refresh_filters(); + }); + + var filter_items = function (submissions) { + const filterTags = $.makeArray($('.filter-submissions button.filter-tag:has(.glyphicon-check)')) + .map(function (elem) { + return $(elem).attr('data-tagslug'); + }); + const filterStatuses = $.makeArray($('.filter-submissions button.filter-status:has(.glyphicon-check)')) + .map(function (elem) { + return $(elem).attr('data-status'); + }); + return submissions.map(function (submission) { + // Set intercetion tags ∩ filters + const intersectTags = submission.tag_slugs.filter(function (tag) { + return filterTags.indexOf(tag) >= 0; + }); + return intersectTags.length === filterTags.length + }); + }; + + function refresh_filters() { + $('.filter-submissions table tr').each(function(){ + $(this).hide(); // hide all rows by default + + $(this).find('tagging.tag').each(function(){ + if (checkedButtons.includes($(this).attr("data-tagslug"))) { + $(this).closest('tr').show(); // show the row if it has a checked button + return false; // exit the inner loop + } + }); + }); + } + + refresh_filters(); +} diff --git a/course/migrations/0059_submissiontag.py b/course/migrations/0059_submissiontag.py new file mode 100644 index 000000000..aa1beed47 --- /dev/null +++ b/course/migrations/0059_submissiontag.py @@ -0,0 +1,84 @@ +# Generated by Django 4.2.11 on 2024-04-24 11:06 + +import colorfield +from django.db import migrations, models +import django.db.models.deletion +import lib.models + + +class Migration(migrations.Migration): + + dependencies = [ + ("course", "0058_coursemodule_model_answer_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="SubmissionTag", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "name", + models.CharField(help_text="Display name for tag", max_length=20), + ), + ( + "slug", + models.SlugField( + help_text="Slug key for tag. If left blank, one is created from name", + max_length=20, + ), + ), + ( + "description", + models.CharField( + blank=True, + help_text="Describe the usage or meaning of this tag", + max_length=155, + ), + ), + ( + "color", + colorfield.ColorField( + default="#CD0000", + help_text="Color that is used as background for this tag", + max_length=7, + ), + ), + ( + "visible_to_students", + models.BooleanField( + default=False, verbose_name="LABEL_VISIBLE_TO_STUDENTS" + ), + ), + ( + "filter_status", + models.BooleanField( + default=False, verbose_name="LABEL_FILTER_STATUS" + ), + ), + ( + "course_instance", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="submissiontags", + to="course.courseinstance", + verbose_name="LABEL_COURSE_INSTANCE", + ), + ), + ], + options={ + "verbose_name": "MODEL_NAME_SUBMISSION_TAG", + "verbose_name_plural": "MODEL_NAME_SUBMISSION_TAG_PLURAL", + "ordering": ["course_instance", "name"], + }, + bases=(lib.models.UrlMixin, models.Model), + ), + ] diff --git a/course/migrations/0060_remove_submissiontag_filter_status.py b/course/migrations/0060_remove_submissiontag_filter_status.py new file mode 100644 index 000000000..b6d07e958 --- /dev/null +++ b/course/migrations/0060_remove_submissiontag_filter_status.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.11 on 2024-04-24 11:13 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("course", "0059_submissiontag"), + ] + + operations = [ + migrations.RemoveField( + model_name="submissiontag", + name="filter_status", + ), + ] diff --git a/course/models.py b/course/models.py index 3be2edb8f..dd78ead71 100644 --- a/course/models.py +++ b/course/models.py @@ -51,7 +51,6 @@ from exercise.exercise_models import LearningObject from threshold.models import CourseModuleRequirement - logger = logging.getLogger('aplus.course') # Read pseudonymization data from file @@ -347,6 +346,37 @@ def is_valid_slug(self, slug_candidate): # pylint: disable=arguments-renamed return not qs.exists() +class SubmissionTag(UrlMixin, ColorTag): + course_instance = models.ForeignKey('CourseInstance', + verbose_name=_('LABEL_COURSE_INSTANCE'), + on_delete=models.CASCADE, + related_name="submissiontags", + ) + + visible_to_students = models.BooleanField( + verbose_name=_('LABEL_VISIBLE_TO_STUDENTS'), + default=False, + ) + + class Meta: + verbose_name = _('MODEL_NAME_SUBMISSION_TAG') + verbose_name_plural = _('MODEL_NAME_SUBMISSION_TAG_PLURAL') + ordering = ['id'] + + def get_url_kwargs(self): + return dict(tag_id=self.id, **self.course_instance.get_url_kwargs()) # pylint: disable=use-dict-literal + + def is_valid_slug(self, slug_candidate): # pylint: disable=arguments-renamed + assert self.course_instance + if not slug_candidate: + return False + qs = self.__class__.objects.filter( + course_instance=self.course_instance, slug=slug_candidate) + if self.pk is not None: + qs = qs.exclude(pk=self.pk) + return not qs.exists() + + class HardcodedUserTag(UserTag): class Meta: verbose_name = _('MODEL_NAME_HARDCODED_USER_TAG') diff --git a/edit_course/course_forms.py b/edit_course/course_forms.py index a92b13818..ce5a1f2f3 100644 --- a/edit_course/course_forms.py +++ b/edit_course/course_forms.py @@ -12,9 +12,10 @@ from django_colortag.forms import ColorTagForm from aplus.api import api_reverse -from course.models import LearningObjectCategory, CourseModule, CourseInstance, UserTag +from course.models import LearningObjectCategory, CourseModule, CourseInstance, UserTag, SubmissionTag from course.sis import get_sis_configuration, StudentInfoSystem from exercise.models import CourseChapter +from exercise.submission_models import SubmissionTagging from lib.validators import generate_url_key_validator from lib.fields import UsersSearchSelectField from lib.widgets import DateTimeLocalInput @@ -267,6 +268,13 @@ class CloneInstanceForm(forms.Form): required=False, initial=True, ) + submissiontags = forms.BooleanField( + label=_('LABEL_SUBMISSION_TAGS'), + help_text=_('LABEL_SUBMISSION_TAGS_HELPTEXT'), + required=False, + initial=True, + ) + if settings.GITMANAGER_URL: key_year = forms.IntegerField( label=_('LABEL_YEAR'), @@ -400,6 +408,30 @@ def get_base_object(self, course_instance): return obj +class SubmissionTagForm(ColorTagForm): + + class Meta(ColorTagForm.Meta): + model = SubmissionTag + fields = [ + 'name', + 'slug', + 'description', + 'color', + ] + labels = { + 'name': _('LABEL_NAME'), + 'slug': _('LABEL_SLUG'), + 'description': _('LABEL_DESCRIPTION'), + 'color': _('LABEL_COLOR'), + } + + @classmethod + def get_base_object(self, course_instance): + obj = self.Meta.model() + obj.course_instance = course_instance + return obj + + class SelectUsersForm(forms.Form): user = UsersSearchSelectField(queryset=UserProfile.objects.none(), initial_queryset=UserProfile.objects.none()) @@ -412,6 +444,19 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: self.fields['user'].queryset = course_instance.get_student_profiles() +class SubmissionTaggingForm(forms.ModelForm): + class Meta(ColorTagForm.Meta): + model = SubmissionTagging + fields = [ + 'tag', + 'submission', + ] + labels = { + 'tag': _('LABEL_SUBMISSION_TAG'), + 'submission': _('LABEL_SUBMISSION'), + } + + class GitmanagerForm(forms.Form): key = forms.SlugField( label=_('LABEL_KEY'), diff --git a/edit_course/templates/edit_course/edit_course_base.html b/edit_course/templates/edit_course/edit_course_base.html index a52187088..f075dee7e 100644 --- a/edit_course/templates/edit_course/edit_course_base.html +++ b/edit_course/templates/edit_course/edit_course_base.html @@ -53,6 +53,11 @@ {% translate "TAGS" %} + +
  • {% translate "SUBMISSION_TAGS" %}
  • +
  • {% translate "ADD_NEW_SUBMISSION_TAG" %}
  • +{% endblock %} +{% block view_tag %}edit-course,course-submission-tags{% endblock %} + +{% block coursecontent %} +
    +
    + {% csrf_token %} + {% translate "ADD_NEW_SUBMISSION_TAG" %} + {{ form|bootstrap_horizontal }} +
    +
    + + {% translate "CANCEL" %} +
    +
    +
    +{% endblock %} diff --git a/edit_course/templates/edit_course/submissiontag_delete.html b/edit_course/templates/edit_course/submissiontag_delete.html new file mode 100644 index 000000000..faecaa286 --- /dev/null +++ b/edit_course/templates/edit_course/submissiontag_delete.html @@ -0,0 +1,36 @@ +{% extends "edit_course/edit_course_base.html" %} +{% load i18n %} +{% load bootstrap %} +{% load course %} +{% load editcourse %} +{% load colortag %} + +{% block editbreadcrumblist %} +{{ block.super }} +
  • {% translate "TAGS" %}
  • +
  • {% translate "REMOVE_SUBMISSION_TAG" %}
  • +{% endblock %} +{% block view_tag %}edit-course,course-submission-tags{% endblock %} + +{% block coursecontent %} +
    +
    + {% csrf_token %} + + {% translate "REMOVE_SUBMISSION_TAG_CONFIRMATION" %} + + +
    + {% translate "REMOVE_SUBMISSION_TAG_CONFIRMATION_ALERT" %} + {{ object|colortag }} +
    + +
    + + + {% translate "CANCEL" %} + +
    +
    + +{% endblock %} diff --git a/edit_course/templates/edit_course/submissiontag_edit.html b/edit_course/templates/edit_course/submissiontag_edit.html new file mode 100644 index 000000000..a69778532 --- /dev/null +++ b/edit_course/templates/edit_course/submissiontag_edit.html @@ -0,0 +1,27 @@ +{% extends "edit_course/edit_course_base.html" %} +{% load i18n %} +{% load bootstrap %} +{% load course %} +{% load editcourse %} + +{% block editbreadcrumblist %} +{{ block.super }} +
  • {% translate "SUBMISSION_TAGS" %}
  • +
  • {% translate "EDIT_SUBMISSION_TAG" %}
  • +{% endblock %} +{% block view_tag %}edit-course,course-submission-tags{% endblock %} + +{% block coursecontent %} +
    +
    + {% csrf_token %} + {% translate "EDIT_SUBMISSION_TAG" %} + {{ form|bootstrap_horizontal }} +
    +
    + + {% translate "CANCEL" %} +
    +
    +
    +{% endblock %} diff --git a/edit_course/templates/edit_course/submissiontag_list.html b/edit_course/templates/edit_course/submissiontag_list.html new file mode 100644 index 000000000..c17954eb5 --- /dev/null +++ b/edit_course/templates/edit_course/submissiontag_list.html @@ -0,0 +1,58 @@ +{% extends "edit_course/edit_course_base.html" %} +{% load i18n %} +{% load bootstrap %} +{% load course %} +{% load editcourse %} +{% load colortag %} + +{% block editbreadcrumblist %} +{{ block.super }} +
  • {% translate "TAGS" %}
  • +{% endblock %} +{% block view_tag %}edit-course,course-submission-tags{% endblock %} + +{% block coursecontent %} +
    +

    + + + {% translate "ADD_NEW_SUBMISSION_TAG" %} + +

    +
    +
    +

    {% translate "SUBMISSION_TAGS" %}

    +
    + + + + + + + + {% with hide_tooltip=True %} + {% for tag in object_list %} + + + + + + + {% empty %} + + + + {% endfor %} + {% endwith %} +
    {% translate "TAG" %}{% translate "SLUG" %}{% translate "DESCRIPTION" %}{% translate "ACTIONS" %}
    {{ tag|colortag }}{{ tag.slug }}{{ tag.description }} + + + {% translate "EDIT" %} + + + + {% translate "REMOVE" %} + +
    {% translate "NO_SUBMISSION_TAGS" %}
    +
    +{% endblock %} diff --git a/edit_course/templates/edit_course/submissiontagging_add.html b/edit_course/templates/edit_course/submissiontagging_add.html new file mode 100644 index 000000000..bf9f9ceb5 --- /dev/null +++ b/edit_course/templates/edit_course/submissiontagging_add.html @@ -0,0 +1,28 @@ +{% extends "edit_course/edit_course_base.html" %} +{% load i18n %} +{% load bootstrap %} +{% load course %} +{% load editcourse %} +{% load static %} + +{% block editbreadcrumblist %} +{{ block.super }} +
  • {% translate "TAGS" %}
  • +
  • {% translate "ADD_NEW_SUBMISSION_TAGGING" %}
  • +{% endblock %} +{% block view_tag %}edit-course,course-submission-tags{% endblock %} + +{% block coursecontent %} +
    +
    + {% csrf_token %} + {% translate "ADD_NEW_SUBMISSION_TAGGING" %} + {{ form|bootstrap_horizontal }} +
    +
    + + {% translate "CANCEL" %} +
    +
    +
    +{% endblock %} diff --git a/edit_course/urls.py b/edit_course/urls.py index 04bd22a6a..61ae6144e 100644 --- a/edit_course/urls.py +++ b/edit_course/urls.py @@ -43,6 +43,18 @@ re_path(EDIT_URL_PREFIX + r'tags/(?P\d+)/new-tagging$', views.UserTaggingAddView.as_view(), name='course-taggings-add'), + re_path(EDIT_URL_PREFIX + r'submission-tags/$', + views.SubmissionTagListView.as_view(), + name='course-submission-tags'), + re_path(EDIT_URL_PREFIX + r'submission-tags/new/$', + views.SubmissionTagAddView.as_view(), + name='course-submission-tags-add'), + re_path(EDIT_URL_PREFIX + r'submission-tags/(?P\d+)/edit$', + views.SubmissionTagEditView.as_view(), + name='course-submission-tags-edit'), + re_path(EDIT_URL_PREFIX + r'submission-tags/(?P\d+)/remove$', + views.SubmissionTagDeleteView.as_view(), + name='course-submission-tags-remove'), re_path(EDIT_URL_PREFIX + r'batch-assess/$', views.BatchCreateSubmissionsView.as_view(), name='batch-assess'), diff --git a/edit_course/views.py b/edit_course/views.py index 4acfb0a91..8c4d2ff0c 100644 --- a/edit_course/views.py +++ b/edit_course/views.py @@ -31,7 +31,8 @@ from exercise.cache.hierarchy import NoSuchContent from exercise.models import LearningObject from .course_forms import CourseInstanceForm, CourseIndexForm, \ - CourseContentForm, CloneInstanceForm, GitmanagerForm, UserTagForm, SelectUsersForm + CourseContentForm, CloneInstanceForm, GitmanagerForm, UserTagForm, SelectUsersForm, SubmissionTagForm, \ + SubmissionTaggingForm from .managers import CategoryManager, ModuleManager, ExerciseManager from .operations.batch import create_submissions from .operations.configure import configure_from_url, get_build_log @@ -302,6 +303,42 @@ def form_valid(self, form): return super().form_valid(form) +class SubmissionTagMixin(CourseInstanceMixin, BaseTemplateMixin, BaseViewMixin): + access_mode = ACCESS.TEACHER + form_class = SubmissionTagForm + pk_url_kwarg = "tag_id" + success_url_name = "course-submission-tags" + + def get_success_url(self): + return self.instance.get_url(self.success_url_name) + + def get_queryset(self): + return self.instance.submissiontags.all() + + +class SubmissionTagListView(SubmissionTagMixin, ListView): + template_name = "edit_course/submissiontag_list.html" + + +class SubmissionTagAddView(SubmissionTagMixin, CreateView): + template_name = "edit_course/submissiontag_add.html" + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + if 'instance' not in kwargs or not kwargs['instance']: + kwargs.update({'instance': self.form_class.get_base_object(self.instance)}) + return kwargs + + +class SubmissionTagEditView(SubmissionTagMixin, UpdateView): + template_name = "edit_course/submissiontag_edit.html" + + +class SubmissionTagDeleteView(SubmissionTagMixin, DeleteView): + form_class = Form + template_name = "edit_course/submissiontag_delete.html" + + class BatchCreateSubmissionsView(CourseInstanceMixin, BaseTemplateView): access_mode = ACCESS.TEACHER template_name = "edit_course/batch_assess.html" diff --git a/exercise/migrations/0050_submissiontagging.py b/exercise/migrations/0050_submissiontagging.py new file mode 100644 index 000000000..cd6b90396 --- /dev/null +++ b/exercise/migrations/0050_submissiontagging.py @@ -0,0 +1,47 @@ +# Generated by Django 4.2.11 on 2024-04-24 11:06 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("course", "0059_submissiontag"), + ("exercise", "0049_submission_unofficial_submission_type"), + ] + + operations = [ + migrations.CreateModel( + name="SubmissionTagging", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "submission", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="submission_taggings", + to="exercise.submission", + verbose_name="LABEL_SUBMISSION", + ), + ), + ( + "tag", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="submission_taggings", + to="course.submissiontag", + verbose_name="LABEL_SUBMISSION_TAG", + ), + ), + ], + ), + ] diff --git a/exercise/staff_views.py b/exercise/staff_views.py index 3862252dc..339a64b54 100644 --- a/exercise/staff_views.py +++ b/exercise/staff_views.py @@ -1,6 +1,8 @@ +import json import logging from typing import Any, Dict +import requests from django.contrib import messages from django.contrib.auth.models import User from django.core.exceptions import PermissionDenied, ValidationError @@ -35,6 +37,7 @@ SubmissionCreateAndReviewForm, EditSubmittersForm, ) +from .submission_models import SubmissionTagging from .tasks import regrade_exercises from .viewbase import ( ExerciseBaseView, @@ -80,9 +83,29 @@ def get_common_objects(self) -> None: self.all = self.request.GET.get('all', None) self.all_url = self.exercise.get_submission_list_url() + "?all=yes" self.submissions = qs if self.all else qs[:self.default_limit] + self.note("all", "all_url", "submissions", "default_limit") +class FilteredSubmissionsView(ExerciseListBaseView): + access_mode = ACCESS.ASSISTANT + template_name = "exercise/staff/list_submissions.html" + ajax_template_name = "exercise/staff/_submissions_table.html" + default_limit = 50 + + def filtered_submissions(self, request): + tag = request.GET.get('tag', None) + if tag is not None: + self.submissions = Submission.objects.filter(tag__slug=tag).values( + 'submission') + print(tag) + else: + submissions = SubmissionTagging.objects.all().values('submission') + print("none") + + return JsonResponse({'submissions': list(submissions)}) + + class SubmissionsSummaryView(ExerciseBaseView): access_mode = ACCESS.ASSISTANT template_name = "exercise/staff/submissions_summary.html" @@ -179,6 +202,7 @@ def get_common_objects(self) -> None: self.lowest_visible_index = self.index - 10 self.highest_visible_index = self.index + 10 + print(str(self.instance.submissiontags.all())) #REMOVE THIS LINE # Find out if there are other submissions that the user should be # notified about (better submissions, later submissions or the final # submission). diff --git a/exercise/submission_models.py b/exercise/submission_models.py index dfb446103..ce7ea001d 100644 --- a/exercise/submission_models.py +++ b/exercise/submission_models.py @@ -15,6 +15,7 @@ from django.utils import timezone from django.utils.translation import get_language, gettext_lazy as _ +from course.models import SubmissionTag from exercise.protocol.exercise_page import ExercisePage from authorization.models import JWTAccessible from authorization.object_permissions import register_jwt_accessible_class @@ -711,6 +712,23 @@ def clear_pending(self): except PendingSubmission.DoesNotExist: pass +class SubmissionTagging(models.Model): + tag = models.ForeignKey(SubmissionTag, + verbose_name=_('LABEL_SUBMISSION_TAG'), + on_delete=models.CASCADE, + related_name="submission_taggings", + ) + submission = models.ForeignKey(Submission, + verbose_name=_('LABEL_SUBMISSION'), + on_delete=models.CASCADE, + related_name="submission_taggings", + db_index=True, + ) + + class Meta: + ordering = ["tag"] + + objects = models.Manager() class SubmissionDraft(models.Model): """ diff --git a/exercise/templates/exercise/staff/_assessment_panel.html b/exercise/templates/exercise/staff/_assessment_panel.html index 0d547873f..0da518f0c 100644 --- a/exercise/templates/exercise/staff/_assessment_panel.html +++ b/exercise/templates/exercise/staff/_assessment_panel.html @@ -2,6 +2,8 @@ {% load course %} {% load exercise %} {% load bootstrap %} +{% load colortag %} +{% load static %}
    @@ -22,6 +24,32 @@ {% translate 'NOT_ASSESSED_MANUALLY' %} {% endif %}
    + +

    + {% for tag in submission.submission_taggings.all %} +

    + {% csrf_token %} + {{tag.tag|colortag}} + +
    + {% endfor %} + +

    + + +
    {% if is_teacher or exercise.allow_assistant_grading %} {% if submission.is_approvable %} @@ -184,3 +212,23 @@
    + + + diff --git a/exercise/templates/exercise/staff/_submissions_table.html b/exercise/templates/exercise/staff/_submissions_table.html index e7d2298ba..293a426ce 100644 --- a/exercise/templates/exercise/staff/_submissions_table.html +++ b/exercise/templates/exercise/staff/_submissions_table.html @@ -2,6 +2,7 @@ {% load course %} {% load exercise %} {% load static %} +{% load colortag %} {% with count=submissions.count %}
    @@ -57,13 +58,24 @@

    - +

    + {% trans "FILTER_SUBMISSIONS_BY_TAG" %}: + {% for tag in instance.submissiontags.all %} + + {% endfor %} +

    + +
    + {% for submission in submissions %} - + @@ -96,6 +108,11 @@ +
    {% translate "SUBMITTERS" %} {% translate "TIME" %} {% translate "STATUS" %} {% translate "GRADE" %}{% translate "TAGS" %}
    {% profiles submission.submitters.all instance is_teacher %} {% format_points submission.grade feedback_revealed False %} + {% for tagging in submission.submission_taggings.all %} + {{ tagging.tag|colortag }} + {% endfor %} + {% if submission.grader %} @@ -121,5 +138,66 @@
    {% include "exercise/staff/_regrade_submissions.html" %} + + {% endwith %} diff --git a/exercise/templates/exercise/staff/list_submissions.html b/exercise/templates/exercise/staff/list_submissions.html index 8746c6a52..8e3b10f5d 100644 --- a/exercise/templates/exercise/staff/list_submissions.html +++ b/exercise/templates/exercise/staff/list_submissions.html @@ -14,4 +14,6 @@
    {% include "exercise/staff/_submissions_table.html" %}
    + + {% endblock %} diff --git a/exercise/urls.py b/exercise/urls.py index 340797e41..a95272c98 100644 --- a/exercise/urls.py +++ b/exercise/urls.py @@ -34,9 +34,21 @@ views.SubmittedFileView.as_view(), name="submission-file"), + re_path(SUBMISSION_URL_PREFIX \ + + r'add-taggings/(?P\d+)$', + views.SubmissionTaggingAddView.as_view(), + name="add-tag-to-submissions"), + re_path(SUBMISSION_URL_PREFIX \ + + r'remove-taggings/(?P\d+)$', + views.SubmissionTaggingRemoveView.as_view(), + name="remove-tag-from-submissions"), + re_path(EXERCISE_URL_PREFIX + r'submissions/$', staff_views.ListSubmissionsView.as_view(), name="submission-list"), + re_path(EXERCISE_URL_PREFIX + r'submissions/filter/$', + staff_views.FilteredSubmissionsView.as_view(), + name="filtered-submission-list"), re_path(EXERCISE_URL_PREFIX + r'submissions/regrade/$', staff_views.StartRegradeView.as_view(), name="submission-mass-regrade"), diff --git a/exercise/views.py b/exercise/views.py index a4394713f..cd4635aae 100644 --- a/exercise/views.py +++ b/exercise/views.py @@ -3,7 +3,7 @@ from django.contrib import messages from django.http.request import HttpRequest from django.http.response import Http404, HttpResponse, HttpResponseNotFound -from django.shortcuts import get_object_or_404 +from django.shortcuts import get_object_or_404, redirect from django.template.loader import render_to_string from django.utils.decorators import method_decorator from django.utils.translation import gettext_lazy as _ @@ -11,10 +11,13 @@ from django.views.decorators.clickjacking import xframe_options_exempt from django.views.decorators.csrf import csrf_exempt from django.db import DatabaseError +from django.views.generic import FormView, CreateView from authorization.permissions import ACCESS -from course.models import CourseModule +from course.models import CourseModule, SubmissionTag from course.viewbase import CourseInstanceBaseView, EnrollableViewMixin +from edit_course.course_forms import SubmissionTaggingForm +from edit_course.views import SubmissionTagMixin from lib.helpers import query_dict_to_list_of_tuples, safe_file_name, is_ajax from lib.remote_page import RemotePageNotFound, request_for_response from lib.viewbase import BaseRedirectMixin, BaseView @@ -22,7 +25,7 @@ from .cache.points import ExercisePoints from .models import BaseExercise, LearningObject, LearningObjectDisplay from .protocol.exercise_page import ExercisePage -from .submission_models import SubmittedFile, Submission +from .submission_models import SubmittedFile, Submission, SubmissionTagging from .viewbase import ( ExerciseBaseView, SubmissionBaseView, @@ -44,6 +47,57 @@ class ResultsView(TableOfContentsView): template_name = "exercise/results.html" +class SubmissionTaggingAddView(SubmissionTagMixin, CreateView): + form_class = SubmissionTaggingForm + template_name = "edit_course/submissiontagging_add.html" + pk_url_kwarg = "subtag_id" + access_mode = ACCESS.ASSISTANT + + def post(self, request, *args, **kwargs): + submission_id = self.kwargs['submission_id'] + subtag_id = self.kwargs['subtag_id'] + + # Get the Submission and SubTag objects using these ids + submission = Submission.objects.get(id=submission_id) + subtag = SubmissionTag.objects.get(id=subtag_id) + + # Create a new SubmissionTagging object + new_object = SubmissionTagging.objects.create(submission=submission, tag=subtag) + + # Redirect back to the previous page + return redirect(request.META.get('HTTP_REFERER', '/')) + + def __init__(self, *args, **kwargs): + self.instance = kwargs.pop('instance', None) + super().__init__(*args, **kwargs) + + +class SubmissionTaggingRemoveView(SubmissionTagMixin, CreateView): + form_class = SubmissionTaggingForm + template_name = "edit_course/submissiontagging_add.html" + pk_url_kwarg = "subtag_id" + access_mode = ACCESS.ASSISTANT + + def post(self, request, *args, **kwargs): + submission_id = self.kwargs['submission_id'] + subtag_id = self.kwargs['subtag_id'] + + # Get the Submission and SubTag objects using these ids + submission = Submission.objects.get(id=submission_id) + subtag = SubmissionTag.objects.get(id=subtag_id) + + # Delete SubmissionTagging object + SubmissionTagging.objects.filter(submission=submission, tag=subtag).delete() + + # Redirect back to the previous page + return redirect(request.META.get('HTTP_REFERER', '/')) + + def __init__(self, *args, **kwargs): + self.instance = kwargs.pop('instance', None) + super().__init__(*args, **kwargs) + + + class ExerciseInfoView(ExerciseBaseView): ajax_template_name = "exercise/_exercise_info.html" diff --git a/locale/en/LC_MESSAGES/django.po b/locale/en/LC_MESSAGES/django.po index d48f2cc1b..723ea1187 100644 --- a/locale/en/LC_MESSAGES/django.po +++ b/locale/en/LC_MESSAGES/django.po @@ -1383,6 +1383,10 @@ msgstr "Add new" msgid "FILTER_USERS_BY_TAG" msgstr "Filter users by tag" +#: course/templates/course/staff/_assessment_panel.html +msgid "FILTER_SUBMISSIONS_BY_TAG" +msgstr "Filter submissions by tag" + #: course/templates/course/staff/participants.html msgid "FILTER_USERS_BY_STATUS" msgstr "Filter users by status" @@ -1449,6 +1453,15 @@ msgstr "Status" msgid "TAGS" msgstr "Tags" +#: edit_course/templates/edit_course/submissiontag_add.html +#: edit_course/templates/edit_course/submissiontag_delete.html +#: edit_course/templates/edit_course/submissiontag_edit.html +#: edit_course/templates/edit_course/submissiontag_list.html +#: edit_course/templates/edit_course/submissiontagging_add.html +msgid "SUBMISSION_TAGS" +msgstr "Submission tags" + + #: course/templates/course/staff/participants.html msgid "ENROLLMENT_REMOVE_MODAL_REMOVE_TITLE" msgstr "Remove student" @@ -2815,6 +2828,10 @@ msgstr "" msgid "ADD_NEW_USER_TAG" msgstr "Add new user tag" +#: edit_course/templates/edit_course/submissiontag_add.html +msgid "ADD_NEW_SUBMISSION_TAG" +msgstr "Add new submission tag" + #: edit_course/templates/edit_course/usertag_add.html msgid "ADD_STUDENT_TAG" msgstr "Add student tag" @@ -2827,6 +2844,15 @@ msgstr "Remove user tag" msgid "REMOVE_USER_TAG_CONFIRMATION" msgstr "Confirm user tag removal" +#: edit_course/templates/edit_course/submissiontag_delete.html +msgid "REMOVE_SUBMISSION_TAG_CONFIRMATION" +msgstr "Remove submission tag" + +#: edit_course/templates/edit_course/submissiontag_delete.html +msgid "REMOVE_SUBMISSION_TAG_CONFIRMATION_ALERT" +msgstr "Confirm submission tag removal" + + #: edit_course/templates/edit_course/usertag_delete.html msgid "REMOVE_USER_TAG_CONFIRMATION_ALERT" msgstr "Are you sure you want to remove following user tag?" @@ -2835,6 +2861,10 @@ msgstr "Are you sure you want to remove following user tag?" msgid "EDIT_STUDENT_TAG" msgstr "Edit student tag" +#: edit_course/templates/edit_course/submissiontag_edit.html +msgid "EDIT_SUBMISSION_TAG" +msgstr "Edit submission tag" + #: edit_course/templates/edit_course/usertag_list.html msgid "ADD_NEW_STUDENT_TAG" msgstr "Add new student tag" diff --git a/locale/fi/LC_MESSAGES/django.po b/locale/fi/LC_MESSAGES/django.po index 79b6a2889..8bda2c470 100644 --- a/locale/fi/LC_MESSAGES/django.po +++ b/locale/fi/LC_MESSAGES/django.po @@ -1391,6 +1391,10 @@ msgstr "Lisää" msgid "FILTER_USERS_BY_TAG" msgstr "Suodata listaa merkinnän perusteella" +#: course/templates/course/staff/_assessment_panel.html +msgid "FILTER_SUBMISSIONS_BY_TAG" +msgstr "Suodata listaa merkinnän perusteella" + #: course/templates/course/staff/participants.html msgid "FILTER_USERS_BY_STATUS" msgstr "Suodata listaa tilan perusteella" @@ -1457,6 +1461,14 @@ msgstr "Tila" msgid "TAGS" msgstr "Merkinnät" +#: edit_course/templates/edit_course/submissiontag_add.html +#: edit_course/templates/edit_course/submissiontag_delete.html +#: edit_course/templates/edit_course/submissiontag_edit.html +#: edit_course/templates/edit_course/submissiontag_list.html +#: edit_course/templates/edit_course/submissiontagging_add.html +msgid "SUBMISSION_TAGS" +msgstr "Palautusmerkinnät" + #: course/templates/course/staff/participants.html msgid "ENROLLMENT_REMOVE_MODAL_REMOVE_TITLE" msgstr "Poista opiskelija" @@ -2830,6 +2842,10 @@ msgstr "" msgid "ADD_NEW_USER_TAG" msgstr "Lisää käyttäjämerkintä" +#: edit_course/templates/edit_course/submissiontag_add.html +msgid "ADD_NEW_SUBMISSION_TAG" +msgstr "Lisää palautusmerkintä" + #: edit_course/templates/edit_course/usertag_add.html msgid "ADD_STUDENT_TAG" msgstr "Lisää opiskelijamerkintä" @@ -2842,6 +2858,14 @@ msgstr "Poista käyttäjämerkintä" msgid "REMOVE_USER_TAG_CONFIRMATION" msgstr "Vahvista käyttäjämerkinnän poisto" +#: edit_course/templates/edit_course/submissiontag_delete.html +msgid "REMOVE_SUBMISSION_TAG_CONFIRMATION" +msgstr "Poista palautusmerkintä" + +#: edit_course/templates/edit_course/submissiontag_delete.html +msgid "REMOVE_SUBMISSION_TAG_CONFIRMATION_ALERT" +msgstr "Vahvista palautusmerkinnän poisto" + #: edit_course/templates/edit_course/usertag_delete.html msgid "REMOVE_USER_TAG_CONFIRMATION_ALERT" msgstr "Haluatko varmasti poistaa seuraavan käyttäjämerkinnän?" @@ -2850,6 +2874,10 @@ msgstr "Haluatko varmasti poistaa seuraavan käyttäjämerkinnän?" msgid "EDIT_STUDENT_TAG" msgstr "Muokkaa opiskelijamerkintää" +#: edit_course/templates/edit_course/submissiontag_edit.html +msgid "EDIT_SUBMISSION_TAG" +msgstr "Muokkaa palautusmerkintää" + #: edit_course/templates/edit_course/usertag_list.html msgid "ADD_NEW_STUDENT_TAG" msgstr "Lisää uusi opiskelijamerkintä"