add role and expose reason field to Instructor Dashboard Manual Enrollment
WL-1473
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('student', '0014_courseenrollmentallowed_user'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='manualenrollmentaudit',
|
||||
name='role',
|
||||
field=models.CharField(max_length=64, null=True, blank=True),
|
||||
),
|
||||
]
|
||||
@@ -1973,9 +1973,10 @@ class ManualEnrollmentAudit(models.Model):
|
||||
time_stamp = models.DateTimeField(auto_now_add=True, null=True)
|
||||
state_transition = models.CharField(max_length=255, choices=TRANSITION_STATES)
|
||||
reason = models.TextField(null=True)
|
||||
role = models.CharField(blank=True, null=True, max_length=64)
|
||||
|
||||
@classmethod
|
||||
def create_manual_enrollment_audit(cls, user, email, state_transition, reason, enrollment=None):
|
||||
def create_manual_enrollment_audit(cls, user, email, state_transition, reason, enrollment=None, role=None):
|
||||
"""
|
||||
saves the student manual enrollment information
|
||||
"""
|
||||
@@ -1984,7 +1985,8 @@ class ManualEnrollmentAudit(models.Model):
|
||||
enrolled_email=email,
|
||||
state_transition=state_transition,
|
||||
reason=reason,
|
||||
enrollment=enrollment
|
||||
enrollment=enrollment,
|
||||
role=role,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -1585,32 +1585,6 @@ class TestInstructorAPIEnrollment(SharedModuleStoreTestCase, LoginEnrollmentTest
|
||||
CourseInstructorRole(paid_course.id).add_users(self.instructor)
|
||||
return paid_course
|
||||
|
||||
def test_reason_field_should_not_be_empty(self):
|
||||
"""
|
||||
test to check that reason field should not be empty when
|
||||
manually enrolling the students for the paid courses.
|
||||
"""
|
||||
paid_course = self.create_paid_course()
|
||||
url = reverse('students_update_enrollment', kwargs={'course_id': text_type(paid_course.id)})
|
||||
params = {'identifiers': self.notregistered_email, 'action': 'enroll', 'email_students': False,
|
||||
'auto_enroll': False}
|
||||
response = self.client.post(url, params)
|
||||
manual_enrollments = ManualEnrollmentAudit.objects.all()
|
||||
self.assertEqual(manual_enrollments.count(), 0)
|
||||
|
||||
# test the response data
|
||||
expected = {
|
||||
"action": "enroll",
|
||||
"auto_enroll": False,
|
||||
"results": [
|
||||
{
|
||||
"error": True
|
||||
}
|
||||
]
|
||||
}
|
||||
res_json = json.loads(response.content)
|
||||
self.assertEqual(res_json, expected)
|
||||
|
||||
def test_unenrolled_allowed_to_enroll_user(self):
|
||||
"""
|
||||
test to unenroll allow to enroll user.
|
||||
@@ -1618,7 +1592,7 @@ class TestInstructorAPIEnrollment(SharedModuleStoreTestCase, LoginEnrollmentTest
|
||||
paid_course = self.create_paid_course()
|
||||
url = reverse('students_update_enrollment', kwargs={'course_id': text_type(paid_course.id)})
|
||||
params = {'identifiers': self.notregistered_email, 'action': 'enroll', 'email_students': False,
|
||||
'auto_enroll': False, 'reason': 'testing..'}
|
||||
'auto_enroll': False, 'reason': 'testing..', 'role': 'Learner'}
|
||||
response = self.client.post(url, params)
|
||||
manual_enrollments = ManualEnrollmentAudit.objects.all()
|
||||
self.assertEqual(manual_enrollments.count(), 1)
|
||||
@@ -1629,7 +1603,7 @@ class TestInstructorAPIEnrollment(SharedModuleStoreTestCase, LoginEnrollmentTest
|
||||
UserFactory(email=self.notregistered_email)
|
||||
url = reverse('students_update_enrollment', kwargs={'course_id': text_type(paid_course.id)})
|
||||
params = {'identifiers': self.notregistered_email, 'action': 'enroll', 'email_students': False,
|
||||
'auto_enroll': False, 'reason': 'testing'}
|
||||
'auto_enroll': False, 'reason': 'testing', 'role': 'Learner'}
|
||||
response = self.client.post(url, params)
|
||||
manual_enrollments = ManualEnrollmentAudit.objects.all()
|
||||
self.assertEqual(manual_enrollments.count(), 2)
|
||||
@@ -1673,7 +1647,7 @@ class TestInstructorAPIEnrollment(SharedModuleStoreTestCase, LoginEnrollmentTest
|
||||
|
||||
url = reverse('students_update_enrollment', kwargs={'course_id': text_type(paid_course.id)})
|
||||
params = {'identifiers': self.notregistered_email, 'action': 'unenroll', 'email_students': False,
|
||||
'auto_enroll': False, 'reason': 'testing'}
|
||||
'auto_enroll': False, 'reason': 'testing', 'role': 'Learner'}
|
||||
|
||||
response = self.client.post(url, params)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
@@ -1730,6 +1704,21 @@ class TestInstructorAPIEnrollment(SharedModuleStoreTestCase, LoginEnrollmentTest
|
||||
)
|
||||
self.assertEqual(course_enrollment.mode, CourseMode.DEFAULT_MODE_SLUG)
|
||||
|
||||
def test_role_and_reason_are_persisted(self):
|
||||
"""
|
||||
test that role and reason fields are persisted in the database
|
||||
"""
|
||||
paid_course = self.create_paid_course()
|
||||
url = reverse('students_update_enrollment', kwargs={'course_id': text_type(paid_course.id)})
|
||||
params = {'identifiers': self.notregistered_email, 'action': 'enroll', 'email_students': False,
|
||||
'auto_enroll': False, 'reason': 'testing', 'role': 'Learner'}
|
||||
response = self.client.post(url, params)
|
||||
|
||||
manual_enrollment = ManualEnrollmentAudit.objects.first()
|
||||
self.assertEqual(manual_enrollment.reason, 'testing')
|
||||
self.assertEqual(manual_enrollment.role, 'Learner')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def _change_student_enrollment(self, user, course, action):
|
||||
"""
|
||||
Helper function that posts to 'students_update_enrollment' to change
|
||||
@@ -1743,7 +1732,8 @@ class TestInstructorAPIEnrollment(SharedModuleStoreTestCase, LoginEnrollmentTest
|
||||
'identifiers': user.email,
|
||||
'action': action,
|
||||
'email_students': True,
|
||||
'reason': 'change user enrollment'
|
||||
'reason': 'change user enrollment',
|
||||
'role': 'Learner'
|
||||
}
|
||||
response = self.client.post(url, params)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@@ -5,6 +5,7 @@ import datetime
|
||||
|
||||
import ddt
|
||||
from django.conf import settings
|
||||
from django.contrib.sites.models import Site
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test.client import RequestFactory
|
||||
from django.test.utils import override_settings
|
||||
@@ -20,6 +21,7 @@ from courseware.tests.factories import StaffFactory, StudentModuleFactory, UserF
|
||||
from courseware.tests.helpers import LoginEnrollmentTestCase
|
||||
from edxmako.shortcuts import render_to_response
|
||||
from lms.djangoapps.instructor.views.gradebook_api import calculate_page_info
|
||||
from openedx.core.djangoapps.site_configuration.models import SiteConfiguration
|
||||
from pyquery import PyQuery as pq
|
||||
from shoppingcart.models import CourseRegCodeItem, Order, PaidCourseRegistration
|
||||
from student.models import CourseEnrollment
|
||||
@@ -152,6 +154,49 @@ class TestInstructorDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase, XssT
|
||||
content('#field-course-organization b').contents()[0].strip()
|
||||
)
|
||||
|
||||
def test_membership_site_configuration_role(self):
|
||||
"""
|
||||
Verify that the role choices set via site configuration are loaded in the membership tab
|
||||
of the instructor dashboard
|
||||
"""
|
||||
|
||||
configuration_values = {
|
||||
"MANUAL_ENROLLMENT_ROLE_CHOICES": [
|
||||
"role1",
|
||||
"role2",
|
||||
]
|
||||
}
|
||||
site = Site.objects.first()
|
||||
SiteConfiguration.objects.create(site=site, values=configuration_values, enabled=True)
|
||||
url = reverse(
|
||||
'instructor_dashboard',
|
||||
kwargs={
|
||||
'course_id': unicode(self.course_info.id)
|
||||
}
|
||||
)
|
||||
|
||||
response = self.client.get(url)
|
||||
self.assertIn('<option value="role1">role1</option>', response.content)
|
||||
self.assertIn('<option value="role2">role2</option>', response.content)
|
||||
|
||||
def test_membership_default_role(self):
|
||||
"""
|
||||
Verify that in the absence of site configuration role choices, default values of role choices are loaded
|
||||
in the membership tab of the instructor dashboard
|
||||
"""
|
||||
|
||||
url = reverse(
|
||||
'instructor_dashboard',
|
||||
kwargs={
|
||||
'course_id': unicode(self.course_info.id)
|
||||
}
|
||||
)
|
||||
|
||||
response = self.client.get(url)
|
||||
self.assertIn('<option value="Learner">Learner</option>', response.content)
|
||||
self.assertIn('<option value="Support">Support</option>', response.content)
|
||||
self.assertIn('<option value="Partner">Partner</option>', response.content)
|
||||
|
||||
def test_student_admin_staff_instructor(self):
|
||||
"""
|
||||
Verify that staff users are not able to see course-wide options, while still
|
||||
|
||||
@@ -628,16 +628,19 @@ def students_update_enrollment(request, course_id):
|
||||
identifiers = _split_input_list(identifiers_raw)
|
||||
auto_enroll = _get_boolean_param(request, 'auto_enroll')
|
||||
email_students = _get_boolean_param(request, 'email_students')
|
||||
is_white_label = CourseMode.is_white_label(course_id)
|
||||
reason = request.POST.get('reason')
|
||||
if is_white_label:
|
||||
if not reason:
|
||||
return JsonResponse(
|
||||
{
|
||||
'action': action,
|
||||
'results': [{'error': True}],
|
||||
'auto_enroll': auto_enroll,
|
||||
}, status=400)
|
||||
role = request.POST.get('role')
|
||||
|
||||
allowed_role_choices = configuration_helpers.get_value('MANUAL_ENROLLMENT_ROLE_CHOICES',
|
||||
settings.MANUAL_ENROLLMENT_ROLE_CHOICES)
|
||||
if role and role not in allowed_role_choices:
|
||||
return JsonResponse(
|
||||
{
|
||||
'action': action,
|
||||
'results': [{'error': True, 'message': 'Not a valid role choice'}],
|
||||
'auto_enroll': auto_enroll,
|
||||
}, status=400)
|
||||
|
||||
enrollment_obj = None
|
||||
state_transition = DEFAULT_TRANSITION_STATE
|
||||
|
||||
@@ -729,7 +732,7 @@ def students_update_enrollment(request, course_id):
|
||||
|
||||
else:
|
||||
ManualEnrollmentAudit.create_manual_enrollment_audit(
|
||||
request.user, email, state_transition, reason, enrollment_obj
|
||||
request.user, email, state_transition, reason, enrollment_obj, role
|
||||
)
|
||||
results.append({
|
||||
'identifier': identifier,
|
||||
|
||||
@@ -122,7 +122,7 @@ def instructor_dashboard_2(request, course_id):
|
||||
|
||||
sections = [
|
||||
_section_course_info(course, access),
|
||||
_section_membership(course, access, is_white_label),
|
||||
_section_membership(course, access),
|
||||
_section_cohort_management(course, access),
|
||||
_section_discussions_management(course, access),
|
||||
_section_student_admin(course, access),
|
||||
@@ -480,16 +480,18 @@ def _section_course_info(course, access):
|
||||
return section_data
|
||||
|
||||
|
||||
def _section_membership(course, access, is_white_label):
|
||||
def _section_membership(course, access):
|
||||
""" Provide data for the corresponding dashboard section """
|
||||
course_key = course.id
|
||||
ccx_enabled = settings.FEATURES.get('CUSTOM_COURSES_EDX', False) and course.enable_ccx
|
||||
enrollment_role_choices = configuration_helpers.get_value('MANUAL_ENROLLMENT_ROLE_CHOICES',
|
||||
settings.MANUAL_ENROLLMENT_ROLE_CHOICES)
|
||||
|
||||
section_data = {
|
||||
'section_key': 'membership',
|
||||
'section_display_name': _('Membership'),
|
||||
'access': access,
|
||||
'ccx_is_enabled': ccx_enabled,
|
||||
'is_white_label': is_white_label,
|
||||
'enroll_button_url': reverse('students_update_enrollment', kwargs={'course_id': unicode(course_key)}),
|
||||
'unenroll_button_url': reverse('students_update_enrollment', kwargs={'course_id': unicode(course_key)}),
|
||||
'upload_student_csv_button_url': reverse('register_and_enroll_students', kwargs={'course_id': unicode(course_key)}),
|
||||
@@ -498,6 +500,7 @@ def _section_membership(course, access, is_white_label):
|
||||
'modify_access_url': reverse('modify_access', kwargs={'course_id': unicode(course_key)}),
|
||||
'list_forum_members_url': reverse('list_forum_members', kwargs={'course_id': unicode(course_key)}),
|
||||
'update_forum_role_membership_url': reverse('update_forum_role_membership', kwargs={'course_id': unicode(course_key)}),
|
||||
'enrollment_role_choices': enrollment_role_choices
|
||||
}
|
||||
return section_data
|
||||
|
||||
|
||||
@@ -67,6 +67,10 @@ LMS_ROOT_URL = "http://localhost:8000"
|
||||
LMS_INTERNAL_ROOT_URL = LMS_ROOT_URL
|
||||
LMS_ENROLLMENT_API_PATH = "/api/enrollment/v1/"
|
||||
|
||||
# Default choices for role dropdown in the membership tab of the instructor dashboard
|
||||
# This setting is used when a site does not define its own choices via site configuration
|
||||
MANUAL_ENROLLMENT_ROLE_CHOICES = ['Learner', 'Support', 'Partner']
|
||||
|
||||
# Features
|
||||
FEATURES = {
|
||||
'DISPLAY_DEBUG_INFO_TO_STAFF': True,
|
||||
|
||||
@@ -593,8 +593,8 @@ such that the value can be defined later than this assignment (file load order).
|
||||
var batchEnroll = this;
|
||||
this.$container = $container;
|
||||
this.$identifier_input = this.$container.find("textarea[name='student-ids']");
|
||||
this.$role = this.$container.find("select[name='role']");
|
||||
this.$enrollment_button = this.$container.find('.enrollment-button');
|
||||
this.$is_course_white_label = this.$container.find('#is_course_white_label').val();
|
||||
this.$reason_field = this.$container.find("textarea[name='reason-field']");
|
||||
this.$checkbox_autoenroll = this.$container.find("input[name='auto-enroll']");
|
||||
this.$checkbox_emailstudents = this.$container.find("input[name='email-students']");
|
||||
@@ -603,16 +603,20 @@ such that the value can be defined later than this assignment (file load order).
|
||||
this.$request_response_error = this.$container.find('.request-response-error');
|
||||
this.$enrollment_button.click(function(event) {
|
||||
var sendData;
|
||||
if (batchEnroll.$is_course_white_label === 'True') {
|
||||
if (!batchEnroll.$reason_field.val()) {
|
||||
batchEnroll.fail_with_error(gettext('Reason field should not be left blank.'));
|
||||
return false;
|
||||
}
|
||||
if (!batchEnroll.$reason_field.val()) {
|
||||
batchEnroll.fail_with_error(gettext('Reason field should not be left blank.'));
|
||||
return false;
|
||||
}
|
||||
if (!batchEnroll.$role.val()) {
|
||||
batchEnroll.fail_with_error(gettext('Role field should not be left unselected.'));
|
||||
return false;
|
||||
}
|
||||
|
||||
emailStudents = batchEnroll.$checkbox_emailstudents.is(':checked');
|
||||
sendData = {
|
||||
action: $(event.target).data('action'),
|
||||
identifiers: batchEnroll.$identifier_input.val(),
|
||||
role: batchEnroll.$role.val(),
|
||||
auto_enroll: batchEnroll.$checkbox_autoenroll.is(':checked'),
|
||||
email_students: emailStudents,
|
||||
reason: batchEnroll.$reason_field.val()
|
||||
|
||||
@@ -615,6 +615,13 @@
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.role {
|
||||
margin: 20px 0;
|
||||
select {
|
||||
margin-left: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
margin-right: ($baseline/4);
|
||||
}
|
||||
|
||||
@@ -12,15 +12,24 @@ from openedx.core.djangolib.markup import HTML, Text
|
||||
${_("You will not get notification for emails that bounce, so please double-check spelling.")}
|
||||
<textarea rows="6" name="student-ids" placeholder="${_("Email Addresses/Usernames")}" spellcheck="false"></textarea>
|
||||
</label>
|
||||
<input type="hidden" id="is_course_white_label" value="${section_data['is_white_label']}">
|
||||
% if section_data['is_white_label']:
|
||||
|
||||
<div class="role">
|
||||
<label>
|
||||
${_("Enter the reason why the students are to be manually enrolled or unenrolled.")}
|
||||
${_("This cannot be left blank and will be recorded and presented in Enrollment Reports.")}
|
||||
${_("Therefore, please give enough detail to account for this action.")}
|
||||
<textarea rows="2" id="reason-field-id" name="reason-field" placeholder="${_('Reason')}" spellcheck="false"></textarea>
|
||||
${_("Role of the users being enrolled.")}
|
||||
<select name="role">
|
||||
% for role in section_data['enrollment_role_choices']:
|
||||
<option value="${role}">${role}</option>
|
||||
% endfor
|
||||
</select>
|
||||
</label>
|
||||
%endif
|
||||
</div>
|
||||
|
||||
<label>
|
||||
${_("Enter the reason why the students are to be manually enrolled or unenrolled.")}
|
||||
${_("This cannot be left blank and will be recorded and presented in Enrollment Reports.")}
|
||||
${_("Therefore, please give enough detail to account for this action.")}
|
||||
<textarea rows="2" id="reason-field-id" name="reason-field" placeholder="${_('Reason')}" spellcheck="false"></textarea>
|
||||
</label>
|
||||
<div class="enroll-option">
|
||||
<label class="has-hint">
|
||||
<input type="checkbox" name="auto-enroll" id="auto-enroll" value="Auto-Enroll" checked="yes" aria-describedby="heading-batch-enrollment">
|
||||
|
||||
Reference in New Issue
Block a user