def setUp(self): xmodule.modulestore.django._MODULESTORES = {} self.full = modulestore().get_course("edX/full/6.002_Spring_2012") self.toy = modulestore().get_course("edX/toy/2012_Fall") # Create two accounts self.student = '*****@*****.**' self.instructor = '*****@*****.**' self.password = '******' self.create_account('u1', self.student, self.password) self.create_account('u2', self.instructor, self.password) self.activate_user(self.student) self.activate_user(self.instructor) def make_instructor(course): group_name = _course_staff_group_name(course.location) g = Group.objects.create(name=group_name) g.user_set.add(get_user(self.instructor)) make_instructor(self.toy) self.logout() self.login(self.instructor, self.password) self.enroll(self.toy)
def test_factories(self): test_course = persistent_factories.PersistentCourseFactory.create( course_id="testx.tempcourse", org="testx", prettyid="tempcourse", display_name="fun test course", user_id="testbot", ) self.assertIsInstance(test_course, CourseDescriptor) self.assertEqual(test_course.display_name, "fun test course") index_info = modulestore("split").get_course_index_info(test_course.location) self.assertEqual(index_info["org"], "testx") self.assertEqual(index_info["prettyid"], "tempcourse") test_chapter = persistent_factories.ItemFactory.create( display_name="chapter 1", parent_location=test_course.location ) self.assertIsInstance(test_chapter, SequenceDescriptor) # refetch parent which should now point to child test_course = modulestore("split").get_course(test_chapter.location) self.assertIn(test_chapter.location.block_id, test_course.children) with self.assertRaises(DuplicateCourseError): persistent_factories.PersistentCourseFactory.create( course_id="testx.tempcourse", org="testx", prettyid="tempcourse", display_name="fun test course", user_id="testbot", )
def test_repeated_course_module_instantiation(self, loops, default_store, course_depth): with modulestore().default_store(default_store): course = CourseFactory.create() chapter = ItemFactory(parent=course, category='chapter', graded=True) section = ItemFactory(parent=chapter, category='sequential') __ = ItemFactory(parent=section, category='problem') fake_request = self.factory.get( reverse('progress', kwargs={'course_id': unicode(course.id)}) ) course = modulestore().get_course(course.id, depth=course_depth) for _ in xrange(loops): field_data_cache = FieldDataCache.cache_for_descriptor_descendents( course.id, self.user, course, depth=course_depth ) course_module = get_module_for_descriptor( self.user, fake_request, course, field_data_cache, course.id, course=course ) for chapter in course_module.get_children(): for section in chapter.get_children(): for item in section.get_children(): self.assertTrue(item.graded)
def _delete_item_at_location(item_location, delete_children=False, delete_all_versions=False, user=None): """ Deletes the item at with the given Location. It is assumed that course permissions have already been checked. """ store = get_modulestore(item_location) item = store.get_item(item_location) if delete_children: _xmodule_recurse(item, lambda i: store.delete_item(i.location, delete_all_versions=delete_all_versions)) else: store.delete_item(item.location, delete_all_versions=delete_all_versions) # cdodge: we need to remove our parent's pointer to us so that it is no longer dangling if delete_all_versions: parent_locs = modulestore('direct').get_parent_locations(item_location, None) item_url = item_location.url() for parent_loc in parent_locs: parent = modulestore('direct').get_item(parent_loc) parent.children.remove(item_url) modulestore('direct').update_item(parent, user.id if user else None) return JsonResponse()
def _upload_asset(request, course_key): ''' This method allows for POST uploading of files into the course asset library, which will be supported by GridFS in MongoDB. ''' # Does the course actually exist?!? Get anything from it to prove its # existence try: modulestore().get_course(course_key) except ItemNotFoundError: # no return it as a Bad Request response logging.error("Could not find course: %s", course_key) return HttpResponseBadRequest() # compute a 'filename' which is similar to the location formatting, we're # using the 'filename' nomenclature since we're using a FileSystem paradigm # here. We're just imposing the Location string formatting expectations to # keep things a bit more consistent upload_file = request.FILES['file'] filename = upload_file.name mime_type = upload_file.content_type content_loc = StaticContent.compute_location(course_key, filename) chunked = upload_file.multiple_chunks() sc_partial = partial(StaticContent, content_loc, filename, mime_type) if chunked: content = sc_partial(upload_file.chunks()) tempfile_path = upload_file.temporary_file_path() else: content = sc_partial(upload_file.read()) tempfile_path = None # first let's see if a thumbnail can be created (thumbnail_content, thumbnail_location) = contentstore().generate_thumbnail( content, tempfile_path=tempfile_path ) # delete cached thumbnail even if one couldn't be created this time (else # the old thumbnail will continue to show) del_cached_content(thumbnail_location) # now store thumbnail location only if we could create it if thumbnail_content is not None: content.thumbnail_location = thumbnail_location # then commit the content contentstore().save(content) del_cached_content(content.location) # readback the saved content - we need the database timestamp readback = contentstore().find(content.location) locked = getattr(content, 'locked', False) response_payload = { 'asset': _get_asset_json(content.name, readback.last_modified_at, content.location, content.thumbnail_location, locked), 'msg': _('Upload completed') } return JsonResponse(response_payload)
def test_success_download_nonyoutube(self): subs_id = str(uuid4()) self.item.data = textwrap.dedent(""" <video youtube="" sub="{}"> <source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.mp4"/> <source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.webm"/> <source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.ogv"/> </video> """.format(subs_id)) modulestore().update_item(self.item, self.user.id) subs = { 'start': [100, 200, 240], 'end': [200, 240, 380], 'text': [ 'subs #1', 'subs #2', 'subs #3' ] } self.save_subs_to_store(subs, subs_id) link = reverse('download_transcripts') resp = self.client.get(link, {'locator': self.video_usage_key, 'subs_id': subs_id}) self.assertEqual(resp.status_code, 200) self.assertEqual( resp.content, '0\n00:00:00,100 --> 00:00:00,200\nsubs #1\n\n1\n00:00:00,200 --> ' '00:00:00,240\nsubs #2\n\n2\n00:00:00,240 --> 00:00:00,380\nsubs #3\n\n' ) transcripts_utils.remove_subs_from_store(subs_id, self.item)
def test_fail_for_non_video_module(self): # Video module: setup data = { 'parent_locator': unicode(self.course.location), 'category': 'videoalpha', 'type': 'videoalpha' } resp = self.client.ajax_post('/xblock/', data) usage_key = self._get_usage_key(resp) subs_id = str(uuid4()) item = modulestore().get_item(usage_key) item.data = textwrap.dedent(""" <videoalpha youtube="" sub="{}"> <source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.mp4"/> <source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.webm"/> <source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.ogv"/> </videoalpha> """.format(subs_id)) modulestore().update_item(item, self.user.id) subs = { 'start': [100, 200, 240], 'end': [200, 240, 380], 'text': [ 'subs #1', 'subs #2', 'subs #3' ] } self.save_subs_to_store(subs, subs_id) link = reverse('download_transcripts') resp = self.client.get(link, {'locator': unicode(usage_key)}) self.assertEqual(resp.status_code, 404)
def test_success_video_module_source_subs_uploading(self): self.item.data = textwrap.dedent(""" <video youtube=""> <source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.mp4"/> <source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.webm"/> <source src="http://www.quirksmode.org/html5/videos/big_buck_bunny.ogv"/> </video> """) modulestore().update_item(self.item, self.user.id) link = reverse('upload_transcripts') filename = os.path.splitext(os.path.basename(self.good_srt_file.name))[0] resp = self.client.post(link, { 'locator': self.video_usage_key, 'transcript-file': self.good_srt_file, 'video_list': json.dumps([{ 'type': 'html5', 'video': filename, 'mode': 'mp4', }]) }) self.assertEqual(resp.status_code, 200) self.assertEqual(json.loads(resp.content).get('status'), 'Success') item = modulestore().get_item(self.video_usage_key) self.assertEqual(item.sub, filename) content_location = StaticContent.compute_location( self.course.id, 'subs_{0}.srt.sjson'.format(filename)) self.assertTrue(contentstore().find(content_location))
def test_fail_for_non_video_module(self): # non_video module: setup data = { 'parent_locator': unicode(self.course.location), 'category': 'non_video', 'type': 'non_video' } resp = self.client.ajax_post('/xblock/', data) usage_key = self._get_usage_key(resp) item = modulestore().get_item(usage_key) item.data = '<non_video youtube="0.75:JMD_ifUUfsU,1.0:hI10vDNYz4M" />' modulestore().update_item(item, self.user.id) # non_video module: testing link = reverse('upload_transcripts') filename = os.path.splitext(os.path.basename(self.good_srt_file.name))[0] resp = self.client.post(link, { 'locator': unicode(usage_key), 'transcript-file': self.good_srt_file, 'video_list': json.dumps([{ 'type': 'html5', 'video': filename, 'mode': 'mp4', }]) }) self.assertEqual(resp.status_code, 400) self.assertEqual(json.loads(resp.content).get('status'), 'Transcripts are supported only for "video" modules.')
def find_peer_grading_module(course): """ Given a course, finds the first peer grading module in it. @param course: A course object. @return: boolean found_module, string problem_url """ # Reverse the base course url. base_course_url = reverse("courses") found_module = False problem_url = "" # Get the peer grading modules currently in the course. Explicitly specify the course id to avoid issues with different runs. items = modulestore().get_items(course.id, category="peergrading") # See if any of the modules are centralized modules (ie display info from multiple problems) items = [i for i in items if not getattr(i, "use_for_single_location", True)] # Loop through all potential peer grading modules, and find the first one that has a path to it. for item in items: # Generate a url for the first module and redirect the user to it. try: problem_url_parts = search.path_to_location(modulestore(), item.location) except NoPathToItem: # In the case of nopathtoitem, the peer grading module that was found is in an invalid state, and # can no longer be accessed. Log an informational message, but this will not impact normal behavior. log.info( u"Invalid peer grading module location {0} in course {1}. This module may need to be removed.".format( item_location, course.id ) ) continue problem_url = generate_problem_url(problem_url_parts, base_course_url) found_module = True return found_module, problem_url
def clean_course_id(self): """Validate the course id""" cleaned_id = self.cleaned_data["course_id"] try: course_key = CourseKey.from_string(cleaned_id) except InvalidKeyError: try: course_key = SlashSeparatedCourseKey.from_deprecated_string(cleaned_id) except InvalidKeyError: msg = u'Course id invalid.' msg += u' --- Entered course id was: "{0}". '.format(cleaned_id) msg += 'Please recheck that you have supplied a valid course id.' raise forms.ValidationError(msg) if not modulestore().has_course(course_key): msg = u'COURSE NOT FOUND' msg += u' --- Entered course id was: "{0}". '.format(course_key.to_deprecated_string()) msg += 'Please recheck that you have supplied a valid course id.' raise forms.ValidationError(msg) # Now, try and discern if it is a Studio course - HTML editor doesn't work with XML courses is_studio_course = modulestore().get_modulestore_type(course_key) != ModuleStoreEnum.Type.xml if not is_studio_course: msg = "Course Email feature is only available for courses authored in Studio. " msg += '"{0}" appears to be an XML backed course.'.format(course_key.to_deprecated_string()) raise forms.ValidationError(msg) return course_key
def handle(self, *args, **options): "Execute the command" if len(args) == 0: raise CommandError("import requires at least one argument: <data directory> [--nostatic] [<course dir>...]") data_dir = args[0] do_import_static = not (options.get('nostatic', False)) if len(args) > 1: course_dirs = args[1:] else: course_dirs = None self.stdout.write("Importing. Data_dir={data}, course_dirs={courses}\n".format( data=data_dir, courses=course_dirs, dis=do_import_static)) try: mstore = modulestore('direct') except KeyError: self.stdout.write('Unable to load direct modulestore, trying ' 'default\n') mstore = modulestore('default') _, course_items = import_from_xml( mstore, data_dir, course_dirs, load_error_modules=False, static_content_store=contentstore(), verbose=True, do_import_static=do_import_static, create_new_course_if_not_present=True, ) for course in course_items: course_id = course.id if not are_permissions_roles_seeded(course_id): self.stdout.write('Seeding forum roles for course {0}\n'.format(course_id)) seed_permissions_roles(course_id)
def create(cls, data): """ Create a Bookmark object. Arguments: data (dict): The data to create the object with. Returns: A Bookmark object. Raises: ItemNotFoundError: If no block exists for the usage_key. """ data = dict(data) usage_key = data.pop('usage_key') with modulestore().bulk_operations(usage_key.course_key): block = modulestore().get_item(usage_key) xblock_cache = XBlockCache.create({ 'usage_key': usage_key, 'display_name': block.display_name_with_default, }) data['_path'] = prepare_path_for_serialization(Bookmark.updated_path(usage_key, xblock_cache)) data['course_key'] = usage_key.course_key data['xblock_cache'] = xblock_cache user = data.pop('user') # Sometimes this ends up in data, but newer versions of Django will fail on having unknown keys in defaults data.pop('display_name', None) bookmark, created = cls.objects.get_or_create(usage_key=usage_key, user=user, defaults=data) return bookmark, created
def test_get_items(self): ''' This verifies a bug we had where the None setting in get_items() meant 'wildcard' Unfortunately, None = published for the revision field, so get_items() would return both draft and non-draft copies. ''' store = modulestore('direct') draft_store = modulestore('draft') import_from_xml(store, 'common/test/data/', ['simple']) html_module = draft_store.get_item(['i4x', 'edX', 'simple', 'html', 'test_html', None]) draft_store.clone_item(html_module.location, html_module.location) # now query get_items() to get this location with revision=None, this should just # return back a single item (not 2) items = store.get_items(['i4x', 'edX', 'simple', 'html', 'test_html', None]) self.assertEqual(len(items), 1) self.assertFalse(getattr(items[0], 'is_draft', False)) # now refetch from the draft store. Note that even though we pass # None in the revision field, the draft store will replace that with 'draft' items = draft_store.get_items(['i4x', 'edX', 'simple', 'html', 'test_html', None]) self.assertEqual(len(items), 1) self.assertTrue(getattr(items[0], 'is_draft', False))
def update_from_json(cls, descriptor, jsondict, filter_tabs=True, user=None): """ Decode the json into CourseMetadata and save any changed attrs to the db. Ensures none of the fields are in the blacklist. """ # Copy the filtered list to avoid permanently changing the class attribute. filtered_list = list(cls.FILTERED_LIST) # Don't filter on the tab attribute if filter_tabs is False. if not filter_tabs: filtered_list.remove("tabs") # Validate the values before actually setting them. key_values = {} for key, model in jsondict.iteritems(): # should it be an error if one of the filtered list items is in the payload? if key in filtered_list: continue try: val = model['value'] if hasattr(descriptor, key) and getattr(descriptor, key) != val: key_values[key] = descriptor.fields[key].from_json(val) except (TypeError, ValueError) as err: raise ValueError(_("Incorrect format for field '{name}'. {detailed_message}".format( name=model['display_name'], detailed_message=err.message))) for key, value in key_values.iteritems(): setattr(descriptor, key, value) if len(key_values) > 0: modulestore().update_item(descriptor, user.id if user else None) return cls.fetch(descriptor)
def test_update_section_grader_type(self): # Get the descriptor and the section_grader_type and assert they are the default values descriptor = modulestore().get_item(self.course.location) section_grader_type = CourseGradingModel.get_section_grader_type(self.course.location) self.assertEqual('notgraded', section_grader_type['graderType']) self.assertEqual(None, descriptor.format) self.assertEqual(False, descriptor.graded) # Change the default grader type to Homework, which should also mark the section as graded CourseGradingModel.update_section_grader_type(self.course, 'Homework', self.user) descriptor = modulestore().get_item(self.course.location) section_grader_type = CourseGradingModel.get_section_grader_type(self.course.location) self.assertEqual('Homework', section_grader_type['graderType']) self.assertEqual('Homework', descriptor.format) self.assertEqual(True, descriptor.graded) # Change the grader type back to notgraded, which should also unmark the section as graded CourseGradingModel.update_section_grader_type(self.course, 'notgraded', self.user) descriptor = modulestore().get_item(self.course.location) section_grader_type = CourseGradingModel.get_section_grader_type(self.course.location) self.assertEqual('notgraded', section_grader_type['graderType']) self.assertEqual(None, descriptor.format) self.assertEqual(False, descriptor.graded)
def test_group(self): self.course.cohort_config = {"cohorted": True} modulestore().update_item(self.course, ModuleStoreEnum.UserID.test) cohort = CohortFactory.create(course_id=self.course.id) serialized = self.serialize(self.make_cs_content({"group_id": cohort.id})) self.assertEqual(serialized["group_id"], cohort.id) self.assertEqual(serialized["group_name"], cohort.name)
def setup_course(self): self.course = CourseFactory.create(data=self.COURSE_DATA) # Turn off cache. modulestore().request_cache = None modulestore().metadata_inheritance_cache_subsystem = None chapter = ItemFactory.create( parent_location=self.course.location, category="sequential", ) self.section = ItemFactory.create( parent_location=chapter.location, category="sequential" ) # username = robot{0}, password = '******' self.users = [ UserFactory.create() for i in range(self.USER_COUNT) ] for user in self.users: CourseEnrollmentFactory.create(user=user, course_id=self.course.id) # login all users for acces to Xmodule self.clients = {user.username: Client() for user in self.users} self.login_statuses = [ self.clients[user.username].login( username=user.username, password='******') for user in self.users ] self.assertTrue(all(self.login_statuses))
def test_entrance_exam_created_and_deleted_successfully(self): self._seed_milestone_relationship_types() settings_details_url = get_url(self.course.id) data = { 'entrance_exam_enabled': 'true', 'entrance_exam_minimum_score_pct': '60', 'syllabus': 'none', 'short_description': 'empty', 'overview': '', 'effort': '', 'category': '', 'intro_video': '' } response = self.client.post(settings_details_url, data=json.dumps(data), content_type='application/json', HTTP_ACCEPT='application/json') self.assertEquals(response.status_code, 200) course = modulestore().get_course(self.course.id) self.assertTrue(course.entrance_exam_enabled) self.assertEquals(course.entrance_exam_minimum_score_pct, .60) # Delete the entrance exam data['entrance_exam_enabled'] = "false" response = self.client.post( settings_details_url, data=json.dumps(data), content_type='application/json', HTTP_ACCEPT='application/json' ) course = modulestore().get_course(self.course.id) self.assertEquals(response.status_code, 200) self.assertFalse(course.entrance_exam_enabled) self.assertEquals(course.entrance_exam_minimum_score_pct, None)
def test_no_ol_course_update(self): '''Test trying to add to a saved course_update which is not an ol.''' # get the updates and set to something wrong location = self.course.location.replace(category='course_info', name='updates') modulestore('direct').create_and_save_xmodule(location) course_updates = modulestore('direct').get_item(location) course_updates.data = 'bad news' modulestore('direct').update_item(location, course_updates.data) init_content = '<iframe width="560" height="315" src="http://www.youtube.com/embed/RocY-Jd93XU" frameborder="0">' content = init_content + '</iframe>' payload = {'content': content, 'date': 'January 8, 2013'} url = reverse('course_info_json', kwargs={'org': self.course.location.org, 'course': self.course.location.course, 'provided_id': ''}) resp = self.client.post(url, json.dumps(payload), "application/json") payload = json.loads(resp.content) self.assertHTMLEqual(payload['content'], content) # now confirm that the bad news and the iframe make up 2 updates url = reverse('course_info_json', kwargs={'org': self.course.location.org, 'course': self.course.location.course, 'provided_id': ''}) resp = self.client.get(url) payload = json.loads(resp.content) self.assertTrue(len(payload) == 2)
def test_entrance_exam_requirement_message_with_correct_percentage(self): """ Unit Test: entrance exam requirement message should be present in response and percentage of required score should be rounded as expected """ minimum_score_pct = 29 self.course.entrance_exam_minimum_score_pct = float(minimum_score_pct) / 100 modulestore().update_item(self.course, self.request.user.id) # pylint: disable=no-member # answer the problem so it results in only 20% correct. answer_entrance_exam_problem(self.course, self.request, self.problem_1, value=1, max_value=5) url = reverse( 'courseware_section', kwargs={ 'course_id': unicode(self.course.id), 'chapter': self.entrance_exam.location.block_id, 'section': self.exam_1.location.block_id } ) resp = self.client.get(url) self.assertEqual(resp.status_code, 200) self.assertIn( 'To access course materials, you must score {}% or higher'.format(minimum_score_pct), resp.content ) self.assertIn('Your current score is 20%.', resp.content)
def submit_delete_entrance_exam_state_for_student(request, usage_key, student): # pylint: disable=invalid-name """ Requests reset of state for entrance exam as a background task. Module state for all problems in entrance exam will be deleted for specified student. Parameters are `usage_key`, which must be a :class:`Location` representing entrance exam section and the `student` as a User object. ItemNotFoundError is raised if entrance exam does not exists for given usage_key, AlreadyRunningError is raised if the entrance exam is already being reset. This method makes sure the InstructorTask entry is committed. When called from any view that is wrapped by TransactionMiddleware, and thus in a "commit-on-success" transaction, an autocommit buried within here will cause any pending transaction to be committed by a successful save here. Any future database operations will take place in a separate transaction. """ # check arguments: make sure entrance exam(section) exists for given usage_key modulestore().get_item(usage_key) task_type = 'delete_problem_state' task_class = delete_problem_state task_input, task_key = encode_entrance_exam_and_student_input(usage_key, student) return submit_task(request, task_type, task_class, usage_key.course_key, task_input, task_key)
def _fulfill_content_milestones(user, course_key, content_key): """ Internal helper to handle milestone fulfillments for the specified content module """ # Fulfillment Use Case: Entrance Exam # If this module is part of an entrance exam, we'll need to see if the student # has reached the point at which they can collect the associated milestone if settings.FEATURES.get('ENTRANCE_EXAMS', False): course = modulestore().get_course(course_key) content = modulestore().get_item(content_key) entrance_exam_enabled = getattr(course, 'entrance_exam_enabled', False) in_entrance_exam = getattr(content, 'in_entrance_exam', False) if entrance_exam_enabled and in_entrance_exam: # We don't have access to the true request object in this context, but we can use a mock request = RequestFactory().request() request.user = user exam_pct = get_entrance_exam_score(request, course) if exam_pct >= course.entrance_exam_minimum_score_pct: exam_key = UsageKey.from_string(course.entrance_exam_id) relationship_types = milestones_helpers.get_milestone_relationship_types() content_milestones = milestones_helpers.get_course_content_milestones( course_key, exam_key, relationship=relationship_types['FULFILLS'] ) # Add each milestone to the user's set... user = {'id': request.user.id} for milestone in content_milestones: milestones_helpers.add_user_milestone(user, milestone)
def _get_module_info(xblock, rewrite_static_links=True, include_ancestor_info=False, include_publishing_info=False): """ metadata, data, id representation of a leaf module fetcher. :param usage_key: A UsageKey """ with modulestore().bulk_operations(xblock.location.course_key): data = getattr(xblock, 'data', '') if rewrite_static_links: data = replace_static_urls( data, None, course_id=xblock.location.course_key ) # Pre-cache has changes for the entire course because we'll need it for the ancestor info # Except library blocks which don't [yet] use draft/publish if not isinstance(xblock.location, LibraryUsageLocator): modulestore().has_changes(modulestore().get_course(xblock.location.course_key, depth=None)) # Note that children aren't being returned until we have a use case. xblock_info = create_xblock_info( xblock, data=data, metadata=own_metadata(xblock), include_ancestor_info=include_ancestor_info ) if include_publishing_info: add_container_page_publishing_info(xblock, xblock_info) return xblock_info
def instrument_course_progress_render(self, course_width, enable_ccx, queries, reads, xblocks): """ Renders the progress page, instrumenting Mongo reads and SQL queries. """ self.setup_course(course_width, enable_ccx) # Switch to published-only mode to simulate the LMS with self.settings(MODULESTORE_BRANCH='published-only'): # Clear all caches before measuring for cache in settings.CACHES: get_cache(cache).clear() # Refill the metadata inheritance cache modulestore().get_course(self.course.id, depth=None) # We clear the request cache to simulate a new request in the LMS. RequestCache.clear_request_cache() # Reset the list of provider classes, so that our django settings changes # can actually take affect. OverrideFieldData.provider_classes = None with self.assertNumQueries(queries): with check_mongo_calls(reads): with check_sum_of_calls(XBlock, ['__init__'], xblocks, xblocks, include_arguments=False): self.grade_course(self.course)
def handle_xblock_callback(request, course_id, usage_id, handler, suffix=None): """ Generic view for extensions. This is where AJAX calls go. Arguments: - request -- the django request. - location -- the module location. Used to look up the XModule instance - course_id -- defines the course context for this request. Return 403 error if the user is not logged in. Raises Http404 if the location and course_id do not identify a valid module, the module is not accessible by the user, or the module raises NotFoundError. If the module raises any other error, it will escape this function. """ if not request.user.is_authenticated(): return HttpResponse('Unauthenticated', status=403) try: course_key = CourseKey.from_string(course_id) except InvalidKeyError: raise Http404("Invalid location") with modulestore().bulk_operations(course_key): try: course = modulestore().get_course(course_key) except ItemNotFoundError: raise Http404("invalid location") return _invoke_xblock_handler(request, course_id, usage_id, handler, suffix, course=course)
def component_handler(request, usage_key_string, handler, suffix=''): """ Dispatch an AJAX action to an xblock Args: usage_id: The usage-id of the block to dispatch to handler (str): The handler to execute suffix (str): The remainder of the url to be passed to the handler Returns: :class:`django.http.HttpResponse`: The response from the handler, converted to a django response """ usage_key = UsageKey.from_string(usage_key_string) descriptor = modulestore().get_item(usage_key) # Let the module handle the AJAX req = django_to_webob_request(request) try: resp = descriptor.handle(handler, req, suffix) except NoSuchHandlerError: log.info("XBlock %s attempted to access missing handler %r", descriptor, handler, exc_info=True) raise Http404 # unintentional update to handle any side effects of handle call; so, request user didn't author # the change modulestore().update_item(descriptor, None) return webob_to_django_response(resp)
def submit_delete_problem_state_for_all_students(request, usage_key): # pylint: disable=invalid-name """ Request to have state deleted for a problem as a background task. The problem's state will be deleted for all students who have accessed the particular problem in a course. Parameters are the `course_id` and the `usage_key`, which must be a :class:`Location`. ItemNotFoundException is raised if the problem doesn't exist, or AlreadyRunningError if the particular problem's state is already being deleted. This method makes sure the InstructorTask entry is committed. When called from any view that is wrapped by TransactionMiddleware, and thus in a "commit-on-success" transaction, an autocommit buried within here will cause any pending transaction to be committed by a successful save here. Any future database operations will take place in a separate transaction. """ # check arguments: make sure that the usage_key is defined # (since that's currently typed in). If the corresponding module descriptor doesn't exist, # an exception will be raised. Let it pass up to the caller. modulestore().get_item(usage_key) task_type = 'delete_problem_state' task_class = delete_problem_state task_input, task_key = encode_problem_and_student_input(usage_key) return submit_task(request, task_type, task_class, usage_key.course_key, task_input, task_key)
def orphan_handler(request, tag=None, package_id=None, branch=None, version_guid=None, block=None): """ View for handling orphan related requests. GET gets all of the current orphans. DELETE removes all orphans (requires is_staff access) An orphan is a block whose category is not in the DETACHED_CATEGORY list, is not the root, and is not reachable from the root via children :param request: :param package_id: Locator syntax package_id """ location = BlockUsageLocator(package_id=package_id, branch=branch, version_guid=version_guid, block_id=block) # DHM: when split becomes back-end, move or conditionalize this conversion old_location = loc_mapper().translate_locator_to_location(location) if request.method == 'GET': if has_course_access(request.user, old_location): return JsonResponse(modulestore().get_orphans(old_location, 'draft')) else: raise PermissionDenied() if request.method == 'DELETE': if request.user.is_staff: items = modulestore().get_orphans(old_location, 'draft') for itemloc in items: modulestore('draft').delete_item(itemloc, delete_all_versions=True) return JsonResponse({'deleted': items}) else: raise PermissionDenied()
def component_handler(request, usage_id, handler, suffix=""): """ Dispatch an AJAX action to an xblock Args: usage_id: The usage-id of the block to dispatch to, passed through `quote_slashes` handler (str): The handler to execute suffix (str): The remainder of the url to be passed to the handler Returns: :class:`django.http.HttpResponse`: The response from the handler, converted to a django response """ location = unquote_slashes(usage_id) descriptor = modulestore().get_item(location) # Let the module handle the AJAX req = django_to_webob_request(request) try: resp = descriptor.handle(handler, req, suffix) except NoSuchHandlerError: log.info("XBlock %s attempted to access missing handler %r", descriptor, handler, exc_info=True) raise Http404 modulestore().save_xmodule(descriptor) return webob_to_django_response(resp)
def preprocess_collection(user, course, collection): """ Prepare `collection(notes_list)` provided by edx-notes-api for rendering in a template: add information about ancestor blocks, convert "updated" to date Raises: ItemNotFoundError - when appropriate module is not found. """ # pylint: disable=too-many-statements store = modulestore() filtered_collection = list() cache = {} with store.bulk_operations(course.id): for model in collection: update = { u"text": sanitize_html(model["text"]), u"quote": sanitize_html(model["quote"]), u"updated": dateutil_parse(model["updated"]), } if "tags" in model: update[u"tags"] = [sanitize_html(tag) for tag in model["tags"]] model.update(update) usage_id = model["usage_id"] if usage_id in cache: model.update(cache[usage_id]) filtered_collection.append(model) continue usage_key = UsageKey.from_string(usage_id) # Add a course run if necessary. usage_key = usage_key.replace(course_key=store.fill_in_run(usage_key.course_key)) try: item = store.get_item(usage_key) except ItemNotFoundError: log.debug("Module not found: %s", usage_key) continue if not has_access(user, "load", item, course_key=course.id): log.debug("User %s does not have an access to %s", user, item) continue unit = get_parent_unit(item) if unit is None: log.debug("Unit not found: %s", usage_key) continue section = unit.get_parent() if not section: log.debug("Section not found: %s", usage_key) continue if section in cache: usage_context = cache[section] usage_context.update({ "unit": get_module_context(course, unit), }) model.update(usage_context) cache[usage_id] = cache[unit] = usage_context filtered_collection.append(model) continue chapter = section.get_parent() if not chapter: log.debug("Chapter not found: %s", usage_key) continue if chapter in cache: usage_context = cache[chapter] usage_context.update({ "unit": get_module_context(course, unit), "section": get_module_context(course, section), }) model.update(usage_context) cache[usage_id] = cache[unit] = cache[section] = usage_context filtered_collection.append(model) continue usage_context = { "unit": get_module_context(course, unit), "section": get_module_context(course, section), "chapter": get_module_context(course, chapter), } model.update(usage_context) cache[usage_id] = cache[unit] = cache[section] = cache[chapter] = usage_context filtered_collection.append(model) return filtered_collection
def setUp(self): """ Test case scaffolding """ super(EntranceExamTestCases, self).setUp() self.course = CourseFactory.create(metadata={ 'entrance_exam_enabled': True, }) with self.store.bulk_operations(self.course.id): self.chapter = ItemFactory.create(parent=self.course, display_name='Overview') self.welcome = ItemFactory.create(parent=self.chapter, display_name='Welcome') ItemFactory.create(parent=self.course, category='chapter', display_name="Week 1") self.chapter_subsection = ItemFactory.create( parent=self.chapter, category='sequential', display_name="Lesson 1") chapter_vertical = ItemFactory.create( parent=self.chapter_subsection, category='vertical', display_name='Lesson 1 Vertical - Unit 1') ItemFactory.create(parent=chapter_vertical, category="problem", display_name="Problem - Unit 1 Problem 1") ItemFactory.create(parent=chapter_vertical, category="problem", display_name="Problem - Unit 1 Problem 2") ItemFactory.create(category="instructor", parent=self.course, data="Instructor Tab", display_name="Instructor") self.entrance_exam = ItemFactory.create( parent=self.course, category="chapter", display_name="Entrance Exam Section - Chapter 1", is_entrance_exam=True, in_entrance_exam=True) self.exam_1 = ItemFactory.create( parent=self.entrance_exam, category='sequential', display_name="Exam Sequential - Subsection 1", graded=True, in_entrance_exam=True) subsection = ItemFactory.create( parent=self.exam_1, category='vertical', display_name='Exam Vertical - Unit 1') problem_xml = MultipleChoiceResponseXMLFactory().build_xml( question_text='The correct answer is Choice 3', choices=[False, False, True, False], choice_names=['choice_0', 'choice_1', 'choice_2', 'choice_3']) self.problem_1 = ItemFactory.create( parent=subsection, category="problem", display_name="Exam Problem - Problem 1", data=problem_xml) self.problem_2 = ItemFactory.create( parent=subsection, category="problem", display_name="Exam Problem - Problem 2") add_entrance_exam_milestone(self.course, self.entrance_exam) self.course.entrance_exam_enabled = True self.course.entrance_exam_minimum_score_pct = 0.50 self.course.entrance_exam_id = six.text_type( self.entrance_exam.scope_ids.usage_id) self.anonymous_user = AnonymousUserFactory() self.addCleanup(set_current_request, None) self.request = get_mock_request(UserFactory()) modulestore().update_item(self.course, self.request.user.id) self.client.login(username=self.request.user.username, password="******") CourseEnrollment.enroll(self.request.user, self.course.id) self.expected_locked_toc = ([{ 'active': True, 'sections': [{ 'url_name': u'Exam_Sequential_-_Subsection_1', 'display_name': u'Exam Sequential - Subsection 1', 'graded': True, 'format': '', 'due': None, 'active': True }], 'url_name': u'Entrance_Exam_Section_-_Chapter_1', 'display_name': u'Entrance Exam Section - Chapter 1', 'display_id': u'entrance-exam-section-chapter-1', }]) self.expected_unlocked_toc = ([{ 'active': False, 'sections': [{ 'url_name': u'Welcome', 'display_name': u'Welcome', 'graded': False, 'format': '', 'due': None, 'active': False }, { 'url_name': u'Lesson_1', 'display_name': u'Lesson 1', 'graded': False, 'format': '', 'due': None, 'active': False }], 'url_name': u'Overview', 'display_name': u'Overview', 'display_id': u'overview' }, { 'active': False, 'sections': [], 'url_name': u'Week_1', 'display_name': u'Week 1', 'display_id': u'week-1' }, { 'active': False, 'sections': [], 'url_name': u'Instructor', 'display_name': u'Instructor', 'display_id': u'instructor' }, { 'active': True, 'sections': [{ 'url_name': u'Exam_Sequential_-_Subsection_1', 'display_name': u'Exam Sequential - Subsection 1', 'graded': True, 'format': '', 'due': None, 'active': True }], 'url_name': u'Entrance_Exam_Section_-_Chapter_1', 'display_name': u'Entrance Exam Section - Chapter 1', 'display_id': u'entrance-exam-section-chapter-1' }])
def enable_edxnotes_for_the_course(course, user_id): """ Enable EdxNotes for the course. """ course.tabs.append(EdxNotesTab()) modulestore().update_item(course, user_id)
def create_xblock(parent_locator, user, category, display_name, boilerplate=None, is_entrance_exam=False): """ Performs the actual grunt work of creating items/xblocks -- knows nothing about requests, views, etc. """ store = modulestore() usage_key = usage_key_with_run(parent_locator) with store.bulk_operations(usage_key.course_key): parent = store.get_item(usage_key) dest_usage_key = usage_key.replace(category=category, name=uuid4().hex) # get the metadata, display_name, and definition from the caller metadata = {} data = None template_id = boilerplate if template_id: clz = parent.runtime.load_block_type(category) if clz is not None: template = clz.get_template(template_id) if template is not None: metadata = template.get('metadata', {}) data = template.get('data') if display_name is not None: metadata['display_name'] = display_name # We should use the 'fields' kwarg for newer module settings/values (vs. metadata or data) fields = {} # Entrance Exams: Chapter module positioning child_position = None if settings.FEATURES.get('ENTRANCE_EXAMS', False): if category == 'chapter' and is_entrance_exam: fields['is_entrance_exam'] = is_entrance_exam fields[ 'in_entrance_exam'] = True # Inherited metadata, all children will have it child_position = 0 # TODO need to fix components that are sending definition_data as strings, instead of as dicts # For now, migrate them into dicts here. if isinstance(data, basestring): data = {'data': data} created_block = store.create_child( user.id, usage_key, dest_usage_key.block_type, block_id=dest_usage_key.block_id, fields=fields, definition_data=data, metadata=metadata, runtime=parent.runtime, position=child_position, ) # Entrance Exams: Grader assignment if settings.FEATURES.get('ENTRANCE_EXAMS', False): course = store.get_course(usage_key.course_key) if hasattr( course, 'entrance_exam_enabled') and course.entrance_exam_enabled: if category == 'sequential' and parent_locator == course.entrance_exam_id: grader = { "type": "Entrance Exam", "min_count": 0, "drop_count": 0, "short_label": "Entrance", "weight": 0 } grading_model = CourseGradingModel.update_grader_from_json( course.id, grader, user) CourseGradingModel.update_section_grader_type( created_block, grading_model['type'], user) # VS[compat] cdodge: This is a hack because static_tabs also have references from the course module, so # if we add one then we need to also add it to the policy information (i.e. metadata) # we should remove this once we can break this reference from the course to static tabs if category == 'static_tab': display_name = display_name or _( "Empty") # Prevent name being None course = store.get_course(dest_usage_key.course_key) course.tabs.append( StaticTab( name=display_name, url_slug=dest_usage_key.name, )) store.update_item(course, user.id) return created_block
def _validate_courses(self): for run in self.catalog_course_runs: course_key = CourseKey.from_string(run.get('key')) # lint-amnesty, pylint: disable=no-member self.assertTrue(modulestore().has_course(course_key)) CourseOverview.objects.get(id=run.get('key')) # lint-amnesty, pylint: disable=no-member
def perform_module_state_update(update_fcn, filter_fcn, _entry_id, course_id, task_input, action_name): """ Performs generic update by visiting StudentModule instances with the update_fcn provided. StudentModule instances are those that match the specified `course_id` and `module_state_key`. If `student_identifier` is not None, it is used as an additional filter to limit the modules to those belonging to that student. If `student_identifier` is None, performs update on modules for all students on the specified problem. If a `filter_fcn` is not None, it is applied to the query that has been constructed. It takes one argument, which is the query being filtered, and returns the filtered version of the query. The `update_fcn` is called on each StudentModule that passes the resulting filtering. It is passed three arguments: the module_descriptor for the module pointed to by the module_state_key, the particular StudentModule to update, and the xmodule_instance_args being passed through. If the value returned by the update function evaluates to a boolean True, the update is successful; False indicates the update on the particular student module failed. A raised exception indicates a fatal condition -- that no other student modules should be considered. The return value is a dict containing the task's results, with the following keys: 'attempted': number of attempts made 'succeeded': number of attempts that "succeeded" 'skipped': number of attempts that "skipped" 'failed': number of attempts that "failed" 'total': number of possible updates to attempt 'action_name': user-visible verb to use in status messages. Should be past-tense. Pass-through of input `action_name`. 'duration_ms': how long the task has (or had) been running. Because this is run internal to a task, it does not catch exceptions. These are allowed to pass up to the next level, so that it can set the failure modes and capture the error trace in the InstructorTask and the result object. """ # get start time for task: start_time = time() module_state_key = task_input.get('problem_url') student_identifier = task_input.get('student') # find the problem descriptor: module_descriptor = modulestore().get_instance(course_id, module_state_key) # find the module in question modules_to_update = StudentModule.objects.filter( course_id=course_id, module_state_key=module_state_key) # give the option of updating an individual student. If not specified, # then updates all students who have responded to a problem so far student = None if student_identifier is not None: # if an identifier is supplied, then look for the student, # and let it throw an exception if none is found. if "@" in student_identifier: student = User.objects.get(email=student_identifier) elif student_identifier is not None: student = User.objects.get(username=student_identifier) if student is not None: modules_to_update = modules_to_update.filter(student_id=student.id) if filter_fcn is not None: modules_to_update = filter_fcn(modules_to_update) # perform the main loop num_attempted = 0 num_succeeded = 0 num_skipped = 0 num_failed = 0 num_total = modules_to_update.count() def get_task_progress(): """Return a dict containing info about current task""" current_time = time() progress = { 'action_name': action_name, 'attempted': num_attempted, 'succeeded': num_succeeded, 'skipped': num_skipped, 'failed': num_failed, 'total': num_total, 'duration_ms': int((current_time - start_time) * 1000), } return progress task_progress = get_task_progress() _get_current_task().update_state(state=PROGRESS, meta=task_progress) for module_to_update in modules_to_update: num_attempted += 1 # There is no try here: if there's an error, we let it throw, and the task will # be marked as FAILED, with a stack trace. with dog_stats_api.timer( 'instructor_tasks.module.time.step', tags=['action:{name}'.format(name=action_name)]): update_status = update_fcn(module_descriptor, module_to_update) if update_status == UPDATE_STATUS_SUCCEEDED: # If the update_fcn returns true, then it performed some kind of work. # Logging of failures is left to the update_fcn itself. num_succeeded += 1 elif update_status == UPDATE_STATUS_FAILED: num_failed += 1 elif update_status == UPDATE_STATUS_SKIPPED: num_skipped += 1 else: raise UpdateProblemModuleStateError( "Unexpected update_status returned: {}".format( update_status)) # update task status: task_progress = get_task_progress() _get_current_task().update_state(state=PROGRESS, meta=task_progress) return task_progress
def student_dashboard(request): """ Provides the LMS dashboard view TODO: This is lms specific and does not belong in common code. Arguments: request: The request object. Returns: The dashboard response. """ user = request.user if not UserProfile.objects.filter(user=user).exists(): return redirect(reverse('account_settings')) platform_name = configuration_helpers.get_value("platform_name", settings.PLATFORM_NAME) enable_verified_certificates = configuration_helpers.get_value( 'ENABLE_VERIFIED_CERTIFICATES', settings.FEATURES.get('ENABLE_VERIFIED_CERTIFICATES')) display_course_modes_on_dashboard = configuration_helpers.get_value( 'DISPLAY_COURSE_MODES_ON_DASHBOARD', settings.FEATURES.get('DISPLAY_COURSE_MODES_ON_DASHBOARD', True)) activation_email_support_link = configuration_helpers.get_value( 'ACTIVATION_EMAIL_SUPPORT_LINK', settings.ACTIVATION_EMAIL_SUPPORT_LINK) or settings.SUPPORT_SITE_LINK hide_dashboard_courses_until_activated = configuration_helpers.get_value( 'HIDE_DASHBOARD_COURSES_UNTIL_ACTIVATED', settings.FEATURES.get('HIDE_DASHBOARD_COURSES_UNTIL_ACTIVATED', False)) empty_dashboard_message = configuration_helpers.get_value( 'EMPTY_DASHBOARD_MESSAGE', None) # Get the org whitelist or the org blacklist for the current site site_org_whitelist, site_org_blacklist = get_org_black_and_whitelist_for_site( ) course_enrollments = list( get_course_enrollments(user, site_org_whitelist, site_org_blacklist)) # Get the entitlements for the user and a mapping to all available sessions for that entitlement # If an entitlement has no available sessions, pass through a mock course overview object (course_entitlements, course_entitlement_available_sessions, unfulfilled_entitlement_pseudo_sessions ) = get_filtered_course_entitlements(user, site_org_whitelist, site_org_blacklist) # Record how many courses there are so that we can get a better # understanding of usage patterns on prod. monitoring_utils.accumulate('num_courses', len(course_enrollments)) # Sort the enrollment pairs by the enrollment date course_enrollments.sort(key=lambda x: x.created, reverse=True) # Retrieve the course modes for each course enrolled_course_ids = [ enrollment.course_id for enrollment in course_enrollments ] __, unexpired_course_modes = CourseMode.all_and_unexpired_modes_for_courses( enrolled_course_ids) course_modes_by_course = { course_id: {mode.slug: mode for mode in modes} for course_id, modes in iteritems(unexpired_course_modes) } # Check to see if the student has recently enrolled in a course. # If so, display a notification message confirming the enrollment. enrollment_message = _create_recent_enrollment_message( course_enrollments, course_modes_by_course) course_optouts = Optout.objects.filter(user=user).values_list('course_id', flat=True) # Display activation message activate_account_message = '' if not user.is_active: activate_account_message = Text( _("Check your {email_start}{email}{email_end} inbox for an account activation link from {platform_name}. " "If you need help, contact {link_start}{platform_name} Support{link_end}." ) ).format( platform_name=platform_name, email_start=HTML("<strong>"), email_end=HTML("</strong>"), email=user.email, link_start=HTML( "<a target='_blank' href='{activation_email_support_link}'>"). format( activation_email_support_link=activation_email_support_link, ), link_end=HTML("</a>"), ) enterprise_message = get_dashboard_consent_notification( request, user, course_enrollments) # Disable lookup of Enterprise consent_required_course due to ENT-727 # Will re-enable after fixing WL-1315 consent_required_courses = set() enterprise_customer_name = None # Account activation message account_activation_messages = [ message for message in messages.get_messages(request) if 'account-activation' in message.tags ] # Global staff can see what courses encountered an error on their dashboard staff_access = False errored_courses = {} if has_access(user, 'staff', 'global'): # Show any courses that encountered an error on load staff_access = True errored_courses = modulestore().get_errored_courses() show_courseware_links_for = { enrollment.course_id: has_access(request.user, 'load', enrollment.course_overview) for enrollment in course_enrollments } # Find programs associated with course runs being displayed. This information # is passed in the template context to allow rendering of program-related # information on the dashboard. meter = ProgramProgressMeter(request.site, user, enrollments=course_enrollments) ecommerce_service = EcommerceService() inverted_programs = meter.invert_programs() urls, programs_data = {}, {} bundles_on_dashboard_flag = WaffleFlag( WaffleFlagNamespace(name=u'student.experiments'), u'bundles_on_dashboard') # TODO: Delete this code and the relevant HTML code after testing LEARNER-3072 is complete if bundles_on_dashboard_flag.is_enabled( ) and inverted_programs and inverted_programs.items(): if len(course_enrollments) < 4: for program in inverted_programs.values(): try: program_uuid = program[0]['uuid'] program_data = get_programs(request.site, uuid=program_uuid) program_data = ProgramDataExtender(program_data, request.user).extend() skus = program_data.get('skus') checkout_page_url = ecommerce_service.get_checkout_page_url( *skus) program_data[ 'completeProgramURL'] = checkout_page_url + '&bundle=' + program_data.get( 'uuid') programs_data[program_uuid] = program_data except: # pylint: disable=bare-except pass # Construct a dictionary of course mode information # used to render the course list. We re-use the course modes dict # we loaded earlier to avoid hitting the database. course_mode_info = { enrollment.course_id: complete_course_mode_info( enrollment.course_id, enrollment, modes=course_modes_by_course[enrollment.course_id]) for enrollment in course_enrollments } # Determine the per-course verification status # This is a dictionary in which the keys are course locators # and the values are one of: # # VERIFY_STATUS_NEED_TO_VERIFY # VERIFY_STATUS_SUBMITTED # VERIFY_STATUS_APPROVED # VERIFY_STATUS_MISSED_DEADLINE # # Each of which correspond to a particular message to display # next to the course on the dashboard. # # If a course is not included in this dictionary, # there is no verification messaging to display. verify_status_by_course = check_verify_status_by_course( user, course_enrollments) cert_statuses = { enrollment.course_id: cert_info(request.user, enrollment.course_overview) for enrollment in course_enrollments } # only show email settings for Mongo course and when bulk email is turned on show_email_settings_for = frozenset( enrollment.course_id for enrollment in course_enrollments if (BulkEmailFlag.feature_enabled(enrollment.course_id))) # Verification Attempts # Used to generate the "you must reverify for course x" banner verification_status = IDVerificationService.user_status(user) verification_errors = get_verification_error_reasons_for_display( verification_status['error']) # Gets data for midcourse reverifications, if any are necessary or have failed statuses = ["approved", "denied", "pending", "must_reverify"] reverifications = reverification_info(statuses) block_courses = frozenset( enrollment.course_id for enrollment in course_enrollments if is_course_blocked( request, CourseRegistrationCode.objects.filter( course_id=enrollment.course_id, registrationcoderedemption__redeemed_by=request.user), enrollment.course_id)) enrolled_courses_either_paid = frozenset( enrollment.course_id for enrollment in course_enrollments if enrollment.is_paid_course()) # If there are *any* denied reverifications that have not been toggled off, # we'll display the banner denied_banner = any(item.display for item in reverifications["denied"]) # Populate the Order History for the side-bar. order_history_list = order_history(user, course_org_filter=site_org_whitelist, org_filter_out_set=site_org_blacklist) # get list of courses having pre-requisites yet to be completed courses_having_prerequisites = frozenset( enrollment.course_id for enrollment in course_enrollments if enrollment.course_overview.pre_requisite_courses) courses_requirements_not_met = get_pre_requisite_courses_not_completed( user, courses_having_prerequisites) if 'notlive' in request.GET: redirect_message = _( "The course you are looking for does not start until {date}." ).format(date=request.GET['notlive']) elif 'course_closed' in request.GET: redirect_message = _( "The course you are looking for is closed for enrollment as of {date}." ).format(date=request.GET['course_closed']) else: redirect_message = '' valid_verification_statuses = [ 'approved', 'must_reverify', 'pending', 'expired' ] display_sidebar_on_dashboard = ( len(order_history_list) or (verification_status['status'] in valid_verification_statuses and verification_status['should_display'])) # Filter out any course enrollment course cards that are associated with fulfilled entitlements for entitlement in [ e for e in course_entitlements if e.enrollment_course_run is not None ]: course_enrollments = [ enr for enr in course_enrollments if entitlement.enrollment_course_run.course_id != enr.course_id ] context = { 'urls': urls, 'programs_data': programs_data, 'enterprise_message': enterprise_message, 'consent_required_courses': consent_required_courses, 'enterprise_customer_name': enterprise_customer_name, 'enrollment_message': enrollment_message, 'redirect_message': redirect_message, 'account_activation_messages': account_activation_messages, 'activate_account_message': activate_account_message, 'course_enrollments': course_enrollments, 'course_entitlements': course_entitlements, 'course_entitlement_available_sessions': course_entitlement_available_sessions, 'unfulfilled_entitlement_pseudo_sessions': unfulfilled_entitlement_pseudo_sessions, 'course_optouts': course_optouts, 'staff_access': staff_access, 'errored_courses': errored_courses, 'show_courseware_links_for': show_courseware_links_for, 'all_course_modes': course_mode_info, 'cert_statuses': cert_statuses, 'credit_statuses': _credit_statuses(user, course_enrollments), 'show_email_settings_for': show_email_settings_for, 'reverifications': reverifications, 'verification_display': verification_status['should_display'], 'verification_status': verification_status['status'], 'verification_status_by_course': verify_status_by_course, 'verification_errors': verification_errors, 'block_courses': block_courses, 'denied_banner': denied_banner, 'billing_email': settings.PAYMENT_SUPPORT_EMAIL, 'user': user, 'logout_url': reverse('logout'), 'platform_name': platform_name, 'enrolled_courses_either_paid': enrolled_courses_either_paid, 'provider_states': [], 'order_history_list': order_history_list, 'courses_requirements_not_met': courses_requirements_not_met, 'nav_hidden': True, 'inverted_programs': inverted_programs, 'show_program_listing': ProgramsApiConfig.is_enabled(), 'show_journal_listing': journals_enabled(), # TODO: Dashboard Plugin required 'show_dashboard_tabs': True, 'disable_courseware_js': True, 'display_course_modes_on_dashboard': enable_verified_certificates and display_course_modes_on_dashboard, 'display_sidebar_on_dashboard': display_sidebar_on_dashboard, 'display_sidebar_account_activation_message': not (user.is_active or hide_dashboard_courses_until_activated), 'display_dashboard_courses': (user.is_active or not hide_dashboard_courses_until_activated), 'empty_dashboard_message': empty_dashboard_message, } if ecommerce_service.is_enabled(request.user): context.update({ 'use_ecommerce_payment_flow': True, 'ecommerce_payment_page': ecommerce_service.payment_page_url(), }) # Gather urls for course card resume buttons. resume_button_urls = ['' for entitlement in course_entitlements] for url in _get_urls_for_resume_buttons(user, course_enrollments): resume_button_urls.append(url) # There must be enough urls for dashboard.html. Template creates course # cards for "enrollments + entitlements". context.update({'resume_button_urls': resume_button_urls}) response = render_to_response('dashboard.html', context) _set_deprecated_user_info_cookie(response, request, user) # pylint: disable=protected-access return response
def add_cert(self, student, course_id, course=None, forced_grade=None, template_file=None, generate_pdf=True): """ Request a new certificate for a student. Arguments: student - User.object course_id - courseenrollment.course_id (CourseKey) forced_grade - a string indicating a grade parameter to pass with the certificate request. If this is given, grading will be skipped. generate_pdf - Boolean should a message be sent in queue to generate certificate PDF Will change the certificate status to 'generating' or `downloadable` in case of web view certificates. Certificate must be in the 'unavailable', 'error', 'deleted' or 'generating' state. If a student has a passing grade or is in the whitelist table for the course a request will be made for a new cert. If a student has allow_certificate set to False in the userprofile table the status will change to 'restricted' If a student does not have a passing grade the status will change to status.notpassing Returns the newly created certificate instance """ valid_statuses = [ status.generating, status.unavailable, status.deleted, status.error, status.notpassing, status.downloadable, status.auditing, status.audit_passing, status.audit_notpassing, ] cert_status = certificate_status_for_student(student, course_id)['status'] cert = None if cert_status not in valid_statuses: LOGGER.warning( (u"Cannot create certificate generation task for user %s " u"in the course '%s'; " u"the certificate status '%s' is not one of %s."), student.id, unicode(course_id), cert_status, unicode(valid_statuses)) return None # The caller can optionally pass a course in to avoid # re-fetching it from Mongo. If they have not provided one, # get it from the modulestore. if course is None: course = modulestore().get_course(course_id, depth=0) profile = UserProfile.objects.get(user=student) profile_name = profile.name # Needed for access control in grading. self.request.user = student self.request.session = {} is_whitelisted = self.whitelist.filter(user=student, course_id=course_id, whitelist=True).exists() grade = grades.grade(student, course) enrollment_mode, __ = CourseEnrollment.enrollment_mode_for_user( student, course_id) mode_is_verified = enrollment_mode in GeneratedCertificate.VERIFIED_CERTS_MODES user_is_verified = SoftwareSecurePhotoVerification.user_is_verified( student) cert_mode = enrollment_mode is_eligible_for_certificate = is_whitelisted or CourseMode.is_eligible_for_certificate( enrollment_mode) unverified = False # For credit mode generate verified certificate if cert_mode == CourseMode.CREDIT_MODE: cert_mode = CourseMode.VERIFIED if template_file is not None: template_pdf = template_file elif mode_is_verified and user_is_verified: template_pdf = "certificate-template-{id.org}-{id.course}-verified.pdf".format( id=course_id) elif mode_is_verified and not user_is_verified: template_pdf = "certificate-template-{id.org}-{id.course}.pdf".format( id=course_id) if CourseMode.mode_for_course(course_id, CourseMode.HONOR): cert_mode = GeneratedCertificate.MODES.honor else: unverified = True else: # honor code and audit students template_pdf = "certificate-template-{id.org}-{id.course}.pdf".format( id=course_id) if forced_grade: grade['grade'] = forced_grade LOGGER.info(( u"Certificate generated for student %s in the course: %s with template: %s. " u"given template: %s, " u"user is verified: %s, " u"mode is verified: %s"), student.username, unicode(course_id), template_pdf, template_file, user_is_verified, mode_is_verified) cert, created = GeneratedCertificate.objects.get_or_create( user=student, course_id=course_id) # pylint: disable=no-member cert.mode = cert_mode cert.user = student cert.grade = grade['percent'] cert.course_id = course_id cert.name = profile_name cert.download_url = '' # Strip HTML from grade range label grade_contents = grade.get('grade', None) try: grade_contents = lxml.html.fromstring( grade_contents).text_content() passing = True except (TypeError, XMLSyntaxError, ParserError) as exc: LOGGER.info((u"Could not retrieve grade for student %s " u"in the course '%s' " u"because an exception occurred while parsing the " u"grade contents '%s' as HTML. " u"The exception was: '%s'"), student.id, unicode(course_id), grade_contents, unicode(exc)) # Log if the student is whitelisted if is_whitelisted: LOGGER.info(u"Student %s is whitelisted in '%s'", student.id, unicode(course_id)) passing = True else: passing = False # If this user's enrollment is not eligible to receive a # certificate, mark it as such for reporting and # analytics. Only do this if the certificate is new, or # already marked as ineligible -- we don't want to mark # existing audit certs as ineligible. cutoff = settings.AUDIT_CERT_CUTOFF_DATE if (cutoff and cert.created_date >= cutoff ) and not is_eligible_for_certificate: cert.status = CertificateStatuses.audit_passing if passing else CertificateStatuses.audit_notpassing cert.save() LOGGER.info( u"Student %s with enrollment mode %s is not eligible for a certificate.", student.id, enrollment_mode) return cert # If they are not passing, short-circuit and don't generate cert elif not passing: cert.status = status.notpassing cert.save() LOGGER.info( (u"Student %s does not have a grade for '%s', " u"so their certificate status has been set to '%s'. " u"No certificate generation task was sent to the XQueue."), student.id, unicode(course_id), cert.status) return cert # Check to see whether the student is on the the embargoed # country restricted list. If so, they should not receive a # certificate -- set their status to restricted and log it. if self.restricted.filter(user=student).exists(): cert.status = status.restricted cert.save() LOGGER.info( (u"Student %s is in the embargoed country restricted " u"list, so their certificate status has been set to '%s' " u"for the course '%s'. " u"No certificate generation task was sent to the XQueue."), student.id, cert.status, unicode(course_id)) return cert if unverified: cert.status = status.unverified cert.save() LOGGER.info( (u"User %s has a verified enrollment in course %s " u"but is missing ID verification. " u"Certificate status has been set to unverified"), student.id, unicode(course_id), ) return cert # Finally, generate the certificate and send it off. return self._generate_cert(cert, course, student, grade_contents, template_pdf, generate_pdf)
def send_credit_notifications(username, course_key): """Sends email notification to user on different phases during credit course e.g., credit eligibility, credit payment etc. """ try: user = User.objects.get(username=username) except User.DoesNotExist: log.error('No user with %s exist', username) return course = modulestore().get_course(course_key, depth=0) course_display_name = course.display_name tracking_context = tracker.get_tracker().resolve_context() tracking_id = str(tracking_context.get('user_id')) client_id = str(tracking_context.get('client_id')) events = '&t=event&ec=email&ea=open' tracking_pixel = 'https://www.google-analytics.com/collect?v=1&tid' + tracking_id + '&cid' + client_id + events dashboard_link = _email_url_parser('dashboard') credit_course_link = _email_url_parser('courses', '?type=credit') # get attached branded logo logo_image = cache.get('credit.email.attached-logo') if logo_image is None: branded_logo = { 'title': 'Logo', 'path': settings.NOTIFICATION_EMAIL_EDX_LOGO, 'cid': str(uuid.uuid4()) } logo_image_id = branded_logo['cid'] logo_image = attach_image(branded_logo, 'Header Logo') if logo_image: cache.set('credit.email.attached-logo', logo_image, settings.CREDIT_NOTIFICATION_CACHE_TIMEOUT) else: # strip enclosing angle brackets from 'logo_image' cache 'Content-ID' logo_image_id = logo_image.get('Content-ID', '')[1:-1] providers_names = get_credit_provider_display_names(course_key) providers_string = make_providers_strings(providers_names) context = { 'full_name': user.get_full_name(), 'platform_name': configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME), 'course_name': course_display_name, 'branded_logo': logo_image_id, 'dashboard_link': dashboard_link, 'credit_course_link': credit_course_link, 'tracking_pixel': tracking_pixel, 'providers': providers_string, } # create the root email message notification_msg = MIMEMultipart('related') # add 'alternative' part to root email message to encapsulate the plain and # HTML versions, so message agents can decide which they want to display. msg_alternative = MIMEMultipart('alternative') notification_msg.attach(msg_alternative) # render the credit notification templates subject = _(u'Course Credit Eligibility') if providers_string: subject = _(u'You are eligible for credit from {providers_string}' ).format(providers_string=providers_string) # add alternative plain text message email_body_plain = render_to_string( 'credit_notifications/credit_eligibility_email.txt', context) msg_alternative.attach( SafeMIMEText(email_body_plain, _subtype='plain', _charset='utf-8')) # add alternative html message email_body_content = cache.get('credit.email.css-email-body') if email_body_content is None: html_file_path = file_path_finder( 'templates/credit_notifications/credit_eligibility_email.html') if html_file_path: with open(html_file_path, 'r') as cur_file: cur_text = cur_file.read() # use html parser to unescape html characters which are changed # by the 'pynliner' while adding inline css to html content html_parser = HTMLParser.HTMLParser() email_body_content = html_parser.unescape( with_inline_css(cur_text)) # cache the email body content before rendering it since the # email context will change for each user e.g., 'full_name' cache.set('credit.email.css-email-body', email_body_content, settings.CREDIT_NOTIFICATION_CACHE_TIMEOUT) else: email_body_content = '' email_body = Template(email_body_content).render([context]) msg_alternative.attach( SafeMIMEText(email_body, _subtype='html', _charset='utf-8')) # attach logo image if logo_image: notification_msg.attach(logo_image) # add email addresses of sender and receiver from_address = configuration_helpers.get_value('email_from_address', settings.DEFAULT_FROM_EMAIL) to_address = user.email # send the root email message msg = EmailMessage(subject, None, from_address, [to_address]) msg.attach(notification_msg) msg.send()
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(SlashSeparatedCourseKey('abc', 'def', 'ghi'), category='vertical')
def test_deprecated_blocks_list_updated_correctly(self, delete_vertical): """ Verify that deprecated blocks list shown on banner is updated correctly. Here is the scenario: This list of deprecated blocks shown on banner contains published and un-published blocks. That list should be updated when we delete un-published block(s). This behavior should be same if we delete unpublished vertical or problem. """ block_types = ['notes'] course_module = modulestore().get_item(self.course.location) vertical1 = ItemFactory.create( parent_location=self.sequential.location, category='vertical', display_name='Vert1 Subsection1') problem1 = ItemFactory.create(parent_location=vertical1.location, category='notes', display_name='notes problem in vert1', publish_item=False) info = _deprecated_blocks_info(course_module, block_types) # info['blocks'] should be empty here because there is nothing # published or un-published present self.assertEqual(info['blocks'], []) vertical2 = ItemFactory.create( parent_location=self.sequential.location, category='vertical', display_name='Vert2 Subsection1') ItemFactory.create(parent_location=vertical2.location, category='notes', display_name='notes problem in vert2', pubish_item=True) # At this point CourseStructure will contain both the above # published and un-published verticals info = _deprecated_blocks_info(course_module, block_types) self.assertItemsEqual( info['blocks'], [[ reverse_usage_url('container_handler', vertical1.location), 'notes problem in vert1' ], [ reverse_usage_url('container_handler', vertical2.location), 'notes problem in vert2' ]]) # Delete the un-published vertical or problem so that CourseStructure updates its data if delete_vertical: self.store.delete_item(vertical1.location, self.user.id) else: self.store.delete_item(problem1.location, self.user.id) info = _deprecated_blocks_info(course_module, block_types) # info['blocks'] should only contain the info about vertical2 which is published. # There shouldn't be any info present about un-published vertical1 self.assertEqual(info['blocks'], [[ reverse_usage_url('container_handler', vertical2.location), 'notes problem in vert2' ]])
def __iter__(self): def parent_or_requested_block_type(usage_key): """ Returns whether the usage_key's block_type is one of self.block_types or a parent type. """ return (usage_key.block_type in self.block_types or usage_key.block_type in BLOCK_TYPES_WITH_CHILDREN) def create_module(descriptor): """ Factory method for creating and binding a module for the given descriptor. """ field_data_cache = FieldDataCache.cache_for_descriptor_descendents( self.course_id, self.request.user, descriptor, depth=0, ) course = get_course_by_id(self.course_id) return get_module_for_descriptor(self.request.user, self.request, descriptor, field_data_cache, self.course_id, course=course) with modulestore().bulk_operations(self.course_id): child_to_parent = {} stack = [self.start_block] while stack: curr_block = stack.pop() if curr_block.hide_from_toc: # For now, if the 'hide_from_toc' setting is set on the block, do not traverse down # the hierarchy. The reason being is that these blocks may not have human-readable names # to display on the mobile clients. # Eventually, we'll need to figure out how we want these blocks to be displayed on the # mobile clients. As they are still accessible in the browser, just not navigatable # from the table-of-contents. continue if curr_block.location.block_type in self.block_types: if not has_access(self.request.user, 'load', curr_block, course_key=self.course_id): continue summary_fn = self.block_types[curr_block.category] block_path = list( path(curr_block, child_to_parent, self.start_block)) unit_url, section_url = find_urls(self.course_id, curr_block, child_to_parent, self.request) yield { "path": block_path, "named_path": [b["name"] for b in block_path], "unit_url": unit_url, "section_url": section_url, "summary": summary_fn(self.course_id, curr_block, self.request, self.local_cache, self.api_version) } if curr_block.has_children: children = get_dynamic_descriptor_children( curr_block, self.request.user.id, create_module, usage_key_filter=parent_or_requested_block_type) for block in reversed(children): stack.append(block) child_to_parent[block] = curr_block
def get_user_partition_info(xblock, schemes=None, course=None): """ Retrieve user partition information for an XBlock for display in editors. * If a partition has been disabled, it will be excluded from the results. * If a group within a partition is referenced by the XBlock, but the group has been deleted, the group will be marked as deleted in the results. Arguments: xblock (XBlock): The courseware component being edited. Keyword Arguments: schemes (iterable of str): If provided, filter partitions to include only schemes with the provided names. course (XBlock): The course descriptor. If provided, uses this to look up the user partitions instead of loading the course. This is useful if we're calling this function multiple times for the same course want to minimize queries to the modulestore. Returns: list Example Usage: >>> get_user_partition_info(block, schemes=["cohort", "verification"]) [ { "id": 12345, "name": "Cohorts" "scheme": "cohort", "groups": [ { "id": 7890, "name": "Foo", "selected": True, "deleted": False, } ] }, { "id": 7292, "name": "Midterm A", "scheme": "verification", "groups": [ { "id": 1, "name": "Completed verification at Midterm A", "selected": False, "deleted": False }, { "id": 0, "name": "Did not complete verification at Midterm A", "selected": False, "deleted": False, } ] } ] """ course = course or modulestore().get_course(xblock.location.course_key) if course is None: log.warning( "Could not find course %s to retrieve user partition information", xblock.location.course_key) return [] if schemes is not None: schemes = set(schemes) partitions = [] for p in sorted(get_all_partitions_for_course(course, active_only=True), key=lambda p: p.name): # Exclude disabled partitions, partitions with no groups defined # The exception to this case is when there is a selected group within that partition, which means there is # a deleted group # Also filter by scheme name if there's a filter defined. selected_groups = set(xblock.group_access.get(p.id, []) or []) if (p.groups or selected_groups) and (schemes is None or p.scheme.name in schemes): # First, add groups defined by the partition groups = [] for g in p.groups: # Falsey group access for a partition mean that all groups # are selected. In the UI, though, we don't show the particular # groups selected, since there's a separate option for "all users". groups.append({ "id": g.id, "name": g.name, "selected": g.id in selected_groups, "deleted": False, }) # Next, add any groups set on the XBlock that have been deleted all_groups = {g.id for g in p.groups} missing_group_ids = selected_groups - all_groups for gid in missing_group_ids: groups.append({ "id": gid, "name": _("Deleted Group"), "selected": True, "deleted": True, }) # Put together the entire partition dictionary partitions.append({ "id": p.id, "name": str(p.name ), # Convert into a string in case ugettext_lazy was used "scheme": p.scheme.name, "groups": groups, }) return partitions
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'})
def new_descriptor_runtime(self): runtime = get_test_descriptor_system() runtime.get_block = modulestore().get_item return runtime
def test_save(self): """Test course saving.""" course = CourseFactory.create() tabs.primitive_insert(course, 3, 'notes', 'aname') course2 = modulestore().get_course(course.id) self.assertEquals(course2.tabs[3], {'type': 'notes', 'name': 'aname'})
def test_fail_block_nonvisible(self): self.setup_course() self.setup_user(admin=False, enroll=True, login=True) self.block_to_be_tested.visible_to_staff_only = True modulestore().update_item(self.block_to_be_tested, self.user.id) self.verify_response(expected_response_code=404)
def register_special_exams(course_key): """ This is typically called on a course published signal. The course is examined for sequences that are marked as timed exams. Then these are registered with the edx-proctoring subsystem. Likewise, if formerly registered exams are unmarked, then those registered exams are marked as inactive """ if not settings.FEATURES.get('ENABLE_SPECIAL_EXAMS'): # if feature is not enabled then do a quick exit return course = modulestore().get_course(course_key) if course is None: raise ItemNotFoundError(u"Course {} does not exist", six.text_type(course_key)) if not course.enable_proctored_exams and not course.enable_timed_exams: # likewise if course does not have these features turned on # then quickly exit return # get all sequences, since they can be marked as timed/proctored exams _timed_exams = modulestore().get_items(course_key, qualifiers={ 'category': 'sequential', }, settings={ 'is_time_limited': True, }) # filter out any potential dangling sequences timed_exams = [ timed_exam for timed_exam in _timed_exams if is_item_in_course_tree(timed_exam) ] # enumerate over list of sequences which are time-limited and # add/update any exam entries in edx-proctoring for timed_exam in timed_exams: msg = ( u'Found {location} as a timed-exam in course structure. Inspecting...' .format(location=six.text_type(timed_exam.location))) log.info(msg) exam_metadata = { 'exam_name': timed_exam.display_name, 'time_limit_mins': timed_exam.default_time_limit_minutes, 'due_date': timed_exam.due if not course.self_paced else None, 'is_proctored': timed_exam.is_proctored_exam, # backends that support onboarding exams will treat onboarding exams as practice 'is_practice_exam': timed_exam.is_practice_exam or timed_exam.is_onboarding_exam, 'is_active': True, 'hide_after_due': timed_exam.hide_after_due, 'backend': course.proctoring_provider, } try: exam = get_exam_by_content_id(six.text_type(course_key), six.text_type(timed_exam.location)) # update case, make sure everything is synced exam_metadata['exam_id'] = exam['id'] exam_id = update_exam(**exam_metadata) msg = u'Updated timed exam {exam_id}'.format(exam_id=exam['id']) log.info(msg) except ProctoredExamNotFoundException: exam_metadata['course_id'] = six.text_type(course_key) exam_metadata['content_id'] = six.text_type(timed_exam.location) exam_id = create_exam(**exam_metadata) msg = u'Created new timed exam {exam_id}'.format(exam_id=exam_id) log.info(msg) exam_review_policy_metadata = { 'exam_id': exam_id, 'set_by_user_id': timed_exam.edited_by, 'review_policy': timed_exam.exam_review_rules, } # only create/update exam policy for the proctored exams if timed_exam.is_proctored_exam and not timed_exam.is_practice_exam and not timed_exam.is_onboarding_exam: try: update_review_policy(**exam_review_policy_metadata) except ProctoredExamReviewPolicyNotFoundException: if timed_exam.exam_review_rules: # won't save an empty rule. create_exam_review_policy(**exam_review_policy_metadata) msg = u'Created new exam review policy with exam_id {exam_id}'.format( exam_id=exam_id) log.info(msg) else: try: # remove any associated review policy remove_review_policy(exam_id=exam_id) except ProctoredExamReviewPolicyNotFoundException: pass # then see which exams we have in edx-proctoring that are not in # our current list. That means the the user has disabled it exams = get_all_exams_for_course(course_key) for exam in exams: if exam['is_active']: # try to look up the content_id in the sequences location search = [ timed_exam for timed_exam in timed_exams if six.text_type(timed_exam.location) == exam['content_id'] ] if not search: # This means it was turned off in Studio, we need to mark # the exam as inactive (we don't delete!) msg = u'Disabling timed exam {exam_id}'.format( exam_id=exam['id']) log.info(msg) update_exam( exam_id=exam['id'], is_proctored=False, is_active=False, )
def _invoke_xblock_handler(request, course_id, usage_id, handler, suffix, user): """ Invoke an XBlock handler, either authenticated or not. """ location = unquote_slashes(usage_id) # Check parameters and fail fast if there's a problem if not Location.is_valid(location): raise Http404("Invalid location") # Check submitted files files = request.FILES or {} error_msg = _check_files_limits(files) if error_msg: return HttpResponse(json.dumps({'success': error_msg})) try: descriptor = modulestore().get_instance(course_id, location) except ItemNotFoundError: log.warn( "Invalid location for course id {course_id}: {location}".format( course_id=course_id, location=location)) raise Http404 field_data_cache = FieldDataCache.cache_for_descriptor_descendents( course_id, user, descriptor) instance = get_module(user, request, location, field_data_cache, course_id, grade_bucket_type='ajax') if instance is None: # Either permissions just changed, or someone is trying to be clever # and load something they shouldn't have access to. log.debug("No module %s for user %s -- access denied?", location, user) raise Http404 req = django_to_webob_request(request) try: resp = instance.handle(handler, req, suffix) except NoSuchHandlerError: log.exception("XBlock %s attempted to access missing handler %r", instance, handler) raise Http404 # If we can't find the module, respond with a 404 except NotFoundError: log.exception("Module indicating to user that request doesn't exist") raise Http404 # For XModule-specific errors, we log the error and respond with an error message except ProcessingError as err: log.warning("Module encountered an error while processing AJAX call", exc_info=True) return JsonResponse(object={'success': err.args[0]}, status=200) # If any other error occurred, re-raise it to trigger a 500 response except Exception: log.exception("error executing xblock handler") raise return webob_to_django_response(resp)
def validate_forus_params_values(params): errors = defaultdict(lambda: []) def mark_as_invalid(field, field_label): # Translators: This is for the ForUs API errors[field].append( _('Invalid {field_label} has been provided').format( field_label=field_label, )) try: validate_email(params.get('email')) try: user = User.objects.get(email=params.get('email')) if user.is_staff or user.is_superuser: errors['email'].append( _("ForUs profile cannot be created for admins and staff.")) except User.DoesNotExist: pass except ValidationError: # Translators: This is for the ForUs API errors['email'].append(_("The provided email format is invalid")) if params.get('gender') not in dict(UserProfile.GENDER_CHOICES): # Translators: This is for the ForUs API mark_as_invalid('gender', _('gender')) if not is_enabled_language(params.get('lang')): # Translators: This is for the ForUs API mark_as_invalid('lang', _('language')) if params.get('country') not in dict(countries): # Translators: This is for the ForUs API mark_as_invalid('lang', _('country')) if params.get('level_of_education') not in dict( UserProfile.LEVEL_OF_EDUCATION_CHOICES): # Translators: This is for the ForUs API mark_as_invalid('lang', _('level of education')) try: course_key = SlashSeparatedCourseKey.from_deprecated_string( params.get('course_id')) course = modulestore().get_course(course_key) if not course: raise ItemNotFoundError() if not course.is_self_paced(): if not course.enrollment_has_started(): # Translators: This is for the ForUs API errors['course_id'].append( _('The course has not yet been opened for enrollment')) if course.enrollment_has_ended(): # Translators: This is for the ForUs API errors['course_id'].append( _('Enrollment for this course has been closed')) except InvalidKeyError: log.warning( u"User {username} tried to {action} with invalid course id: {course_id}" .format( username=params.get('username'), action=params.get('enrollment_action'), course_id=params.get('course_id'), )) mark_as_invalid('course_id', _('course id')) except ItemNotFoundError: # Translators: This is for the ForUs API errors['course_id'].append(_('The requested course does not exist')) try: if int(params['year_of_birth']) not in UserProfile.VALID_YEARS: # Translators: This is for the ForUs API mark_as_invalid('year_of_birth', _('birth year')) except ValueError: # Translators: This is for the ForUs API mark_as_invalid('year_of_birth', _('birth year')) try: time = datetime.strptime(params.get('time'), DATE_TIME_FORMAT) now = datetime.utcnow() if time > now: # Translators: This is for the ForUs API errors['time'].append(_('future date has been provided')) if time < (now - timedelta(days=1)): # Translators: This is for the ForUs API errors['time'].append(_('Request has expired')) except ValueError: # Translators: This is for the ForUs API mark_as_invalid('time', _('date format')) if len(errors): raise ValidationError(errors)
def load_from_module_store(cls, course_id): """ Load a CourseDescriptor, create or update a CourseOverview from it, cache the overview, and return it. Arguments: course_id (CourseKey): the ID of the course overview to be loaded. Returns: CourseOverview: overview of the requested course. Raises: - CourseOverview.DoesNotExist if the course specified by course_id was not found. - IOError if some other error occurs while trying to load the course from the module store. """ log.info( "Attempting to load CourseOverview for course %s from modulestore.", course_id, ) store = modulestore() with store.bulk_operations(course_id): course = store.get_course(course_id) if isinstance(course, CourseDescriptor): try: course_overview = cls._create_or_update(course) with transaction.atomic(): course_overview.save() # Remove and recreate all the course tabs CourseOverviewTab.objects.filter( course_overview=course_overview).delete() CourseOverviewTab.objects.bulk_create([ CourseOverviewTab( tab_id=tab.tab_id, type=tab.type, name=tab.name, course_staff_only=tab.course_staff_only, url_slug=tab.get('url_slug'), link=tab.get('link'), is_hidden=tab.get('is_hidden', False), course_overview=course_overview) for tab in course.tabs ]) # Remove and recreate course images CourseOverviewImageSet.objects.filter( course_overview=course_overview).delete() CourseOverviewImageSet.create(course_overview, course) except IntegrityError: # There is a rare race condition that will occur if # CourseOverview.get_from_id is called while a # another identical overview is already in the process # of being created. # One of the overviews will be saved normally, while the # other one will cause an IntegrityError because it tries # to save a duplicate. # (see: https://openedx.atlassian.net/browse/TNL-2854). log.info( "Multiple CourseOverviews for course %s requested " "simultaneously; will only save one.", course_id, ) except Exception: log.exception( "Saving CourseOverview for course %s failed with " "unexpected exception!", course_id, ) raise return course_overview elif course is not None: raise IOError( "Error while loading CourseOverview for course {} " "from the module store: {}", six.text_type(course_id), course.error_msg if isinstance( course, ErrorDescriptor) else six.text_type(course)) else: log.info( "Could not create CourseOverview for non-existent course: %s", course_id, ) raise cls.DoesNotExist()
def test_fail_block_unreleased(self): self.setup_course() self.setup_user(admin=False, enroll=True, login=True) self.block_to_be_tested.start = datetime.max modulestore().update_item(self.block_to_be_tested, self.user.id) self.verify_response(expected_response_code=404)
def post(self, request, course_id): """Takes the form submission from the page and parses it. Args: request (`Request`): The Django Request object. course_id (unicode): The slash-separated course key. Returns: Status code 400 when the requested mode is unsupported. When the honor mode is selected, redirects to the dashboard. When the verified mode is selected, returns error messages if the indicated contribution amount is invalid or below the minimum, otherwise redirects to the verification flow. """ course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) user = request.user # This is a bit redundant with logic in student.views.change_enrollment, # but I don't really have the time to refactor it more nicely and test. course = modulestore().get_course(course_key) if not has_access(user, 'enroll', course): error_msg = _("Enrollment is closed") return self.get(request, course_id, error=error_msg) requested_mode = self._get_requested_mode(request.POST) allowed_modes = CourseMode.modes_for_course_dict(course_key) if requested_mode not in allowed_modes: return HttpResponseBadRequest(_("Enrollment mode not supported")) if requested_mode == 'honor': # The user will have already been enrolled in the honor mode at this # point, so we just redirect them to the dashboard, thereby avoiding # hitting the database a second time attempting to enroll them. return redirect(reverse('dashboard')) mode_info = allowed_modes[requested_mode] if requested_mode == 'verified': amount = request.POST.get("contribution") or \ request.POST.get("contribution-other-amt") or 0 try: # Validate the amount passed in and force it into two digits amount_value = decimal.Decimal(amount).quantize( decimal.Decimal('.01'), rounding=decimal.ROUND_DOWN) except decimal.InvalidOperation: error_msg = _("Invalid amount selected.") return self.get(request, course_id, error=error_msg) # Check for minimum pricing if amount_value < mode_info.min_price: error_msg = _( "No selected price or selected price is too low.") return self.get(request, course_id, error=error_msg) donation_for_course = request.session.get("donation_for_course", {}) donation_for_course[unicode(course_key)] = amount_value request.session["donation_for_course"] = donation_for_course return redirect( reverse('verify_student_start_flow', kwargs={'course_id': unicode(course_key)}))
def test_studio_user_permissions(self): """ Test that user could attach to the problem only libraries that he has access (or which were created by him). This test was created on the basis of bug described in the pull requests on github: https://github.com/edx/edx-platform/pull/11331 https://github.com/edx/edx-platform/pull/11611 """ self._create_library(org='admin_org_1', library='lib_adm_1', display_name='admin_lib_1') self._create_library(org='admin_org_2', library='lib_adm_2', display_name='admin_lib_2') self._login_as_non_staff_user() self._create_library(org='staff_org_1', library='lib_staff_1', display_name='staff_lib_1') self._create_library(org='staff_org_2', library='lib_staff_2', display_name='staff_lib_2') with modulestore().default_store(ModuleStoreEnum.Type.split): course = CourseFactory.create() instructor_role = CourseInstructorRole(course.id) auth.add_users(self.user, instructor_role, self.non_staff_user) lib_block = ItemFactory.create(category='library_content', parent_location=course.location, user_id=self.non_staff_user.id, publish_item=False) def _get_settings_html(): """ Helper function to get block settings HTML Used to check the available libraries. """ edit_view_url = reverse_usage_url("xblock_view_handler", lib_block.location, {"view_name": STUDIO_VIEW}) resp = self.client.get_json(edit_view_url) self.assertEquals(resp.status_code, 200) return parse_json(resp)['html'] self._login_as_staff_user() staff_settings_html = _get_settings_html() self.assertIn('staff_lib_1', staff_settings_html) self.assertIn('staff_lib_2', staff_settings_html) self.assertIn('admin_lib_1', staff_settings_html) self.assertIn('admin_lib_2', staff_settings_html) self._login_as_non_staff_user() response = self.client.get_json(LIBRARY_REST_URL) staff_libs = parse_json(response) self.assertEquals(2, len(staff_libs)) non_staff_settings_html = _get_settings_html() self.assertIn('staff_lib_1', non_staff_settings_html) self.assertIn('staff_lib_2', non_staff_settings_html) self.assertNotIn('admin_lib_1', non_staff_settings_html) self.assertNotIn('admin_lib_2', non_staff_settings_html)
def _section_course_info(course, access): """ Provide data for the corresponding dashboard section """ course_key = course.id section_data = { 'section_key': 'course_info', 'section_display_name': _('Course Info'), 'access': access, 'course_id': course_key, 'course_display_name': course.display_name, 'has_started': course.has_started(), 'has_ended': course.has_ended(), 'start_date': get_default_time_display(course.start), 'end_date': get_default_time_display(course.end) or _('No end date set'), 'num_sections': len(course.children), 'list_instructor_tasks_url': reverse('list_instructor_tasks', kwargs={'course_id': unicode(course_key)}), } if settings.FEATURES.get('DISPLAY_ANALYTICS_ENROLLMENTS'): section_data[ 'enrollment_count'] = CourseEnrollment.objects.enrollment_counts( course_key) if settings.ANALYTICS_DASHBOARD_URL: dashboard_link = _get_dashboard_link(course_key) message = _("Enrollment data is now available in {dashboard_link}." ).format(dashboard_link=dashboard_link) section_data['enrollment_message'] = message if settings.FEATURES.get('ENABLE_SYSADMIN_DASHBOARD'): section_data['detailed_gitlogs_url'] = reverse( 'gitlogs_detail', kwargs={'course_id': unicode(course_key)}) try: sorted_cutoffs = sorted(course.grade_cutoffs.items(), key=lambda i: i[1], reverse=True) advance = lambda memo, (letter, score): "{}: {}, ".format( letter, score) + memo section_data['grade_cutoffs'] = reduce(advance, sorted_cutoffs, "")[:-2] except Exception: # pylint: disable=broad-except section_data['grade_cutoffs'] = "Not Available" # section_data['offline_grades'] = offline_grades_available(course_key) try: section_data['course_errors'] = [ (escape(a), '') for (a, _unused) in modulestore().get_course_errors(course.id) ] except Exception: # pylint: disable=broad-except section_data['course_errors'] = [('Error fetching errors', '')] return section_data
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) # We assume that, if 'professional' is one of the modes, it is the *only* mode. # If we offer more modes alongside 'professional' in the future, this will need to route # to the usual "choose your track" page same is true for no-id-professional mode. has_enrolled_professional = ( CourseMode.is_professional_slug(enrollment_mode) and is_active) if CourseMode.has_professional_mode( modes) and not has_enrolled_professional: return redirect( reverse('verify_student_start_flow', kwargs={'course_id': unicode(course_key)})) # 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) context = { "course_modes_choose_url": reverse("course_modes_choose", kwargs={'course_id': course_key.to_deprecated_string()}), "modes": modes, "course_name": course.display_name_with_default, "course_org": course.display_org_with_default, "course_num": course.display_number_with_default, "chosen_price": chosen_price, "error": error, "can_audit": "audit" in modes, "responsive": True } if "verified" in modes: context["suggested_prices"] = [ decimal.Decimal(x.strip()) for x in modes["verified"].suggested_prices.split(",") if x.strip() ] context["currency"] = modes["verified"].currency.upper() context["min_price"] = modes["verified"].min_price context["verified_name"] = modes["verified"].name context["verified_description"] = modes["verified"].description return render_to_response("course_modes/choose.html", context)
def render_to_fragment(self, request, course_id, user_is_enrolled=True, **kwargs): # pylint: disable=arguments-differ """ Renders the course outline as a fragment. """ from lms.urls import RESET_COURSE_DEADLINES_NAME from openedx.features.course_experience.urls import COURSE_HOME_VIEW_NAME course_key = CourseKey.from_string(course_id) course_overview = get_course_overview_with_access( request.user, 'load', course_key, check_if_enrolled=user_is_enrolled) course = modulestore().get_course(course_key) course_block_tree = get_course_outline_block_tree( request, course_id, request.user if user_is_enrolled else None) if not course_block_tree: return None resume_block = get_resume_block( course_block_tree) if user_is_enrolled else None if not resume_block: self.mark_first_unit_to_resume(course_block_tree) xblock_display_names = self.create_xblock_id_and_name_dict( course_block_tree) gated_content = self.get_content_milestones(request, course_key) missed_deadlines, missed_gated_content = dates_banner_should_display( course_key, request.user) reset_deadlines_url = reverse(RESET_COURSE_DEADLINES_NAME) context = { 'csrf': csrf(request)['csrf_token'], 'course': course_overview, 'due_date_display_format': course.due_date_display_format, 'blocks': course_block_tree, 'enable_links': user_is_enrolled or course.course_visibility == COURSE_VISIBILITY_PUBLIC, 'course_key': course_key, 'gated_content': gated_content, 'xblock_display_names': xblock_display_names, 'self_paced': course.self_paced, # We're using this flag to prevent old self-paced dates from leaking out on courses not # managed by edx-when. 'in_edx_when': edx_when_api.is_enabled_for_course(course_key), 'reset_deadlines_url': reset_deadlines_url, 'verified_upgrade_link': verified_upgrade_deadline_link(request.user, course=course), 'on_course_outline_page': True, 'missed_deadlines': missed_deadlines, 'missed_gated_content': missed_gated_content, 'has_ended': course.has_ended(), } html = render_to_string( 'course_experience/course-outline-fragment.html', context) return Fragment(html)
def get(self, request, course_id, chapter=None, section=None, position=None): """ Displays courseware accordion and associated content. If course, chapter, and section are all specified, renders the page, or returns an error if they are invalid. If section is not specified, displays the accordion opened to the right chapter. If neither chapter or section are specified, displays the user's most recent chapter, or the first chapter if this is the user's first visit. Arguments: request: HTTP request course_id (unicode): course id chapter (unicode): chapter url_name section (unicode): section url_name position (unicode): position in module, eg of <sequential> module """ self.course_key = CourseKey.from_string(course_id) if not (request.user.is_authenticated or self.enable_unenrolled_access): return redirect_to_login(request.get_full_path()) self.original_chapter_url_name = chapter self.original_section_url_name = section self.chapter_url_name = chapter self.section_url_name = section self.position = position self.chapter, self.section = None, None self.course = None self.url = request.path try: set_custom_metrics_for_course_key(self.course_key) self._clean_position() with modulestore().bulk_operations(self.course_key): self.view = STUDENT_VIEW # Do the enrollment check if enable_unenrolled_access is not enabled. self.course = get_course_with_access( request.user, 'load', self.course_key, depth=CONTENT_DEPTH, check_if_enrolled=not self.enable_unenrolled_access, ) self.course_overview = CourseOverview.get_from_id( self.course.id) if self.enable_unenrolled_access: # Check if the user is considered enrolled (i.e. is an enrolled learner or staff). try: check_course_access( self.course, request.user, 'load', check_if_enrolled=True, ) except CourseAccessRedirect as exception: # If the user is not considered enrolled: if self.course.course_visibility == COURSE_VISIBILITY_PUBLIC: # If course visibility is public show the XBlock public_view. self.view = PUBLIC_VIEW else: # Otherwise deny them access. raise exception else: # If the user is considered enrolled show the default XBlock student_view. pass self.can_masquerade = request.user.has_perm( MASQUERADE_AS_STUDENT, self.course) self.is_staff = has_access(request.user, 'staff', self.course) self._setup_masquerade_for_effective_user() return self.render(request) except Exception as exception: # pylint: disable=broad-except return CourseTabView.handle_exceptions(request, self.course_key, self.course, exception)
def handle(self, *args, **options): """ Given a content library archive path, import the corresponding course to mongo. """ archive_path = options['archive_path'] username = options['owner_username'] data_root = Path(settings.GITHUB_REPO_ROOT) subdir = base64.urlsafe_b64encode(os.path.basename(archive_path)) course_dir = data_root / subdir # Extract library archive tar_file = tarfile.open(archive_path) try: safetar_extractall(tar_file, course_dir.encode('utf-8')) except SuspiciousOperation as exc: raise CommandError( u'\n=== Course import {0}: Unsafe tar file - {1}\n'.format( archive_path, exc.args[0])) finally: tar_file.close() # Paths to the library.xml file abs_xml_path = os.path.join(course_dir, 'library') rel_xml_path = os.path.relpath(abs_xml_path, data_root) # Gather library metadata from XML file xml_root = etree.parse(abs_xml_path / 'library.xml').getroot() if xml_root.tag != 'library': raise CommandError( u'Failed to import {0}: Not a library archive'.format( archive_path)) metadata = xml_root.attrib org = metadata['org'] library = metadata['library'] display_name = metadata['display_name'] # Fetch user and library key user = User.objects.get(username=username) courselike_key, created = _get_or_create_library( org, library, display_name, user) # Check if data would be overwritten ans = '' while not created and ans not in ['y', 'yes', 'n', 'no']: inp = input( u'Library "{0}" already exists, overwrite it? [y/n] '.format( courselike_key)) ans = inp.lower() if ans.startswith('n'): print(u'Aborting import of "{0}"'.format(courselike_key)) return # At last, import the library try: import_library_from_xml(modulestore(), user.id, settings.GITHUB_REPO_ROOT, [rel_xml_path], load_error_modules=False, static_content_store=contentstore(), target_id=courselike_key) except Exception: print(u'\n=== Failed to import library-v1:{0}+{1}'.format( org, library)) raise print(u'Library "{0}" imported to "{1}"'.format( archive_path, courselike_key))
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(SlashSeparatedCourseKey('a', 'b', 'c'), qualifiers={'category': 'vertical'})