# lint-amnesty, pylint: disable=missing-module-docstring import logging import dateutil from pytz import UTC from rest_framework.generics import GenericAPIView from rest_framework.response import Response from cms.djangoapps.contentstore.course_info_model import get_course_updates from cms.djangoapps.contentstore.views.certificates import CertificateManager from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, view_auth_classes from xmodule.course_metadata_utils import DEFAULT_GRADING_POLICY from xmodule.modulestore.django import modulestore from .utils import course_author_access_required, get_bool_param log = logging.getLogger(__name__) @view_auth_classes() class CourseValidationView(DeveloperErrorViewMixin, GenericAPIView): """ **Use Case** **Example Requests** GET /api/courses/v1/validation/{course_id}/ **GET Parameters** A GET request may include the following parameters. * all * dates * assignments * grades * certificates * updates * graded_only (boolean) - whether to included graded subsections only in the assignments information. * validate_oras (boolean) - whether to check the dates in ORA problems in addition to assignment due dates. **GET Response Values** The HTTP 200 response has the following values. * is_self_paced - whether the course is self-paced. * dates * has_start_date - whether the start date is set on the course. * has_end_date - whether the end date is set on the course. * assignments * total_number - total number of assignments in the course. * total_visible - number of assignments visible to learners in the course. * assignments_with_dates_before_start - assignments with due dates before the start date. * assignments_with_dates_after_end - assignments with due dates after the end date. * grades * sum_of_weights - sum of weights for all assignments in the course (valid ones should equal 1). * certificates * is_activated - whether the certificate is activated for the course. * has_certificate - whether the course has a certificate. * updates * has_update - whether at least one course update exists. """ @course_author_access_required def get(self, request, course_key): """ Returns validation information for the given course. """ 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)) response = dict( is_self_paced=course.self_paced, ) if get_bool_param(request, 'dates', all_requested): response.update( dates=self._dates_validation(course) ) if get_bool_param(request, 'assignments', all_requested): response.update( assignments=self._assignments_validation(course, request) ) if get_bool_param(request, 'grades', all_requested): response.update( grades=self._grades_validation(course) ) if get_bool_param(request, 'certificates', all_requested): response.update( certificates=self._certificates_validation(course) ) if get_bool_param(request, 'updates', all_requested): response.update( updates=self._updates_validation(course, request) ) return Response(response) def _required_course_depth(self, request, all_requested): if get_bool_param(request, 'assignments', all_requested): return 2 else: return 0 def _dates_validation(self, course): return dict( has_start_date=self._has_start_date(course), has_end_date=course.end is not None, ) def _assignments_validation(self, course, request): # lint-amnesty, pylint: disable=missing-function-docstring assignments, visible_assignments = self._get_assignments(course) assignments_with_dates = [ a for a in visible_assignments if a.due ] assignments_with_dates_before_start = ( [ {'id': str(a.location), 'display_name': a.display_name} for a in assignments_with_dates if a.due < course.start ] if self._has_start_date(course) else [] ) assignments_with_dates_after_end = ( [ {'id': str(a.location), 'display_name': a.display_name} for a in assignments_with_dates if a.due > course.end ] if course.end else [] ) if get_bool_param(request, 'graded_only', False): assignments_with_dates = [ a for a in visible_assignments if a.due and a.graded ] assignments_with_dates_before_start = ( [ {'id': str(a.location), 'display_name': a.display_name} for a in assignments_with_dates if a.due < course.start ] if self._has_start_date(course) else [] ) assignments_with_dates_after_end = ( [ {'id': str(a.location), 'display_name': a.display_name} for a in assignments_with_dates if a.due > course.end ] if course.end else [] ) assignments_with_ora_dates_before_start = [] assignments_with_ora_dates_after_end = [] if get_bool_param(request, 'validate_oras', False): # Iterate over all ORAs to find any with dates outside # acceptable range for ora in self._get_open_responses( course, get_bool_param(request, 'graded_only', False) ): if course.start and self._has_date_before_start(ora, course.start): parent_unit = modulestore().get_item(ora.parent) parent_assignment = modulestore().get_item(parent_unit.parent) assignments_with_ora_dates_before_start.append({ 'id': str(parent_assignment.location), 'display_name': parent_assignment.display_name }) if course.end and self._has_date_after_end(ora, course.end): parent_unit = modulestore().get_item(ora.parent) parent_assignment = modulestore().get_item(parent_unit.parent) assignments_with_ora_dates_after_end.append({ 'id': str(parent_assignment.location), 'display_name': parent_assignment.display_name }) return dict( total_number=len(assignments), total_visible=len(visible_assignments), assignments_with_dates_before_start=assignments_with_dates_before_start, assignments_with_dates_after_end=assignments_with_dates_after_end, assignments_with_ora_dates_before_start=assignments_with_ora_dates_before_start, assignments_with_ora_dates_after_end=assignments_with_ora_dates_after_end, ) def _grades_validation(self, course): has_grading_policy = self._has_grading_policy(course) sum_of_weights = course.grader.sum_of_weights return dict( has_grading_policy=has_grading_policy, sum_of_weights=sum_of_weights, ) def _certificates_validation(self, course): is_activated, certificates = CertificateManager.is_activated(course) certificates_enabled = certificates is not None return dict( is_activated=is_activated, has_certificate=certificates_enabled and len(certificates) > 0, is_enabled=certificates_enabled, ) def _updates_validation(self, course, request): updates_usage_key = course.id.make_usage_key('course_info', 'updates') updates = get_course_updates(updates_usage_key, provided_id=None, user_id=request.user.id) return dict( has_update=len(updates) > 0, ) def _get_assignments(self, course): # lint-amnesty, pylint: disable=missing-function-docstring store = modulestore() sections = [store.get_item(section_usage_key) for section_usage_key in course.children] assignments = [ store.get_item(assignment_usage_key) for section in sections for assignment_usage_key in section.children ] visible_sections = [ s for s in sections if not s.visible_to_staff_only and not s.hide_from_toc ] assignments_in_visible_sections = [ store.get_item(assignment_usage_key) for visible_section in visible_sections for assignment_usage_key in visible_section.children ] visible_assignments = [ a for a in assignments_in_visible_sections if not a.visible_to_staff_only ] return assignments, visible_assignments def _get_open_responses(self, course, graded_only): oras = modulestore().get_items(course.id, qualifiers={'category': 'openassessment'}) return oras if not graded_only else [ora for ora in oras if ora.graded] def _has_date_before_start(self, ora, start): # lint-amnesty, pylint: disable=missing-function-docstring if ora.submission_start: if dateutil.parser.parse(ora.submission_start).replace(tzinfo=UTC) < start: return True if ora.submission_due: if dateutil.parser.parse(ora.submission_due).replace(tzinfo=UTC) < start: return True for assessment in ora.rubric_assessments: if assessment['start']: if dateutil.parser.parse(assessment['start']).replace(tzinfo=UTC) < start: return True if assessment['due']: if dateutil.parser.parse(assessment['due']).replace(tzinfo=UTC) < start: return True return False def _has_date_after_end(self, ora, end): # lint-amnesty, pylint: disable=missing-function-docstring if ora.submission_start: if dateutil.parser.parse(ora.submission_start).replace(tzinfo=UTC) > end: return True if ora.submission_due: if dateutil.parser.parse(ora.submission_due).replace(tzinfo=UTC) > end: return True for assessment in ora.rubric_assessments: if assessment['start']: if dateutil.parser.parse(assessment['start']).replace(tzinfo=UTC) > end: return True if assessment['due']: if dateutil.parser.parse(assessment['due']).replace(tzinfo=UTC) > end: return True return False def _has_start_date(self, course): return not course.start_date_is_still_default def _has_grading_policy(self, course): # lint-amnesty, pylint: disable=missing-function-docstring grading_policy_formatted = {} default_grading_policy_formatted = {} for grader, assignment_type, weight in course.grader.subgraders: grading_policy_formatted[assignment_type] = { 'type': assignment_type, 'short_label': grader.short_label, 'min_count': grader.min_count, 'drop_count': grader.drop_count, 'weight': weight, } # the default grading policy Lab assignment type does not have a short-label, # but courses with the default grading policy do return a short-label for Lab # assignments, so we ignore the Lab short-label if 'Lab' in grading_policy_formatted: grading_policy_formatted['Lab'].pop('short_label') for assignment in DEFAULT_GRADING_POLICY['GRADER']: default_assignment_grading_policy_formatted = { 'type': assignment['type'], 'min_count': assignment['min_count'], 'drop_count': assignment['drop_count'], 'weight': assignment['weight'], } # the default grading policy Lab assignment type does not have a short-label, so only # add short_label to dictionary when the assignment has one if 'short_label' in assignment: default_assignment_grading_policy_formatted['short_label'] = assignment['short_label'] default_grading_policy_formatted[assignment['type']] = default_assignment_grading_policy_formatted # check for equality if len(grading_policy_formatted) != len(default_grading_policy_formatted): return True else: for assignment_type in grading_policy_formatted: if (assignment_type not in default_grading_policy_formatted or grading_policy_formatted[assignment_type] != default_grading_policy_formatted[assignment_type]): return True return False