Example #1
0
    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)
Example #2
0
    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)
Example #4
0
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()
Example #5
0
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.')
Example #10
0
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
Example #11
0
    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
Example #12
0
    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)
Example #13
0
    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))
Example #15
0
    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)
Example #17
0
 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)
Example #18
0
    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)
Example #22
0
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)
Example #23
0
 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)
Example #24
0
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)
Example #27
0
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)
Example #28
0
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)
Example #29
0
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()
Example #30
0
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)
Example #31
0
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'
        }])
Example #33
0
def enable_edxnotes_for_the_course(course, user_id):
    """
    Enable EdxNotes for the course.
    """
    course.tabs.append(EdxNotesTab())
    modulestore().update_item(course, user_id)
Example #34
0
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
Example #36
0
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
Example #37
0
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
Example #38
0
    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)
Example #39
0
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()
Example #40
0
    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
Example #43
0
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
Example #44
0
    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'})
Example #45
0
 def new_descriptor_runtime(self):
     runtime = get_test_descriptor_system()
     runtime.get_block = modulestore().get_item
     return runtime
Example #46
0
 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'})
Example #47
0
 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)
Example #48
0
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,
                )
Example #49
0
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)
Example #50
0
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)
Example #51
0
    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()
Example #52
0
 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)
Example #53
0
    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)
Example #55
0
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
Example #56
0
    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)
Example #57
0
    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)
Example #58
0
    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)
Example #59
0
    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))
Example #60
0
    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'})