def _grade(student, course, keep_raw_scores, course_structure=None): """ Unwrapped version of "grade" This grades a student as quickly as possible. It returns the output from the course grader, augmented with the final letter grade. The keys in the output are: - course: a CourseDescriptor - keep_raw_scores : if True, then value for key 'raw_scores' contains scores for every graded module More information on the format is in the docstring for CourseGrader. """ if course_structure is None: course_structure = get_course_blocks(student, course.location) grading_context_result = grading_context(course_structure) scorable_locations = [block.location for block in grading_context_result['all_graded_blocks']] with outer_atomic(): scores_client = ScoresClient.create_for_locations(course.id, student.id, scorable_locations) # Dict of item_ids -> (earned, possible) point tuples. This *only* grabs # scores that were registered with the submissions API, which for the moment # means only openassessment (edx-ora2) # We need to import this here to avoid a circular dependency of the form: # XBlock --> submissions --> Django Rest Framework error strings --> # Django translation --> ... --> courseware --> submissions from submissions import api as sub_api # installed from the edx-submissions repository with outer_atomic(): submissions_scores = sub_api.get_scores( course.id.to_deprecated_string(), anonymous_id_for_user(student, course.id) ) totaled_scores, raw_scores = _calculate_totaled_scores( student, grading_context_result, submissions_scores, scores_client, keep_raw_scores ) with outer_atomic(): # Grading policy might be overriden by a CCX, need to reset it course.set_grading_policy(course.grading_policy) grade_summary = course.grader.grade(totaled_scores, generate_random_scores=settings.GENERATE_PROFILE_SCORES) # We round the grade here, to make sure that the grade is a whole percentage and # doesn't get displayed differently than it gets grades grade_summary['percent'] = round(grade_summary['percent'] * 100 + 0.05) / 100 letter_grade = grade_for_percentage(course.grade_cutoffs, grade_summary['percent']) grade_summary['grade'] = letter_grade grade_summary['totaled_scores'] = totaled_scores # make this available, eg for instructor download & debugging if keep_raw_scores: # way to get all RAW scores out to instructor # so grader can be double-checked grade_summary['raw_scores'] = raw_scores return grade_summary
def _summary(student, course, keep_raw_scores, course_structure=None): """ This grades a student as quickly as possible. It returns the output from the course grader, augmented with the final letter grade. The keys in the output are: - course: a CourseDescriptor - keep_raw_scores : if True, then value for key 'raw_scores' contains scores for every graded module More information on the format is in the docstring for CourseGrader. """ if course_structure is None: course_structure = get_course_blocks(student, course.location) grading_context_result = grading_context(course_structure) scorable_locations = [block.location for block in grading_context_result['all_graded_blocks']] with outer_atomic(): scores_client = ScoresClient.create_for_locations(course.id, student.id, scorable_locations) # Dict of item_ids -> (earned, possible) point tuples. This *only* grabs # scores that were registered with the submissions API, which for the moment # means only openassessment (edx-ora2) # We need to import this here to avoid a circular dependency of the form: # XBlock --> submissions --> Django Rest Framework error strings --> # Django translation --> ... --> courseware --> submissions from submissions import api as sub_api # installed from the edx-submissions repository with outer_atomic(): submissions_scores = sub_api.get_scores( course.id.to_deprecated_string(), anonymous_id_for_user(student, course.id) ) totaled_scores, raw_scores = _calculate_totaled_scores( student, grading_context_result, submissions_scores, scores_client, keep_raw_scores ) with outer_atomic(): # Grading policy might be overriden by a CCX, need to reset it course.set_grading_policy(course.grading_policy) grade_summary = course.grader.grade(totaled_scores, generate_random_scores=settings.GENERATE_PROFILE_SCORES) # We round the grade here, to make sure that the grade is a whole percentage and # doesn't get displayed differently than it gets grades grade_summary['percent'] = round(grade_summary['percent'] * 100 + 0.05) / 100 letter_grade = _letter_grade(course.grade_cutoffs, grade_summary['percent']) grade_summary['grade'] = letter_grade grade_summary['totaled_scores'] = totaled_scores # make this available, eg for instructor download & debugging if keep_raw_scores: # way to get all RAW scores out to instructor # so grader can be double-checked grade_summary['raw_scores'] = raw_scores return grade_summary
def test_outer_atomic_nesting(self): """ Test that outer_atomic raises an error if it is nested inside another atomic. """ if connection.vendor != 'mysql': raise unittest.SkipTest('Only works on MySQL.') def do_nothing(): """Just return.""" return outer_atomic()(do_nothing)() with atomic(): atomic()(do_nothing)() with outer_atomic(): atomic()(do_nothing)() with self.assertRaisesRegexp(TransactionManagementError, 'Cannot be inside an atomic block.'): with atomic(): outer_atomic()(do_nothing)() with self.assertRaisesRegexp(TransactionManagementError, 'Cannot be inside an atomic block.'): with outer_atomic(): outer_atomic()(do_nothing)()
def test_outer_atomic_nesting(self): """ Test that outer_atomic raises an error if it is nested inside another atomic. """ if connection.vendor != 'mysql': raise unittest.SkipTest('Only works on MySQL.') outer_atomic()(do_nothing)() with atomic(): atomic()(do_nothing)() with outer_atomic(): atomic()(do_nothing)() with self.assertRaisesRegexp(TransactionManagementError, 'Cannot be inside an atomic block.'): with atomic(): outer_atomic()(do_nothing)() with self.assertRaisesRegexp(TransactionManagementError, 'Cannot be inside an atomic block.'): with outer_atomic(): outer_atomic()(do_nothing)()
def submit_task(request, task_type, task_class, course_key, task_input, task_key): """ Helper method to submit a task. Reserves the requested task, based on the `course_key`, `task_type`, and `task_key`, checking to see if the task is already running. The `task_input` is also passed so that it can be stored in the resulting InstructorTask entry. Arguments are extracted from the `request` provided by the originating server request. Then the task is submitted to run asynchronously, using the specified `task_class` and using the task_id constructed for it. Cannot be inside an atomic block. `AlreadyRunningError` is raised if the task is already running. """ with outer_atomic(): # check to see if task is already running, and reserve it otherwise: instructor_task = _reserve_task(course_key, task_type, task_key, task_input, request.user) # make sure all data has been committed before handing off task to celery. task_id = instructor_task.task_id task_args = [instructor_task.id, _get_xmodule_instance_args(request, task_id)] try: task_class.apply_async(task_args, task_id=task_id) except Exception as error: _handle_instructor_task_failure(instructor_task, error) return instructor_task
def submit_task(request, task_type, task_class, course_key, task_input, task_key): """ Helper method to submit a task. Reserves the requested task, based on the `course_key`, `task_type`, and `task_key`, checking to see if the task is already running. The `task_input` is also passed so that it can be stored in the resulting InstructorTask entry. Arguments are extracted from the `request` provided by the originating server request. Then the task is submitted to run asynchronously, using the specified `task_class` and using the task_id constructed for it. Cannot be inside an atomic block. `AlreadyRunningError` is raised if the task is already running. """ with outer_atomic(): # check to see if task is already running, and reserve it otherwise: instructor_task = _reserve_task(course_key, task_type, task_key, task_input, request.user) # make sure all data has been committed before handing off task to celery. task_id = instructor_task.task_id task_args = [ instructor_task.id, _get_xmodule_instance_args(request, task_id) ] try: task_class.apply_async(task_args, task_id=task_id) except Exception as error: _handle_instructor_task_failure(instructor_task, error) return instructor_task
def run_main_task(entry_id, task_fcn, action_name): """ Applies the `task_fcn` to the arguments defined in `entry_id` InstructorTask. Arguments passed to `task_fcn` are: `entry_id` : the primary key for the InstructorTask entry representing the task. `course_id` : the id for the course. `task_input` : dict containing task-specific arguments, JSON-decoded from InstructorTask's task_input. `action_name` : past-tense verb to use for constructing status messages. If no exceptions are raised, the `task_fcn` should return a dict containing the task's result with the following keys: 'attempted': number of attempts made 'succeeded': number of attempts that "succeeded" 'skipped': number of attempts that "skipped" 'failed': number of attempts that "failed" 'total': number of possible subtasks to attempt 'action_name': user-visible verb to use in status messages. Should be past-tense. Pass-through of input `action_name`. 'duration_ms': how long the task has (or had) been running. """ # Get the InstructorTask to be updated. If this fails then let the exception return to Celery. # There's no point in catching it here. with outer_atomic(): entry = InstructorTask.objects.get(pk=entry_id) entry.task_state = PROGRESS entry.save_now() # Get inputs to use in this task from the entry task_id = entry.task_id course_id = entry.course_id task_input = json.loads(entry.task_input) # Construct log message fmt = u'Task: {task_id}, InstructorTask ID: {entry_id}, Course: {course_id}, Input: {task_input}' task_info_string = fmt.format(task_id=task_id, entry_id=entry_id, course_id=course_id, task_input=task_input) TASK_LOG.info(u'%s, Starting update (nothing %s yet)', task_info_string, action_name) # Check that the task_id submitted in the InstructorTask matches the current task # that is running. request_task_id = _get_current_task().request.id if task_id != request_task_id: fmt = u'{task_info}, Requested task did not match actual task "{actual_id}"' message = fmt.format(task_info=task_info_string, actual_id=request_task_id) TASK_LOG.error(message) raise ValueError(message) # Now do the work task_progress = task_fcn(entry_id, course_id, task_input, action_name) # Release any queries that the connection has been hanging onto reset_queries() # Log and exit, returning task_progress info as task result TASK_LOG.info(u'%s, Task type: %s, Finishing task: %s', task_info_string, action_name, task_progress) return task_progress
def save(self, *args, **kwargs): self.full_clean(validate_unique=False) log.info("Saving CohortMembership for user '%s' in '%s'", self.user.id, self.course_id) # Avoid infinite recursion if creating from get_or_create() call below. # This block also allows middleware to use CohortMembership.get_or_create without worrying about outer_atomic if 'force_insert' in kwargs and kwargs['force_insert'] is True: with transaction.atomic(): self.course_user_group.users.add(self.user) super(CohortMembership, self).save(*args, **kwargs) return # This block will transactionally commit updates to CohortMembership and underlying course_user_groups. # Note the use of outer_atomic, which guarantees that operations are committed to the database on block exit. # If called from a view method, that method must be marked with @transaction.non_atomic_requests. with outer_atomic(read_committed=True): saved_membership, created = CohortMembership.objects.select_for_update( ).get_or_create(user__id=self.user.id, course_id=self.course_id, defaults={ 'course_user_group': self.course_user_group, 'user': self.user }) # If the membership was newly created, all the validation and course_user_group logic was settled # with a call to self.save(force_insert=True), which gets handled above. if created: return if saved_membership.course_user_group == self.course_user_group: raise ValueError( "User {user_name} already present in cohort {cohort_name}". format(user_name=self.user.username, cohort_name=self.course_user_group.name)) self.previous_cohort = saved_membership.course_user_group self.previous_cohort_name = saved_membership.course_user_group.name self.previous_cohort_id = saved_membership.course_user_group.id self.previous_cohort.users.remove(self.user) saved_membership.course_user_group = self.course_user_group self.course_user_group.users.add(self.user) super(CohortMembership, saved_membership).save(update_fields=['course_user_group'])
def save(self, *args, **kwargs): self.full_clean(validate_unique=False) # Avoid infinite recursion if creating from get_or_create() call below. # This block also allows middleware to use CohortMembership.get_or_create without worrying about outer_atomic if 'force_insert' in kwargs and kwargs['force_insert'] is True: with transaction.atomic(): self.course_user_group.users.add(self.user) self.course_user_group.save() super(CohortMembership, self).save(*args, **kwargs) return # This block will transactionally commit updates to CohortMembership and underlying course_user_groups. # Note the use of outer_atomic, which guarantees that operations are committed to the database on block exit. # If called from a view method, that method must be marked with @transaction.non_atomic_requests. with outer_atomic(read_committed=True): saved_membership, created = CohortMembership.objects.select_for_update().get_or_create( user__id=self.user.id, course_id=self.course_id, defaults={ 'course_user_group': self.course_user_group, 'user': self.user } ) # If the membership was newly created, all the validation and course_user_group logic was settled # with a call to self.save(force_insert=True), which gets handled above. if created: return if saved_membership.course_user_group == self.course_user_group: raise ValueError("User {user_name} already present in cohort {cohort_name}".format( user_name=self.user.username, cohort_name=self.course_user_group.name )) self.previous_cohort = saved_membership.course_user_group self.previous_cohort_name = saved_membership.course_user_group.name self.previous_cohort_id = saved_membership.course_user_group.id self.previous_cohort.users.remove(self.user) self.previous_cohort.save() saved_membership.course_user_group = self.course_user_group self.course_user_group.users.add(self.user) self.course_user_group.save() super(CohortMembership, saved_membership).save(update_fields=['course_user_group'])
def has_passed(request, course_id, section_url_name): """ Returns True if the student has higher or equeal grades in asssignment type. """ student = request.user # Get the course by ID course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) course = get_course_with_access(student, 'load', course_key, depth=None) # Get the grade summary with outer_atomic(): field_data_cache = grades.field_data_cache_for_grading(course, student) scores_client = ScoresClient.from_field_data_cache(field_data_cache) grade_summary = grades.grade(student, request, course, field_data_cache=field_data_cache, scores_client=scores_client) # Get assignment type wise percent assignments = {} for section in grade_summary['section_breakdown']: if section.get('prominent', False): assignments.update({section['category']: section['percent']}) # Get the section assignment type section_assignment_type = '' for chapter in course.get_children(): for sequenctial in chapter.get_children(): if sequenctial.url_name == section_url_name: section_assignment_type = sequenctial.format break # Get section assignment percent percentage = assignments.get(section_assignment_type, 0.0) # Return passing status return percentage * 100 == 100
def test_outer_atomic_nesting(self): """ Test that outer_atomic raises an error if it is nested inside another atomic. """ if connection.vendor != "mysql": raise unittest.SkipTest("Only works on MySQL.") outer_atomic()(do_nothing)() with atomic(): atomic()(do_nothing)() with outer_atomic(): atomic()(do_nothing)() with self.assertRaisesRegexp(TransactionManagementError, "Cannot be inside an atomic block."): with atomic(): outer_atomic()(do_nothing)() with self.assertRaisesRegexp(TransactionManagementError, "Cannot be inside an atomic block."): with outer_atomic(): outer_atomic()(do_nothing)()
def queue_subtasks_for_query( entry, action_name, create_subtask_fcn, item_querysets, item_fields, items_per_task, total_num_items, ): """ Generates and queues subtasks to each execute a chunk of "items" generated by a queryset. Arguments: `entry` : the InstructorTask object for which subtasks are being queued. `action_name` : a past-tense verb that can be used for constructing readable status messages. `create_subtask_fcn` : a function of two arguments that constructs the desired kind of subtask object. Arguments are the list of items to be processed by this subtask, and a SubtaskStatus object reflecting initial status (and containing the subtask's id). `item_querysets` : a list of query sets that define the "items" that should be passed to subtasks. `item_fields` : the fields that should be included in the dict that is returned. These are in addition to the 'pk' field. `items_per_task` : maximum size of chunks to break each query chunk into for use by a subtask. `total_num_items` : total amount of items that will be put into subtasks Returns: the task progress as stored in the InstructorTask object. """ task_id = entry.task_id # Calculate the number of tasks that will be created, and create a list of ids for each task. total_num_subtasks = _get_number_of_subtasks(total_num_items, items_per_task) subtask_id_list = [str(uuid4()) for _ in range(total_num_subtasks)] # Update the InstructorTask with information about the subtasks we've defined. TASK_LOG.info( u"Task %s: updating InstructorTask %s with subtask info for %s subtasks to process %s items.", task_id, entry.id, total_num_subtasks, total_num_items, ) # Make sure this is committed to database before handing off subtasks to celery. with outer_atomic(): progress = initialize_subtask_info(entry, action_name, total_num_items, subtask_id_list) # Construct a generator that will return the recipients to use for each subtask. # Pass in the desired fields to fetch for each recipient. item_list_generator = _generate_items_for_subtask( item_querysets, item_fields, total_num_items, items_per_task, total_num_subtasks, entry.course_id, ) # Now create the subtasks, and start them running. TASK_LOG.info( u"Task %s: creating %s subtasks to process %s items.", task_id, total_num_subtasks, total_num_items, ) num_subtasks = 0 for item_list in item_list_generator: subtask_id = subtask_id_list[num_subtasks] num_subtasks += 1 subtask_status = SubtaskStatus.create(subtask_id) new_subtask = create_subtask_fcn(item_list, subtask_status) TASK_LOG.info( u"Queueing BulkEmail Task: %s Subtask: %s at timestamp: %s", task_id, subtask_id, datetime.now()) new_subtask.apply_async() # Subtasks have been queued so no exceptions should be raised after this point. # Return the task progress as stored in the InstructorTask object. return progress
def queue_subtasks_for_query( entry, action_name, create_subtask_fcn, item_querysets, item_fields, items_per_task, total_num_items, ): """ Generates and queues subtasks to each execute a chunk of "items" generated by a queryset. Arguments: `entry` : the InstructorTask object for which subtasks are being queued. `action_name` : a past-tense verb that can be used for constructing readable status messages. `create_subtask_fcn` : a function of two arguments that constructs the desired kind of subtask object. Arguments are the list of items to be processed by this subtask, and a SubtaskStatus object reflecting initial status (and containing the subtask's id). `item_querysets` : a list of query sets that define the "items" that should be passed to subtasks. `item_fields` : the fields that should be included in the dict that is returned. These are in addition to the 'pk' field. `items_per_task` : maximum size of chunks to break each query chunk into for use by a subtask. `total_num_items` : total amount of items that will be put into subtasks Returns: the task progress as stored in the InstructorTask object. """ task_id = entry.task_id # Calculate the number of tasks that will be created, and create a list of ids for each task. total_num_subtasks = _get_number_of_subtasks(total_num_items, items_per_task) subtask_id_list = [str(uuid4()) for _ in range(total_num_subtasks)] # Update the InstructorTask with information about the subtasks we've defined. TASK_LOG.info( "Task %s: updating InstructorTask %s with subtask info for %s subtasks to process %s items.", task_id, entry.id, total_num_subtasks, total_num_items, ) # Make sure this is committed to database before handing off subtasks to celery. with outer_atomic(): progress = initialize_subtask_info(entry, action_name, total_num_items, subtask_id_list) # Construct a generator that will return the recipients to use for each subtask. # Pass in the desired fields to fetch for each recipient. item_list_generator = _generate_items_for_subtask( item_querysets, item_fields, total_num_items, items_per_task, total_num_subtasks, entry.course_id, ) # Now create the subtasks, and start them running. TASK_LOG.info( "Task %s: creating %s subtasks to process %s items.", task_id, total_num_subtasks, total_num_items, ) num_subtasks = 0 for item_list in item_list_generator: subtask_id = subtask_id_list[num_subtasks] num_subtasks += 1 subtask_status = SubtaskStatus.create(subtask_id) new_subtask = create_subtask_fcn(item_list, subtask_status) new_subtask.apply_async() # Subtasks have been queued so no exceptions should be raised after this point. # Return the task progress as stored in the InstructorTask object. return progress
class SubmitPhotosView(View): """ End-point for submitting photos for verification. """ @method_decorator(transaction.non_atomic_requests) def dispatch(self, *args, **kwargs): # pylint: disable=missing-docstring return super(SubmitPhotosView, self).dispatch(*args, **kwargs) @method_decorator(login_required) @method_decorator(outer_atomic(read_committed=True)) def post(self, request): """ Submit photos for verification. This end-point is used for the following cases: * Initial verification through the pay-and-verify flow. * Initial verification initiated from a checkpoint within a course. * Re-verification initiated from a checkpoint within a course. POST Parameters: face_image (str): base64-encoded image data of the user's face. photo_id_image (str): base64-encoded image data of the user's photo ID. full_name (str): The user's full name, if the user is requesting a name change as well. course_key (str): Identifier for the course, if initiated from a checkpoint. checkpoint (str): Location of the checkpoint in the course. """ # If the user already has an initial verification attempt, we can re-use the photo ID # the user submitted with the initial attempt. initial_verification = SoftwareSecurePhotoVerification.get_initial_verification( request.user) # Validate the POST parameters params, response = self._validate_parameters( request, bool(initial_verification)) if response is not None: return response # If necessary, update the user's full name if "full_name" in params: response = self._update_full_name(request.user, params["full_name"]) if response is not None: return response # Retrieve the image data # Validation ensures that we'll have a face image, but we may not have # a photo ID image if this is a reverification. face_image, photo_id_image, response = self._decode_image_data( params["face_image"], params.get("photo_id_image")) # If we have a photo_id we do not want use the initial verification image. if photo_id_image is not None: initial_verification = None if response is not None: return response # Submit the attempt attempt = self._submit_attempt(request.user, face_image, photo_id_image, initial_verification) self._fire_event(request.user, "edx.bi.verify.submitted", {"category": "verification"}) self._send_confirmation_email(request.user) return JsonResponse({}) def _validate_parameters(self, request, has_initial_verification): """ Check that the POST parameters are valid. Arguments: request (HttpRequest): The request object. has_initial_verification (bool): Whether the user has an initial verification attempt. Returns: HttpResponse or None """ # Pull out the parameters we care about. params = { param_name: request.POST[param_name] for param_name in ["face_image", "photo_id_image", "course_key", "full_name"] if param_name in request.POST } # If the user already has an initial verification attempt, then we don't # require the user to submit a photo ID image, since we can re-use the photo ID # image from the initial attempt. # If we don't have an initial verification OR a photo ID image, something has gone # terribly wrong in the JavaScript. Log this as an error so we can track it down. if "photo_id_image" not in params and not has_initial_verification: log.error( ("User %s does not have an initial verification attempt " "and no photo ID image data was provided. " "This most likely means that the JavaScript client is not " "correctly constructing the request to submit photos."), request.user.id) return None, HttpResponseBadRequest( _("Photo ID image is required if the user does not have an initial verification attempt." )) # The face image is always required. if "face_image" not in params: msg = _("Missing required parameter face_image") return None, HttpResponseBadRequest(msg) # If provided, parse the course key and checkpoint location if "course_key" in params: try: params["course_key"] = CourseKey.from_string( params["course_key"]) except InvalidKeyError: return None, HttpResponseBadRequest(_("Invalid course key")) return params, None def _update_full_name(self, user, full_name): """ Update the user's full name. Arguments: user (User): The user to update. full_name (unicode): The user's updated full name. Returns: HttpResponse or None """ try: update_account_settings(user, {"name": full_name}) except UserNotFound: return HttpResponseBadRequest(_("No profile found for user")) except AccountValidationError: msg = _("Name must be at least {min_length} characters long." ).format(min_length=NAME_MIN_LENGTH) return HttpResponseBadRequest(msg) def _decode_image_data(self, face_data, photo_id_data=None): """ Decode image data sent with the request. Arguments: face_data (str): base64-encoded face image data. Keyword Arguments: photo_id_data (str): base64-encoded photo ID image data. Returns: tuple of (str, str, HttpResponse) """ try: # Decode face image data (used for both an initial and re-verification) face_image = decode_image_data(face_data) # Decode the photo ID image data if it's provided photo_id_image = (decode_image_data(photo_id_data) if photo_id_data is not None else None) return face_image, photo_id_image, None except InvalidImageData: msg = _("Image data is not valid.") return None, None, HttpResponseBadRequest(msg) def _submit_attempt(self, user, face_image, photo_id_image=None, initial_verification=None): """ Submit a verification attempt. Arguments: user (User): The user making the attempt. face_image (str): Decoded face image data. Keyword Arguments: photo_id_image (str or None): Decoded photo ID image data. initial_verification (SoftwareSecurePhotoVerification): The initial verification attempt. """ attempt = SoftwareSecurePhotoVerification(user=user) # We will always have face image data, so upload the face image attempt.upload_face_image(face_image) # If an ID photo wasn't submitted, re-use the ID photo from the initial attempt. # Earlier validation rules ensure that at least one of these is available. if photo_id_image is not None: attempt.upload_photo_id_image(photo_id_image) elif initial_verification is None: # Earlier validation should ensure that we never get here. log.error( "Neither a photo ID image or initial verification attempt provided. " "Parameter validation in the view should prevent this from happening!" ) # Submit the attempt attempt.mark_ready() attempt.submit(copy_id_photo_from=initial_verification) return attempt def _send_confirmation_email(self, user): """ Send an email confirming that the user submitted photos for initial verification. """ context = { 'full_name': user.profile.name, 'platform_name': configuration_helpers.get_value("PLATFORM_NAME", settings.PLATFORM_NAME) } subject = _("Verification photos received") message = render_to_string('emails/photo_submission_confirmation.txt', context) from_address = configuration_helpers.get_value( 'email_from_address', settings.DEFAULT_FROM_EMAIL) to_address = user.email try: send_mail(subject, message, from_address, [to_address], fail_silently=False) except: # pylint: disable=bare-except # We catch all exceptions and log them. # It would be much, much worse to roll back the transaction due to an uncaught # exception than to skip sending the notification email. log.exception( "Could not send notification email for initial verification for user %s", user.id) def _fire_event(self, user, event_name, parameters): """ Fire an analytics event. Arguments: user (User): The user who submitted photos. event_name (str): Name of the analytics event. parameters (dict): Event parameters. Returns: None """ if settings.LMS_SEGMENT_KEY: tracking_context = tracker.get_tracker().resolve_context() context = { 'ip': tracking_context.get('ip'), 'Google Analytics': { 'clientId': tracking_context.get('client_id') } } analytics.track(user.id, event_name, parameters, context=context)
def prepare_sections_with_grade(request, course): ''' Create sections with grade details. Return format: { 'sections': [ { 'display_name': name, # in case of cohorts or any other accessibility settings 'hidden': hidden, 'url_name': url_name, 'units': UNITS, 'rank': rank, 'badge': bagde status, 'points': grade points, 'podium': podium status, 'week': section_index + 1, }, ], } where UNITS is a list [ { 'display_name': name, 'position': unit position in section, 'css_class': css class, } , ... ] sections with name 'hidden' are skipped. NOTE: assumes that if we got this far, user has access to course. Returns [] if this is not the case. ''' # Set the student to request user student = request.user # Get the field data cache field_data_cache = FieldDataCache.cache_for_descriptor_descendents( course.id, student, course, depth=2, ) # Get the course module with modulestore().bulk_operations(course.id): course_module = get_module_for_descriptor( student, request, course, field_data_cache, course.id, course=course ) if course_module is None: return [] # Get the field data cache staff_user = User.objects.filter(is_staff=1)[0] staff_field_data_cache = FieldDataCache.cache_for_descriptor_descendents( course.id, staff_user, course, depth=2, ) # Get the course module with modulestore().bulk_operations(course.id): staff_course_module = get_module_for_descriptor( staff_user, request, course, staff_field_data_cache, course.id, course=course ) # staff accessible chapters staff_chapters = staff_course_module.get_display_items() # find the passing grade for the course nonzero_cutoffs = [cutoff for cutoff in course.grade_cutoffs.values() if cutoff > 0] success_cutoff = min(nonzero_cutoffs) if nonzero_cutoffs else 0 # find the course progress progress = get_course_progress(student, course.id) # prepare a list of discussions participated by user discussions_participated = get_discussions_participated( request, course.id.to_deprecated_string(), student.id ) # get courseware summary with outer_atomic(): field_data_cache = grades.field_data_cache_for_grading(course, student) scores_client = ScoresClient.from_field_data_cache(field_data_cache) courseware_summary = grades.progress_summary( student, request, course, field_data_cache=field_data_cache, scores_client=scores_client ) section_grades = {} for section in courseware_summary: earned = 0 total = 0 for sub_section in section['sections']: earned += sub_section['section_total'].earned total += sub_section['section_total'].possible section_score = earned / total if earned > 0 and total > 0 else 0 section_grades[section['url_name']] = { 'earned': earned, 'total': total, 'css_class': ('text-red', 'text-green')[int(section_score >= 0.6)] if total > 0 else '' } # Check for content which needs to be completed # before the rest of the content is made available required_content = milestones_helpers.get_required_content(course, student) # Check for gated content gated_content = gating_api.get_gated_content(course, student) # The user may not actually have to complete the entrance exam, if one is required if not user_must_complete_entrance_exam(request, student, course): required_content = [content for content in required_content if not content == course.entrance_exam_id] # define inner function def create_module(descriptor): '''creates an XModule instance given a descriptor''' return get_module_for_descriptor( student, request, descriptor, field_data_cache, course.id, course=course ) with outer_atomic(): submissions_scores = sub_api.get_scores( course.id.to_deprecated_string(), anonymous_id_for_user(student, course.id) ) max_scores_cache = grades.MaxScoresCache.create_for_course(course) max_scores_cache.fetch_from_remote(field_data_cache.scorable_locations) sections = list() student_chapters = course_module.get_display_items() urlname_chapters = {} for student_chap in student_chapters: urlname_chapters.update({student_chap.url_name:student_chap}) final_chapters = OrderedDict() for chapter_index, chapter in enumerate(staff_chapters): fin_chap = urlname_chapters.get(chapter.url_name) if fin_chap: final_chapters.update({str(chapter_index+1):{'hidden': False, 'chapter':fin_chap}}) else: final_chapters.update({str(chapter_index+1):{'hidden':True}}) for section_index, chapter_info in final_chapters.items(): # Mark as hidden and Skip the current chapter if a hide flag is tripped if chapter_info['hidden']: sections.append({ 'hidden': True, 'week': "WEEK {week}: ".format(week=section_index), 'points': { 'total': 0, 'earned': 0, 'css_class': 'text-disabled' }, }) continue chapter = chapter_info['chapter'] # get the points section_points = section_grades.get(chapter.url_name, {}) units = list() for sequential in chapter.get_display_items(): # Set hidden status of the sequential if it is gated/hidden from the user hidden = ( gated_content and unicode(sequential.location) in gated_content or sequential.hide_from_toc ) if hidden: continue for index, unit in enumerate(sequential.get_display_items()): css_class = 'dark-gray' if unit.graded: total_excercises = 0 attempted_excercises = 0 unit_max_score = 0 unit_score = 0 for component in unit.get_display_items(): if component.category == 'problem': if component.graded: total_excercises += 1 attempted_excercises += is_attempted_internal( str(component.location), progress ) (correct, total) = grades.get_score( student, component, create_module, scores_client, submissions_scores, max_scores_cache, ) unit_max_score += total unit_score += correct if total_excercises: css_class = 'blue' if attempted_excercises == total_excercises: css_class = 'green' if unit_max_score and unit_score / unit_max_score < success_cutoff: css_class = 'red' position = index + 1 # For jumping to the unit directly unit_context = { 'display_name': unit.display_name_with_default_escaped, 'position': position, 'css_class': css_class, 'courseware_url': reverse( 'courseware_position', args=[ course.id, chapter.url_name, sequential.url_name, position ] ) } units.append(unit_context) competency = None if int(section_points.get('total')): competency = int(section_points.get('earned')) == int(section_points.get('total')) section_context = { 'display_name': chapter.display_name_with_default_escaped, 'url_name': chapter.url_name, 'hidden': False, 'rank': 1, 'competency': competency, 'points': { 'total': int(section_points.get('total')), 'earned': int(section_points.get('earned')), 'css_class': section_points.get('css_class') }, 'participation': discussions_participated.get(chapter.url_name), 'units': units, 'week': "WEEK {week}: ".format(week=section_index), } sections.append(section_context) return sections
class TransactionManagersTestCase(TransactionTestCase): """ Tests commit_on_success and outer_atomic. Note: This TestCase only works with MySQL. To test do: "./manage.py lms --settings=test_with_mysql test util.tests.test_db" """ DECORATORS = { 'outer_atomic': outer_atomic(), 'outer_atomic_read_committed': outer_atomic(read_committed=True), 'commit_on_success': commit_on_success(), 'commit_on_success_read_committed': commit_on_success(read_committed=True), } @ddt.data( ('outer_atomic', IntegrityError, None, True), ('outer_atomic_read_committed', type(None), False, True), ('commit_on_success', IntegrityError, None, True), ('commit_on_success_read_committed', type(None), False, True), ) @ddt.unpack def test_concurrent_requests(self, transaction_decorator_name, exception_class, created_in_1, created_in_2): """ Test that when isolation level is set to READ COMMITTED get_or_create() for the same row in concurrent requests does not raise an IntegrityError. """ transaction_decorator = self.DECORATORS[transaction_decorator_name] if connection.vendor != 'mysql': raise unittest.SkipTest('Only works on MySQL.') class RequestThread(threading.Thread): """ A thread which runs a dummy view.""" def __init__(self, delay, **kwargs): super(RequestThread, self).__init__(**kwargs) self.delay = delay self.status = {} @transaction_decorator def run(self): """A dummy view.""" try: try: User.objects.get(username='******', email='*****@*****.**') except User.DoesNotExist: pass else: raise AssertionError( 'Did not raise User.DoesNotExist.') if self.delay > 0: time.sleep(self.delay) __, created = User.objects.get_or_create( username='******', email='*****@*****.**') except Exception as exception: # pylint: disable=broad-except self.status['exception'] = exception else: self.status['created'] = created thread1 = RequestThread(delay=1) thread2 = RequestThread(delay=0) thread1.start() thread2.start() thread2.join() thread1.join() self.assertIsInstance(thread1.status.get('exception'), exception_class) self.assertEqual(thread1.status.get('created'), created_in_1) self.assertIsNone(thread2.status.get('exception')) self.assertEqual(thread2.status.get('created'), created_in_2) def test_outer_atomic_nesting(self): """ Test that outer_atomic raises an error if it is nested inside another atomic. """ if connection.vendor != 'mysql': raise unittest.SkipTest('Only works on MySQL.') outer_atomic()(do_nothing)() with atomic(): atomic()(do_nothing)() with outer_atomic(): atomic()(do_nothing)() with self.assertRaisesRegexp(TransactionManagementError, 'Cannot be inside an atomic block.'): with atomic(): outer_atomic()(do_nothing)() with self.assertRaisesRegexp(TransactionManagementError, 'Cannot be inside an atomic block.'): with outer_atomic(): outer_atomic()(do_nothing)() def test_commit_on_success_nesting(self): """ Test that commit_on_success raises an error if it is nested inside atomic or if the isolation level is changed when it is nested inside another commit_on_success. """ # pylint: disable=not-callable if connection.vendor != 'mysql': raise unittest.SkipTest('Only works on MySQL.') commit_on_success(read_committed=True)(do_nothing)() with self.assertRaisesRegexp( TransactionManagementError, 'Cannot change isolation level when nested.'): with commit_on_success(): commit_on_success(read_committed=True)(do_nothing)() with self.assertRaisesRegexp(TransactionManagementError, 'Cannot be inside an atomic block.'): with atomic(): commit_on_success(read_committed=True)(do_nothing)() def test_named_outer_atomic_nesting(self): """ Test that a named outer_atomic raises an error only if nested in enable_named_outer_atomic and inside another atomic. """ if connection.vendor != 'mysql': raise unittest.SkipTest('Only works on MySQL.') outer_atomic(name='abc')(do_nothing)() with atomic(): outer_atomic(name='abc')(do_nothing)() with enable_named_outer_atomic('abc'): outer_atomic(name='abc')(do_nothing)() # Not nested. with atomic(): outer_atomic(name='pqr')(do_nothing)() # Not enabled. with self.assertRaisesRegexp(TransactionManagementError, 'Cannot be inside an atomic block.'): with atomic(): outer_atomic(name='abc')(do_nothing)() with enable_named_outer_atomic('abc', 'def'): outer_atomic(name='def')(do_nothing)() # Not nested. with atomic(): outer_atomic(name='pqr')(do_nothing)() # Not enabled. with self.assertRaisesRegexp(TransactionManagementError, 'Cannot be inside an atomic block.'): with atomic(): outer_atomic(name='def')(do_nothing)() with self.assertRaisesRegexp(TransactionManagementError, 'Cannot be inside an atomic block.'): with outer_atomic(): outer_atomic(name='def')(do_nothing)() with self.assertRaisesRegexp(TransactionManagementError, 'Cannot be inside an atomic block.'): with atomic(): outer_atomic(name='abc')(do_nothing)() with self.assertRaisesRegexp(TransactionManagementError, 'Cannot be inside an atomic block.'): with outer_atomic(): outer_atomic(name='abc')(do_nothing)()
def _calculate_totaled_scores( student, grading_context_result, submissions_scores, scores_client, keep_raw_scores, ): """ Returns a tuple of totaled scores and raw scores, which can be passed to the grader. """ raw_scores = [] totaled_scores = {} for section_format, sections in grading_context_result['all_graded_sections'].iteritems(): format_scores = [] for section_info in sections: section = section_info['section_block'] section_name = block_metadata_utils.display_name_with_default(section) with outer_atomic(): # Check to # see if any of our locations are in the scores from the submissions # API. If scores exist, we have to calculate grades for this section. should_grade_section = any( unicode(descendant.location) in submissions_scores for descendant in section_info['scored_descendants'] ) if not should_grade_section: should_grade_section = any( descendant.location in scores_client for descendant in section_info['scored_descendants'] ) # If we haven't seen a single problem in the section, we don't have # to grade it at all! We can assume 0% if should_grade_section: scores = [] for descendant in section_info['scored_descendants']: (correct, total) = get_score( student, descendant, scores_client, submissions_scores, ) if correct is None and total is None: continue if settings.GENERATE_PROFILE_SCORES: # for debugging! if total > 1: correct = random.randrange(max(total - 2, 1), total + 1) else: correct = total graded = descendant.graded if not total > 0: # We simply cannot grade a problem that is 12/0, because we might need it as a percentage graded = False scores.append( Score( correct, total, graded, block_metadata_utils.display_name_with_default_escaped(descendant), descendant.location ) ) __, graded_total = graders.aggregate_scores(scores, section_name) if keep_raw_scores: raw_scores += scores else: graded_total = Score(0.0, 1.0, True, section_name, None) # Add the graded total to totaled_scores if graded_total.possible > 0: format_scores.append(graded_total) else: log.info( "Unable to grade a section with a total possible score of zero. " + str(section.location) ) totaled_scores[section_format] = format_scores return totaled_scores, raw_scores
def test_named_outer_atomic_nesting(self): """ Test that a named outer_atomic raises an error only if nested in enable_named_outer_atomic and inside another atomic. """ if connection.vendor != "mysql": raise unittest.SkipTest("Only works on MySQL.") outer_atomic(name="abc")(do_nothing)() with atomic(): outer_atomic(name="abc")(do_nothing)() with enable_named_outer_atomic("abc"): outer_atomic(name="abc")(do_nothing)() # Not nested. with atomic(): outer_atomic(name="pqr")(do_nothing)() # Not enabled. with self.assertRaisesRegexp(TransactionManagementError, "Cannot be inside an atomic block."): with atomic(): outer_atomic(name="abc")(do_nothing)() with enable_named_outer_atomic("abc", "def"): outer_atomic(name="def")(do_nothing)() # Not nested. with atomic(): outer_atomic(name="pqr")(do_nothing)() # Not enabled. with self.assertRaisesRegexp(TransactionManagementError, "Cannot be inside an atomic block."): with atomic(): outer_atomic(name="def")(do_nothing)() with self.assertRaisesRegexp(TransactionManagementError, "Cannot be inside an atomic block."): with outer_atomic(): outer_atomic(name="def")(do_nothing)() with self.assertRaisesRegexp(TransactionManagementError, "Cannot be inside an atomic block."): with atomic(): outer_atomic(name="abc")(do_nothing)() with self.assertRaisesRegexp(TransactionManagementError, "Cannot be inside an atomic block."): with outer_atomic(): outer_atomic(name="abc")(do_nothing)()
def _progress_summary(student, course, course_structure=None): """ Unwrapped version of "progress_summary". This pulls a summary of all problems in the course. Returns - courseware_summary is a summary of all sections with problems in the course. It is organized as an array of chapters, each containing an array of sections, each containing an array of scores. This contains information for graded and ungraded problems, and is good for displaying a course summary with due dates, etc. - None if the student does not have access to load the course module. Arguments: student: A User object for the student to grade course: A Descriptor containing the course to grade """ if course_structure is None: course_structure = get_course_blocks(student, course.location) if not len(course_structure): return None scorable_locations = [block_key for block_key in course_structure if possibly_scored(block_key)] with outer_atomic(): scores_client = ScoresClient.create_for_locations(course.id, student.id, scorable_locations) # We need to import this here to avoid a circular dependency of the form: # XBlock --> submissions --> Django Rest Framework error strings --> # Django translation --> ... --> courseware --> submissions from submissions import api as sub_api # installed from the edx-submissions repository with outer_atomic(): submissions_scores = sub_api.get_scores( unicode(course.id), anonymous_id_for_user(student, course.id) ) # Check for gated content gated_content = gating_api.get_gated_content(course, student) chapters = [] locations_to_weighted_scores = {} for chapter_key in course_structure.get_children(course_structure.root_block_usage_key): chapter = course_structure[chapter_key] sections = [] for section_key in course_structure.get_children(chapter_key): if unicode(section_key) in gated_content: continue section = course_structure[section_key] graded = getattr(section, 'graded', False) scores = [] for descendant_key in course_structure.post_order_traversal( filter_func=possibly_scored, start_node=section_key, ): descendant = course_structure[descendant_key] (correct, total) = get_score( student, descendant, scores_client, submissions_scores, ) if correct is None and total is None: continue weighted_location_score = Score( correct, total, graded, block_metadata_utils.display_name_with_default_escaped(descendant), descendant.location ) scores.append(weighted_location_score) locations_to_weighted_scores[descendant.location] = weighted_location_score escaped_section_name = block_metadata_utils.display_name_with_default_escaped(section) section_total, _ = graders.aggregate_scores(scores, escaped_section_name) sections.append({ 'display_name': escaped_section_name, 'url_name': block_metadata_utils.url_name_for_block(section), 'scores': scores, 'section_total': section_total, 'format': getattr(section, 'format', ''), 'due': getattr(section, 'due', None), 'graded': graded, }) chapters.append({ 'course': course.display_name_with_default_escaped, 'display_name': block_metadata_utils.display_name_with_default_escaped(chapter), 'url_name': block_metadata_utils.url_name_for_block(chapter), 'sections': sections }) return ProgressSummary(chapters, locations_to_weighted_scores, course_structure.get_children)
def _calculate_totaled_scores( student, grading_context_result, submissions_scores, scores_client, keep_raw_scores, ): """ Returns the totaled scores, which can be passed to the grader. """ raw_scores = [] totaled_scores = {} for section_format, sections in grading_context_result['all_graded_sections'].iteritems(): format_scores = [] for section_info in sections: section = section_info['section_block'] section_name = block_metadata_utils.display_name_with_default(section) with outer_atomic(): # Check to # see if any of our locations are in the scores from the submissions # API. If scores exist, we have to calculate grades for this section. should_grade_section = any( unicode(descendant.location) in submissions_scores for descendant in section_info['scored_descendants'] ) if not should_grade_section: should_grade_section = any( descendant.location in scores_client for descendant in section_info['scored_descendants'] ) # If we haven't seen a single problem in the section, we don't have # to grade it at all! We can assume 0% if should_grade_section: scores = [] for descendant in section_info['scored_descendants']: (correct, total) = get_score( student, descendant, scores_client, submissions_scores, ) if correct is None and total is None: continue if settings.GENERATE_PROFILE_SCORES: # for debugging! if total > 1: correct = random.randrange(max(total - 2, 1), total + 1) else: correct = total graded = descendant.graded if not total > 0: # We simply cannot grade a problem that is 12/0, because we might need it as a percentage graded = False scores.append( Score( correct, total, graded, block_metadata_utils.display_name_with_default_escaped(descendant), descendant.location ) ) __, graded_total = graders.aggregate_scores(scores, section_name) if keep_raw_scores: raw_scores += scores else: graded_total = Score(0.0, 1.0, True, section_name, None) # Add the graded total to totaled_scores if graded_total.possible > 0: format_scores.append(graded_total) else: log.info( "Unable to grade a section with a total possible score of zero. " + str(section.location) ) totaled_scores[section_format] = format_scores return totaled_scores, raw_scores
def create_account_with_params(request, params): """ Given a request and a dict of parameters (which may or may not have come from the request), create an account for the requesting user, including creating a comments service user object and sending an activation email. This also takes external/third-party auth into account, updates that as necessary, and authenticates the user for the request's session. Does not return anything. Raises AccountValidationError if an account with the username or email specified by params already exists, or ValidationError if any of the given parameters is invalid for any other reason. Issues with this code: * It is non-transactional except where explicitly wrapped in atomic to alleviate deadlocks and improve performance. This means failures at different places in registration can leave users in inconsistent states. * Third-party auth passwords are not verified. There is a comment that they are unused, but it would be helpful to have a sanity check that they are sane. * The user-facing text is rather unfriendly (e.g. "Username must be a minimum of two characters long" rather than "Please use a username of at least two characters"). * Duplicate email raises a ValidationError (rather than the expected AccountValidationError). Duplicate username returns an inconsistent user message (i.e. "An account with the Public Username '{username}' already exists." rather than "It looks like {username} belongs to an existing account. Try again with a different username.") The two checks occur at different places in the code; as a result, registering with both a duplicate username and email raises only a ValidationError for email only. """ # Copy params so we can modify it; we can't just do dict(params) because if # params is request.POST, that results in a dict containing lists of values params = dict(params.items()) # allow to define custom set of required/optional/hidden fields via configuration extra_fields = configuration_helpers.get_value( 'REGISTRATION_EXTRA_FIELDS', getattr(settings, 'REGISTRATION_EXTRA_FIELDS', {}) ) # registration via third party (Google, Facebook) using mobile application # doesn't use social auth pipeline (no redirect uri(s) etc involved). # In this case all related info (required for account linking) # is sent in params. # `third_party_auth_credentials_in_api` essentially means 'request # is made from mobile application' third_party_auth_credentials_in_api = 'provider' in params is_third_party_auth_enabled = third_party_auth.is_enabled() if is_third_party_auth_enabled and (pipeline.running(request) or third_party_auth_credentials_in_api): params["password"] = generate_password() # in case user is registering via third party (Google, Facebook) and pipeline has expired, show appropriate # error message if is_third_party_auth_enabled and ('social_auth_provider' in params and not pipeline.running(request)): raise ValidationError( {'session_expired': [ _(u"Registration using {provider} has timed out.").format( provider=params.get('social_auth_provider')) ]} ) do_external_auth, eamap = pre_account_creation_external_auth(request, params) extended_profile_fields = configuration_helpers.get_value('extended_profile_fields', []) # Can't have terms of service for certain SHIB users, like at Stanford registration_fields = getattr(settings, 'REGISTRATION_EXTRA_FIELDS', {}) tos_required = ( registration_fields.get('terms_of_service') != 'hidden' or registration_fields.get('honor_code') != 'hidden' ) and ( not settings.FEATURES.get("AUTH_USE_SHIB") or not settings.FEATURES.get("SHIB_DISABLE_TOS") or not do_external_auth or not eamap.external_domain.startswith(settings.SHIBBOLETH_DOMAIN_PREFIX) ) form = AccountCreationForm( data=params, extra_fields=extra_fields, extended_profile_fields=extended_profile_fields, do_third_party_auth=do_external_auth, tos_required=tos_required, ) custom_form = get_registration_extension_form(data=params) # Perform operations within a transaction that are critical to account creation with outer_atomic(read_committed=True): # first, create the account (user, profile, registration) = do_create_account(form, custom_form) third_party_provider, running_pipeline = _link_user_to_third_party_provider( is_third_party_auth_enabled, third_party_auth_credentials_in_api, user, request, params, ) new_user = authenticate_new_user(request, user.username, params['password']) django_login(request, new_user) request.session.set_expiry(0) post_account_creation_external_auth(do_external_auth, eamap, new_user) # Check if system is configured to skip activation email for the current user. skip_email = _skip_activation_email( user, do_external_auth, running_pipeline, third_party_provider, ) if skip_email: registration.activate() else: compose_and_send_activation_email(user, profile, registration) # Perform operations that are non-critical parts of account creation create_or_set_user_attribute_created_on_site(user, request.site) preferences_api.set_user_preference(user, LANGUAGE_KEY, get_language()) if settings.FEATURES.get('ENABLE_DISCUSSION_EMAIL_DIGEST'): try: enable_notifications(user) except Exception: # pylint: disable=broad-except log.exception("Enable discussion notifications failed for user {id}.".format(id=user.id)) dog_stats_api.increment("common.student.account_created") _track_user_registration(user, profile, params, third_party_provider) # Announce registration REGISTER_USER.send(sender=None, user=user, registration=registration) create_comments_service_user(user) try: _record_registration_attributions(request, new_user) # Don't prevent a user from registering due to attribution errors. except Exception: # pylint: disable=broad-except log.exception('Error while attributing cookies to user registration.') # TODO: there is no error checking here to see that the user actually logged in successfully, # and is not yet an active user. if new_user is not None: AUDIT_LOG.info(u"Login success on new account creation - {0}".format(new_user.username)) return new_user
def _external_login_or_signup(request, external_id, external_domain, credentials, email, fullname, retfun=None): """ Generic external auth login or signup """ # pylint: disable=too-many-statements # see if we have a map from this external_id to an edX username eamap_defaults = { 'external_credentials': json.dumps(credentials), 'external_email': email, 'external_name': fullname, 'internal_password': generate_password() } # We are not guaranteed to be in a transaction here since some upstream views # use non_atomic_requests with outer_atomic(): eamap, created = ExternalAuthMap.objects.get_or_create( external_id=external_id, external_domain=external_domain, defaults=eamap_defaults) if created: log.debug(u'Created eamap=%s', eamap) else: log.debug(u'Found eamap=%s', eamap) log.info(u"External_Auth login_or_signup for %s : %s : %s : %s", external_domain, external_id, email, fullname) uses_shibboleth = settings.FEATURES.get( 'AUTH_USE_SHIB') and external_domain.startswith( SHIBBOLETH_DOMAIN_PREFIX) uses_certs = settings.FEATURES.get('AUTH_USE_CERTIFICATES') internal_user = eamap.user if internal_user is None: if uses_shibboleth: # If we are using shib, try to link accounts # For Stanford shib, the email the idp returns is actually under the control of the user. # Since the id the idps return is not user-editable, and is of the from "*****@*****.**", # use the id to link accounts instead. try: with outer_atomic(): link_user = User.objects.get(email=eamap.external_id) if not ExternalAuthMap.objects.filter( user=link_user).exists(): # if there's no pre-existing linked eamap, we link the user eamap.user = link_user eamap.save() internal_user = link_user log.info(u'SHIB: Linking existing account for %s', eamap.external_id) # now pass through to log in else: # otherwise, there must have been an error, b/c we've already linked a user with these external # creds failure_msg = _( "You have already created an account using " "an external login like WebAuth or Shibboleth. " "Please contact {tech_support_email} for support." ).format(tech_support_email=get_value( 'email_from_address', settings.TECH_SUPPORT_EMAIL), ) return default_render_failure(request, failure_msg) except User.DoesNotExist: log.info(u'SHIB: No user for %s yet, doing signup', eamap.external_email) return _signup(request, eamap, retfun) else: log.info(u'No user for %s yet. doing signup', eamap.external_email) return _signup(request, eamap, retfun) # We trust shib's authentication, so no need to authenticate using the password again uname = internal_user.username if uses_shibboleth: user = internal_user # Assuming this 'AUTHENTICATION_BACKENDS' is set in settings, which I think is safe if settings.AUTHENTICATION_BACKENDS: auth_backend = settings.AUTHENTICATION_BACKENDS[0] else: auth_backend = 'ratelimitbackend.backends.RateLimitModelBackend' user.backend = auth_backend if settings.FEATURES['SQUELCH_PII_IN_LOGS']: AUDIT_LOG.info( u'Linked user.id: {0} logged in via Shibboleth'.format( user.id)) else: AUDIT_LOG.info( u'Linked user "{0}" logged in via Shibboleth'.format( user.email)) elif uses_certs: # Certificates are trusted, so just link the user and log the action user = internal_user user.backend = 'ratelimitbackend.backends.RateLimitModelBackend' if settings.FEATURES['SQUELCH_PII_IN_LOGS']: AUDIT_LOG.info( u'Linked user_id {0} logged in via SSL certificate'.format( user.id)) else: AUDIT_LOG.info( u'Linked user "{0}" logged in via SSL certificate'.format( user.email)) else: user = authenticate(username=uname, password=eamap.internal_password, request=request) if user is None: # we want to log the failure, but don't want to log the password attempted: if settings.FEATURES['SQUELCH_PII_IN_LOGS']: AUDIT_LOG.warning(u'External Auth Login failed') else: AUDIT_LOG.warning( u'External Auth Login failed for "{0}"'.format(uname)) return _signup(request, eamap, retfun) if not user.is_active: if settings.FEATURES.get('BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH'): # if BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH, we trust external auth and activate any users # that aren't already active user.is_active = True user.save() if settings.FEATURES['SQUELCH_PII_IN_LOGS']: AUDIT_LOG.info( u'Activating user {0} due to external auth'.format( user.id)) else: AUDIT_LOG.info( u'Activating user "{0}" due to external auth'.format( uname)) else: if settings.FEATURES['SQUELCH_PII_IN_LOGS']: AUDIT_LOG.warning( u'User {0} is not active after external login'.format( user.id)) else: AUDIT_LOG.warning( u'User "{0}" is not active after external login'.format( uname)) # TODO: improve error page msg = 'Account not yet activated: please look for link in your email' return default_render_failure(request, msg) login(request, user) request.session.set_expiry(0) if settings.FEATURES['SQUELCH_PII_IN_LOGS']: AUDIT_LOG.info(u"Login success - user.id: {0}".format(user.id)) else: AUDIT_LOG.info(u"Login success - {0} ({1})".format( user.username, user.email)) if retfun is None: return redirect('/') return retfun()
def _progress_summary(student, request, course, field_data_cache=None, scores_client=None): """ Unwrapped version of "progress_summary". This pulls a summary of all problems in the course. Returns - courseware_summary is a summary of all sections with problems in the course. It is organized as an array of chapters, each containing an array of sections, each containing an array of scores. This contains information for graded and ungraded problems, and is good for displaying a course summary with due dates, etc. Arguments: student: A User object for the student to grade course: A Descriptor containing the course to grade If the student does not have access to load the course module, this function will return None. """ with outer_atomic(): if field_data_cache is None: field_data_cache = field_data_cache_for_grading(course, student) if scores_client is None: scores_client = ScoresClient.from_field_data_cache(field_data_cache) course_module = get_module_for_descriptor( student, request, course, field_data_cache, course.id, course=course ) if not course_module: return None course_module = getattr(course_module, '_x_module', course_module) # We need to import this here to avoid a circular dependency of the form: # XBlock --> submissions --> Django Rest Framework error strings --> # Django translation --> ... --> courseware --> submissions from submissions import api as sub_api # installed from the edx-submissions repository with outer_atomic(): submissions_scores = sub_api.get_scores( course.id.to_deprecated_string(), anonymous_id_for_user(student, course.id) ) max_scores_cache = MaxScoresCache.create_for_course(course) # For the moment, we have to get scorable_locations from field_data_cache # and not from scores_client, because scores_client is ignorant of things # in the submissions API. As a further refactoring step, submissions should # be hidden behind the ScoresClient. max_scores_cache.fetch_from_remote(field_data_cache.scorable_locations) chapters = [] locations_to_children = defaultdict(list) locations_to_weighted_scores = {} # Don't include chapters that aren't displayable (e.g. due to error) for chapter_module in course_module.get_display_items(): # Skip if the chapter is hidden if chapter_module.hide_from_toc: continue sections = [] for section_module in chapter_module.get_display_items(): # Skip if the section is hidden with outer_atomic(): if section_module.hide_from_toc: continue graded = section_module.graded scores = [] module_creator = section_module.xmodule_runtime.get_module for module_descriptor in yield_dynamic_descriptor_descendants( section_module, student.id, module_creator ): locations_to_children[module_descriptor.parent].append(module_descriptor.location) (correct, total) = get_score( student, module_descriptor, module_creator, scores_client, submissions_scores, max_scores_cache, ) if correct is None and total is None: continue weighted_location_score = Score( correct, total, graded, module_descriptor.display_name_with_default, module_descriptor.location ) scores.append(weighted_location_score) locations_to_weighted_scores[module_descriptor.location] = weighted_location_score scores.reverse() section_total, _ = graders.aggregate_scores( scores, section_module.display_name_with_default) module_format = section_module.format if section_module.format is not None else '' sections.append({ 'display_name': section_module.display_name_with_default, 'url_name': section_module.url_name, 'scores': scores, 'section_total': section_total, 'format': module_format, 'due': section_module.due, 'graded': graded, }) chapters.append({ 'course': course.display_name_with_default, 'display_name': chapter_module.display_name_with_default, 'url_name': chapter_module.url_name, 'sections': sections }) max_scores_cache.push_to_remote() return ProgressSummary(chapters, locations_to_weighted_scores, locations_to_children)
def _grade(student, request, course, keep_raw_scores, field_data_cache, scores_client): """ Unwrapped version of "grade" This grades a student as quickly as possible. It returns the output from the course grader, augmented with the final letter grade. The keys in the output are: course: a CourseDescriptor - grade : A final letter grade. - percent : The final percent for the class (rounded up). - section_breakdown : A breakdown of each section that makes up the grade. (For display) - grade_breakdown : A breakdown of the major components that make up the final grade. (For display) - keep_raw_scores : if True, then value for key 'raw_scores' contains scores for every graded module More information on the format is in the docstring for CourseGrader. """ with outer_atomic(): if field_data_cache is None: field_data_cache = field_data_cache_for_grading(course, student) if scores_client is None: scores_client = ScoresClient.from_field_data_cache(field_data_cache) # Dict of item_ids -> (earned, possible) point tuples. This *only* grabs # scores that were registered with the submissions API, which for the moment # means only openassessment (edx-ora2) # We need to import this here to avoid a circular dependency of the form: # XBlock --> submissions --> Django Rest Framework error strings --> # Django translation --> ... --> courseware --> submissions from submissions import api as sub_api # installed from the edx-submissions repository with outer_atomic(): submissions_scores = sub_api.get_scores( course.id.to_deprecated_string(), anonymous_id_for_user(student, course.id) ) max_scores_cache = MaxScoresCache.create_for_course(course) # For the moment, we have to get scorable_locations from field_data_cache # and not from scores_client, because scores_client is ignorant of things # in the submissions API. As a further refactoring step, submissions should # be hidden behind the ScoresClient. max_scores_cache.fetch_from_remote(field_data_cache.scorable_locations) grading_context = course.grading_context raw_scores = [] totaled_scores = {} # This next complicated loop is just to collect the totaled_scores, which is # passed to the grader for section_format, sections in grading_context['graded_sections'].iteritems(): format_scores = [] for section in sections: section_descriptor = section['section_descriptor'] section_name = section_descriptor.display_name_with_default with outer_atomic(): # some problems have state that is updated independently of interaction # with the LMS, so they need to always be scored. (E.g. combinedopenended ORA1) # TODO This block is causing extra savepoints to be fired that are empty because no queries are executed # during the loop. When refactoring this code please keep this outer_atomic call in mind and ensure we # are not making unnecessary database queries. should_grade_section = any( descriptor.always_recalculate_grades for descriptor in section['xmoduledescriptors'] ) # If there are no problems that always have to be regraded, check to # see if any of our locations are in the scores from the submissions # API. If scores exist, we have to calculate grades for this section. if not should_grade_section: should_grade_section = any( descriptor.location.to_deprecated_string() in submissions_scores for descriptor in section['xmoduledescriptors'] ) if not should_grade_section: should_grade_section = any( descriptor.location in scores_client for descriptor in section['xmoduledescriptors'] ) # If we haven't seen a single problem in the section, we don't have # to grade it at all! We can assume 0% if should_grade_section: scores = [] def create_module(descriptor): '''creates an XModule instance given a descriptor''' # TODO: We need the request to pass into here. If we could forego that, our arguments # would be simpler return get_module_for_descriptor( student, request, descriptor, field_data_cache, course.id, course=course ) descendants = yield_dynamic_descriptor_descendants(section_descriptor, student.id, create_module) for module_descriptor in descendants: user_access = has_access( student, 'load', module_descriptor, module_descriptor.location.course_key ) if not user_access: continue (correct, total) = get_score( student, module_descriptor, create_module, scores_client, submissions_scores, max_scores_cache, ) if correct is None and total is None: continue if settings.GENERATE_PROFILE_SCORES: # for debugging! if total > 1: correct = random.randrange(max(total - 2, 1), total + 1) else: correct = total graded = module_descriptor.graded if not total > 0: # We simply cannot grade a problem that is 12/0, because we might need it as a percentage graded = False scores.append( Score( correct, total, graded, module_descriptor.display_name_with_default, module_descriptor.location ) ) __, graded_total = graders.aggregate_scores(scores, section_name) if keep_raw_scores: raw_scores += scores else: graded_total = Score(0.0, 1.0, True, section_name, None) #Add the graded total to totaled_scores if graded_total.possible > 0: format_scores.append(graded_total) else: log.info( "Unable to grade a section with a total possible score of zero. " + str(section_descriptor.location) ) totaled_scores[section_format] = format_scores with outer_atomic(): # Grading policy might be overriden by a CCX, need to reset it course.set_grading_policy(course.grading_policy) grade_summary = course.grader.grade(totaled_scores, generate_random_scores=settings.GENERATE_PROFILE_SCORES) # We round the grade here, to make sure that the grade is an whole percentage and # doesn't get displayed differently than it gets grades grade_summary['percent'] = round(grade_summary['percent'] * 100 + 0.05) / 100 letter_grade = grade_for_percentage(course.grade_cutoffs, grade_summary['percent']) grade_summary['grade'] = letter_grade grade_summary['totaled_scores'] = totaled_scores # make this available, eg for instructor download & debugging if keep_raw_scores: # way to get all RAW scores out to instructor # so grader can be double-checked grade_summary['raw_scores'] = raw_scores max_scores_cache.push_to_remote() return grade_summary
class ChooseModeView(View): """View used when the user is asked to pick a mode. When a get request is used, shows the selection page. When a post request is used, assumes that it is a form submission from the selection page, parses the response, and then sends user to the next step in the flow. """ @method_decorator(transaction.non_atomic_requests) def dispatch(self, *args, **kwargs): """Disable atomicity for the view. Otherwise, we'd be unable to commit to the database until the request had concluded; Django will refuse to commit when an atomic() block is active, since that would break atomicity. """ return super(ChooseModeView, self).dispatch(*args, **kwargs) @method_decorator(tpa_hint_ends_existing_session) @method_decorator(login_required) @method_decorator(transaction.atomic) def get(self, request, course_id, error=None): """Displays the course mode choice page. Args: request (`Request`): The Django Request object. course_id (unicode): The slash-separated course key. Keyword Args: error (unicode): If provided, display this error message on the page. Returns: Response """ course_key = CourseKey.from_string(course_id) # Check whether the user has access to this course # based on country access rules. embargo_redirect = embargo_api.redirect_if_blocked( course_key, user=request.user, ip_address=get_ip(request), url=request.path) if embargo_redirect: return redirect(embargo_redirect) enrollment_mode, is_active = CourseEnrollment.enrollment_mode_for_user( request.user, course_key) modes = CourseMode.modes_for_course_dict(course_key) ecommerce_service = EcommerceService() # We assume that, if 'professional' is one of the modes, it should be the *only* mode. # If there are both modes, default to non-id-professional. has_enrolled_professional = ( CourseMode.is_professional_slug(enrollment_mode) and is_active) if CourseMode.has_professional_mode( modes) and not has_enrolled_professional: purchase_workflow = request.GET.get("purchase_workflow", "single") verify_url = reverse('verify_student_start_flow', kwargs={'course_id': unicode(course_key)}) redirect_url = "{url}?purchase_workflow={workflow}".format( url=verify_url, workflow=purchase_workflow) if ecommerce_service.is_enabled(request.user): professional_mode = modes.get( CourseMode.NO_ID_PROFESSIONAL_MODE) or modes.get( CourseMode.PROFESSIONAL) if purchase_workflow == "single" and professional_mode.sku: redirect_url = ecommerce_service.get_checkout_page_url( professional_mode.sku) if purchase_workflow == "bulk" and professional_mode.bulk_sku: redirect_url = ecommerce_service.get_checkout_page_url( professional_mode.bulk_sku) return redirect(redirect_url) # If there isn't a verified mode available, then there's nothing # to do on this page. Send the user to the dashboard. if not CourseMode.has_verified_mode(modes): # If the learner has arrived at this screen via the traditional enrollment workflow, # then they should already be enrolled in an audit mode for the course, assuming one has # been configured. However, alternative enrollment workflows have been introduced into the # system, such as third-party discovery. These workflows result in learners arriving # directly at this screen, and they will not necessarily be pre-enrolled in the audit mode. # In this particular case, Audit is the ONLY option available, and thus we need to ensure # that the learner is truly enrolled before we redirect them away to the dashboard. if len(modes) == 1 and modes.get(CourseMode.AUDIT): CourseEnrollment.enroll(request.user, course_key, CourseMode.AUDIT) return redirect( self._get_redirect_url_for_audit_enrollment( request, course_id)) return redirect(reverse('dashboard')) # If a user has already paid, redirect them to the dashboard. if is_active and (enrollment_mode in CourseMode.VERIFIED_MODES + [CourseMode.NO_ID_PROFESSIONAL_MODE]): return redirect(reverse('dashboard')) donation_for_course = request.session.get("donation_for_course", {}) chosen_price = donation_for_course.get(unicode(course_key), None) course = modulestore().get_course(course_key) if CourseEnrollment.is_enrollment_closed(request.user, course): locale = to_locale(get_language()) enrollment_end_date = format_datetime(course.enrollment_end, 'short', locale=locale) params = urllib.urlencode({'course_closed': enrollment_end_date}) return redirect('{0}?{1}'.format(reverse('dashboard'), params)) # When a credit mode is available, students will be given the option # to upgrade from a verified mode to a credit mode at the end of the course. # This allows students who have completed photo verification to be eligible # for univerity credit. # Since credit isn't one of the selectable options on the track selection page, # we need to check *all* available course modes in order to determine whether # a credit mode is available. If so, then we show slightly different messaging # for the verified track. has_credit_upsell = any( CourseMode.is_credit_mode(mode) for mode in CourseMode.modes_for_course(course_key, only_selectable=False)) course_id = course_key.to_deprecated_string() context = { "course_modes_choose_url": reverse("course_modes_choose", kwargs={'course_id': course_id}), "modes": modes, "has_credit_upsell": has_credit_upsell, "course_name": course.display_name_with_default_escaped, "course_org": course.display_org_with_default, "course_num": course.display_number_with_default, "chosen_price": chosen_price, "error": error, "responsive": True, "nav_hidden": True, } title_content = _( "Congratulations! You are now enrolled in {course_name}").format( course_name=course.display_name_with_default_escaped) enterprise_learner_data = enterprise_api.get_enterprise_learner_data( site=request.site, user=request.user) if enterprise_learner_data: enterprise_learner = enterprise_learner_data[0] is_course_in_enterprise_catalog = enterprise_api.is_course_in_enterprise_catalog( site=request.site, course_id=course_id, enterprise_catalog_id=enterprise_learner['enterprise_customer'] ['catalog']) if is_course_in_enterprise_catalog: partner_names = partner_name = course.display_organization \ if course.display_organization else course.org enterprise_name = enterprise_learner['enterprise_customer'][ 'name'] organizations = organization_api.get_course_organizations( course_id=course.id) if organizations: partner_names = ' and '.join([ org.get('name', partner_name) for org in organizations ]) title_content = _( "Welcome, {username}! You are about to enroll in {course_name}," " from {partner_names}, sponsored by {enterprise_name}. Please select your enrollment" " information below.").format( username=request.user.username, course_name=course.display_name_with_default_escaped, partner_names=partner_names, enterprise_name=enterprise_name) # Hide the audit modes for this enterprise customer, if necessary if not enterprise_learner['enterprise_customer'].get( 'enable_audit_enrollment'): for audit_mode in CourseMode.AUDIT_MODES: modes.pop(audit_mode, None) context["title_content"] = title_content if "verified" in modes: verified_mode = modes["verified"] context["suggested_prices"] = [ decimal.Decimal(x.strip()) for x in verified_mode.suggested_prices.split(",") if x.strip() ] context["currency"] = verified_mode.currency.upper() context["min_price"] = verified_mode.min_price context["verified_name"] = verified_mode.name context["verified_description"] = verified_mode.description if verified_mode.sku: context[ "use_ecommerce_payment_flow"] = ecommerce_service.is_enabled( request.user) context[ "ecommerce_payment_page"] = ecommerce_service.payment_page_url( ) context["sku"] = verified_mode.sku context["bulk_sku"] = verified_mode.bulk_sku return render_to_response("course_modes/choose.html", context) @method_decorator(tpa_hint_ends_existing_session) @method_decorator(transaction.non_atomic_requests) @method_decorator(login_required) @method_decorator(outer_atomic(read_committed=True)) def post(self, request, course_id): """Takes the form submission from the page and parses it. Args: request (`Request`): The Django Request object. course_id (unicode): The slash-separated course key. Returns: Status code 400 when the requested mode is unsupported. When the honor mode is selected, redirects to the dashboard. When the verified mode is selected, returns error messages if the indicated contribution amount is invalid or below the minimum, otherwise redirects to the verification flow. """ course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) user = request.user # This is a bit redundant with logic in student.views.change_enrollment, # but I don't really have the time to refactor it more nicely and test. course = modulestore().get_course(course_key) if not has_access(user, 'enroll', course): error_msg = _("Enrollment is closed") return self.get(request, course_id, error=error_msg) requested_mode = self._get_requested_mode(request.POST) allowed_modes = CourseMode.modes_for_course_dict(course_key) if requested_mode not in allowed_modes: return HttpResponseBadRequest(_("Enrollment mode not supported")) if requested_mode in CourseMode.AUDIT_MODES: # If the learner has arrived at this screen via the traditional enrollment workflow, # then they should already be enrolled in an audit mode for the course, assuming one has # been configured. However, alternative enrollment workflows have been introduced into the # system, such as third-party discovery. These workflows result in learners arriving # directly at this screen, and they will not necessarily be pre-enrolled in the audit mode. CourseEnrollment.enroll(user, course_key, mode=requested_mode) return redirect( self._get_redirect_url_for_audit_enrollment( request, course_id)) mode_info = allowed_modes[requested_mode] if requested_mode == 'verified': amount = request.POST.get("contribution") or \ request.POST.get("contribution-other-amt") or 0 try: # Validate the amount passed in and force it into two digits amount_value = decimal.Decimal(amount).quantize( decimal.Decimal('.01'), rounding=decimal.ROUND_DOWN) except decimal.InvalidOperation: error_msg = _("Invalid amount selected.") return self.get(request, course_id, error=error_msg) # Check for minimum pricing if amount_value < mode_info.min_price: error_msg = _( "No selected price or selected price is too low.") return self.get(request, course_id, error=error_msg) donation_for_course = request.session.get("donation_for_course", {}) donation_for_course[unicode(course_key)] = amount_value request.session["donation_for_course"] = donation_for_course return redirect( reverse('verify_student_start_flow', kwargs={'course_id': unicode(course_key)})) def _get_redirect_url_for_audit_enrollment(self, request, course_id): """ After a user has been enrolled in a course in an audit mode, determine the appropriate location to which they ought to be redirected, bearing in mind enterprise data sharing consent considerations. """ enterprise_learner_data = enterprise_api.get_enterprise_learner_data( site=request.site, user=request.user) if enterprise_learner_data: enterprise_learner = enterprise_learner_data[0] # If we have an enterprise learner, check to see if the current course is in the enterprise's catalog. is_course_in_enterprise_catalog = enterprise_api.is_course_in_enterprise_catalog( site=request.site, course_id=course_id, enterprise_catalog_id=enterprise_learner['enterprise_customer'] ['catalog']) # If the course is in the catalog, check for an existing Enterprise enrollment if is_course_in_enterprise_catalog: client = enterprise_api.EnterpriseApiClient() if not client.get_enterprise_course_enrollment( enterprise_learner['id'], course_id): # If there's no existing Enterprise enrollment, create one. client.post_enterprise_course_enrollment( request.user.username, course_id, None) # Check if consent is required, and generate a redirect URL to the # consent service if so; this function returns None if consent # is not required or has already been granted. consent_url = get_enterprise_consent_url( request, course_id, user=request.user, return_to='dashboard', course_specific_return=False, ) # If we got a redirect URL for consent, go there. if consent_url: return consent_url # If the enrollment isn't Enterprise-linked, or if consent isn't necessary, go to the Dashboard. return reverse('dashboard') def _get_requested_mode(self, request_dict): """Get the user's requested mode Args: request_dict (`QueryDict`): A dictionary-like object containing all given HTTP POST parameters. Returns: The course mode slug corresponding to the choice in the POST parameters, None if the choice in the POST parameters is missing or is an unsupported mode. """ if 'verified_mode' in request_dict: return 'verified' if 'honor_mode' in request_dict: return 'honor' if 'audit_mode' in request_dict: return 'audit' else: return None
class ChooseModeView(View): """View used when the user is asked to pick a mode. When a get request is used, shows the selection page. When a post request is used, assumes that it is a form submission from the selection page, parses the response, and then sends user to the next step in the flow. """ @method_decorator(transaction.non_atomic_requests) def dispatch(self, *args, **kwargs): """Disable atomicity for the view. Otherwise, we'd be unable to commit to the database until the request had concluded; Django will refuse to commit when an atomic() block is active, since that would break atomicity. """ return super(ChooseModeView, self).dispatch(*args, **kwargs) @method_decorator(login_required) @method_decorator(transaction.atomic) def get(self, request, course_id, error=None): """Displays the course mode choice page. Args: request (`Request`): The Django Request object. course_id (unicode): The slash-separated course key. Keyword Args: error (unicode): If provided, display this error message on the page. Returns: Response """ course_key = CourseKey.from_string(course_id) # Check whether the user has access to this course # based on country access rules. embargo_redirect = embargo_api.redirect_if_blocked( course_key, user=request.user, ip_address=get_ip(request), url=request.path ) if embargo_redirect: return redirect(embargo_redirect) enrollment_mode, is_active = CourseEnrollment.enrollment_mode_for_user(request.user, course_key) modes = CourseMode.modes_for_course_dict(course_key) ecommerce_service = EcommerceService() # We assume that, if 'professional' is one of the modes, it should be the *only* mode. # If there are both modes, default to non-id-professional. has_enrolled_professional = (CourseMode.is_professional_slug(enrollment_mode) and is_active) if CourseMode.has_professional_mode(modes) and not has_enrolled_professional: purchase_workflow = request.GET.get("purchase_workflow", "single") verify_url = reverse('verify_student_start_flow', kwargs={'course_id': unicode(course_key)}) redirect_url = "{url}?purchase_workflow={workflow}".format(url=verify_url, workflow=purchase_workflow) if ecommerce_service.is_enabled(request.user): professional_mode = modes.get(CourseMode.NO_ID_PROFESSIONAL_MODE) or modes.get(CourseMode.PROFESSIONAL) if purchase_workflow == "single" and professional_mode.sku: redirect_url = ecommerce_service.get_checkout_page_url(professional_mode.sku) if purchase_workflow == "bulk" and professional_mode.bulk_sku: redirect_url = ecommerce_service.get_checkout_page_url(professional_mode.bulk_sku) return redirect(redirect_url) course = modulestore().get_course(course_key) # If there isn't a verified mode available, then there's nothing # to do on this page. Send the user to the dashboard. if not CourseMode.has_verified_mode(modes): return redirect(reverse('dashboard')) # If a user has already paid, redirect them to the dashboard. if is_active and (enrollment_mode in CourseMode.VERIFIED_MODES + [CourseMode.NO_ID_PROFESSIONAL_MODE]): # If the course has started redirect to course home instead if course.has_started(): return redirect(reverse('openedx.course_experience.course_home', kwargs={'course_id': course_key})) return redirect(reverse('dashboard')) donation_for_course = request.session.get("donation_for_course", {}) chosen_price = donation_for_course.get(unicode(course_key), None) if CourseEnrollment.is_enrollment_closed(request.user, course): locale = to_locale(get_language()) enrollment_end_date = format_datetime(course.enrollment_end, 'short', locale=locale) params = urllib.urlencode({'course_closed': enrollment_end_date}) return redirect('{0}?{1}'.format(reverse('dashboard'), params)) # When a credit mode is available, students will be given the option # to upgrade from a verified mode to a credit mode at the end of the course. # This allows students who have completed photo verification to be eligible # for univerity credit. # Since credit isn't one of the selectable options on the track selection page, # we need to check *all* available course modes in order to determine whether # a credit mode is available. If so, then we show slightly different messaging # for the verified track. has_credit_upsell = any( CourseMode.is_credit_mode(mode) for mode in CourseMode.modes_for_course(course_key, only_selectable=False) ) course_id = text_type(course_key) context = { "course_modes_choose_url": reverse( "course_modes_choose", kwargs={'course_id': course_id} ), "modes": modes, "has_credit_upsell": has_credit_upsell, "course_name": course.display_name_with_default, "course_org": course.display_org_with_default, "course_num": course.display_number_with_default, "chosen_price": chosen_price, "error": error, "responsive": True, "nav_hidden": True, "content_gating_enabled": ContentTypeGatingConfig.enabled_for_enrollment( user=request.user, course_key=course_key ), "course_duration_limit_enabled": CourseDurationLimitConfig.enabled_for_enrollment( user=request.user, course_key=course_key ), } context.update( get_experiment_user_metadata_context( course, request.user, ) ) title_content = _("Congratulations! You are now enrolled in {course_name}").format( course_name=course.display_name_with_default ) context["title_content"] = title_content if "verified" in modes: verified_mode = modes["verified"] context["suggested_prices"] = [ decimal.Decimal(x.strip()) for x in verified_mode.suggested_prices.split(",") if x.strip() ] context["currency"] = verified_mode.currency.upper() context["min_price"] = verified_mode.min_price context["verified_name"] = verified_mode.name context["verified_description"] = verified_mode.description if verified_mode.sku: context["use_ecommerce_payment_flow"] = ecommerce_service.is_enabled(request.user) context["ecommerce_payment_page"] = ecommerce_service.payment_page_url() context["sku"] = verified_mode.sku context["bulk_sku"] = verified_mode.bulk_sku context['currency_data'] = [] if waffle.switch_is_active('local_currency'): if 'edx-price-l10n' not in request.COOKIES: currency_data = get_currency_data() try: context['currency_data'] = json.dumps(currency_data) except TypeError: pass return render_to_response("course_modes/choose.html", context) @method_decorator(transaction.non_atomic_requests) @method_decorator(login_required) @method_decorator(outer_atomic(read_committed=True)) def post(self, request, course_id): """Takes the form submission from the page and parses it. Args: request (`Request`): The Django Request object. course_id (unicode): The slash-separated course key. Returns: Status code 400 when the requested mode is unsupported. When the honor mode is selected, redirects to the dashboard. When the verified mode is selected, returns error messages if the indicated contribution amount is invalid or below the minimum, otherwise redirects to the verification flow. """ course_key = CourseKey.from_string(course_id) user = request.user # This is a bit redundant with logic in student.views.change_enrollment, # but I don't really have the time to refactor it more nicely and test. course = modulestore().get_course(course_key) if not has_access(user, 'enroll', course): error_msg = _("Enrollment is closed") return self.get(request, course_id, error=error_msg) requested_mode = self._get_requested_mode(request.POST) allowed_modes = CourseMode.modes_for_course_dict(course_key) if requested_mode not in allowed_modes: return HttpResponseBadRequest(_("Enrollment mode not supported")) if requested_mode == 'audit': # If the learner has arrived at this screen via the traditional enrollment workflow, # then they should already be enrolled in an audit mode for the course, assuming one has # been configured. However, alternative enrollment workflows have been introduced into the # system, such as third-party discovery. These workflows result in learners arriving # directly at this screen, and they will not necessarily be pre-enrolled in the audit mode. CourseEnrollment.enroll(request.user, course_key, CourseMode.AUDIT) # If the course has started redirect to course home instead if course.has_started(): return redirect(reverse('openedx.course_experience.course_home', kwargs={'course_id': course_key})) return redirect(reverse('dashboard')) if requested_mode == 'honor': CourseEnrollment.enroll(user, course_key, mode=requested_mode) # If the course has started redirect to course home instead if course.has_started(): return redirect(reverse('openedx.course_experience.course_home', kwargs={'course_id': course_key})) return redirect(reverse('dashboard')) mode_info = allowed_modes[requested_mode] if requested_mode == 'verified': amount = request.POST.get("contribution") or \ request.POST.get("contribution-other-amt") or 0 try: # Validate the amount passed in and force it into two digits amount_value = decimal.Decimal(amount).quantize(decimal.Decimal('.01'), rounding=decimal.ROUND_DOWN) except decimal.InvalidOperation: error_msg = _("Invalid amount selected.") return self.get(request, course_id, error=error_msg) # Check for minimum pricing if amount_value < mode_info.min_price: error_msg = _("No selected price or selected price is too low.") return self.get(request, course_id, error=error_msg) donation_for_course = request.session.get("donation_for_course", {}) donation_for_course[unicode(course_key)] = amount_value request.session["donation_for_course"] = donation_for_course return redirect( reverse( 'verify_student_start_flow', kwargs={'course_id': unicode(course_key)} ) ) def _get_requested_mode(self, request_dict): """Get the user's requested mode Args: request_dict (`QueryDict`): A dictionary-like object containing all given HTTP POST parameters. Returns: The course mode slug corresponding to the choice in the POST parameters, None if the choice in the POST parameters is missing or is an unsupported mode. """ if 'verified_mode' in request_dict: return 'verified' if 'honor_mode' in request_dict: return 'honor' if 'audit_mode' in request_dict: return 'audit' else: return None
def test_named_outer_atomic_nesting(self): """ Test that a named outer_atomic raises an error only if nested in enable_named_outer_atomic and inside another atomic. """ if connection.vendor != 'mysql': raise unittest.SkipTest('Only works on MySQL.') outer_atomic(name='abc')(do_nothing)() with atomic(): outer_atomic(name='abc')(do_nothing)() with enable_named_outer_atomic('abc'): outer_atomic(name='abc')(do_nothing)() # Not nested. with atomic(): outer_atomic(name='pqr')(do_nothing)() # Not enabled. with self.assertRaisesRegexp(TransactionManagementError, 'Cannot be inside an atomic block.'): with atomic(): outer_atomic(name='abc')(do_nothing)() with enable_named_outer_atomic('abc', 'def'): outer_atomic(name='def')(do_nothing)() # Not nested. with atomic(): outer_atomic(name='pqr')(do_nothing)() # Not enabled. with self.assertRaisesRegexp(TransactionManagementError, 'Cannot be inside an atomic block.'): with atomic(): outer_atomic(name='def')(do_nothing)() with self.assertRaisesRegexp(TransactionManagementError, 'Cannot be inside an atomic block.'): with outer_atomic(): outer_atomic(name='def')(do_nothing)() with self.assertRaisesRegexp(TransactionManagementError, 'Cannot be inside an atomic block.'): with atomic(): outer_atomic(name='abc')(do_nothing)() with self.assertRaisesRegexp(TransactionManagementError, 'Cannot be inside an atomic block.'): with outer_atomic(): outer_atomic(name='abc')(do_nothing)()
class ChooseModeView(View): """View used when the user is asked to pick a mode. When a get request is used, shows the selection page. When a post request is used, assumes that it is a form submission from the selection page, parses the response, and then sends user to the next step in the flow. """ @method_decorator(transaction.non_atomic_requests) def dispatch(self, *args, **kwargs): # pylint: disable=missing-docstring return super(ChooseModeView, self).dispatch(*args, **kwargs) @method_decorator(login_required) @method_decorator(transaction.atomic) def get(self, request, course_id, error=None): """Displays the course mode choice page. Args: request (`Request`): The Django Request object. course_id (unicode): The slash-separated course key. Keyword Args: error (unicode): If provided, display this error message on the page. Returns: Response """ course_key = CourseKey.from_string(course_id) # Check whether the user has access to this course # based on country access rules. embargo_redirect = embargo_api.redirect_if_blocked( course_key, user=request.user, ip_address=get_ip(request), url=request.path) if embargo_redirect: return redirect(embargo_redirect) enrollment_mode, is_active = CourseEnrollment.enrollment_mode_for_user( request.user, course_key) modes = CourseMode.modes_for_course_dict(course_key) # We assume that, if 'professional' is one of the modes, it is the *only* mode. # If we offer more modes alongside 'professional' in the future, this will need to route # to the usual "choose your track" page same is true for no-id-professional mode. has_enrolled_professional = ( CourseMode.is_professional_slug(enrollment_mode) and is_active) if CourseMode.has_professional_mode( modes) and not has_enrolled_professional: return redirect( reverse('verify_student_start_flow', kwargs={'course_id': unicode(course_key)})) # If there isn't a verified mode available, then there's nothing # to do on this page. The user has almost certainly been auto-registered # in the "honor" track by this point, so we send the user # to the dashboard. if not CourseMode.has_verified_mode(modes): return redirect(reverse('dashboard')) # If a user has already paid, redirect them to the dashboard. if is_active and (enrollment_mode in CourseMode.VERIFIED_MODES + [CourseMode.NO_ID_PROFESSIONAL_MODE]): return redirect(reverse('dashboard')) donation_for_course = request.session.get("donation_for_course", {}) chosen_price = donation_for_course.get(unicode(course_key), None) course = modulestore().get_course(course_key) # When a credit mode is available, students will be given the option # to upgrade from a verified mode to a credit mode at the end of the course. # This allows students who have completed photo verification to be eligible # for univerity credit. # Since credit isn't one of the selectable options on the track selection page, # we need to check *all* available course modes in order to determine whether # a credit mode is available. If so, then we show slightly different messaging # for the verified track. has_credit_upsell = any( CourseMode.is_credit_mode(mode) for mode in CourseMode.modes_for_course(course_key, only_selectable=False)) context = { "course_modes_choose_url": reverse("course_modes_choose", kwargs={'course_id': course_key.to_deprecated_string()}), "modes": modes, "has_credit_upsell": has_credit_upsell, "course_name": course.display_name_with_default, "course_org": course.display_org_with_default, "course_num": course.display_number_with_default, "chosen_price": chosen_price, "error": error, "responsive": True, "nav_hidden": True, } if "verified" in modes: context["suggested_prices"] = [ decimal.Decimal(x.strip()) for x in modes["verified"].suggested_prices.split(",") if x.strip() ] context["currency"] = modes["verified"].currency.upper() context["min_price"] = modes["verified"].min_price context["verified_name"] = modes["verified"].name context["verified_description"] = modes["verified"].description return render_to_response("course_modes/choose.html", context) @method_decorator(transaction.non_atomic_requests) @method_decorator(login_required) @method_decorator(outer_atomic(read_committed=True)) def post(self, request, course_id): """Takes the form submission from the page and parses it. Args: request (`Request`): The Django Request object. course_id (unicode): The slash-separated course key. Returns: Status code 400 when the requested mode is unsupported. When the honor mode is selected, redirects to the dashboard. When the verified mode is selected, returns error messages if the indicated contribution amount is invalid or below the minimum, otherwise redirects to the verification flow. """ course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) user = request.user # This is a bit redundant with logic in student.views.change_enrollment, # but I don't really have the time to refactor it more nicely and test. course = modulestore().get_course(course_key) if not has_access(user, 'enroll', course): error_msg = _("Enrollment is closed") return self.get(request, course_id, error=error_msg) requested_mode = self._get_requested_mode(request.POST) allowed_modes = CourseMode.modes_for_course_dict(course_key) if requested_mode not in allowed_modes: return HttpResponseBadRequest(_("Enrollment mode not supported")) if requested_mode == 'honor': # The user will have already been enrolled in the honor mode at this # point, so we just redirect them to the dashboard, thereby avoiding # hitting the database a second time attempting to enroll them. return redirect(reverse('dashboard')) mode_info = allowed_modes[requested_mode] if requested_mode == 'verified': amount = request.POST.get("contribution") or \ request.POST.get("contribution-other-amt") or 0 try: # Validate the amount passed in and force it into two digits amount_value = decimal.Decimal(amount).quantize( decimal.Decimal('.01'), rounding=decimal.ROUND_DOWN) except decimal.InvalidOperation: error_msg = _("Invalid amount selected.") return self.get(request, course_id, error=error_msg) # Check for minimum pricing if amount_value < mode_info.min_price: error_msg = _( "No selected price or selected price is too low.") return self.get(request, course_id, error=error_msg) donation_for_course = request.session.get("donation_for_course", {}) donation_for_course[unicode(course_key)] = amount_value request.session["donation_for_course"] = donation_for_course return redirect( reverse('verify_student_start_flow', kwargs={'course_id': unicode(course_key)})) def _get_requested_mode(self, request_dict): """Get the user's requested mode Args: request_dict (`QueryDict`): A dictionary-like object containing all given HTTP POST parameters. Returns: The course mode slug corresponding to the choice in the POST parameters, None if the choice in the POST parameters is missing or is an unsupported mode. """ if 'verified_mode' in request_dict: return 'verified' if 'honor_mode' in request_dict: return 'honor' else: return None
def course_data(request, course_id): """ Get course's data(title, short description), Total Points/Earned Points or 404 if there is no such course. Assumes the course_id is in a valid format. """ course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) with modulestore().bulk_operations(course_key): course = get_course_with_access(request.user, 'load', course_key, depth=None, check_if_enrolled=True) access_response = has_access(request.user, 'load', course, course_key) context={} if course.has_started(): staff_access = bool(has_access(request.user, 'staff', course)) student = request.user # NOTE: To make sure impersonation by instructor works, use # student instead of request.user in the rest of the function. # The pre-fetching of groups is done to make auth checks not require an # additional DB lookup (this kills the Progress page in particular). student = User.objects.prefetch_related("groups").get(id=student.id) with outer_atomic(): field_data_cache = grades.field_data_cache_for_grading(course, student) scores_client = ScoresClient.from_field_data_cache(field_data_cache) title = course.display_name_with_default loc = course.location.replace(category='about', name='short_description') about_module = get_module( request.user, request, loc, field_data_cache, log_if_not_found=False, wrap_xmodule_display=False, static_asset_path=course.static_asset_path, course=course ) short_description = about_module.render(STUDENT_VIEW).content courseware_summary = grades.progress_summary( student, request, course, field_data_cache=field_data_cache, scores_client=scores_client ) grade_summary = grades.grade( student, request, course, field_data_cache=field_data_cache, scores_client=scores_client ) total_points = 0 earned_points = 0 for chapter in courseware_summary: for section in chapter['sections']: total_points += section['section_total'].possible earned_points += section['section_total'].earned percentage_points = float(earned_points)*(100.0/float(total_points)) context = { "started": course.has_started(), "course_image": course_image_url(course), "total": total_points, "earned": earned_points, "percentage": percentage_points, 'title': title, 'short_description' : short_description, 'staff_access': staff_access, 'student': student.id, 'passed': is_course_passed(course, grade_summary), } else: context={ "started": course.has_started(), } return JsonResponse(context)
def summary(student, course, course_structure=None): """ This pulls a summary of all problems in the course. Returns - courseware_summary is a summary of all sections with problems in the course. It is organized as an array of chapters, each containing an array of sections, each containing an array of scores. This contains information for graded and ungraded problems, and is good for displaying a course summary with due dates, etc. - None if the student does not have access to load the course module. Arguments: student: A User object for the student to grade course: A Descriptor containing the course to grade """ if course_structure is None: course_structure = get_course_blocks(student, course.location) if not len(course_structure): return ProgressSummary() scorable_locations = [ block_key for block_key in course_structure if possibly_scored(block_key) ] with outer_atomic(): scores_client = ScoresClient.create_for_locations( course.id, student.id, scorable_locations) # We need to import this here to avoid a circular dependency of the form: # XBlock --> submissions --> Django Rest Framework error strings --> # Django translation --> ... --> courseware --> submissions from submissions import api as sub_api # installed from the edx-submissions repository with outer_atomic(): submissions_scores = sub_api.get_scores( unicode(course.id), anonymous_id_for_user(student, course.id)) # Check for gated content gated_content = gating_api.get_gated_content(course, student) chapters = [] locations_to_weighted_scores = {} for chapter_key in course_structure.get_children( course_structure.root_block_usage_key): chapter = course_structure[chapter_key] sections = [] for section_key in course_structure.get_children(chapter_key): if unicode(section_key) in gated_content: continue section = course_structure[section_key] graded = getattr(section, 'graded', False) scores = [] for descendant_key in course_structure.post_order_traversal( filter_func=possibly_scored, start_node=section_key, ): descendant = course_structure[descendant_key] (correct, total) = get_score( student, descendant, scores_client, submissions_scores, ) if correct is None and total is None: continue weighted_location_score = Score( correct, total, graded, block_metadata_utils.display_name_with_default_escaped( descendant), descendant.location) scores.append(weighted_location_score) locations_to_weighted_scores[ descendant.location] = weighted_location_score escaped_section_name = block_metadata_utils.display_name_with_default_escaped( section) section_total, _ = graders.aggregate_scores( scores, escaped_section_name) sections.append({ 'display_name': escaped_section_name, 'url_name': block_metadata_utils.url_name_for_block(section), 'scores': scores, 'section_total': section_total, 'format': getattr(section, 'format', ''), 'due': getattr(section, 'due', None), 'graded': graded, }) chapters.append({ 'course': course.display_name_with_default_escaped, 'display_name': block_metadata_utils.display_name_with_default_escaped(chapter), 'url_name': block_metadata_utils.url_name_for_block(chapter), 'sections': sections }) return ProgressSummary(chapters, locations_to_weighted_scores, course_structure.get_children)
def _grade(student, request, course, keep_raw_scores, field_data_cache, scores_client): """ Unwrapped version of "grade" This grades a student as quickly as possible. It returns the output from the course grader, augmented with the final letter grade. The keys in the output are: course: a CourseDescriptor - grade : A final letter grade. - percent : The final percent for the class (rounded up). - section_breakdown : A breakdown of each section that makes up the grade. (For display) - grade_breakdown : A breakdown of the major components that make up the final grade. (For display) - keep_raw_scores : if True, then value for key 'raw_scores' contains scores for every graded module More information on the format is in the docstring for CourseGrader. """ with outer_atomic(): if field_data_cache is None: field_data_cache = field_data_cache_for_grading(course, student) if scores_client is None: scores_client = ScoresClient.from_field_data_cache( field_data_cache) # Dict of item_ids -> (earned, possible) point tuples. This *only* grabs # scores that were registered with the submissions API, which for the moment # means only openassessment (edx-ora2) # We need to import this here to avoid a circular dependency of the form: # XBlock --> submissions --> Django Rest Framework error strings --> # Django translation --> ... --> courseware --> submissions from submissions import api as sub_api # installed from the edx-submissions repository with outer_atomic(): submissions_scores = sub_api.get_scores( course.id.to_deprecated_string(), anonymous_id_for_user(student, course.id)) max_scores_cache = MaxScoresCache.create_for_course(course) # For the moment, we have to get scorable_locations from field_data_cache # and not from scores_client, because scores_client is ignorant of things # in the submissions API. As a further refactoring step, submissions should # be hidden behind the ScoresClient. max_scores_cache.fetch_from_remote(field_data_cache.scorable_locations) grading_context = course.grading_context raw_scores = [] totaled_scores = {} # This next complicated loop is just to collect the totaled_scores, which is # passed to the grader for section_format, sections in grading_context[ 'graded_sections'].iteritems(): format_scores = [] for section in sections: section_descriptor = section['section_descriptor'] section_name = section_descriptor.display_name_with_default_escaped with outer_atomic(): # some problems have state that is updated independently of interaction # with the LMS, so they need to always be scored. (E.g. combinedopenended ORA1) # TODO This block is causing extra savepoints to be fired that are empty because no queries are executed # during the loop. When refactoring this code please keep this outer_atomic call in mind and ensure we # are not making unnecessary database queries. should_grade_section = any( descriptor.always_recalculate_grades for descriptor in section['xmoduledescriptors']) # If there are no problems that always have to be regraded, check to # see if any of our locations are in the scores from the submissions # API. If scores exist, we have to calculate grades for this section. if not should_grade_section: should_grade_section = any( descriptor.location.to_deprecated_string() in submissions_scores for descriptor in section['xmoduledescriptors']) if not should_grade_section: should_grade_section = any( descriptor.location in scores_client for descriptor in section['xmoduledescriptors']) # If we haven't seen a single problem in the section, we don't have # to grade it at all! We can assume 0% if should_grade_section: scores = [] def create_module(descriptor): '''creates an XModule instance given a descriptor''' # TODO: We need the request to pass into here. If we could forego that, our arguments # would be simpler return get_module_for_descriptor(student, request, descriptor, field_data_cache, course.id, course=course) descendants = yield_dynamic_descriptor_descendants( section_descriptor, student.id, create_module) for module_descriptor in descendants: user_access = has_access( student, 'load', module_descriptor, module_descriptor.location.course_key) if not user_access: continue (correct, total) = get_score( student, module_descriptor, create_module, scores_client, submissions_scores, max_scores_cache, ) if correct is None and total is None: continue if settings.GENERATE_PROFILE_SCORES: # for debugging! if total > 1: correct = random.randrange( max(total - 2, 1), total + 1) else: correct = total graded = module_descriptor.graded if not total > 0: # We simply cannot grade a problem that is 12/0, because we might need it as a percentage graded = False scores.append( Score( correct, total, graded, module_descriptor. display_name_with_default_escaped, module_descriptor.location)) __, graded_total = graders.aggregate_scores( scores, section_name) if keep_raw_scores: raw_scores += scores else: graded_total = Score(0.0, 1.0, True, section_name, None) #Add the graded total to totaled_scores if graded_total.possible > 0: format_scores.append(graded_total) else: log.info( "Unable to grade a section with a total possible score of zero. " + str(section_descriptor.location)) totaled_scores[section_format] = format_scores with outer_atomic(): # Grading policy might be overriden by a CCX, need to reset it course.set_grading_policy(course.grading_policy) grade_summary = course.grader.grade( totaled_scores, generate_random_scores=settings.GENERATE_PROFILE_SCORES) # We round the grade here, to make sure that the grade is an whole percentage and # doesn't get displayed differently than it gets grades grade_summary['percent'] = round(grade_summary['percent'] * 100 + 0.05) / 100 letter_grade = grade_for_percentage(course.grade_cutoffs, grade_summary['percent']) grade_summary['grade'] = letter_grade grade_summary[ 'totaled_scores'] = totaled_scores # make this available, eg for instructor download & debugging if keep_raw_scores: # way to get all RAW scores out to instructor # so grader can be double-checked grade_summary['raw_scores'] = raw_scores max_scores_cache.push_to_remote() return grade_summary
def _progress_summary(student, request, course, field_data_cache=None, scores_client=None): """ Unwrapped version of "progress_summary". This pulls a summary of all problems in the course. Returns - courseware_summary is a summary of all sections with problems in the course. It is organized as an array of chapters, each containing an array of sections, each containing an array of scores. This contains information for graded and ungraded problems, and is good for displaying a course summary with due dates, etc. Arguments: student: A User object for the student to grade course: A Descriptor containing the course to grade If the student does not have access to load the course module, this function will return None. """ with outer_atomic(): if field_data_cache is None: field_data_cache = field_data_cache_for_grading(course, student) if scores_client is None: scores_client = ScoresClient.from_field_data_cache( field_data_cache) course_module = get_module_for_descriptor(student, request, course, field_data_cache, course.id, course=course) if not course_module: return None course_module = getattr(course_module, '_x_module', course_module) # We need to import this here to avoid a circular dependency of the form: # XBlock --> submissions --> Django Rest Framework error strings --> # Django translation --> ... --> courseware --> submissions from submissions import api as sub_api # installed from the edx-submissions repository with outer_atomic(): submissions_scores = sub_api.get_scores( course.id.to_deprecated_string(), anonymous_id_for_user(student, course.id)) max_scores_cache = MaxScoresCache.create_for_course(course) # For the moment, we have to get scorable_locations from field_data_cache # and not from scores_client, because scores_client is ignorant of things # in the submissions API. As a further refactoring step, submissions should # be hidden behind the ScoresClient. max_scores_cache.fetch_from_remote(field_data_cache.scorable_locations) chapters = [] locations_to_children = defaultdict(list) locations_to_weighted_scores = {} # Don't include chapters that aren't displayable (e.g. due to error) for chapter_module in course_module.get_display_items(): # Skip if the chapter is hidden if chapter_module.hide_from_toc: continue sections = [] for section_module in chapter_module.get_display_items(): # Skip if the section is hidden with outer_atomic(): if section_module.hide_from_toc: continue graded = section_module.graded scores = [] module_creator = section_module.xmodule_runtime.get_module for module_descriptor in yield_dynamic_descriptor_descendants( section_module, student.id, module_creator): locations_to_children[module_descriptor.parent].append( module_descriptor.location) (correct, total) = get_score( student, module_descriptor, module_creator, scores_client, submissions_scores, max_scores_cache, ) if correct is None and total is None: continue weighted_location_score = Score( correct, total, graded, module_descriptor.display_name_with_default_escaped, module_descriptor.location) scores.append(weighted_location_score) locations_to_weighted_scores[ module_descriptor.location] = weighted_location_score scores.reverse() section_total, _ = graders.aggregate_scores( scores, section_module.display_name_with_default_escaped) module_format = section_module.format if section_module.format is not None else '' sections.append({ 'display_name': section_module.display_name_with_default_escaped, 'url_name': section_module.url_name, 'scores': scores, 'section_total': section_total, 'format': force_translate(module_format, ugettext_noop('Entrance Exam')), 'due': section_module.due, 'graded': graded, }) chapters.append({ 'course': course.display_name_with_default_escaped, 'display_name': chapter_module.display_name_with_default_escaped, 'url_name': chapter_module.url_name, 'sections': sections }) max_scores_cache.push_to_remote() return ProgressSummary(chapters, locations_to_weighted_scores, locations_to_children)
def create_account_with_params(request, params): """ Given a request and a dict of parameters (which may or may not have come from the request), create an account for the requesting user, including creating a comments service user object and sending an activation email. This also takes external/third-party auth into account, updates that as necessary, and authenticates the user for the request's session. Does not return anything. Raises AccountValidationError if an account with the username or email specified by params already exists, or ValidationError if any of the given parameters is invalid for any other reason. Issues with this code: * It is non-transactional except where explicitly wrapped in atomic to alleviate deadlocks and improve performance. This means failures at different places in registration can leave users in inconsistent states. * Third-party auth passwords are not verified. There is a comment that they are unused, but it would be helpful to have a sanity check that they are sane. * The user-facing text is rather unfriendly (e.g. "Username must be a minimum of two characters long" rather than "Please use a username of at least two characters"). * Duplicate email raises a ValidationError (rather than the expected AccountValidationError). Duplicate username returns an inconsistent user message (i.e. "An account with the Public Username '{username}' already exists." rather than "It looks like {username} belongs to an existing account. Try again with a different username.") The two checks occur at different places in the code; as a result, registering with both a duplicate username and email raises only a ValidationError for email only. """ # Copy params so we can modify it; we can't just do dict(params) because if # params is request.POST, that results in a dict containing lists of values params = dict(params.items()) # allow to define custom set of required/optional/hidden fields via configuration extra_fields = configuration_helpers.get_value( 'REGISTRATION_EXTRA_FIELDS', getattr(settings, 'REGISTRATION_EXTRA_FIELDS', {}) ) # registration via third party (Google, Facebook) using mobile application # doesn't use social auth pipeline (no redirect uri(s) etc involved). # In this case all related info (required for account linking) # is sent in params. # `third_party_auth_credentials_in_api` essentially means 'request # is made from mobile application' third_party_auth_credentials_in_api = 'provider' in params is_third_party_auth_enabled = third_party_auth.is_enabled() if is_third_party_auth_enabled and (pipeline.running(request) or third_party_auth_credentials_in_api): params["password"] = generate_password() # in case user is registering via third party (Google, Facebook) and pipeline has expired, show appropriate # error message if is_third_party_auth_enabled and ('social_auth_provider' in params and not pipeline.running(request)): raise ValidationError( {'session_expired': [ _(u"Registration using {provider} has timed out.").format( provider=params.get('social_auth_provider')) ]} ) do_external_auth, eamap = pre_account_creation_external_auth(request, params) extended_profile_fields = configuration_helpers.get_value('extended_profile_fields', []) # Can't have terms of service for certain SHIB users, like at Stanford registration_fields = getattr(settings, 'REGISTRATION_EXTRA_FIELDS', {}) tos_required = ( registration_fields.get('terms_of_service') != 'hidden' or registration_fields.get('honor_code') != 'hidden' ) and ( not settings.FEATURES.get("AUTH_USE_SHIB") or not settings.FEATURES.get("SHIB_DISABLE_TOS") or not do_external_auth or not eamap.external_domain.startswith(settings.SHIBBOLETH_DOMAIN_PREFIX) ) form = AccountCreationForm( data=params, extra_fields=extra_fields, extended_profile_fields=extended_profile_fields, do_third_party_auth=do_external_auth, tos_required=tos_required, ) custom_form = get_registration_extension_form(data=params) # Perform operations within a transaction that are critical to account creation with outer_atomic(read_committed=True): # first, create the account (user, profile, registration) = do_create_account(form, custom_form) third_party_provider, running_pipeline = _link_user_to_third_party_provider( is_third_party_auth_enabled, third_party_auth_credentials_in_api, user, request, params, ) new_user = authenticate_new_user(request, user.username, params['password']) django_login(request, new_user) request.session.set_expiry(0) post_account_creation_external_auth(do_external_auth, eamap, new_user) # Check if system is configured to skip activation email for the current user. skip_email = _skip_activation_email( user, do_external_auth, running_pipeline, third_party_provider, ) if skip_email: registration.activate() else: compose_and_send_activation_email(user, profile, registration) # Perform operations that are non-critical parts of account creation create_or_set_user_attribute_created_on_site(user, request.site) preferences_api.set_user_preference(user, LANGUAGE_KEY, get_language()) if settings.FEATURES.get('ENABLE_DISCUSSION_EMAIL_DIGEST'): try: enable_notifications(user) except Exception: # pylint: disable=broad-except log.exception(u"Enable discussion notifications failed for user {id}.".format(id=user.id)) _track_user_registration(user, profile, params, third_party_provider) # Announce registration REGISTER_USER.send(sender=None, user=user, registration=registration) create_comments_service_user(user) try: _record_registration_attributions(request, new_user) # Don't prevent a user from registering due to attribution errors. except Exception: # pylint: disable=broad-except log.exception('Error while attributing cookies to user registration.') # TODO: there is no error checking here to see that the user actually logged in successfully, # and is not yet an active user. if new_user is not None: AUDIT_LOG.info(u"Login success on new account creation - {0}".format(new_user.username)) return new_user
def _external_login_or_signup(request, external_id, external_domain, credentials, email, fullname, retfun=None): """ Generic external auth login or signup """ # pylint: disable=too-many-statements # see if we have a map from this external_id to an edX username eamap_defaults = { 'external_credentials': json.dumps(credentials), 'external_email': email, 'external_name': fullname, 'internal_password': generate_password() } # We are not guaranteed to be in a transaction here since some upstream views # use non_atomic_requests with outer_atomic(): eamap, created = ExternalAuthMap.objects.get_or_create( external_id=external_id, external_domain=external_domain, defaults=eamap_defaults ) if created: log.debug(u'Created eamap=%s', eamap) else: log.debug(u'Found eamap=%s', eamap) log.info(u"External_Auth login_or_signup for %s : %s : %s : %s", external_domain, external_id, email, fullname) uses_shibboleth = settings.FEATURES.get('AUTH_USE_SHIB') and external_domain.startswith(SHIBBOLETH_DOMAIN_PREFIX) uses_certs = settings.FEATURES.get('AUTH_USE_CERTIFICATES') internal_user = eamap.user if internal_user is None: if uses_shibboleth: # If we are using shib, try to link accounts # For Stanford shib, the email the idp returns is actually under the control of the user. # Since the id the idps return is not user-editable, and is of the from "*****@*****.**", # use the id to link accounts instead. try: with outer_atomic(): link_user = User.objects.get(email=eamap.external_id) if not ExternalAuthMap.objects.filter(user=link_user).exists(): # if there's no pre-existing linked eamap, we link the user eamap.user = link_user eamap.save() internal_user = link_user log.info(u'SHIB: Linking existing account for %s', eamap.external_id) # now pass through to log in else: # otherwise, there must have been an error, b/c we've already linked a user with these external # creds failure_msg = _( "You have already created an account using " "an external login like WebAuth or Shibboleth. " "Please contact {tech_support_email} for support." ).format( tech_support_email=get_value('email_from_address', settings.TECH_SUPPORT_EMAIL), ) return default_render_failure(request, failure_msg) except User.DoesNotExist: log.info(u'SHIB: No user for %s yet, doing signup', eamap.external_email) return _signup(request, eamap, retfun) else: log.info(u'No user for %s yet. doing signup', eamap.external_email) return _signup(request, eamap, retfun) # We trust shib's authentication, so no need to authenticate using the password again uname = internal_user.username if uses_shibboleth: user = internal_user # Assuming this 'AUTHENTICATION_BACKENDS' is set in settings, which I think is safe if settings.AUTHENTICATION_BACKENDS: auth_backend = settings.AUTHENTICATION_BACKENDS[0] else: auth_backend = 'ratelimitbackend.backends.RateLimitModelBackend' user.backend = auth_backend if settings.FEATURES['SQUELCH_PII_IN_LOGS']: AUDIT_LOG.info(u'Linked user.id: {0} logged in via Shibboleth'.format(user.id)) else: AUDIT_LOG.info(u'Linked user "{0}" logged in via Shibboleth'.format(user.email)) elif uses_certs: # Certificates are trusted, so just link the user and log the action user = internal_user user.backend = 'ratelimitbackend.backends.RateLimitModelBackend' if settings.FEATURES['SQUELCH_PII_IN_LOGS']: AUDIT_LOG.info(u'Linked user_id {0} logged in via SSL certificate'.format(user.id)) else: AUDIT_LOG.info(u'Linked user "{0}" logged in via SSL certificate'.format(user.email)) else: user = authenticate(username=uname, password=eamap.internal_password, request=request) if user is None: # we want to log the failure, but don't want to log the password attempted: if settings.FEATURES['SQUELCH_PII_IN_LOGS']: AUDIT_LOG.warning(u'External Auth Login failed') else: AUDIT_LOG.warning(u'External Auth Login failed for "{0}"'.format(uname)) return _signup(request, eamap, retfun) if not user.is_active: if settings.FEATURES.get('BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH'): # if BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH, we trust external auth and activate any users # that aren't already active user.is_active = True user.save() if settings.FEATURES['SQUELCH_PII_IN_LOGS']: AUDIT_LOG.info(u'Activating user {0} due to external auth'.format(user.id)) else: AUDIT_LOG.info(u'Activating user "{0}" due to external auth'.format(uname)) else: if settings.FEATURES['SQUELCH_PII_IN_LOGS']: AUDIT_LOG.warning(u'User {0} is not active after external login'.format(user.id)) else: AUDIT_LOG.warning(u'User "{0}" is not active after external login'.format(uname)) # TODO: improve error page msg = 'Account not yet activated: please look for link in your email' return default_render_failure(request, msg) login(request, user) request.session.set_expiry(0) if settings.FEATURES['SQUELCH_PII_IN_LOGS']: AUDIT_LOG.info(u"Login success - user.id: {0}".format(user.id)) else: AUDIT_LOG.info(u"Login success - {0} ({1})".format(user.username, user.email)) if retfun is None: return redirect('/') return retfun()