コード例 #1
0
 def __init__(self,
              access_token,
              course_id,
              timeout=settings.LMS_DEFAULT_TIMEOUT):
     super(CourseAPIPresenterMixin, self).__init__(course_id, timeout)
     self.course_api_client = CourseStructureApiClient(
         settings.COURSE_API_URL, access_token)
コード例 #2
0
 def __init__(self,
              access_token,
              course_id,
              timeout=settings.LMS_DEFAULT_TIMEOUT):
     super(CoursePerformancePresenter,
           self).__init__(access_token, course_id, timeout)
     self.grading_policy_client = CourseStructureApiClient(
         settings.GRADING_POLICY_API_URL, access_token)
コード例 #3
0
    def dispatch(self, request, *args, **kwargs):
        self.course_api_enabled = switch_is_active('enable_course_api')

        if self.course_api_enabled and request.user.is_authenticated():
            self.access_token = settings.COURSE_API_KEY or request.user.access_token
            self.course_api = CourseStructureApiClient(settings.COURSE_API_URL, self.access_token).courses

        return super(CourseAPIMixin, self).dispatch(request, *args, **kwargs)
コード例 #4
0
 def __init__(self,
              access_token,
              course_id,
              timeout=settings.LMS_DEFAULT_TIMEOUT):
     super(CoursePerformancePresenter,
           self).__init__(access_token, course_id, timeout)
     # the deprecated course structure API has grading policy. This will be replaced in AN-7716
     self.grading_policy_client = CourseStructureApiClient(
         settings.GRADING_POLICY_API_URL, access_token)
コード例 #5
0
    def dispatch(self, request, *args, **kwargs):
        self.course_api_enabled = switch_is_active('enable_course_api')

        if self.course_api_enabled and request.user.is_authenticated():
            self.access_token = settings.COURSE_API_KEY or EdxRestApiClient.get_and_cache_jwt_oauth_access_token(
                settings.BACKEND_SERVICE_EDX_OAUTH2_PROVIDER_URL,
                settings.BACKEND_SERVICE_EDX_OAUTH2_KEY,
                settings.BACKEND_SERVICE_EDX_OAUTH2_SECRET,
            )[0]
            self.course_api = CourseStructureApiClient(settings.COURSE_API_URL,
                                                       self.access_token)

        return super(CourseAPIMixin, self).dispatch(request, *args, **kwargs)
コード例 #6
0
 def __init__(self, access_token, course_id, timeout=settings.LMS_DEFAULT_TIMEOUT):
     super(CourseAPIPresenterMixin, self).__init__(course_id, timeout)
     self.course_api_client = CourseStructureApiClient(settings.COURSE_API_URL, access_token)
コード例 #7
0
class CourseAPIPresenterMixin(object):
    """
    This mixin provides access to the course structure API and processes the hierarchy
    for sections, subsections, modules, and leaves (e.g. videos, problems, etc.).
    """
    __metaclass__ = abc.ABCMeta

    _last_updated = None

    def __init__(self, access_token, course_id, timeout=settings.LMS_DEFAULT_TIMEOUT):
        super(CourseAPIPresenterMixin, self).__init__(course_id, timeout)
        self.course_api_client = CourseStructureApiClient(settings.COURSE_API_URL, access_token)

    def _get_structure(self):
        """ Retrieves course structure from the course API. """
        key = self.get_cache_key('structure')
        structure = cache.get(key)
        if not structure:
            logger.debug('Retrieving structure for course: %s', self.course_id)
            blocks_kwargs = {
                'course_id': self.course_id,
                'depth': 'all',
                'all_blocks': 'true',
                'requested_fields': 'children,format,graded',
            }
            structure = self.course_api_client.blocks().get(**blocks_kwargs)
            cache.set(key, structure)

        return structure

    @abc.abstractproperty
    def section_type_template(self):
        """ Template for key generation to store/retrieve and cached structure data. E.g. "video_{}_{}" """
        pass

    @abc.abstractproperty
    def all_sections_key(self):
        """ Cache key for storing/retrieving structure for all sections. """
        pass

    @abc.abstractproperty
    def module_type(self):
        """ Module type to retrieve structure for. E.g. video, problem. """
        pass

    @property
    def module_graded_type(self):
        """
        Property used to filter modules by.  True/False will include only modules with
        that grade field.  Set to None if not filtering by the graded value.
        """
        return None

    def get_cache_key(self, name):
        """ Returns sanitized key for caching. """
        return sanitize_cache_key(u'{}_{}'.format(self.course_id, name))

    def course_structure(self, section_id=None, subsection_id=None):
        """
        Returns course structure from cache.  If structure isn't found, it is fetched from the
        course structure API.  If no arguments are provided, all sections and children are returned.
        If only section_id is provided, that section is returned.  If both section_id and
        subsection_id is provided, the structure for the subsection is returned.
        """
        if section_id is None and subsection_id is not None:
            raise ValueError('section_id must be specified if subsection_id is specified.')

        structure_type_key = self.get_cache_key(self.section_type_template.format(section_id, subsection_id))
        found_structure = cache.get(structure_type_key)

        if not found_structure:
            all_sections_key = self.get_cache_key(self.all_sections_key)
            found_structure = cache.get(all_sections_key)

            if not found_structure:
                structure = self._get_structure()
                found_structure = CourseStructure.course_structure_to_sections(structure, self.module_type,
                                                                               graded=self.module_graded_type)
                cache.set(all_sections_key, found_structure)

            for section in found_structure:
                self.add_child_data_to_parent_blocks(section['children'],
                                                     self.build_module_url_func(section['id']))
                self.attach_data_to_parents(section['children'],
                                            self.build_subsection_url_func(section['id']))
                section['num_modules'] = sum(child.get('num_modules', 0) for child in section['children'])

            self.attach_data_to_parents(found_structure, self.build_section_url)

            if found_structure:
                if section_id:
                    found_structure = [section for section in found_structure if section['id'] == section_id]

                    if found_structure and subsection_id:
                        found_structure = \
                            [section for section in found_structure[0]['children'] if section['id'] == subsection_id]

            cache.set(structure_type_key, found_structure)

        return found_structure

    def attach_data_to_parents(self, parents, url_func=None):
        """ Convenience method for adding aggregated data from children."""
        for index, parent in enumerate(parents):
            self.attach_aggregated_data_to_parent(index, parent, url_func)

    @abc.abstractmethod
    def attach_aggregated_data_to_parent(self, index, parent, url_func=None):
        """ Adds aggregate data from the child modules to the parent. """
        pass

    @abc.abstractproperty
    def default_block_data(self):
        """
        Returns a dictionary of default data for a block.  Typically, this would be the expected fields
        with empty/zero values.
        """
        pass

    @abc.abstractmethod
    def fetch_course_module_data(self):
        """
        Fetch course module data from the data API.  Use _course_module_data() for cached data.
        """
        pass

    @abc.abstractmethod
    def attach_computed_data(self, module_data):
        """
        Called by _course_module_data() to attach computed data (e.g. percentages, new IDs, etc.) to
        data returned from the analytics data api.
        """
        pass

    def _course_module_data(self):
        """ Retrieves course problems (from cache or course API) and calls process_module_data to attach data. """

        key = self.get_cache_key(self.module_type)
        module_data = cache.get(key)

        if not module_data:
            module_data = self.fetch_course_module_data()

            # Create a lookup table so that submission data can be quickly retrieved by downstream consumers.
            table = OrderedDict()
            last_updated = datetime.datetime.min

            for datum in module_data:
                self.attach_computed_data(datum)
                table[datum['id']] = datum

                # Set the last_updated value
                created = datum.pop('created', None)
                if created:
                    created = self.parse_api_datetime(created)
                    last_updated = max(last_updated, created)

            if last_updated is not datetime.datetime.min:
                _key = self.get_cache_key('{}_last_updated'.format(self.module_type))
                cache.set(_key, last_updated)
                self._last_updated = last_updated

            module_data = table
            cache.set(key, module_data)

        return module_data

    def module_id_to_data_id(self, module):
        """ Translates the course structure module to the ID used by the analytics data API. """
        return module['id']

    def add_child_data_to_parent_blocks(self, parent_blocks, url_func=None):
        """ Attaches data from the analytics data API to the course structure modules. """
        try:
            module_data = self._course_module_data()
        except BaseCourseError as e:
            logger.warning(e)
            module_data = OrderedDict()

        for parent_block in parent_blocks:
            parent_block['num_modules'] = len(parent_block['children'])
            for index, child in enumerate(parent_block['children']):
                data = module_data.get(self.module_id_to_data_id(child), self.default_block_data)

                # map empty names to None so that the UI catches them and displays as '(empty)'
                if len(child['name']) < 1:
                    child['name'] = None
                data['index'] = index + 1
                self.post_process_adding_data_to_blocks(data, parent_block, child, url_func)
                child.update(data)

    def post_process_adding_data_to_blocks(self, data, parent_block, child, url_func=None):
        """
        Override this if additional data is needed on the child block (e.g. problem part data).

        Arguments:
            data: Data for data API.
            parent_block: Parent of the child .
            child: Block that will be processed.
            url_func: URL generating function if needed to attach a URL to the child.
        """
        pass

    def build_section_url(self, _section):
        return None

    def build_subsection_url_func(self, _section_id):
        """
        Optionally override to return a function for creating the subsection URL.
        """
        return None

    def build_module_url_func(self, _section_id):
        """ Returns a function for generating a URL to the module (subsection child). """
        return None

    def sections(self):
        return self.course_structure()

    def section(self, section_id):
        section = None
        if section_id:
            section = self.course_structure(section_id)
            section = section[0] if section else None
        return section

    def subsections(self, section_id):
        sections = self.section(section_id)
        if sections:
            return sections.get('children', None)
        return None

    def subsection(self, section_id, subsection_id):
        subsection = None
        if section_id and subsection_id:
            subsection = self.course_structure(section_id, subsection_id)
            subsection = subsection[0] if subsection else None
        return subsection

    def subsection_children(self, section_id, subsection_id):
        """ Returns children (e.g. problems, videos) of a subsection. """
        subsections = self.subsection(section_id, subsection_id)
        if subsections:
            return subsections.get('children', None)
        return None

    def subsection_child(self, section_id, subsection_id, child_id):
        """ Return the specified child of a subsection (e.g. problem, video). """
        found_child = None
        children = self.subsection_children(section_id, subsection_id)
        if children:
            found_children = [child for child in children if child['id'] == child_id]
            found_child = found_children[0] if found_children else None
        return found_child

    def block(self, block_id):
        """ Retrieve a specific block (e.g. problem, video). """
        block = self._get_structure()['blocks'][block_id]
        block['name'] = block.get('display_name')
        return block

    def sibling_block(self, block_id, sibling_offset):
        """
        Returns a sibling block of the same type as the one denoted by
        `block_id`, where order is course ordering.  The sibling is chosen by
        `sibling_offset` which is the difference in index between the block and
        its requested sibling.  Returns `None` if no such sibling is found.
        Only siblings with data are returned.
        """
        sections = self.sections()
        siblings = [
            component
            for section in sections
            for subsection in section['children']
            for component in subsection['children']
            if component.get('url')  # Only consider siblings with data, hence with URLs
        ]
        try:
            block_index = (index for index, sibling in enumerate(siblings) if sibling['id'] == block_id).next()
            sibling_index = block_index + sibling_offset
            if sibling_index < 0:
                return None
            else:
                return siblings[sibling_index]
        except (StopIteration, IndexError):
            # StopIteration: requested video not found in the course structure
            # IndexError: No such video with the requested offset
            return None

    def next_block(self, block_id):
        """
        Get the next block in the course with the same block type as the block
        denoted by `block_id`.
        """
        return self.sibling_block(block_id, 1)

    def previous_block(self, block_id):
        """
        Get the previous block in the course with the same block type as the
        block denoted by `block_id`.
        """
        return self.sibling_block(block_id, -1)

    @abc.abstractmethod
    def blocks_have_data(self, blocks):
        """ Returns whether blocks contains any displayable data. """
        pass

    @property
    def last_updated(self):
        """ Returns when data was last updated according to the data api. """
        if not self._last_updated:
            key = self.get_cache_key('{}_last_updated'.format(self.module_type))
            self._last_updated = cache.get(key)

        return self._last_updated

    def build_view_live_url(self, base_url, module_id):
        """ Returns URL to view the module on the LMS. """
        view_live_url = None
        if base_url:
            view_live_url = u'{0}/{1}/jump_to/{2}'.format(base_url, self.course_id, module_id)
        return view_live_url

    def build_render_xblock_url(self, base_url, module_id):
        xblock_url = None
        if base_url:
            xblock_url = self._build_url(base_url, module_id)
        return xblock_url

    def _build_url(self, *args):
        """
        Removes trailing slashes from urls.  urllib.urljoin doesn't work because
        paths in our urls can include module IDs (e.g. i4x://edx/demo/video/12345ab2).
        """
        return '/'.join(str(arg).rstrip('/') for arg in args)
コード例 #8
0
class CourseAPIMixin(object):
    access_token = None
    course_api_enabled = False
    course_api = None
    course_id = None

    @cached_property
    def course_info(self):
        """
        Returns course info.

        All requests for course info should be made against this property to take advantage of caching.
        """
        return self.get_course_info(self.course_id)

    def dispatch(self, request, *args, **kwargs):
        self.course_api_enabled = switch_is_active('enable_course_api')

        if self.course_api_enabled and request.user.is_authenticated():
            self.access_token = settings.COURSE_API_KEY or request.user.access_token
            self.course_api = CourseStructureApiClient(settings.COURSE_API_URL,
                                                       self.access_token)

        return super(CourseAPIMixin, self).dispatch(request, *args, **kwargs)

    def _course_detail_cache_key(self, course_id):
        return sanitize_cache_key(u'course_{}_details'.format(course_id))

    def get_course_info(self, course_id):
        """
        Retrieve course info from the Course API.

        Retrieved data is cached.

        Arguments
            course_id       -- ID of the course for which data should be retrieved
        """
        key = self._course_detail_cache_key(course_id)
        info = cache.get(key)

        if not info:
            try:
                logger.debug("Retrieving detail for course: %s", course_id)
                info = self.course_api.courses(course_id).get()
                cache.set(key, info)
            except HttpClientError as e:
                logger.error("Unable to retrieve course info for %s: %s",
                             course_id, e)
                info = {}

        return info

    def get_courses(self):
        # Check the cache for the user's courses
        key = sanitize_cache_key(u'user_{}_courses'.format(
            self.request.user.pk))
        courses = cache.get(key)

        # If no cached courses, iterate over the data from the course API.
        if not courses:
            courses = []
            page = 1

            while page:
                try:
                    logger.debug('Retrieving page %d of course info...', page)
                    response = self.course_api.courses.get(page=page,
                                                           page_size=100)
                    course_details = response['results']

                    # Cache the information so that it doesn't need to be retrieved later.
                    for course in course_details:
                        course_id = course['id']
                        _key = self._course_detail_cache_key(course_id)
                        cache.set(_key, course)

                    courses += course_details

                    if response['pagination']['next']:
                        page += 1
                    else:
                        page = None
                        logger.debug(
                            'Completed retrieval of course info. Retrieved info for %d courses.',
                            len(courses))
                except HttpClientError as e:
                    logger.error("Unable to retrieve course data: %s", e)
                    page = None
                    break

        cache.set(key, courses)
        return courses
コード例 #9
0
class CourseAPIPresenterMixin(object):
    """
    This mixin provides access to the course structure API and processes the hierarchy
    for sections, subsections, modules, and leaves (e.g. videos, problems, etc.).
    """
    __metaclass__ = abc.ABCMeta

    _last_updated = None

    def __init__(self, access_token, course_id, timeout=settings.LMS_DEFAULT_TIMEOUT):
        super(CourseAPIPresenterMixin, self).__init__(course_id, timeout)
        self.course_api_client = CourseStructureApiClient(settings.COURSE_API_URL, access_token)

    def _get_structure(self):
        """ Retrieves course structure from the course API. """
        key = self.get_cache_key('structure')
        structure = cache.get(key)

        if not structure:
            logger.debug('Retrieving structure for course: %s', self.course_id)
            structure = self.course_api_client.course_structures(self.course_id).get()
            cache.set(key, structure)

        return structure

    @abc.abstractproperty
    def section_type_template(self):
        """ Template for key generation to store/retrieve and cached structure data. E.g. "video_{}_{}" """
        pass

    @abc.abstractproperty
    def all_sections_key(self):
        """ Cache key for storing/retrieving structure for all sections. """
        pass

    @abc.abstractproperty
    def module_type(self):
        """ Module type to retrieve structure for. E.g. video, problem. """
        pass

    @property
    def module_graded_type(self):
        """
        Property used to filter modules by.  True/False will include only modules with
        that grade field.  Set to None if not filtering by the graded value.
        """
        return None

    def get_cache_key(self, name):
        """ Returns sanitized key for caching. """
        return sanitize_cache_key(u'{}_{}'.format(self.course_id, name))

    def course_structure(self, section_id=None, subsection_id=None):
        """
        Returns course structure from cache.  If structure isn't found, it is fetched from the
        course structure API.  If no arguments are provided, all sections and children are returned.
        If only section_id is provided, that section is returned.  If both section_id and
        subsection_id is provided, the structure for the subsection is returned.
        """
        if section_id is None and subsection_id is not None:
            raise ValueError('section_id must be specified if subsection_id is specified.')

        structure_type_key = self.get_cache_key(self.section_type_template.format(section_id, subsection_id))
        found_structure = cache.get(structure_type_key)

        if not found_structure:
            all_sections_key = self.get_cache_key(self.all_sections_key)
            found_structure = cache.get(all_sections_key)

            if not found_structure:
                structure = self._get_structure()
                found_structure = CourseStructure.course_structure_to_sections(structure, self.module_type,
                                                                               graded=self.module_graded_type)
                cache.set(all_sections_key, found_structure)

            for section in found_structure:
                self.add_child_data_to_parent_blocks(section['children'],
                                                     self.build_module_url_func(section['id']))
                self.attach_data_to_parents(section['children'],
                                            self.build_subsection_url_func(section['id']))
                section['num_modules'] = sum(child.get('num_modules', 0) for child in section['children'])

            self.attach_data_to_parents(found_structure, self.build_section_url)

            if found_structure:
                if section_id:
                    found_structure = [section for section in found_structure if section['id'] == section_id]

                    if found_structure and subsection_id:
                        found_structure = \
                            [section for section in found_structure[0]['children'] if section['id'] == subsection_id]

            cache.set(structure_type_key, found_structure)

        return found_structure

    def attach_data_to_parents(self, parents, url_func=None):
        """ Convenience method for adding aggregated data from children."""
        for index, parent in enumerate(parents):
            self.attach_aggregated_data_to_parent(index, parent, url_func)

    @abc.abstractmethod
    def attach_aggregated_data_to_parent(self, index, parent, url_func=None):
        """ Adds aggregate data from the child modules to the parent. """
        pass

    @abc.abstractproperty
    def default_block_data(self):
        """
        Returns a dictionary of default data for a block.  Typically, this would be the expected fields
        with empty/zero values.
        """
        pass

    @abc.abstractmethod
    def fetch_course_module_data(self):
        """
        Fetch course module data from the data API.  Use _course_module_data() for cached data.
        """
        pass

    @abc.abstractmethod
    def attach_computed_data(self, module_data):
        """
        Called by _course_module_data() to attach computed data (e.g. percentages, new IDs, etc.) to
        data returned from the analytics data api.
        """
        pass

    def _course_module_data(self):
        """ Retrieves course problems (from cache or course API) and calls process_module_data to attach data. """

        key = self.get_cache_key(self.module_type)
        module_data = cache.get(key)

        if not module_data:
            module_data = self.fetch_course_module_data()

            # Create a lookup table so that submission data can be quickly retrieved by downstream consumers.
            table = {}
            last_updated = datetime.datetime.min

            for datum in module_data:
                self.attach_computed_data(datum)
                table[datum['id']] = datum

                # Set the last_updated value
                created = datum.pop('created', None)
                if created:
                    created = self.parse_api_datetime(created)
                    last_updated = max(last_updated, created)

            if last_updated is not datetime.datetime.min:
                _key = self.get_cache_key('{}_last_updated'.format(self.module_type))
                cache.set(_key, last_updated)
                self._last_updated = last_updated

            module_data = table
            cache.set(key, module_data)

        return module_data

    def module_id_to_data_id(self, module):
        """ Translates the course structure module to the ID used by the analytics data API. """
        return module['id']

    def add_child_data_to_parent_blocks(self, parent_blocks, url_func=None):
        """ Attaches data from the analytics data API to the course structure modules. """
        try:
            module_data = self._course_module_data()
        except BaseCourseError as e:
            logger.warning(e)
            module_data = {}

        for parent_block in parent_blocks:
            parent_block['num_modules'] = len(parent_block['children'])
            for index, child in enumerate(parent_block['children']):
                data = module_data.get(self.module_id_to_data_id(child), self.default_block_data)

                # map empty names to None so that the UI catches them and displays as '(empty)'
                if len(child['name']) < 1:
                    child['name'] = None
                data['index'] = index + 1
                self.post_process_adding_data_to_blocks(data, parent_block, child, url_func)
                child.update(data)

    def post_process_adding_data_to_blocks(self, data, parent_block, child, url_func=None):
        """
        Override this if additional data is needed on the child block (e.g. problem part data).

        Arguments:
            data: Data for data API.
            parent_block: Parent of the child .
            child: Block that will be processed.
            url_func: URL generating function if needed to attach a URL to the child.
        """
        pass

    def build_section_url(self, _section):
        return None

    def build_subsection_url_func(self, _section_id):
        """
        Optionally override to return a function for creating the subsection URL.
        """
        return None

    def build_module_url_func(self, _section_id):
        """ Returns a function for generating a URL to the module (subsection child). """
        return None

    def sections(self):
        return self.course_structure()

    def section(self, section_id):
        section = None
        if section_id:
            section = self.course_structure(section_id)
            section = section[0] if section else None
        return section

    def subsections(self, section_id):
        sections = self.section(section_id)
        if sections:
            return sections.get('children', None)
        return None

    def subsection(self, section_id, subsection_id):
        subsection = None
        if section_id and subsection_id:
            subsection = self.course_structure(section_id, subsection_id)
            subsection = subsection[0] if subsection else None
        return subsection

    def subsection_children(self, section_id, subsection_id):
        """ Returns children (e.g. problems, videos) of a subsection. """
        subsections = self.subsection(section_id, subsection_id)
        if subsections:
            return subsections.get('children', None)
        return None

    def subsection_child(self, section_id, subsection_id, child_id):
        """ Return the specified child of a subsection (e.g. problem, video). """
        found_child = None
        children = self.subsection_children(section_id, subsection_id)
        if children:
            found_children = [child for child in children if child['id'] == child_id]
            found_child = found_children[0] if found_children else None
        return found_child

    def block(self, block_id):
        """ Retrieve a specific block (e.g. problem, video). """
        block = self._get_structure()['blocks'][block_id]
        block['name'] = block.get('display_name')
        return block

    def sibling_block(self, block_id, sibling_offset):
        """
        Returns a sibling block of the same type as the one denoted by
        `block_id`, where order is course ordering.  The sibling is chosen by
        `sibling_offset` which is the difference in index between the block and
        its requested sibling.  Returns `None` if no such sibling is found.
        Only siblings with data are returned.
        """
        sections = self.sections()
        siblings = [
            component
            for section in sections
            for subsection in section['children']
            for component in subsection['children']
            if component.get('url')  # Only consider siblings with data, hence with URLs
        ]
        try:
            block_index = (index for index, sibling in enumerate(siblings) if sibling['id'] == block_id).next()
            sibling_index = block_index + sibling_offset
            if sibling_index < 0:
                return None
            else:
                return siblings[sibling_index]
        except (StopIteration, IndexError):
            # StopIteration: requested video not found in the course structure
            # IndexError: No such video with the requested offset
            return None

    def next_block(self, block_id):
        """
        Get the next block in the course with the same block type as the block
        denoted by `block_id`.
        """
        return self.sibling_block(block_id, 1)

    def previous_block(self, block_id):
        """
        Get the previous block in the course with the same block type as the
        block denoted by `block_id`.
        """
        return self.sibling_block(block_id, -1)

    @abc.abstractmethod
    def blocks_have_data(self, blocks):
        """ Returns whether blocks contains any displayable data. """
        pass

    @property
    def last_updated(self):
        """ Returns when data was last updated according to the data api. """
        if not self._last_updated:
            key = self.get_cache_key('{}_last_updated'.format(self.module_type))
            self._last_updated = cache.get(key)

        return self._last_updated

    def build_view_live_url(self, base_url, module_id):
        """ Returns URL to view the module on the LMS. """
        view_live_url = None
        if base_url:
            view_live_url = u'{0}/{1}/jump_to/{2}'.format(base_url, self.course_id, module_id)
        return view_live_url

    def build_render_xblock_url(self, base_url, module_id):
        xblock_url = None
        if base_url:
            xblock_url = self._build_url(base_url, module_id)
        return xblock_url

    def _build_url(self, *args):
        """
        Removes trailing slashes from urls.  urllib.urljoin doesn't work because
        paths in our urls can include module IDs (e.g. i4x://edx/demo/video/12345ab2).
        """
        return '/'.join(str(arg).rstrip('/') for arg in args)
コード例 #10
0
 def test_explicit_timeout(self):
     client = CourseStructureApiClient('http://example.com/', 'arbitrary_access_token', timeout=2.5)
     # pylint: disable=protected-access
     self.assertEqual(client._store['session'].timeout, 2.5)
コード例 #11
0
 def test_default_timeout(self):
     client = CourseStructureApiClient('http://example.com/', 'arbitrary_access_token')
     # pylint: disable=protected-access
     self.assertEqual(client._store['session'].timeout, settings.LMS_DEFAULT_TIMEOUT)
class CoursePerformancePresenter(CourseAPIPresenterMixin, CoursePresenter):
    """
    Presenter for the performance page.
    """

    # limit for the number of bars to display in the answer distribution chart
    CHART_LIMIT = 12

    # minimum screen space a grading policy bar will take up (even if a policy is 0%, display some bar)
    MIN_POLICY_DISPLAY_PERCENT = 5

    def __init__(self, access_token, course_id, timeout=settings.LMS_DEFAULT_TIMEOUT):
        super(CoursePerformancePresenter, self).__init__(access_token, course_id, timeout)
        self.grading_policy_client = CourseStructureApiClient(settings.GRADING_POLICY_API_URL, access_token)

    def course_module_data(self):
        try:
            return self._course_module_data()
        except BaseCourseError:
            raise NotFoundError

    def get_answer_distribution(self, problem_id, problem_part_id):
        """
        Retrieve answer distributions for a particular module/problem and problem part.
        """

        module = self.client.modules(self.course_id, problem_id)

        api_response = module.answer_distribution()
        questions = self._build_questions(api_response)

        filtered_active_question = [i for i in questions if i['part_id'] == problem_part_id]
        if len(filtered_active_question) == 0:
            raise NotFoundError
        else:
            active_question = filtered_active_question[0]['question']

        answer_distributions = self._build_answer_distribution(api_response, problem_part_id)
        problem_part_description = self._build_problem_description(problem_part_id, questions)

        is_random = self._is_answer_distribution_random(answer_distributions)
        answer_distribution_limited = None
        if not is_random:
            # only display the top in the chart
            answer_distribution_limited = answer_distributions[:self.CHART_LIMIT]

        answer_type = self._get_answer_type(answer_distributions)
        last_updated = self.parse_api_datetime(answer_distributions[0]['created'])
        self._last_updated = last_updated

        return AnswerDistributionEntry(last_updated, questions, active_question, answer_distributions,
                                       answer_distribution_limited, is_random, answer_type, problem_part_description)

    def _build_problem_description(self, problem_part_id, questions):
        """ Returns the displayable problem name. """
        problem = [q for q in questions if q['part_id'] == problem_part_id][0]
        return problem['short_description']

    def _get_answer_type(self, answer_distributions):
        """
        Returns either 'text' or 'numeric' to describe the answer and used in the JS table to format
        and sort the dataset.
        """
        field = 'answer_value'
        for ad in answer_distributions:
            if ad[field] is not None and not utils.number.is_number(ad[field]):
                return 'text'
        return 'numeric'

    def _is_answer_distribution_random(self, answer_distributions):
        """
        Problems are considered randomized if variant is populated with values
        greater than 1.
        """
        for ad in answer_distributions:
            variant = ad['variant']
            if variant is not None and variant is not 1:
                return True
        return False

    # pylint: disable=redefined-variable-type
    def _build_questions(self, answer_distributions):
        """
        Builds the questions and part_id from the answer distribution. Displayed
        drop down.
        """
        questions = []
        part_id_to_problem = {}

        # Collect unique questions from the answer distribution
        for question_answer in answer_distributions:
            question = question_answer.get('question_text', None)
            problem_name = question_answer.get('problem_display_name', None)
            part_id_to_problem[question_answer['part_id']] = {
                'question': question,
                'problem_name': problem_name
            }

        for part_id, problem in part_id_to_problem.iteritems():
            questions.append({
                'part_id': part_id,
                'question': problem['question'],
                'problem_name': problem['problem_name']
            })

        utils.sorting.natural_sort(questions, 'part_id')

        # add an enumerated label
        has_parts = len(questions) > 1
        for i, question in enumerate(questions):
            text = question['question']
            question_num = i + 1
            question_template = _('Submissions')
            short_description_template = ''
            if text:
                if has_parts:
                    question_template = _('Submissions for Part {part_number}: {part_description}')
                    short_description_template = _('Part {part_number}: {part_description}')
                else:
                    question_template = _('Submissions: {part_description}')
                    short_description_template = _('{part_description}')
            else:
                if has_parts:
                    question_template = _('Submissions for Part {part_number}')
                    short_description_template = _('Part {part_number}')

            # pylint: disable=no-member
            question['question'] = question_template.format(part_number=question_num, part_description=text)
            question['short_description'] = short_description_template.format(
                part_number=question_num, part_description=text)

        return questions

    def _build_answer_distribution(self, api_response, problem_part_id):
        """ Filter for this problem part and sort descending order. """
        answer_distributions = [i for i in api_response if i['part_id'] == problem_part_id]
        for answer_dist in answer_distributions:
            # First and last response counts were added, we can handle both types of API responses at the moment.
            # If just the count is specified it is assumed to be the last response count.
            # TODO: teach downstream logic about first and last response counts
            count = answer_dist.get('last_response_count')
            if count is not None:
                answer_dist['count'] = count
        answer_distributions = sorted(answer_distributions, key=lambda a: -a['count'])
        return answer_distributions

    def grading_policy(self):
        """ Returns the grading policy for the represented course."""
        key = self.get_cache_key('grading_policy')
        grading_policy = cache.get(key)

        if not grading_policy:
            logger.debug('Retrieving grading policy for course: %s', self.course_id)
            grading_policy = self.grading_policy_client.courses(self.course_id).policy.get()

            # Remove empty assignment types as they are not useful and will cause issues downstream.
            grading_policy = [item for item in grading_policy if item['assignment_type']]

            cache.set(key, grading_policy)

        return grading_policy

    def get_max_policy_display_percent(self, grading_policy):
        """
        Returns the maximum width that a grading bar can be for display, given
        the min width, MIN_POLICY_DISPLAY_PERCENT.
        """
        max_percent = 100
        for policy in grading_policy:
            if policy['weight'] < (self.MIN_POLICY_DISPLAY_PERCENT / 100.0):
                max_percent -= self.MIN_POLICY_DISPLAY_PERCENT
        return max_percent

    def assignment_types(self):
        """ Returns the assignment types for the represented course."""
        grading_policy = self.grading_policy()

        # return the results in a similar format to the course structure for standard parsing
        return [
            {
                'name': gp['assignment_type'],
                'url': reverse('courses:performance:graded_content_by_type',
                               kwargs={
                                   'course_id': self.course_id,
                                   'assignment_type': slugify(gp['assignment_type'])
                               })
            } for gp in grading_policy
        ]

    def fetch_course_module_data(self):
        # Implementation of abstract method.  Returns problems from data api.
        try:
            problems = self.client.courses(self.course_id).problems()
        except NotFoundError:
            raise NoAnswerSubmissionsError(course_id=self.course_id)
        return problems

    def attach_computed_data(self, problem):
        # Change the id key name
        problem['id'] = problem.pop('module_id')
        # Add an percent and incorrect_submissions field
        total = problem['total_submissions']
        problem['correct_percent'] = utils.math.calculate_percent(problem['correct_submissions'], total)
        problem['incorrect_submissions'] = total - problem['correct_submissions']
        problem['incorrect_percent'] = utils.math.calculate_percent(problem['incorrect_submissions'], total)

    def post_process_adding_data_to_blocks(self, data, parent_block, child_block, url_func=None):
        # not all problems have submissions
        if len(data['part_ids']) > 0:
            utils.sorting.natural_sort(data['part_ids'])
            if url_func:
                data['url'] = url_func(parent_block, child_block, data)

    @property
    def default_block_data(self):
        return {
            'total_submissions': 0,
            'correct_submissions': 0,
            'correct_percent': 0,
            'incorrect_submissions': 0,
            'incorrect_percent': 0,
            'part_ids': []
        }

    def assignments(self, assignment_type=None):
        """ Returns the assignments (and problems) for the represented course. """

        assignment_type_name = None if assignment_type is None else assignment_type['name']
        assignment_type_key = self.get_cache_key(u'assignments_{}'.format(assignment_type_name))
        assignments = cache.get(assignment_type_key)

        if not assignments:
            all_assignments_key = self.get_cache_key(u'assignments')
            assignments = cache.get(all_assignments_key)

            if not assignments:
                structure = self._get_structure()
                assignments = CourseStructure.course_structure_to_assignments(
                    structure, graded=True, assignment_type=None)
                cache.set(all_assignments_key, assignments)

            if assignment_type:
                assignment_type['name'] = assignment_type['name'].lower()
                assignments = [assignment for assignment in assignments if
                               assignment['assignment_type'].lower() == assignment_type['name']]

            self.add_child_data_to_parent_blocks(assignments, self._build_graded_answer_distribution_url)
            self.attach_data_to_parents(assignments, self._build_assignment_url)

            # Cache the data for the course-assignment_type combination.
            cache.set(assignment_type_key, assignments)

        return assignments

    def attach_aggregated_data_to_parent(self, index, parent, url_func=None):
        children = parent['children']
        total_submissions = sum(child.get('total_submissions', 0) for child in children)
        correct_submissions = sum(child.get('correct_submissions', 0) for child in children)
        incorrect_submissions = total_submissions - correct_submissions
        parent.update({
            'total_submissions': total_submissions,
            'correct_submissions': correct_submissions,
            'correct_percent': utils.math.calculate_percent(correct_submissions, total_submissions),
            'incorrect_submissions': incorrect_submissions,
            'incorrect_percent': utils.math.calculate_percent(incorrect_submissions, total_submissions),
            'index': index + 1,
            'average_submissions': 0,
            'average_correct_submissions': 0,
            'average_incorrect_submissions': 0,
        })

        if parent['num_modules']:
            num_modules = float(parent['num_modules'])
            parent.update({
                'average_submissions': total_submissions / num_modules,
                'average_correct_submissions': correct_submissions / num_modules,
                'average_incorrect_submissions': incorrect_submissions / num_modules,
            })

        # removing the URL keeps navigation between the menu and bar chart consistent
        if url_func and parent['total_submissions'] > 0:
            parent['url'] = url_func(parent)

    def _build_graded_answer_distribution_url(self, parent, problem, parts):
        return reverse('courses:performance:answer_distribution',
                       kwargs={
                           'course_id': self.course_id,
                           'assignment_id': parent['id'],
                           'problem_id': problem['id'],
                           'problem_part_id': parts['part_ids'][0]
                       })

    def build_module_url_func(self, section_id):
        def build_url(parent, problem, parts):
            return reverse('courses:performance:ungraded_answer_distribution',
                           kwargs={
                               'course_id': self.course_id,
                               'section_id': section_id,
                               'subsection_id': parent['id'],
                               'problem_id': problem['id'],
                               'problem_part_id': parts['part_ids'][0]
                           })

        return build_url

    def _build_assignment_url(self, assignment):
        return reverse('courses:performance:assignment', kwargs={
            'course_id': self.course_id, 'assignment_id': assignment['id']})

    def build_section_url(self, section):
        return reverse('courses:performance:ungraded_section', kwargs={'course_id': self.course_id,
                                                                       'section_id': section['id']})

    def build_subsection_url_func(self, section_id):
        """
        Returns a function for creating the ungraded subsection URL.
        """
        # Using closures to keep the section ID available
        def subsection_url(subsection):
            return reverse('courses:performance:ungraded_subsection',
                           kwargs={'course_id': self.course_id,
                                   'section_id': section_id,
                                   'subsection_id': subsection['id']})
        return subsection_url

    def blocks_have_data(self, assignments):
        if assignments:
            for assignment in assignments:
                if assignment['total_submissions'] > 0:
                    return True
        return False

    def assignment(self, assignment_id):
        """ Retrieve a specific assignment. """
        filtered = [assignment for assignment in self.assignments() if assignment['id'] == assignment_id]
        if filtered:
            return filtered[0]
        else:
            return None

    @property
    def section_type_template(self):
        return u'ungraded_sections_{}_{}'

    @property
    def all_sections_key(self):
        return u'ungraded_sections'

    @property
    def module_type(self):
        return 'problem'

    @property
    def module_graded_type(self):
        """
        Get ungraded blocks.

        This is a bit confusing as this presenter is used to show both graded and
        ungraded content.  The ungraded content uses CourseAPIPresenterMixin::course_structure
        which then gets the module grade type for filtering.
        """
        return False
コード例 #13
0
 def __init__(self, access_token, course_id, timeout=settings.LMS_DEFAULT_TIMEOUT):
     super(CoursePerformancePresenter, self).__init__(access_token, course_id, timeout)
     self.grading_policy_client = CourseStructureApiClient(settings.GRADING_POLICY_API_URL, access_token)
コード例 #14
0
class CoursePerformancePresenter(CourseAPIPresenterMixin, CoursePresenter):
    """
    Presenter for the performance page.
    """

    # limit for the number of bars to display in the answer distribution chart
    CHART_LIMIT = 12

    # minimum screen space a grading policy bar will take up (even if a policy is 0%, display some bar)
    MIN_POLICY_DISPLAY_PERCENT = 5

    def __init__(self, access_token, course_id, timeout=settings.LMS_DEFAULT_TIMEOUT):
        super(CoursePerformancePresenter, self).__init__(access_token, course_id, timeout)
        self.grading_policy_client = CourseStructureApiClient(settings.GRADING_POLICY_API_URL, access_token)

    def course_module_data(self):
        try:
            return self._course_module_data()
        except BaseCourseError:
            raise NotFoundError

    def get_answer_distribution(self, problem_id, problem_part_id):
        """
        Retrieve answer distributions for a particular module/problem and problem part.
        """

        module = self.client.modules(self.course_id, problem_id)

        api_response = module.answer_distribution()
        questions = self._build_questions(api_response)

        filtered_active_question = [i for i in questions if i['part_id'] == problem_part_id]
        if not filtered_active_question:
            raise NotFoundError
        else:
            active_question = filtered_active_question[0]['question']

        answer_distributions = self._build_answer_distribution(api_response, problem_part_id)
        problem_part_description = self._build_problem_description(problem_part_id, questions)

        is_random = self._is_answer_distribution_random(answer_distributions)
        answer_distribution_limited = None
        if not is_random:
            # only display the top in the chart
            answer_distribution_limited = answer_distributions[:self.CHART_LIMIT]

        answer_type = self._get_answer_type(answer_distributions)
        last_updated = self.parse_api_datetime(answer_distributions[0]['created'])
        self._last_updated = last_updated

        return AnswerDistributionEntry(last_updated, questions, active_question, answer_distributions,
                                       answer_distribution_limited, is_random, answer_type, problem_part_description)

    def _build_problem_description(self, problem_part_id, questions):
        """ Returns the displayable problem name. """
        problem = [q for q in questions if q['part_id'] == problem_part_id][0]
        return problem['short_description']

    def _get_answer_type(self, answer_distributions):
        """
        Returns either 'text' or 'numeric' to describe the answer and used in the JS table to format
        and sort the dataset.
        """
        field = 'answer_value'
        for ad in answer_distributions:
            if ad[field] is not None and not utils.number.is_number(ad[field]):
                return 'text'
        return 'numeric'

    def _is_answer_distribution_random(self, answer_distributions):
        """
        Problems are considered randomized if variant is populated with values
        greater than 1.
        """
        for ad in answer_distributions:
            variant = ad['variant']
            if variant is not None and variant is not 1:
                return True
        return False

    # pylint: disable=redefined-variable-type
    def _build_questions(self, answer_distributions):
        """
        Builds the questions and part_id from the answer distribution. Displayed
        drop down.
        """
        questions = []
        part_id_to_problem = {}

        # Collect unique questions from the answer distribution
        for question_answer in answer_distributions:
            question = question_answer.get('question_text', None)
            problem_name = question_answer.get('problem_display_name', None)
            part_id_to_problem[question_answer['part_id']] = {
                'question': question,
                'problem_name': problem_name
            }

        for part_id, problem in part_id_to_problem.iteritems():
            questions.append({
                'part_id': part_id,
                'question': problem['question'],
                'problem_name': problem['problem_name']
            })

        utils.sorting.natural_sort(questions, 'part_id')

        # add an enumerated label
        has_parts = len(questions) > 1
        for i, question in enumerate(questions):
            text = question['question']
            question_num = i + 1
            question_template = _('Submissions')
            short_description_template = ''
            if text:
                if has_parts:
                    question_template = _('Submissions for Part {part_number}: {part_description}')
                    short_description_template = _('Part {part_number}: {part_description}')
                else:
                    question_template = _('Submissions: {part_description}')
                    short_description_template = _('{part_description}')
            else:
                if has_parts:
                    question_template = _('Submissions for Part {part_number}')
                    short_description_template = _('Part {part_number}')

            # pylint: disable=no-member
            question['question'] = question_template.format(part_number=question_num, part_description=text)
            question['short_description'] = short_description_template.format(
                part_number=question_num, part_description=text)

        return questions

    def _build_answer_distribution(self, api_response, problem_part_id):
        """ Filter for this problem part and sort descending order. """
        answer_distributions = [i for i in api_response if i['part_id'] == problem_part_id]
        for answer_dist in answer_distributions:
            # First and last response counts were added, we can handle both types of API responses at the moment.
            # If just the count is specified it is assumed to be the last response count.
            # TODO: teach downstream logic about first and last response counts
            count = answer_dist.get('last_response_count')
            if count is not None:
                answer_dist['count'] = count
        answer_distributions = sorted(answer_distributions, key=lambda a: -a['count'])
        return answer_distributions

    def grading_policy(self):
        """ Returns the grading policy for the represented course."""
        key = self.get_cache_key('grading_policy')
        grading_policy = cache.get(key)

        if not grading_policy:
            logger.debug('Retrieving grading policy for course: %s', self.course_id)
            grading_policy = self.grading_policy_client.courses(self.course_id).policy.get()

            # Remove empty assignment types as they are not useful and will cause issues downstream.
            grading_policy = [item for item in grading_policy if item['assignment_type']]

            cache.set(key, grading_policy)

        return grading_policy

    def get_max_policy_display_percent(self, grading_policy):
        """
        Returns the maximum width that a grading bar can be for display, given
        the min width, MIN_POLICY_DISPLAY_PERCENT.
        """
        max_percent = 100
        for policy in grading_policy:
            if policy['weight'] < (self.MIN_POLICY_DISPLAY_PERCENT / 100.0):
                max_percent -= self.MIN_POLICY_DISPLAY_PERCENT
        return max_percent

    def assignment_types(self):
        """ Returns the assignment types for the represented course."""
        grading_policy = self.grading_policy()

        # return the results in a similar format to the course structure for standard parsing
        return [
            {
                'name': gp['assignment_type'],
                'url': reverse('courses:performance:graded_content_by_type',
                               kwargs={
                                   'course_id': self.course_id,
                                   'assignment_type': slugify(gp['assignment_type'])
                               })
            } for gp in grading_policy
        ]

    def fetch_course_module_data(self):
        # Implementation of abstract method.  Returns problems from data api.
        try:
            problems = self.client.courses(self.course_id).problems()
        except NotFoundError:
            raise NoAnswerSubmissionsError(course_id=self.course_id)
        return problems

    def attach_computed_data(self, problem):
        # Change the id key name
        problem['id'] = problem.pop('module_id')
        # Add an percent and incorrect_submissions field
        total = problem['total_submissions']
        problem['correct_percent'] = utils.math.calculate_percent(problem['correct_submissions'], total)
        problem['incorrect_submissions'] = total - problem['correct_submissions']
        problem['incorrect_percent'] = utils.math.calculate_percent(problem['incorrect_submissions'], total)

    def post_process_adding_data_to_blocks(self, data, parent_block, child_block, url_func=None):
        # not all problems have submissions
        if data['part_ids']:
            utils.sorting.natural_sort(data['part_ids'])
            if url_func:
                data['url'] = url_func(parent_block, child_block, data)

    @property
    def default_block_data(self):
        return {
            'total_submissions': 0,
            'correct_submissions': 0,
            'correct_percent': 0,
            'incorrect_submissions': 0,
            'incorrect_percent': 0,
            'part_ids': []
        }

    def assignments(self, assignment_type=None):
        """ Returns the assignments (and problems) for the represented course. """

        assignment_type_name = None if assignment_type is None else assignment_type['name']
        assignment_type_key = self.get_cache_key(u'assignments_{}'.format(assignment_type_name))
        assignments = cache.get(assignment_type_key)

        if not assignments:
            all_assignments_key = self.get_cache_key(u'assignments')
            assignments = cache.get(all_assignments_key)

            if not assignments:
                structure = self._get_structure()
                assignments = CourseStructure.course_structure_to_assignments(
                    structure, graded=True, assignment_type=None)
                cache.set(all_assignments_key, assignments)

            if assignment_type:
                assignment_type['name'] = assignment_type['name'].lower()
                assignments = [assignment for assignment in assignments if
                               assignment['assignment_type'].lower() == assignment_type['name']]

            self.add_child_data_to_parent_blocks(assignments, self._build_graded_answer_distribution_url)
            self.attach_data_to_parents(assignments, self._build_assignment_url)

            # Cache the data for the course-assignment_type combination.
            cache.set(assignment_type_key, assignments)

        return assignments

    def attach_aggregated_data_to_parent(self, index, parent, url_func=None):
        children = parent['children']
        total_submissions = sum(child.get('total_submissions', 0) for child in children)
        correct_submissions = sum(child.get('correct_submissions', 0) for child in children)
        incorrect_submissions = total_submissions - correct_submissions
        parent.update({
            'total_submissions': total_submissions,
            'correct_submissions': correct_submissions,
            'correct_percent': utils.math.calculate_percent(correct_submissions, total_submissions),
            'incorrect_submissions': incorrect_submissions,
            'incorrect_percent': utils.math.calculate_percent(incorrect_submissions, total_submissions),
            'index': index + 1,
            'average_submissions': 0,
            'average_correct_submissions': 0,
            'average_incorrect_submissions': 0,
        })

        if parent['num_modules']:
            num_modules = float(parent['num_modules'])
            parent.update({
                'average_submissions': total_submissions / num_modules,
                'average_correct_submissions': correct_submissions / num_modules,
                'average_incorrect_submissions': incorrect_submissions / num_modules,
            })

        # removing the URL keeps navigation between the menu and bar chart consistent
        if url_func and parent['total_submissions'] > 0:
            parent['url'] = url_func(parent)

    def _build_graded_answer_distribution_url(self, parent, problem, parts):
        return reverse('courses:performance:answer_distribution',
                       kwargs={
                           'course_id': self.course_id,
                           'assignment_id': parent['id'],
                           'problem_id': problem['id'],
                           'problem_part_id': parts['part_ids'][0]
                       })

    def build_module_url_func(self, section_id):
        def build_url(parent, problem, parts):
            return reverse('courses:performance:ungraded_answer_distribution',
                           kwargs={
                               'course_id': self.course_id,
                               'section_id': section_id,
                               'subsection_id': parent['id'],
                               'problem_id': problem['id'],
                               'problem_part_id': parts['part_ids'][0]
                           })

        return build_url

    def _build_assignment_url(self, assignment):
        return reverse('courses:performance:assignment', kwargs={
            'course_id': self.course_id, 'assignment_id': assignment['id']})

    def build_section_url(self, section):
        return reverse('courses:performance:ungraded_section', kwargs={'course_id': self.course_id,
                                                                       'section_id': section['id']})

    def build_subsection_url_func(self, section_id):
        """
        Returns a function for creating the ungraded subsection URL.
        """
        # Using closures to keep the section ID available
        def subsection_url(subsection):
            return reverse('courses:performance:ungraded_subsection',
                           kwargs={'course_id': self.course_id,
                                   'section_id': section_id,
                                   'subsection_id': subsection['id']})
        return subsection_url

    def blocks_have_data(self, assignments):
        if assignments:
            for assignment in assignments:
                if assignment['total_submissions'] > 0:
                    return True
        return False

    def assignment(self, assignment_id):
        """ Retrieve a specific assignment. """
        filtered = [assignment for assignment in self.assignments() if assignment['id'] == assignment_id]
        if filtered:
            return filtered[0]
        return None

    @property
    def section_type_template(self):
        return u'ungraded_sections_{}_{}'

    @property
    def all_sections_key(self):
        return u'ungraded_sections'

    @property
    def module_type(self):
        return 'problem'

    @property
    def module_graded_type(self):
        """
        Get ungraded blocks.

        This is a bit confusing as this presenter is used to show both graded and
        ungraded content.  The ungraded content uses CourseAPIPresenterMixin::course_structure
        which then gets the module grade type for filtering.
        """
        return False
コード例 #15
0
 def test_required_args(self, url, access_token):
     with self.assertRaises(ValueError):
         CourseStructureApiClient(url, access_token)
コード例 #16
0
class CourseAPIMixin(object):
    access_token = None
    course_api_enabled = False
    course_api = None
    course_id = None

    @cached_property
    def course_info(self):
        """
        Returns course info.

        All requests for course info should be made against this property to take advantage of caching.
        """
        return self.get_course_info(self.course_id)

    def dispatch(self, request, *args, **kwargs):
        self.course_api_enabled = switch_is_active('enable_course_api')

        if self.course_api_enabled and request.user.is_authenticated():
            self.access_token = settings.COURSE_API_KEY or request.user.access_token
            self.course_api = CourseStructureApiClient(settings.COURSE_API_URL, self.access_token).courses

        return super(CourseAPIMixin, self).dispatch(request, *args, **kwargs)

    def _course_detail_cache_key(self, course_id):
        return sanitize_cache_key(u'course_{}_details'.format(course_id))

    def get_course_info(self, course_id):
        """
        Retrieve course info from the Course API.

        Retrieved data is cached.

        Arguments
            course_id       -- ID of the course for which data should be retrieved
        """
        key = self._course_detail_cache_key(course_id)
        info = cache.get(key)

        if not info:
            try:
                logger.debug("Retrieving detail for course: %s", course_id)
                info = self.course_api(course_id).get()
                cache.set(key, info)
            except HttpClientError as e:
                logger.error("Unable to retrieve course info for %s: %s", course_id, e)
                info = {}

        return info

    def get_courses(self):
        # Check the cache for the user's courses
        key = sanitize_cache_key(u'user_{}_courses'.format(self.request.user.pk))
        courses = cache.get(key)

        # If no cached courses, iterate over the data from the course API.
        if not courses:
            courses = []
            page = 1

            while page:
                try:
                    logger.debug('Retrieving page %d of course info...', page)
                    response = self.course_api.get(page=page, page_size=100)
                    course_details = response['results']

                    # Cache the information so that it doesn't need to be retrieved later.
                    for course in course_details:
                        course_id = course['id']
                        _key = self._course_detail_cache_key(course_id)
                        cache.set(_key, course)

                    courses += course_details

                    if response['next']:
                        page += 1
                    else:
                        page = None
                        logger.debug('Completed retrieval of course info. Retrieved info for %d courses.', len(courses))
                except HttpClientError as e:
                    logger.error("Unable to retrieve course data: %s", e)
                    page = None
                    break

        cache.set(key, courses)
        return courses