Beispiel #1
0
    def _credit_provider_callback(self, request_uuid, status, **kwargs):
        """
        Simulate a response from the credit provider approving
        or rejecting the credit request.

        Arguments:
            request_uuid (str): The UUID of the credit request.
            status (str): The status of the credit request.

        Keyword Arguments:
            provider_id (str): Identifier for the credit provider.
            secret_key (str): Shared secret key for signing messages.
            timestamp (datetime): Timestamp of the message.
            sig (str): Digital signature to use on messages.
            keys (dict): Override for CREDIT_PROVIDER_SECRET_KEYS setting.

        """
        provider_id = kwargs.get('provider_id', self.provider.provider_id)
        secret_key = kwargs.get('secret_key', '931433d583c84ca7ba41784bad3232e6')
        timestamp = kwargs.get('timestamp', to_timestamp(datetime.datetime.now(pytz.UTC)))
        keys = kwargs.get('keys', {self.provider.provider_id: secret_key})

        url = reverse('credit:provider_callback', args=[provider_id])

        parameters = {
            'request_uuid': request_uuid,
            'status': status,
            'timestamp': timestamp,
        }
        parameters['signature'] = kwargs.get('sig', signature(parameters, secret_key))

        with override_settings(CREDIT_PROVIDER_SECRET_KEYS=keys):
            return self.client.post(url, data=json.dumps(parameters), content_type=JSON)
Beispiel #2
0
def enqueue_subsection_update(sender, **kwargs):  # pylint: disable=unused-argument
    """
    Handles the PROBLEM_WEIGHTED_SCORE_CHANGED signal by
    enqueueing a subsection update operation to occur asynchronously.
    """
    _emit_problem_submitted_event(kwargs)
    result = recalculate_subsection_grade_v3.apply_async(
        kwargs=dict(
            user_id=kwargs['user_id'],
            anonymous_user_id=kwargs.get('anonymous_user_id'),
            course_id=kwargs['course_id'],
            usage_id=kwargs['usage_id'],
            only_if_higher=kwargs.get('only_if_higher'),
            expected_modified_time=to_timestamp(kwargs['modified']),
            score_deleted=kwargs.get('score_deleted', False),
            event_transaction_id=unicode(get_event_transaction_id()),
            event_transaction_type=unicode(get_event_transaction_type()),
            score_db_table=kwargs['score_db_table'],
        ),
        countdown=RECALCULATE_GRADE_DELAY,
    )
    log.info(
        u'Grades: Request async calculation of subsection grades with args: {}. Task [{}]'.format(
            ', '.join('{}:{}'.format(arg, kwargs[arg]) for arg in sorted(kwargs)),
            getattr(result, 'id', 'N/A'),
        )
    )
Beispiel #3
0
    def _create_override(self, request_user, subsection_grade_model, **override_data):
        """
        Helper method to create a `PersistentSubsectionGradeOverride` object
        and send a `SUBSECTION_OVERRIDE_CHANGED` signal.
        """
        override = PersistentSubsectionGradeOverride.update_or_create_override(
            requesting_user=request_user,
            subsection_grade_model=subsection_grade_model,
            feature=grades_constants.GradeOverrideFeatureEnum.gradebook,
            **override_data
        )

        set_event_transaction_type(grades_events.SUBSECTION_GRADE_CALCULATED)
        create_new_event_transaction_id()

        recalculate_subsection_grade_v3.apply(
            kwargs=dict(
                user_id=subsection_grade_model.user_id,
                anonymous_user_id=None,
                course_id=text_type(subsection_grade_model.course_id),
                usage_id=text_type(subsection_grade_model.usage_key),
                only_if_higher=False,
                expected_modified_time=to_timestamp(override.modified),
                score_deleted=False,
                event_transaction_id=six.text_type(get_event_transaction_id()),
                event_transaction_type=six.text_type(get_event_transaction_type()),
                score_db_table=grades_constants.ScoreDatabaseTableEnum.overrides,
                force_update_subsections=True,
            )
        )
        # Emit events to let our tracking system to know we updated subsection grade
        grades_events.subsection_grade_calculated(subsection_grade_model)
        return override
Beispiel #4
0
    def _credit_provider_callback(self, request_uuid, status, **kwargs):
        """
        Simulate a response from the credit provider approving
        or rejecting the credit request.

        Arguments:
            request_uuid (str): The UUID of the credit request.
            status (str): The status of the credit request.

        Keyword Arguments:
            provider_id (str): Identifier for the credit provider.
            secret_key (str): Shared secret key for signing messages.
            timestamp (datetime): Timestamp of the message.
            sig (str): Digital signature to use on messages.

        """
        provider_id = kwargs.get("provider_id", self.PROVIDER_ID)
        secret_key = kwargs.get("secret_key", TEST_CREDIT_PROVIDER_SECRET_KEY)
        timestamp = kwargs.get("timestamp", to_timestamp(datetime.datetime.now(pytz.UTC)))

        url = reverse("credit:provider_callback", args=[provider_id])

        parameters = {
            "request_uuid": request_uuid,
            "status": status,
            "timestamp": timestamp,
        }
        parameters["signature"] = kwargs.get("sig", signature(parameters, secret_key))

        return self.client.post(url, data=json.dumps(parameters), content_type=JSON)
    def post(obj, create, extracted, **kwargs):
        """
        Post-generation handler.

        Sets up parameters field.
        """
        if not obj.parameters:
            course_key = obj.course.course_key
            user = User.objects.get(username=obj.username)
            user_profile = user.profile

            obj.parameters = json.dumps({
                "request_uuid": obj.uuid,
                "timestamp": to_timestamp(datetime.datetime.now(pytz.UTC)),
                "course_org": course_key.org,
                "course_num": course_key.course,
                "course_run": course_key.run,
                "final_grade": '0.96',
                "user_username": user.username,
                "user_email": user.email,
                "user_full_name": user_profile.name,
                "user_mailing_address": "",
                "user_country": user_profile.country.code or "",
            })

        obj.save()
Beispiel #6
0
    def test_credit_provider_callback_handles_string_timestamp(self):
        request_uuid = self._create_credit_request_and_get_uuid(self.USERNAME, self.COURSE_KEY)

        # Simulate a callback from the credit provider with a timestamp
        # encoded as a string instead of an integer.
        timestamp = str(to_timestamp(datetime.datetime.now(pytz.UTC)))
        response = self._credit_provider_callback(request_uuid, "approved", timestamp=timestamp)
        self.assertEqual(response.status_code, 200)
Beispiel #7
0
    def test_credit_provider_callback_validates_timestamp(self):
        request_uuid = self._create_credit_request_and_get_uuid(self.USERNAME, self.COURSE_KEY)

        # Simulate a callback from the credit provider with a timestamp too far in the past
        # (slightly more than 15 minutes)
        # Since the message isn't timely, respond with a 403.
        timestamp = to_timestamp(datetime.datetime.now(pytz.UTC) - datetime.timedelta(0, 60 * 15 + 1))
        response = self._credit_provider_callback(request_uuid, "approved", timestamp=timestamp)
        self.assertEqual(response.status_code, 403)
Beispiel #8
0
 def test_post_with_invalid_timestamp(self, timedelta):
     """ Verify HTTP 400 is returned for requests with an invalid timestamp. """
     if timedelta == 'invalid':
         timestamp = timedelta
     else:
         timestamp = to_timestamp(datetime.datetime.now(pytz.UTC) + timedelta)
     request_uuid = self._create_credit_request_and_get_uuid()
     response = self._credit_provider_callback(request_uuid, 'approved', timestamp=timestamp)
     self.assertEqual(response.status_code, 400)
    def handle(self, *args, **options):
        if 'modified_start' not in options:
            raise CommandError('modified_start must be provided.')

        if 'modified_end' not in options:
            raise CommandError('modified_end must be provided.')

        modified_start = utc.localize(datetime.strptime(options['modified_start'], DATE_FORMAT))
        modified_end = utc.localize(datetime.strptime(options['modified_end'], DATE_FORMAT))
        event_transaction_id = create_new_event_transaction_id()
        set_event_transaction_type(PROBLEM_SUBMITTED_EVENT_TYPE)
        kwargs = {'modified__range': (modified_start, modified_end), 'module_type': 'problem'}
        for record in StudentModule.objects.filter(**kwargs):
            task_args = {
                "user_id": record.student_id,
                "course_id": unicode(record.course_id),
                "usage_id": unicode(record.module_state_key),
                "only_if_higher": False,
                "expected_modified_time": to_timestamp(record.modified),
                "score_deleted": False,
                "event_transaction_id": unicode(event_transaction_id),
                "event_transaction_type": PROBLEM_SUBMITTED_EVENT_TYPE,
                "score_db_table": ScoreDatabaseTableEnum.courseware_student_module,
            }
            recalculate_subsection_grade_v3.apply_async(kwargs=task_args)

        kwargs = {'created_at__range': (modified_start, modified_end)}
        for record in Submission.objects.filter(**kwargs):
            task_args = {
                "user_id": user_by_anonymous_id(record.student_item.student_id).id,
                "anonymous_user_id": record.student_item.student_id,
                "course_id": unicode(record.student_item.course_id),
                "usage_id": unicode(record.student_item.item_id),
                "only_if_higher": False,
                "expected_modified_time": to_timestamp(record.created_at),
                "score_deleted": False,
                "event_transaction_id": unicode(event_transaction_id),
                "event_transaction_type": PROBLEM_SUBMITTED_EVENT_TYPE,
                "score_db_table": ScoreDatabaseTableEnum.submissions,
            }
            recalculate_subsection_grade_v3.apply_async(kwargs=task_args)
Beispiel #10
0
    def set_up_course(self, enable_persistent_grades=True, create_multiple_subsections=False):
        """
        Configures the course for this test.
        """
        # pylint: disable=attribute-defined-outside-init,no-member
        self.course = CourseFactory.create(
            org='edx',
            name='course',
            run='run',
        )
        if not enable_persistent_grades:
            PersistentGradesEnabledFlag.objects.create(enabled=False)

        self.chapter = ItemFactory.create(parent=self.course, category="chapter", display_name="Chapter")
        self.sequential = ItemFactory.create(parent=self.chapter, category='sequential', display_name="Sequential1")
        self.problem = ItemFactory.create(parent=self.sequential, category='problem', display_name='Problem')

        if create_multiple_subsections:
            seq2 = ItemFactory.create(parent=self.chapter, category='sequential')
            ItemFactory.create(parent=seq2, category='problem')

        self.frozen_now_datetime = datetime.now().replace(tzinfo=pytz.UTC)
        self.frozen_now_timestamp = to_timestamp(self.frozen_now_datetime)

        self.problem_weighted_score_changed_kwargs = OrderedDict([
            ('weighted_earned', 1.0),
            ('weighted_possible', 2.0),
            ('user_id', self.user.id),
            ('anonymous_user_id', 5),
            ('course_id', unicode(self.course.id)),
            ('usage_id', unicode(self.problem.location)),
            ('only_if_higher', None),
            ('modified', self.frozen_now_datetime),
            ('score_db_table', ScoreDatabaseTableEnum.courseware_student_module),
        ])

        create_new_event_transaction_id()

        self.recalculate_subsection_grade_kwargs = OrderedDict([
            ('user_id', self.user.id),
            ('course_id', unicode(self.course.id)),
            ('usage_id', unicode(self.problem.location)),
            ('anonymous_user_id', 5),
            ('only_if_higher', None),
            ('expected_modified_time', self.frozen_now_timestamp),
            ('score_deleted', False),
            ('event_transaction_id', unicode(get_event_transaction_id())),
            ('event_transaction_type', u'edx.grades.problem.submitted'),
            ('score_db_table', ScoreDatabaseTableEnum.courseware_student_module),
        ])

        # this call caches the anonymous id on the user object, saving 4 queries in all happy path tests
        _ = anonymous_id_for_user(self.user, self.course.id)
Beispiel #11
0
    def set_up_course(self, enable_subsection_grades=True):
        """
        Configures the course for this test.
        """
        # pylint: disable=attribute-defined-outside-init,no-member
        self.course = CourseFactory.create(
            org='edx',
            name='course',
            run='run',
        )
        if not enable_subsection_grades:
            PersistentGradesEnabledFlag.objects.create(enabled=False)

        self.chapter = ItemFactory.create(parent=self.course,
                                          category="chapter",
                                          display_name="Chapter")
        self.sequential = ItemFactory.create(parent=self.chapter,
                                             category='sequential',
                                             display_name="Sequential1")
        self.problem = ItemFactory.create(parent=self.sequential,
                                          category='problem',
                                          display_name='Problem')

        self.frozen_now_datetime = datetime.now().replace(tzinfo=pytz.UTC)
        self.frozen_now_timestamp = to_timestamp(self.frozen_now_datetime)

        self.problem_weighted_score_changed_kwargs = OrderedDict([
            ('weighted_earned', 1.0),
            ('weighted_possible', 2.0),
            ('user_id', self.user.id),
            ('course_id', unicode(self.course.id)),
            ('usage_id', unicode(self.problem.location)),
            ('only_if_higher', None),
            ('modified', self.frozen_now_datetime),
        ])

        create_new_event_transaction_id()

        self.recalculate_subsection_grade_kwargs = OrderedDict([
            ('user_id', self.user.id),
            ('course_id', unicode(self.course.id)),
            ('usage_id', unicode(self.problem.location)),
            ('only_if_higher', None),
            ('expected_modified_time', self.frozen_now_timestamp),
            ('score_deleted', False),
            ('event_transaction_id', unicode(get_event_transaction_id())),
            ('event_transaction_type', u'edx.grades.problem.submitted'),
        ])

        # this call caches the anonymous id on the user object, saving 4 queries in all happy path tests
        _ = anonymous_id_for_user(self.user, self.course.id)
Beispiel #12
0
    def test_credit_provider_callback_validates_timestamp(self):
        request_uuid = self._create_credit_request_and_get_uuid(
            self.USERNAME, self.COURSE_KEY)

        # Simulate a callback from the credit provider with a timestamp too far in the past
        # (slightly more than 15 minutes)
        # Since the message isn't timely, respond with a 403.
        timestamp = to_timestamp(
            datetime.datetime.now(pytz.UTC) -
            datetime.timedelta(0, 60 * 15 + 1))
        response = self._credit_provider_callback(request_uuid,
                                                  "approved",
                                                  timestamp=timestamp)
        self.assertEqual(response.status_code, 403)
    def _run_command_and_check_output(self, task_mock, score_db_table, include_anonymous_id=False):
        self.command.handle(modified_start='2016-08-25 16:42', modified_end='2018-08-25 16:44')
        kwargs = {
            "user_id": "ID",
            "course_id": u'x/y/z',
            "usage_id": u'abc',
            "only_if_higher": False,
            "expected_modified_time": to_timestamp(utc.localize(datetime.strptime('2016-08-23 16:43', DATE_FORMAT))),
            "score_deleted": False,
            "event_transaction_id": unicode(get_event_transaction_id()),
            "event_transaction_type": u'edx.grades.problem.submitted',
            "score_db_table": score_db_table,
        }

        if include_anonymous_id:
            kwargs['anonymous_user_id'] = 'anonymousID'

        task_mock.apply_async.assert_called_with(kwargs=kwargs)
Beispiel #14
0
def enqueue_subsection_update(sender, **kwargs):  # pylint: disable=unused-argument
    """
    Handles the PROBLEM_WEIGHTED_SCORE_CHANGED or SUBSECTION_OVERRIDE_CHANGED signals by
    enqueueing a subsection update operation to occur asynchronously.
    """
    _emit_event(kwargs)
    result = recalculate_subsection_grade_v3.apply_async(
        kwargs=dict(
            user_id=kwargs['user_id'],
            anonymous_user_id=kwargs.get('anonymous_user_id'),
            course_id=kwargs['course_id'],
            usage_id=kwargs['usage_id'],
            only_if_higher=kwargs.get('only_if_higher'),
            expected_modified_time=to_timestamp(kwargs['modified']),
            score_deleted=kwargs.get('score_deleted', False),
            event_transaction_id=unicode(get_event_transaction_id()),
            event_transaction_type=unicode(get_event_transaction_type()),
            score_db_table=kwargs['score_db_table'],
        ),
        countdown=RECALCULATE_GRADE_DELAY,
    )
Beispiel #15
0
def enqueue_subsection_update(sender, **kwargs):  # pylint: disable=unused-argument
    """
    Handles the PROBLEM_WEIGHTED_SCORE_CHANGED or SUBSECTION_OVERRIDE_CHANGED signals by
    enqueueing a subsection update operation to occur asynchronously.
    """
    events.grade_updated(**kwargs)
    recalculate_subsection_grade_v3.apply_async(
        kwargs=dict(
            user_id=kwargs['user_id'],
            anonymous_user_id=kwargs.get('anonymous_user_id'),
            course_id=kwargs['course_id'],
            usage_id=kwargs['usage_id'],
            only_if_higher=kwargs.get('only_if_higher'),
            expected_modified_time=to_timestamp(kwargs['modified']),
            score_deleted=kwargs.get('score_deleted', False),
            event_transaction_id=unicode(get_event_transaction_id()),
            event_transaction_type=unicode(get_event_transaction_type()),
            score_db_table=kwargs['score_db_table'],
        ),
        countdown=RECALCULATE_GRADE_DELAY_SECONDS,
    )
Beispiel #16
0
    def _credit_provider_callback(self, request_uuid, status, **kwargs):
        """
        Simulate a response from the credit provider approving
        or rejecting the credit request.

        Arguments:
            request_uuid (str): The UUID of the credit request.
            status (str): The status of the credit request.

        Keyword Arguments:
            provider_id (str): Identifier for the credit provider.
            secret_key (str): Shared secret key for signing messages.
            timestamp (datetime): Timestamp of the message.
            sig (str): Digital signature to use on messages.
            keys (dict): Override for CREDIT_PROVIDER_SECRET_KEYS setting.

        """
        provider_id = kwargs.get('provider_id', self.provider.provider_id)
        secret_key = kwargs.get('secret_key',
                                '931433d583c84ca7ba41784bad3232e6')
        timestamp = kwargs.get('timestamp',
                               to_timestamp(datetime.datetime.now(pytz.UTC)))
        keys = kwargs.get('keys', {self.provider.provider_id: secret_key})

        url = reverse('credit:provider_callback', args=[provider_id])

        parameters = {
            'request_uuid': request_uuid,
            'status': status,
            'timestamp': timestamp,
        }
        parameters['signature'] = kwargs.get('sig',
                                             signature(parameters, secret_key))

        with override_settings(CREDIT_PROVIDER_SECRET_KEYS=keys):
            return self.client.post(url,
                                    data=json.dumps(parameters),
                                    content_type=JSON)
Beispiel #17
0
    def post(obj, create, extracted, **kwargs):
        """
        Post-generation handler.

        Sets up parameters field.
        """
        if not obj.parameters:
            course_key = obj.course.course_key
            user = User.objects.get(username=obj.username)
            user_profile = user.profile

            # pylint:disable=access-member-before-definition
            obj.parameters = json.dumps({
                "request_uuid":
                obj.uuid,
                "timestamp":
                to_timestamp(datetime.datetime.now(pytz.UTC)),
                "course_org":
                course_key.org,
                "course_num":
                course_key.course,
                "course_run":
                course_key.run,
                "final_grade":
                '0.96',
                "user_username":
                user.username,
                "user_email":
                user.email,
                "user_full_name":
                user_profile.name,
                "user_mailing_address":
                "",
                "user_country":
                user_profile.country.code or "",
            })

        obj.save()
def enqueue_subsection_update(sender, **kwargs):  # pylint: disable=unused-argument
    """
    Handles the PROBLEM_WEIGHTED_SCORE_CHANGED signal by
    enqueueing a subsection update operation to occur asynchronously.
    """
    _emit_problem_submitted_event(kwargs)
    result = recalculate_subsection_grade_v2.apply_async(kwargs=dict(
        user_id=kwargs['user_id'],
        course_id=kwargs['course_id'],
        usage_id=kwargs['usage_id'],
        only_if_higher=kwargs.get('only_if_higher'),
        expected_modified_time=to_timestamp(kwargs['modified']),
        score_deleted=kwargs.get('score_deleted', False),
        event_transaction_id=unicode(get_event_transaction_id()),
        event_transaction_type=unicode(get_event_transaction_type()),
    ))
    log.info(
        u'Grades: Request async calculation of subsection grades with args: {}. Task [{}]'
        .format(
            ', '.join('{}:{}'.format(arg, kwargs[arg])
                      for arg in sorted(kwargs)),
            getattr(result, 'id', 'N/A'),
        ))
Beispiel #19
0
    def _create_override(self, request_user, subsection_grade_model, **override_data):
        """
        Helper method to create a `PersistentSubsectionGradeOverride` object
        and send a `SUBSECTION_OVERRIDE_CHANGED` signal.
        """
        override, _ = PersistentSubsectionGradeOverride.objects.update_or_create(
            grade=subsection_grade_model,
            defaults=self._clean_override_data(override_data),
        )

        _ = PersistentSubsectionGradeOverrideHistory.objects.create(
            override_id=override.id,
            user=request_user,
            feature=PersistentSubsectionGradeOverrideHistory.GRADEBOOK,
            action=PersistentSubsectionGradeOverrideHistory.CREATE_OR_UPDATE,
        )

        set_event_transaction_type(SUBSECTION_GRADE_CALCULATED)
        create_new_event_transaction_id()

        recalculate_subsection_grade_v3.apply(
            kwargs=dict(
                user_id=subsection_grade_model.user_id,
                anonymous_user_id=None,
                course_id=text_type(subsection_grade_model.course_id),
                usage_id=text_type(subsection_grade_model.usage_key),
                only_if_higher=False,
                expected_modified_time=to_timestamp(override.modified),
                score_deleted=False,
                event_transaction_id=unicode(get_event_transaction_id()),
                event_transaction_type=unicode(get_event_transaction_type()),
                score_db_table=ScoreDatabaseTableEnum.overrides,
                force_update_subsections=True,
            )
        )
        # Emit events to let our tracking system to know we updated subsection grade
        subsection_grade_calculated(subsection_grade_model)
    def _create_override(self, request_user, subsection_grade_model, **override_data):
        """
        Helper method to create a `PersistentSubsectionGradeOverride` object
        and send a `SUBSECTION_OVERRIDE_CHANGED` signal.
        """
        override, _ = PersistentSubsectionGradeOverride.objects.update_or_create(
            grade=subsection_grade_model,
            defaults=self._clean_override_data(override_data),
        )

        _ = PersistentSubsectionGradeOverrideHistory.objects.create(
            override_id=override.id,
            user=request_user,
            feature=PersistentSubsectionGradeOverrideHistory.GRADEBOOK,
            action=PersistentSubsectionGradeOverrideHistory.CREATE_OR_UPDATE,
        )

        set_event_transaction_type(SUBSECTION_GRADE_CALCULATED)
        create_new_event_transaction_id()

        recalculate_subsection_grade_v3.apply(
            kwargs=dict(
                user_id=subsection_grade_model.user_id,
                anonymous_user_id=None,
                course_id=text_type(subsection_grade_model.course_id),
                usage_id=text_type(subsection_grade_model.usage_key),
                only_if_higher=False,
                expected_modified_time=to_timestamp(override.modified),
                score_deleted=False,
                event_transaction_id=unicode(get_event_transaction_id()),
                event_transaction_type=unicode(get_event_transaction_type()),
                score_db_table=ScoreDatabaseTableEnum.overrides,
                force_update_subsections=True,
            )
        )
        # Emit events to let our tracking system to know we updated subsection grade
        subsection_grade_calculated(subsection_grade_model)
from util.date_utils import to_timestamp

from ..constants import ScoreDatabaseTableEnum
from ..signals.handlers import (
    enqueue_subsection_update,
    submissions_score_set_handler,
    submissions_score_reset_handler,
    problem_raw_score_changed_handler,
)

UUID_REGEX = re.compile(
    ur'%(hex)s{8}-%(hex)s{4}-%(hex)s{4}-%(hex)s{4}-%(hex)s{12}' %
    {'hex': u'[0-9a-f]'})

FROZEN_NOW_DATETIME = datetime.now().replace(tzinfo=pytz.UTC)
FROZEN_NOW_TIMESTAMP = to_timestamp(FROZEN_NOW_DATETIME)

SUBMISSION_SET_KWARGS = {
    'points_possible': 10,
    'points_earned': 5,
    'anonymous_user_id': 'anonymous_id',
    'course_id': 'CourseID',
    'item_id': 'i4x://org/course/usage/123456',
    'created_at': FROZEN_NOW_TIMESTAMP,
}

SUBMISSION_RESET_KWARGS = {
    'anonymous_user_id': 'anonymous_id',
    'course_id': 'CourseID',
    'item_id': 'i4x://org/course/usage/123456',
    'created_at': FROZEN_NOW_TIMESTAMP,
Beispiel #22
0
class CreditProviderCallbackViewTests(UserMixin, TestCase):
    """ Tests for CreditProviderCallbackView. """
    def setUp(self):
        super(CreditProviderCallbackViewTests, self).setUp()

        # Authentication should NOT be required for this endpoint.
        self.client.logout()

        self.provider = CreditProviderFactory()
        self.path = reverse('credit:provider_callback',
                            args=[self.provider.provider_id])
        self.eligibility = CreditEligibilityFactory(
            username=self.user.username)

    def _credit_provider_callback(self, request_uuid, status, **kwargs):
        """
        Simulate a response from the credit provider approving
        or rejecting the credit request.

        Arguments:
            request_uuid (str): The UUID of the credit request.
            status (str): The status of the credit request.

        Keyword Arguments:
            provider_id (str): Identifier for the credit provider.
            secret_key (str): Shared secret key for signing messages.
            timestamp (datetime): Timestamp of the message.
            sig (str): Digital signature to use on messages.
            keys (dict): Override for CREDIT_PROVIDER_SECRET_KEYS setting.

        """
        provider_id = kwargs.get('provider_id', self.provider.provider_id)
        secret_key = kwargs.get('secret_key',
                                '931433d583c84ca7ba41784bad3232e6')
        timestamp = kwargs.get('timestamp',
                               to_timestamp(datetime.datetime.now(pytz.UTC)))
        keys = kwargs.get('keys', {self.provider.provider_id: secret_key})

        url = reverse('credit:provider_callback', args=[provider_id])

        parameters = {
            'request_uuid': request_uuid,
            'status': status,
            'timestamp': timestamp,
        }
        parameters['signature'] = kwargs.get('sig',
                                             signature(parameters, secret_key))

        with override_settings(CREDIT_PROVIDER_SECRET_KEYS=keys):
            return self.client.post(url,
                                    data=json.dumps(parameters),
                                    content_type=JSON)

    def _create_credit_request_and_get_uuid(self,
                                            username=None,
                                            course_key=None):
        """ Initiate a request for credit and return the request UUID. """
        username = username or self.user.username
        course = CreditCourse.objects.get(
            course_key=course_key) if course_key else self.eligibility.course
        credit_request = CreditRequestFactory(username=username,
                                              course=course,
                                              provider=self.provider)
        return credit_request.uuid

    def _assert_request_status(self, uuid, expected_status):
        """ Check the status of a credit request. """
        request = CreditRequest.objects.get(uuid=uuid)
        self.assertEqual(request.status, expected_status)

    def test_post_invalid_provider_id(self):
        """ Verify the endpoint returns HTTP 404 if the provider does not exist. """
        provider_id = 'fakey-provider'
        self.assertFalse(
            CreditProvider.objects.filter(provider_id=provider_id).exists())

        path = reverse('credit:provider_callback', args=[provider_id])
        response = self.client.post(path, {})
        self.assertEqual(response.status_code, 404)

    def test_post_with_invalid_signature(self):
        """ Verify the endpoint returns HTTP 403 if a request is received with an invalid signature. """
        request_uuid = self._create_credit_request_and_get_uuid()

        # Simulate a callback from the credit provider with an invalid signature
        # Since the signature is invalid, we respond with a 403 Not Authorized.
        response = self._credit_provider_callback(request_uuid,
                                                  "approved",
                                                  sig="invalid")
        self.assertEqual(response.status_code, 403)

    @ddt.data(
        to_timestamp(
            datetime.datetime.now(pytz.UTC) -
            datetime.timedelta(0, 60 * 15 + 1)), 'invalid')
    def test_post_with_invalid_timestamp(self, timestamp):
        """ Verify HTTP 400 is returned for requests with an invalid timestamp. """
        request_uuid = self._create_credit_request_and_get_uuid()
        response = self._credit_provider_callback(request_uuid,
                                                  'approved',
                                                  timestamp=timestamp)
        self.assertEqual(response.status_code, 400)

    def test_post_with_string_timestamp(self):
        """ Verify the endpoint supports timestamps transmitted as strings instead of integers. """
        request_uuid = self._create_credit_request_and_get_uuid()
        timestamp = str(to_timestamp(datetime.datetime.now(pytz.UTC)))
        response = self._credit_provider_callback(request_uuid,
                                                  'approved',
                                                  timestamp=timestamp)
        self.assertEqual(response.status_code, 200)

    def test_credit_provider_callback_is_idempotent(self):
        """ Verify clients can make subsequent calls with the same status. """
        request_uuid = self._create_credit_request_and_get_uuid()

        # Initially, the status should be "pending"
        self._assert_request_status(request_uuid, "pending")

        # First call sets the status to approved
        self._credit_provider_callback(request_uuid, 'approved')
        self._assert_request_status(request_uuid, "approved")

        # Second call succeeds as well; status is still approved
        self._credit_provider_callback(request_uuid, 'approved')
        self._assert_request_status(request_uuid, "approved")

    def test_credit_provider_invalid_status(self):
        """ Verify requests with an invalid status value return HTTP 400. """
        request_uuid = self._create_credit_request_and_get_uuid()
        response = self._credit_provider_callback(request_uuid, 'invalid')
        self.assertEqual(response.status_code, 400)

    def test_request_associated_with_another_provider(self):
        """ Verify the endpoint returns HTTP 404 if a request is received for the incorrect provider. """
        other_provider_id = 'other-provider'
        other_provider_secret_key = '1d01f067a5a54b0b8059f7095a7c636d'

        # Create an additional credit provider
        CreditProvider.objects.create(provider_id=other_provider_id,
                                      enable_integration=True)

        # Initiate a credit request with the first provider
        request_uuid = self._create_credit_request_and_get_uuid()

        # Attempt to update the request status for a different provider
        response = self._credit_provider_callback(
            request_uuid,
            'approved',
            provider_id=other_provider_id,
            secret_key=other_provider_secret_key,
            keys={other_provider_id: other_provider_secret_key})

        # Response should be a 404 to avoid leaking request UUID values to other providers.
        self.assertEqual(response.status_code, 404)

        # Request status should still be 'pending'
        self._assert_request_status(request_uuid, 'pending')

    def test_credit_provider_key_not_configured(self):
        """ Verify the endpoint returns HTTP 403 if the provider has no key configured. """
        request_uuid = self._create_credit_request_and_get_uuid()

        # Callback from the provider is not authorized, because the shared secret isn't configured.
        with override_settings(CREDIT_PROVIDER_SECRET_KEYS={}):
            response = self._credit_provider_callback(request_uuid,
                                                      'approved',
                                                      keys={})
            self.assertEqual(response.status_code, 403)
Beispiel #23
0
 def test_post_with_string_timestamp(self):
     """ Verify the endpoint supports timestamps transmitted as strings instead of integers. """
     request_uuid = self._create_credit_request_and_get_uuid()
     timestamp = str(to_timestamp(datetime.datetime.now(pytz.UTC)))
     response = self._credit_provider_callback(request_uuid, 'approved', timestamp=timestamp)
     self.assertEqual(response.status_code, 200)
from submissions.models import score_reset, score_set
from util.date_utils import to_timestamp

from ..constants import ScoreDatabaseTableEnum
from ..signals.handlers import (
    disconnect_submissions_signal_receiver,
    problem_raw_score_changed_handler,
    submissions_score_reset_handler,
    submissions_score_set_handler,
)
from ..signals.signals import PROBLEM_RAW_SCORE_CHANGED

UUID_REGEX = re.compile(u'%(hex)s{8}-%(hex)s{4}-%(hex)s{4}-%(hex)s{4}-%(hex)s{12}' % {'hex': u'[0-9a-f]'})

FROZEN_NOW_DATETIME = datetime.now().replace(tzinfo=pytz.UTC)
FROZEN_NOW_TIMESTAMP = to_timestamp(FROZEN_NOW_DATETIME)

SUBMISSIONS_SCORE_SET_HANDLER = 'submissions_score_set_handler'
SUBMISSIONS_SCORE_RESET_HANDLER = 'submissions_score_reset_handler'
HANDLERS = {
    SUBMISSIONS_SCORE_SET_HANDLER: submissions_score_set_handler,
    SUBMISSIONS_SCORE_RESET_HANDLER: submissions_score_reset_handler,
}

SUBMISSION_SET_KWARGS = 'submission_set_kwargs'
SUBMISSION_RESET_KWARGS = 'submission_reset_kwargs'
SUBMISSION_KWARGS = {
    SUBMISSION_SET_KWARGS: {
        'points_possible': 10,
        'points_earned': 5,
        'anonymous_user_id': 'anonymous_id',
Beispiel #25
0
def create_credit_request(course_key, provider_id, username):
    """
    Initiate a request for credit from a credit provider.

    This will return the parameters that the user's browser will need to POST
    to the credit provider.  It does NOT calculate the signature.

    Only users who are eligible for credit (have satisfied all credit requirements) are allowed to make requests.

    A provider can be configured either with *integration enabled* or not.
    If automatic integration is disabled, this method will simply return
    a URL to the credit provider and method set to "GET", so the student can
    visit the URL and request credit directly.  No database record will be created
    to track these requests.

    If automatic integration *is* enabled, then this will also return the parameters
    that the user's browser will need to POST to the credit provider.
    These parameters will be digitally signed using a secret key shared with the credit provider.

    A database record will be created to track the request with a 32-character UUID.
    The returned dictionary can be used by the user's browser to send a POST request to the credit provider.

    If a pending request already exists, this function should return a request description with the same UUID.
    (Other parameters, such as the user's full name may be different than the original request).

    If a completed request (either accepted or rejected) already exists, this function will
    raise an exception.  Users are not allowed to make additional requests once a request
    has been completed.

    Arguments:
        course_key (CourseKey): The identifier for the course.
        provider_id (str): The identifier of the credit provider.
        username (str): The user initiating the request.

    Returns: dict

    Raises:
        UserIsNotEligible: The user has not satisfied eligibility requirements for credit.
        CreditProviderNotConfigured: The credit provider has not been configured for this course.
        RequestAlreadyCompleted: The user has already submitted a request and received a response
            from the credit provider.

    Example Usage:
        >>> create_credit_request(course.id, "hogwarts", "ron")
        {
            "url": "https://credit.example.com/request",
            "method": "POST",
            "parameters": {
                "request_uuid": "557168d0f7664fe59097106c67c3f847",
                "timestamp": 1434631630,
                "course_org": "HogwartsX",
                "course_num": "Potions101",
                "course_run": "1T2015",
                "final_grade": "0.95",
                "user_username": "******",
                "user_email": "*****@*****.**",
                "user_full_name": "Ron Weasley",
                "user_mailing_address": "",
                "user_country": "US",
                "signature": "cRCNjkE4IzY+erIjRwOQCpRILgOvXx4q2qvx141BCqI="
            }
        }

    """
    try:
        user_eligibility = CreditEligibility.objects.select_related(
            'course').get(username=username, course__course_key=course_key)
        credit_course = user_eligibility.course
        credit_provider = CreditProvider.objects.get(provider_id=provider_id)
    except CreditEligibility.DoesNotExist:
        log.warning(
            u'User "%s" tried to initiate a request for credit in course "%s", '
            u'but the user is not eligible for credit', username, course_key)
        raise UserIsNotEligible
    except CreditProvider.DoesNotExist:
        log.error(u'Credit provider with ID "%s" has not been configured.',
                  provider_id)
        raise CreditProviderNotConfigured

    # Check if we've enabled automatic integration with the credit
    # provider.  If not, we'll show the user a link to a URL
    # where the user can request credit directly from the provider.
    # Note that we do NOT track these requests in our database,
    # since the state would always be "pending" (we never hear back).
    if not credit_provider.enable_integration:
        return {
            "url": credit_provider.provider_url,
            "method": "GET",
            "parameters": {}
        }
    else:
        # If automatic credit integration is enabled, then try
        # to retrieve the shared signature *before* creating the request.
        # That way, if there's a misconfiguration, we won't have requests
        # in our system that we know weren't sent to the provider.
        shared_secret_key = get_shared_secret_key(credit_provider.provider_id)
        if shared_secret_key is None:
            msg = u'Credit provider with ID "{provider_id}" does not have a secret key configured.'.format(
                provider_id=credit_provider.provider_id)
            log.error(msg)
            raise CreditProviderNotConfigured(msg)

    # Initiate a new request if one has not already been created
    credit_request, created = CreditRequest.objects.get_or_create(
        course=credit_course,
        provider=credit_provider,
        username=username,
    )

    # Check whether we've already gotten a response for a request,
    # If so, we're not allowed to issue any further requests.
    # Skip checking the status if we know that we just created this record.
    if not created and credit_request.status != "pending":
        log.warning((
            u'Cannot initiate credit request because the request with UUID "%s" '
            u'exists with status "%s"'), credit_request.uuid,
                    credit_request.status)
        raise RequestAlreadyCompleted

    if created:
        credit_request.uuid = uuid.uuid4().hex

    # Retrieve user account and profile info
    user = User.objects.select_related('profile').get(username=username)

    # Retrieve the final grade from the eligibility table
    try:
        final_grade = CreditRequirementStatus.objects.get(
            username=username,
            requirement__namespace="grade",
            requirement__name="grade",
            requirement__course__course_key=course_key,
            status="satisfied").reason["final_grade"]

        # NOTE (CCB): Limiting the grade to seven characters is a hack for ASU.
        if len(unicode(final_grade)) > 7:
            final_grade = u'{:.5f}'.format(final_grade)
        else:
            final_grade = unicode(final_grade)

    except (CreditRequirementStatus.DoesNotExist, TypeError, KeyError):
        log.exception(
            "Could not retrieve final grade from the credit eligibility table "
            "for user %s in course %s.", user.id, course_key)
        raise UserIsNotEligible

    parameters = {
        "request_uuid":
        credit_request.uuid,
        "timestamp":
        to_timestamp(datetime.datetime.now(pytz.UTC)),
        "course_org":
        course_key.org,
        "course_num":
        course_key.course,
        "course_run":
        course_key.run,
        "final_grade":
        final_grade,
        "user_username":
        user.username,
        "user_email":
        user.email,
        "user_full_name":
        user.profile.name,
        "user_mailing_address":
        "",
        "user_country": (user.profile.country.code
                         if user.profile.country.code is not None else ""),
    }

    credit_request.parameters = parameters
    credit_request.save()

    if created:
        log.info(u'Created new request for credit with UUID "%s"',
                 credit_request.uuid)
    else:
        log.info(
            u'Updated request for credit with UUID "%s" so the user can re-issue the request',
            credit_request.uuid)

    # Sign the parameters using a secret key we share with the credit provider.
    parameters["signature"] = signature(parameters, shared_secret_key)

    return {
        "url": credit_provider.provider_url,
        "method": "POST",
        "parameters": parameters
    }
Beispiel #26
0
def create_credit_request(course_key, provider_id, username):
    """
    Initiate a request for credit from a credit provider.

    This will return the parameters that the user's browser will need to POST
    to the credit provider.  It does NOT calculate the signature.

    Only users who are eligible for credit (have satisfied all credit requirements) are allowed to make requests.

    A provider can be configured either with *integration enabled* or not.
    If automatic integration is disabled, this method will simply return
    a URL to the credit provider and method set to "GET", so the student can
    visit the URL and request credit directly.  No database record will be created
    to track these requests.

    If automatic integration *is* enabled, then this will also return the parameters
    that the user's browser will need to POST to the credit provider.
    These parameters will be digitally signed using a secret key shared with the credit provider.

    A database record will be created to track the request with a 32-character UUID.
    The returned dictionary can be used by the user's browser to send a POST request to the credit provider.

    If a pending request already exists, this function should return a request description with the same UUID.
    (Other parameters, such as the user's full name may be different than the original request).

    If a completed request (either accepted or rejected) already exists, this function will
    raise an exception.  Users are not allowed to make additional requests once a request
    has been completed.

    Arguments:
        course_key (CourseKey): The identifier for the course.
        provider_id (str): The identifier of the credit provider.
        user (User): The user initiating the request.

    Returns: dict

    Raises:
        UserIsNotEligible: The user has not satisfied eligibility requirements for credit.
        CreditProviderNotConfigured: The credit provider has not been configured for this course.
        RequestAlreadyCompleted: The user has already submitted a request and received a response
            from the credit provider.

    Example Usage:
        >>> create_credit_request(course.id, "hogwarts", "ron")
        {
            "url": "https://credit.example.com/request",
            "method": "POST",
            "parameters": {
                "request_uuid": "557168d0f7664fe59097106c67c3f847",
                "timestamp": 1434631630,
                "course_org": "HogwartsX",
                "course_num": "Potions101",
                "course_run": "1T2015",
                "final_grade": 0.95,
                "user_username": "******",
                "user_email": "*****@*****.**",
                "user_full_name": "Ron Weasley",
                "user_mailing_address": "",
                "user_country": "US",
                "signature": "cRCNjkE4IzY+erIjRwOQCpRILgOvXx4q2qvx141BCqI="
            }
        }

    """
    try:
        user_eligibility = CreditEligibility.objects.select_related('course').get(
            username=username,
            course__course_key=course_key
        )
        credit_course = user_eligibility.course
        credit_provider = CreditProvider.objects.get(provider_id=provider_id)
    except CreditEligibility.DoesNotExist:
        log.warning(
            u'User "%s" tried to initiate a request for credit in course "%s", '
            u'but the user is not eligible for credit',
            username, course_key
        )
        raise UserIsNotEligible
    except CreditProvider.DoesNotExist:
        log.error(u'Credit provider with ID "%s" has not been configured.', provider_id)
        raise CreditProviderNotConfigured

    # Check if we've enabled automatic integration with the credit
    # provider.  If not, we'll show the user a link to a URL
    # where the user can request credit directly from the provider.
    # Note that we do NOT track these requests in our database,
    # since the state would always be "pending" (we never hear back).
    if not credit_provider.enable_integration:
        return {
            "url": credit_provider.provider_url,
            "method": "GET",
            "parameters": {}
        }
    else:
        # If automatic credit integration is enabled, then try
        # to retrieve the shared signature *before* creating the request.
        # That way, if there's a misconfiguration, we won't have requests
        # in our system that we know weren't sent to the provider.
        shared_secret_key = get_shared_secret_key(credit_provider.provider_id)
        if shared_secret_key is None:
            msg = u'Credit provider with ID "{provider_id}" does not have a secret key configured.'.format(
                provider_id=credit_provider.provider_id
            )
            log.error(msg)
            raise CreditProviderNotConfigured(msg)

    # Initiate a new request if one has not already been created
    credit_request, created = CreditRequest.objects.get_or_create(
        course=credit_course,
        provider=credit_provider,
        username=username,
    )

    # Check whether we've already gotten a response for a request,
    # If so, we're not allowed to issue any further requests.
    # Skip checking the status if we know that we just created this record.
    if not created and credit_request.status != "pending":
        log.warning(
            (
                u'Cannot initiate credit request because the request with UUID "%s" '
                u'exists with status "%s"'
            ), credit_request.uuid, credit_request.status
        )
        raise RequestAlreadyCompleted

    if created:
        credit_request.uuid = uuid.uuid4().hex

    # Retrieve user account and profile info
    user = User.objects.select_related('profile').get(username=username)

    # Retrieve the final grade from the eligibility table
    try:
        final_grade = CreditRequirementStatus.objects.get(
            username=username,
            requirement__namespace="grade",
            requirement__name="grade",
            status="satisfied"
        ).reason["final_grade"]
    except (CreditRequirementStatus.DoesNotExist, TypeError, KeyError):
        log.exception(
            "Could not retrieve final grade from the credit eligibility table "
            "for user %s in course %s.",
            user.id, course_key
        )
        raise UserIsNotEligible

    parameters = {
        "request_uuid": credit_request.uuid,
        "timestamp": to_timestamp(datetime.datetime.now(pytz.UTC)),
        "course_org": course_key.org,
        "course_num": course_key.course,
        "course_run": course_key.run,
        "final_grade": final_grade,
        "user_username": user.username,
        "user_email": user.email,
        "user_full_name": user.profile.name,
        "user_mailing_address": (
            user.profile.mailing_address
            if user.profile.mailing_address is not None
            else ""
        ),
        "user_country": (
            user.profile.country.code
            if user.profile.country.code is not None
            else ""
        ),
    }

    credit_request.parameters = parameters
    credit_request.save()

    if created:
        log.info(u'Created new request for credit with UUID "%s"', credit_request.uuid)
    else:
        log.info(
            u'Updated request for credit with UUID "%s" so the user can re-issue the request',
            credit_request.uuid
        )

    # Sign the parameters using a secret key we share with the credit provider.
    parameters["signature"] = signature(parameters, shared_secret_key)

    return {
        "url": credit_provider.provider_url,
        "method": "POST",
        "parameters": parameters
    }