def test_update_creator_group(self): with mock.patch.dict('django.conf.settings.FEATURES', {"ENABLE_CREATOR_GROUP": True}): self.assertFalse(is_user_in_creator_group(self.user)) update_course_creator_group(self.admin, self.user, True) self.assertTrue(is_user_in_creator_group(self.user)) update_course_creator_group(self.admin, self.user, False) self.assertFalse(is_user_in_creator_group(self.user))
def test_update_creator_group(self): with mock.patch.dict('django.conf.settings.MITX_FEATURES', {"ENABLE_CREATOR_GROUP": True}): self.assertFalse(is_user_in_creator_group(self.user)) update_course_creator_group(self.admin, self.user, True) self.assertTrue(is_user_in_creator_group(self.user)) update_course_creator_group(self.admin, self.user, False) self.assertFalse(is_user_in_creator_group(self.user))
def test_creator_group_enabled_but_empty(self): """ Tests creator group feature on, but group empty. """ with mock.patch.dict('django.conf.settings.FEATURES', {"ENABLE_CREATOR_GROUP": True}): self.assertFalse(is_user_in_creator_group(self.user)) # Make user staff. This will cause is_user_in_creator_group to return True. self.user.is_staff = True self.assertTrue(is_user_in_creator_group(self.user))
def test_creator_group_enabled_nonempty(self): """ Tests creator group feature on, user added. """ with mock.patch.dict('django.conf.settings.FEATURES', {"ENABLE_CREATOR_GROUP": True}): self.assertTrue(add_user_to_creator_group(self.admin, self.user)) self.assertTrue(is_user_in_creator_group(self.user)) # check that a user who has not been added to the group still returns false user_not_added = User.objects.create_user('testuser2', '*****@*****.**', 'foo2') self.assertFalse(is_user_in_creator_group(user_not_added)) # remove first user from the group and verify that is_user_in_creator_group now returns false remove_user_from_creator_group(self.admin, self.user) self.assertFalse(is_user_in_creator_group(self.user))
def test_add_granted(self): with mock.patch.dict('django.conf.settings.FEATURES', {"ENABLE_CREATOR_GROUP": True}): # Calling add_user_with_status_granted impacts is_user_in_course_group_role. self.assertFalse(is_user_in_creator_group(self.user)) add_user_with_status_granted(self.admin, self.user) self.assertEqual('granted', get_course_creator_status(self.user)) # Calling add again will be a no-op (even if state is different). add_user_with_status_unrequested(self.user) self.assertEqual('granted', get_course_creator_status(self.user)) self.assertTrue(is_user_in_creator_group(self.user))
def test_add_granted(self): with mock.patch.dict('django.conf.settings.MITX_FEATURES', {"ENABLE_CREATOR_GROUP": True}): # Calling add_user_with_status_granted impacts is_user_in_course_group_role. self.assertFalse(is_user_in_creator_group(self.user)) add_user_with_status_granted(self.admin, self.user) self.assertEqual('granted', get_course_creator_status(self.user)) # Calling add again will be a no-op (even if state is different). add_user_with_status_unrequested(self.user) self.assertEqual('granted', get_course_creator_status(self.user)) self.assertTrue(is_user_in_creator_group(self.user))
def test_grant_creator_access(self): """ Test for _grant_instructors_creator_access. """ [creator1, staff1] = self.create_course(1) [creator2, staff2] = self.create_course(2) with mock.patch.dict('django.conf.settings.MITX_FEATURES', {"ENABLE_CREATOR_GROUP": True}): # Initially no creators. self.assertFalse(is_user_in_creator_group(creator1)) self.assertFalse(is_user_in_creator_group(creator2)) self.assertFalse(is_user_in_creator_group(staff1)) self.assertFalse(is_user_in_creator_group(staff2)) admin = User.objects.create_user('populate_creators_command', '*****@*****.**', 'foo') admin.is_staff = True _grant_instructors_creator_access(admin) # Now instructors only are creators. self.assertTrue(is_user_in_creator_group(creator1)) self.assertTrue(is_user_in_creator_group(creator2)) self.assertFalse(is_user_in_creator_group(staff1)) self.assertFalse(is_user_in_creator_group(staff2))
def test_change_status(self): """ Tests that updates to state impact the creator group maintained in authz.py. """ def change_state(state, is_creator): """ Helper method for changing state """ self.table_entry.state = state self.creator_admin.save_model(self.request, self.table_entry, None, True) self.assertEqual(is_creator, is_user_in_creator_group(self.user)) with mock.patch.dict('django.conf.settings.MITX_FEATURES', {"ENABLE_CREATOR_GROUP": True}): # User is initially unrequested. self.assertFalse(is_user_in_creator_group(self.user)) change_state(CourseCreator.GRANTED, True) change_state(CourseCreator.DENIED, False) change_state(CourseCreator.GRANTED, True) change_state(CourseCreator.PENDING, False) change_state(CourseCreator.GRANTED, True) change_state(CourseCreator.UNREQUESTED, False)
def create_new_course(request): if not is_user_in_creator_group(request.user): raise PermissionDenied() # This logic is repeated in xmodule/modulestore/tests/factories.py # so if you change anything here, you need to also change it there. # TODO: write a test that creates two courses, one with the factory and # the other with this method, then compare them to make sure they are # equivalent. template = Location(request.POST['template']) org = request.POST.get('org') number = request.POST.get('number') display_name = request.POST.get('display_name') try: dest_location = Location('i4x', org, number, 'course', Location.clean(display_name)) except InvalidLocationError as error: return HttpResponse(json.dumps({'ErrMsg': "Unable to create course '" + display_name + "'.\n\n" + error.message})) # see if the course already exists existing_course = None try: existing_course = modulestore('direct').get_item(dest_location) except ItemNotFoundError: pass if existing_course is not None: return HttpResponse(json.dumps({'ErrMsg': 'There is already a course defined with this name.'})) course_search_location = ['i4x', dest_location.org, dest_location.course, 'course', None] courses = modulestore().get_items(course_search_location) if len(courses) > 0: return HttpResponse(json.dumps({'ErrMsg': 'There is already a course defined with the same organization and course number.'})) new_course = modulestore('direct').clone_item(template, dest_location) # clone a default 'about' module as well about_template_location = Location(['i4x', 'edx', 'templates', 'about', 'overview']) dest_about_location = dest_location._replace(category='about', name='overview') modulestore('direct').clone_item(about_template_location, dest_about_location) if display_name is not None: new_course.display_name = display_name # set a default start date to now new_course.start = datetime.datetime.now(UTC()) initialize_course_tabs(new_course) create_all_course_groups(request.user, new_course.location) # seed the forums seed_permissions_roles(new_course.location.course_id) return HttpResponse(json.dumps({'id': new_course.location.url()}))
def test_course_creation_disabled(self): """ Tests that the COURSE_CREATION_DISABLED flag overrides course creator group settings. """ with mock.patch.dict('django.conf.settings.FEATURES', {'DISABLE_COURSE_CREATION': True, "ENABLE_CREATOR_GROUP": True}): # Add user to creator group. self.assertTrue(add_user_to_creator_group(self.admin, self.user)) # DISABLE_COURSE_CREATION overrides (user is not marked as staff). self.assertFalse(is_user_in_creator_group(self.user)) # Mark as staff. Now is_user_in_creator_group returns true. self.user.is_staff = True self.assertTrue(is_user_in_creator_group(self.user)) # Remove user from creator group. is_user_in_creator_group still returns true because is_staff=True remove_user_from_creator_group(self.admin, self.user) self.assertTrue(is_user_in_creator_group(self.user))
def create_new_course(request): """ Create a new course """ if not is_user_in_creator_group(request.user): raise PermissionDenied() org = request.POST.get("org") number = request.POST.get("number") display_name = request.POST.get("display_name") try: dest_location = Location("i4x", org, number, "course", Location.clean(display_name)) except InvalidLocationError as error: return JsonResponse( {"ErrMsg": "Unable to create course '{name}'.\n\n{err}".format(name=display_name, err=error.message)} ) # see if the course already exists existing_course = None try: existing_course = modulestore("direct").get_item(dest_location) except ItemNotFoundError: pass if existing_course is not None: return JsonResponse({"ErrMsg": "There is already a course defined with this name."}) course_search_location = ["i4x", dest_location.org, dest_location.course, "course", None] courses = modulestore().get_items(course_search_location) if len(courses) > 0: return JsonResponse( {"ErrMsg": "There is already a course defined with the same organization and course number."} ) # instantiate the CourseDescriptor and then persist it # note: no system to pass if display_name is None: metadata = {} else: metadata = {"display_name": display_name} modulestore("direct").create_and_save_xmodule(dest_location, metadata=metadata) new_course = modulestore("direct").get_item(dest_location) # clone a default 'about' overview module as well dest_about_location = dest_location.replace(category="about", name="overview") overview_template = AboutDescriptor.get_template("overview.yaml") modulestore("direct").create_and_save_xmodule( dest_about_location, system=new_course.system, definition_data=overview_template.get("data") ) initialize_course_tabs(new_course) create_all_course_groups(request.user, new_course.location) # seed the forums seed_permissions_roles(new_course.location.course_id) return JsonResponse({"id": new_course.location.url()})
def test_course_creation_disabled(self): """ Tests that the COURSE_CREATION_DISABLED flag overrides course creator group settings. """ with mock.patch.dict('django.conf.settings.FEATURES', { 'DISABLE_COURSE_CREATION': True, "ENABLE_CREATOR_GROUP": True }): # Add user to creator group. self.assertTrue(add_user_to_creator_group(self.admin, self.user)) # DISABLE_COURSE_CREATION overrides (user is not marked as staff). self.assertFalse(is_user_in_creator_group(self.user)) # Mark as staff. Now is_user_in_creator_group returns true. self.user.is_staff = True self.assertTrue(is_user_in_creator_group(self.user)) # Remove user from creator group. is_user_in_creator_group still returns true because is_staff=True remove_user_from_creator_group(self.admin, self.user) self.assertTrue(is_user_in_creator_group(self.user))
def test_change_status(self, email_user): """ Tests that updates to state impact the creator group maintained in authz.py and that e-mails are sent. """ STUDIO_REQUEST_EMAIL = '*****@*****.**' def change_state(state, is_creator): """ Helper method for changing state """ self.table_entry.state = state self.creator_admin.save_model(self.request, self.table_entry, None, True) self.assertEqual(is_creator, is_user_in_creator_group(self.user)) context = {'studio_request_email': STUDIO_REQUEST_EMAIL} if state == CourseCreator.GRANTED: template = 'emails/course_creator_granted.txt' elif state == CourseCreator.DENIED: template = 'emails/course_creator_denied.txt' else: template = 'emails/course_creator_revoked.txt' email_user.assert_called_with( mock_render_to_string('emails/course_creator_subject.txt', context), mock_render_to_string(template, context), STUDIO_REQUEST_EMAIL ) with mock.patch.dict( 'django.conf.settings.MITX_FEATURES', { "ENABLE_CREATOR_GROUP": True, "STUDIO_REQUEST_EMAIL": STUDIO_REQUEST_EMAIL } ): # User is initially unrequested. self.assertFalse(is_user_in_creator_group(self.user)) change_state(CourseCreator.GRANTED, True) change_state(CourseCreator.DENIED, False) change_state(CourseCreator.GRANTED, True) change_state(CourseCreator.PENDING, False) change_state(CourseCreator.GRANTED, True) change_state(CourseCreator.UNREQUESTED, False)
def change_state_and_verify_email(state, is_creator): """ Changes user state, verifies creator status, and verifies e-mail is sent based on transition """ self._change_state(state) self.assertEqual(is_creator, is_user_in_creator_group(self.user)) context = {'studio_request_email': self.studio_request_email} if state == CourseCreator.GRANTED: template = 'emails/course_creator_granted.txt' elif state == CourseCreator.DENIED: template = 'emails/course_creator_denied.txt' else: template = 'emails/course_creator_revoked.txt' email_user.assert_called_with( mock_render_to_string('emails/course_creator_subject.txt', context), mock_render_to_string(template, context), self.studio_request_email )
def change_state(state, is_creator): """ Helper method for changing state """ self.table_entry.state = state self.creator_admin.save_model(self.request, self.table_entry, None, True) self.assertEqual(is_creator, is_user_in_creator_group(self.user)) context = {'studio_request_email': STUDIO_REQUEST_EMAIL} if state == CourseCreator.GRANTED: template = 'emails/course_creator_granted.txt' elif state == CourseCreator.DENIED: template = 'emails/course_creator_denied.txt' else: template = 'emails/course_creator_revoked.txt' email_user.assert_called_with( mock_render_to_string('emails/course_creator_subject.txt', context), mock_render_to_string(template, context), STUDIO_REQUEST_EMAIL )
def test_change_status(self, email_user): """ Tests that updates to state impact the creator group maintained in authz.py and that e-mails are sent. """ def change_state_and_verify_email(state, is_creator): """ Changes user state, verifies creator status, and verifies e-mail is sent based on transition """ self._change_state(state) self.assertEqual(is_creator, is_user_in_creator_group(self.user)) context = {'studio_request_email': self.studio_request_email} if state == CourseCreator.GRANTED: template = 'emails/course_creator_granted.txt' elif state == CourseCreator.DENIED: template = 'emails/course_creator_denied.txt' else: template = 'emails/course_creator_revoked.txt' email_user.assert_called_with( mock_render_to_string('emails/course_creator_subject.txt', context), mock_render_to_string(template, context), self.studio_request_email ) with mock.patch.dict('django.conf.settings.MITX_FEATURES', self.enable_creator_group_patch): # User is initially unrequested. self.assertFalse(is_user_in_creator_group(self.user)) change_state_and_verify_email(CourseCreator.GRANTED, True) change_state_and_verify_email(CourseCreator.DENIED, False) change_state_and_verify_email(CourseCreator.GRANTED, True) change_state_and_verify_email(CourseCreator.PENDING, False) change_state_and_verify_email(CourseCreator.GRANTED, True) change_state_and_verify_email(CourseCreator.UNREQUESTED, False) change_state_and_verify_email(CourseCreator.DENIED, False)
def create_new_course(request): """ Create a new course. Returns the URL for the course overview page. """ if not is_user_in_creator_group(request.user): raise PermissionDenied() org = request.json.get('org') number = request.json.get('number') display_name = request.json.get('display_name') run = request.json.get('run') try: dest_location = Location('i4x', org, number, 'course', run) except InvalidLocationError as error: return JsonResponse({ "ErrMsg": _("Unable to create course '{name}'.\n\n{err}").format( name=display_name, err=error.message)}) # see if the course already exists existing_course = None try: existing_course = modulestore('direct').get_item(dest_location) except ItemNotFoundError: pass if existing_course is not None: return JsonResponse({ 'ErrMsg': _('There is already a course defined with the same ' 'organization, course number, and course run. Please ' 'change either organization or course number to be ' 'unique.'), 'OrgErrMsg': _('Please change either the organization or ' 'course number so that it is unique.'), 'CourseErrMsg': _('Please change either the organization or ' 'course number so that it is unique.'), }) # dhm: this query breaks the abstraction, but I'll fix it when I do my suspended refactoring of this # file for new locators. get_items should accept a query rather than requiring it be a legal location course_search_location = bson.son.SON({ '_id.tag': 'i4x', # cannot pass regex to Location constructor; thus this hack '_id.org': re.compile('^{}$'.format(dest_location.org), re.IGNORECASE), '_id.course': re.compile('^{}$'.format(dest_location.course), re.IGNORECASE), '_id.category': 'course', }) courses = modulestore().collection.find(course_search_location, fields=('_id')) if courses.count() > 0: return JsonResponse({ 'ErrMsg': _('There is already a course defined with the same ' 'organization and course number. Please ' 'change at least one field to be unique.'), 'OrgErrMsg': _('Please change either the organization or ' 'course number so that it is unique.'), 'CourseErrMsg': _('Please change either the organization or ' 'course number so that it is unique.'), }) # instantiate the CourseDescriptor and then persist it # note: no system to pass if display_name is None: metadata = {} else: metadata = {'display_name': display_name} modulestore('direct').create_and_save_xmodule( dest_location, metadata=metadata ) new_course = modulestore('direct').get_item(dest_location) # clone a default 'about' overview module as well dest_about_location = dest_location.replace( category='about', name='overview' ) overview_template = AboutDescriptor.get_template('overview.yaml') modulestore('direct').create_and_save_xmodule( dest_about_location, system=new_course.system, definition_data=overview_template.get('data') ) initialize_course_tabs(new_course) create_all_course_groups(request.user, new_course.location) # seed the forums seed_permissions_roles(new_course.location.course_id) # auto-enroll the course creator in the course so that "View Live" will # work. CourseEnrollment.enroll(request.user, new_course.location.course_id) new_location = loc_mapper().translate_location(new_course.location.course_id, new_course.location, False, True) return JsonResponse({'url': new_location.url_reverse("course/", "")})
def create_new_course(request): if not is_user_in_creator_group(request.user): raise PermissionDenied() # This logic is repeated in xmodule/modulestore/tests/factories.py # so if you change anything here, you need to also change it there. # TODO: write a test that creates two courses, one with the factory and # the other with this method, then compare them to make sure they are # equivalent. template = Location(request.POST['template']) org = request.POST.get('org') number = request.POST.get('number') display_name = request.POST.get('display_name') try: dest_location = Location('i4x', org, number, 'course', Location.clean(display_name)) except InvalidLocationError as error: return HttpResponse( json.dumps({ 'ErrMsg': "Unable to create course '" + display_name + "'.\n\n" + error.message })) # see if the course already exists existing_course = None try: existing_course = modulestore('direct').get_item(dest_location) except ItemNotFoundError: pass if existing_course is not None: return HttpResponse( json.dumps({ 'ErrMsg': 'There is already a course defined with this name.' })) course_search_location = [ 'i4x', dest_location.org, dest_location.course, 'course', None ] courses = modulestore().get_items(course_search_location) if len(courses) > 0: return HttpResponse( json.dumps({ 'ErrMsg': 'There is already a course defined with the same organization and course number.' })) new_course = modulestore('direct').clone_item(template, dest_location) # clone a default 'about' module as well about_template_location = Location( ['i4x', 'edx', 'templates', 'about', 'overview']) dest_about_location = dest_location._replace(category='about', name='overview') modulestore('direct').clone_item(about_template_location, dest_about_location) if display_name is not None: new_course.display_name = display_name # set a default start date to now new_course.start = datetime.datetime.now(UTC()) initialize_course_tabs(new_course) create_all_course_groups(request.user, new_course.location) # seed the forums seed_permissions_roles(new_course.location.course_id) return HttpResponse(json.dumps({'id': new_course.location.url()}))
def change_state(state, is_creator): """ Helper method for changing state """ self.table_entry.state = state self.creator_admin.save_model(self.request, self.table_entry, None, True) self.assertEqual(is_creator, is_user_in_creator_group(self.user))
def create_new_course(request): """ Create a new course """ if not is_user_in_creator_group(request.user): raise PermissionDenied() org = request.POST.get('org') number = request.POST.get('number') display_name = request.POST.get('display_name') run = request.POST.get('run') try: dest_location = Location('i4x', org, number, 'course', run) except InvalidLocationError as error: return JsonResponse({ "ErrMsg": _("Unable to create course '{name}'.\n\n{err}").format( name=display_name, err=error.message)}) # see if the course already exists existing_course = None try: existing_course = modulestore('direct').get_item(dest_location) except ItemNotFoundError: pass if existing_course is not None: return JsonResponse({ 'ErrMsg': _('There is already a course defined with the same ' 'organization, course number, and course run. Please ' 'change either organization or course number to be ' 'unique.'), 'OrgErrMsg': _('Please change either the organization or ' 'course number so that it is unique.'), 'CourseErrMsg': _('Please change either the organization or ' 'course number so that it is unique.'), }) course_search_location = ['i4x', dest_location.org, dest_location.course, 'course', None ] courses = modulestore().get_items(course_search_location) if len(courses) > 0: return JsonResponse({ 'ErrMsg': _('There is already a course defined with the same ' 'organization and course number. Please ' 'change at least one field to be unique.'), 'OrgErrMsg': _('Please change either the organization or ' 'course number so that it is unique.'), 'CourseErrMsg': _('Please change either the organization or ' 'course number so that it is unique.'), }) # instantiate the CourseDescriptor and then persist it # note: no system to pass if display_name is None: metadata = {} else: metadata = {'display_name': display_name} modulestore('direct').create_and_save_xmodule( dest_location, metadata=metadata ) new_course = modulestore('direct').get_item(dest_location) # clone a default 'about' overview module as well dest_about_location = dest_location.replace( category='about', name='overview' ) overview_template = AboutDescriptor.get_template('overview.yaml') modulestore('direct').create_and_save_xmodule( dest_about_location, system=new_course.system, definition_data=overview_template.get('data') ) initialize_course_tabs(new_course) create_all_course_groups(request.user, new_course.location) # seed the forums seed_permissions_roles(new_course.location.course_id) # auto-enroll the course creator in the course so that "View Live" will # work. CourseEnrollment.enroll(request.user, new_course.location.course_id) return JsonResponse({'id': new_course.location.url()})
def create_new_course(request): """ Create a new course. Returns the URL for the course overview page. """ if not is_user_in_creator_group(request.user): raise PermissionDenied() org = request.json.get('org') number = request.json.get('number') display_name = request.json.get('display_name') run = request.json.get('run') try: dest_location = Location('i4x', org, number, 'course', run) except InvalidLocationError as error: return JsonResponse({ "ErrMsg": _("Unable to create course '{name}'.\n\n{err}").format( name=display_name, err=error.message)}) # see if the course already exists existing_course = None try: existing_course = modulestore('direct').get_item(dest_location) except ItemNotFoundError: pass if existing_course is not None: return JsonResponse({ 'ErrMsg': _( 'There is already a course defined with the same ' 'organization, course number, and course run. Please ' 'change either organization or course number to be ' 'unique.' ), 'OrgErrMsg': _( 'Please change either the organization or ' 'course number so that it is unique.' ), 'CourseErrMsg': _( 'Please change either the organization or ' 'course number so that it is unique.' ), }) # dhm: this query breaks the abstraction, but I'll fix it when I do my suspended refactoring of this # file for new locators. get_items should accept a query rather than requiring it be a legal location course_search_location = bson.son.SON({ '_id.tag': 'i4x', # cannot pass regex to Location constructor; thus this hack '_id.org': re.compile('^{}$'.format(dest_location.org), re.IGNORECASE), '_id.course': re.compile('^{}$'.format(dest_location.course), re.IGNORECASE), '_id.category': 'course', }) courses = modulestore().collection.find(course_search_location, fields=('_id')) if courses.count() > 0: return JsonResponse({ 'ErrMsg': _( 'There is already a course defined with the same ' 'organization and course number. Please ' 'change at least one field to be unique.'), 'OrgErrMsg': _( 'Please change either the organization or ' 'course number so that it is unique.'), 'CourseErrMsg': _( 'Please change either the organization or ' 'course number so that it is unique.'), }) # instantiate the CourseDescriptor and then persist it # note: no system to pass if display_name is None: metadata = {} else: metadata = {'display_name': display_name} modulestore('direct').create_and_save_xmodule( dest_location, metadata=metadata ) new_course = modulestore('direct').get_item(dest_location) # clone a default 'about' overview module as well dest_about_location = dest_location.replace( category='about', name='overview' ) overview_template = AboutDescriptor.get_template('overview.yaml') modulestore('direct').create_and_save_xmodule( dest_about_location, system=new_course.system, definition_data=overview_template.get('data') ) initialize_course_tabs(new_course) new_location = loc_mapper().translate_location(new_course.location.course_id, new_course.location, False, True) create_all_course_groups(request.user, new_location) # seed the forums seed_permissions_roles(new_course.location.course_id) # auto-enroll the course creator in the course so that "View Live" will # work. CourseEnrollment.enroll(request.user, new_course.location.course_id) return JsonResponse({'url': new_location.url_reverse("course/", "")})
def test_creator_group_not_enabled(self): """ Tests that is_user_in_creator_group always returns True if ENABLE_CREATOR_GROUP and DISABLE_COURSE_CREATION are both not turned on. """ self.assertTrue(is_user_in_creator_group(self.user))