diff --git a/course/constants.py b/course/constants.py index dbe177f78..88cd4d458 100644 --- a/course/constants.py +++ b/course/constants.py @@ -133,6 +133,8 @@ class participation_permission: # noqa query_participation = "query_participation" edit_participation = "edit_participation" preapprove_participation = "preapprove_participation" + edit_participation_role = "edit_participation_role" + edit_participation_tag = "edit_participation_tag" manage_instant_flow_requests = "manage_instant_flow_requests" @@ -248,6 +250,12 @@ class participation_permission: # noqa pgettext_lazy("Participation permission", "Edit participation")), (participation_permission.preapprove_participation, pgettext_lazy("Participation permission", "Preapprove participation")), + (participation_permission.edit_participation_role, + pgettext_lazy( + "Participation permission", "Edit participation role")), + (participation_permission.edit_participation_tag, + pgettext_lazy( + "Participation permission", "Edit participation tag")), (participation_permission.manage_instant_flow_requests, pgettext_lazy("Participation permission", diff --git a/course/enrollment.py b/course/enrollment.py index c04c7bc0a..35db41237 100644 --- a/course/enrollment.py +++ b/course/enrollment.py @@ -1110,4 +1110,194 @@ def edit_participation(pctx, participation_id): # }}} + +# {{{ edit_participation_tag + +class EditParticipationTagForm(StyledModelForm): + def __init__(self, add_new, *args, **kwargs): + # type: (bool, *Any, **Any) -> None + super(EditParticipationTagForm, self).__init__(*args, **kwargs) + + if add_new: + self.helper.add_input( + Submit("submit", _("Add"), css_class="btn-success")) + else: + self.helper.add_input( + Submit("submit", _("Update"), css_class="btn-success")) + self.helper.add_input( + Submit("delete", _("Delete"), css_class="btn-danger")) + + class Meta: + model = ParticipationTag + exclude = ("course",) + + +@course_view +def view_participation_tag_list(pctx): + if not pctx.has_permission(pperm.view_gradebook): + raise PermissionDenied(_("may not edit participation tags")) + + participation_tags = list(ParticipationTag.objects.filter(course=pctx.course)) + + return render_course_page(pctx, "course/participation-tag-list.html", { + "participation_tags": participation_tags, + }) + + +@course_view +def edit_participation_tag(pctx, ptag_id): + # type: (CoursePageContext, int) -> http.HttpResponse + if not pctx.has_permission(pperm.edit_participation_tag): + raise PermissionDenied() + + request = pctx.request + + num_ptag_id = int(ptag_id) + + if num_ptag_id == -1: + ptag = ParticipationTag(course=pctx.course) + add_new = True + else: + ptag = get_object_or_404(ParticipationTag, id=num_ptag_id) + add_new = False + + if ptag.course.id != pctx.course.id: + raise SuspiciousOperation( + "may not edit participation tag in different course") + + if request.method == "POST": + form = EditParticipationTagForm(add_new, request.POST, instance=ptag) + try: + if form.is_valid(): + if "submit" in request.POST or "update" in request.POST: + # Ref: https://stackoverflow.com/q/21458387/3437454 + with transaction.atomic(): + form.save() + + if "submit" in request.POST: + assert add_new + msg = _("New participation tag saved.") + else: + msg = _("Changes saved.") + elif "delete" in request.POST: + ptag.delete() + msg = (_("successfully deleted participation tag '%(tag)s'.") + % {"tag": ptag.name}) + else: + raise SuspiciousOperation(_("invalid operation")) + + messages.add_message(request, messages.SUCCESS, msg) + return redirect( + "relate-view_participation_tags", pctx.course.identifier) + except IntegrityError: + messages.add_message( + request, messages.ERROR, + _("A participation tag with that name already exists.")) + + else: + form = EditParticipationTagForm(add_new, instance=ptag) + + return render_course_page(pctx, "course/generic-course-form.html", { + "form_description": _("Edit Participation Tag"), + "form": form, + }) + +# }}} + + +# {{{ edit_participation_role + +class EditParticipationRoleForm(StyledModelForm): + def __init__(self, add_new, *args, **kwargs): + # type: (bool, *Any, **Any) -> None + super(EditParticipationRoleForm, self).__init__(*args, **kwargs) + + if add_new: + self.helper.add_input( + Submit("submit", _("Add"), css_class="btn-success")) + else: + self.helper.add_input( + Submit("submit", _("Update"), css_class="btn-success")) + self.helper.add_input( + Submit("delete", _("Delete"), css_class="btn-danger")) + + class Meta: + model = ParticipationRole + exclude = ("course",) + + +@course_view +def view_participation_role_list(pctx): + if not pctx.has_permission(pperm.view_gradebook): + raise PermissionDenied(_("may not edit participation tags")) + + participation_roles = list(ParticipationRole.objects.filter(course=pctx.course)) + + return render_course_page(pctx, "course/participation-role-list.html", { + "participation_roles": participation_roles, + }) + + +@course_view +def edit_participation_role(pctx, prole_id): + # type: (CoursePageContext, int) -> http.HttpResponse + if not pctx.has_permission(pperm.edit_participation_role): + raise PermissionDenied() + + request = pctx.request + + num_prole_id = int(prole_id) + + if num_prole_id == -1: + prole = ParticipationRole(course=pctx.course) + add_new = True + else: + prole = get_object_or_404(ParticipationRole, id=num_prole_id) + add_new = False + + if prole.course.id != pctx.course.id: + raise SuspiciousOperation( + "may not edit participation role in different course") + + if request.method == "POST": + form = EditParticipationRoleForm(add_new, request.POST, instance=prole) + try: + if form.is_valid(): + if "submit" in request.POST or "update" in request.POST: + # Ref: https://stackoverflow.com/q/21458387/3437454 + with transaction.atomic(): + form.save() + + if "submit" in request.POST: + assert add_new + msg = _("New participation role saved.") + else: + msg = _("Changes saved.") + elif "delete" in request.POST: + prole.delete() + msg = ( + _("successfully deleted participation role '%(role)s'.") + % {"role": prole.identifier}) + else: + raise SuspiciousOperation(_("invalid operation")) + + messages.add_message(request, messages.SUCCESS, msg) + return redirect( + "relate-view_participation_roles", pctx.course.identifier) + except IntegrityError: + messages.add_message( + request, messages.ERROR, + _("A participation role with that identifier already exists.")) + + else: + form = EditParticipationRoleForm(add_new, instance=prole) + + return render_course_page(pctx, "course/generic-course-form.html", { + "form_description": _("Edit Participation Role"), + "form": form, + }) + +# }}} + + # vim: foldmethod=marker diff --git a/course/migrations/0115_added_edit_prole_and_ptag_permission.py b/course/migrations/0115_added_edit_prole_and_ptag_permission.py new file mode 100644 index 000000000..5569ac17e --- /dev/null +++ b/course/migrations/0115_added_edit_prole_and_ptag_permission.py @@ -0,0 +1,49 @@ +# Generated by Django 3.0.8 on 2020-10-26 08:48 + +from __future__ import unicode_literals + +from django.db import migrations, models + + +def add_edit_prole_and_ptag_permission(apps, schema_editor): + from course.constants import participation_permission as pperm + + ParticipationRolePermission = apps.get_model("course", "ParticipationRolePermission") # noqa + + roles_pks = ( + ParticipationRolePermission.objects.filter( + permission=pperm.preapprove_participation) + .values_list("role", flat=True) + ) + + if roles_pks.count(): + for pk in roles_pks: + ParticipationRolePermission.objects.get_or_create( + role_id=pk, + permission=pperm.edit_participation_role + ) + ParticipationRolePermission.objects.get_or_create( + role_id=pk, + permission=pperm.edit_participation_tag + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ('course', '0114_alter_helptext_for_ptag_and_prole_fix_typo'), + ] + + operations = [ + migrations.AlterField( + model_name='participationpermission', + name='permission', + field=models.CharField(choices=[('edit_course', 'Edit course'), ('use_admin_interface', 'Use admin interface'), ('manage_authentication_tokens', 'Manage authentication tokens'), ('impersonate_role', 'Impersonate role'), ('set_fake_time', 'Set fake time'), ('set_pretend_facility', 'Pretend to be in facility'), ('edit_course_permissions', 'Edit course permissions'), ('view_hidden_course_page', 'View hidden course page'), ('view_calendar', 'View calendar'), ('send_instant_message', 'Send instant message'), ('access_files_for', 'Access files for'), ('included_in_grade_statistics', 'Included in grade statistics'), ('skip_during_manual_grading', 'Skip during manual grading'), ('edit_exam', 'Edit exam'), ('issue_exam_ticket', 'Issue exam ticket'), ('batch_issue_exam_ticket', 'Batch issue exam ticket'), ('view_participant_masked_profile', "View participants' masked profile only"), ('view_flow_sessions_from_role', 'View flow sessions from role'), ('view_gradebook', 'View gradebook'), ('edit_grading_opportunity', 'Edit grading opportunity'), ('assign_grade', 'Assign grade'), ('view_grader_stats', 'View grader stats'), ('batch_import_grade', 'Batch-import grades'), ('batch_export_grade', 'Batch-export grades'), ('batch_download_submission', 'Batch-download submissions'), ('impose_flow_session_deadline', 'Impose flow session deadline'), ('batch_impose_flow_session_deadline', 'Batch-impose flow session deadline'), ('end_flow_session', 'End flow session'), ('batch_end_flow_session', 'Batch-end flow sessions'), ('regrade_flow_session', 'Regrade flow session'), ('batch_regrade_flow_session', 'Batch-regrade flow sessions'), ('recalculate_flow_session_grade', 'Recalculate flow session grade'), ('batch_recalculate_flow_session_grade', 'Batch-recalculate flow sesssion grades'), ('reopen_flow_session', 'Reopen flow session'), ('grant_exception', 'Grant exception'), ('view_analytics', 'View analytics'), ('preview_content', 'Preview content'), ('update_content', 'Update content'), ('use_git_endpoint', 'Use direct git endpoint'), ('use_markup_sandbox', 'Use markup sandbox'), ('use_page_sandbox', 'Use page sandbox'), ('test_flow', 'Test flow'), ('edit_events', 'Edit events'), ('query_participation', 'Query participation'), ('edit_participation', 'Edit participation'), ('preapprove_participation', 'Preapprove participation'), ('edit_participation_role', 'Edit participation role'), ('edit_participation_tag', 'Edit participation tag'), ('manage_instant_flow_requests', 'Manage instant flow requests')], db_index=True, max_length=200, verbose_name='Permission'), + ), + migrations.AlterField( + model_name='participationrolepermission', + name='permission', + field=models.CharField(choices=[('edit_course', 'Edit course'), ('use_admin_interface', 'Use admin interface'), ('manage_authentication_tokens', 'Manage authentication tokens'), ('impersonate_role', 'Impersonate role'), ('set_fake_time', 'Set fake time'), ('set_pretend_facility', 'Pretend to be in facility'), ('edit_course_permissions', 'Edit course permissions'), ('view_hidden_course_page', 'View hidden course page'), ('view_calendar', 'View calendar'), ('send_instant_message', 'Send instant message'), ('access_files_for', 'Access files for'), ('included_in_grade_statistics', 'Included in grade statistics'), ('skip_during_manual_grading', 'Skip during manual grading'), ('edit_exam', 'Edit exam'), ('issue_exam_ticket', 'Issue exam ticket'), ('batch_issue_exam_ticket', 'Batch issue exam ticket'), ('view_participant_masked_profile', "View participants' masked profile only"), ('view_flow_sessions_from_role', 'View flow sessions from role'), ('view_gradebook', 'View gradebook'), ('edit_grading_opportunity', 'Edit grading opportunity'), ('assign_grade', 'Assign grade'), ('view_grader_stats', 'View grader stats'), ('batch_import_grade', 'Batch-import grades'), ('batch_export_grade', 'Batch-export grades'), ('batch_download_submission', 'Batch-download submissions'), ('impose_flow_session_deadline', 'Impose flow session deadline'), ('batch_impose_flow_session_deadline', 'Batch-impose flow session deadline'), ('end_flow_session', 'End flow session'), ('batch_end_flow_session', 'Batch-end flow sessions'), ('regrade_flow_session', 'Regrade flow session'), ('batch_regrade_flow_session', 'Batch-regrade flow sessions'), ('recalculate_flow_session_grade', 'Recalculate flow session grade'), ('batch_recalculate_flow_session_grade', 'Batch-recalculate flow sesssion grades'), ('reopen_flow_session', 'Reopen flow session'), ('grant_exception', 'Grant exception'), ('view_analytics', 'View analytics'), ('preview_content', 'Preview content'), ('update_content', 'Update content'), ('use_git_endpoint', 'Use direct git endpoint'), ('use_markup_sandbox', 'Use markup sandbox'), ('use_page_sandbox', 'Use page sandbox'), ('test_flow', 'Test flow'), ('edit_events', 'Edit events'), ('query_participation', 'Query participation'), ('edit_participation', 'Edit participation'), ('preapprove_participation', 'Preapprove participation'), ('edit_participation_role', 'Edit participation role'), ('edit_participation_tag', 'Edit participation tag'), ('manage_instant_flow_requests', 'Manage instant flow requests')], db_index=True, max_length=200, verbose_name='Permission'), + ), + migrations.RunPython(add_edit_prole_and_ptag_permission), + ] diff --git a/course/models.py b/course/models.py index f437457c4..f13e89096 100644 --- a/course/models.py +++ b/course/models.py @@ -730,6 +730,8 @@ def add_instructor_permissions(role): rpm(role=role, permission=pp.edit_events).save() rpm(role=role, permission=pp.manage_instant_flow_requests).save() rpm(role=role, permission=pp.preapprove_participation).save() + rpm(role=role, permission=pp.edit_participation_tag).save() + rpm(role=role, permission=pp.edit_participation_role).save() add_teaching_assistant_permissions(role) diff --git a/course/templates/course/course-base.html b/course/templates/course/course-base.html index 4d206bbfc..d9db3f0d4 100644 --- a/course/templates/course/course-base.html +++ b/course/templates/course/course-base.html @@ -48,18 +48,26 @@ {% endif %} - {% if pperm.view_gradebook %} + {% if pperm.view_gradebook or pperm.edit_participation %}