diff --git a/course/enrollment.py b/course/enrollment.py index c0c230247..7dc910e7a 100644 --- a/course/enrollment.py +++ b/course/enrollment.py @@ -1096,4 +1096,100 @@ def edit_participation( # }}} + +# {{{ edit_participation_tag + +class ParticipationTagForm(StyledModelForm): + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.helper.add_input(Submit("submit", _("Save"))) + + class Meta: + model = ParticipationTag + fields = ("name", "shown_to_participant") + + +@course_view +def edit_participation_tag( + pctx: CoursePageContext, tag_id: int) -> http.HttpResponse: + if not pctx.has_permission(PPerm.edit_participation): + raise PermissionDenied() + + request = pctx.request + + num_tag_id = int(tag_id) + + if num_tag_id == -1: + tag = ParticipationTag(course=pctx.course) + add_new = True + else: + tag = get_object_or_404(ParticipationTag, id=num_tag_id) + add_new = False + + if tag.course.id != pctx.course.id: + raise SuspiciousOperation( + "may not edit participation tag in different course") + + if request.method == "POST": + form = ParticipationTagForm(request.POST, instance=tag) + if form.is_valid(): + tag = form.save(commit=False) + tag.course = pctx.course + tag.save() + messages.add_message(request, messages.SUCCESS, + _("Changes saved.")) + return redirect( + "relate-list_participation_tags", + pctx.course.identifier) + else: + form = ParticipationTagForm(instance=tag) + + return render_course_page(pctx, "course/generic-course-form.html", { + "form_description": ( + _("Add Participation Tag") if add_new + else _("Edit Participation Tag")), + "form": form, + }) + + +@course_view +def delete_participation_tag( + pctx: CoursePageContext, tag_id: int) -> http.HttpResponse: + if not pctx.has_permission(PPerm.edit_participation): + raise PermissionDenied() + + request = pctx.request + + tag = get_object_or_404( + ParticipationTag, id=int(tag_id), course=pctx.course) + + if request.method == "POST": + tag.delete() + messages.add_message(request, messages.SUCCESS, + _("Participation tag deleted.")) + return redirect( + "relate-list_participation_tags", + pctx.course.identifier) + + return render_course_page( + pctx, "course/confirm-delete-participation-tag.html", { + "participation_tag": tag, + }) + + +@course_view +def list_participation_tags(pctx: CoursePageContext) -> http.HttpResponse: + if not pctx.has_permission(PPerm.edit_participation): + raise PermissionDenied() + + tags = ParticipationTag.objects.filter(course=pctx.course).order_by("name") + + return render_course_page( + pctx, "course/participation-tags.html", { + "participation_tags": tags, + }) + +# }}} + # vim: foldmethod=marker diff --git a/course/templates/course/confirm-delete-participation-tag.html b/course/templates/course/confirm-delete-participation-tag.html new file mode 100644 index 000000000..7043cdd7c --- /dev/null +++ b/course/templates/course/confirm-delete-participation-tag.html @@ -0,0 +1,18 @@ +{% extends "course/course-base.html" %} +{% load i18n %} + +{% block title %} + {% trans "Delete Participation Tag" %} - {{ relate_site_name }} +{% endblock %} + +{% block content %} +

{% trans "Delete Participation Tag" %}

+ +

{% blocktrans with name=participation_tag.name %}Are you sure you want to delete the participation tag "{{ name }}"?{% endblocktrans %}

+ +
+ {% csrf_token %} + {% trans "Cancel" %} + +
+{% endblock %} diff --git a/course/templates/course/course-base.html b/course/templates/course/course-base.html index 116f182d9..70371eea5 100644 --- a/course/templates/course/course-base.html +++ b/course/templates/course/course-base.html @@ -144,7 +144,7 @@ {% endif %} - {% if pperm.query_participation or pperm.manage_instant_flow_requests or pperm.preapprove_participation %} + {% if pperm.query_participation or pperm.manage_instant_flow_requests or pperm.preapprove_participation or pperm.edit_participation %} {% if not pperm.view_participant_masked_profile %}
  • {% trans "Query participations" context "menu item" %}
  • {% endif %} + {% if pperm.edit_participation %} +
  • {% trans "Manage participation tags" context "menu item" %}
  • + {% endif %} {% if pperm.manage_instant_flow_requests %} diff --git a/course/templates/course/participation-tags.html b/course/templates/course/participation-tags.html new file mode 100644 index 000000000..7083f473a --- /dev/null +++ b/course/templates/course/participation-tags.html @@ -0,0 +1,42 @@ +{% extends "course/course-base.html" %} +{% load i18n %} + +{% block title %} + {% trans "Participation Tags" %} - {{ relate_site_name }} +{% endblock %} + +{% block content %} +

    {% trans "Participation Tags" %}

    + +

    + + {% trans "Add participation tag" %} + +

    + + {% if participation_tags %} + + + + + + + + + + {% for tag in participation_tags %} + + + + + + {% endfor %} + +
    {% trans "Name" %}{% trans "Shown to participant" %}{% trans "Actions" %}
    {{ tag.name }}{{ tag.shown_to_participant|yesno }} + {% trans "Edit" %} + {% trans "Delete" %} +
    + {% else %} +

    {% trans "No participation tags defined." %}

    + {% endif %} +{% endblock %} diff --git a/relate/urls.py b/relate/urls.py index 390285e28..4436ec364 100644 --- a/relate/urls.py +++ b/relate/urls.py @@ -302,6 +302,26 @@ "/$", course.enrollment.edit_participation, name="relate-edit_participation"), + re_path(r"^course" + "/" + COURSE_ID_REGEX + + "/participation-tags" + "/$", + course.enrollment.list_participation_tags, + name="relate-list_participation_tags"), + re_path(r"^course" + "/" + COURSE_ID_REGEX + + "/edit-participation-tag" + "/(?P[-0-9]+)" + "/$", + course.enrollment.edit_participation_tag, + name="relate-edit_participation_tag"), + re_path(r"^course" + "/" + COURSE_ID_REGEX + + "/delete-participation-tag" + "/(?P[0-9]+)" + "/$", + course.enrollment.delete_participation_tag, + name="relate-delete_participation_tag"), # }}} diff --git a/tests/test_enrollment.py b/tests/test_enrollment.py index b1b214c1e..54a8f5f9d 100644 --- a/tests/test_enrollment.py +++ b/tests/test_enrollment.py @@ -1897,4 +1897,116 @@ def test_drop(self): # }}} +# {{{ participation tag CRUD tests + +class ParticipationTagCRUDTest(MockAddMessageMixing, SingleCourseTestMixin, + TestCase): + + @classmethod + def setUpTestData(cls): + super().setUpTestData() + cls.tag = factories.ParticipationTagFactory( + course=cls.course, name="mytag") + + @classmethod + def get_list_url(cls): + return reverse("relate-list_participation_tags", + args=[cls.course.identifier]) + + @classmethod + def get_edit_url(cls, tag_id): + return reverse("relate-edit_participation_tag", + args=[cls.course.identifier, tag_id]) + + @classmethod + def get_delete_url(cls, tag_id): + return reverse("relate-delete_participation_tag", + args=[cls.course.identifier, tag_id]) + + def test_list_permission_denied(self): + with self.temporarily_switch_to_user(self.student_participation.user): + resp = self.client.get(self.get_list_url()) + self.assertEqual(resp.status_code, 403) + + def test_list_success(self): + with self.temporarily_switch_to_user(self.instructor_participation.user): + resp = self.client.get(self.get_list_url()) + self.assertEqual(resp.status_code, 200) + self.assertIn(b"mytag", resp.content) + + def test_edit_get_new(self): + with self.temporarily_switch_to_user(self.instructor_participation.user): + resp = self.client.get(self.get_edit_url(-1)) + self.assertEqual(resp.status_code, 200) + + def test_edit_get_existing(self): + with self.temporarily_switch_to_user(self.instructor_participation.user): + resp = self.client.get(self.get_edit_url(self.tag.pk)) + self.assertEqual(resp.status_code, 200) + + def test_edit_post_new(self): + with self.temporarily_switch_to_user(self.instructor_participation.user): + resp = self.client.post(self.get_edit_url(-1), { + "name": "newtag", + "shown_to_participant": False, + "submit": "", + }) + self.assertRedirects(resp, self.get_list_url()) + from course.models import ParticipationTag + self.assertTrue(ParticipationTag.objects.filter( + course=self.course, name="newtag").exists()) + + def test_edit_post_existing(self): + from tests.factories import ParticipationTagFactory + tag = ParticipationTagFactory(course=self.course, name="edittag") + with self.temporarily_switch_to_user(self.instructor_participation.user): + resp = self.client.post(self.get_edit_url(tag.pk), { + "name": "edittagrenamed", + "shown_to_participant": True, + "submit": "", + }) + self.assertRedirects(resp, self.get_list_url()) + tag.refresh_from_db() + self.assertEqual(tag.name, "edittagrenamed") + + def test_edit_permission_denied(self): + with self.temporarily_switch_to_user(self.student_participation.user): + resp = self.client.get(self.get_edit_url(-1)) + self.assertEqual(resp.status_code, 403) + + def test_edit_wrong_course(self): + other_course = factories.CourseFactory( + identifier="another-course", git_source="other_git_source") + other_tag = factories.ParticipationTagFactory( + course=other_course, name="othertag") + with self.temporarily_switch_to_user(self.instructor_participation.user): + resp = self.client.get(self.get_edit_url(other_tag.pk)) + self.assertEqual(resp.status_code, 400) + + def test_delete_get(self): + from tests.factories import ParticipationTagFactory + tag = ParticipationTagFactory(course=self.course, name="deltag") + with self.temporarily_switch_to_user(self.instructor_participation.user): + resp = self.client.get(self.get_delete_url(tag.pk)) + self.assertEqual(resp.status_code, 200) + self.assertIn(b"deltag", resp.content) + + def test_delete_post(self): + from course.models import ParticipationTag + from tests.factories import ParticipationTagFactory + tag = ParticipationTagFactory(course=self.course, name="tagToDelete") + tag_id = tag.pk + with self.temporarily_switch_to_user(self.instructor_participation.user): + resp = self.client.post(self.get_delete_url(tag_id), {}) + self.assertRedirects(resp, self.get_list_url()) + self.assertFalse(ParticipationTag.objects.filter(pk=tag_id).exists()) + + def test_delete_permission_denied(self): + with self.temporarily_switch_to_user(self.student_participation.user): + resp = self.client.get(self.get_delete_url(self.tag.pk)) + self.assertEqual(resp.status_code, 403) + +# }}} + + # vim: foldmethod=marker