def test_course_agnostic(self): lib_key = CourseKey.from_string('library-v1:TestX+lib1+version@519665f6223ebd6980884f2b') lib_key2 = CourseKey.from_string('library-v1:version@519665f6223ebd6980884f2b') lib_key3 = lib_key.course_agnostic() self.assertEqual(lib_key2, lib_key3) self.assertEqual(lib_key3.org, None) self.assertEqual(lib_key3.library, None)
def test_three_courses_but_only_two_unique(self): self.user_create_and_signin(1) self.link_edx_account_to_social(self.users[1], self.BACKEND, self.USERS[1]['FB_ID']) self.set_sharing_preferences(self.users[1], True) self.course_2 = CourseFactory.create(mobile_available=True) self.enroll_in_course(self.users[1], self.course_2) self.enroll_in_course(self.users[1], self.course) self.user_create_and_signin(2) self.link_edx_account_to_social(self.users[2], self.BACKEND, self.USERS[2]['FB_ID']) self.set_sharing_preferences(self.users[2], True) # Enroll another user in course_2 self.enroll_in_course(self.users[2], self.course_2) self.set_facebook_interceptor_for_friends( {'data': [ {'name': self.USERS[1]['USERNAME'], 'id': self.USERS[1]['FB_ID']}, {'name': self.USERS[2]['USERNAME'], 'id': self.USERS[2]['FB_ID']}, ]} ) url = reverse('courses-with-friends') response = self.client.get(url, {'oauth_token': self._FB_USER_ACCESS_TOKEN}) self.assertEqual(response.status_code, 200) self.assertEqual(self.course.id, CourseKey.from_string(response.data[0]['course']['id'])) # pylint: disable=E1101 self.assertEqual(self.course_2.id, CourseKey.from_string(response.data[1]['course']['id'])) # pylint: disable=E1101 # Assert that only two courses are returned self.assertEqual(len(response.data), 2) # pylint: disable=E1101
def test_sandbox_inclusion(self): """ Test to make sure that a match works across course runs """ self.assertTrue(can_execute_unsafe_code(CourseKey.from_string('edX/full/2012_Fall'))) self.assertTrue(can_execute_unsafe_code(CourseKey.from_string('edX/full/2013_Spring'))) self.assertFalse(can_execute_unsafe_code(LibraryLocator('edX', 'test_bank')))
def rerun_course(source_course_key_string, destination_course_key_string, user_id, fields=None): """ Reruns a course in a new celery task. """ # import here, at top level this import prevents the celery workers from starting up correctly from edxval.api import copy_course_videos source_course_key = CourseKey.from_string(source_course_key_string) destination_course_key = CourseKey.from_string(destination_course_key_string) try: # deserialize the payload fields = deserialize_fields(fields) if fields else None # use the split modulestore as the store for the rerun course, # as the Mongo modulestore doesn't support multiple runs of the same course. store = modulestore() with store.default_store('split'): execute_and_log_time( store.clone_course, source_course_key, destination_course_key, user_id, fields=fields ) # set initial permissions for the user to access the course. execute_and_log_time(initialize_permissions, destination_course_key, User.objects.get(id=user_id)) # update state: Succeeded CourseRerunState.objects.succeeded(course_key=destination_course_key) # call edxval to attach videos to the rerun execute_and_log_time(copy_course_videos, source_course_key, destination_course_key) # Copy OrganizationCourse organization_course = OrganizationCourse.objects.filter(course_id=source_course_key_string).first() if organization_course: clone_instance(organization_course, {'course_id': destination_course_key_string}) execute_and_log_time(add_course_restrictions, source_course_key, destination_course_key) return "succeeded" except DuplicateCourseError: # do NOT delete the original course, only update the status CourseRerunState.objects.failed(course_key=destination_course_key) LOGGER.exception(u'Course Rerun Error') return "duplicate course" # catch all exceptions so we can update the state and properly cleanup the course. except Exception as exc: # pylint: disable=broad-except # update state: Failed CourseRerunState.objects.failed(course_key=destination_course_key) LOGGER.exception(u'Course Rerun Error') try: # cleanup any remnants of the course modulestore().delete_course(destination_course_key, user_id) except ItemNotFoundError: # it's possible there was an error even before the course module was created pass return u"exception: " + text_type(exc)
def test_get_courses_for_wiki(self, _from_json): """ Test the get_courses_for_wiki method """ for course_number in self.courses: course_locations = self.draft_store.get_courses_for_wiki(course_number) assert_equals(len(course_locations), 1) assert_equals(CourseKey.from_string('/'.join(['edX', course_number, '2012_Fall'])), course_locations[0]) course_locations = self.draft_store.get_courses_for_wiki('no_such_wiki') assert_equals(len(course_locations), 0) # set toy course to share the wiki with simple course toy_course = self.draft_store.get_course(CourseKey.from_string('edX/toy/2012_Fall')) toy_course.wiki_slug = 'simple' self.draft_store.update_item(toy_course, ModuleStoreEnum.UserID.test) # now toy_course should not be retrievable with old wiki_slug course_locations = self.draft_store.get_courses_for_wiki('toy') assert_equals(len(course_locations), 0) # but there should be two courses with wiki_slug 'simple' course_locations = self.draft_store.get_courses_for_wiki('simple') assert_equals(len(course_locations), 2) for course_number in ['toy', 'simple']: assert_in(CourseKey.from_string('/'.join(['edX', course_number, '2012_Fall'])), course_locations) # configure simple course to use unique wiki_slug. simple_course = self.draft_store.get_course(CourseKey.from_string('edX/simple/2012_Fall')) simple_course.wiki_slug = 'edX.simple.2012_Fall' self.draft_store.update_item(simple_course, ModuleStoreEnum.UserID.test) # it should be retrievable with its new wiki_slug course_locations = self.draft_store.get_courses_for_wiki('edX.simple.2012_Fall') assert_equals(len(course_locations), 1) assert_in(CourseKey.from_string('edX/simple/2012_Fall'), course_locations)
def test_get_org_courses(self, _from_json): """ Make sure that we can query for a filtered list of courses for a given ORG """ courses = self.draft_store.get_courses(org='guestx') assert_equals(len(courses), 1) course_ids = [course.id for course in courses] for course_key in [ CourseKey.from_string('/'.join(fields)) for fields in [ ['guestx', 'foo', 'bar'] ] ]: assert_in(course_key, course_ids) courses = self.draft_store.get_courses(org='edX') assert_equals(len(courses), 5) course_ids = [course.id for course in courses] for course_key in [ CourseKey.from_string('/'.join(fields)) for fields in [ ['edX', 'simple', '2012_Fall'], ['edX', 'simple_with_draft', '2012_Fall'], ['edX', 'test_import_course', '2012_Fall'], ['edX', 'test_unicode', '2012_Fall'], ['edX', 'toy', '2012_Fall'], ] ]: assert_in(course_key, course_ids)
def test_course_listing_has_pre_requisite_courses(self): """ Creates four courses. Enroll test user in all courses Sets two of them as pre-requisites of another course. Checks course where pre-requisite course is set has appropriate info. """ seed_milestone_relationship_types() course_location2 = CourseKey.from_string('Org1/Course2/Run2') self._create_course_with_access_groups(course_location2) pre_requisite_course_location = CourseKey.from_string('Org1/Course3/Run3') self._create_course_with_access_groups(pre_requisite_course_location) pre_requisite_course_location2 = CourseKey.from_string('Org1/Course4/Run4') self._create_course_with_access_groups(pre_requisite_course_location2) # create a course with pre_requisite_courses pre_requisite_courses = [ unicode(pre_requisite_course_location), unicode(pre_requisite_course_location2), ] course_location = CourseKey.from_string('Org1/Course1/Run1') self._create_course_with_access_groups(course_location, { 'pre_requisite_courses': pre_requisite_courses }) set_prerequisite_courses(course_location, pre_requisite_courses) # get dashboard course_enrollment_pairs = list(get_course_enrollment_pairs(self.student, None, [])) courses_having_prerequisites = frozenset(course.id for course, _enrollment in course_enrollment_pairs if course.pre_requisite_courses) courses_requirements_not_met = get_pre_requisite_courses_not_completed( self.student, courses_having_prerequisites ) self.assertEqual(len(courses_requirements_not_met[course_location]['courses']), len(pre_requisite_courses))
def test_get_courses(self, _from_json): '''Make sure the course objects loaded properly''' courses = self.draft_store.get_courses() assert_equals(len(courses), 6) course_ids = [course.id for course in courses] for course_key in [ CourseKey.from_string('/'.join(fields)) for fields in [ ['edX', 'simple', '2012_Fall'], ['edX', 'simple_with_draft', '2012_Fall'], ['edX', 'test_import_course', '2012_Fall'], ['edX', 'test_unicode', '2012_Fall'], ['edX', 'toy', '2012_Fall'], ['guestx', 'foo', 'bar'], ] ]: assert_in(course_key, course_ids) course = self.draft_store.get_course(course_key) assert_not_none(course) assert_true(self.draft_store.has_course(course_key)) mix_cased = CourseKey.from_string( '/'.join([course_key.org.upper(), course_key.course.upper(), course_key.run.lower()]) ) assert_false(self.draft_store.has_course(mix_cased)) assert_true(self.draft_store.has_course(mix_cased, ignore_case=True))
def test_caching(self): deadlines = { CourseKey.from_string("edX/DemoX/Fall"): datetime.now(pytz.UTC), CourseKey.from_string("edX/DemoX/Spring"): datetime.now(pytz.UTC) + timedelta(days=1) } course_keys = deadlines.keys() # Initially, no deadlines are set with self.assertNumQueries(1): all_deadlines = VerificationDeadline.deadlines_for_courses(course_keys) self.assertEqual(all_deadlines, {}) # Create the deadlines for course_key, deadline in deadlines.iteritems(): VerificationDeadline.objects.create( course_key=course_key, deadline=deadline, ) # Warm the cache with self.assertNumQueries(1): VerificationDeadline.deadlines_for_courses(course_keys) # Load the deadlines from the cache with self.assertNumQueries(0): all_deadlines = VerificationDeadline.deadlines_for_courses(course_keys) self.assertEqual(all_deadlines, deadlines) # Delete the deadlines VerificationDeadline.objects.all().delete() # Verify that the deadlines are updated correctly with self.assertNumQueries(1): all_deadlines = VerificationDeadline.deadlines_for_courses(course_keys) self.assertEqual(all_deadlines, {})
def test_empty_path(self): """ Test InvalidKeyError when asset path is empty """ with self.assertRaises(InvalidKeyError): CourseKey.from_string('course-locator:org+course+run').make_asset_key('asset', '') with self.assertRaises(InvalidKeyError): CourseKey.from_string('org/course/run').make_asset_key('asset', '')
def validate_course_key(course_key): """ Validate the course_key is correct. """ try: CourseKey.from_string(course_key) except InvalidKeyError: raise ValidationError(_("Invalid course key."))
def setUp(self): super(TestGetRunMarketingUrls, self).setUp() self.user = UserFactory() self.course_runs = [factories.CourseRun() for __ in range(2)] self.course_keys = [ CourseKey.from_string(self.course_runs[0]["key"]), CourseKey.from_string(self.course_runs[1]["key"]), ]
def test_map_into_course_location(self): original_course = CourseKey.from_string('org/course/run') new_course = CourseKey.from_string('edX/toy/2012_Fall') loc = BlockUsageLocator(original_course, 'cat', 'name:more_name', deprecated=True) self.assertEquals( BlockUsageLocator(new_course, 'cat', 'name:more_name', deprecated=True), loc.map_into_course(new_course) )
def test_empty_path(self): with self.assertRaises(InvalidKeyError): CourseKey.from_string('course-locator:org+course+run').make_asset_key('asset', '') self.assertEquals( '/c4x/org/course/asset/', unicode(CourseKey.from_string('org/course/run').make_asset_key('asset', '')) )
def test_map_into_course_asset_location(self): original_course = CourseKey.from_string('org/course/run') new_course = CourseKey.from_string('edX/toy/2012_Fall') loc = AssetLocator(original_course, 'asset', 'foo.bar') self.assertEquals( AssetLocator(new_course, 'asset', 'foo.bar', deprecated=True), loc.map_into_course(new_course) )
def course_key_is_valid(course_key): if course_key is None: return False try: CourseKey.from_string(unicode(course_key)) except InvalidKeyError: return False return True
def get(self, request): """GET /api/team/v0/team_membership""" specified_username_or_team = False username = None team_id = None requested_course_id = None requested_course_key = None accessible_course_ids = None if 'course_id' in request.QUERY_PARAMS: requested_course_id = request.QUERY_PARAMS['course_id'] try: requested_course_key = CourseKey.from_string(requested_course_id) except InvalidKeyError: return Response(status=status.HTTP_404_NOT_FOUND) if 'team_id' in request.QUERY_PARAMS: specified_username_or_team = True team_id = request.QUERY_PARAMS['team_id'] try: team = CourseTeam.objects.get(team_id=team_id) except CourseTeam.DoesNotExist: return Response(status=status.HTTP_404_NOT_FOUND) if requested_course_key is not None and requested_course_key != team.course_id: return Response(status=status.HTTP_400_BAD_REQUEST) if not has_team_api_access(request.user, team.course_id): return Response(status=status.HTTP_404_NOT_FOUND) if 'username' in request.QUERY_PARAMS: specified_username_or_team = True username = request.QUERY_PARAMS['username'] if not request.user.is_staff: enrolled_courses = ( CourseEnrollment.enrollments_for_user(request.user).values_list('course_id', flat=True) ) staff_courses = ( CourseAccessRole.objects.filter(user=request.user, role='staff').values_list('course_id', flat=True) ) accessible_course_ids = [item for sublist in (enrolled_courses, staff_courses) for item in sublist] if requested_course_id is not None and requested_course_id not in accessible_course_ids: return Response(status=status.HTTP_400_BAD_REQUEST) if not specified_username_or_team: return Response( build_api_error(ugettext_noop("username or team_id must be specified.")), status=status.HTTP_400_BAD_REQUEST ) course_keys = None if requested_course_key is not None: course_keys = [requested_course_key] elif accessible_course_ids is not None: course_keys = [CourseKey.from_string(course_string) for course_string in accessible_course_ids] queryset = CourseTeamMembership.get_memberships(username, course_keys, team_id) page = self.paginate_queryset(queryset) serializer = self.get_pagination_serializer(page) return Response(serializer.data) # pylint: disable=maybe-no-member
def test_course_key(self): """ Test CourseKey """ key = CourseKey.from_string('org.id/course_id/run') self.assertEqual(key.org, 'org.id') key = CourseKey.from_string('course-v1:org.id+course_id+run') self.assertEqual(key.org, 'org.id')
def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) self.perform_create(serializer) entitlement = serializer.instance user = entitlement.user # find all course_runs within the course course_runs = get_course_runs_for_course(entitlement.course_uuid) # check if the user has enrollments for any of the course_runs user_run_enrollments = [ CourseEnrollment.get_enrollment(user, CourseKey.from_string(course_run.get('key'))) for course_run in course_runs if CourseEnrollment.get_enrollment(user, CourseKey.from_string(course_run.get('key'))) ] # filter to just enrollments that can be upgraded. upgradeable_enrollments = [ enrollment for enrollment in user_run_enrollments if enrollment.is_active and enrollment.upgrade_deadline and enrollment.upgrade_deadline > timezone.now() ] # if there is only one upgradeable enrollment, convert it from audit to the entitlement.mode # if there is any ambiguity about which enrollment to upgrade # (i.e. multiple upgradeable enrollments or no available upgradeable enrollment), dont enroll if len(upgradeable_enrollments) == 1: enrollment = upgradeable_enrollments[0] log.info( 'Upgrading enrollment [%s] from %s to %s while adding entitlement for user [%s] for course [%s]', enrollment, enrollment.mode, serializer.data.get('mode'), user.username, serializer.data.get('course_uuid') ) enrollment.update_enrollment(mode=entitlement.mode) entitlement.set_enrollment(enrollment) else: log.info( 'No enrollment upgraded while adding entitlement for user [%s] for course [%s] ', user.username, serializer.data.get('course_uuid') ) headers = self.get_success_headers(serializer.data) # Note, the entitlement is re-serialized before getting added to the Response, # so that the 'modified' date reflects changes that occur when upgrading enrollment. return Response( CourseEntitlementSerializer(entitlement).data, status=status.HTTP_201_CREATED, headers=headers )
def get(self, request, *args, **kwargs): self.course_id = self.kwargs.get('course_id', request.QUERY_PARAMS.get('course_id', None)) if not self.course_id: raise CourseNotSpecifiedError() try: CourseKey.from_string(self.course_id) except InvalidKeyError: raise CourseKeyMalformedError(course_id=self.course_id) return super(CourseViewMixin, self).get(request, *args, **kwargs)
def _validate_course_ids(self, course_ids): """ Validate a list of course key strings. """ try: for course_id in course_ids: CourseKey.from_string(course_id) return course_ids except InvalidKeyError as error: raise CommandError('Invalid key specified: {}'.format(text_type(error)))
def test_versions(self): lib_key = CourseKey.from_string('library-v1:TestX+lib1+version@519665f6223ebd6980884f2b') lib_key2 = CourseKey.from_string('library-v1:TestX+lib1') lib_key3 = lib_key.version_agnostic() self.assertEqual(lib_key2, lib_key3) self.assertEqual(lib_key3.version_guid, None) new_version = '123445678912345678912345' lib_key4 = lib_key.for_version(new_version) self.assertEqual(lib_key4.version_guid, ObjectId(new_version))
def inner(request, *args, **kwargs): course_key = kwargs.get('course_key_string') or kwargs.get('course_id') if course_key is not None: try: CourseKey.from_string(course_key) except InvalidKeyError: raise Http404 response = view_func(request, *args, **kwargs) return response
def test_list(self): """ Verify the endpoint supports listing all CreditCourse objects. """ cc1 = CreditCourse.objects.create(course_key=CourseKey.from_string('a/b/c')) cc2 = CreditCourse.objects.create(course_key=CourseKey.from_string('d/e/f'), enabled=True) expected = [self._serialize_credit_course(cc1), self._serialize_credit_course(cc2)] response = self.client.get(self.path) self.assertEqual(response.status_code, 200) # Verify the API returns a list of serialized CreditCourse objects self.assertListEqual(json.loads(response.content), expected)
def delete_course_task(user_id, course_key_string): profile = UserProfile.objects.get(pk=user_id) user = User.objects.get(pk=profile.user_id) course_key = CourseKey.from_string(course_key_string) delete_course_and_groups(course_key, user.id) searcher = SearchEngine.get_search_engine(CoursewareSearchIndexer.INDEX_NAME) if searcher != None: CoursewareSearchIndexer.remove_deleted_items(searcher, CourseKey.from_string(course_key_string), []) searcher.remove(CourseAboutSearchIndexer.DISCOVERY_DOCUMENT_TYPE, [course_key_string])
def setUp(self): super(BatchCompletionMethodTests, self).setUp() self.override_waffle_switch(True) self.user = UserFactory.create() self.other_user = UserFactory.create() self.course_key = CourseKey.from_string("edX/MOOC101/2049_T2") self.other_course_key = CourseKey.from_string("course-v1:ReedX+Hum110+1904") self.block_keys = [UsageKey.from_string("i4x://edX/MOOC101/video/{}".format(number)) for number in xrange(5)] self.submit_fake_completions()
def validate_courses(self, value): """ Check that each course key in list is valid. """ course_keys = value for course in course_keys: try: CourseKey.from_string(course) except InvalidKeyError: raise serializers.ValidationError(u"Course key not valid: {}".format(course)) return value
def course_key_is_valid(course_key): """ Course key object validation """ if course_key is None: return False try: CourseKey.from_string(six.text_type(course_key)) except InvalidKeyError: return False return True
def clean_course_key(self): """Clean the course key to make sure format is valid.""" course_key = self.cleaned_data['course_key'] try: CourseKey.from_string(course_key) except InvalidKeyError: raise forms.ValidationError('Invalid CourseKey {course_key}!'.format( course_key=course_key )) return course_key
def course_key_is_valid(course_key): """ Course key object validation """ if course_key is None: return False try: CourseKey.from_string(unicode(course_key)) except (InvalidKeyError, UnicodeDecodeError): return False return True
def handle(self, *args, **options): if not options['course']: raise CommandError('You must specify a course id') COURSE_ID = options['course'] # Print update after this many students if options['info_interval']: STATUS_INTERVAL = options['info_interval'] else: STATUS_INTERVAL = 100 print "\nEnroll all active students to '%s'" % COURSE_ID try: # CourseKeys are needed to query databases (SQL, Mongo) course_key = CourseKey.from_string(COURSE_ID) if not modulestore().get_course(course_key, depth=2): print "\n Course %s not found." % COURSE_ID return enrolled = 0 start = datetime.datetime.now(UTC) # Fetch all users that are not superuser, exclude non active students if necessary if options['exclude']: users = User.objects.exclude( Q(is_active=False) | Q(profile__isnull=True)) else: users = User.objects.exclude(profile__isnull=True) total_students = users.count() for user in users: if enrolled % STATUS_INTERVAL == 0: # Print a status update with an approximation of # how much time is left based on how long the last # interval took diff = datetime.datetime.now(UTC) - start timeleft = diff * (total_students - enrolled) / STATUS_INTERVAL hours, remainder = divmod(timeleft.seconds, 3600) minutes, _seconds = divmod(remainder, 60) print( "{0} students out of {1} were successfully enrolled to " "course {2} ~{3:02}:{4:02}m remaining").format( enrolled, total_students, COURSE_ID, hours, minutes) start = datetime.datetime.now(UTC) if not CourseEnrollment.objects.filter( user=user, course_id=course_key).exists(): CourseEnrollment.objects.create(user=user, course_id=course_key, is_active=True, mode='honor') enrolled += 1 print( "\nSubscription progress is over : {0} students out of {1}" " were successfully enrolled to course {2}").format( enrolled, total_students, COURSE_ID) except InvalidKeyError: print("Course id {} could not be parsed as a CourseKey;".format( COURSE_ID))
def setUp(self, **kwargs): super(CreditApiTestBase, self).setUp() self.course_key = CourseKey.from_string("edX/DemoX/Demo_Course")
def import_olx(self, user_id, course_key_string, archive_path, archive_name, language): """ Import a course or library from a provided OLX .tar.gz archive. """ courselike_key = CourseKey.from_string(course_key_string) try: user = User.objects.get(pk=user_id) except User.DoesNotExist: with respect_language(language): self.status.fail(_(u'Unknown User ID: {0}').format(user_id)) return if not has_course_author_access(user, courselike_key): with respect_language(language): self.status.fail(_(u'Permission denied')) return is_library = isinstance(courselike_key, LibraryLocator) is_course = not is_library if is_library: root_name = LIBRARY_ROOT courselike_module = modulestore().get_library(courselike_key) import_func = import_library_from_xml else: root_name = COURSE_ROOT courselike_module = modulestore().get_course(courselike_key) import_func = import_course_from_xml # Locate the uploaded OLX archive (and download it from S3 if necessary) # Do everything in a try-except block to make sure everything is properly cleaned up. data_root = path(settings.GITHUB_REPO_ROOT) subdir = base64.urlsafe_b64encode(repr(courselike_key)) course_dir = data_root / subdir try: self.status.set_state(u'Unpacking') if not archive_name.endswith(u'.tar.gz'): with respect_language(language): self.status.fail(_(u'We only support uploading a .tar.gz file.')) return temp_filepath = course_dir / get_valid_filename(archive_name) if not course_dir.isdir(): # pylint: disable=no-value-for-parameter os.mkdir(course_dir) LOGGER.debug(u'importing course to {0}'.format(temp_filepath)) # Copy the OLX archive from where it was uploaded to (S3, Swift, file system, etc.) if not course_import_export_storage.exists(archive_path): LOGGER.info(u'Course import %s: Uploaded file %s not found', courselike_key, archive_path) with respect_language(language): self.status.fail(_(u'Tar file not found')) return with course_import_export_storage.open(archive_path, 'rb') as source: with open(temp_filepath, 'wb') as destination: def read_chunk(): """ Read and return a sequence of bytes from the source file. """ return source.read(FILE_READ_CHUNK) for chunk in iter(read_chunk, b''): destination.write(chunk) LOGGER.info(u'Course import %s: Download from storage complete', courselike_key) # Delete from source location course_import_export_storage.delete(archive_path) # If the course has an entrance exam then remove it and its corresponding milestone. # current course state before import. if is_course: if courselike_module.entrance_exam_enabled: fake_request = RequestFactory().get(u'/') fake_request.user = user from contentstore.views.entrance_exam import remove_entrance_exam_milestone_reference # TODO: Is this really ok? Seems dangerous for a live course remove_entrance_exam_milestone_reference(fake_request, courselike_key) LOGGER.info( u'entrance exam milestone content reference for course %s has been removed', courselike_module.id ) # Send errors to client with stage at which error occurred. except Exception as exception: # pylint: disable=broad-except if course_dir.isdir(): # pylint: disable=no-value-for-parameter shutil.rmtree(course_dir) LOGGER.info(u'Course import %s: Temp data cleared', courselike_key) LOGGER.exception(u'Error importing course %s', courselike_key) self.status.fail(text_type(exception)) return # try-finally block for proper clean up after receiving file. try: tar_file = tarfile.open(temp_filepath) try: safetar_extractall(tar_file, (course_dir + u'/').encode(u'utf-8')) except SuspiciousOperation as exc: LOGGER.info(u'Course import %s: Unsafe tar file - %s', courselike_key, exc.args[0]) with respect_language(language): self.status.fail(_(u'Unsafe tar file. Aborting import.')) return finally: tar_file.close() LOGGER.info(u'Course import %s: Uploaded file extracted', courselike_key) self.status.set_state(u'Verifying') self.status.increment_completed_steps() # find the 'course.xml' file def get_all_files(directory): """ For each file in the directory, yield a 2-tuple of (file-name, directory-path) """ for directory_path, _dirnames, filenames in os.walk(directory): for filename in filenames: yield (filename, directory_path) def get_dir_for_filename(directory, filename): """ Returns the directory path for the first file found in the directory with the given name. If there is no file in the directory with the specified name, return None. """ for name, directory_path in get_all_files(directory): if name == filename: return directory_path return None dirpath = get_dir_for_filename(course_dir, root_name) if not dirpath: with respect_language(language): self.status.fail(_(u'Could not find the {0} file in the package.').format(root_name)) return dirpath = os.path.relpath(dirpath, data_root) LOGGER.debug(u'found %s at %s', root_name, dirpath) LOGGER.info(u'Course import %s: Extracted file verified', courselike_key) self.status.set_state(u'Updating') self.status.increment_completed_steps() with dog_stats_api.timer( u'courselike_import.time', tags=[u"courselike:{}".format(courselike_key)] ): courselike_items = import_func( modulestore(), user.id, settings.GITHUB_REPO_ROOT, [dirpath], load_error_modules=False, static_content_store=contentstore(), target_id=courselike_key ) new_location = courselike_items[0].location LOGGER.debug(u'new course at %s', new_location) LOGGER.info(u'Course import %s: Course import successful', courselike_key) self.status.increment_completed_steps() except Exception as exception: # pylint: disable=broad-except LOGGER.exception(u'error importing course') self.status.fail(text_type(exception)) finally: if course_dir.isdir(): # pylint: disable=no-value-for-parameter shutil.rmtree(course_dir) LOGGER.info(u'Course import %s: Temp data cleared', courselike_key) if self.status.completed_steps == 3 and is_course: # Reload the course so we have the latest state course = modulestore().get_course(courselike_key) if course.entrance_exam_enabled: entrance_exam_chapter = modulestore().get_items( course.id, qualifiers={u'category': u'chapter'}, settings={u'is_entrance_exam': True} )[0] metadata = {u'entrance_exam_id': text_type(entrance_exam_chapter.location)} CourseMetadata.update_from_dict(metadata, course, user) from contentstore.views.entrance_exam import add_entrance_exam_milestone add_entrance_exam_milestone(course.id, entrance_exam_chapter) LOGGER.info(u'Course %s Entrance exam imported', course.id)
def prepare_org(self, obj): course_run = filter_visible_runs(obj.course_runs).first() if course_run: return CourseKey.from_string(course_run.key).org return None
def render_html_view(request, course_id, certificate=None): # pylint: disable=too-many-statements """ This public view generates an HTML representation of the specified user and course If a certificate is not available, we display a "Sorry!" screen instead It can be overridden by setting `OVERRIDE_RENDER_CERTIFICATE_VIEW` to an alternative implementation. """ user = certificate.user if certificate else request.user user_id = user.id preview_mode = request.GET.get('preview', None) platform_name = configuration_helpers.get_value("platform_name", settings.PLATFORM_NAME) configuration = CertificateHtmlViewConfiguration.get_config() # Kick the user back to the "Invalid" screen if the feature is disabled globally if not settings.FEATURES.get('CERTIFICATES_HTML_VIEW', False): return _render_invalid_certificate(request, course_id, platform_name, configuration) # Load the course and user objects try: course_key = CourseKey.from_string(course_id) course = get_course_by_id(course_key) # For any course or user exceptions, kick the user back to the "Invalid" screen except (InvalidKeyError, Http404) as exception: error_str = ( "Invalid cert: error finding course %s " "Specific error: %s" ) log.info(error_str, course_id, str(exception)) return _render_invalid_certificate(request, course_id, platform_name, configuration) course_overview = get_course_overview_or_none(course_key) # Kick the user back to the "Invalid" screen if the feature is disabled for the course if not course.cert_html_view_enabled: log.info( "Invalid cert: HTML certificates disabled for %s. User id: %d", course_id, user_id, ) return _render_invalid_certificate(request, course_id, platform_name, configuration) # Load user's certificate user_certificate = _get_user_certificate(request, user, course_key, course_overview, preview_mode) if not user_certificate: log.info( "Invalid cert: User %d does not have eligible cert for %s.", user_id, course_id, ) return _render_invalid_certificate(request, course_id, platform_name, configuration) # Get the active certificate configuration for this course # If we do not have an active certificate, we'll need to send the user to the "Invalid" screen # Passing in the 'preview' parameter, if specified, will return a configuration, if defined active_configuration = get_active_web_certificate(course, preview_mode) if active_configuration is None: log.info( "Invalid cert: course %s does not have an active configuration. User id: %d", course_id, user_id, ) return _render_invalid_certificate(request, course_id, platform_name, configuration) # Get data from Discovery service that will be necessary for rendering this Certificate. catalog_data = _get_catalog_data_for_course(course_key) # Determine whether to use the standard or custom template to render the certificate. custom_template = None custom_template_language = None if settings.FEATURES.get('CUSTOM_CERTIFICATE_TEMPLATES_ENABLED', False): log.info("Custom certificate for course %s", course_id) custom_template, custom_template_language = _get_custom_template_and_language( course.id, user_certificate.mode, catalog_data.pop('content_language', None) ) # Determine the language that should be used to render the certificate. # For the standard certificate template, use the user language. For custom templates, use # the language associated with the template. user_language = translation.get_language() certificate_language = custom_template_language if custom_template else user_language log.info( "certificate language is: %s for the course: %s", certificate_language, course_key ) # Generate the certificate context in the correct language, then render the template. with translation.override(certificate_language): context = {'user_language': user_language} _update_context_with_basic_info(context, course_id, platform_name, configuration) context['certificate_data'] = active_configuration # Append/Override the existing view context values with any mode-specific ConfigurationModel values context.update(configuration.get(user_certificate.mode, {})) # Append organization info _update_organization_context(context, course) # Append course info _update_course_context(request, context, course, platform_name) # Append course run info from discovery context.update(catalog_data) # Append user info _update_context_with_user_info(context, user, user_certificate) # Append social sharing info _update_social_context(request, context, course, user_certificate, platform_name) # Append/Override the existing view context values with certificate specific values _update_certificate_context(context, course, course_overview, user_certificate, platform_name) # Append badge info _update_badge_context(context, course, user) # Add certificate header/footer data to current context context.update(get_certificate_header_context(is_secure=request.is_secure())) context.update(get_certificate_footer_context()) # Append/Override the existing view context values with any course-specific static values from Advanced Settings context.update(course.cert_html_view_overrides) # Track certificate view events _track_certificate_events(request, course, user, user_certificate) try: # .. filter_implemented_name: CertificateRenderStarted # .. filter_type: org.openedx.learning.certificate.render.started.v1 context, custom_template = CertificateRenderStarted.run_filter( context=context, custom_template=custom_template, ) except CertificateRenderStarted.RenderAlternativeInvalidCertificate as exc: response = _render_invalid_certificate( request, course_id, platform_name, configuration, cert_path=exc.template_name or INVALID_CERTIFICATE_TEMPLATE_PATH, ) except CertificateRenderStarted.RedirectToPage as exc: response = HttpResponseRedirect(exc.redirect_to) except CertificateRenderStarted.RenderCustomResponse as exc: response = exc.response else: response = _render_valid_certificate(request, context, custom_template) # Render the certificate return response
def change_enrollment(request, check_access=True): """ Modify the enrollment status for the logged-in user. TODO: This is lms specific and does not belong in common code. The request parameter must be a POST request (other methods return 405) that specifies course_id and enrollment_action parameters. If course_id or enrollment_action is not specified, if course_id is not valid, if enrollment_action is something other than "enroll" or "unenroll", if enrollment_action is "enroll" and enrollment is closed for the course, or if enrollment_action is "unenroll" and the user is not enrolled in the course, a 400 error will be returned. If the user is not logged in, 403 will be returned; it is important that only this case return 403 so the front end can redirect the user to a registration or login page when this happens. This function should only be called from an AJAX request, so the error messages in the responses should never actually be user-visible. Args: request (`Request`): The Django request object Keyword Args: check_access (boolean): If True, we check that an accessible course actually exists for the given course_key before we enroll the student. The default is set to False to avoid breaking legacy code or code with non-standard flows (ex. beta tester invitations), but for any standard enrollment flow you probably want this to be True. Returns: Response """ # Get the user user = request.user # Ensure the user is authenticated if not user.is_authenticated: return HttpResponseForbidden() # Ensure we received a course_id action = request.POST.get("enrollment_action") if 'course_id' not in request.POST: return HttpResponseBadRequest(_("Course id not specified")) try: course_id = CourseKey.from_string(request.POST.get("course_id")) except InvalidKeyError: log.warning( "User %s tried to %s with invalid course id: %s", user.username, action, request.POST.get("course_id"), ) return HttpResponseBadRequest(_("Invalid course id")) # Allow us to monitor performance of this transaction on a per-course basis since we often roll-out features # on a per-course basis. monitoring_utils.set_custom_attribute('course_id', str(course_id)) if action == "enroll": # Make sure the course exists # We don't do this check on unenroll, or a bad course id can't be unenrolled from if not modulestore().has_course(course_id): log.warning("User %s tried to enroll in non-existent course %s", user.username, course_id) return HttpResponseBadRequest(_("Course id is invalid")) # Record the user's email opt-in preference if settings.FEATURES.get('ENABLE_MKTG_EMAIL_OPT_IN'): _update_email_opt_in(request, course_id.org) available_modes = CourseMode.modes_for_course_dict(course_id) # Check whether the user is blocked from enrolling in this course # This can occur if the user's IP is on a global blacklist # or if the user is enrolling in a country in which the course # is not available. redirect_url = embargo_api.redirect_if_blocked( course_id, user=user, ip_address=get_client_ip(request)[0], url=request.path) if redirect_url: return HttpResponse(redirect_url) if CourseEntitlement.check_for_existing_entitlement_and_enroll( user=user, course_run_key=course_id): return HttpResponse(reverse('courseware', args=[str(course_id)])) # Check that auto enrollment is allowed for this course # (= the course is NOT behind a paywall) if CourseMode.can_auto_enroll(course_id): # Enroll the user using the default mode (audit) # We're assuming that users of the course enrollment table # will NOT try to look up the course enrollment model # by its slug. If they do, it's possible (based on the state of the database) # for no such model to exist, even though we've set the enrollment type # to "audit". try: enroll_mode = CourseMode.auto_enroll_mode( course_id, available_modes) if enroll_mode: CourseEnrollment.enroll(user, course_id, check_access=check_access, mode=enroll_mode) except Exception: # pylint: disable=broad-except return HttpResponseBadRequest(_("Could not enroll")) # If we have more than one course mode or professional ed is enabled, # then send the user to the choose your track page. # (In the case of no-id-professional/professional ed, this will redirect to a page that # funnels users directly into the verification / payment flow) if CourseMode.has_verified_mode( available_modes) or CourseMode.has_professional_mode( available_modes): return HttpResponse( reverse("course_modes_choose", kwargs={'course_id': str(course_id)})) # Otherwise, there is only one mode available (the default) return HttpResponse() elif action == "unenroll": enrollment = CourseEnrollment.get_enrollment(user, course_id) if not enrollment: return HttpResponseBadRequest( _("You are not enrolled in this course")) certificate_info = cert_info(user, enrollment.course_overview) if certificate_info.get('status') in DISABLE_UNENROLL_CERT_STATES: return HttpResponseBadRequest( _("Your certificate prevents you from unenrolling from this course" )) CourseEnrollment.unenroll(user, course_id) REFUND_ORDER.send(sender=None, course_enrollment=enrollment) return HttpResponse() else: return HttpResponseBadRequest(_("Enrollment action is invalid"))
def instructor_dashboard_2(request, course_id): """ Display the instructor dashboard for a course. """ try: course_key = CourseKey.from_string(course_id) except InvalidKeyError: log.error( u"Unable to find course with course key %s while loading the Instructor Dashboard.", course_id) return HttpResponseServerError() course = get_course_by_id(course_key, depth=0) access = { 'admin': request.user.is_staff, 'instructor': bool(has_access(request.user, 'instructor', course)), 'finance_admin': CourseFinanceAdminRole(course_key).has_user(request.user), 'sales_admin': CourseSalesAdminRole(course_key).has_user(request.user), 'staff': bool(has_access(request.user, 'staff', course)), 'forum_admin': has_forum_access(request.user, course_key, FORUM_ROLE_ADMINISTRATOR), } if not access['staff']: raise Http404() is_white_label = CourseMode.is_white_label(course_key) sections = [ _section_course_info(course, access), _section_membership(course, access, is_white_label), _section_cohort_management(course, access), _section_student_admin(course, access), _section_data_download(course, access), ] analytics_dashboard_message = None if settings.ANALYTICS_DASHBOARD_URL: # Construct a URL to the external analytics dashboard analytics_dashboard_url = '{0}/courses/{1}'.format( settings.ANALYTICS_DASHBOARD_URL, unicode(course_key)) link_start = "<a href=\"{}\" target=\"_blank\">".format( analytics_dashboard_url) analytics_dashboard_message = _( "To gain insights into student enrollment and participation {link_start}" "visit {analytics_dashboard_name}, our new course analytics product{link_end}." ) analytics_dashboard_message = analytics_dashboard_message.format( link_start=link_start, link_end="</a>", analytics_dashboard_name=settings.ANALYTICS_DASHBOARD_NAME) # Temporarily show the "Analytics" section until we have a better way of linking to Insights sections.append(_section_analytics(course, access)) # Check if there is corresponding entry in the CourseMode Table related to the Instructor Dashboard course course_mode_has_price = False paid_modes = CourseMode.paid_modes_for_course(course_key) if len(paid_modes) == 1: course_mode_has_price = True elif len(paid_modes) > 1: log.error( u"Course %s has %s course modes with payment options. Course must only have " u"one paid course mode to enable eCommerce options.", unicode(course_key), len(paid_modes)) if settings.FEATURES.get('INDIVIDUAL_DUE_DATES') and access['instructor']: sections.insert(3, _section_extensions(course)) # Gate access to course email by feature flag & by course-specific authorization if bulk_email_is_enabled_for_course(course_key): sections.append(_section_send_email(course, access)) # Gate access to Metrics tab by featue flag and staff authorization if settings.FEATURES['CLASS_DASHBOARD'] and access['staff']: sections.append(_section_metrics(course, access)) # Gate access to Ecommerce tab if course_mode_has_price and (access['finance_admin'] or access['sales_admin']): sections.append( _section_e_commerce(course, access, paid_modes[0], is_white_label, is_white_label)) # Gate access to Special Exam tab depending if either timed exams or proctored exams # are enabled in the course # NOTE: For now, if we only have procotred exams enabled, then only platform Staff # (user.is_staff) will be able to view the special exams tab. This may # change in the future can_see_special_exams = ( ((course.enable_proctored_exams and request.user.is_staff) or course.enable_timed_exams) and settings.FEATURES.get('ENABLE_SPECIAL_EXAMS', False)) if can_see_special_exams: sections.append(_section_special_exams(course, access)) # Certificates panel # This is used to generate example certificates # and enable self-generated certificates for a course. certs_enabled = CertificateGenerationConfiguration.current().enabled if certs_enabled and access['admin']: sections.append(_section_certificates(course)) disable_buttons = not _is_small_course(course_key) certificate_white_list = CertificateWhitelist.get_certificate_white_list( course_key) generate_certificate_exceptions_url = reverse( # pylint: disable=invalid-name 'generate_certificate_exceptions', kwargs={ 'course_id': unicode(course_key), 'generate_for': '' }) generate_bulk_certificate_exceptions_url = reverse( # pylint: disable=invalid-name 'generate_bulk_certificate_exceptions', kwargs={'course_id': unicode(course_key)}) certificate_exception_view_url = reverse( 'certificate_exception_view', kwargs={'course_id': unicode(course_key)}) context = { 'course': course, 'studio_url': get_studio_url(course, 'course'), 'sections': sections, 'disable_buttons': disable_buttons, 'analytics_dashboard_message': analytics_dashboard_message, 'certificate_white_list': certificate_white_list, 'generate_certificate_exceptions_url': generate_certificate_exceptions_url, 'generate_bulk_certificate_exceptions_url': generate_bulk_certificate_exceptions_url, 'certificate_exception_view_url': certificate_exception_view_url } if settings.FEATURES['ENABLE_INSTRUCTOR_LEGACY_DASHBOARD']: context['old_dashboard_url'] = reverse( 'instructor_dashboard_legacy', kwargs={'course_id': unicode(course_key)}) return render_to_response( 'instructor/instructor_dashboard_2/instructor_dashboard_2.html', context)
def test_generate_no_such_course(self): response = self._generate( course_key=CourseKey.from_string("edx/invalid/course"), username=self.STUDENT_USERNAME) self.assertEqual(response.status_code, 400)
class CertificateSupportTestCase(ModuleStoreTestCase): """ Base class for tests of the certificate support views. """ SUPPORT_USERNAME = "******" SUPPORT_EMAIL = "*****@*****.**" SUPPORT_PASSWORD = "******" STUDENT_USERNAME = "******" STUDENT_EMAIL = "*****@*****.**" STUDENT_PASSWORD = "******" CERT_COURSE_KEY = CourseKey.from_string("edX/DemoX/Demo_Course") COURSE_NOT_EXIST_KEY = CourseKey.from_string( "test/TestX/Test_Course_Not_Exist") EXISTED_COURSE_KEY_1 = CourseKey.from_string( "test1/Test1X/Test_Course_Exist_1") EXISTED_COURSE_KEY_2 = CourseKey.from_string( "test2/Test2X/Test_Course_Exist_2") CERT_GRADE = 0.89 CERT_STATUS = CertificateStatuses.downloadable CERT_MODE = "verified" CERT_DOWNLOAD_URL = "http://www.example.com/cert.pdf" def setUp(self): """ Create a support team member and a student with a certificate. Log in as the support team member. """ super(CertificateSupportTestCase, self).setUp() CourseFactory( org=CertificateSupportTestCase.EXISTED_COURSE_KEY_1.org, course=CertificateSupportTestCase.EXISTED_COURSE_KEY_1.course, run=CertificateSupportTestCase.EXISTED_COURSE_KEY_1.run, ) # Create the support staff user self.support = UserFactory( username=self.SUPPORT_USERNAME, email=self.SUPPORT_EMAIL, password=self.SUPPORT_PASSWORD, ) SupportStaffRole().add_users(self.support) # Create a student self.student = UserFactory( username=self.STUDENT_USERNAME, email=self.STUDENT_EMAIL, password=self.STUDENT_PASSWORD, ) # Create certificates for the student self.cert = GeneratedCertificate.eligible_certificates.create( user=self.student, course_id=self.CERT_COURSE_KEY, grade=self.CERT_GRADE, status=self.CERT_STATUS, mode=self.CERT_MODE, download_url=self.CERT_DOWNLOAD_URL, ) # Login as support staff success = self.client.login(username=self.SUPPORT_USERNAME, password=self.SUPPORT_PASSWORD) self.assertTrue(success, msg="Couldn't log in as support staff")
def search_certificates(request): """ Search for certificates for a particular user OR along with the given course. Supports search by either username or email address along with course id. First filter the records for the given username/email and then filter against the given course id (if given). Show the 'Regenerate' button if a record found in 'generatedcertificate' model otherwise it will show the Generate button. Arguments: request (HttpRequest): The request object. Returns: JsonResponse Example Usage: GET /certificates/[email protected] GET /certificates/[email protected]&course_id=xyz Response: 200 OK Content-Type: application/json [ { "username": "******", "course_key": "edX/DemoX/Demo_Course", "type": "verified", "status": "downloadable", "download_url": "http://www.example.com/cert.pdf", "grade": "0.98", "created": 2015-07-31T00:00:00Z, "modified": 2015-07-31T00:00:00Z } ] """ user_filter = urllib.unquote(urllib.quote_plus(request.GET.get("user", ""))) if not user_filter: msg = _("user is not given.") return HttpResponseBadRequest(msg) try: user = User.objects.get(Q(email=user_filter) | Q(username=user_filter)) except User.DoesNotExist: return HttpResponseBadRequest( _("user '{user}' does not exist").format(user=user_filter)) certificates = api.get_certificates_for_user(user.username) for cert in certificates: cert["course_key"] = unicode(cert["course_key"]) cert["created"] = cert["created"].isoformat() cert["modified"] = cert["modified"].isoformat() cert["regenerate"] = True course_id = urllib.quote_plus(request.GET.get("course_id", ""), safe=':/') if course_id: try: course_key = CourseKey.from_string(course_id) except InvalidKeyError: return HttpResponseBadRequest( _("Course id '{course_id}' is not valid").format( course_id=course_id)) else: try: if CourseOverview.get_from_id(course_key): certificates = [ certificate for certificate in certificates if certificate['course_key'] == course_id ] if not certificates: return JsonResponse([{ 'username': user.username, 'course_key': course_id, 'regenerate': False }]) except CourseOverview.DoesNotExist: msg = _( "The course does not exist against the given key '{course_key}'" ).format(course_key=course_key) return HttpResponseBadRequest(msg) return JsonResponse(certificates)
def get(self, request, *args, **kwargs): """Shows logs of imports that happened as a result of a git import""" course_id = kwargs.get('course_id') if course_id: course_id = CourseKey.from_string(course_id) page_size = 10 # Set mongodb defaults even if it isn't defined in settings mongo_db = { 'host': 'localhost', 'user': '', 'password': '', 'db': 'xlog', } # Allow overrides if hasattr(settings, 'MONGODB_LOG'): for config_item in [ 'host', 'user', 'password', 'db', ]: mongo_db[config_item] = settings.MONGODB_LOG.get( config_item, mongo_db[config_item]) mongouri = 'mongodb://{user}:{password}@{host}/{db}'.format(**mongo_db) error_msg = '' try: if mongo_db['user'] and mongo_db['password']: mdb = mongoengine.connect(mongo_db['db'], host=mongouri) else: mdb = mongoengine.connect(mongo_db['db'], host=mongo_db['host']) except mongoengine.connection.ConnectionError: log.exception('Unable to connect to mongodb to save log, ' 'please check MONGODB_LOG settings.') if course_id is None: # Require staff if not going to specific course if not request.user.is_staff: raise Http404 cilset = CourseImportLog.objects.order_by('-created') else: try: course = get_course_by_id(course_id) except Exception: log.info('Cannot find course %s', course_id) raise Http404 # Allow only course team, instructors, and staff if not (request.user.is_staff or CourseInstructorRole(course.id).has_user(request.user) or CourseStaffRole(course.id).has_user(request.user)): raise Http404 log.debug('course_id=%s', course_id) cilset = CourseImportLog.objects.filter( course_id=course_id).order_by('-created') log.debug('cilset length=%s', len(cilset)) # Paginate the query set paginator = Paginator(cilset, page_size) try: logs = paginator.page(request.GET.get('page')) except PageNotAnInteger: logs = paginator.page(1) except EmptyPage: # If the page is too high or low given_page = int(request.GET.get('page')) page = min(max(1, given_page), paginator.num_pages) logs = paginator.page(page) mdb.disconnect() context = { 'logs': logs, 'course_id': course_id.to_deprecated_string() if course_id else None, 'error_msg': error_msg, 'page_size': page_size } return render_to_response(self.template_name, context)
def handle(self, *args, **options): if len(args) != 1: raise CommandError( "check_course requires one argument: <course_id>") try: course_key = CourseKey.from_string(args[0]) except InvalidKeyError: course_key = SlashSeparatedCourseKey.from_deprecated_string( args[0]) store = modulestore() course = store.get_course(course_key, depth=3) err_cnt = 0 def _xlint_metadata(module): err_cnt = check_module_metadata_editability(module) for child in module.get_children(): err_cnt = err_cnt + _xlint_metadata(child) return err_cnt err_cnt = err_cnt + _xlint_metadata(course) # we've had a bug where the xml_attributes field can we rewritten as a string rather than a dict def _check_xml_attributes_field(module): err_cnt = 0 if hasattr(module, 'xml_attributes') and isinstance( module.xml_attributes, basestring): print 'module = {0} has xml_attributes as a string. It should be a dict'.format( module.location) err_cnt = err_cnt + 1 for child in module.get_children(): err_cnt = err_cnt + _check_xml_attributes_field(child) return err_cnt err_cnt = err_cnt + _check_xml_attributes_field(course) # check for dangling discussion items, this can cause errors in the forums def _get_discussion_items(module): discussion_items = [] if module.location.category == 'discussion': discussion_items = discussion_items + [module.location] for child in module.get_children(): discussion_items = discussion_items + _get_discussion_items( child) return discussion_items discussion_items = _get_discussion_items(course) # now query all discussion items via get_items() and compare with the tree-traversal queried_discussion_items = store.get_items( course_key=course_key, category='discussion', ) for item in queried_discussion_items: if item.location not in discussion_items: print 'Found dangling discussion module = {0}'.format( item.location)
def test_get_items_with_course_items(self): store = modulestore() # fix was to allow get_items() to take the course_id parameter store.get_items(CourseKey.from_string('abc/def/ghi'), qualifiers={'category': 'vertical'})
from lms.djangoapps.courseware.tests.tests import LoginEnrollmentTestCase from lms.djangoapps.instructor_task.api_helper import encode_problem_and_student_input from lms.djangoapps.instructor_task.models import PROGRESS, QUEUING, ReportStore from lms.djangoapps.instructor_task.tests.factories import InstructorTaskFactory from lms.djangoapps.instructor_task.views import instructor_task_status from openedx.core.djangolib.testing.utils import CacheIsolationTestCase from openedx.core.lib.url_utils import quote_slashes from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore.django import modulestore from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory TEST_COURSE_ORG = 'edx' TEST_COURSE_NAME = 'test_course' TEST_COURSE_NUMBER = '1.23x' TEST_COURSE_KEY = CourseKey.from_string('/'.join( [TEST_COURSE_ORG, TEST_COURSE_NUMBER, TEST_COURSE_NAME])) TEST_CHAPTER_NAME = "Section" TEST_SECTION_NAME = "Subsection" TEST_FAILURE_MESSAGE = 'task failed horribly' TEST_FAILURE_EXCEPTION = 'RandomCauseError' OPTION_1 = 'Option 1' OPTION_2 = 'Option 2' class InstructorTaskTestCase(CacheIsolationTestCase): """ Tests API and view methods that involve the reporting of status for background tasks. """ def setUp(self):
def supplement_program_data(program_data, user): """Supplement program course codes with CourseOverview and CourseEnrollment data. Arguments: program_data (dict): Representation of a program. user (User): The user whose enrollments to inspect. """ for organization in program_data['organizations']: # TODO: Cache the results of the get_organization_by_short_name call so # the database is hit less frequently. org_obj = get_organization_by_short_name(organization['key']) if org_obj and org_obj.get('logo'): organization['img'] = org_obj['logo'].url for course_code in program_data['course_codes']: for run_mode in course_code['run_modes']: course_key = CourseKey.from_string(run_mode['course_key']) course_overview = CourseOverview.get_from_id(course_key) course_url = reverse('course_root', args=[course_key]) course_image_url = course_overview.course_image_url start_date_string = course_overview.start_datetime_text() end_date_string = course_overview.end_datetime_text() end_date = course_overview.end or datetime.datetime.max.replace( tzinfo=pytz.UTC) is_course_ended = end_date < timezone.now() is_enrolled = CourseEnrollment.is_enrolled(user, course_key) enrollment_start = course_overview.enrollment_start or datetime.datetime.min.replace( tzinfo=pytz.UTC) enrollment_end = course_overview.enrollment_end or datetime.datetime.max.replace( tzinfo=pytz.UTC) is_enrollment_open = enrollment_start <= timezone.now( ) < enrollment_end enrollment_open_date = None if is_enrollment_open else strftime_localized( enrollment_start, 'SHORT_DATE') certificate_data = certificate_api.certificate_downloadable_status( user, course_key) certificate_uuid = certificate_data.get('uuid') certificate_url = certificate_api.get_certificate_url( course_id=course_key, uuid=certificate_uuid, ) if certificate_uuid else None required_mode_slug = run_mode['mode_slug'] enrolled_mode_slug, _ = CourseEnrollment.enrollment_mode_for_user( user, course_key) is_mode_mismatch = required_mode_slug != enrolled_mode_slug is_upgrade_required = is_enrolled and is_mode_mismatch # Requires that the ecommerce service be in use. required_mode = CourseMode.mode_for_course(course_key, required_mode_slug) ecommerce = EcommerceService() sku = getattr(required_mode, 'sku', None) if ecommerce.is_enabled(user) and sku: upgrade_url = ecommerce.checkout_page_url( required_mode.sku) if is_upgrade_required else None else: upgrade_url = None run_mode.update({ 'certificate_url': certificate_url, 'course_image_url': course_image_url, 'course_url': course_url, 'end_date': end_date_string, 'enrollment_open_date': enrollment_open_date, 'is_course_ended': is_course_ended, 'is_enrolled': is_enrolled, 'is_enrollment_open': is_enrollment_open, # TODO: Not currently available on LMS. 'marketing_url': None, 'start_date': start_date_string, 'upgrade_url': upgrade_url, }) return program_data
def cohort_discussion_topics(request, course_key_string): """ The handler for cohort discussion categories requests. This will raise 404 if user is not staff. Returns the JSON representation of discussion topics w.r.t categories for the course. Example: >>> example = { >>> "course_wide_discussions": { >>> "entries": { >>> "General": { >>> "sort_key": "General", >>> "is_cohorted": True, >>> "id": "i4x-edx-eiorguegnru-course-foobarbaz" >>> } >>> } >>> "children": ["General"] >>> }, >>> "inline_discussions" : { >>> "subcategories": { >>> "Getting Started": { >>> "subcategories": {}, >>> "children": [ >>> "Working with Videos", >>> "Videos on edX" >>> ], >>> "entries": { >>> "Working with Videos": { >>> "sort_key": None, >>> "is_cohorted": False, >>> "id": "d9f970a42067413cbb633f81cfb12604" >>> }, >>> "Videos on edX": { >>> "sort_key": None, >>> "is_cohorted": False, >>> "id": "98d8feb5971041a085512ae22b398613" >>> } >>> } >>> }, >>> "children": ["Getting Started"] >>> }, >>> } >>> } """ course_key = CourseKey.from_string(course_key_string) course = get_course_with_access(request.user, 'staff', course_key) discussion_topics = {} discussion_category_map = get_discussion_category_map( course, request.user, cohorted_if_in_list=True, exclude_unstarted=False) # We extract the data for the course wide discussions from the category map. course_wide_entries = discussion_category_map.pop('entries') course_wide_children = [] inline_children = [] for name in discussion_category_map['children']: if name in course_wide_entries: course_wide_children.append(name) else: inline_children.append(name) discussion_topics['course_wide_discussions'] = { 'entries': course_wide_entries, 'children': course_wide_children } discussion_category_map['children'] = inline_children discussion_topics['inline_discussions'] = discussion_category_map return JsonResponse(discussion_topics)
def async_course_overview_update(*args, **kwargs): course_keys = [CourseKey.from_string(arg) for arg in args] CourseOverview.update_select_courses(course_keys, force_update=kwargs['force_update'])
def get(self, request, course_id, error=None): """Displays the course mode choice page. Args: request (`Request`): The Django Request object. course_id (unicode): The slash-separated course key. Keyword Args: error (unicode): If provided, display this error message on the page. Returns: Response """ course_key = CourseKey.from_string(course_id) # Check whether the user has access to this course # based on country access rules. embargo_redirect = embargo_api.redirect_if_blocked( course_key, user=request.user, ip_address=get_ip(request), url=request.path) if embargo_redirect: return redirect(embargo_redirect) enrollment_mode, is_active = CourseEnrollment.enrollment_mode_for_user( request.user, course_key) modes = CourseMode.modes_for_course_dict(course_key) ecommerce_service = EcommerceService() # We assume that, if 'professional' is one of the modes, it should be the *only* mode. # If there are both modes, default to non-id-professional. has_enrolled_professional = ( CourseMode.is_professional_slug(enrollment_mode) and is_active) if CourseMode.has_professional_mode( modes) and not has_enrolled_professional: redirect_url = reverse('verify_student_start_flow', kwargs={'course_id': unicode(course_key)}) if ecommerce_service.is_enabled(request.user): professional_mode = modes.get( CourseMode.NO_ID_PROFESSIONAL_MODE) or modes.get( CourseMode.PROFESSIONAL) if professional_mode.sku: redirect_url = ecommerce_service.checkout_page_url( professional_mode.sku) return redirect(redirect_url) # If there isn't a verified mode available, then there's nothing # to do on this page. The user has almost certainly been auto-registered # in the "honor" track by this point, so we send the user # to the dashboard. if not CourseMode.has_verified_mode(modes): return redirect(reverse('dashboard')) # If a user has already paid, redirect them to the dashboard. if is_active and (enrollment_mode in CourseMode.VERIFIED_MODES + [CourseMode.NO_ID_PROFESSIONAL_MODE]): return redirect(reverse('dashboard')) donation_for_course = request.session.get("donation_for_course", {}) chosen_price = donation_for_course.get(unicode(course_key), None) course = modulestore().get_course(course_key) if CourseEnrollment.is_enrollment_closed(request.user, course): locale = to_locale(get_language()) enrollment_end_date = format_datetime(course.enrollment_end, 'short', locale=locale) params = urllib.urlencode({'course_closed': enrollment_end_date}) return redirect('{0}?{1}'.format(reverse('dashboard'), params)) # When a credit mode is available, students will be given the option # to upgrade from a verified mode to a credit mode at the end of the course. # This allows students who have completed photo verification to be eligible # for univerity credit. # Since credit isn't one of the selectable options on the track selection page, # we need to check *all* available course modes in order to determine whether # a credit mode is available. If so, then we show slightly different messaging # for the verified track. has_credit_upsell = any( CourseMode.is_credit_mode(mode) for mode in CourseMode.modes_for_course(course_key, only_selectable=False)) context = { "course_modes_choose_url": reverse("course_modes_choose", kwargs={'course_id': course_key.to_deprecated_string()}), "modes": modes, "has_credit_upsell": has_credit_upsell, "course_name": course.display_name_with_default_escaped, "course_org": course.display_org_with_default, "course_num": course.display_number_with_default, "chosen_price": chosen_price, "error": error, "responsive": True, "nav_hidden": True, } if "verified" in modes: verified_mode = modes["verified"] context["suggested_prices"] = [ decimal.Decimal(x.strip()) for x in verified_mode.suggested_prices.split(",") if x.strip() ] context["currency"] = verified_mode.currency.upper() context["min_price"] = verified_mode.min_price context["verified_name"] = verified_mode.name context["verified_description"] = verified_mode.description if verified_mode.sku: context[ "use_ecommerce_payment_flow"] = ecommerce_service.is_enabled( request.user) context[ "ecommerce_payment_page"] = ecommerce_service.payment_page_url( ) context["sku"] = verified_mode.sku return render_to_response("course_modes/choose.html", context)
def create_thread(request, course_id, commentable_id): """ Given a course and commentble ID, create the thread """ log.debug("Creating new thread in %r, id %r", course_id, commentable_id) course_key = CourseKey.from_string(course_id) course = get_course_with_access(request.user, 'load', course_key) post = request.POST user = request.user if course.allow_anonymous: anonymous = post.get('anonymous', 'false').lower() == 'true' else: anonymous = False if course.allow_anonymous_to_peers: anonymous_to_peers = post.get('anonymous_to_peers', 'false').lower() == 'true' else: anonymous_to_peers = False if 'title' not in post or not post['title'].strip(): return JsonError(_("Title can't be empty")) if 'body' not in post or not post['body'].strip(): return JsonError(_("Body can't be empty")) params = { 'anonymous': anonymous, 'anonymous_to_peers': anonymous_to_peers, 'commentable_id': commentable_id, 'course_id': course_key.to_deprecated_string(), 'user_id': user.id, 'thread_type': post["thread_type"], 'body': post["body"], 'title': post["title"], } # Check for whether this commentable belongs to a team, and add the right context if get_team(commentable_id) is not None: params['context'] = ThreadContext.STANDALONE else: params['context'] = ThreadContext.COURSE thread = cc.Thread(**params) # Divide the thread if required try: group_id = get_group_id_for_comments_service(request, course_key, commentable_id) except ValueError: return HttpResponseServerError("Invalid group id for commentable") if group_id is not None: thread.group_id = group_id thread.save() thread_created.send(sender=None, user=user, post=thread) # patch for backward compatibility to comments service if 'pinned' not in thread.attributes: thread['pinned'] = False follow = post.get('auto_subscribe', 'false').lower() == 'true' if follow: cc_user = cc.User.from_django_user(user) cc_user.follow(thread) data = thread.to_dict() add_courseware_context([data], course, user) track_thread_created_event(request, course, thread, follow) if request.is_ajax(): return ajax_content_response(request, course_key, data) else: return JsonResponse(prepare_content(data, course_key))
def get_course_key(request, course_id=None): if not course_id: return CourseKey.from_string(request.GET.get('course_id')) return CourseKey.from_string(course_id)
def setup(self): self.course_key_string = COURSE_ID_STR_TEMPLATE.format(1) self.course_key = CourseKey.from_string( self.course_key_string)
def course_discussions_settings_handler(request, course_key_string): """ The restful handler for divided discussion setting requests. Requires JSON. This will raise 404 if user is not staff. GET Returns the JSON representation of divided discussion settings for the course. PATCH Updates the divided discussion settings for the course. Returns the JSON representation of updated settings. """ course_key = CourseKey.from_string(course_key_string) course = get_course_with_access(request.user, 'staff', course_key) discussion_settings = get_course_discussion_settings(course_key) if request.method == 'PATCH': divided_course_wide_discussions, divided_inline_discussions = get_divided_discussions( course, discussion_settings ) settings_to_change = {} if 'divided_course_wide_discussions' in request.json or 'divided_inline_discussions' in request.json: divided_course_wide_discussions = request.json.get( 'divided_course_wide_discussions', divided_course_wide_discussions ) divided_inline_discussions = request.json.get( 'divided_inline_discussions', divided_inline_discussions ) settings_to_change['divided_discussions'] = divided_course_wide_discussions + divided_inline_discussions if 'always_divide_inline_discussions' in request.json: settings_to_change['always_divide_inline_discussions'] = request.json.get( 'always_divide_inline_discussions' ) if 'division_scheme' in request.json: settings_to_change['division_scheme'] = request.json.get( 'division_scheme' ) if not settings_to_change: return JsonResponse({"error": unicode("Bad Request")}, 400) try: if settings_to_change: discussion_settings = set_course_discussion_settings(course_key, **settings_to_change) except ValueError as err: # Note: error message not translated because it is not exposed to the user (UI prevents this state). return JsonResponse({"error": unicode(err)}, 400) divided_course_wide_discussions, divided_inline_discussions = get_divided_discussions( course, discussion_settings ) return JsonResponse({ 'id': discussion_settings.id, 'divided_inline_discussions': divided_inline_discussions, 'divided_course_wide_discussions': divided_course_wide_discussions, 'always_divide_inline_discussions': discussion_settings.always_divide_inline_discussions, 'division_scheme': discussion_settings.division_scheme, 'available_division_schemes': available_division_schemes(course_key) })
def handle(self, *args, **options): # Find all courses that have ended if options['course']: course_id = CourseKey.from_string(options['course']) else: raise CommandError("You must specify a course") cert_data = {} # find students who are active # number of enrolled students = downloadable + notpassing print(u"Looking up certificate states for {0}".format( options['course'])) enrolled_current = User.objects.filter( courseenrollment__course_id=course_id, courseenrollment__is_active=True) enrolled_total = User.objects.filter( courseenrollment__course_id=course_id) verified_enrolled = GeneratedCertificate.objects.filter( course_id__exact=course_id, mode__exact='verified') honor_enrolled = GeneratedCertificate.objects.filter( course_id__exact=course_id, mode__exact='honor') audit_enrolled = GeneratedCertificate.objects.filter( course_id__exact=course_id, mode__exact='audit') cert_data[course_id] = { 'enrolled_current': enrolled_current.count(), 'enrolled_total': enrolled_total.count(), 'verified_enrolled': verified_enrolled.count(), 'honor_enrolled': honor_enrolled.count(), 'audit_enrolled': audit_enrolled.count() } status_tally = GeneratedCertificate.objects.filter( course_id__exact=course_id).values('status').annotate( dcount=Count('status')) cert_data[course_id].update( {status['status']: status['dcount'] for status in status_tally}) mode_tally = GeneratedCertificate.objects.filter( course_id__exact=course_id, status__exact='downloadable').values('mode').annotate( dcount=Count('mode')) cert_data[course_id].update( {mode['mode']: mode['dcount'] for mode in mode_tally}) # all states we have seen far all courses status_headings = sorted( set([ status for course in cert_data for status in cert_data[course] ]) # lint-amnesty, pylint: disable=consider-using-set-comprehension ) # print the heading for the report print("{:>26}".format("course ID"), end=' ') print(' '.join( ["{:>16}".format(heading) for heading in status_headings])) # print the report print("{0:>26}".format(text_type(course_id)), end=' ') for heading in status_headings: if heading in cert_data[course_id]: print("{:>16}".format(cert_data[course_id][heading]), end=' ') else: print(" " * 16, end=' ') print()
def wrapped_view(request, course_id, *args, **kwargs): # pylint: disable=missing-docstring course_key = CourseKey.from_string(course_id) with modulestore().bulk_operations(course_key): return view_func(request, course_key, *args, **kwargs)
from lti_provider.signature_validator import SignatureValidator from opaque_keys.edx.keys import CourseKey, UsageKey from student.tests.factories import UserFactory LTI_DEFAULT_PARAMS = { 'roles': u'Instructor,urn:lti:instrole:ims/lis/Administrator', 'context_id': u'lti_launch_context_id', 'oauth_version': u'1.0', 'oauth_consumer_key': u'consumer_key', 'oauth_signature': u'OAuth Signature', 'oauth_signature_method': u'HMAC-SHA1', 'oauth_timestamp': u'OAuth Timestamp', 'oauth_nonce': u'OAuth Nonce', } COURSE_KEY = CourseKey.from_string('some/course/id') USAGE_KEY = UsageKey.from_string( 'i4x://some/course/problem/uuid').map_into_course(COURSE_KEY) COURSE_PARAMS = {'course_key': COURSE_KEY, 'usage_key': USAGE_KEY} ALL_PARAMS = dict(LTI_DEFAULT_PARAMS.items() + COURSE_PARAMS.items()) def build_launch_request(authenticated=True): """ Helper method to create a new request object for the LTI launch. """ request = RequestFactory().post('/') request.user = UserFactory.create() request.user.is_authenticated = MagicMock(return_value=authenticated) request.session = {}
def listen_for_grade_calculation(sender, user, course_grade, course_key, deadline, **kwargs): # pylint: disable=unused-argument """Receive 'MIN_GRADE_REQUIREMENT_STATUS' signal and update minimum grade requirement status. Args: sender: None user(User): User Model object course_grade(CourseGrade): CourseGrade object course_key(CourseKey): The key for the course deadline(datetime): Course end date or None Kwargs: kwargs : None """ # This needs to be imported here to avoid a circular dependency # that can cause migrations to fail. from openedx.core.djangoapps.credit import api course_id = CourseKey.from_string(unicode(course_key)) is_credit = api.is_credit_course(course_id) if is_credit: requirements = api.get_credit_requirements(course_id, namespace='grade') if requirements: criteria = requirements[0].get('criteria') if criteria: min_grade = criteria.get('min_grade') passing_grade = course_grade.percent >= min_grade now = timezone.now() status = None reason = None if (deadline and now < deadline) or not deadline: # Student completed coursework on-time if passing_grade: # Student received a passing grade status = 'satisfied' reason = {'final_grade': course_grade.percent} else: # Submission after deadline if passing_grade: # Grade was good, but submission arrived too late status = 'failed' reason = {'current_date': now, 'deadline': deadline} else: # Student failed to receive minimum grade status = 'failed' reason = { 'final_grade': course_grade.percent, 'minimum_grade': min_grade } # We do not record a status if the user has not yet earned the minimum grade, but still has # time to do so. if status and reason: api.set_credit_requirement_status(user, course_id, 'grade', 'grade', status=status, reason=reason)
def setUp(self): super(TestCredentialsSignalsSendGrade, self).setUp() self.user = UserFactory() self.key = CourseKey.from_string(CourseRunFactory()['key'])
def post(self, request): """Enrolls the currently logged-in user in a course. Server-to-server calls may deactivate or modify the mode of existing enrollments. All other requests go through `add_enrollment()`, which allows creation of new and reactivation of old enrollments. """ # Get the User, Course ID, and Mode from the request. username = request.data.get('user', request.user.username) course_id = request.data.get('course_details', {}).get('course_id') if not course_id: return Response( status=status.HTTP_400_BAD_REQUEST, data={ "message": u"Course ID must be specified to create a new enrollment." }) try: course_id = CourseKey.from_string(course_id) except InvalidKeyError: return Response( status=status.HTTP_400_BAD_REQUEST, data={ "message": u"No course '{course_id}' found for enrollment".format( course_id=course_id) }) mode = request.data.get('mode') has_api_key_permissions = self.has_api_key_permissions(request) # Check that the user specified is either the same user, or this is a server-to-server request. if not username: username = request.user.username if username != request.user.username and not has_api_key_permissions: # Return a 404 instead of a 403 (Unauthorized). If one user is looking up # other users, do not let them deduce the existence of an enrollment. return Response(status=status.HTTP_404_NOT_FOUND) if mode not in (CourseMode.AUDIT, CourseMode.HONOR, None) and not has_api_key_permissions: return Response( status=status.HTTP_403_FORBIDDEN, data={ "message": u"User does not have permission to create enrollment with mode [{mode}]." .format(mode=mode) }) try: # Lookup the user, instead of using request.user, since request.user may not match the username POSTed. user = User.objects.get(username=username) except ObjectDoesNotExist: return Response(status=status.HTTP_406_NOT_ACCEPTABLE, data={ 'message': u'The user {} does not exist.'.format(username) }) embargo_response = embargo_api.get_embargo_response( request, course_id, user) if embargo_response: return embargo_response try: is_active = request.data.get('is_active') # Check if the requested activation status is None or a Boolean if is_active is not None and not isinstance(is_active, bool): return Response( status=status.HTTP_400_BAD_REQUEST, data={ 'message': (u"'{value}' is an invalid enrollment activation status." ).format(value=is_active) }) enterprise_course_consent = request.data.get( 'enterprise_course_consent') explicit_linked_enterprise = request.data.get( 'linked_enterprise_customer') if (enterprise_course_consent or explicit_linked_enterprise ) and has_api_key_permissions and enterprise_enabled(): enterprise_api_client = EnterpriseApiClient() consent_client = ConsentApiClient() # We received an explicitly-linked EnterpriseCustomer for the enrollment if explicit_linked_enterprise is not None: try: enterprise_api_client.post_enterprise_course_enrollment( username, unicode(course_id), None) except EnterpriseApiException as error: log.exception( "An unexpected error occurred while creating the new EnterpriseCourseEnrollment " "for user [%s] in course run [%s]", username, course_id) raise CourseEnrollmentError(error.message) kwargs = { 'username': username, 'course_id': unicode(course_id), 'enterprise_customer_uuid': explicit_linked_enterprise, } consent_client.provide_consent(**kwargs) # We received an implicit "consent granted" parameter from ecommerce # TODO: Once ecommerce has been deployed with explicit enterprise support, remove this # entire chunk of logic, related tests, and any supporting methods no longer required. elif enterprise_course_consent is not None: # Check if the enterprise_course_enrollment is a boolean if not isinstance(enterprise_course_consent, bool): return Response( status=status.HTTP_400_BAD_REQUEST, data={ 'message': (u"'{value}' is an invalid enterprise course consent value." ).format(value=enterprise_course_consent) }) try: enterprise_api_client.post_enterprise_course_enrollment( username, unicode(course_id), enterprise_course_consent) except EnterpriseApiException as error: log.exception( "An unexpected error occurred while creating the new EnterpriseCourseEnrollment " "for user [%s] in course run [%s]", username, course_id) raise CourseEnrollmentError(error.message) enrollment_attributes = request.data.get('enrollment_attributes') enrollment = api.get_enrollment(username, unicode(course_id)) mode_changed = enrollment and mode is not None and enrollment[ 'mode'] != mode active_changed = enrollment and is_active is not None and enrollment[ 'is_active'] != is_active missing_attrs = [] if enrollment_attributes: actual_attrs = [ u"{namespace}:{name}".format(**attr) for attr in enrollment_attributes ] missing_attrs = set(REQUIRED_ATTRIBUTES.get( mode, [])) - set(actual_attrs) if has_api_key_permissions and (mode_changed or active_changed): if mode_changed and active_changed and not is_active: # if the requester wanted to deactivate but specified the wrong mode, fail # the request (on the assumption that the requester had outdated information # about the currently active enrollment). msg = u"Enrollment mode mismatch: active mode={}, requested mode={}. Won't deactivate.".format( enrollment["mode"], mode) log.warning(msg) return Response(status=status.HTTP_400_BAD_REQUEST, data={"message": msg}) if len(missing_attrs) > 0: msg = u"Missing enrollment attributes: requested mode={} required attributes={}".format( mode, REQUIRED_ATTRIBUTES.get(mode)) log.warning(msg) return Response(status=status.HTTP_400_BAD_REQUEST, data={"message": msg}) response = api.update_enrollment( username, unicode(course_id), mode=mode, is_active=is_active, enrollment_attributes=enrollment_attributes, # If we are updating enrollment by authorized api caller, we should allow expired modes include_expired=has_api_key_permissions) else: # Will reactivate inactive enrollments. response = api.add_enrollment( username, unicode(course_id), mode=mode, is_active=is_active, enrollment_attributes=enrollment_attributes) email_opt_in = request.data.get('email_opt_in', None) if email_opt_in is not None: org = course_id.org update_email_opt_in(request.user, org, email_opt_in) log.info( 'The user [%s] has already been enrolled in course run [%s].', username, course_id) return Response(response) except CourseModeNotFoundError as error: return Response( status=status.HTTP_400_BAD_REQUEST, data={ "message": (u"The [{mode}] course mode is expired or otherwise unavailable for course run [{course_id}]." ).format(mode=mode, course_id=course_id), "course_details": error.data }) except CourseNotFoundError: return Response( status=status.HTTP_400_BAD_REQUEST, data={ "message": u"No course '{course_id}' found for enrollment".format( course_id=course_id) }) except CourseEnrollmentExistsError as error: log.warning( 'An enrollment already exists for user [%s] in course run [%s].', username, course_id) return Response(data=error.enrollment) except CourseEnrollmentError: log.exception( "An error occurred while creating the new course enrollment for user " "[%s] in course run [%s]", username, course_id) return Response( status=status.HTTP_400_BAD_REQUEST, data={ "message": (u"An error occurred while creating the new course enrollment for user " u"'{username}' in course '{course_id}'").format( username=username, course_id=course_id) }) finally: # Assumes that the ecommerce service uses an API key to authenticate. if has_api_key_permissions: current_enrollment = api.get_enrollment( username, unicode(course_id)) audit_log('enrollment_change_requested', course_id=unicode(course_id), requested_mode=mode, actual_mode=current_enrollment['mode'] if current_enrollment else None, requested_activation=is_active, actual_activation=current_enrollment['is_active'] if current_enrollment else None, user_id=user.id)
def _handouts_loc(self): """ Return the locator string for the course handouts """ course_key = CourseKey.from_string(self._course_key) return six.text_type(course_key.make_usage_key('course_info', 'handouts'))
def render_to_fragment(self, request, course_id=None, **kwargs): """ Renders the course's home page as a fragment. """ course_key = CourseKey.from_string(course_id) course = get_course_with_access(request.user, 'load', course_key) # Render the course dates as a fragment dates_fragment = CourseDatesFragmentView().render_to_fragment( request, course_id=course_id, **kwargs) # Render the full content to enrolled users, as well as to course and global staff. # Unenrolled users who are not course or global staff are given only a subset. user_access = { 'is_anonymous': request.user.is_anonymous(), 'is_enrolled': CourseEnrollment.is_enrolled(request.user, course_key), 'is_staff': has_access(request.user, 'staff', course_key), } if user_access['is_enrolled'] or user_access['is_staff']: outline_fragment = CourseOutlineFragmentView().render_to_fragment( request, course_id=course_id, **kwargs) welcome_message_fragment = WelcomeMessageFragmentView( ).render_to_fragment(request, course_id=course_id, **kwargs) course_sock_fragment = CourseSockFragmentView().render_to_fragment( request, course=course, **kwargs) has_visited_course, resume_course_url = self._get_resume_course_info( request, course_id) else: # Redirect the user to the dashboard if they are not enrolled and # this is a course that does not support direct enrollment. if not can_self_enroll_in_course(course_key): raise CourseAccessRedirect(reverse('dashboard')) # Set all the fragments outline_fragment = None welcome_message_fragment = None course_sock_fragment = None has_visited_course = None resume_course_url = None # Get the handouts handouts_html = self._get_course_handouts(request, course) # Get the course tools enabled for this user and course course_tools = CourseToolsPluginManager.get_enabled_course_tools( request, course_key) # Grab the course home messages fragment to render any relevant django messages course_home_message_fragment = CourseHomeMessageFragmentView( ).render_to_fragment(request, course_id=course_id, user_access=user_access, **kwargs) # Render the course home fragment context = { 'request': request, 'csrf': csrf(request)['csrf_token'], 'course': course, 'course_key': course_key, 'outline_fragment': outline_fragment, 'handouts_html': handouts_html, 'course_home_message_fragment': course_home_message_fragment, 'has_visited_course': has_visited_course, 'resume_course_url': resume_course_url, 'course_tools': course_tools, 'dates_fragment': dates_fragment, 'welcome_message_fragment': welcome_message_fragment, 'course_sock_fragment': course_sock_fragment, 'disable_courseware_js': True, 'uses_pattern_library': True, } html = render_to_string('course_experience/course-home-fragment.html', context) return Fragment(html)