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 %}
{% trans "Grading" context "menu item" %}
{% endif %}
- {% if pperm.query_participation or pperm.manage_instant_flow_requests or pperm.preapprove_participation %}
+ {% if pperm.manage_instant_flow_requests or pperm.preapprove_participation %}
{% if not pperm.view_participant_masked_profile %}
{% trans "Instructor" context "menu item" %}
@@ -148,10 +156,6 @@
{% if pperm.preapprove_participation %}
{% trans "Preapprove enrollments" context "menu item" %}
{% endif %}
- {% if pperm.query_participation %}
- {% trans "Query participations" context "menu item" %}
- {% endif %}
-
{% if pperm.manage_instant_flow_requests %}
diff --git a/course/templates/course/gradebook-participant-list.html b/course/templates/course/gradebook-participant-list.html
index 4c0eb6273..0d268dd22 100644
--- a/course/templates/course/gradebook-participant-list.html
+++ b/course/templates/course/gradebook-participant-list.html
@@ -12,7 +12,7 @@
{% endblock %}
{% block content %}
- {% trans "List of Participants" %}
+ {% trans "List of participants" %}
{% trans "Add participant" %}
diff --git a/course/templates/course/participation-role-list.html b/course/templates/course/participation-role-list.html
new file mode 100644
index 000000000..fbd5a848f
--- /dev/null
+++ b/course/templates/course/participation-role-list.html
@@ -0,0 +1,21 @@
+{% extends "course/course-base.html" %}
+{% load i18n %}
+
+{% load static %}
+
+{% block title %}
+ {% trans "Participation Roles" %} - {{ relate_site_name }}
+{% endblock %}
+
+{% block header_extra %}
+ {% include "datatables-header.html" %}
+{% endblock %}
+
+{% block content %}
+ {% trans "Participation roles" %}
+
+ {% trans "Add participation role" %}
+
+ {% include "course/participation-role-table.html" with participation_roles=participation_roles %}
+
+{% endblock %}
diff --git a/course/templates/course/participation-role-table.html b/course/templates/course/participation-role-table.html
new file mode 100644
index 000000000..204f903b4
--- /dev/null
+++ b/course/templates/course/participation-role-table.html
@@ -0,0 +1,57 @@
+{% load i18n %}
+
+
+
+ | {% trans "Role identifier" %} |
+ {% trans "Role name" %} |
+ {% trans "Default for new participants" %} |
+ {% trans "Default for unenrolled" %} |
+ {% if pperm.edit_participation_role %}
+ {% trans "Actions" %} |
+ {% endif %}
+
+
+ {% for role in participation_roles %}
+
+ | {{ role.identifier }} |
+ {{ role.name }} |
+
+ {% if role.is_default_for_new_participants %}
+
+ {% else %}
+
+ {% endif %}
+ |
+
+ {% if role.is_default_for_unenrolled %}
+
+ {% else %}
+
+ {% endif %}
+ |
+ {% if pperm.edit_participation_role %}
+
+ {% trans "Edit" %}
+ |
+ {% endif %}
+
+ {% endfor %}
+
+
+
+
+{% block participation_role_table_bottom_js %}
+ {% load static %}
+ {% get_current_js_lang_name as LANG %}
+
+{% endblock %}
diff --git a/course/templates/course/participation-tag-list.html b/course/templates/course/participation-tag-list.html
new file mode 100644
index 000000000..31ab74761
--- /dev/null
+++ b/course/templates/course/participation-tag-list.html
@@ -0,0 +1,21 @@
+{% extends "course/course-base.html" %}
+{% load i18n %}
+
+{% load static %}
+
+{% block title %}
+ {% trans "Participant Tags" %} - {{ relate_site_name }}
+{% endblock %}
+
+{% block header_extra %}
+ {% include "datatables-header.html" %}
+{% endblock %}
+
+{% block content %}
+ {% trans "Participant Tags" %}
+
+ {% trans "Add participation tag" %}
+
+ {% include "course/participation-tag-table.html" with participation_tags=participation_tags %}
+
+{% endblock %}
diff --git a/course/templates/course/participation-tag-table.html b/course/templates/course/participation-tag-table.html
new file mode 100644
index 000000000..ad0b8be8b
--- /dev/null
+++ b/course/templates/course/participation-tag-table.html
@@ -0,0 +1,48 @@
+{% load i18n %}
+
+
+
+
+{% block participation_tag_table_bottom_js %}
+ {% load static %}
+ {% get_current_js_lang_name as LANG %}
+
+
+{% endblock %}
diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po
index 0abebd22c..91b5c57cf 100644
--- a/locale/de/LC_MESSAGES/django.po
+++ b/locale/de/LC_MESSAGES/django.po
@@ -2156,7 +2156,7 @@ msgid "Role name"
msgstr "Kurs-Modul"
#, fuzzy
-#| msgid "List of Participants"
+#| msgid "List of participants"
msgid "Is default role for new participants"
msgstr "Teilnehmerliste"
@@ -3367,7 +3367,7 @@ msgstr "Analytics-Überblick"
msgid "Grade Book"
msgstr "Notenbuch"
-msgid "List of Participants"
+msgid "List of participants"
msgstr "Teilnehmerliste"
msgid "List of Grading Opportunities"
diff --git a/locale/zh_Hans/LC_MESSAGES/django.po b/locale/zh_Hans/LC_MESSAGES/django.po
index 8db2d1672..a4cf46b37 100644
--- a/locale/zh_Hans/LC_MESSAGES/django.po
+++ b/locale/zh_Hans/LC_MESSAGES/django.po
@@ -1,5 +1,5 @@
# Translation for Django RELATE package (https://github.com/inducer/relate/)
-# Language: zh_hans
+# Language: zh_hans
# Copyright (C) 2015 Andreas Kloeckner, Dong Zhuang
# This file is distributed under the same license as the RELATE package.
# Dong Zhuang , 2015.
@@ -9,7 +9,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2018-04-20 16:43+0800\n"
+"POT-Creation-Date: 2020-10-23 01:13+0800\n"
"Last-Translator: Dong Zhuang \n"
"Language-Team: Dong Zhuang \n"
"Language: \n"
@@ -236,8 +236,14 @@ msgstr "已经模拟了某个用户. "
msgid "Impersonate user"
msgstr "用户模拟"
-msgid "Stop impersonating"
-msgstr "停止模拟用户"
+msgid "only AJAX POST is allowed"
+msgstr "只允许AJAX POST操作"
+
+msgid "odd POST parameters"
+msgstr "怪异的POST参数设置"
+
+msgid "may not stop impersonating"
+msgstr "无法停止模拟用户"
msgid "Not currently impersonating anyone."
msgstr "当前未模拟任何用户. "
@@ -245,9 +251,6 @@ msgstr "当前未模拟任何用户. "
msgid "No longer impersonating anyone."
msgstr "未模拟任何用户. "
-msgid "Stop impersonating user"
-msgstr "停止模拟用户"
-
msgid "Sign in"
msgstr "登录"
@@ -389,6 +392,9 @@ msgstr "用户信息"
msgid "You've already signed out."
msgstr "您已经登出网站. "
+msgid "Relate direct git access for {}"
+msgstr "Relate直接git访问{}"
+
msgid "Create"
msgstr "创建"
@@ -448,34 +454,36 @@ msgid "Count"
msgstr "次数"
#, python-format
-msgid "'%(event_kind)s %(event_ordinal)d' already exists"
-msgstr "%(event_kind)s %(event_ordinal)d' 已经存在"
-
-msgctxt "Unkown time interval"
-msgid "unknown interval"
-msgstr "未知的时间间隔"
+msgid "'%(exist_event)s' already exists"
+msgstr "%(exist_event)s' 已经存在"
msgid "may not edit events"
msgstr "不允许编辑event"
-msgid "No events created."
-msgstr "未创建event. "
-
msgid "Events created."
msgstr "Event创建成功. "
+msgid "No events created."
+msgstr "未创建event. "
+
msgid "Create recurring events"
msgstr "创建重复进行的event"
+msgid "The starting ordinal of this kind of events"
+msgstr ""
+
+msgid "Tick to preserve the order of ordinals of existing events."
+msgstr ""
+
+msgid "Preserve ordinal order"
+msgstr ""
+
msgid "Renumber"
msgstr "重新编号"
msgid "Events renumbered."
msgstr "Event重新编号完成. "
-msgid "No events found."
-msgstr "未找到Event. "
-
msgid "Renumber events"
msgstr "重新编号"
@@ -658,6 +666,10 @@ msgctxt "Participation permission"
msgid "Update content"
msgstr "更新内容"
+msgctxt "Participation permission"
+msgid "Use direct git endpoint"
+msgstr "使用直接git端"
+
msgctxt "Participation permission"
msgid "Use markup sandbox"
msgstr "使用Markup沙箱"
@@ -847,10 +859,6 @@ msgstr "未找到资源\"%s\""
msgid "I have no idea what a processing instruction is."
msgstr "我也不知道处理的指令是什么"
-#, python-format
-msgid "invalid period: %s"
-msgstr "无效的周期:%s"
-
#, python-format
msgid "unrecognized date/time specification: '%s' (interpreted as 'now')"
msgstr "无法识别的日期/时间设定: '%s' (被解释为\"现在\")"
@@ -900,9 +908,6 @@ msgstr "您的电子邮件地址尚未确定, 请先确定邮件再继续. "
msgid "Enrollment not allowed. Please use your '%s' email to enroll."
msgstr "不允许加入, 请使用您的电子邮件\"%s\"申请加入课程. "
-msgid "Successfully enrolled."
-msgstr "成功加入课程"
-
msgid "New enrollment request"
msgstr "新的加入课程申请"
@@ -911,6 +916,9 @@ msgid ""
"request has been acted upon."
msgstr "加入课程的申请已经发送, 如果您的要求被批准, 您将会收到邮件通知. "
+msgid "Successfully enrolled."
+msgstr "成功加入课程"
+
msgid "A participation already exists. Enrollment attempt aborted."
msgstr "该课程参与已经存在,加入课程申请的请求被放弃."
@@ -933,8 +941,8 @@ msgid "Preapproval type"
msgstr "预先批准类型"
msgid ""
-"Enter fully qualified data according to the \"Preapproval type\" you "
-"selected, one per line."
+"Enter fully qualified data according to the 'Preapproval type' you selected, "
+"one per line."
msgstr "输入允许加入课程用户\"预先批准类型\"字段的数据, 每行一个. "
msgid "Preapproval data"
@@ -1033,6 +1041,45 @@ msgstr ""
msgid "Edit Participation"
msgstr "编辑课程参与"
+msgid "Add"
+msgstr "添加"
+
+msgid "may not edit participation tags"
+msgstr "不允许编辑课程参与标签"
+
+msgid "A participation tag with that name already exists."
+msgstr "同名的课程参与标签已经存在"
+
+msgid "Edit Participation Tag"
+msgstr "编辑课程参与标签"
+
+#, python-format
+msgid ""
+"Error when deleting participation tag '%(tag)s'. %(error_type)s: %(error)s."
+msgstr "在删除'%(tag)s'时出现错误: %(error_type)s: %(error)s."
+
+#, python-format
+msgid "successfully deleted participation tag '%(tag)s'."
+msgstr "成功删除课程参与标签'%(tag)s."
+
+msgid "invalid operation"
+msgstr "无效操作"
+
+msgid "A participation role with that name already exists."
+msgstr "同名的课程参与角色已经存在."
+
+msgid "Edit Participation Role"
+msgstr "编辑课程参与角色"
+
+#, python-format
+msgid ""
+"Error when deleting participation role '%(role)s'. %(error_type)s: %(error)s."
+msgstr "在删除'%(role)s'时出现错误: %(error_type)s: %(error)s."
+
+#, python-format
+msgid "successfully deleted participation role '%(role)s'."
+msgstr "成功删除课程参与角色'%(role)s."
+
msgid "Select participant for whom ticket is to be issued."
msgstr "选择发放ticket的的用户. "
@@ -1130,12 +1177,12 @@ msgstr ""
"{{ tkt.code }}, {{ tkt.exam.description }}, 以及"
"{{ checkin_uri }} 作为占位符. 请参考以上的例子."
-msgid "Ticket Format"
-msgstr "ticket格式"
-
msgid "Revoke prior exam tickets"
msgstr "撤回之前的测验ticket"
+msgid "Ticket Format"
+msgstr "ticket格式"
+
msgid "Issue tickets"
msgstr "发布ticket"
@@ -1371,9 +1418,6 @@ msgstr "只能更改正在进行中的session"
msgid "invalid expiration mode"
msgstr "无效的过期模式"
-msgid "odd POST parameters"
-msgstr "怪异的POST参数设置"
-
msgid "Cannot end a session that's already ended"
msgstr "无法结束已经结束的session"
@@ -1492,9 +1536,6 @@ msgstr "不允许批量实施截止flow"
msgid "may not batch-recalculate grades"
msgstr "不允许批量重新计算评分"
-msgid "invalid operation"
-msgstr "无效操作"
-
msgid "Set access rules tag"
msgstr "设置访问规则tag"
@@ -1984,6 +2025,15 @@ msgstr "事件(event)"
msgid "Events"
msgstr "事件(event)"
+msgid "End time must not be ahead of start time."
+msgstr ""
+
+#, python-brace-format
+msgid ""
+"May not create multiple ordinal-less events of kind '{evt_kind}' in course "
+"'{course}'"
+msgstr ""
+
#. Translators: name format of ParticipationTag
msgid "Format is lower-case-with-hyphens. Do not use spaces."
msgstr "格式:只允许小写字母与减号, 不允许有空格."
@@ -2836,6 +2886,9 @@ msgstr "其它允许的选项是"
msgid "choice %(idx)d: unable to convert to string"
msgstr "选项%(idx)d: 无法转换为字符串"
+msgid "(Non-string in 'HTML' output filtered out)"
+msgstr "(非HTML的输出已被过滤)"
+
#, python-format
msgid "data file '%(file)s' not found"
msgstr "data file '%(file)s' 未找到"
@@ -2920,9 +2973,6 @@ msgstr "你的代码得到以下的图"
msgid "Figure"
msgstr "图"
-msgid "(Non-string in 'HTML' output filtered out)"
-msgstr "(非HTML的输出已被过滤)"
-
msgid "The following code is a valid answer"
msgstr "以下的代码是一个有效的回答"
@@ -3276,15 +3326,15 @@ msgctxt "menu item"
msgid "Grading"
msgstr "成绩管理"
+msgid "List of participants"
+msgstr "参与者列表"
+
msgid "Analytics Overview"
msgstr "分析总览"
msgid "Grade Book"
msgstr "成绩册"
-msgid "List of Participants"
-msgstr "参与者列表"
-
msgid "List of Grading Opportunities"
msgstr "得分机会列表"
@@ -3361,9 +3411,18 @@ msgstr "目前正在进行一项互动活动. "
msgid "Go to activity"
msgstr "转到课程活动"
+msgid "View activity analytics"
+msgstr "查看活动统计"
+
msgid "There are some interactive activities going on right now."
msgstr "目前正在进行一些互动活动. "
+msgid "View activity"
+msgstr "查看活动"
+
+msgid "analytics"
+msgstr "分析统计"
+
msgid "This course is only visible to course staff at the moment."
msgstr "本课程目前只对课务人员显示. "
@@ -3684,6 +3743,9 @@ msgstr "(当前的)"
msgid "(submitted)"
msgstr "(已提交的)"
+msgid "not started"
+msgstr "未开始"
+
msgid "unfinished"
msgstr "未完成"
@@ -3805,6 +3867,12 @@ msgstr "当前预览的git SHA"
msgid "None"
msgstr "无"
+msgid "Direct git endpoint"
+msgstr ""
+
+msgid "Manage access tokens"
+msgstr "管理访问token"
+
msgid "Grading"
msgstr "评分"
@@ -4308,9 +4376,36 @@ msgstr "无限制的"
msgid "(never used)"
msgstr "(从未使用)"
+msgid "Add participation role"
+msgstr "添加课程参与角色"
+
+msgid "Default for new participants"
+msgstr "是否是新参与者的默认角色?"
+
+msgid "Default for unenrolled"
+msgstr "是否是未参与者的默认角色?"
+
+msgid "Delete"
+msgstr "删除"
+
+msgid "Close"
+msgstr "关闭"
+
msgid "Status"
msgstr "状态"
+msgid "Participant Tags"
+msgstr "课程参与标签"
+
+msgid "Add participation tag"
+msgstr "添加课程参与标签"
+
+msgid "Tag name"
+msgstr "标签名"
+
+msgid "Shown to participant"
+msgstr "向学员显示"
+
#. Translators: "set-up" stands for set-up code for code question
msgid "Problem set-up code"
msgstr "问题设置代码"
@@ -4538,6 +4633,21 @@ msgstr "group \"%(group_id)s\": group 是空的"
msgid "group '%(group_id)s': max_page_count is not positive"
msgstr "group \"%(group_id)s\": max_page_count 不是正数"
+#, python-format
+msgid "%(location)s, group '%(group_id)s': "
+msgstr "%(location)s, 组'%(group_id)s': "
+
+#, fuzzy
+#| msgid ""
+#| "'credit_mode' will be required on multi-select choice questions in a "
+#| "future version. set 'credit_mode: {}' to match current behavior."
+msgid ""
+"shuffle attribute will be required for groups withmax_page_count in a future "
+"version. set 'shuffle: False' to match current behavior."
+msgstr ""
+"在将来的版本中, 多项选择问题必须设置'credit_mode'. 目前,可用'credit_mode:"
+"{}'的方式来对应当前的行为. "
+
#, python-format
msgid "page id '%(page_desc_id)s' not unique"
msgstr "page id \"%(page_desc_id)s\" 应是唯一的"
@@ -4744,8 +4854,8 @@ msgstr "课程创建失败"
msgid "invalid command"
msgstr "无效的命令"
-msgid "fetch would discard commits, refusing"
-msgstr "fetch将会放弃修改, 拒绝fetch"
+msgid "internal git repo has more commits. Fetch, merge and push."
+msgstr ""
msgid "Fetch successful."
msgstr "fetch成功. "
@@ -4857,6 +4967,9 @@ msgid ""
"facility, as a reminder that this pretending is in progress."
msgstr "为所有虚拟教学设备的页面添加header,以提示目前正在虚拟过程中."
+msgid "may not pretend facilities"
+msgstr "不允许假装在设施中"
+
msgid "Pretend to be in Facilities"
msgstr "假装置身于教学设施中"
@@ -5024,8 +5137,10 @@ msgid "credit"
msgstr "应得分"
#, python-format
-msgid "Exception granted to '%(participation)s' for '%(flow_id)s'."
-msgstr "已对%(participation)s的%(flow_id)s给予破例. "
+msgid ""
+"'%(exception_type)s' exception granted to '%(participation)s' for "
+"'%(flow_id)s'."
+msgstr "已对%(participation)s的%(flow_id)s给予'%(exception_type)s'破例. "
#, python-format
msgid ""
@@ -5151,6 +5266,9 @@ msgstr "假装置身于教学设施中"
msgid "Now impersonating"
msgstr "正在模拟"
+msgid "Stop impersonating"
+msgstr "停止模拟用户"
+
msgid ""
"This website may not be compatible with your outdated Internet Explorer "
"version. If you want use Internet Explorer, please install one with version "
@@ -5252,3 +5370,20 @@ msgstr ""
msgid "whatever"
msgstr ""
+
+#~ msgid "Stop impersonating user"
+#~ msgstr "停止模拟用户"
+
+#~ msgctxt "Unkown time interval"
+#~ msgid "unknown interval"
+#~ msgstr "未知的时间间隔"
+
+#~ msgid "No events found."
+#~ msgstr "未找到Event. "
+
+#, python-format
+#~ msgid "invalid period: %s"
+#~ msgstr "无效的周期:%s"
+
+#~ msgid "fetch would discard commits, refusing"
+#~ msgstr "fetch将会放弃修改, 拒绝fetch"
diff --git a/relate/urls.py b/relate/urls.py
index ec29c0181..cf2aa8f8b 100644
--- a/relate/urls.py
+++ b/relate/urls.py
@@ -301,6 +301,32 @@
course.enrollment.edit_participation,
name="relate-edit_participation"),
+ url(r"^course"
+ "/" + COURSE_ID_REGEX
+ + "/participation-tags/$",
+ course.enrollment.view_participation_tag_list,
+ name="relate-view_participation_tags"),
+ url(r"^course"
+ "/" + COURSE_ID_REGEX
+ + "/edit-participation-tag"
+ "/(?P[-0-9]+)"
+ "/$",
+ course.enrollment.edit_participation_tag,
+ name="relate-edit_participation_tag"),
+
+ url(r"^course"
+ "/" + COURSE_ID_REGEX
+ + "/participation-roles/$",
+ course.enrollment.view_participation_role_list,
+ name="relate-view_participation_roles"),
+ url(r"^course"
+ "/" + COURSE_ID_REGEX
+ + "/edit-participation-role"
+ "/(?P[-0-9]+)"
+ "/$",
+ course.enrollment.edit_participation_role,
+ name="relate-edit_participation_role"),
+
# }}}
# {{{ media
diff --git a/tests/base_test_mixins.py b/tests/base_test_mixins.py
index 8d75ecfd1..bc9a9134b 100644
--- a/tests/base_test_mixins.py
+++ b/tests/base_test_mixins.py
@@ -225,6 +225,10 @@ def assertResponseContextEqual(self, resp, context_name, expected_value): # noq
except Exception:
self.assertEqual(value, expected_value)
+ def assertResponseContextLengthEqual(self, resp, context_name, expected_length): # noqa
+ value = self.get_response_context_value_by_name(resp, context_name)
+ self.assertEqual(len(value), expected_length)
+
def assertResponseContextContains(self, resp, # noqa
context_name, expected_value, html=False,
in_bulk=False):
diff --git a/tests/test_enrollment.py b/tests/test_enrollment.py
index d2dc83f1b..0f5ec259b 100644
--- a/tests/test_enrollment.py
+++ b/tests/test_enrollment.py
@@ -24,6 +24,7 @@
import unittest
import pytest
+from random import randint
from django.test import TestCase, RequestFactory
from django.conf import settings
from django.test.utils import override_settings # noqa
@@ -36,7 +37,7 @@
from course import constants
from course import enrollment
from course.models import (
- Participation, ParticipationRole, ParticipationPreapproval)
+ Participation, ParticipationRole, ParticipationPreapproval, ParticipationTag)
from course.constants import (
participation_status as p_status, user_status as u_status)
@@ -133,6 +134,26 @@ def get_participation_edit_url(cls, participation_id):
return reverse("relate-edit_participation",
args=[cls.course.identifier, participation_id])
+ @ classmethod
+ def get_participation_tag_list_url(cls):
+ return reverse("relate-view_participation_tags",
+ args=[cls.course.identifier])
+
+ @classmethod
+ def get_participation_tag_edit_url(cls, ptag_id):
+ return reverse("relate-edit_participation_tag",
+ args=[cls.course.identifier, ptag_id])
+
+ @ classmethod
+ def get_participation_role_list_url(cls):
+ return reverse("relate-view_participation_roles",
+ args=[cls.course.identifier])
+
+ @classmethod
+ def get_participation_role_edit_url(cls, prole_id):
+ return reverse("relate-edit_participation_role",
+ args=[cls.course.identifier, prole_id])
+
def get_participation_count_by_status(self, status):
return Participation.objects.filter(
course__identifier=self.course.identifier,
@@ -1904,4 +1925,503 @@ def test_drop(self):
# }}}
+class ParticipationTagCRUDTest(
+ SingleCourseTestMixin, EnrollmentTestMixin, TestCase):
+ def setUp(self):
+ super(ParticipationTagCRUDTest, self).setUp()
+ self.c.force_login(self.instructor_participation.user)
+
+ @staticmethod
+ def get_default_edit_ptag_post_data(**kwargs):
+ data = {"name": "a_tag"}
+ data.update(kwargs)
+ return data
+
+ def get_default_create_ptag_post_data(self, **kwargs):
+ data = self.get_default_edit_ptag_post_data()
+ data["submit"] = ""
+ data.update(kwargs)
+ return data
+
+ def get_default_update_ptag_post_data(self, **kwargs):
+ data = self.get_default_edit_ptag_post_data()
+ data["update"] = ""
+ data.update(kwargs)
+ return data
+
+ def get_default_delete_ptag_post_data(self, **kwargs):
+ data = self.get_default_edit_ptag_post_data()
+ data["delete"] = ""
+ data.update(kwargs)
+ return data
+
+ @property
+ def ptag_list_url(self):
+ return self.get_participation_tag_list_url()
+
+ def test_view_ptag_list_permission_denied(self):
+ with self.temporarily_switch_to_user(self.student_participation.user):
+ resp = self.c.get(self.ptag_list_url)
+ self.assertEqual(resp.status_code, 403)
+
+ def test_view_ptag_list_success(self):
+ n_tags = randint(2, 10)
+ factories.ParticipationTagFactory.create_batch(
+ size=n_tags, course=self.course)
+
+ resp = self.c.get(self.ptag_list_url)
+ self.assertEqual(resp.status_code, 200)
+
+ self.assertResponseContextLengthEqual(
+ resp, "participation_tags", n_tags)
+
+ def test_edit_ptag_permission_denied(self):
+ self.c.force_login(self.student_participation.user)
+
+ # GET requests
+ resp = self.c.get(self.get_participation_tag_edit_url(0))
+ self.assertEqual(resp.status_code, 403)
+
+ n_tags = randint(2, 10)
+ ptags = factories.ParticipationTagFactory.create_batch(
+ size=n_tags, course=self.course)
+
+ resp = self.c.get(self.get_participation_tag_edit_url(ptags[0].id))
+ self.assertEqual(resp.status_code, 403)
+
+ resp = self.c.get(self.get_participation_tag_edit_url(-1))
+ self.assertEqual(resp.status_code, 403)
+
+ # POST requests
+ resp = self.c.post(self.get_participation_tag_edit_url(-1),
+ data=self.get_default_edit_ptag_post_data())
+ self.assertEqual(resp.status_code, 403)
+
+ resp = self.c.post(self.get_participation_tag_edit_url(0),
+ data=self.get_default_edit_ptag_post_data())
+ self.assertEqual(resp.status_code, 403)
+
+ def test_edit_ptag_get_success(self):
+ resp = self.c.get(self.get_participation_tag_edit_url(0))
+ self.assertEqual(resp.status_code, 404)
+
+ n_tags = randint(2, 10)
+ ptags = factories.ParticipationTagFactory.create_batch(
+ size=n_tags, course=self.course)
+
+ resp = self.c.get(self.get_participation_tag_edit_url(ptags[-1].id))
+ self.assertEqual(resp.status_code, 200)
+
+ resp = self.c.get(self.get_participation_tag_edit_url(-1))
+ self.assertEqual(resp.status_code, 200)
+
+ def test_edit_ptag_post_update(self):
+ n_tags = randint(2, 10)
+ ptags = factories.ParticipationTagFactory.create_batch(
+ size=n_tags, course=self.course)
+
+ expected_ptag_name = "some_tag_name"
+
+ # Get a ptag with has different name with expected_ptag_name
+ ptag = None
+ for ptag in ptags:
+ if ptag.name != expected_ptag_name:
+ break
+
+ resp = self.c.post(
+ self.get_participation_tag_edit_url(ptag.id),
+ data=self.get_default_update_ptag_post_data(name=expected_ptag_name),
+ )
+ self.assertEqual(ParticipationTag.objects.count(), n_tags)
+ self.assertEqual(resp.status_code, 302)
+ self.assertEqual(ParticipationTag.objects.get(pk=ptag.id).name,
+ expected_ptag_name)
+ self.assertAddMessageCallCount(1)
+ self.assertAddMessageCalledWith("Changes saved.")
+
+ def test_edit_ptag_from_another_course(self):
+ ptag = factories.ParticipationTagFactory(
+ course=factories.CourseFactory(identifier="another-course"))
+
+ resp = self.c.get(self.get_participation_tag_edit_url(ptag.id))
+ self.assertEqual(resp.status_code, 400)
+
+ def test_edit_ptag_post_update_integrity_error(self):
+ n_tags = randint(2, 10)
+ ptags = factories.ParticipationTagFactory.create_batch(
+ size=n_tags, course=self.course)
+
+ exist_ptag_name = None
+
+ ptag = None
+ for ptag in ptags:
+ if exist_ptag_name is None:
+ exist_ptag_name = ptag.name
+ continue
+ if ptag.name != exist_ptag_name:
+ break
+
+ ptag_name = ptag.name
+
+ resp = self.c.post(
+ self.get_participation_tag_edit_url(ptag.id),
+ data=self.get_default_update_ptag_post_data(name=exist_ptag_name),
+ )
+
+ self.assertEqual(ParticipationTag.objects.count(), n_tags)
+ self.assertEqual(resp.status_code, 200)
+ self.assertEqual(ParticipationTag.objects.get(pk=ptag.id).name,
+ ptag_name)
+ self.assertAddMessageCallCount(1)
+ self.assertAddMessageCalledWith(
+ "A participation tag with that name already exists.")
+
+ def test_edit_ptag_post_create_new_success(self):
+ n_tags = randint(2, 10)
+ factories.ParticipationTagFactory.create_batch(
+ size=n_tags, course=self.course)
+
+ resp = self.c.post(
+ self.get_participation_tag_edit_url(-1),
+ data=self.get_default_create_ptag_post_data(),
+ )
+
+ self.assertEqual(ParticipationTag.objects.count(), n_tags+1)
+ self.assertEqual(resp.status_code, 302)
+ self.assertAddMessageCallCount(1)
+ self.assertAddMessageCalledWith("New participation tag saved.")
+
+ def test_edit_ptag_post_create_new_integrity_error(self):
+ n_tags = randint(2, 10)
+ ptags = factories.ParticipationTagFactory.create_batch(
+ size=n_tags, course=self.course)
+
+ resp = self.c.post(
+ self.get_participation_tag_edit_url(-1),
+ data=self.get_default_create_ptag_post_data(name=ptags[0].name),
+ )
+
+ self.assertEqual(ParticipationTag.objects.count(), n_tags)
+ self.assertEqual(resp.status_code, 200)
+ self.assertAddMessageCallCount(1)
+ self.assertAddMessageCalledWith(
+ "A participation tag with that name already exists.")
+
+ def test_edit_ptag_post_form_invalid(self):
+ n_tags = randint(2, 10)
+ factories.ParticipationTagFactory.create_batch(
+ size=n_tags, course=self.course)
+
+ # Spaces are not allowed in ptag
+ resp = self.c.post(
+ self.get_participation_tag_edit_url(-1),
+ data=self.get_default_create_ptag_post_data(name="a tag"),
+ )
+
+ self.assertEqual(ParticipationTag.objects.count(), n_tags)
+ self.assertEqual(resp.status_code, 200)
+ self.assertAddMessageCallCount(0)
+ self.assertFormErrorLoose(resp, "invalid characters.")
+
+ def test_delete_ptag_permission_denied(self):
+ n_tags = randint(2, 10)
+ ptags = factories.ParticipationTagFactory.create_batch(
+ size=n_tags, course=self.course)
+
+ with self.temporarily_switch_to_user(self.student_participation.user):
+ resp = self.c.get(self.get_participation_tag_edit_url(ptags[0].id))
+ self.assertEqual(resp.status_code, 403)
+
+ resp = self.c.post(
+ self.get_participation_tag_edit_url(ptags[0].id),
+ data=self.get_default_delete_ptag_post_data(),
+ )
+ self.assertEqual(resp.status_code, 403)
+ self.assertEqual(ParticipationTag.objects.count(), n_tags)
+
+ def test_delete_ptag_success(self):
+ n_tags = randint(2, 10)
+ ptags = factories.ParticipationTagFactory.create_batch(
+ size=n_tags, course=self.course)
+
+ resp = self.c.post(
+ self.get_participation_tag_edit_url(ptags[0].id),
+ data=self.get_default_delete_ptag_post_data(),
+ )
+ self.assertEqual(resp.status_code, 302)
+ self.assertEqual(ParticipationTag.objects.count(), n_tags-1)
+
+ def test_delete_ptag_from_another_course(self):
+ ptag = factories.ParticipationTagFactory(
+ course=factories.CourseFactory(identifier="another-course"))
+
+ resp = self.c.post(
+ self.get_participation_tag_edit_url(ptag.id),
+ data=self.get_default_delete_ptag_post_data(),
+ )
+
+ self.assertEqual(resp.status_code, 400)
+ self.assertEqual(ParticipationTag.objects.count(), 1)
+
+ def test_edit_ptag_suspicious(self):
+ n_tags = randint(2, 10)
+ ptags = factories.ParticipationTagFactory.create_batch(
+ size=n_tags, course=self.course)
+
+ with mock.patch("course.models.ParticipationTag.save") as mock_ptag_save:
+ resp = self.c.post(
+ self.get_participation_tag_edit_url(ptags[0].id),
+ # No submit/update/delete
+ data=self.get_default_edit_ptag_post_data(),
+ )
+ self.assertEqual(mock_ptag_save.call_count, 0)
+
+ self.assertEqual(resp.status_code, 400)
+ self.assertEqual(ParticipationTag.objects.count(), n_tags)
+
+
+class ParticipationRoleCRUDTest(
+ SingleCourseTestMixin, EnrollmentTestMixin, TestCase):
+ def setUp(self):
+ super(ParticipationRoleCRUDTest, self).setUp()
+ self.c.force_login(self.instructor_participation.user)
+ self.default_number_of_roles = ParticipationRole.objects.count()
+
+ @staticmethod
+ def get_default_edit_role_post_data(**kwargs):
+ data = {"identifier": "a_role", "name": "the name"}
+ data.update(kwargs)
+ return data
+
+ def get_default_create_role_post_data(self, **kwargs):
+ data = self.get_default_edit_role_post_data()
+ data["submit"] = ""
+ data.update(kwargs)
+ return data
+
+ def get_default_update_role_post_data(self, **kwargs):
+ data = self.get_default_edit_role_post_data()
+ data["update"] = ""
+ data.update(kwargs)
+ return data
+
+ def get_default_delete_role_post_data(self, **kwargs):
+ data = self.get_default_edit_role_post_data()
+ data["delete"] = ""
+ data.update(kwargs)
+ return data
+
+ @property
+ def prole_list_url(self):
+ return self.get_participation_role_list_url()
+
+ def test_view_prole_list_permission_denied(self):
+ with self.temporarily_switch_to_user(self.student_participation.user):
+ resp = self.c.get(self.prole_list_url)
+ self.assertEqual(resp.status_code, 403)
+
+ def test_view_prole_list_success(self):
+ factories.ParticipationRoleFactory(
+ course=self.course, identifier="some_role")
+
+ resp = self.c.get(self.prole_list_url)
+ self.assertEqual(resp.status_code, 200)
+
+ self.assertResponseContextLengthEqual(
+ resp, "participation_roles", self.default_number_of_roles + 1)
+
+ def test_edit_prole_permission_denied(self):
+ self.c.force_login(self.student_participation.user)
+
+ # GET requests
+ resp = self.c.get(self.get_participation_role_edit_url(0))
+ self.assertEqual(resp.status_code, 403)
+
+ prole = factories.ParticipationRoleFactory(
+ identifier="some_role", course=self.course)
+
+ resp = self.c.get(self.get_participation_role_edit_url(prole.id))
+ self.assertEqual(resp.status_code, 403)
+
+ resp = self.c.get(self.get_participation_role_edit_url(-1))
+ self.assertEqual(resp.status_code, 403)
+
+ # POST requests
+ resp = self.c.post(self.get_participation_role_edit_url(-1),
+ data=self.get_default_edit_role_post_data())
+ self.assertEqual(resp.status_code, 403)
+
+ resp = self.c.post(self.get_participation_role_edit_url(0),
+ data=self.get_default_edit_role_post_data())
+ self.assertEqual(resp.status_code, 403)
+
+ def test_edit_prole_get_success(self):
+ resp = self.c.get(self.get_participation_role_edit_url(0))
+ self.assertEqual(resp.status_code, 404)
+
+ prole = factories.ParticipationRoleFactory(
+ identifier="some_role", course=self.course)
+
+ resp = self.c.get(self.get_participation_role_edit_url(prole.id))
+ self.assertEqual(resp.status_code, 200)
+
+ resp = self.c.get(self.get_participation_role_edit_url(-1))
+ self.assertEqual(resp.status_code, 200)
+
+ def test_edit_prole_post_update(self):
+ expected_prole_identifier = "some_role_identifier"
+
+ # Get a prole with has different identifier with expected_prole_identifier
+ prole = None
+ for prole in ParticipationRole.objects.all():
+ if prole.identifier != expected_prole_identifier:
+ break
+
+ resp = self.c.post(
+ self.get_participation_role_edit_url(prole.id),
+ data=self.get_default_update_role_post_data(
+ identifier=expected_prole_identifier),
+ )
+ self.assertEqual(ParticipationRole.objects.count(),
+ self.default_number_of_roles)
+ self.assertEqual(resp.status_code, 302)
+ self.assertEqual(ParticipationRole.objects.get(pk=prole.id).identifier,
+ expected_prole_identifier)
+ self.assertAddMessageCallCount(1)
+ self.assertAddMessageCalledWith("Changes saved.")
+
+ def test_edit_prole_from_another_course(self):
+ prole = factories.ParticipationRoleFactory(
+ course=factories.CourseFactory(identifier="another-course"))
+
+ resp = self.c.get(self.get_participation_role_edit_url(prole.id))
+ self.assertEqual(resp.status_code, 400)
+
+ def test_edit_prole_post_update_integrity_error(self):
+ new_prole = factories.ParticipationRoleFactory(
+ identifier="some_role", course=self.course)
+
+ new_prole_identifier = new_prole.identifier
+
+ prole = None
+ for prole in ParticipationRole.objects.all():
+ if prole.identifier != new_prole_identifier:
+ break
+
+ prole_identifier = prole.identifier
+
+ resp = self.c.post(
+ self.get_participation_role_edit_url(prole.id),
+ data=self.get_default_update_role_post_data(
+ identifier=new_prole_identifier),
+ )
+
+ self.assertEqual(ParticipationRole.objects.count(),
+ self.default_number_of_roles + 1)
+ self.assertEqual(resp.status_code, 200)
+ self.assertEqual(ParticipationRole.objects.get(pk=prole.id).identifier,
+ prole_identifier)
+ self.assertAddMessageCallCount(1)
+ self.assertAddMessageCalledWith(
+ "A participation role with that identifier already exists.")
+
+ def test_edit_prole_post_create_new_success(self):
+ resp = self.c.post(
+ self.get_participation_role_edit_url(-1),
+ data=self.get_default_create_role_post_data(),
+ )
+
+ self.assertEqual(ParticipationRole.objects.count(),
+ self.default_number_of_roles + 1)
+ self.assertEqual(resp.status_code, 302)
+ self.assertAddMessageCallCount(1)
+ self.assertAddMessageCalledWith("New participation role saved.")
+
+ def test_edit_prole_post_create_new_integrity_error(self):
+ prole = factories.ParticipationRoleFactory(
+ identifier="some_role", course=self.course)
+
+ resp = self.c.post(
+ self.get_participation_role_edit_url(-1),
+ data=self.get_default_create_role_post_data(identifier=prole.identifier),
+ )
+
+ self.assertEqual(ParticipationRole.objects.count(),
+ self.default_number_of_roles + 1)
+ self.assertEqual(resp.status_code, 200)
+ self.assertAddMessageCallCount(1)
+ self.assertAddMessageCalledWith(
+ "A participation role with that identifier already exists.")
+
+ def test_edit_prole_post_form_invalid(self):
+ # Spaces are not allowed in prole
+ resp = self.c.post(
+ self.get_participation_role_edit_url(-1),
+ data=self.get_default_create_role_post_data(identifier="a role"),
+ )
+
+ self.assertEqual(ParticipationRole.objects.count(),
+ self.default_number_of_roles)
+ self.assertEqual(resp.status_code, 200)
+ self.assertAddMessageCallCount(0)
+ self.assertFormErrorLoose(resp, "invalid characters.")
+
+ def test_delete_prole_permission_denied(self):
+ prole = factories.ParticipationRoleFactory(
+ identifier="some_role", course=self.course)
+
+ with self.temporarily_switch_to_user(self.student_participation.user):
+ resp = self.c.get(self.get_participation_role_edit_url(prole.id))
+ self.assertEqual(resp.status_code, 403)
+
+ resp = self.c.post(
+ self.get_participation_role_edit_url(prole.id),
+ data=self.get_default_delete_role_post_data(),
+ )
+ self.assertEqual(resp.status_code, 403)
+ self.assertEqual(ParticipationRole.objects.count(),
+ self.default_number_of_roles + 1)
+
+ def test_delete_prole_success(self):
+ prole = factories.ParticipationRoleFactory(
+ identifier="some_role", course=self.course)
+
+ resp = self.c.post(
+ self.get_participation_role_edit_url(prole.id),
+ data=self.get_default_delete_role_post_data(),
+ )
+ self.assertEqual(resp.status_code, 302)
+ self.assertEqual(ParticipationRole.objects.count(),
+ self.default_number_of_roles)
+
+ def test_delete_prole_from_another_course(self):
+ prole = factories.ParticipationRoleFactory(
+ course=factories.CourseFactory(identifier="another-course"))
+
+ prole_counts = ParticipationRole.objects.count()
+
+ resp = self.c.post(
+ self.get_participation_role_edit_url(prole.id),
+ data=self.get_default_delete_role_post_data(),
+ )
+
+ self.assertEqual(resp.status_code, 400)
+ self.assertEqual(ParticipationRole.objects.count(), prole_counts)
+
+ def test_edit_prole_suspicious(self):
+ prole = factories.ParticipationRoleFactory(
+ identifier="some_role", course=self.course)
+
+ with mock.patch("course.models.ParticipationRole.save") as mock_prole_save:
+ resp = self.c.post(
+ self.get_participation_role_edit_url(prole.id),
+ # No submit/update/delete
+ data=self.get_default_edit_role_post_data(),
+ )
+ self.assertEqual(resp.status_code, 400)
+ self.assertEqual(ParticipationRole.objects.count(),
+ self.default_number_of_roles + 1)
+ self.assertEqual(mock_prole_save.call_count, 0)
+
+
# vim: foldmethod=marker