Commit 58bde7f3 by Amir Qayyum Khan

Excluded admins, staff and coach of master course from max enrollment

parent d9ad47c9
...@@ -879,6 +879,33 @@ class CourseEnrollmentManager(models.Manager): ...@@ -879,6 +879,33 @@ class CourseEnrollmentManager(models.Manager):
return enrollment_number return enrollment_number
def num_enrolled_in_exclude_admins(self, course_id):
"""
Returns the count of active enrollments in a course excluding instructors, staff and CCX coaches.
Arguments:
course_id (CourseLocator): course_id to return enrollments (count).
Returns:
int: Count of enrollments excluding staff, instructors and CCX coaches.
"""
# To avoid circular imports.
from student.roles import CourseCcxCoachRole, CourseInstructorRole, CourseStaffRole
course_locator = course_id
if getattr(course_id, 'ccx', None):
course_locator = course_id.to_course_locator()
staff = CourseStaffRole(course_locator).users_with_role()
admins = CourseInstructorRole(course_locator).users_with_role()
coaches = CourseCcxCoachRole(course_locator).users_with_role()
return super(CourseEnrollmentManager, self).get_queryset().filter(
course_id=course_id,
is_active=1,
).exclude(user__in=staff).exclude(user__in=admins).exclude(user__in=coaches).count()
def is_course_full(self, course): def is_course_full(self, course):
""" """
Returns a boolean value regarding whether a course has already reached it's max enrollment Returns a boolean value regarding whether a course has already reached it's max enrollment
...@@ -886,7 +913,8 @@ class CourseEnrollmentManager(models.Manager): ...@@ -886,7 +913,8 @@ class CourseEnrollmentManager(models.Manager):
""" """
is_course_full = False is_course_full = False
if course.max_student_enrollments_allowed is not None: if course.max_student_enrollments_allowed is not None:
is_course_full = self.num_enrolled_in(course.id) >= course.max_student_enrollments_allowed is_course_full = self.num_enrolled_in_exclude_admins(course.id) >= course.max_student_enrollments_allowed
return is_course_full return is_course_full
def users_enrolled_in(self, course_id): def users_enrolled_in(self, course_id):
......
...@@ -13,7 +13,11 @@ from xmodule.modulestore.tests.factories import CourseFactory ...@@ -13,7 +13,11 @@ from xmodule.modulestore.tests.factories import CourseFactory
from util.testing import UrlResetMixin from util.testing import UrlResetMixin
from embargo.test_utils import restrict_course from embargo.test_utils import restrict_course
from student.tests.factories import UserFactory, CourseModeFactory from student.tests.factories import UserFactory, CourseModeFactory
from student.models import CourseEnrollment from student.models import CourseEnrollment, CourseFullError
from student.roles import (
CourseInstructorRole,
CourseStaffRole,
)
@ddt.ddt @ddt.ddt
...@@ -31,6 +35,7 @@ class EnrollmentTest(UrlResetMixin, SharedModuleStoreTestCase): ...@@ -31,6 +35,7 @@ class EnrollmentTest(UrlResetMixin, SharedModuleStoreTestCase):
def setUpClass(cls): def setUpClass(cls):
super(EnrollmentTest, cls).setUpClass() super(EnrollmentTest, cls).setUpClass()
cls.course = CourseFactory.create() cls.course = CourseFactory.create()
cls.course_limited = CourseFactory.create()
@patch.dict(settings.FEATURES, {'EMBARGO': True}) @patch.dict(settings.FEATURES, {'EMBARGO': True})
def setUp(self): def setUp(self):
...@@ -38,7 +43,8 @@ class EnrollmentTest(UrlResetMixin, SharedModuleStoreTestCase): ...@@ -38,7 +43,8 @@ class EnrollmentTest(UrlResetMixin, SharedModuleStoreTestCase):
super(EnrollmentTest, self).setUp('embargo') super(EnrollmentTest, self).setUp('embargo')
self.user = UserFactory.create(username=self.USERNAME, email=self.EMAIL, password=self.PASSWORD) self.user = UserFactory.create(username=self.USERNAME, email=self.EMAIL, password=self.PASSWORD)
self.client.login(username=self.USERNAME, password=self.PASSWORD) self.client.login(username=self.USERNAME, password=self.PASSWORD)
self.course_limited.max_student_enrollments_allowed = 1
self.store.update_item(self.course_limited, self.user.id)
self.urls = [ self.urls = [
reverse('course_modes_choose', kwargs={'course_id': unicode(self.course.id)}) reverse('course_modes_choose', kwargs={'course_id': unicode(self.course.id)})
] ]
...@@ -202,6 +208,48 @@ class EnrollmentTest(UrlResetMixin, SharedModuleStoreTestCase): ...@@ -202,6 +208,48 @@ class EnrollmentTest(UrlResetMixin, SharedModuleStoreTestCase):
resp = self._change_enrollment('unenroll', course_id="edx/") resp = self._change_enrollment('unenroll', course_id="edx/")
self.assertEqual(resp.status_code, 400) self.assertEqual(resp.status_code, 400)
def test_enrollment_limit(self):
"""
Assert that in a course with max student limit set to 1, we can enroll staff and instructor along with
student. To make sure course full check excludes staff and instructors.
"""
self.assertEqual(self.course_limited.max_student_enrollments_allowed, 1)
user1 = UserFactory.create(username="tester1", email="tester1@e.com", password="test")
user2 = UserFactory.create(username="tester2", email="tester2@e.com", password="test")
# create staff on course.
staff = UserFactory.create(username="staff", email="staff@e.com", password="test")
role = CourseStaffRole(self.course_limited.id)
role.add_users(staff)
# create instructor on course.
instructor = UserFactory.create(username="instructor", email="instructor@e.com", password="test")
role = CourseInstructorRole(self.course_limited.id)
role.add_users(instructor)
CourseEnrollment.enroll(staff, self.course_limited.id, check_access=True)
CourseEnrollment.enroll(instructor, self.course_limited.id, check_access=True)
self.assertTrue(
CourseEnrollment.objects.filter(course_id=self.course_limited.id, user=staff).exists()
)
self.assertTrue(
CourseEnrollment.objects.filter(course_id=self.course_limited.id, user=instructor).exists()
)
CourseEnrollment.enroll(user1, self.course_limited.id, check_access=True)
self.assertTrue(
CourseEnrollment.objects.filter(course_id=self.course_limited.id, user=user1).exists()
)
with self.assertRaises(CourseFullError):
CourseEnrollment.enroll(user2, self.course_limited.id, check_access=True)
self.assertFalse(
CourseEnrollment.objects.filter(course_id=self.course_limited.id, user=user2).exists()
)
def _change_enrollment(self, action, course_id=None, email_opt_in=None): def _change_enrollment(self, action, course_id=None, email_opt_in=None):
"""Change the student's enrollment status in a course. """Change the student's enrollment status in a course.
......
...@@ -46,7 +46,6 @@ from student.tests.factories import ( ...@@ -46,7 +46,6 @@ from student.tests.factories import (
from xmodule.x_module import XModuleMixin from xmodule.x_module import XModuleMixin
from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import ( from xmodule.modulestore.tests.django_utils import (
ModuleStoreTestCase, ModuleStoreTestCase,
SharedModuleStoreTestCase, SharedModuleStoreTestCase,
...@@ -134,26 +133,6 @@ class TestAdminAccessCoachDashboard(CcxTestCase, LoginEnrollmentTestCase): ...@@ -134,26 +133,6 @@ class TestAdminAccessCoachDashboard(CcxTestCase, LoginEnrollmentTestCase):
""" """
MODULESTORE = TEST_DATA_SPLIT_MODULESTORE MODULESTORE = TEST_DATA_SPLIT_MODULESTORE
def make_staff(self):
"""
create staff user
"""
staff = AdminFactory.create(password="test")
role = CourseStaffRole(self.course.id)
role.add_users(staff)
return staff
def make_instructor(self):
"""
create staff instructor
"""
instructor = AdminFactory.create(password="test")
role = CourseInstructorRole(self.course.id)
role.add_users(instructor)
return instructor
def test_staff_access_coach_dashboard(self): def test_staff_access_coach_dashboard(self):
""" """
User is staff, should access coach dashboard. User is staff, should access coach dashboard.
...@@ -607,10 +586,14 @@ class TestCoachDashboard(CcxTestCase, LoginEnrollmentTestCase): ...@@ -607,10 +586,14 @@ class TestCoachDashboard(CcxTestCase, LoginEnrollmentTestCase):
# create ccx and limit the maximum amount of students that can be enrolled to 2 # create ccx and limit the maximum amount of students that can be enrolled to 2
ccx = self.make_ccx(max_students_allowed=2) ccx = self.make_ccx(max_students_allowed=2)
ccx_course_key = CCXLocator.from_course_locator(self.course.id, ccx.id) ccx_course_key = CCXLocator.from_course_locator(self.course.id, ccx.id)
staff = self.make_staff()
instructor = self.make_instructor()
# create some users # create some users
students = [ students = [instructor, staff, self.coach] + [
UserFactory.create(is_staff=False) for _ in range(3) UserFactory.create(is_staff=False) for _ in range(3)
] ]
url = reverse( url = reverse(
'ccx_invite', 'ccx_invite',
kwargs={'course_id': ccx_course_key} kwargs={'course_id': ccx_course_key}
...@@ -621,15 +604,26 @@ class TestCoachDashboard(CcxTestCase, LoginEnrollmentTestCase): ...@@ -621,15 +604,26 @@ class TestCoachDashboard(CcxTestCase, LoginEnrollmentTestCase):
} }
response = self.client.post(url, data=data, follow=True) response = self.client.post(url, data=data, follow=True)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
# a CcxMembership exists for the first two students but not the third # even if course is coach can enroll staff and admins of master course into ccx
self.assertTrue( self.assertTrue(
CourseEnrollment.objects.filter(course_id=ccx_course_key, user=students[0]).exists() CourseEnrollment.objects.filter(course_id=ccx_course_key, user=instructor).exists()
) )
self.assertTrue( self.assertTrue(
CourseEnrollment.objects.filter(course_id=ccx_course_key, user=students[1]).exists() CourseEnrollment.objects.filter(course_id=ccx_course_key, user=staff).exists()
)
self.assertTrue(
CourseEnrollment.objects.filter(course_id=ccx_course_key, user=self.coach).exists()
)
# a CcxMembership exists for the first five students but not the sixth
self.assertTrue(
CourseEnrollment.objects.filter(course_id=ccx_course_key, user=students[3]).exists()
)
self.assertTrue(
CourseEnrollment.objects.filter(course_id=ccx_course_key, user=students[4]).exists()
) )
self.assertFalse( self.assertFalse(
CourseEnrollment.objects.filter(course_id=ccx_course_key, user=students[2]).exists() CourseEnrollment.objects.filter(course_id=ccx_course_key, user=students[5]).exists()
) )
def test_manage_student_enrollment_limit(self): def test_manage_student_enrollment_limit(self):
...@@ -641,11 +635,13 @@ class TestCoachDashboard(CcxTestCase, LoginEnrollmentTestCase): ...@@ -641,11 +635,13 @@ class TestCoachDashboard(CcxTestCase, LoginEnrollmentTestCase):
""" """
students_limit = 1 students_limit = 1
self.make_coach() self.make_coach()
staff = self.make_staff()
ccx = self.make_ccx(max_students_allowed=students_limit) ccx = self.make_ccx(max_students_allowed=students_limit)
ccx_course_key = CCXLocator.from_course_locator(self.course.id, ccx.id) ccx_course_key = CCXLocator.from_course_locator(self.course.id, ccx.id)
students = [ students = [
UserFactory.create(is_staff=False) for _ in range(2) UserFactory.create(is_staff=False) for _ in range(2)
] ]
url = reverse( url = reverse(
'ccx_manage_student', 'ccx_manage_student',
kwargs={'course_id': CCXLocator.from_course_locator(self.course.id, ccx.id)} kwargs={'course_id': CCXLocator.from_course_locator(self.course.id, ccx.id)}
...@@ -653,7 +649,7 @@ class TestCoachDashboard(CcxTestCase, LoginEnrollmentTestCase): ...@@ -653,7 +649,7 @@ class TestCoachDashboard(CcxTestCase, LoginEnrollmentTestCase):
# enroll the first student # enroll the first student
data = { data = {
'student-action': 'add', 'student-action': 'add',
'student-id': u','.join([students[0].email, ]), 'student-id': students[0].email,
} }
response = self.client.post(url, data=data, follow=True) response = self.client.post(url, data=data, follow=True)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
...@@ -661,11 +657,12 @@ class TestCoachDashboard(CcxTestCase, LoginEnrollmentTestCase): ...@@ -661,11 +657,12 @@ class TestCoachDashboard(CcxTestCase, LoginEnrollmentTestCase):
self.assertTrue( self.assertTrue(
CourseEnrollment.objects.filter(course_id=ccx_course_key, user=students[0]).exists() CourseEnrollment.objects.filter(course_id=ccx_course_key, user=students[0]).exists()
) )
# try to enroll the second student without success # try to enroll the second student without success
# enroll the first student # enroll the first student
data = { data = {
'student-action': 'add', 'student-action': 'add',
'student-id': u','.join([students[1].email, ]), 'student-id': students[1].email,
} }
response = self.client.post(url, data=data, follow=True) response = self.client.post(url, data=data, follow=True)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
...@@ -678,6 +675,27 @@ class TestCoachDashboard(CcxTestCase, LoginEnrollmentTestCase): ...@@ -678,6 +675,27 @@ class TestCoachDashboard(CcxTestCase, LoginEnrollmentTestCase):
) )
self.assertContains(response, error_message, status_code=200) self.assertContains(response, error_message, status_code=200)
# try to enroll the 3rd student which is staff
data = {
'student-action': 'add',
'student-id': staff.email,
}
response = self.client.post(url, data=data, follow=True)
self.assertEqual(response.status_code, 200)
# staff gets enroll
self.assertTrue(
CourseEnrollment.objects.filter(course_id=ccx_course_key, user=staff).exists()
)
self.assertEqual(CourseEnrollment.objects.num_enrolled_in_exclude_admins(ccx_course_key), 1)
# asert that number of enroll is still 0 because staff and instructor do not count.
CourseEnrollment.enroll(staff, self.course.id)
self.assertEqual(CourseEnrollment.objects.num_enrolled_in_exclude_admins(self.course.id), 0)
# assert that handles wrong ccx id code
ccx_course_key_fake = CCXLocator.from_course_locator(self.course.id, 55)
self.assertEqual(CourseEnrollment.objects.num_enrolled_in_exclude_admins(ccx_course_key_fake), 0)
@ddt.data( @ddt.data(
('ccx_invite', True, 1, 'student-ids', ('enrollment-button', 'Unenroll')), ('ccx_invite', True, 1, 'student-ids', ('enrollment-button', 'Unenroll')),
('ccx_invite', False, 0, 'student-ids', ('enrollment-button', 'Unenroll')), ('ccx_invite', False, 0, 'student-ids', ('enrollment-button', 'Unenroll')),
......
...@@ -6,15 +6,13 @@ import pytz ...@@ -6,15 +6,13 @@ import pytz
from django.conf import settings from django.conf import settings
from lms.djangoapps.ccx.overrides import override_field_for_ccx
from lms.djangoapps.ccx.tests.factories import CcxFactory
from student.roles import ( from student.roles import (
CourseCcxCoachRole, CourseCcxCoachRole,
CourseInstructorRole, CourseInstructorRole,
CourseStaffRole CourseStaffRole
) )
from student.tests.factories import ( from student.tests.factories import (
UserFactory, UserFactory
) )
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import ( from xmodule.modulestore.tests.django_utils import (
...@@ -26,6 +24,9 @@ from xmodule.modulestore.tests.factories import ( ...@@ -26,6 +24,9 @@ from xmodule.modulestore.tests.factories import (
ItemFactory, ItemFactory,
) )
from lms.djangoapps.ccx.overrides import override_field_for_ccx
from lms.djangoapps.ccx.tests.factories import CcxFactory
class CcxTestCase(SharedModuleStoreTestCase): class CcxTestCase(SharedModuleStoreTestCase):
""" """
...@@ -78,7 +79,6 @@ class CcxTestCase(SharedModuleStoreTestCase): ...@@ -78,7 +79,6 @@ class CcxTestCase(SharedModuleStoreTestCase):
Set up tests Set up tests
""" """
super(CcxTestCase, self).setUp() super(CcxTestCase, self).setUp()
# Create instructor account # Create instructor account
self.coach = UserFactory.create() self.coach = UserFactory.create()
# create an instance of modulestore # create an instance of modulestore
......
...@@ -32,7 +32,11 @@ from instructor.views.tools import get_student_from_identifier ...@@ -32,7 +32,11 @@ from instructor.views.tools import get_student_from_identifier
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.djangoapps.content.course_structures.models import CourseStructure from openedx.core.djangoapps.content.course_structures.models import CourseStructure
from student.models import CourseEnrollment, CourseEnrollmentException from student.models import CourseEnrollment, CourseEnrollmentException
from student.roles import CourseCcxCoachRole from student.roles import (
CourseCcxCoachRole,
CourseInstructorRole,
CourseStaffRole
)
from lms.djangoapps.ccx.overrides import get_override_for_ccx from lms.djangoapps.ccx.overrides import get_override_for_ccx
from lms.djangoapps.ccx.custom_exception import CCXUserValidationException from lms.djangoapps.ccx.custom_exception import CCXUserValidationException
...@@ -181,7 +185,7 @@ def get_ccx_by_ccx_id(course, coach, ccx_id): ...@@ -181,7 +185,7 @@ def get_ccx_by_ccx_id(course, coach, ccx_id):
return ccx return ccx
def get_valid_student_email(identifier): def get_valid_student_with_email(identifier):
""" """
Helper function to get an user email from an identifier and validate it. Helper function to get an user email from an identifier and validate it.
...@@ -195,7 +199,9 @@ def get_valid_student_email(identifier): ...@@ -195,7 +199,9 @@ def get_valid_student_email(identifier):
identifier (str): Username or email of the user to enroll identifier (str): Username or email of the user to enroll
Returns: Returns:
str: A validated email for the user to enroll (tuple): tuple containing:
email (str): A validated email for the user to enroll.
user (User): A valid User object or None.
Raises: Raises:
CCXUserValidationException: if the username is not found or the email CCXUserValidationException: if the username is not found or the email
...@@ -212,10 +218,10 @@ def get_valid_student_email(identifier): ...@@ -212,10 +218,10 @@ def get_valid_student_email(identifier):
validate_email(email) validate_email(email)
except ValidationError: except ValidationError:
raise CCXUserValidationException('Could not find a user with name or email "{0}" '.format(identifier)) raise CCXUserValidationException('Could not find a user with name or email "{0}" '.format(identifier))
return email return email, user
def ccx_students_enrolling_center(action, identifiers, email_students, course_key, email_params): def ccx_students_enrolling_center(action, identifiers, email_students, course_key, email_params, coach):
""" """
Function to enroll/add or unenroll/revoke students. Function to enroll/add or unenroll/revoke students.
...@@ -231,6 +237,7 @@ def ccx_students_enrolling_center(action, identifiers, email_students, course_ke ...@@ -231,6 +237,7 @@ def ccx_students_enrolling_center(action, identifiers, email_students, course_ke
email_students (bool): Flag to send an email to students email_students (bool): Flag to send an email to students
course_key (CCXLocator): a CCX course key course_key (CCXLocator): a CCX course key
email_params (dict): dictionary of settings for the email to be sent email_params (dict): dictionary of settings for the email to be sent
coach (User): ccx coach
Returns: Returns:
list: list of error list: list of error
...@@ -239,24 +246,32 @@ def ccx_students_enrolling_center(action, identifiers, email_students, course_ke ...@@ -239,24 +246,32 @@ def ccx_students_enrolling_center(action, identifiers, email_students, course_ke
if action == 'Enroll' or action == 'add': if action == 'Enroll' or action == 'add':
ccx_course_overview = CourseOverview.get_from_id(course_key) ccx_course_overview = CourseOverview.get_from_id(course_key)
course_locator = course_key.to_course_locator()
staff = CourseStaffRole(course_locator).users_with_role()
admins = CourseInstructorRole(course_locator).users_with_role()
for identifier in identifiers: for identifier in identifiers:
if CourseEnrollment.objects.is_course_full(ccx_course_overview): must_enroll = False
error = _('The course is full: the limit is {max_student_enrollments_allowed}').format(
max_student_enrollments_allowed=ccx_course_overview.max_student_enrollments_allowed)
log.info("%s", error)
errors.append(error)
break
try: try:
email = get_valid_student_email(identifier) email, student = get_valid_student_with_email(identifier)
if student:
must_enroll = student in staff or student in admins or student == coach
except CCXUserValidationException as exp: except CCXUserValidationException as exp:
log.info("%s", exp) log.info("%s", exp)
errors.append("{0}".format(exp)) errors.append("{0}".format(exp))
continue continue
if CourseEnrollment.objects.is_course_full(ccx_course_overview) and not must_enroll:
error = _('The course is full: the limit is {max_student_enrollments_allowed}').format(
max_student_enrollments_allowed=ccx_course_overview.max_student_enrollments_allowed)
log.info("%s", error)
errors.append(error)
break
enroll_email(course_key, email, auto_enroll=True, email_students=email_students, email_params=email_params) enroll_email(course_key, email, auto_enroll=True, email_students=email_students, email_params=email_params)
elif action == 'Unenroll' or action == 'revoke': elif action == 'Unenroll' or action == 'revoke':
for identifier in identifiers: for identifier in identifiers:
try: try:
email = get_valid_student_email(identifier) email, __ = get_valid_student_with_email(identifier)
except CCXUserValidationException as exp: except CCXUserValidationException as exp:
log.info("%s", exp) log.info("%s", exp)
errors.append("{0}".format(exp)) errors.append("{0}".format(exp))
......
...@@ -447,7 +447,7 @@ def ccx_invite(request, course, ccx=None): ...@@ -447,7 +447,7 @@ def ccx_invite(request, course, ccx=None):
course_key = CCXLocator.from_course_locator(course.id, ccx.id) course_key = CCXLocator.from_course_locator(course.id, ccx.id)
email_params = get_email_params(course, auto_enroll=True, course_key=course_key, display_name=ccx.display_name) email_params = get_email_params(course, auto_enroll=True, course_key=course_key, display_name=ccx.display_name)
ccx_students_enrolling_center(action, identifiers, email_students, course_key, email_params) ccx_students_enrolling_center(action, identifiers, email_students, course_key, email_params, ccx.coach)
url = reverse('ccx_coach_dashboard', kwargs={'course_id': course_key}) url = reverse('ccx_coach_dashboard', kwargs={'course_id': course_key})
return redirect(url) return redirect(url)
...@@ -470,7 +470,7 @@ def ccx_student_management(request, course, ccx=None): ...@@ -470,7 +470,7 @@ def ccx_student_management(request, course, ccx=None):
course_key = CCXLocator.from_course_locator(course.id, ccx.id) course_key = CCXLocator.from_course_locator(course.id, ccx.id)
email_params = get_email_params(course, auto_enroll=True, course_key=course_key, display_name=ccx.display_name) email_params = get_email_params(course, auto_enroll=True, course_key=course_key, display_name=ccx.display_name)
errors = ccx_students_enrolling_center(action, identifiers, email_students, course_key, email_params) errors = ccx_students_enrolling_center(action, identifiers, email_students, course_key, email_params, ccx.coach)
for error_message in errors: for error_message in errors:
messages.error(request, error_message) messages.error(request, error_message)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment