def courses_custom(request): """ Render "find courses" page. The course selection work is done in courseware.courses. """ courses_list = [] programs_list = [] course_discovery_meanings = getattr(settings, 'COURSE_DISCOVERY_MEANINGS', {}) if not settings.FEATURES.get('ENABLE_COURSE_DISCOVERY'): current_date = datetime.now(utc) courses_list = get_courses( request.user, filter_={'end__gte': current_date.date() - timedelta(days=1)}) if configuration_helpers.get_value( "ENABLE_COURSE_SORTING_BY_START_DATE", settings.FEATURES["ENABLE_COURSE_SORTING_BY_START_DATE"]): courses_list = sort_by_start_date(courses_list) else: courses_list = sort_by_announcement(courses_list) # Getting all the programs from course-catalog service. The programs_list is being added to the context but it's # not being used currently in courseware/courses.html. To use this list, you need to create a custom theme that # overrides courses.html. The modifications to courses.html to display the programs will be done after the support # for edx-pattern-library is added. if configuration_helpers.get_value( "DISPLAY_PROGRAMS_ON_MARKETING_PAGES", settings.FEATURES.get("DISPLAY_PROGRAMS_ON_MARKETING_PAGES")): programs_list = get_programs_data(request.user) if request.user.is_authenticated(): add_tag_to_enrolled_courses(request.user, courses_list) for course in courses_list: course_key = SlashSeparatedCourseKey.from_deprecated_string( course.id.to_deprecated_string()) with modulestore().bulk_operations(course_key): if has_access(request.user, 'load', course): access_link = get_last_accessed_courseware( get_course_by_id(course_key, 0), request, request.user) first_chapter_url, first_section = get_course_related_keys( request, get_course_by_id(course_key, 0)) first_target = reverse('courseware_section', args=[ course.id.to_deprecated_string(), first_chapter_url, first_section ]) course.course_target = access_link if access_link != None else first_target else: course.course_target = '/courses/' + course.id.to_deprecated_string( ) return render_to_response( "courseware/courses.html", { 'courses': courses_list, 'course_discovery_meanings': course_discovery_meanings, 'programs_list': programs_list })
def confirmation(self, request, suffix=''): uid = request.params.get('uid') user = self.runtime.get_real_user(uid) try: if user and self.is_for_external_course: submissions_api.create_submission(self.student_item, {"survey_completed": True}) except Exception: LOG.info( "Error creating a submission for the survey %s related to course %s", self.survey_name, self.course_id.html_id(), ) course = get_course_by_id(self.course_id) context = { "completed_survey": self.verify_completion(), "css": self.resource_string("static/css/surveymonkey.css"), "course_link": course.other_course_settings.get("external_course_target"), } return Response( LOADER.render_template( "static/html/surveymonkey_confirmation_page.html", context))
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 = courses.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 setUp(self): """ Set up tests """ super(TestFieldOverrides, self).setUp() self.ccx = ccx = CustomCourseForEdX(course_id=self.course.id, display_name='Test CCX', coach=AdminFactory.create()) ccx.save() patch = mock.patch('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 recalculate_subsection_grade_handler(sender, **kwargs): # pylint: disable=unused-argument """ Consume the SCORE_CHANGED signal and trigger an update. This method expects that the kwargs dictionary will contain the following entries (See the definition of SCORE_CHANGED): - points_possible: Maximum score available for the exercise - points_earned: Score obtained by the user - user: User object - course_id: Unicode string representing the course - usage_id: Unicode string indicating the courseware instance """ student = kwargs['user'] course_key = CourseLocator.from_string(kwargs['course_id']) if not PersistentGradesEnabledFlag.feature_enabled(course_key): return scored_block_usage_key = UsageKey.from_string( kwargs['usage_id']).replace(course_key=course_key) collected_block_structure = get_course_in_cache(course_key) course = get_course_by_id(course_key, depth=0) subsections_to_update = collected_block_structure.get_transformer_block_field( scored_block_usage_key, GradesTransformer, 'subsections', set()) subsection_grade_factory = SubsectionGradeFactory( student, course, collected_block_structure) for subsection_usage_key in subsections_to_update: transformed_subsection_structure = get_course_blocks( student, subsection_usage_key, collected_block_structure=collected_block_structure, ) subsection_grade_factory.update( transformed_subsection_structure[subsection_usage_key], transformed_subsection_structure)
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, six.text_type(ccx.id)) try: course = get_course_by_id(ccx_locator) except Http404: log.error('Could not migrate access for CCX course: %s', six.text_type(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".', six.text_type(ccx_locator))
def recalculate_subsection_grade_handler(sender, **kwargs): # pylint: disable=unused-argument """ Consume the SCORE_CHANGED signal and trigger an update. This method expects that the kwargs dictionary will contain the following entries (See the definition of SCORE_CHANGED): - points_possible: Maximum score available for the exercise - points_earned: Score obtained by the user - user: User object - course_id: Unicode string representing the course - usage_id: Unicode string indicating the courseware instance """ student = kwargs['user'] course_key = CourseLocator.from_string(kwargs['course_id']) if not PersistentGradesEnabledFlag.feature_enabled(course_key): return scored_block_usage_key = UsageKey.from_string(kwargs['usage_id']).replace(course_key=course_key) collected_block_structure = get_course_in_cache(course_key) course = get_course_by_id(course_key, depth=0) subsections_to_update = collected_block_structure.get_transformer_block_field( scored_block_usage_key, GradesTransformer, 'subsections', set() ) for subsection_usage_key in subsections_to_update: transformed_subsection_structure = get_course_blocks( student, subsection_usage_key, collected_block_structure=collected_block_structure, ) SubsectionGradeFactory(student).update(subsection_usage_key, transformed_subsection_structure, 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, six.text_type(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 _generate(self, context): """ Generate a CSV containing all students' problem grades within a given `course_id`. """ context.update_status( 'ProblemGradeReport Step 1: Starting problem grades') course = get_course_by_id(context.course_id) header_row = OrderedDict([('id', 'Student ID'), ('email', 'Email'), ('username', 'Username')]) context.update_status( 'ProblemGradeReport Step 2: Retrieving scorable grade blocks for course' ) graded_scorable_blocks = self._graded_scorable_blocks_to_header(course) context.update_status( 'ProblemGradeReport Step 3: Setting up headers for problem grade report' ) success_headers = self._success_headers(header_row, graded_scorable_blocks) error_headers = self._error_headers(header_row) context.update_status( 'ProblemGradeReport Step 4: Retrieving problem scores for course for enrolled users' ) generated_rows = self._batched_rows(context, header_row, graded_scorable_blocks) success_rows, error_rows = self._compile(context, generated_rows) context.update_status( 'ProblemGradeReport Step 5: Uploading data to CSV report file') self._upload(context, [success_headers] + success_rows, [error_headers] + error_rows) return context.update_status( 'ProblemGradeReport Step 6: Completed problem grades report')
def generate_email(email, redirect_url): """" Generate html/plain message and send email to each user """ platform_name = configuration_helpers.get_value( 'PLATFORM_NAME', settings.PLATFORM_NAME) # course_key = CourseKey.from_string(course_id) course = get_course_by_id(email.course_id) context = { "course_name": course.display_name_with_default, "platform_name": platform_name, "redirect_url": redirect_url, "message": email.message, "sender_name": email.sender_user.profile.name.title() } from_email = configuration_helpers.get_value( 'email_from_address', settings.BULK_EMAIL_DEFAULT_FROM_EMAIL ) html_message = render_to_string( 'eol_course_email/email.txt', context) plain_message = strip_tags(html_message) email_subject = "{} - {}".format(email.subject, course.display_name_with_default) files = json.dumps(list(email.files.all().values('file_name', 'file_path', 'content_type')), default=json_util.default) for u in email.receiver_users.all(): send_email.delay( from_email, email.sender_user.email, u.email, email_subject, html_message, plain_message, files )
def recalculate_subsection_grade(user_id, course_id, usage_id): """ Updates a saved subsection grade. This method expects the following parameters: - user_id: serialized id of applicable User object - course_id: Unicode string representing the course - usage_id: Unicode string indicating the courseware instance """ course_key = CourseLocator.from_string(course_id) if not PersistentGradesEnabledFlag.feature_enabled(course_key): return student = User.objects.get(id=user_id) scored_block_usage_key = UsageKey.from_string(usage_id).replace( course_key=course_key) collected_block_structure = get_course_in_cache(course_key) course = get_course_by_id(course_key, depth=0) subsection_grade_factory = SubsectionGradeFactory( student, course, collected_block_structure) subsections_to_update = collected_block_structure.get_transformer_block_field( scored_block_usage_key, GradesTransformer, 'subsections', set()) for subsection_usage_key in subsections_to_update: transformed_subsection_structure = get_course_blocks( student, subsection_usage_key, collected_block_structure=collected_block_structure, ) subsection_grade_factory.update( transformed_subsection_structure[subsection_usage_key], transformed_subsection_structure)
def __init__(self, _xmodule_instance_args, _entry_id, course_id, _task_input, action_name): task_id = _xmodule_instance_args.get( 'task_id') if _xmodule_instance_args is not None else None self.task_info_string = ('Task: {task_id}, ' 'InstructorTask ID: {entry_id}, ' 'Course: {course_id}, ' 'Input: {task_input}').format( task_id=task_id, entry_id=_entry_id, course_id=course_id, task_input=_task_input, ) self.task_id = task_id self.entry_id = _entry_id self.task_input = _task_input self.action_name = action_name self.course_id = course_id self.report_for_verified_only = generate_grade_report_for_verified_only( ) self.task_progress = TaskProgress(self.action_name, total=None, start_time=time()) self.course = get_course_by_id(self.course_id) self.file_name = 'problem_grade_report'
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 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, six.text_type(ccx.id)) try: course = get_course_by_id(ccx_locator) except Http404: log.error('Could not migrate access for CCX course: %s', six.text_type(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".', six.text_type(ccx_locator))
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 verify_for_closed_enrollment(user, cart=None): """ A multi-output helper function. inputs: user: a user object cart: If a cart is provided it uses the same object, otherwise fetches the user's cart. Returns: is_any_course_expired: True if any of the items in the cart has it's enrollment period closed. False otherwise. expired_cart_items: List of courses with enrollment period closed. expired_cart_item_names: List of names of the courses with enrollment period closed. valid_cart_item_tuples: List of courses which are still open for enrollment. """ if cart is None: cart = Order.get_cart_for_user(user) expired_cart_items = [] expired_cart_item_names = [] valid_cart_item_tuples = [] course_ids = [] cart_items = cart.orderitem_set.all().select_subclasses() is_any_course_expired = False for cart_item in cart_items: course_key = getattr(cart_item, 'course_id', None) if course_key is not None: course_ids.append(course_key) course = get_course_by_id(course_key, depth=0) if CourseEnrollment.is_enrollment_closed(user, course): is_any_course_expired = True expired_cart_items.append(cart_item) expired_cart_item_names.append(course.display_name) else: valid_cart_item_tuples.append((cart_item, course)) return is_any_course_expired, expired_cart_items, expired_cart_item_names, valid_cart_item_tuples, course_ids
def meeting_start_email(block_id, user_email): """ Send mail to specific user at meeting start """ platform_name = configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME) usage_key = UsageKey.from_string(block_id) course = get_course_by_id(usage_key.course_key) subject = 'Ha comenzado una sesión de Zoom en el curso: {}'.format( course.display_name_with_default) redirect_url = get_lms_link_for_item(usage_key) context = { "course_name": course.display_name_with_default, "platform_name": platform_name, "redirect_url": redirect_url } html_message = render_to_string('emails/meeting_start.txt', context) plain_message = strip_tags(html_message) from_email = configuration_helpers.get_value( 'email_from_address', settings.BULK_EMAIL_DEFAULT_FROM_EMAIL) mail = send_mail(subject, plain_message, from_email, [user_email], fail_silently=False, html_message=html_message) return mail
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 has_permission(self, request, view): try: course_key = CourseKey.from_string(view.kwargs.get('course_id')) except InvalidKeyError: raise Http404() course = get_course_by_id(course_key) return has_access(request.user, 'staff', course)
def _get_course(self): """ Fetches the course using the URL kwarg. Returns: Course: Fetched course. """ return get_course_by_id(self.course_key)
def get_enrollment_info(self, user, course_id): """ Returns the User Enrollment information. """ course = get_course_by_id(course_id, depth=0) is_course_staff = bool(has_access(user, 'staff', course)) manual_enrollment_reason = 'N/A' # check the user enrollment role if user.is_staff: platform_name = configuration_helpers.get_value('platform_name', settings.PLATFORM_NAME) enrollment_role = _(u'{platform_name} Staff').format(platform_name=platform_name) elif is_course_staff: enrollment_role = _('Course Staff') else: enrollment_role = _('Student') course_enrollment = CourseEnrollment.get_enrollment(user=user, course_key=course_id) if is_course_staff: enrollment_source = _('Staff') else: # get the registration_code_redemption object if exists registration_code_redemption = RegistrationCodeRedemption.registration_code_used_for_enrollment( course_enrollment) # get the paid_course registration item if exists paid_course_reg_item = PaidCourseRegistration.get_course_item_for_user_enrollment( user=user, course_id=course_id, course_enrollment=course_enrollment ) # from where the user get here if registration_code_redemption is not None: enrollment_source = _('Used Registration Code') elif paid_course_reg_item is not None: enrollment_source = _('Credit Card - Individual') else: manual_enrollment = ManualEnrollmentAudit.get_manual_enrollment(course_enrollment) if manual_enrollment is not None: enrollment_source = _( u'manually enrolled by username: {username}' ).format(username=manual_enrollment.enrolled_by.username) manual_enrollment_reason = manual_enrollment.reason else: enrollment_source = _('Manually Enrolled') enrollment_date = course_enrollment.created.strftime(u"%B %d, %Y") currently_enrolled = course_enrollment.is_active course_enrollment_data = collections.OrderedDict() course_enrollment_data['Enrollment Date'] = enrollment_date course_enrollment_data['Currently Enrolled'] = currently_enrolled course_enrollment_data['Enrollment Source'] = enrollment_source course_enrollment_data['Manual (Un)Enrollment Reason'] = manual_enrollment_reason course_enrollment_data['Enrollment Role'] = enrollment_role return course_enrollment_data
def registration_code_details(request, course_id): """ Post handler to mark the registration code as 1) valid 2) invalid 3) Unredeem. """ course_key = CourseKey.from_string(course_id) code = request.POST.get('registration_code') action_type = request.POST.get('action_type') course = get_course_by_id(course_key, depth=0) action_type_messages = { 'invalidate_registration_code': _('This enrollment code has been canceled. It can no longer be used.'), 'unredeem_registration_code': _('This enrollment code has been marked as unused.'), 'validate_registration_code': _('The enrollment code has been restored.') } try: registration_code = CourseRegistrationCode.objects.get(code=code) except CourseRegistrationCode.DoesNotExist: return JsonResponse( { 'message': _(u'The enrollment code ({code}) was not found for the {course_name} course.' ).format(code=code, course_name=course.display_name) }, status=400) if action_type == 'invalidate_registration_code': registration_code.is_valid = False registration_code.save() if RegistrationCodeRedemption.is_registration_code_redeemed(code): code_redemption = RegistrationCodeRedemption.get_registration_code_redemption( code, course_key) delete_redemption_entry(request, code_redemption, course_key) if action_type == 'validate_registration_code': registration_code.is_valid = True registration_code.save() if action_type == 'unredeem_registration_code': code_redemption = RegistrationCodeRedemption.get_registration_code_redemption( code, course_key) if code_redemption is None: return JsonResponse( { 'message': _(u'The redemption does not exist against enrollment code ({code}).' ).format(code=code) }, status=400) delete_redemption_entry(request, code_redemption, course_key) return JsonResponse({'message': action_type_messages[action_type]})
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(u'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 = courses.get_course_by_id(course_key) except Http404: log.info(u'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(u'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(u'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. """ from lms.djangoapps.courseware.courses import get_course_by_id 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( u"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': six.text_type(course_key), 'user_id': instance.user.id, 'verified_cohort_name': verified_cohort_name, 'default_cohort_name': random_cohort.name } log.info( u"Queuing automatic cohorting for user '%s' in course '%s' " u"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( u"Automatic verified cohorting enabled for course '%s', " u"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 post(self, request): """Handle all actions from courses view""" if not request.user.is_staff: raise Http404 action = request.POST.get('action', '') track.views.server_track(request, action, {}, page='courses_sysdashboard') courses = {course.id: course for course in self.get_courses()} if action == 'add_course': gitloc = request.POST.get('repo_location', '').strip().replace(' ', '').replace(';', '') branch = request.POST.get('repo_branch', '').strip().replace(' ', '').replace(';', '') self.msg += self.get_course_from_git(gitloc, branch) elif action == 'del_course': course_id = request.POST.get('course_id', '').strip() course_key = CourseKey.from_string(course_id) course_found = False if course_key in courses: course_found = True course = courses[course_key] else: try: course = get_course_by_id(course_key) course_found = True except Exception as err: # pylint: disable=broad-except self.msg += _( HTML( u'Error - cannot get course with ID {0}<br/><pre>{1}</pre>' )).format(course_key, escape(str(err))) if course_found: # delete course that is stored with mongodb backend self.def_ms.delete_course(course.id, request.user.id) # don't delete user permission groups, though self.msg += \ HTML(u"<font color='red'>{0} {1} = {2} ({3})</font>").format( _('Deleted'), text_type(course.location), text_type(course.id), course.display_name) context = { 'datatable': self.make_datatable(list(courses.values())), 'msg': self.msg, 'djangopid': os.getpid(), 'modeflag': { 'courses': 'active-section' }, } return render_to_response(self.template_name, context)
def completion(self, request, suffix=''): context = { "course": get_course_by_id(self.course_id), "completed_survey": self.verify_completion(), "css": self.resource_string("static/css/surveymonkey.css"), "online_help_token": "online_help_token", } return Response( LOADER.render_template( "static/html/surveymonkey_completion_page.html", context))
def get_legacy_discussion_settings(course_key): 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 = courses.get_course_by_id(course_key) return _get_cohort_settings_from_modulestore(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 = courses.get_course_by_id(course_key) return _get_cohort_settings_from_modulestore(course)
def setUp(self): """ Set up the course and user context """ super(CoursesRenderTest, self).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 compute_grades_for_course(course_key, offset, batch_size, **kwargs): # pylint: disable=unused-argument """ Compute and save grades for a set of students in the specified course. The set of students will be determined by the order of enrollment date, and limited to at most <batch_size> students, starting from the specified offset. """ course = courses.get_course_by_id(CourseKey.from_string(course_key)) enrollments = CourseEnrollment.objects.filter(course_id=course.id).order_by('created') student_iter = (enrollment.user for enrollment in enrollments[offset:offset + batch_size]) for result in CourseGradeFactory().iter(users=student_iter, course=course, force_update=True): if result.error is not None: raise result.error
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, 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': unicode(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 compute_grades_for_course(course_key, offset, batch_size): """ Compute grades for a set of students in the specified course. The set of students will be determined by the order of enrollment date, and limited to at most <batch_size> students, starting from the specified offset. """ course = courses.get_course_by_id(CourseKey.from_string(course_key)) enrollments = CourseEnrollment.objects.filter(course_id=course.id).order_by('created') student_iter = (enrollment.user for enrollment in enrollments[offset:offset + batch_size]) list(CourseGradeFactory().iter( course, students=student_iter, read_only=False, ))
def upload_user_grades_csv(_xmodule_instance_args, _entry_id, course_id, _task_input, action_name): # pylint: disable=too-many-statements """ For a given `course_id`, for given usernames generates a grades CSV file, and store using a `ReportStore`. Once created, the files can be accessed by instantiating another `ReportStore` (via `ReportStore.from_config()`) and calling `link_for()` on it. Unenrolled users and unknown usernames are stored in *_err_*.csv This task is very close to the .upload_grades_csv from instructor_tasks.task_helper The difference is that we filter enrolled students against requested usernames and we push info about this into PLP """ start_time = time() start_date = datetime.now(UTC) status_interval = 100 fmt = u'Task: {task_id}, InstructorTask ID: {entry_id}, Course: {course_id}, Input: {task_input}' task_info_string = fmt.format( task_id=_xmodule_instance_args.get('task_id') if _xmodule_instance_args is not None else None, entry_id=_entry_id, course_id=course_id, task_input=_task_input ) TASK_LOG.info(u'%s, Task type: %s, Starting task execution', task_info_string, action_name) extended_kwargs_id = _task_input.get("extended_kwargs_id") extended_kwargs = InstructorTaskExtendedKwargs.get_kwargs_for_id(extended_kwargs_id) usernames = extended_kwargs.get("usernames", None) err_rows = [["id", "username", "error_msg"]] if usernames is None: message = "Error occured during edx task execution: no usersnames in InstructorTaskExtendedKwargs." TASK_LOG.error(u'%s, Task type: %s, ' + message, task_info_string) err_rows.append(["-1", "__", message]) usernames = [] enrolled_students = CourseEnrollment.objects.users_enrolled_in(course_id) enrolled_students = enrolled_students.filter(username__in=usernames) total_enrolled_students = enrolled_students.count() requester_id = _task_input.get("requester_id") task_progress = TaskProgress(action_name, total_enrolled_students, start_time) course = get_course_by_id(course_id) course_is_cohorted = is_course_cohorted(course.id) teams_enabled = course.teams_enabled cohorts_header = ['Cohort Name'] if course_is_cohorted else [] teams_header = ['Team Name'] if teams_enabled else [] experiment_partitions = get_split_user_partitions(course.user_partitions) group_configs_header = [u'Experiment Group ({})'.format(partition.name) for partition in experiment_partitions] certificate_info_header = ['Certificate Eligible', 'Certificate Delivered', 'Certificate Type'] certificate_whitelist = CertificateWhitelist.objects.filter(course_id=course_id, whitelist=True) whitelisted_user_ids = [entry.user_id for entry in certificate_whitelist] # Loop over all our students and build our CSV lists in memory rows = [] current_step = {'step': 'Calculating Grades'} TASK_LOG.info( u'%s, Task type: %s, Current step: %s, Starting grade calculation for total students: %s', task_info_string, action_name, current_step, total_enrolled_students, ) found_students = User.objects.filter(username__in=usernames) # Check invalid usernames if len(found_students)!= len(usernames): found_students_usernames = [x.username for x in found_students] for u in usernames: if u not in found_students_usernames: err_rows.append([-1, u, "invalid_username"]) # Check not enrolled requested students if found_students != enrolled_students: diff = found_students.exclude(id__in=enrolled_students) for u in diff: if u in diff: err_rows.append([u.id, u.username, "enrollment_for_username_not_found"]) total_enrolled_students = enrolled_students.count() student_counter = 0 TASK_LOG.info( u'%s, Task type: %s, Current step: %s, Starting grade calculation for total students: %s', task_info_string, action_name, current_step, total_enrolled_students ) graded_assignments = course.grading.graded_assignments(course_id) grade_header = course.grading.grade_header(graded_assignments) rows.append( ["Student ID", "Email", "Username", "Last Name", "First Name", "Second Name", "Grade", "Grade Percent"] + grade_header + cohorts_header + group_configs_header + teams_header + ['Enrollment Track', 'Verification Status'] + certificate_info_header ) for student, course_grade, err_msg in CourseGradeFactory().iter(course, enrolled_students): # Periodically update task status (this is a cache write) if task_progress.attempted % status_interval == 0: task_progress.update_task_state(extra_meta=current_step) task_progress.attempted += 1 # Now add a log entry after each student is graded to get a sense # of the task's progress student_counter += 1 TASK_LOG.info( u'%s, Task type: %s, Current step: %s, Grade calculation in-progress for students: %s/%s', task_info_string, action_name, current_step, student_counter, total_enrolled_students ) if not course_grade: # An empty course_grade means we failed to grade a student. task_progress.failed += 1 err_rows.append([student.id, student.username, err_msg]) continue # We were able to successfully grade this student for this course. task_progress.succeeded += 1 cohorts_group_name = [] if course_is_cohorted: group = get_cohort(student, course_id, assign=False) cohorts_group_name.append(group.name if group else '') group_configs_group_names = [] for partition in experiment_partitions: group = PartitionService(course_id).get_group(student, partition, assign=False) group_configs_group_names.append(group.name if group else '') team_name = [] if teams_enabled: try: membership = CourseTeamMembership.objects.get(user=student, team__course_id=course_id) team_name.append(membership.team.name) except CourseTeamMembership.DoesNotExist: team_name.append('') enrollment_mode = CourseEnrollment.enrollment_mode_for_user(student, course_id)[0] verification_status = SoftwareSecurePhotoVerification.verification_status_for_user( student, course_id, enrollment_mode ) certificate_info = certificate_info_for_user( student, course_id, course_grade.letter_grade, student.id in whitelisted_user_ids ) second_name = '' try: up = UserProfile.objects.get(user=student) if up.goals: second_name = json.loads(up.goals).get('second_name', '') except ValueError: pass if certificate_info[0] == 'Y': TASK_LOG.info( u'Student is marked eligible_for_certificate' u'(user=%s, course_id=%s, grade_percent=%s gradecutoffs=%s, allow_certificate=%s, is_whitelisted=%s)', student, course_id, course_grade.percent, course.grade_cutoffs, student.profile.allow_certificate, student.id in whitelisted_user_ids ) grade_results = course.grading.grade_results(graded_assignments, course_grade) grade_results = list(chain.from_iterable(grade_results)) rows.append( [student.id, student.email, student.username, student.last_name, student.first_name, second_name, course_grade.percent, course_grade.percent*100] + grade_results + cohorts_group_name + group_configs_group_names + team_name + [enrollment_mode] + [verification_status] + certificate_info ) TASK_LOG.info( u'%s, Task type: %s, Current step: %s, Grade calculation completed for students: %s/%s', task_info_string, action_name, current_step, student_counter, total_enrolled_students ) # By this point, we've got the rows we're going to stuff into our CSV files. current_step = {'step': 'Uploading CSVs'} task_progress.update_task_state(extra_meta=current_step) TASK_LOG.info(u'%s, Task type: %s, Current step: %s', task_info_string, action_name, current_step) # Perform the actual upload custom_grades_download = get_custom_grade_config() report_hash_unique_hash = hex(random.getrandbits(32))[2:] report_name = 'plp_grade_users_report_{}_id_{}'.format(report_hash_unique_hash, requester_id) err_report_name = 'plp_grade_users_report_err_{}_id_{}'.format(report_hash_unique_hash, requester_id) upload_csv_to_report_store(rows, report_name, course_id, start_date, config_name=custom_grades_download) # If there are any error rows (don't count the header), write them out as well has_errors = len(err_rows) > 1 if has_errors: upload_csv_to_report_store(err_rows, err_report_name, course_id, start_date, config_name=custom_grades_download) callback_url = _task_input.get("callback_url", None) if callback_url: report_store = ReportStore.from_config(config_name=custom_grades_download) files_urls_pairs = report_store.links_for(course_id) find_by_name = lambda name: [url for filename, url in files_urls_pairs if name in filename][0] try: csv_url = find_by_name(report_name) csv_err_url = find_by_name(err_report_name) if has_errors else None PlpApiClient().push_grade_api_result(callback_url, csv_url, csv_err_url) except Exception as e: TASK_LOG.error("Failed push to PLP:{}".format(str(e))) # One last update before we close out... TASK_LOG.info(u'%s, Task type: %s, Finalizing grade task', task_info_string, action_name) return task_progress.update_task_state(extra_meta=current_step)
def course_info_to_ccxcon(course_key): """ Function that gathers informations about the course and makes a post request to a CCXCon with the data. Args: course_key (CourseLocator): the master course key """ try: course = get_course_by_id(course_key) except Http404: log.error('Master Course with key "%s" not found', unicode(course_key)) return if not course.enable_ccx: log.debug('ccx not enabled for course key "%s"', unicode(course_key)) return if not course.ccx_connector: log.debug('ccx connector not defined for course key "%s"', unicode(course_key)) return if not is_valid_url(course.ccx_connector): log.error( 'ccx connector URL "%s" for course key "%s" is not a valid URL.', course.ccx_connector, unicode(course_key) ) return # get the oauth credential for this URL try: ccxcon = CCXCon.objects.get(url=course.ccx_connector) except CCXCon.DoesNotExist: log.error('ccx connector Oauth credentials not configured for URL "%s".', course.ccx_connector) return # get an oauth client with a valid token oauth_ccxcon = get_oauth_client( server_token_url=urlparse.urljoin(course.ccx_connector, CCXCON_TOKEN_URL), client_id=ccxcon.oauth_client_id, client_secret=ccxcon.oauth_client_secret ) # get the entire list of instructors course_instructors = CourseInstructorRole(course.id).users_with_role() # get anonymous ids for each of them course_instructors_ids = [anonymous_id_for_user(user, course_key) for user in course_instructors] # extract the course details course_details = CourseDetails.fetch(course_key) payload = { 'course_id': unicode(course_key), 'title': course.display_name, 'author_name': None, 'overview': course_details.overview, 'description': course_details.short_description, 'image_url': course_details.course_image_asset_path, 'instructors': course_instructors_ids } headers = {'content-type': 'application/json'} # make the POST request add_course_url = urlparse.urljoin(course.ccx_connector, CCXCON_COURSEXS_URL) resp = oauth_ccxcon.post( url=add_course_url, json=payload, headers=headers, timeout=CCXCON_REQUEST_TIMEOUT ) if resp.status_code >= 500: raise CCXConnServerError('Server returned error Status: %s, Content: %s', resp.status_code, resp.content) if resp.status_code >= 400: log.error("Error creating course on ccxcon. Status: %s, Content: %s", resp.status_code, resp.content) # this API performs a POST request both for POST and PATCH, but the POST returns 201 and the PATCH returns 200 elif resp.status_code != HTTP_200_OK and resp.status_code != HTTP_201_CREATED: log.error('Server returned unexpected status code %s', resp.status_code) else: log.debug('Request successful. Status: %s, Content: %s', resp.status_code, resp.content)
def handle(self, *args, **options): if os.path.exists(options['output']): raise CommandError("File {0} already exists".format( options['output'])) status_interval = 100 # parse out the course into a coursekey if options['course']: try: course_key = CourseKey.from_string(options['course']) # if it's not a new-style course key, parse it from an old-style # course key except InvalidKeyError: course_key = SlashSeparatedCourseKey.from_deprecated_string(options['course']) print "Fetching enrolled students for {0}".format(course_key) enrolled_students = User.objects.filter( courseenrollment__course_id=course_key ) factory = RequestMock() request = factory.get('/') total = enrolled_students.count() print "Total enrolled: {0}".format(total) course = courses.get_course_by_id(course_key) total = enrolled_students.count() start = datetime.datetime.now() rows = [] header = None print "Fetching certificate data" cert_grades = { cert.user.username: cert.grade for cert in list( GeneratedCertificate.objects.filter( # pylint: disable=no-member course_id=course_key ).prefetch_related('user') ) } print "Grading students" for count, student in enumerate(enrolled_students): count += 1 if count % status_interval == 0: # Print a status update with an approximation of # how much time is left based on how long the last # interval took diff = datetime.datetime.now() - start timeleft = diff * (total - count) / status_interval hours, remainder = divmod(timeleft.seconds, 3600) minutes, __ = divmod(remainder, 60) print "{0}/{1} completed ~{2:02}:{3:02}m remaining".format( count, total, hours, minutes) start = datetime.datetime.now() request.user = student grade = CourseGradeFactory().create(student, course) if not header: header = [section['label'] for section in grade.summary[u'section_breakdown']] rows.append(["email", "username", "certificate-grade", "grade"] + header) percents = {section['label']: section['percent'] for section in grade.summary[u'section_breakdown']} row_percents = [percents[label] for label in header] if student.username in cert_grades: rows.append( [student.email, student.username, cert_grades[student.username], grade.percent] + row_percents, ) else: rows.append([student.email, student.username, "N/A", grade.percent] + row_percents) with open(options['output'], 'wb') as f: writer = csv.writer(f) writer.writerows(rows)