Files
edx-platform/cms/djangoapps/contentstore/api/views/course_quality.py
Feanil Patel ff4d084e8b fix: Update to adjust for new scipy.stats.mode behavior.
Previously the default for the `keepdims` parameter was True, but as of
SciPy 1.11.0 it is false. This is actually the behavior we want here
since we only care about the mode value and not other values.

https://docs.scipy.org/doc/scipy/release/1.11.0-notes.html#expired-deprecations
2024-06-11 11:30:22 -04:00

277 lines
11 KiB
Python

# lint-amnesty, pylint: disable=missing-module-docstring
import logging
import time
import numpy as np
from edxval.api import get_videos_for_course
from rest_framework.generics import GenericAPIView
from rest_framework.response import Response
from scipy import stats
from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, view_auth_classes
from openedx.core.lib.cache_utils import request_cached
from openedx.core.lib.graph_traversals import traverse_pre_order
from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order
from .utils import course_author_access_required, get_bool_param
log = logging.getLogger(__name__)
@view_auth_classes()
class CourseQualityView(DeveloperErrorViewMixin, GenericAPIView):
"""
**Use Case**
**Example Requests**
GET /api/courses/v1/quality/{course_id}/
**GET Parameters**
A GET request may include the following parameters.
* all
* sections
* subsections
* units
* videos
* exclude_graded (boolean) - whether to exclude graded subsections in the subsections and units information.
**GET Response Values**
The HTTP 200 response has the following values.
* is_self_paced - whether the course is self-paced.
* sections
* total_number - number of sections in the course.
* total_visible - number of sections visible to learners in the course.
* number_with_highlights - number of sections that have at least one highlight entered.
* highlights_enabled - whether highlights are enabled in the course.
* subsections
* total_visible - number of subsections visible to learners in the course.
* num_with_one_block_type - number of visible subsections containing only one type of block.
* num_block_types - statistics for number of block types across all visible subsections.
* min
* max
* mean
* median
* mode
* units
* total_visible - number of units visible to learners in the course.
* num_blocks - statistics for number of block across all visible units.
* min
* max
* mean
* median
* mode
* videos
* total_number - number of video blocks in the course.
* num_with_val_id - number of video blocks that include video pipeline IDs.
* num_mobile_encoded - number of videos encoded through the video pipeline.
* durations - statistics for video duration across all videos encoded through the video pipeline.
* min
* max
* mean
* median
* mode
"""
@course_author_access_required
def get(self, request, course_key):
"""
Returns validation information for the given course.
"""
def _execute_method_and_log_time(log_time, func, *args):
"""
Call func passed in method with logging the time it took to complete.
Logging is temporary, we will remove this once we get required information.
"""
if log_time:
start_time = time.time()
output = func(*args)
log.info('[%s] completed in [%f]', func.__name__, (time.time() - start_time))
else:
output = func(*args)
return output
all_requested = get_bool_param(request, 'all', False)
store = modulestore()
with store.bulk_operations(course_key):
course = store.get_course(course_key, depth=self._required_course_depth(request, all_requested))
# Added for EDUCATOR-3660
course_key_harvard = str(course_key) == 'course-v1:HarvardX+SW12.1x+2016'
response = dict(
is_self_paced=course.self_paced,
)
if get_bool_param(request, 'sections', all_requested):
response.update(
sections=_execute_method_and_log_time(course_key_harvard, self._sections_quality, course)
)
if get_bool_param(request, 'subsections', all_requested):
response.update(
subsections=_execute_method_and_log_time(
course_key_harvard, self._subsections_quality, course, request
)
)
if get_bool_param(request, 'units', all_requested):
response.update(
units=_execute_method_and_log_time(course_key_harvard, self._units_quality, course, request)
)
if get_bool_param(request, 'videos', all_requested):
response.update(
videos=_execute_method_and_log_time(course_key_harvard, self._videos_quality, course)
)
return Response(response)
def _required_course_depth(self, request, all_requested): # lint-amnesty, pylint: disable=missing-function-docstring
if get_bool_param(request, 'units', all_requested):
# The num_blocks metric for "units" requires retrieving all blocks in the graph.
return None
elif get_bool_param(request, 'subsections', all_requested):
# The num_block_types metric for "subsections" requires retrieving all blocks in the graph.
return None
elif get_bool_param(request, 'sections', all_requested):
return 1
else:
return 0
def _sections_quality(self, course):
sections, visible_sections = self._get_sections(course)
sections_with_highlights = [section for section in visible_sections if section.highlights]
return dict(
total_number=len(sections),
total_visible=len(visible_sections),
number_with_highlights=len(sections_with_highlights),
highlights_active_for_course=course.highlights_enabled_for_messaging,
highlights_enabled=True, # used to be controlled by a waffle switch, now just always enabled
)
def _subsections_quality(self, course, request): # lint-amnesty, pylint: disable=missing-function-docstring
subsection_unit_dict = self._get_subsections_and_units(course, request)
num_block_types_per_subsection_dict = {}
for subsection_key, unit_dict in subsection_unit_dict.items():
leaf_block_types_in_subsection = (
unit_info['leaf_block_types']
for unit_info in unit_dict.values()
)
num_block_types_per_subsection_dict[subsection_key] = len(set().union(*leaf_block_types_in_subsection))
return dict(
total_visible=len(num_block_types_per_subsection_dict),
num_with_one_block_type=list(num_block_types_per_subsection_dict.values()).count(1),
num_block_types=self._stats_dict(list(num_block_types_per_subsection_dict.values())),
)
def _units_quality(self, course, request): # lint-amnesty, pylint: disable=missing-function-docstring
subsection_unit_dict = self._get_subsections_and_units(course, request)
num_leaf_blocks_per_unit = [
unit_info['num_leaf_blocks']
for unit_dict in subsection_unit_dict.values()
for unit_info in unit_dict.values()
]
return dict(
total_visible=len(num_leaf_blocks_per_unit),
num_blocks=self._stats_dict(num_leaf_blocks_per_unit),
)
def _videos_quality(self, course): # lint-amnesty, pylint: disable=missing-function-docstring
video_blocks_in_course = modulestore().get_items(course.id, qualifiers={'category': 'video'})
videos, __ = get_videos_for_course(course.id)
videos_in_val = list(videos)
video_durations = [video['duration'] for video in videos_in_val]
return dict(
total_number=len(video_blocks_in_course),
num_mobile_encoded=len(videos_in_val),
num_with_val_id=len([v for v in video_blocks_in_course if v.edx_video_id]),
durations=self._stats_dict(video_durations),
)
@classmethod
@request_cached()
def _get_subsections_and_units(cls, course, request):
"""
Returns {subsection_key: {unit_key: {num_leaf_blocks: <>, leaf_block_types: set(<>) }}}
for all visible subsections and units.
"""
_, visible_sections = cls._get_sections(course)
subsection_dict = {}
for section in visible_sections:
visible_subsections = cls._get_visible_children(section)
if get_bool_param(request, 'exclude_graded', False):
visible_subsections = [s for s in visible_subsections if not s.graded]
for subsection in visible_subsections:
unit_dict = {}
visible_units = cls._get_visible_children(subsection)
for unit in visible_units:
leaf_blocks = cls._get_leaf_blocks(unit)
unit_dict[unit.location] = dict(
num_leaf_blocks=len(leaf_blocks),
leaf_block_types={block.location.block_type for block in leaf_blocks},
)
subsection_dict[subsection.location] = unit_dict
return subsection_dict
@classmethod
@request_cached()
def _get_sections(cls, course):
return cls._get_all_children(course)
@classmethod
def _get_all_children(cls, parent): # lint-amnesty, pylint: disable=missing-function-docstring
store = modulestore()
children = [store.get_item(child_usage_key) for child_usage_key in cls._get_children(parent)]
visible_children = [
c for c in children
if not c.visible_to_staff_only and not c.hide_from_toc
]
return children, visible_children
@classmethod
def _get_visible_children(cls, parent):
_, visible_chidren = cls._get_all_children(parent)
return visible_chidren
@classmethod
def _get_children(cls, parent): # lint-amnesty, pylint: disable=missing-function-docstring
if not hasattr(parent, 'children'):
return []
else:
return parent.children
@classmethod
def _get_leaf_blocks(cls, unit): # lint-amnesty, pylint: disable=missing-function-docstring
def leaf_filter(block):
return (
block.location.block_type not in ('chapter', 'sequential', 'vertical') and
len(cls._get_children(block)) == 0
)
return list(traverse_pre_order(unit, cls._get_visible_children, leaf_filter))
def _stats_dict(self, data): # lint-amnesty, pylint: disable=missing-function-docstring
if not data:
return dict(
min=None,
max=None,
mean=None,
median=None,
mode=None,
)
else:
return dict(
min=min(data),
max=max(data),
mean=np.around(np.mean(data)),
median=np.around(np.median(data)),
mode=stats.mode(data, axis=None)[0],
)