def test_credit_request(self): # Initiate a credit request request = api.create_credit_request(self.course_key, self.PROVIDER_ID, self.USER_INFO['username']) # Validate the URL and method self.assertIn('url', request) self.assertEqual(request['url'], self.PROVIDER_URL) self.assertIn('method', request) self.assertEqual(request['method'], "POST") self.assertIn('parameters', request) parameters = request['parameters'] # Validate the UUID self.assertIn('request_uuid', parameters) self.assertEqual(len(parameters['request_uuid']), 32) # Validate the timestamp self.assertIn('timestamp', parameters) parsed_date = from_timestamp(parameters['timestamp']) self.assertTrue(parsed_date < datetime.datetime.now(pytz.UTC)) # Validate course information self.assertEqual(parameters['course_org'], self.course_key.org) self.assertEqual(parameters['course_num'], self.course_key.course) self.assertEqual(parameters['course_run'], self.course_key.run) self.assertEqual(parameters['final_grade'], unicode(self.FINAL_GRADE)) # Validate user information for key in self.USER_INFO.keys(): param_key = 'user_{key}'.format(key=key) self.assertIn(param_key, parameters) expected = '' if key == 'mailing_address' else self.USER_INFO[key] self.assertEqual(parameters[param_key], expected)
def test_credit_request(self): # Initiate a credit request request = api.create_credit_request(self.course_key, self.PROVIDER_ID, self.USER_INFO['username']) # Validate the URL and method self.assertIn('url', request) self.assertEqual(request['url'], self.PROVIDER_URL) self.assertIn('method', request) self.assertEqual(request['method'], "POST") self.assertIn('parameters', request) parameters = request['parameters'] # Validate the UUID self.assertIn('request_uuid', parameters) self.assertEqual(len(parameters['request_uuid']), 32) # Validate the timestamp self.assertIn('timestamp', parameters) parsed_date = from_timestamp(parameters['timestamp']) self.assertLess(parsed_date, datetime.datetime.now(pytz.UTC)) # Validate course information self.assertEqual(parameters['course_org'], self.course_key.org) self.assertEqual(parameters['course_num'], self.course_key.course) self.assertEqual(parameters['course_run'], self.course_key.run) self.assertEqual(parameters['final_grade'], unicode(self.FINAL_GRADE)) # Validate user information for key in self.USER_INFO.keys(): param_key = 'user_{key}'.format(key=key) self.assertIn(param_key, parameters) expected = '' if key == 'mailing_address' else self.USER_INFO[key] self.assertEqual(parameters[param_key], expected)
def _validate_timestamp(timestamp_value, provider_id): """ Check that the timestamp of the request is recent. Arguments: timestamp (int or string): Number of seconds since Jan. 1, 1970 UTC. If specified as a string, it will be converted to an integer. provider_id (unicode): Identifier for the credit provider. Returns: HttpResponse or None """ timestamp = from_timestamp(timestamp_value) if timestamp is None: msg = u'"{timestamp}" is not a valid timestamp'.format( timestamp=timestamp_value) log.warning(msg) return HttpResponseBadRequest(msg) # Check that the timestamp is recent elapsed_seconds = (datetime.datetime.now(pytz.UTC) - timestamp).total_seconds() if elapsed_seconds > settings.CREDIT_PROVIDER_TIMESTAMP_EXPIRATION: log.warning( (u'Timestamp %s is too far in the past (%s seconds), ' u'so we are rejecting the notification from the credit provider "%s".' ), timestamp_value, elapsed_seconds, provider_id, ) return HttpResponseForbidden(u"Timestamp is too far in the past.")
def _validate_timestamp(timestamp_value, provider_id): """ Check that the timestamp of the request is recent. Arguments: timestamp (int or string): Number of seconds since Jan. 1, 1970 UTC. If specified as a string, it will be converted to an integer. provider_id (unicode): Identifier for the credit provider. Returns: HttpResponse or None """ timestamp = from_timestamp(timestamp_value) if timestamp is None: msg = u'"{timestamp}" is not a valid timestamp'.format(timestamp=timestamp_value) log.warning(msg) return HttpResponseBadRequest(msg) # Check that the timestamp is recent elapsed_seconds = (datetime.datetime.now(pytz.UTC) - timestamp).total_seconds() if elapsed_seconds > settings.CREDIT_PROVIDER_TIMESTAMP_EXPIRATION: log.warning( ( u'Timestamp %s is too far in the past (%s seconds), ' u'so we are rejecting the notification from the credit provider "%s".' ), timestamp_value, elapsed_seconds, provider_id, ) return HttpResponseForbidden(u"Timestamp is too far in the past.")
def recalculate_subsection_grade_v2(**kwargs): """ Updates a saved subsection grade. Arguments: user_id (int): id of applicable User object course_id (string): identifying the course usage_id (string): identifying the course block only_if_higher (boolean): indicating whether grades should be updated only if the new raw_earned is higher than the previous value. expected_modified_time (serialized timestamp): indicates when the task was queued so that we can verify the underlying data update. score_deleted (boolean): indicating whether the grade change is a result of the problem's score being deleted. event_transaction_id(string): uuid identifying the current event transaction. event_transaction_type(string): human-readable type of the event at the root of the current event transaction. """ try: course_key = CourseLocator.from_string(kwargs['course_id']) if not PersistentGradesEnabledFlag.feature_enabled(course_key): return score_deleted = kwargs['score_deleted'] scored_block_usage_key = UsageKey.from_string(kwargs['usage_id']).replace(course_key=course_key) expected_modified_time = from_timestamp(kwargs['expected_modified_time']) # The request cache is not maintained on celery workers, # where this code runs. So we take the values from the # main request cache and store them in the local request # cache. This correlates model-level grading events with # higher-level ones. set_event_transaction_id(kwargs.pop('event_transaction_id', None)) set_event_transaction_type(kwargs.pop('event_transaction_type', None)) # Verify the database has been updated with the scores when the task was # created. This race condition occurs if the transaction in the task # creator's process hasn't committed before the task initiates in the worker # process. if not _has_database_updated_with_new_score( kwargs['user_id'], scored_block_usage_key, expected_modified_time, score_deleted, ): raise _retry_recalculate_subsection_grade(**kwargs) _update_subsection_grades( course_key, scored_block_usage_key, kwargs['only_if_higher'], kwargs['user_id'], ) except Exception as exc: # pylint: disable=broad-except if not isinstance(exc, KNOWN_RETRY_ERRORS): log.info("tnl-6244 grades unexpected failure: {}. kwargs={}".format( repr(exc), kwargs )) raise _retry_recalculate_subsection_grade(exc=exc, **kwargs)
def test_credit_request(self): # Initiate a credit request request = api.create_credit_request(self.course_key, self.PROVIDER_ID, self.USER_INFO["username"]) # Validate the URL and method self.assertIn("url", request) self.assertEqual(request["url"], self.PROVIDER_URL) self.assertIn("method", request) self.assertEqual(request["method"], "POST") self.assertIn("parameters", request) parameters = request["parameters"] # Validate the UUID self.assertIn("request_uuid", parameters) self.assertEqual(len(parameters["request_uuid"]), 32) # Validate the timestamp self.assertIn("timestamp", parameters) parsed_date = from_timestamp(parameters["timestamp"]) self.assertLess(parsed_date, datetime.datetime.now(pytz.UTC)) # Validate course information self.assertEqual(parameters["course_org"], self.course_key.org) self.assertEqual(parameters["course_num"], self.course_key.course) self.assertEqual(parameters["course_run"], self.course_key.run) self.assertEqual(parameters["final_grade"], unicode(self.FINAL_GRADE)) # Validate user information for key in self.USER_INFO.keys(): param_key = "user_{key}".format(key=key) self.assertIn(param_key, parameters) expected = "" if key == "mailing_address" else self.USER_INFO[key] self.assertEqual(parameters[param_key], expected)
def _has_db_updated_with_new_score(self, scored_block_usage_key, **kwargs): """ Returns whether the database has been updated with the expected new score values for the given problem and user. """ if kwargs[ 'score_db_table'] == ScoreDatabaseTableEnum.courseware_student_module: score = get_score(kwargs['user_id'], scored_block_usage_key) found_modified_time = score.modified if score is not None else None elif kwargs['score_db_table'] == ScoreDatabaseTableEnum.submissions: score = sub_api.get_score({ "student_id": kwargs['anonymous_user_id'], "course_id": six.text_type(scored_block_usage_key.course_key), "item_id": six.text_type(scored_block_usage_key), "item_type": scored_block_usage_key.block_type, }) found_modified_time = score['created_at'] if score is not None else None else: assert kwargs['score_db_table'] == ScoreDatabaseTableEnum.overrides from . import api score = api.get_subsection_grade_override( user_id=kwargs['user_id'], course_key_or_id=kwargs['course_id'], usage_key_or_id=kwargs['usage_id']) found_modified_time = score.modified if score is not None else None if score is None: # score should be None only if it was deleted. # Otherwise, it hasn't yet been saved. db_is_updated = kwargs['score_deleted'] else: db_is_updated = found_modified_time >= from_timestamp( kwargs['expected_modified_time']) if not db_is_updated: log.info( u"Grades: tasks._has_database_updated_with_new_score is False. Task ID: {}. Kwargs: {}. Found " u"modified time: {}".format( self.request.id, kwargs, found_modified_time, )) return db_is_updated
def _has_db_updated_with_new_score(self, scored_block_usage_key, **kwargs): """ Returns whether the database has been updated with the expected new score values for the given problem and user. """ if kwargs['score_db_table'] == ScoreDatabaseTableEnum.courseware_student_module: score = get_score(kwargs['user_id'], scored_block_usage_key) found_modified_time = score.modified if score is not None else None elif kwargs['score_db_table'] == ScoreDatabaseTableEnum.submissions: score = sub_api.get_score( { "student_id": kwargs['anonymous_user_id'], "course_id": unicode(scored_block_usage_key.course_key), "item_id": unicode(scored_block_usage_key), "item_type": scored_block_usage_key.block_type, } ) found_modified_time = score['created_at'] if score is not None else None else: assert kwargs['score_db_table'] == ScoreDatabaseTableEnum.overrides score = GradesService().get_subsection_grade_override( user_id=kwargs['user_id'], course_key_or_id=kwargs['course_id'], usage_key_or_id=kwargs['usage_id'] ) found_modified_time = score.modified if score is not None else None if score is None: # score should be None only if it was deleted. # Otherwise, it hasn't yet been saved. db_is_updated = kwargs['score_deleted'] else: db_is_updated = found_modified_time >= from_timestamp(kwargs['expected_modified_time']) if not db_is_updated: log.info( u"Grades: tasks._has_database_updated_with_new_score is False. Task ID: {}. Kwargs: {}. Found " u"modified time: {}".format( self.request.id, kwargs, found_modified_time, ) ) return db_is_updated
def validate_timestamp(self, value): """ Ensure the request has been received in a timely manner. """ date_time = from_timestamp(value) # Ensure we converted the timestamp to a datetime if not date_time: msg = '[{}] is not a valid timestamp'.format(value) log.warning(msg) raise serializers.ValidationError(msg) elapsed = (datetime.datetime.now(pytz.UTC) - date_time).total_seconds() if elapsed > settings.CREDIT_PROVIDER_TIMESTAMP_EXPIRATION: msg = '[{value}] is too far in the past (over [{elapsed}] seconds).'.format(value=value, elapsed=elapsed) log.warning(msg) raise serializers.ValidationError(msg) return value