Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
96 changes: 96 additions & 0 deletions course/enrollment.py
Original file line number Diff line number Diff line change
Expand Up @@ -1096,4 +1096,100 @@

# }}}


# {{{ edit_participation_tag

class ParticipationTagForm(StyledModelForm):

def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)

Check warning on line 1105 in course/enrollment.py

View workflow job for this annotation

GitHub Actions / Lint and typecheck Python

Type of "__init__" is partially unknown   Type of "__init__" is "(...) -> None" (reportUnknownMemberType)
self.helper.add_input(Submit("submit", _("Save")))

Check warning on line 1106 in course/enrollment.py

View workflow job for this annotation

GitHub Actions / Lint and typecheck Python

Type of "add_input" is partially unknown   Type of "add_input" is "(input_object: Unknown) -> None" (reportUnknownMemberType)

class Meta:
model = ParticipationTag

Check warning on line 1109 in course/enrollment.py

View workflow job for this annotation

GitHub Actions / Lint and typecheck Python

Type annotation for attribute `model` is required because this class is not decorated with `@final` (reportUnannotatedClassAttribute)
fields = ("name", "shown_to_participant")

Check warning on line 1110 in course/enrollment.py

View workflow job for this annotation

GitHub Actions / Lint and typecheck Python

Type annotation for attribute `fields` is required because this class is not decorated with `@final` (reportUnannotatedClassAttribute)


@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
18 changes: 18 additions & 0 deletions course/templates/course/confirm-delete-participation-tag.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{% extends "course/course-base.html" %}
{% load i18n %}

{% block title %}
{% trans "Delete Participation Tag" %} - {{ relate_site_name }}
{% endblock %}

{% block content %}
<h1>{% trans "Delete Participation Tag" %}</h1>

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

<form method="post">
{% csrf_token %}
<a href="{% url "relate-list_participation_tags" course.identifier %}" class="btn btn-secondary">{% trans "Cancel" %}</a>
<button type="submit" class="btn btn-danger">{% trans "Delete" %}</button>
</form>
{% endblock %}
5 changes: 4 additions & 1 deletion course/templates/course/course-base.html
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@
</li>
{% 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 %}
<li class="nav-item relate-dropdown-menu">
<a href="#" id="dropdownInstructorMenu" data-bs-toggle="dropdown" aria-expanded="false">
Expand All @@ -158,6 +158,9 @@
{% if pperm.query_participation %}
<li><a class="dropdown-item" href="{% url "relate-query_participations" course.identifier %}">{% trans "Query participations" context "menu item" %}</a></li>
{% endif %}
{% if pperm.edit_participation %}
<li><a class="dropdown-item" href="{% url "relate-list_participation_tags" course.identifier %}">{% trans "Manage participation tags" context "menu item" %}</a></li>
{% endif %}

{% if pperm.manage_instant_flow_requests %}
<li class="dropdown-divider"></li>
Expand Down
42 changes: 42 additions & 0 deletions course/templates/course/participation-tags.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{% extends "course/course-base.html" %}
{% load i18n %}

{% block title %}
{% trans "Participation Tags" %} - {{ relate_site_name }}
{% endblock %}

{% block content %}
<h1>{% trans "Participation Tags" %}</h1>

<p>
<a href="{% url "relate-edit_participation_tag" course.identifier -1 %}" class="btn btn-primary">
{% trans "Add participation tag" %}
</a>
</p>

{% if participation_tags %}
<table class="table table-striped">
<thead>
<tr>
<th>{% trans "Name" %}</th>
<th>{% trans "Shown to participant" %}</th>
<th>{% trans "Actions" %}</th>
</tr>
</thead>
<tbody>
{% for tag in participation_tags %}
<tr>
<td>{{ tag.name }}</td>
<td>{{ tag.shown_to_participant|yesno }}</td>
<td>
<a href="{% url "relate-edit_participation_tag" course.identifier tag.id %}" class="btn btn-outline-secondary btn-sm">{% trans "Edit" %}</a>
<a href="{% url "relate-delete_participation_tag" course.identifier tag.id %}" class="btn btn-outline-danger btn-sm">{% trans "Delete" %}</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>{% trans "No participation tags defined." %}</p>
{% endif %}
{% endblock %}
20 changes: 20 additions & 0 deletions relate/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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<tag_id>[-0-9]+)"
"/$",
course.enrollment.edit_participation_tag,
name="relate-edit_participation_tag"),
re_path(r"^course"
"/" + COURSE_ID_REGEX
+ "/delete-participation-tag"
"/(?P<tag_id>[0-9]+)"
"/$",
course.enrollment.delete_participation_tag,
name="relate-delete_participation_tag"),

# }}}

Expand Down
112 changes: 112 additions & 0 deletions tests/test_enrollment.py
Original file line number Diff line number Diff line change
Expand Up @@ -1897,4 +1897,116 @@
# }}}


# {{{ 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):

Check warning on line 1917 in tests/test_enrollment.py

View workflow job for this annotation

GitHub Actions / Lint and typecheck Python

Type annotation is missing for parameter "tag_id" (reportMissingParameterType)
return reverse("relate-edit_participation_tag",
args=[cls.course.identifier, tag_id])

@classmethod
def get_delete_url(cls, tag_id):

Check warning on line 1922 in tests/test_enrollment.py

View workflow job for this annotation

GitHub Actions / Lint and typecheck Python

Type annotation is missing for parameter "tag_id" (reportMissingParameterType)
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
Loading