add role and expose reason field to Instructor Dashboard Manual Enrollment

WL-1473
This commit is contained in:
Ibrahim
2018-03-06 16:35:42 +05:00
parent 4e82734cc2
commit 2bb790680a
10 changed files with 144 additions and 58 deletions

View File

@@ -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),
),
]

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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,

View File

@@ -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

View File

@@ -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,

View File

@@ -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()

View File

@@ -615,6 +615,13 @@
width: 90%;
}
.role {
margin: 20px 0;
select {
margin-left: 15px;
}
}
input {
margin-right: ($baseline/4);
}

View File

@@ -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">