Files
edx-platform/cms/djangoapps/contentstore/api/views/course_quality.py
Michael Terry 74887aa216 feat: turn on schedule creation by default
This commit removes several waffle toggles that have been enabled
on edx.org for years. It's time to remove the rollout gating for
these features and enable them by default.

This doesn't directly change any behavior. But it does create new
database objects by default now and allows for enabling other
schedule based features more easily.

Specifically, the following toggles were affected.

schedules.create_schedules_for_course
- Waffle flag removed as always-enabled
- We now always create a schedule when an enrollment is created

schedules.send_updates_for_course
- Waffle flag removed as always-enabled
- Course update emails are sent as long as the ScheduleConfig
  allows it.
- This is not a change in default behavior, because ScheduleConfig
  is off by default.

dynamic_pacing.studio_course_update
- Waffle switch removed as always-enabled
- Course teams can now always edit course updates directly in Studio

ScheduleConfig.create_schedules
ScheduleConfig.hold_back_ratio
- Model fields for rolling out the schedules feature
- Schedules are now always created
- This commit only removes references to these fields, they still
  exist in the database. A future commit will remove them entirely

This commit also adds a new has_highlights field to CourseOverview.
This is used to cache whether a course has highlights, used to
decide which course update email behavior they get. Previously every
enrollment had to dig into the modulestore to determine that.
2021-02-23 12:34:02 -05:00

281 lines
11 KiB
Python

# lint-amnesty, pylint: disable=missing-module-docstring
import logging
import time
import numpy as np
import six
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
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(u'[%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 six.iteritems(subsection_unit_dict):
leaf_block_types_in_subsection = (
unit_info['leaf_block_types']
for unit_info in six.itervalues(unit_dict)
)
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(six.itervalues(num_block_types_per_subsection_dict)).count(1),
num_block_types=self._stats_dict(list(six.itervalues(num_block_types_per_subsection_dict))),
)
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 six.itervalues(subsection_unit_dict)
for unit_info in six.itervalues(unit_dict)
]
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=set(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 [
block for block in # lint-amnesty, pylint: disable=unnecessary-comprehension
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][0],
)