def add_cohort(course_key, name, assignment_type): """ Add a cohort to a course. Raises ValueError if a cohort of the same name already exists. """ log.debug("Adding cohort %s to %s", name, course_key) if is_cohort_exists(course_key, name): raise ValueError(_("You cannot create two cohorts with the same name")) try: course = get_course_by_id(course_key) except Http404: raise ValueError("Invalid course_key") # lint-amnesty, pylint: disable=raise-missing-from cohort = CourseCohort.create( cohort_name=name, course_id=course.id, assignment_type=assignment_type ).course_user_group tracker.emit( "edx.cohort.creation_requested", {"cohort_name": cohort.name, "cohort_id": cohort.id} ) return cohort
def _get_course(self): """ Get course and save it in the context, so it doesn't need to be reloaded. """ if self.context.get('course') is None: self.context['course'] = get_course_by_id(self.instance.context_key) return self.context['course']
def save_display_name(apps, schema_editor): ''' Add override for `display_name` for CCX courses that don't have one yet. ''' CcxFieldOverride = apps.get_model('ccx', 'CcxFieldOverride') CustomCourseForEdX = apps.get_model('ccx', 'CustomCourseForEdX') # Build list of CCX courses that don't have an override for `display_name` yet ccx_display_name_present_ids = list( CcxFieldOverride.objects.filter(field='display_name').values_list( 'ccx__id', flat=True)) ccx_list = CustomCourseForEdX.objects.exclude( id__in=ccx_display_name_present_ids) # Create `display_name` overrides for these CCX courses for ccx in ccx_list: try: course = get_course_by_id(ccx.course_id, depth=None) except Http404: log.error( "Root course %s not found. Can't create display_name override for %s.", ccx.course_id, ccx.display_name) continue display_name = course.fields['display_name'] display_name_json = display_name.to_json(ccx.display_name) serialized_display_name = json.dumps(display_name_json) CcxFieldOverride.objects.get_or_create( ccx=ccx, location=course.location, field='display_name', defaults={'value': serialized_display_name}, )
def remove_master_course_staff_from_ccx_for_existing_ccx(apps, schema_editor): """ Remove all staff and instructors of master course from respective CCX(s). Arguments: apps (Applications): Apps in edX platform. schema_editor (SchemaEditor): For editing database schema i.e create, delete field (column) """ CustomCourseForEdX = apps.get_model("ccx", "CustomCourseForEdX") list_ccx = CustomCourseForEdX.objects.all() for ccx in list_ccx: if not ccx.course_id or ccx.course_id.deprecated: # prevent migration for deprecated course ids or invalid ids. continue ccx_locator = CCXLocator.from_course_locator(ccx.course_id, str(ccx.id)) try: course = get_course_by_id(ccx.course_id) remove_master_course_staff_from_ccx(course, ccx_locator, ccx.display_name, send_email=False) except Http404: log.warning( "Unable to remove instructors and staff of master course %s from ccx %s.", ccx.course_id, ccx_locator)
def __init__(self, course_key, request, username=''): self.request = request self.overview = course_detail( self.request, username or self.request.user.username, course_key, ) # We must compute course load access *before* setting up masquerading, # else course staff (who are not enrolled) will not be able view # their course from the perspective of a learner. self.load_access = check_course_access( self.overview, self.request.user, 'load', check_if_enrolled=True, check_if_authenticated=True, ) self.original_user_is_staff = has_access(self.request.user, 'staff', self.overview).has_access self.original_user_is_global_staff = self.request.user.is_staff self.course_key = course_key self.course = get_course_by_id(self.course_key) self.course_masquerade, self.effective_user = setup_masquerade( self.request, course_key, staff_access=self.original_user_is_staff, ) self.request.user = self.effective_user self.is_staff = has_access(self.effective_user, 'staff', self.overview).has_access self.enrollment_object = CourseEnrollment.get_enrollment(self.effective_user, self.course_key, select_related=['celebration', 'user__celebration']) self.can_view_legacy_courseware = courseware_legacy_is_visible( course_key=course_key, is_global_staff=self.original_user_is_global_staff, )
def test_post_list(self): """ Test the creation of a CCX """ outbox = self.get_outbox() data = { 'master_course_id': self.master_course_key_str, 'max_students_allowed': 111, 'display_name': 'CCX Test Title', 'coach_email': self.coach.email, 'course_modules': self.master_course_chapters[0:1] } resp = self.client.post(self.list_url, data, format='json', HTTP_AUTHORIZATION=self.auth) assert resp.status_code == status.HTTP_201_CREATED # check if the response has at least the same data of the request for key, val in data.items(): assert resp.data.get(key) == val assert 'ccx_course_id' in resp.data # check that the new CCX actually exists course_key = CourseKey.from_string(resp.data.get('ccx_course_id')) ccx_course = CustomCourseForEdX.objects.get(pk=course_key.ccx) assert str(CCXLocator.from_course_locator(ccx_course.course.id, ccx_course.id)) ==\ resp.data.get('ccx_course_id') # check that the coach user has coach role on the master course coach_role_on_master_course = CourseCcxCoachRole(self.master_course_key) assert coach_role_on_master_course.has_user(self.coach) # check that the coach has been enrolled in the ccx ccx_course_object = get_course_by_id(course_key) assert CourseEnrollment.objects.filter(course_id=ccx_course_object.id, user=self.coach).exists() # check that an email has been sent to the coach assert len(outbox) == 1 assert self.coach.email in outbox[0].recipients()
def get_group_names_by_id( course_discussion_settings: CourseDiscussionSettings ) -> Dict[str, str]: """ Creates of a dict of group_id to learner-facing group names, for the division_scheme in use as specified by course_discussion_settings. Args: course_discussion_settings: CourseDiscussionSettings model instance Returns: dict of group_id to learner-facing group names. If no division_scheme is in use, returns an empty dict. """ division_scheme = get_course_division_scheme(course_discussion_settings) course_key = course_discussion_settings.course_id if division_scheme == CourseDiscussionSettings.COHORT: return get_cohort_names(get_course_by_id(course_key)) elif division_scheme == CourseDiscussionSettings.ENROLLMENT_TRACK: # We negate the group_ids from dynamic partitions so that they will not conflict # with cohort IDs (which are an auto-incrementing integer field, starting at 1). return { -1 * group.id: group.name for group in _get_enrollment_track_groups(course_key) } else: return {}
def test_patch_detail(self): """ Test for successful patch """ outbox = self.get_outbox() # create a new coach new_coach = AdminFactory.create() data = { 'max_students_allowed': 111, 'display_name': 'CCX Title', 'coach_email': new_coach.email } resp = self.client.patch(self.detail_url, data, format='json', HTTP_AUTHORIZATION=self.auth) assert resp.status_code == status.HTTP_204_NO_CONTENT ccx_from_db = CustomCourseForEdX.objects.get(id=self.ccx.id) assert ccx_from_db.max_student_enrollments_allowed == data['max_students_allowed'] assert ccx_from_db.display_name == data['display_name'] assert ccx_from_db.coach.email == data['coach_email'] # check that the coach user has coach role on the master course coach_role_on_master_course = CourseCcxCoachRole(self.master_course_key) assert coach_role_on_master_course.has_user(new_coach) # check that the coach has been enrolled in the ccx ccx_course_object = get_course_by_id(self.ccx_key) assert CourseEnrollment.objects.filter(course_id=ccx_course_object.id, user=new_coach).exists() # check that an email has been sent to the coach assert len(outbox) == 1 assert new_coach.email in outbox[0].recipients()
def revert_ccx_staff_to_coaches(apps, schema_editor): """ Modify all staff on CCX courses so that they no longer have the staff role on the course that they coach. Arguments: apps (Applications): Apps in edX platform. schema_editor (SchemaEditor): For editing database schema (unused) """ CustomCourseForEdX = apps.get_model('ccx', 'CustomCourseForEdX') db_alias = schema_editor.connection.alias if not db_alias == 'default': return list_ccx = CustomCourseForEdX.objects.using(db_alias).all() for ccx in list_ccx: ccx_locator = CCXLocator.from_course_locator(ccx.course_id, str(ccx.id)) try: course = get_course_by_id(ccx_locator) except Http404: log.error('Could not migrate access for CCX course: %s', str(ccx_locator)) else: coach = User.objects.get(id=ccx.coach.id) allow_access(course, coach, 'ccx_coach', send_email=False) revoke_access(course, coach, 'staff', send_email=False) log.info( 'The CCX coach of CCX %s has been switched from "Staff" to "CCX Coach".', str(ccx_locator))
def change_existing_ccx_coaches_to_staff(apps, schema_editor): """ Modify all coaches of CCX courses so that they have the staff role on the CCX course they coach, but retain the CCX Coach role on the parent course. Arguments: apps (Applications): Apps in edX platform. schema_editor (SchemaEditor): For editing database schema (unused) """ CustomCourseForEdX = apps.get_model('ccx', 'CustomCourseForEdX') db_alias = schema_editor.connection.alias if not db_alias == 'default': # This migration is not intended to run against the student_module_history database and # will fail if it does. Ensure that it'll only run against the default database. return list_ccx = CustomCourseForEdX.objects.using(db_alias).all() for ccx in list_ccx: ccx_locator = CCXLocator.from_course_locator(ccx.course_id, str(ccx.id)) try: course = get_course_by_id(ccx_locator) except Http404: log.error('Could not migrate access for CCX course: %s', str(ccx_locator)) else: coach = User.objects.get(id=ccx.coach.id) allow_access(course, coach, 'staff', send_email=False) revoke_access(course, coach, 'ccx_coach', send_email=False) log.info( 'The CCX coach of CCX %s has been switched from "CCX Coach" to "Staff".', str(ccx_locator))
def _update_plugin_configuration( self, instance: DiscussionsConfiguration, validated_data: dict, ) -> DiscussionsConfiguration: """ Create/update legacy provider settings """ updated_provider_type = validated_data.get( 'provider_type') or instance.provider_type will_support_legacy = bool(updated_provider_type == 'legacy') if will_support_legacy: course_key = instance.context_key course = get_course_by_id(course_key) legacy_settings = LegacySettingsSerializer( course, context={ 'user_id': self.context['user_id'], }, data=validated_data.get('plugin_configuration', {}), ) if legacy_settings.is_valid(raise_exception=True): legacy_settings.save() instance.plugin_configuration = {} else: instance.plugin_configuration = validated_data.get( 'plugin_configuration') or {} return instance
def to_representation(self, instance: DiscussionsConfiguration) -> dict: """ Serialize data into a dictionary, to be used as a response """ course_key = instance.context_key active_provider = instance.provider_type provider_type = self.context.get('provider_type') or active_provider payload = super().to_representation(instance) course_pii_sharing_allowed = get_lti_pii_sharing_state_for_course(course_key) # LTI configuration is only stored for the active provider. if provider_type == active_provider: lti_configuration = LtiSerializer(instance=instance.lti_configuration) lti_configuration_data = lti_configuration.data plugin_configuration = instance.plugin_configuration else: lti_configuration_data = {} plugin_configuration = {} course = get_course_by_id(course_key) if provider_type in [Provider.LEGACY, Provider.OPEN_EDX]: legacy_settings = LegacySettingsSerializer(course, data=plugin_configuration) if legacy_settings.is_valid(raise_exception=True): plugin_configuration = legacy_settings.data if provider_type == Provider.OPEN_EDX: plugin_configuration.update({ "group_at_subsection": instance.plugin_configuration.get("group_at_subsection", False) }) lti_configuration_data.update({'pii_sharing_allowed': course_pii_sharing_allowed}) payload.update({ 'provider_type': provider_type, 'lti_configuration': lti_configuration_data, 'plugin_configuration': plugin_configuration, }) return payload
def __init__(self, course_key, request, username=''): self.request = request self.overview = course_detail( self.request, username or self.request.user.username, course_key, ) self.original_user_is_staff = has_access(self.request.user, 'staff', self.overview).has_access self.original_user_is_global_staff = self.request.user.is_staff self.course_key = course_key self.course = get_course_by_id(self.course_key) self.course_masquerade, self.effective_user = setup_masquerade( self.request, course_key, staff_access=self.original_user_is_staff, ) self.request.user = self.effective_user self.is_staff = has_access(self.effective_user, 'staff', self.overview).has_access self.enrollment_object = CourseEnrollment.get_enrollment( self.effective_user, self.course_key, select_related=['celebration', 'user__celebration']) self.can_view_legacy_courseware = courseware_legacy_is_visible( course_key=course_key, is_global_staff=self.original_user_is_global_staff, )
def handle(self, *args, **options): course = get_course_by_id(CourseKey.from_string(options['course'])) print( 'Warning: this command directly edits the list of course tabs in mongo.' ) print('Tabs before any changes:') print_course(course) try: if options['delete']: num = int(options['delete'][0]) if num < 3: raise CommandError("Tabs 1 and 2 cannot be changed.") if query_yes_no(f'Deleting tab {num} Confirm?', default='no'): tabs.primitive_delete(course, num - 1) # -1 for 0-based indexing elif options['insert']: num, tab_type, name = options['insert'] num = int(num) if num < 3: raise CommandError("Tabs 1 and 2 cannot be changed.") if query_yes_no( f'Inserting tab {num} "{tab_type}" "{name}" Confirm?', default='no'): tabs.primitive_insert(course, num - 1, tab_type, name) # -1 as above except ValueError as e: # Cute: translate to CommandError so the CLI error prints nicely. raise CommandError(e) # lint-amnesty, pylint: disable=raise-missing-from
def user_has_passing_grade(self): """ Returns a boolean on if the effective_user has a passing grade in the course """ if not self.effective_user.is_anonymous: course = get_course_by_id(self.course_key) user_grade = CourseGradeFactory().read(self.effective_user, course).percent return user_grade >= course.lowest_passing_grade return False
def setUp(self): """ Set up tests """ super().setUp() self.ccx = ccx = CustomCourseForEdX(course_id=self.course.id, display_name='Test CCX', coach=AdminFactory.create()) ccx.save() patch = mock.patch('lms.djangoapps.ccx.overrides.get_current_ccx') self.get_ccx = get_ccx = patch.start() get_ccx.return_value = ccx self.addCleanup(patch.stop) self.addCleanup(RequestCache.clear_all_namespaces) inject_field_overrides(iter_blocks(ccx.course), self.course, AdminFactory.create()) self.ccx_key = CCXLocator.from_course_locator(self.course.id, ccx.id) self.ccx_course = get_course_by_id(self.ccx_key, depth=None) def cleanup_provider_classes(): """ After everything is done, clean up by un-doing the change to the OverrideFieldData object that is done during the wrap method. """ OverrideFieldData.provider_classes = None self.addCleanup(cleanup_provider_classes)
def to_representation(self, instance: DiscussionsConfiguration) -> dict: """ Serialize data into a dictionary, to be used as a response """ payload = super().to_representation(instance) lti_configuration_data = {} supports_lti = instance.supports('lti') if supports_lti: lti_configuration = LtiSerializer(instance.lti_configuration) lti_configuration_data = lti_configuration.data provider_type = instance.provider_type or DEFAULT_PROVIDER_TYPE plugin_configuration = instance.plugin_configuration if provider_type == 'legacy': course_key = instance.context_key course = get_course_by_id(course_key) legacy_settings = LegacySettingsSerializer( course, data=plugin_configuration, ) if legacy_settings.is_valid(raise_exception=True): plugin_configuration = legacy_settings.data features_list = [feature.value for feature in Features] payload.update({ 'features': features_list, 'lti_configuration': lti_configuration_data, 'plugin_configuration': plugin_configuration, 'providers': { 'active': provider_type or DEFAULT_PROVIDER_TYPE, 'available': PROVIDER_FEATURE_MAP, }, }) return payload
def set_enabled(cls, course_key: CourseKey, enabled: bool, user: '******') -> bool: """ Enable/disable edxnotes in the modulestore. """ course = get_course_by_id(course_key) course.edxnotes = enabled modulestore().update_item(course, user.id) return enabled
def certificate_data(self): """ Returns certificate data if the effective_user is enrolled. Note: certificate data can be None depending on learner and/or course state. """ course = get_course_by_id(self.course_key) if self.enrollment_object: return get_cert_data(self.effective_user, course, self.enrollment_object.mode)
def set_enabled(cls, course_key: CourseKey, enabled: bool, user: '******') -> bool: """ The progress course enabled/disabled status is stored in the course module. """ course = get_course_by_id(course_key) course.hide_progress_tab = not enabled modulestore().update_item(course, user.id) return enabled
def set_enabled(cls, course_key: CourseKey, enabled: bool, user: '******') -> bool: """ Update calculator enabled status in modulestore. """ course = get_course_by_id(course_key) course.show_calculator = enabled modulestore().update_item(course, user.id) return enabled
def get_valid_course(course_id, is_ccx=False, advanced_course_check=False): """ Helper function used to validate and get a course from a course_id string. It works with both master and ccx course id. Args: course_id (str): A string representation of a Master or CCX Course ID. is_ccx (bool): Flag to perform the right validation advanced_course_check (bool): Flag to perform extra validations for the master course Returns: tuple: a tuple of course_object, course_key, error_code, http_status_code """ if course_id is None: # the ccx detail view cannot call this function with a "None" value # so the following `error_code` should be never used, but putting it # to avoid a `NameError` exception in case this function will be used # elsewhere in the future error_code = 'course_id_not_provided' if not is_ccx: log.info('Master course ID not provided') error_code = 'master_course_id_not_provided' return None, None, error_code, status.HTTP_400_BAD_REQUEST try: course_key = CourseKey.from_string(course_id) except InvalidKeyError: log.info('Course ID string "%s" is not valid', course_id) return None, None, 'course_id_not_valid', status.HTTP_400_BAD_REQUEST if not is_ccx: try: course_object = get_course_by_id(course_key) except Http404: log.info('Master Course with ID "%s" not found', course_id) return None, None, 'course_id_does_not_exist', status.HTTP_404_NOT_FOUND if advanced_course_check: if course_object.id.deprecated: return None, None, 'deprecated_master_course_id', status.HTTP_400_BAD_REQUEST if not course_object.enable_ccx: return None, None, 'ccx_not_enabled_for_master_course', status.HTTP_403_FORBIDDEN return course_object, course_key, None, None else: try: ccx_id = course_key.ccx except AttributeError: log.info('Course ID string "%s" is not a valid CCX ID', course_id) return None, None, 'course_id_not_valid_ccx_id', status.HTTP_400_BAD_REQUEST # get the master_course key master_course_key = course_key.to_course_locator() try: ccx_course = CustomCourseForEdX.objects.get( id=ccx_id, course_id=master_course_key) return ccx_course, course_key, None, None except CustomCourseForEdX.DoesNotExist: log.info('CCX Course with ID "%s" not found', course_id) return None, None, 'ccx_course_id_does_not_exist', status.HTTP_404_NOT_FOUND
def move_to_verified_cohort(sender, instance, **kwargs): # pylint: disable=unused-argument """ If the learner has changed modes, update assigned cohort iff the course is using the Automatic Verified Track Cohorting MVP feature. """ course_key = instance.course_id verified_cohort_enabled = VerifiedTrackCohortedCourse.is_verified_track_cohort_enabled( course_key) verified_cohort_name = VerifiedTrackCohortedCourse.verified_cohort_name_for_course( course_key) if verified_cohort_enabled and (instance.mode != instance._old_mode): # pylint: disable=protected-access if not is_course_cohorted(course_key): log.error( "Automatic verified cohorting enabled for course '%s', but course is not cohorted.", course_key) else: course = get_course_by_id(course_key) existing_manual_cohorts = get_course_cohorts( course, assignment_type=CourseCohort.MANUAL) if any(cohort.name == verified_cohort_name for cohort in existing_manual_cohorts): # Get a random cohort to use as the default cohort (for audit learners). # Note that calling this method will create a "Default Group" random cohort if no random # cohort yet exist. random_cohort = get_random_cohort(course_key) args = { 'course_id': str(course_key), 'user_id': instance.user.id, 'verified_cohort_name': verified_cohort_name, 'default_cohort_name': random_cohort.name } log.info( "Queuing automatic cohorting for user '%s' in course '%s' " "due to change in enrollment mode from '%s' to '%s'.", instance.user.id, course_key, instance._old_mode, instance.mode # pylint: disable=protected-access ) # Do the update with a 3-second delay in hopes that the CourseEnrollment transaction has been # completed before the celery task runs. We want a reasonably short delay in case the learner # immediately goes to the courseware. sync_cohort_with_mode.apply_async(kwargs=args, countdown=3) # In case the transaction actually was not committed before the celery task runs, # run it again after 5 minutes. If the first completed successfully, this task will be a no-op. sync_cohort_with_mode.apply_async(kwargs=args, countdown=300) else: log.error( "Automatic verified cohorting enabled for course '%s', " "but verified cohort named '%s' does not exist.", course_key, verified_cohort_name, )
def is_enabled(cls, request, course_key): """ Returns True if the user should be shown course updates for this course. """ if DISABLE_UNIFIED_COURSE_TAB_FLAG.is_enabled(course_key): return False if not CourseEnrollment.is_enrolled(request.user, course_key): return False course = get_course_by_id(course_key) return CourseUpdatesFragmentView.has_updates(request, course)
def get_legacy_discussion_settings(course_key): # lint-amnesty, pylint: disable=missing-function-docstring try: course_cohort_settings = CourseCohortsSettings.objects.get(course_id=course_key) return { 'is_cohorted': course_cohort_settings.is_cohorted, 'cohorted_discussions': course_cohort_settings.cohorted_discussions, 'always_cohort_inline_discussions': course_cohort_settings.always_cohort_inline_discussions } except CourseCohortsSettings.DoesNotExist: course = get_course_by_id(course_key) return _get_cohort_settings_from_modulestore(course)
def setUp(self): """ Set up the course and user context """ super().setUp() store = modulestore() course_items = import_course_from_xml(store, self.user.id, TEST_DATA_DIR, ['toy']) course_key = course_items[0].id self.course = get_course_by_id(course_key) self.addCleanup(set_current_request, None) self.request = get_mock_request(UserFactory.create())
def un_flag_abuse_for_comment(request, course_id, comment_id): """ given a course_id and comment id, unflag comment for abuse ajax only """ user = cc.User.from_django_user(request.user) course_key = CourseKey.from_string(course_id) course = get_course_by_id(course_key) remove_all = bool( has_permission(request.user, 'openclose_thread', course_key) or has_access(request.user, 'staff', course)) comment = cc.Comment.find(comment_id) comment.unFlagAbuse(user, comment, remove_all) return JsonResponse(prepare_content(comment.to_dict(), course_key))
def set_enabled(cls, course_key: CourseKey, enabled: bool, user: '******') -> bool: """ Enable/disable edxnotes in the modulestore. """ course = get_course_by_id(course_key) course.edxnotes = enabled if enabled: notes_tab = CourseTabList.get_tab_by_id(course.tabs, 'edxnotes') if notes_tab is None: # If the course doesn't already have the notes tab, add it. notes_tab = CourseTab.load("edxnotes") course.tabs.append(notes_tab) modulestore().update_item(course, user.id) return enabled
def un_flag_abuse_for_thread(request, course_id, thread_id): """ given a course id and thread id, remove abuse flag for this thread ajax only """ user = cc.User.from_django_user(request.user) course_key = CourseKey.from_string(course_id) course = get_course_by_id(course_key) thread = cc.Thread.find(thread_id) remove_all = bool( has_permission(request.user, 'openclose_thread', course_key) or has_access(request.user, 'staff', course)) thread.unFlagAbuse(user, thread, remove_all) return JsonResponse(prepare_content(thread.to_dict(), course_key))
def set_enabled(cls, course_key: CourseKey, enabled: bool, user: User) -> bool: """ Returns the enabled status of teams. Args: course_key (CourseKey): The course for which to set the status of teams enabled (bool): The new satus for the app. user (User): The user performing the operation Returns: (bool): the new status of the app """ course = get_course_by_id(course_key) course.teams_configuration.is_enabled = enabled modulestore().update_item(course, user.id) return enabled