def get_course_info(course): course_info = {} course_info['course_id'] = course.course_id course_info['name'] = course.name course_info['thumbnail'] = course.thumbnail course_info['course_num'] = course.course_num course_info['modified'] = naturalFormatDate(course.modified) course_info['enrollment'] = course_stat.stat.enrollment_total_count( SlashSeparatedCourseKey.from_deprecated_string(course.course_id)) course_info['comment'] = course_stat.stat.comment_total_count( SlashSeparatedCourseKey.from_deprecated_string(course.course_id)) course_info['subtitle'] = course.subtitle course_info['about'] = reverse('about_course', args=[course.course_id]) course_info['start_time'] = time_format(course.start) course_info['end_time'] = time_format(course.end) try: staff = course.staff.all()[0] course_info['staff_avatar'] = staff.avartar course_info['staff_name'] = staff.name course_info['staff_title'] = "%s %s %s" % ( staff.company, staff.department, staff.position) except: course_info['staff_avatar'] = '' course_info['staff_name'] = '' course_info['staff_title'] = '' return course_info
class TestInstructorEnrollmentStudentModule(TestCase): """ Test student module manipulations. """ def setUp(self): super(TestInstructorEnrollmentStudentModule, self).setUp() self.course_key = SlashSeparatedCourseKey("fake", "course", "id") def test_reset_student_attempts(self): user = UserFactory() msk = self.course_key.make_usage_key("dummy", "module") original_state = json.dumps({"attempts": 32, "otherstuff": "alsorobots"}) StudentModule.objects.create( student=user, course_id=self.course_key, module_state_key=msk, state=original_state ) # lambda to reload the module state from the database module = lambda: StudentModule.objects.get(student=user, course_id=self.course_key, module_state_key=msk) self.assertEqual(json.loads(module().state)["attempts"], 32) reset_student_attempts(self.course_key, user, msk) self.assertEqual(json.loads(module().state)["attempts"], 0) def test_delete_student_attempts(self): user = UserFactory() msk = self.course_key.make_usage_key("dummy", "module") original_state = json.dumps({"attempts": 32, "otherstuff": "alsorobots"}) StudentModule.objects.create( student=user, course_id=self.course_key, module_state_key=msk, state=original_state ) self.assertEqual( StudentModule.objects.filter(student=user, course_id=self.course_key, module_state_key=msk).count(), 1 ) reset_student_attempts(self.course_key, user, msk, delete_module=True) self.assertEqual( StudentModule.objects.filter(student=user, course_id=self.course_key, module_state_key=msk).count(), 0 ) def test_delete_submission_scores(self): user = UserFactory() problem_location = self.course_key.make_usage_key("dummy", "module") # Create a student module for the user StudentModule.objects.create( student=user, course_id=self.course_key, module_state_key=problem_location, state=json.dumps({}) ) # Create a submission and score for the student using the submissions API student_item = { "student_id": anonymous_id_for_user(user, self.course_key), "course_id": self.course_key.to_deprecated_string(), "item_id": problem_location.to_deprecated_string(), "item_type": "openassessment", } submission = sub_api.create_submission(student_item, "test answer") sub_api.set_score(submission["uuid"], 1, 2) # Delete student state using the instructor dash reset_student_attempts(self.course_key, user, problem_location, delete_module=True) # Verify that the student's scores have been reset in the submissions API score = sub_api.get_score(student_item) self.assertIs(score, None)
def is_enrolled_by_partial(cls, user, course_id_partial): """ Returns `True` if the user is enrolled in a course that starts with `course_id_partial`. Otherwise, returns False. Can be used to determine whether a student is enrolled in a course whose run name is unknown. `user` is a Django User object. If it hasn't been saved yet (no `.id` attribute), this method will automatically save it before adding an enrollment for it. `course_id_partial` (CourseKey) is missing the run component """ assert isinstance(course_id_partial, SlashSeparatedCourseKey) assert not course_id_partial.run # None or empty string course_key = SlashSeparatedCourseKey(course_id_partial.org, course_id_partial.course, '') querystring = unicode(course_key.to_deprecated_string()) try: return CourseEnrollment.objects.filter( user=user, course_id__startswith=querystring, is_active=1 ).exists() except cls.DoesNotExist: return False
def _decorated(request, course_id, *args, **kwargs): try: SlashSeparatedCourseKey.from_deprecated_string(course_id) except InvalidKeyError: raise Http404 response = view_func(request, course_id, *args, **kwargs) return response
def test_delete_course_map(self): """ Test that course location is properly remove from loc_mapper and cache when course is deleted """ org = u"foo_org" course = u"bar_course" run = u"baz_run" course_location = SlashSeparatedCourseKey(org, course, run) loc_mapper().create_map_entry(course_location) # pylint: disable=protected-access entry = loc_mapper().location_map.find_one({"_id": loc_mapper()._construct_course_son(course_location)}) self.assertIsNotNone(entry, "Entry not found in loc_mapper") self.assertEqual(entry["offering"], u"{1}.{2}".format(org, course, run)) # now delete course location from loc_mapper and cache and test that course location no longer # exists in loca_mapper and cache loc_mapper().delete_course_mapping(course_location) # pylint: disable=protected-access entry = loc_mapper().location_map.find_one({"_id": loc_mapper()._construct_course_son(course_location)}) self.assertIsNone(entry, "Entry found in loc_mapper") # pylint: disable=protected-access cached_value = loc_mapper()._get_location_from_cache(course_location.make_usage_key("course", run)) self.assertIsNone(cached_value, "course_locator found in cache") # pylint: disable=protected-access cached_value = loc_mapper()._get_course_location_from_cache(course_location) self.assertIsNone(cached_value, "Entry found in cache")
def test_round_trip(self, source_builder, dest_builder, source_content_builder, dest_content_builder, course_data_name): source_course_key = SlashSeparatedCourseKey('source', 'course', 'key') dest_course_key = SlashSeparatedCourseKey('dest', 'course', 'key') # Construct the contentstore for storing the first import with source_content_builder.build() as source_content: # Construct the modulestore for storing the first import (using the previously created contentstore) with source_builder.build(source_content) as source_store: # Construct the contentstore for storing the second import with dest_content_builder.build() as dest_content: # Construct the modulestore for storing the second import (using the second contentstore) with dest_builder.build(dest_content) as dest_store: import_from_xml( source_store, 'test_user', 'common/test/data', course_dirs=[course_data_name], static_content_store=source_content, target_course_id=source_course_key, create_new_course_if_not_present=True, ) export_to_xml( source_store, source_content, source_course_key, self.export_dir, 'exported_course', ) import_from_xml( dest_store, 'test_user', self.export_dir, static_content_store=dest_content, target_course_id=dest_course_key, create_new_course_if_not_present=True, ) self.exclude_field(source_course_key.make_usage_key('course', 'key'), 'wiki_slug') self.exclude_field(None, 'xml_attributes') self.ignore_asset_key('_id') self.ignore_asset_key('uploadDate') self.ignore_asset_key('content_son') self.ignore_asset_key('thumbnail_location') self.assertCoursesEqual( source_store, source_course_key, dest_store, dest_course_key, ) self.assertAssetsEqual( source_content, source_course_key, dest_content, dest_course_key, )
def setUp(self): course_key = SlashSeparatedCourseKey('edX', 'toy', '2012_Fall') self.course = course_key.make_usage_key('course', course_key.run) self.anonymous_user = AnonymousUserFactory() self.student = UserFactory() self.global_staff = UserFactory(is_staff=True) self.course_staff = StaffFactory(course_key=self.course.course_key) self.course_instructor = InstructorFactory(course_key=self.course.course_key)
def setUp(self): system = get_test_descriptor_system() course_key = SlashSeparatedCourseKey("org", "course", "run") usage_key = course_key.make_usage_key("video", "name") self.descriptor = system.construct_xblock_from_class( VideoDescriptor, scope_ids=ScopeIds(None, None, usage_key, usage_key), field_data=DictFieldData({}) ) self.descriptor.runtime.handler_url = MagicMock()
def i_click_on_error_dialog(step): world.click_link_by_text('Correct failed component') assert_true(world.css_html("span.inline-error").startswith("Problem i4x://MITx/999/problem")) course_key = SlashSeparatedCourseKey("MITx", "999", "Robot_Super_Course") # we don't know the actual ID of the vertical. So just check that we did go to a # vertical page in the course (there should only be one). vertical_usage_key = course_key.make_usage_key("vertical", "") vertical_url = reverse_usage_url('unit_handler', vertical_usage_key) assert_equal(1, world.browser.url.count(vertical_url))
def setup_xml_course(self): """ Define the XML backed course to use. Toy courses are already loaded in XML and mixed modulestores. """ course_key = SlashSeparatedCourseKey("edX", "toy", "2012_Fall") location = course_key.make_usage_key("chapter", "Overview") descriptor = modulestore().get_item(location) self.module = self._get_module(course_key, descriptor, location)
def instantiate_descriptor(**field_data): """ Instantiate descriptor with most properties. """ system = get_test_descriptor_system() course_key = SlashSeparatedCourseKey("org", "course", "run") usage_key = course_key.make_usage_key("html", "SampleHtml") return system.construct_xblock_from_class( HtmlDescriptor, scope_ids=ScopeIds(None, None, usage_key, usage_key), field_data=DictFieldData(field_data) )
def test_deprecated_replace(self): with self.assertDeprecationWarning(count=3): ssck = SlashSeparatedCourseKey("foo", "bar", "baz") ssck_boo = ssck.replace(org='boo') ssck_copy = ssck.replace() self.assertTrue(isinstance(ssck_boo, CourseLocator)) self.assertTrue(ssck_boo.deprecated) self.assertNotEqual(id(ssck), id(ssck_boo)) self.assertNotEqual(id(ssck), id(ssck_copy)) self.assertNotEqual(ssck, ssck_boo) self.assertEqual(ssck, ssck_copy)
def setUp(self): super(AccessTestCase, self).setUp() course_key = SlashSeparatedCourseKey("edX", "toy", "2012_Fall") self.course = course_key.make_usage_key("course", course_key.run) self.anonymous_user = AnonymousUserFactory() self.beta_user = BetaTesterFactory(course_key=self.course.course_key) self.student = UserFactory() self.global_staff = UserFactory(is_staff=True) self.course_staff = StaffFactory(course_key=self.course.course_key) self.course_instructor = InstructorFactory(course_key=self.course.course_key) self.staff = GlobalStaffFactory()
def instantiate_descriptor(**field_data): """ Instantiate descriptor with most properties. """ system = get_test_descriptor_system() course_key = SlashSeparatedCourseKey('org', 'course', 'run') usage_key = course_key.make_usage_key('video', 'SampleProblem') return system.construct_xblock_from_class( VideoDescriptor, scope_ids=ScopeIds(None, None, usage_key, usage_key), field_data=DictFieldData(field_data), )
def import_and_populate_course(self): """ Imports the test toy course and populates it with additional test data """ content_store = contentstore() import_from_xml(self.store, self.user.id, 'common/test/data/', ['toy'], static_content_store=content_store) course_id = SlashSeparatedCourseKey('edX', 'toy', '2012_Fall') # create an Orphan # We had a bug where orphaned draft nodes caused export to fail. This is here to cover that case. vertical = self.store.get_item(course_id.make_usage_key('vertical', self.TEST_VERTICAL), depth=1) vertical.location = vertical.location.replace(name='no_references') self.store.update_item(vertical, self.user.id, allow_not_found=True) orphan_vertical = self.store.get_item(vertical.location) self.assertEqual(orphan_vertical.location.name, 'no_references') self.assertEqual(len(orphan_vertical.children), len(vertical.children)) # create a Draft vertical vertical = self.store.get_item(course_id.make_usage_key('vertical', self.TEST_VERTICAL), depth=1) draft_vertical = self.store.convert_to_draft(vertical.location, self.user.id) self.assertEqual(self.store.compute_publish_state(draft_vertical), PublishState.draft) # create a Private (draft only) vertical private_vertical = self.store.create_item(self.user.id, course_id, 'vertical', self.PRIVATE_VERTICAL) self.assertEqual(self.store.compute_publish_state(private_vertical), PublishState.private) # create a Published (no draft) vertical public_vertical = self.store.create_item(self.user.id, course_id, 'vertical', self.PUBLISHED_VERTICAL) public_vertical = self.store.publish(public_vertical.location, self.user.id) self.assertEqual(self.store.compute_publish_state(public_vertical), PublishState.public) # add the new private and new public as children of the sequential sequential = self.store.get_item(course_id.make_usage_key('sequential', self.SEQUENTIAL)) sequential.children.append(private_vertical.location) sequential.children.append(public_vertical.location) self.store.update_item(sequential, self.user.id) # lock an asset content_store.set_attr(self.LOCKED_ASSET_KEY, 'locked', True) # create a non-portable link - should be rewritten in new courses html_module = self.store.get_item(course_id.make_usage_key('html', 'nonportable')) new_data = html_module.data = html_module.data.replace( '/static/', '/c4x/{0}/{1}/asset/'.format(course_id.org, course_id.course) ) self.store.update_item(html_module, self.user.id) html_module = self.store.get_item(html_module.location) self.assertEqual(new_data, html_module.data) return course_id
def i_click_on_error_dialog(step): world.click_link_by_text('Correct failed component') assert_true(world.css_html("span.inline-error").startswith("Problem i4x://MITx/999/problem")) course_key = SlashSeparatedCourseKey("MITx", "999", "Robot_Super_Course") # we don't know the actual ID of the vertical. So just check that we did go to a # vertical page in the course (there should only be one). vertical_usage_key = course_key.make_usage_key("vertical", None) vertical_url = reverse_usage_url('container_handler', vertical_usage_key) # Remove the trailing "/None" from the URL - we don't know the course ID, so we just want to # check that we visited a vertical URL. if vertical_url.endswith("/None"): vertical_url = vertical_url[:-5] assert_equal(1, world.browser.url.count(vertical_url))
def create_course(self, org, offering, user_id=None, fields=None, **kwargs): """ Creates and returns the course. Args: org (str): the organization that owns the course offering (str): the name of the course offering user_id: id of the user creating the course fields (dict): Fields to set on the course at initialization kwargs: Any optional arguments understood by a subset of modulestores to customize instantiation Returns: a CourseDescriptor Raises: InvalidLocationError: If a course with the same org and offering already exists """ course, _, run = offering.partition('/') course_id = SlashSeparatedCourseKey(org, course, run) # Check if a course with this org/course has been defined before (case-insensitive) course_search_location = SON([ ('_id.tag', 'i4x'), ('_id.org', re.compile(u'^{}$'.format(course_id.org), re.IGNORECASE)), ('_id.course', re.compile(u'^{}$'.format(course_id.course), re.IGNORECASE)), ('_id.category', 'course'), ]) courses = self.collection.find(course_search_location, fields=('_id')) if courses.count() > 0: raise InvalidLocationError( "There are already courses with the given org and course id: {}".format([ course['_id'] for course in courses ])) location = course_id.make_usage_key('course', course_id.run) course = self.create_and_save_xmodule(location, user_id, fields=fields, **kwargs) # clone a default 'about' overview module as well about_location = location.replace( category='about', name='overview' ) overview_template = AboutDescriptor.get_template('overview.yaml') self.create_and_save_xmodule( about_location, user_id, definition_data=overview_template.get('data'), runtime=course.system ) return course
def post(self, request, course_id, format=None): """ Enroll in Course """ user = request.user err = {} try: course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) course = course_from_key(course_key) except ItemNotFoundError: err['err_type'] = 'InvalidCourseId' err['err_msg'] = _("Course id is invalid") return Response(err, status=status.HTTP_400_BAD_REQUEST) if not has_access(user, 'enroll', course): err['err_type'] = 'InvalidEnrollment' err['err_msg'] = _("Enrollment is closed") return Response(err, status=status.HTTP_400_BAD_REQUEST) # see if we have already filled up all allowed enrollments is_course_full = CourseEnrollment.is_course_full(course) if is_course_full: err['err_type'] = 'InvalidEnrollment' err['err_msg'] = _("Course is full") return Response(err, status=status.HTTP_400_BAD_REQUEST) # If this course is available in multiple modes, redirect them to a page # where they can choose which mode they want. available_modes = CourseMode.modes_for_course(course_id) available_modes_dict = CourseMode.modes_for_course_dict(course_id, available_modes) if CourseMode.has_verified_mode(available_modes_dict): err['err_type'] = 'InvalidEnrollment' err['err_msg'] = _("Missing course mode") return Response(err, status=status.HTTP_400_BAD_REQUEST) current_mode = available_modes[0] course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) dog_stats_api.increment( "common.student.enrollment", tags=[u"org:{0}".format(course_key.org), u"course:{0}".format(course_key.course), u"run:{0}".format(course_key.run)] ) server_track(request, 'api.course.enrollment', { 'username': user.username, 'course_id': course_id, }) CourseEnrollment.enroll(user, course.id, mode=current_mode.slug) return Response()
def test_graphicslidertool_import(self): ''' Check to see if definition_from_xml in gst_module.py works properly. Pulls data from the graphic_slider_tool directory in the test data directory. ''' modulestore = XMLModuleStore(DATA_DIR, course_dirs=['graphic_slider_tool']) sa_id = SlashSeparatedCourseKey("edX", "gst_test", "2012_Fall") location = sa_id.make_usage_key("graphical_slider_tool", "sample_gst") gst_sample = modulestore.get_item(location) render_string_from_sample_gst_xml = """ <slider var="a" style="width:400px;float:left;"/>\ <plot style="margin-top:15px;margin-bottom:15px;"/>""".strip() self.assertIn(render_string_from_sample_gst_xml, gst_sample.data)
def lms_link_test(self): """ Tests get_lms_link_for_item. """ course_key = SlashSeparatedCourseKey("mitX", "101", "test") location = course_key.make_usage_key("vertical", "contacting_us") link = utils.get_lms_link_for_item(location, False) self.assertEquals(link, "//localhost:8000/courses/mitX/101/test/jump_to/i4x://mitX/101/vertical/contacting_us") # test preview link = utils.get_lms_link_for_item(location, True) self.assertEquals(link, "//preview/courses/mitX/101/test/jump_to/i4x://mitX/101/vertical/contacting_us") # now test with the course' location location = course_key.make_usage_key("course", "test") link = utils.get_lms_link_for_item(location) self.assertEquals(link, "//localhost:8000/courses/mitX/101/test/jump_to/i4x://mitX/101/course/test")
def static_tab(request, course_id, tab_slug): """ Display the courses tab with the given name. Assumes the course_id is in a valid format. """ try: course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) except InvalidKeyError: raise Http404 course = get_course_with_access(request.user, 'load', course_key) tab = CourseTabList.get_tab_by_slug(course.tabs, tab_slug) if tab is None: raise Http404 contents = get_static_tab_contents( request, course, tab ) if contents is None: raise Http404 return render_to_response('courseware/static_tab.html', { 'course': course, 'tab': tab, 'tab_contents': contents, })
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 request_certificate(request): """Request the on-demand creation of a certificate for some user, course. A request doesn't imply a guarantee that such a creation will take place. We intentionally use the same machinery as is used for doing certification at the end of a course run, so that we can be sure users get graded and then if and only if they pass, do they get a certificate issued. """ if request.method == "POST": if request.user.is_authenticated(): xqci = XQueueCertInterface() username = request.user.username student = User.objects.get(username=username) course_key = SlashSeparatedCourseKey.from_deprecated_string(request.POST.get("course_id")) course = modulestore().get_course(course_key, depth=2) status = certificate_status_for_student(student, course_key)["status"] if status in [CertificateStatuses.unavailable, CertificateStatuses.notpassing, CertificateStatuses.error]: logger.info( "Grading and certification requested for user {} in course {} via /request_certificate call".format( username, course_key ) ) status = xqci.add_cert(student, course_key, course=course) return HttpResponse(json.dumps({"add_status": status}), mimetype="application/json") return HttpResponse(json.dumps({"add_status": "ERRORANONYMOUSUSER"}), mimetype="application/json")
def jump_to(request, course_id, location): """ Show the page that contains a specific location. If the location is invalid or not in any class, return a 404. Otherwise, delegates to the index view to figure out whether this user has access, and what they should see. """ try: course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) usage_key = course_key.make_usage_key_from_deprecated_string(location) except InvalidKeyError: raise Http404(u"Invalid course_key or usage_key") try: (course_key, chapter, section, position) = path_to_location(modulestore(), usage_key) except ItemNotFoundError: raise Http404(u"No data at this location: {0}".format(usage_key)) except NoPathToItem: raise Http404(u"This location is not in any class: {0}".format(usage_key)) # choose the appropriate view (and provide the necessary args) based on the # args provided by the redirect. # Rely on index to do all error handling and access control. if chapter is None: return redirect('courseware', course_id=course_key.to_deprecated_string()) elif section is None: return redirect('courseware_chapter', course_id=course_key.to_deprecated_string(), chapter=chapter) elif position is None: return redirect('courseware_section', course_id=course_key.to_deprecated_string(), chapter=chapter, section=section) else: return redirect('courseware_position', course_id=course_key.to_deprecated_string(), chapter=chapter, section=section, position=position)
def course_info(request, course_id): """ Display the course's info.html, or 404 if there is no such course. Assumes the course_id is in a valid format. """ course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) course = get_course_with_access(request.user, 'load', course_key) staff_access = has_access(request.user, 'staff', course) masq = setup_masquerade(request, staff_access) # allow staff to toggle masquerade on info page reverifications = fetch_reverify_banner_info(request, course_key) studio_url = get_studio_url(course_key, 'course_info') context = { 'request': request, 'course_id': course_key.to_deprecated_string(), 'cache': None, 'course': course, 'staff_access': staff_access, 'masquerade': masq, 'studio_url': studio_url, 'reverifications': reverifications, } return render_to_response('courseware/info.html', context)
def setUp(self): 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) self.course_id = SlashSeparatedCourseKey("edX", "toy", "2012_Fall") self.location_string = self.course_id.make_usage_key('html', 'TestLocation').to_deprecated_string() self.toy = modulestore().get_course(self.course_id) location = "i4x://edX/toy/peergrading/init" field_data = DictFieldData({'data': "<peergrading/>", 'location': location, 'category': 'peergrading'}) self.mock_service = peer_grading_service.MockPeerGradingService() self.system = LmsModuleSystem( static_url=settings.STATIC_URL, track_function=None, get_module=None, render_template=render_to_string, replace_urls=None, s3_interface=test_util_open_ended.S3_INTERFACE, open_ended_grading_interface=test_util_open_ended.OPEN_ENDED_GRADING_INTERFACE, mixins=settings.XBLOCK_MIXINS, error_descriptor_class=ErrorDescriptor, descriptor_runtime=None, ) self.descriptor = peer_grading_module.PeerGradingDescriptor(self.system, field_data, ScopeIds(None, None, None, None)) self.descriptor.xmodule_runtime = self.system self.peer_module = self.descriptor self.peer_module.peer_gs = self.mock_service self.logout()
def progress(request, course_id, student_id=None): """ Wraps "_progress" with the manual_transaction context manager just in case there are unanticipated errors. """ with grades.manual_transaction(): return _progress(request, SlashSeparatedCourseKey.from_deprecated_string(course_id), student_id)
def __init__( self, data_dir, default_class=None, course_dirs=None, course_ids=None, load_error_modules=True, i18n_service=None, **kwargs ): """ Initialize an XMLModuleStore from data_dir Args: data_dir (str): path to data directory containing the course directories default_class (str): dot-separated string defining the default descriptor class to use if none is specified in entry_points course_dirs or course_ids (list of str): If specified, the list of course_dirs or course_ids to load. Otherwise, load all courses. Note, providing both """ super(XMLModuleStore, self).__init__(**kwargs) self.data_dir = path(data_dir) self.modules = defaultdict(dict) # course_id -> dict(location -> XBlock) self.courses = {} # course_dir -> XBlock for the course self.errored_courses = {} # course_dir -> errorlog, for dirs that failed to load if course_ids is not None: course_ids = [SlashSeparatedCourseKey.from_deprecated_string(course_id) for course_id in course_ids] self.load_error_modules = load_error_modules if default_class is None: self.default_class = None else: module_path, _, class_name = default_class.rpartition(".") class_ = getattr(import_module(module_path), class_name) self.default_class = class_ self.parent_trackers = defaultdict(ParentTracker) self.reference_type = Location # All field data will be stored in an inheriting field data. self.field_data = inheriting_field_data(kvs=DictKeyValueStore()) self.i18n_service = i18n_service # If we are specifically asked for missing courses, that should # be an error. If we are asked for "all" courses, find the ones # that have a course.xml. We sort the dirs in alpha order so we always # read things in the same order (OS differences in load order have # bitten us in the past.) if course_dirs is None: course_dirs = sorted( [d for d in os.listdir(self.data_dir) if os.path.exists(self.data_dir / d / "course.xml")] ) for course_dir in course_dirs: self.try_load_course(course_dir, course_ids)
def handle(self, *args, **options): if not options['course']: raise CommandError(Command.course_option.help) try: course_key = CourseKey.from_string(options['course']) except InvalidKeyError: course_key = SlashSeparatedCourseKey.from_deprecated_string(options['course']) course = get_course_by_id(course_key) print 'Warning: this command directly edits the list of course tabs in mongo.' print 'Tabs before any changes:' print_course(course) try: if options['delete']: if len(args) != 1: raise CommandError(Command.delete_option.help) num = int(args[0]) if query_yes_no('Deleting tab {0} Confirm?'.format(num), default='no'): primitive_delete(course, num - 1) # -1 for 0-based indexing elif options['insert']: if len(args) != 3: raise CommandError(Command.insert_option.help) num = int(args[0]) tab_type = args[1] name = args[2] if query_yes_no('Inserting tab {0} "{1}" "{2}" Confirm?'.format(num, tab_type, name), default='no'): primitive_insert(course, num - 1, tab_type, name) # -1 as above except ValueError as e: # Cute: translate to CommandError so the CLI error prints nicely. raise CommandError(e)
def set_course_mode_price(request, course_id): """ set the new course price and add new entry in the CourseModesArchive Table """ try: course_price = int(request.POST['course_price']) except ValueError: return JsonResponse( {'message': _("Please Enter the numeric value for the course price")}, status=400) # status code 400: Bad Request currency = request.POST['currency'] course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) course_honor_mode = CourseMode.objects.filter(mode_slug='honor', course_id=course_key) if not course_honor_mode: return JsonResponse( {'message': _("CourseMode with the mode slug({mode_slug}) DoesNotExist").format(mode_slug='honor')}, status=400) # status code 400: Bad Request CourseModesArchive.objects.create( course_id=course_id, mode_slug='honor', mode_display_name='Honor Code Certificate', min_price=course_honor_mode[0].min_price, currency=course_honor_mode[0].currency, expiration_datetime=datetime.datetime.now(pytz.utc), expiration_date=datetime.date.today() ) course_honor_mode.update( min_price=course_price, currency=currency ) return JsonResponse({'message': _("CourseMode price updated successfully")})
def setUp(self): self.course_key = SlashSeparatedCourseKey('Test', 'TestCourse', 'TestCourseRun') CourseMode.objects.all().delete()
def combined_notifications(request, course_id): """ Gets combined notifications from the grading controller and displays them """ course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) course = get_course_with_access(request.user, 'load', course_key) user = request.user notifications = open_ended_notifications.combined_notifications( course, user) response = notifications['response'] notification_tuples = open_ended_notifications.NOTIFICATION_TYPES notification_list = [] for response_num in xrange(len(notification_tuples)): tag = notification_tuples[response_num][0] if tag in response: url_name = notification_tuples[response_num][1] human_name = notification_tuples[response_num][2] url = _reverse_without_slash(url_name, course_key) has_img = response[tag] # check to make sure we have descriptions and alert messages if human_name in DESCRIPTION_DICT: description = DESCRIPTION_DICT[human_name] else: description = "" if human_name in ALERT_DICT: alert_message = ALERT_DICT[human_name] else: alert_message = "" notification_item = { 'url': url, 'name': human_name, 'alert': has_img, 'description': description, 'alert_message': alert_message } #The open ended panel will need to link the "peer grading" button in the panel to a peer grading #xmodule defined in the course. This checks to see if the human name of the server notification #that we are currently processing is "peer grading". If it is, it looks for a peer grading #module in the course. If none exists, it removes the peer grading item from the panel. if human_name == "Peer Grading": found_module, problem_url = find_peer_grading_module(course) if found_module: notification_list.append(notification_item) else: notification_list.append(notification_item) ajax_url = _reverse_with_slash('open_ended_notifications', course_key) combined_dict = { 'error_text': "", 'notification_list': notification_list, 'course': course, 'success': True, 'ajax_url': ajax_url, } return render_to_response( 'open_ended_problems/combined_notifications.html', combined_dict)
def test_toy_course_loads(self): # Load one of the XML based courses # Our test mapping rules allow the MixedModuleStore # to load this course from XML, not Mongo. self.check_all_pages_load( SlashSeparatedCourseKey('edX', 'toy', '2012_Fall'))
def course_about(request, course_id): """ Display the course's about page. Assumes the course_id is in a valid format. """ if microsite.get_value('ENABLE_MKTG_SITE', settings.FEATURES.get('ENABLE_MKTG_SITE', False)): raise Http404 course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) course = get_course_with_access(request.user, 'see_exists', course_key) registered = registered_for_course(course, request.user) staff_access = has_access(request.user, 'staff', course) studio_url = get_studio_url(course_key, 'settings/details') if has_access(request.user, 'load', course): course_target = reverse('info', args=[course.id.to_deprecated_string()]) else: course_target = reverse('about_course', args=[course.id.to_deprecated_string()]) show_courseware_link = (has_access(request.user, 'load', course) or settings.FEATURES.get('ENABLE_LMS_MIGRATION')) # Note: this is a flow for payment for course registration, not the Verified Certificate flow. registration_price = 0 in_cart = False reg_then_add_to_cart_link = "" if (settings.FEATURES.get('ENABLE_SHOPPING_CART') and settings.FEATURES.get('ENABLE_PAID_COURSE_REGISTRATION')): registration_price = CourseMode.min_course_price_for_currency( course_key, settings.PAID_COURSE_REGISTRATION_CURRENCY[0]) if request.user.is_authenticated(): cart = shoppingcart.models.Order.get_cart_for_user(request.user) in_cart = shoppingcart.models.PaidCourseRegistration.contained_in_order( cart, course_key) reg_then_add_to_cart_link = "{reg_url}?course_id={course_id}&enrollment_action=add_to_cart".format( reg_url=reverse('register_user'), course_id=course.id.to_deprecated_string()) # Used to provide context to message to student if enrollment not allowed can_enroll = has_access(request.user, 'enroll', course) invitation_only = course.invitation_only is_course_full = CourseEnrollment.is_course_full(course) # Register button should be disabled if one of the following is true: # - Student is already registered for course # - Course is already full # - Student cannot enroll in course active_reg_button = not (registered or is_course_full or not can_enroll) is_shib_course = uses_shib(course) return render_to_response( 'courseware/course_about.html', { 'course': course, 'staff_access': staff_access, 'studio_url': studio_url, 'registered': registered, 'course_target': course_target, 'registration_price': registration_price, 'in_cart': in_cart, 'reg_then_add_to_cart_link': reg_then_add_to_cart_link, 'show_courseware_link': show_courseware_link, 'is_course_full': is_course_full, 'can_enroll': can_enroll, 'invitation_only': invitation_only, 'active_reg_button': active_reg_button, 'is_shib_course': is_shib_course, })
def index(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, redirects to user's most recent chapter, or the first chapter if this is the user's first visit. Arguments: - request : HTTP request - course_id : course id (str: ORG/course/URL_NAME) - chapter : chapter url_name (str) - section : section url_name (str) - position : position in module, eg of <sequential> module (str) Returns: - HTTPresponse """ course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) user = User.objects.prefetch_related("groups").get(id=request.user.id) request.user = user # keep just one instance of User course = get_course_with_access(user, 'load', course_key, depth=2) staff_access = has_access(user, 'staff', course) registered = registered_for_course(course, user) if not registered: # TODO (vshnayder): do course instructors need to be registered to see course? log.debug(u'User %s tried to view course %s but is not enrolled', user, course.location.to_deprecated_string()) return redirect( reverse('about_course', args=[course_key.to_deprecated_string()])) masq = setup_masquerade(request, staff_access) try: field_data_cache = FieldDataCache.cache_for_descriptor_descendents( course_key, user, course, depth=2) course_module = get_module_for_descriptor(user, request, course, field_data_cache, course_key) if course_module is None: log.warning( u'If you see this, something went wrong: if we got this' u' far, should have gotten a course module for this user') return redirect( reverse('about_course', args=[course_key.to_deprecated_string()])) studio_url = get_studio_url(course_key, 'course') context = { 'csrf': csrf(request)['csrf_token'], 'accordion': render_accordion(request, course, chapter, section, field_data_cache), 'COURSE_TITLE': course.display_name_with_default, 'course': course, 'init': '', 'fragment': Fragment(), 'staff_access': staff_access, 'studio_url': studio_url, 'masquerade': masq, 'xqa_server': settings.FEATURES.get( 'USE_XQA_SERVER', 'http://*****:*****@content-qa.mitx.mit.edu/xqa'), 'reverifications': fetch_reverify_banner_info(request, course_key), } has_content = course.has_children_at_depth(CONTENT_DEPTH) if not has_content: # Show empty courseware for a course with no units return render_to_response('courseware/courseware.html', context) elif chapter is None: # passing CONTENT_DEPTH avoids returning 404 for a course with an # empty first section and a second section with content return redirect_to_course_position(course_module, CONTENT_DEPTH) # Only show the chat if it's enabled by the course and in the # settings. show_chat = course.show_chat and settings.FEATURES['ENABLE_CHAT'] if show_chat: context['chat'] = chat_settings(course, user) # If we couldn't load the chat settings, then don't show # the widget in the courseware. if context['chat'] is None: show_chat = False context['show_chat'] = show_chat chapter_descriptor = course.get_child_by( lambda m: m.location.name == chapter) if chapter_descriptor is not None: save_child_position(course_module, chapter) else: raise Http404( 'No chapter descriptor found with name {}'.format(chapter)) chapter_module = course_module.get_child_by( lambda m: m.location.name == chapter) if chapter_module is None: # User may be trying to access a chapter that isn't live yet if masq == 'student': # if staff is masquerading as student be kinder, don't 404 log.debug('staff masq as student: no chapter %s' % chapter) return redirect( reverse('courseware', args=[course.id.to_deprecated_string()])) raise Http404 if section is not None: section_descriptor = chapter_descriptor.get_child_by( lambda m: m.location.name == section) if section_descriptor is None: # Specifically asked-for section doesn't exist if masq == 'student': # if staff is masquerading as student be kinder, don't 404 log.debug('staff masq as student: no section %s' % section) return redirect( reverse('courseware', args=[course.id.to_deprecated_string()])) raise Http404 ## Allow chromeless operation if section_descriptor.chrome: chrome = [ s.strip() for s in section_descriptor.chrome.lower().split(",") ] if 'accordion' not in chrome: context['disable_accordion'] = True if 'tabs' not in chrome: context['disable_tabs'] = True if section_descriptor.default_tab: context['default_tab'] = section_descriptor.default_tab # cdodge: this looks silly, but let's refetch the section_descriptor with depth=None # which will prefetch the children more efficiently than doing a recursive load section_descriptor = modulestore().get_item( section_descriptor.location, depth=None) # Load all descendants of the section, because we're going to display its # html, which in general will need all of its children section_field_data_cache = FieldDataCache.cache_for_descriptor_descendents( course_key, user, section_descriptor, depth=None) # Verify that position a string is in fact an int if position is not None: try: int(position) except ValueError: raise Http404( "Position {} is not an integer!".format(position)) section_module = get_module_for_descriptor( request.user, request, section_descriptor, section_field_data_cache, course_key, position) if section_module is None: # User may be trying to be clever and access something # they don't have access to. raise Http404 # Save where we are in the chapter save_child_position(chapter_module, section) context['fragment'] = section_module.render(STUDENT_VIEW) context[ 'section_title'] = section_descriptor.display_name_with_default else: # section is none, so display a message studio_url = get_studio_url(course_key, 'course') prev_section = get_current_child(chapter_module) if prev_section is None: # Something went wrong -- perhaps this chapter has no sections visible to the user raise Http404 prev_section_url = reverse('courseware_section', kwargs={ 'course_id': course_key.to_deprecated_string(), 'chapter': chapter_descriptor.url_name, 'section': prev_section.url_name }) context['fragment'] = Fragment(content=render_to_string( 'courseware/welcome-back.html', { 'course': course, 'studio_url': studio_url, 'chapter_module': chapter_module, 'prev_section': prev_section, 'prev_section_url': prev_section_url })) result = render_to_response('courseware/courseware.html', context) except Exception as e: # Doesn't bar Unicode characters from URL, but if Unicode characters do # cause an error it is a graceful failure. if isinstance(e, UnicodeEncodeError): raise Http404("URL contains Unicode characters") if isinstance(e, Http404): # let it propagate raise # In production, don't want to let a 500 out for any reason if settings.DEBUG: raise else: log.exception( u"Error in index view: user={user}, course={course}, chapter={chapter}" u" section={section} position={position}".format( user=user, course=course, chapter=chapter, section=section, position=position)) try: result = render_to_response('courseware/courseware-error.html', { 'staff_access': staff_access, 'course': course }) except: # Let the exception propagate, relying on global config to at # at least return a nice error message log.exception("Error while rendering courseware-error page") raise return result
def get_about_page_link(self): """ create mock course and return the about page link """ course_key = SlashSeparatedCourseKey('mitX', '101', 'test') return utils.get_lms_link_for_about_page(course_key)
def setUp(self): super(TestPartitionService, self).setUp() self.course = Mock( id=SlashSeparatedCourseKey('org_0', 'course_0', 'run_0')) self.partition_service = self._create_service("ma")
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'})
def get_titles(self): if self.course_key is None: self.course_key = SlashSeparatedCourseKey.from_deprecated_string( self.course_id) #get all course structure course_usage_key = modulestore().make_course_usage_key(self.course_key) blocks = get_blocks(self.request, course_usage_key, depth='all', requested_fields=['display_name', 'children']) _root = blocks['root'] blocks_overviews = [] #return unit title and component root try: children = blocks['blocks'][_root]['children'] for z in children: child = blocks['blocks'][z] try: sub_section = child['children'] for s in sub_section: sub_ = blocks['blocks'][s] vertical = sub_['children'] try: for v in vertical: unit = blocks['blocks'][v] w = {} w['id'] = unit['id'] w['display_name'] = unit['display_name'] try: w['children'] = unit['children'] except: pass blocks_overviews.append(w) except: pass except: pass except: pass studentmodule = StudentModule.objects.raw( "SELECT id,course_id,module_id FROM courseware_studentmodule WHERE course_id = %s AND max_grade IS NOT NULL AND grade <= max_grade GROUP BY module_id ORDER BY created", [self.course_id]) title = [] for n in studentmodule: usage_key = n.module_state_key _current = get_blocks(self.request, usage_key, depth='all', requested_fields=['display_name']) root = _current['root'] unit_name = '' for over in blocks_overviews: if str(root) in over.get('children'): unit_name = over.get('display_name') q = { "title": _current['blocks'][root]['display_name'], "root": root, 'unit': unit_name } title.append(q) return title
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 == 'audit': # The user will have already been enrolled in the audit 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')) if requested_mode == 'honor': CourseEnrollment.enroll(user, course_key, mode=requested_mode) 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)}))
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from opaque_keys.edx.locations import Location, SlashSeparatedCourseKey from student.tests.factories import CourseEnrollmentFactory, UserFactory from courseware.model_data import StudentModule from courseware.tests.tests import LoginEnrollmentTestCase, TEST_DATA_MIXED_MODULESTORE from instructor_task.api_helper import encode_problem_and_student_input from instructor_task.models import PROGRESS, QUEUING from instructor_task.tests.factories import InstructorTaskFactory from instructor_task.views import instructor_task_status TEST_COURSE_ORG = 'edx' TEST_COURSE_NAME = 'test_course' TEST_COURSE_NUMBER = '1.23x' TEST_COURSE_KEY = SlashSeparatedCourseKey(TEST_COURSE_ORG, TEST_COURSE_NUMBER, TEST_COURSE_NAME) TEST_SECTION_NAME = "Problem" TEST_FAILURE_MESSAGE = 'task failed horribly' TEST_FAILURE_EXCEPTION = 'RandomCauseError' OPTION_1 = 'Option 1' OPTION_2 = 'Option 2' class InstructorTaskTestCase(TestCase): """ Tests API and view methods that involve the reporting of status for background tasks. """ def setUp(self): self.student = UserFactory.create(username="******",
def __init__( self, data_dir, default_class=None, source_dirs=None, course_ids=None, load_error_modules=True, i18n_service=None, fs_service=None, user_service=None, signal_handler=None, target_course_id=None, **kwargs # pylint: disable=unused-argument ): """ Initialize an XMLModuleStore from data_dir Args: data_dir (str): path to data directory containing the course directories default_class (str): dot-separated string defining the default descriptor class to use if none is specified in entry_points source_dirs or course_ids (list of str): If specified, the list of source_dirs or course_ids to load. Otherwise, load all courses. Note, providing both """ super(XMLModuleStore, self).__init__(**kwargs) self.data_dir = path(data_dir) self.modules = defaultdict( dict) # course_id -> dict(location -> XBlock) self.courses = {} # course_dir -> XBlock for the course self.errored_courses = { } # course_dir -> errorlog, for dirs that failed to load if course_ids is not None: course_ids = [ SlashSeparatedCourseKey.from_deprecated_string(course_id) for course_id in course_ids ] self.load_error_modules = load_error_modules if default_class is None: self.default_class = None else: module_path, _, class_name = default_class.rpartition('.') class_ = getattr(import_module(module_path), class_name) self.default_class = class_ # All field data will be stored in an inheriting field data. self.field_data = inheriting_field_data(kvs=DictKeyValueStore()) self.i18n_service = i18n_service self.fs_service = fs_service self.user_service = user_service # If we are specifically asked for missing courses, that should # be an error. If we are asked for "all" courses, find the ones # that have a course.xml. We sort the dirs in alpha order so we always # read things in the same order (OS differences in load order have # bitten us in the past.) if source_dirs is None: source_dirs = sorted([ d for d in os.listdir(self.data_dir) if os.path.exists(self.data_dir / d / self.parent_xml) ]) for course_dir in source_dirs: self.try_load_course(course_dir, course_ids, target_course_id)
def handle(self, *args, **options): # Scrub the username from the log message cleaned_options = copy.copy(options) if 'username' in cleaned_options: cleaned_options['username'] = '******' LOGGER.info((u"Starting to create tasks to regenerate certificates " u"with arguments %s and options %s"), unicode(args), unicode(cleaned_options)) if options['course']: # try to parse out the course from the serialized form try: course_id = CourseKey.from_string(options['course']) except InvalidKeyError: LOGGER.warning(( u"Course id %s could not be parsed as a CourseKey; " u"falling back to SlashSeparatedCourseKey.from_deprecated_string()" ), options['course']) course_id = SlashSeparatedCourseKey.from_deprecated_string( options['course']) else: raise CommandError("You must specify a course") user = options['username'] if not (course_id and user): raise CommandError( 'both course id and student username are required') student = None if '@' in user: student = User.objects.get(email=user, courseenrollment__course_id=course_id) else: student = User.objects.get(username=user, courseenrollment__course_id=course_id) course = modulestore().get_course(course_id, depth=2) if not options['noop']: LOGGER.info( (u"Adding task to the XQueue to generate a certificate " u"for student %s in course '%s'."), student.id, course_id) if course.issue_badges: badge_class = get_completion_badge(course_id, student) badge = badge_class.get_for_user(student) if badge: badge.delete() LOGGER.info(u"Cleared badge for student %s.", student.id) # Add the certificate request to the queue ret = regenerate_user_certificates( student, course_id, course=course, forced_grade=options['grade_value'], template_file=options['template_file'], insecure=options['insecure']) LOGGER.info( (u"Added a certificate regeneration task to the XQueue " u"for student %s in course '%s'. " u"The new certificate status is '%s'."), student.id, unicode(course_id), ret) else: LOGGER.info((u"Skipping certificate generation for " u"student %s in course '%s' " u"because the noop flag is set."), student.id, unicode(course_id)) LOGGER.info((u"Finished regenerating certificates command for " u"user %s and course '%s'."), student.id, unicode(course_id))
class SysadminBaseTestCase(SharedModuleStoreTestCase): """ Base class with common methods used in XML and Mongo tests """ TEST_REPO = 'https://github.com/mitocw/edx4edx_lite.git' TEST_BRANCH = 'testing_do_not_delete' TEST_BRANCH_COURSE = SlashSeparatedCourseKey('MITx', 'edx4edx_branch', 'edx4edx') def setUp(self): """Setup test case by adding primary user.""" super(SysadminBaseTestCase, self).setUp() self.user = UserFactory.create(username='******', email='*****@*****.**', password='******') self.client = Client() def _setstaff_login(self): """Makes the test user staff and logs them in""" GlobalStaff().add_users(self.user) self.client.login(username=self.user.username, password='******') def _add_edx4edx(self, branch=None): """Adds the edx4edx sample course""" post_dict = { 'repo_location': self.TEST_REPO, 'action': 'add_course', } if branch: post_dict['repo_branch'] = branch return self.client.post(reverse('sysadmin_courses'), post_dict) def _rm_edx4edx(self): """Deletes the sample course from the XML store""" def_ms = modulestore() course_path = '{0}/edx4edx_lite'.format( os.path.abspath(settings.DATA_DIR)) try: # using XML store course = def_ms.courses.get(course_path, None) except AttributeError: # Using mongo store course = def_ms.get_course( SlashSeparatedCourseKey('MITx', 'edx4edx', 'edx4edx')) # Delete git loaded course response = self.client.post( reverse('sysadmin_courses'), { 'course_id': course.id.to_deprecated_string(), 'action': 'del_course', }) self.addCleanup(self._rm_glob, '{0}_deleted_*'.format(course_path)) return response def _rm_glob(self, path): """ Create a shell expansion of passed in parameter and iteratively remove them. Must only expand to directories. """ for path in glob.glob(path): shutil.rmtree(path) def _mkdir(self, path): """ Create directory and add the cleanup for it. """ os.mkdir(path) self.addCleanup(shutil.rmtree, path)
def course_wiki_redirect(request, course_id): # pylint: disable=unused-argument """ This redirects to whatever page on the wiki that the course designates as it's home page. A course's wiki must be an article on the root (for example, "/6.002x") to keep things simple. """ course = get_course_by_id( SlashSeparatedCourseKey.from_deprecated_string(course_id)) course_slug = course_wiki_slug(course) valid_slug = True if not course_slug: log.exception( "This course is improperly configured. The slug cannot be empty.") valid_slug = False if re.match(r'^[-\w\.]+$', course_slug) is None: log.exception( "This course is improperly configured. The slug can only contain letters, numbers, periods or hyphens." ) valid_slug = False if not valid_slug: return redirect("wiki:get", path="") # The wiki needs a Site object created. We make sure it exists here try: Site.objects.get_current() except Site.DoesNotExist: new_site = Site() new_site.domain = settings.SITE_NAME new_site.name = "edX" new_site.save() site_id = str(new_site.id) if site_id != str(settings.SITE_ID): raise ImproperlyConfigured( "No site object was created and the SITE_ID doesn't match the newly created one. {} != {}" .format(site_id, settings.SITE_ID)) try: urlpath = URLPath.get_by_path(course_slug, select_related=True) results = list(Article.objects.filter(id=urlpath.article.id)) if results: article = results[0] else: article = None except (NoRootURL, URLPath.DoesNotExist): # We will create it in the next block urlpath = None article = None if not article: # create it root = get_or_create_root() if urlpath: # Somehow we got a urlpath without an article. Just delete it and # recerate it. urlpath.delete() content = cgi.escape( # Translators: this string includes wiki markup. Leave the ** and the _ alone. _("This is the wiki for **{organization}**'s _{course_name}_." ).format( organization=course.display_org_with_default, course_name=course.display_name_with_default_escaped, )) urlpath = URLPath.create_article( root, course_slug, title=course_slug, content=content, user_message=_("Course page automatically created."), user=None, ip_address=None, article_kwargs={ 'owner': None, 'group': None, 'group_read': True, 'group_write': True, 'other_read': True, 'other_write': True, }) return redirect("wiki:get", path=urlpath.path)
def get(self, request, *args, **kwargs): """Shows logs of imports that happened as a result of a git import""" course_id = kwargs.get('course_id') if course_id: course_id = SlashSeparatedCourseKey.from_deprecated_string( course_id) page_size = 10 # Set mongodb defaults even if it isn't defined in settings mongo_db = { 'host': 'localhost', 'user': '', 'password': '', 'db': 'xlog', } # Allow overrides if hasattr(settings, 'MONGODB_LOG'): for config_item in [ 'host', 'user', 'password', 'db', ]: mongo_db[config_item] = settings.MONGODB_LOG.get( config_item, mongo_db[config_item]) mongouri = 'mongodb://{user}:{password}@{host}/{db}'.format(**mongo_db) error_msg = '' try: if mongo_db['user'] and mongo_db['password']: mdb = mongoengine.connect(mongo_db['db'], host=mongouri) else: mdb = mongoengine.connect(mongo_db['db'], host=mongo_db['host']) except mongoengine.connection.ConnectionError: log.exception('Unable to connect to mongodb to save log, ' 'please check MONGODB_LOG settings.') if course_id is None: # Require staff if not going to specific course if not request.user.is_staff: raise Http404 cilset = CourseImportLog.objects.order_by('-created') else: try: course = get_course_by_id(course_id) except Exception: log.info('Cannot find course %s', course_id) raise Http404 # Allow only course team, instructors, and staff if not (request.user.is_staff or CourseInstructorRole(course.id).has_user(request.user) or CourseStaffRole(course.id).has_user(request.user)): raise Http404 log.debug('course_id=%s', course_id) cilset = CourseImportLog.objects.filter( course_id=course_id).order_by('-created') log.debug('cilset length=%s', len(cilset)) # Paginate the query set paginator = Paginator(cilset, page_size) try: logs = paginator.page(request.GET.get('page')) except PageNotAnInteger: logs = paginator.page(1) except EmptyPage: # If the page is too high or low given_page = int(request.GET.get('page')) page = min(max(1, given_page), paginator.num_pages) logs = paginator.page(page) mdb.disconnect() context = { 'logs': logs, 'course_id': course_id.to_deprecated_string() if course_id else None, 'error_msg': error_msg, 'page_size': page_size } return render_to_response(self.template_name, context)
class CourseEnrollmentAllowedFactory(DjangoModelFactory): class Meta(object): model = CourseEnrollmentAllowed email = '*****@*****.**' course_id = SlashSeparatedCourseKey('edX', 'toy', '2012_Fall')
def setUp(self): self.user = UserFactory.create() self.course_id = SlashSeparatedCourseKey('test_org', 'test_course_number', 'test_run') self.test_key = 'test_key'
from static_replace import (replace_static_urls, replace_course_urls, _url_replace_regex, process_static_urls, make_static_urls_absolute) from mock import patch, Mock from opaque_keys.edx.locations import SlashSeparatedCourseKey from xmodule.contentstore.content import StaticContent from xmodule.contentstore.django import contentstore from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore.mongo import MongoModuleStore from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory, check_mongo_calls from xmodule.modulestore.xml import XMLModuleStore DATA_DIRECTORY = 'data_dir' COURSE_KEY = SlashSeparatedCourseKey('org', 'course', 'run') STATIC_SOURCE = '"/static/file.png"' def test_multi_replace(): course_source = '"/course/file.png"' assert_equals( replace_static_urls(STATIC_SOURCE, DATA_DIRECTORY), replace_static_urls(replace_static_urls(STATIC_SOURCE, DATA_DIRECTORY), DATA_DIRECTORY)) assert_equals( replace_course_urls(course_source, COURSE_KEY), replace_course_urls(replace_course_urls(course_source, COURSE_KEY), COURSE_KEY))
def cohort_handler(request, course_key_string, cohort_id=None): """ The restful handler for cohort requests. Requires JSON. GET If a cohort ID is specified, returns a JSON representation of the cohort (name, id, user_count, assignment_type, user_partition_id, group_id). If no cohort ID is specified, returns the JSON representation of all cohorts. This is returned as a dict with the list of cohort information stored under the key `cohorts`. PUT or POST or PATCH If a cohort ID is specified, updates the cohort with the specified ID. Currently the only properties that can be updated are `name`, `user_partition_id` and `group_id`. Returns the JSON representation of the updated cohort. If no cohort ID is specified, creates a new cohort and returns the JSON representation of the updated cohort. """ course_key = SlashSeparatedCourseKey.from_deprecated_string( course_key_string) course = get_course_with_access(request.user, 'staff', course_key) if request.method == 'GET': if not cohort_id: all_cohorts = [ _get_cohort_representation(c, course) for c in cohorts.get_course_cohorts(course) ] return JsonResponse({'cohorts': all_cohorts}) else: cohort = cohorts.get_cohort_by_id(course_key, cohort_id) return JsonResponse(_get_cohort_representation(cohort, course)) else: # If cohort_id is specified, update the existing cohort. Otherwise, create a new cohort. if cohort_id: cohort = cohorts.get_cohort_by_id(course_key, cohort_id) name = request.json.get('name') if name != cohort.name: if cohorts.CohortAssignmentType.get( cohort, course) == cohorts.CohortAssignmentType.RANDOM: return JsonResponse( # Note: error message not translated because it is not exposed to the user (UI prevents). { "error": "Renaming of random cohorts is not supported at this time." }, 400) cohort.name = name cohort.save() else: name = request.json.get('name') if not name: # Note: error message not translated because it is not exposed to the user (UI prevents this state). return JsonResponse( { "error": "In order to create a cohort, a name must be specified." }, 400) try: cohort = cohorts.add_cohort(course_key, name) except ValueError as err: return JsonResponse({"error": unicode(err)}, 400) group_id = request.json.get('group_id') if group_id is not None: user_partition_id = request.json.get('user_partition_id') if user_partition_id is None: # Note: error message not translated because it is not exposed to the user (UI prevents this state). return JsonResponse( { "error": "If group_id is specified, user_partition_id must also be specified." }, 400) existing_group_id, existing_partition_id = cohorts.get_group_info_for_cohort( cohort) if group_id != existing_group_id or user_partition_id != existing_partition_id: unlink_cohort_partition_group(cohort) link_cohort_to_partition_group(cohort, user_partition_id, group_id) else: # If group_id was specified as None, unlink the cohort if it previously was associated with a group. existing_group_id, _ = cohorts.get_group_info_for_cohort(cohort) if existing_group_id is not None: unlink_cohort_partition_group(cohort) return JsonResponse(_get_cohort_representation(cohort, course))
def test_static_url_generation(self): course_key = SlashSeparatedCourseKey('org', 'class', 'run') location = course_key.make_asset_key('asset', 'my_file_name.jpg') path = StaticContent.get_static_path_from_location(location) self.assertEquals(path, '/static/my_file_name.jpg')
def import_and_populate_course(self): """ Imports the test toy course and populates it with additional test data """ content_store = contentstore() import_course_from_xml(self.store, self.user.id, TEST_DATA_DIR, ['toy'], static_content_store=content_store) course_id = SlashSeparatedCourseKey('edX', 'toy', '2012_Fall') # create an Orphan # We had a bug where orphaned draft nodes caused export to fail. This is here to cover that case. vertical = self.store.get_item(course_id.make_usage_key( 'vertical', self.TEST_VERTICAL), depth=1) vertical.location = vertical.location.replace(name='no_references') self.store.update_item(vertical, self.user.id, allow_not_found=True) orphan_vertical = self.store.get_item(vertical.location) self.assertEqual(orphan_vertical.location.name, 'no_references') self.assertEqual(len(orphan_vertical.children), len(vertical.children)) # create an orphan vertical and html; we already don't try to import # the orphaned vertical, but we should make sure we don't import # the orphaned vertical's child html, too orphan_draft_vertical = self.store.create_item( self.user.id, course_id, 'vertical', self.ORPHAN_DRAFT_VERTICAL) orphan_draft_html = self.store.create_item(self.user.id, course_id, 'html', self.ORPHAN_DRAFT_HTML) orphan_draft_vertical.children.append(orphan_draft_html.location) self.store.update_item(orphan_draft_vertical, self.user.id) # create a Draft vertical vertical = self.store.get_item(course_id.make_usage_key( 'vertical', self.TEST_VERTICAL), depth=1) draft_vertical = self.store.convert_to_draft(vertical.location, self.user.id) self.assertTrue(self.store.has_published_version(draft_vertical)) # create a Private (draft only) vertical private_vertical = self.store.create_item(self.user.id, course_id, 'vertical', self.PRIVATE_VERTICAL) self.assertFalse(self.store.has_published_version(private_vertical)) # create a Published (no draft) vertical public_vertical = self.store.create_item(self.user.id, course_id, 'vertical', self.PUBLISHED_VERTICAL) public_vertical = self.store.publish(public_vertical.location, self.user.id) self.assertTrue(self.store.has_published_version(public_vertical)) # add the new private and new public as children of the sequential sequential = self.store.get_item( course_id.make_usage_key('sequential', self.SEQUENTIAL)) sequential.children.append(private_vertical.location) sequential.children.append(public_vertical.location) self.store.update_item(sequential, self.user.id) # create an html and video component to make drafts: draft_html = self.store.create_item(self.user.id, course_id, 'html', self.DRAFT_HTML) draft_video = self.store.create_item(self.user.id, course_id, 'video', self.DRAFT_VIDEO) # add them as children to the public_vertical public_vertical.children.append(draft_html.location) public_vertical.children.append(draft_video.location) self.store.update_item(public_vertical, self.user.id) # publish changes to vertical self.store.publish(public_vertical.location, self.user.id) # convert html/video to draft self.store.convert_to_draft(draft_html.location, self.user.id) self.store.convert_to_draft(draft_video.location, self.user.id) # lock an asset content_store.set_attr(self.LOCKED_ASSET_KEY, 'locked', True) # create a non-portable link - should be rewritten in new courses html_module = self.store.get_item( course_id.make_usage_key('html', 'nonportable')) new_data = html_module.data = html_module.data.replace( '/static/', '/c4x/{0}/{1}/asset/'.format(course_id.org, course_id.course)) self.store.update_item(html_module, self.user.id) html_module = self.store.get_item(html_module.location) self.assertEqual(new_data, html_module.data) return course_id
def _invoke_xblock_handler(request, course_id, usage_id, handler, suffix, user): """ Invoke an XBlock handler, either authenticated or not. Arguments: request (HttpRequest): the current request course_id (str): A string of the form org/course/run usage_id (str): A string of the form i4x://org/course/category/name@revision handler (str): The name of the handler to invoke suffix (str): The suffix to pass to the handler when invoked user (User): The currently logged in user """ try: course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) usage_key = course_id.make_usage_key_from_deprecated_string(unquote_slashes(usage_id)) except InvalidKeyError: 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_item(usage_key) except ItemNotFoundError: log.warn( "Invalid location for course id {course_id}: {usage_key}".format( course_id=usage_key.course_key, usage_key=usage_key ) ) raise Http404 tracking_context_name = 'module_callback_handler' tracking_context = { 'module': { 'display_name': descriptor.display_name_with_default, } } field_data_cache = FieldDataCache.cache_for_descriptor_descendents( course_id, user, descriptor ) instance = get_module(user, request, usage_key, field_data_cache, 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?", usage_key, user) raise Http404 req = django_to_webob_request(request) try: with tracker.get_tracker().context(tracking_context_name, tracking_context): 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 add_users_to_cohort(request, course_key_string, cohort_id): """ Return json dict of: {'success': True, 'added': [{'username': ..., 'name': ..., 'email': ...}, ...], 'changed': [{'username': ..., 'name': ..., 'email': ..., 'previous_cohort': ...}, ...], 'present': [str1, str2, ...], # already there 'unknown': [str1, str2, ...]} Raises Http404 if the cohort cannot be found for the given course. """ # this is a string when we get it here course_key = SlashSeparatedCourseKey.from_deprecated_string( course_key_string) get_course_with_access(request.user, 'staff', course_key) try: cohort = cohorts.get_cohort_by_id(course_key, cohort_id) except CourseUserGroup.DoesNotExist: raise Http404( "Cohort (ID {cohort_id}) not found for {course_key_string}".format( cohort_id=cohort_id, course_key_string=course_key_string)) users = request.POST.get('users', '') added = [] changed = [] present = [] unknown = [] for username_or_email in split_by_comma_and_whitespace(users): if not username_or_email: continue try: (user, previous_cohort) = cohorts.add_user_to_cohort( cohort, username_or_email) info = { 'username': user.username, 'name': user.profile.name, 'email': user.email, } if previous_cohort: info['previous_cohort'] = previous_cohort changed.append(info) else: added.append(info) except ValueError: present.append(username_or_email) except User.DoesNotExist: unknown.append(username_or_email) return json_http_response({ 'success': True, 'added': added, 'changed': changed, 'present': present, 'unknown': unknown })
def instructor_dashboard_2(request, course_id): """ Display the instructor dashboard for a course. """ course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) course = get_course_by_id(course_key, depth=None) access = { 'admin': request.user.is_staff, 'instructor': has_access(request.user, 'instructor', course), 'finance_admin': CourseFinanceAdminRole(course_key).has_user(request.user), 'staff': has_access(request.user, 'staff', course), 'forum_admin': has_forum_access(request.user, course_key, FORUM_ROLE_ADMINISTRATOR), } if not access['staff']: raise Http404() sections = [ _section_course_info(course, access), _section_membership(course, access), _section_student_admin(course, access), _section_data_download(course, access), _section_analytics(course, access), ] #check if there is corresponding entry in the CourseMode Table related to the Instructor Dashboard course course_honor_mode = CourseMode.mode_for_course(course_key, 'honor') course_mode_has_price = False if course_honor_mode and course_honor_mode.min_price > 0: course_mode_has_price = True if (settings.FEATURES.get('INDIVIDUAL_DUE_DATES') and access['instructor']): sections.insert(3, _section_extensions(course)) # Gate access to course email by feature flag & by course-specific authorization if bulk_email_is_enabled_for_course(course_key): sections.append(_section_send_email(course, access)) # Gate access to Metrics tab by featue flag and staff authorization if settings.FEATURES['CLASS_DASHBOARD'] and access['staff']: sections.append(_section_metrics(course, access)) # Gate access to Ecommerce tab if course_mode_has_price: sections.append(_section_e_commerce(course, access)) disable_buttons = not _is_small_course(course_key) analytics_dashboard_message = None if settings.ANALYTICS_DASHBOARD_URL: # Construct a URL to the external analytics dashboard analytics_dashboard_url = '{0}/courses/{1}'.format( settings.ANALYTICS_DASHBOARD_URL, unicode(course_key)) link_start = "<a href=\"{}\" target=\"_blank\">".format( analytics_dashboard_url) analytics_dashboard_message = _( "To gain insights into student enrollment and participation {link_start}visit {analytics_dashboard_name}, our new course analytics product{link_end}." ) analytics_dashboard_message = analytics_dashboard_message.format( link_start=link_start, link_end="</a>", analytics_dashboard_name=settings.ANALYTICS_DASHBOARD_NAME) context = { 'course': course, 'old_dashboard_url': reverse('instructor_dashboard_legacy', kwargs={'course_id': course_key.to_deprecated_string()}), 'studio_url': get_studio_url(course, 'course'), 'sections': sections, 'disable_buttons': disable_buttons, 'analytics_dashboard_message': analytics_dashboard_message } return render_to_response( 'instructor/instructor_dashboard_2/instructor_dashboard_2.html', context)
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 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 create_new_course(request): """ Create a new course. Returns the URL for the course overview page. """ if not auth.has_access(request.user, CourseCreatorRole()): raise PermissionDenied() org = request.json.get('org') number = request.json.get('number') display_name = request.json.get('display_name') run = request.json.get('run') # allow/disable unicode characters in course_id according to settings if not settings.FEATURES.get('ALLOW_UNICODE_COURSE_ID'): if _has_non_ascii_characters(org) or _has_non_ascii_characters( number) or _has_non_ascii_characters(run): return JsonResponse( { 'error': _('Special characters not allowed in organization, course number, and course run.' ) }, status=400) try: course_key = SlashSeparatedCourseKey(org, number, run) # instantiate the CourseDescriptor and then persist it # note: no system to pass if display_name is None: metadata = {} else: metadata = {'display_name': display_name} # Set a unique wiki_slug for newly created courses. To maintain active wiki_slugs for # existing xml courses this cannot be changed in CourseDescriptor. # # TODO get rid of defining wiki slug in this org/course/run specific way and reconcile # w/ xmodule.course_module.CourseDescriptor.__init__ wiki_slug = u"{0}.{1}.{2}".format(course_key.org, course_key.course, course_key.run) definition_data = {'wiki_slug': wiki_slug} # Create the course then fetch it from the modulestore # Check if role permissions group for a course named like this already exists # Important because role groups are case insensitive if CourseRole.course_group_already_exists(course_key): raise InvalidLocationError() fields = {} fields.update(definition_data) fields.update(metadata) # Creating the course raises InvalidLocationError if an existing course with this org/name is found new_course = modulestore('direct').create_course( course_key.org, course_key.offering, fields=fields, ) # can't use auth.add_users here b/c it requires request.user to already have Instructor perms in this course # however, we can assume that b/c this user had authority to create the course, the user can add themselves CourseInstructorRole(new_course.id).add_users(request.user) auth.add_users(request.user, CourseStaffRole(new_course.id), request.user) # seed the forums seed_permissions_roles(new_course.id) # auto-enroll the course creator in the course so that "View Live" will # work. CourseEnrollment.enroll(request.user, new_course.id) _users_assign_default_role(new_course.id) return JsonResponse( {'url': reverse_course_url('course_handler', new_course.id)}) except InvalidLocationError: return JsonResponse({ 'ErrMsg': _('There is already a course defined with the same ' 'organization, course number, and course run. Please ' 'change either organization or course number to be unique.'), 'OrgErrMsg': _('Please change either the organization or ' 'course number so that it is unique.'), 'CourseErrMsg': _('Please change either the organization or ' 'course number so that it is unique.'), }) except InvalidKeyError as error: return JsonResponse({ "ErrMsg": _("Unable to create course '{name}'.\n\n{err}").format( name=display_name, err=error.message) })
class ApiTest(TestCase): def setUp(self): super(ApiTest, self).setUp() self.client = Client() # Mocks patcher = patch.object(api, 'api_enabled', Mock(return_value=True)) patcher.start() self.addCleanup(patcher.stop) # Create two accounts self.password = '******' self.student = User.objects.create_user('student', '*****@*****.**', self.password) self.student2 = User.objects.create_user('student2', '*****@*****.**', self.password) self.instructor = User.objects.create_user('instructor', '*****@*****.**', self.password) self.course_key = SlashSeparatedCourseKey('HarvardX', 'CB22x', 'The_Ancient_Greek_Hero') self.note = { 'user': self.student, 'course_id': self.course_key, 'uri': '/', 'text': 'foo', 'quote': 'bar', 'range_start': 0, 'range_start_offset': 0, 'range_end': 100, 'range_end_offset': 0, 'tags': 'a,b,c' } # Make sure no note with this ID ever exists for testing purposes self.NOTE_ID_DOES_NOT_EXIST = 99999 def login(self, as_student=None): username = None password = self.password if as_student is None: username = self.student.username else: username = as_student.username self.client.login(username=username, password=password) def url(self, name, args={}): args.update({'course_id': self.course_key.to_deprecated_string()}) return reverse(name, kwargs=args) def create_notes(self, num_notes, create=True): notes = [] for __ in range(num_notes): note = models.Note(**self.note) if create: note.save() notes.append(note) return notes def test_root(self): self.login() resp = self.client.get(self.url('notes_api_root')) self.assertEqual(resp.status_code, 200) self.assertNotEqual(resp.content, '') content = json.loads(resp.content) self.assertEqual(set(('name', 'version')), set(content.keys())) self.assertIsInstance(content['version'], int) self.assertEqual(content['name'], 'Notes API') def test_index_empty(self): self.login() resp = self.client.get(self.url('notes_api_notes')) self.assertEqual(resp.status_code, 200) self.assertNotEqual(resp.content, '') content = json.loads(resp.content) self.assertEqual(len(content), 0) def test_index_with_notes(self): num_notes = 3 self.login() self.create_notes(num_notes) resp = self.client.get(self.url('notes_api_notes')) self.assertEqual(resp.status_code, 200) self.assertNotEqual(resp.content, '') content = json.loads(resp.content) self.assertIsInstance(content, list) self.assertEqual(len(content), num_notes) def test_index_max_notes(self): self.login() MAX_LIMIT = api.API_SETTINGS.get('MAX_NOTE_LIMIT') num_notes = MAX_LIMIT + 1 self.create_notes(num_notes) resp = self.client.get(self.url('notes_api_notes')) self.assertEqual(resp.status_code, 200) self.assertNotEqual(resp.content, '') content = json.loads(resp.content) self.assertIsInstance(content, list) self.assertEqual(len(content), MAX_LIMIT) def test_create_note(self): self.login() notes = self.create_notes(1) self.assertEqual(len(notes), 1) note_dict = notes[0].as_dict() excluded_fields = ['id', 'user_id', 'created', 'updated'] note = dict([(k, v) for k, v in note_dict.items() if k not in excluded_fields]) resp = self.client.post(self.url('notes_api_notes'), json.dumps(note), content_type='application/json', HTTP_X_REQUESTED_WITH='XMLHttpRequest') self.assertEqual(resp.status_code, 303) self.assertEqual(len(resp.content), 0) def test_create_empty_notes(self): self.login() for empty_test in [None, [], '']: resp = self.client.post(self.url('notes_api_notes'), json.dumps(empty_test), content_type='application/json', HTTP_X_REQUESTED_WITH='XMLHttpRequest') self.assertEqual(resp.status_code, 400) def test_create_note_missing_ranges(self): self.login() notes = self.create_notes(1) self.assertEqual(len(notes), 1) note_dict = notes[0].as_dict() excluded_fields = ['id', 'user_id', 'created', 'updated'] + ['ranges'] note = dict([(k, v) for k, v in note_dict.items() if k not in excluded_fields]) resp = self.client.post(self.url('notes_api_notes'), json.dumps(note), content_type='application/json', HTTP_X_REQUESTED_WITH='XMLHttpRequest') self.assertEqual(resp.status_code, 400) def test_read_note(self): self.login() notes = self.create_notes(3) self.assertEqual(len(notes), 3) for note in notes: resp = self.client.get( self.url('notes_api_note', {'note_id': note.pk})) self.assertEqual(resp.status_code, 200) self.assertNotEqual(resp.content, '') content = json.loads(resp.content) self.assertEqual(content['id'], note.pk) self.assertEqual(content['user_id'], note.user_id) def test_note_doesnt_exist_to_read(self): self.login() resp = self.client.get( self.url('notes_api_note', {'note_id': self.NOTE_ID_DOES_NOT_EXIST})) self.assertEqual(resp.status_code, 404) self.assertEqual(resp.content, '') def test_student_doesnt_have_permission_to_read_note(self): notes = self.create_notes(1) self.assertEqual(len(notes), 1) note = notes[0] # set the student id to a different student (not the one that created the notes) self.login(as_student=self.student2) resp = self.client.get(self.url('notes_api_note', {'note_id': note.pk})) self.assertEqual(resp.status_code, 403) self.assertEqual(resp.content, '') def test_delete_note(self): self.login() notes = self.create_notes(1) self.assertEqual(len(notes), 1) note = notes[0] resp = self.client.delete( self.url('notes_api_note', {'note_id': note.pk})) self.assertEqual(resp.status_code, 204) self.assertEqual(resp.content, '') with self.assertRaises(models.Note.DoesNotExist): models.Note.objects.get(pk=note.pk) def test_note_does_not_exist_to_delete(self): self.login() resp = self.client.delete( self.url('notes_api_note', {'note_id': self.NOTE_ID_DOES_NOT_EXIST})) self.assertEqual(resp.status_code, 404) self.assertEqual(resp.content, '') def test_student_doesnt_have_permission_to_delete_note(self): notes = self.create_notes(1) self.assertEqual(len(notes), 1) note = notes[0] self.login(as_student=self.student2) resp = self.client.delete( self.url('notes_api_note', {'note_id': note.pk})) self.assertEqual(resp.status_code, 403) self.assertEqual(resp.content, '') try: models.Note.objects.get(pk=note.pk) except models.Note.DoesNotExist: self.fail( 'note should exist and not be deleted because the student does not have permission to do so' ) def test_update_note(self): notes = self.create_notes(1) note = notes[0] updated_dict = note.as_dict() updated_dict.update({ 'text': 'itchy and scratchy', 'tags': ['simpsons', 'cartoons', 'animation'] }) self.login() resp = self.client.put(self.url('notes_api_note', {'note_id': note.pk}), json.dumps(updated_dict), content_type='application/json', HTTP_X_REQUESTED_WITH='XMLHttpRequest') self.assertEqual(resp.status_code, 303) self.assertEqual(resp.content, '') actual = models.Note.objects.get(pk=note.pk) actual_dict = actual.as_dict() for field in ['text', 'tags']: self.assertEqual(actual_dict[field], updated_dict[field]) def test_search_note_params(self): self.login() total = 3 notes = self.create_notes(total) invalid_uri = ''.join([note.uri for note in notes]) tests = [{ 'limit': 0, 'offset': 0, 'expected_rows': total }, { 'limit': 0, 'offset': 2, 'expected_rows': total - 2 }, { 'limit': 0, 'offset': total, 'expected_rows': 0 }, { 'limit': 1, 'offset': 0, 'expected_rows': 1 }, { 'limit': 2, 'offset': 0, 'expected_rows': 2 }, { 'limit': total, 'offset': 2, 'expected_rows': 1 }, { 'limit': total, 'offset': total, 'expected_rows': 0 }, { 'limit': total + 1, 'offset': total + 1, 'expected_rows': 0 }, { 'limit': total + 1, 'offset': 0, 'expected_rows': total }, { 'limit': 0, 'offset': 0, 'uri': invalid_uri, 'expected_rows': 0, 'expected_total': 0 }] for test in tests: params = dict([(k, str(test[k])) for k in ('limit', 'offset', 'uri') if k in test]) resp = self.client.get(self.url('notes_api_search'), params, content_type='application/json', HTTP_X_REQUESTED_WITH='XMLHttpRequest') self.assertEqual(resp.status_code, 200) self.assertNotEqual(resp.content, '') content = json.loads(resp.content) for expected_key in ('total', 'rows'): self.assertIn(expected_key, content) if 'expected_total' in test: self.assertEqual(content['total'], test['expected_total']) else: self.assertEqual(content['total'], total) self.assertEqual(len(content['rows']), test['expected_rows']) for row in content['rows']: self.assertIn('id', row)
def handle(self, *args, **options): if os.path.exists(options['output']): raise CommandError("File {0} already exists".format( options['output'])) STATUS_INTERVAL = 100 # parse out the course into a coursekey if options['course']: try: course_key = CourseKey.from_string(options['course']) # if it's not a new-style course key, parse it from an old-style # course key except InvalidKeyError: course_key = SlashSeparatedCourseKey.from_deprecated_string( options['course']) print "Fetching enrolled students for {0}".format(course_key) enrolled_students = User.objects.filter( courseenrollment__course_id=course_key) factory = RequestMock() request = factory.get('/') total = enrolled_students.count() print "Total enrolled: {0}".format(total) course = courses.get_course_by_id(course_key) total = enrolled_students.count() start = datetime.datetime.now() rows = [] header = None print "Fetching certificate data" cert_grades = { cert.user.username: cert.grade for cert in list( GeneratedCertificate.objects.filter( # pylint: disable=no-member course_id=course_key).prefetch_related('user')) } print "Grading students" for count, student in enumerate(enrolled_students): count += 1 if count % STATUS_INTERVAL == 0: # Print a status update with an approximation of # how much time is left based on how long the last # interval took diff = datetime.datetime.now() - start timeleft = diff * (total - count) / STATUS_INTERVAL hours, remainder = divmod(timeleft.seconds, 3600) minutes, seconds = divmod(remainder, 60) print "{0}/{1} completed ~{2:02}:{3:02}m remaining".format( count, total, hours, minutes) start = datetime.datetime.now() request.user = student grade = course_grades.summary(student, request, course) if not header: header = [ section['label'] for section in grade[u'section_breakdown'] ] rows.append( ["email", "username", "certificate-grade", "grade"] + header) percents = { section['label']: section['percent'] for section in grade[u'section_breakdown'] } row_percents = [percents[label] for label in header] if student.username in cert_grades: rows.append([ student.email, student.username, cert_grades[ student.username], grade['percent'] ] + row_percents) else: rows.append( [student.email, student.username, "N/A", grade['percent'] ] + row_percents) with open(options['output'], 'wb') as f: writer = csv.writer(f) writer.writerows(rows)