Ejemplo n.º 1
0
 def post(self, request, user_id):  # pylint: disable=unused-argument
     """
     Deletes all user data for the particular user_id
     from all configured backends
     """
     if not request.user.has_perm('accounts.can_retire_user'):
         return Response(status=403)
     results = {}
     code = 200
     seen = set()
     # pylint: disable=no-member
     attempts = ProctoredExamStudentAttempt.objects.filter(user_id=user_id).select_related('proctored_exam')
     if attempts:
         for attempt in attempts:
             backend_name = attempt.proctored_exam.backend
             if backend_name in seen or not attempt.taking_as_proctored:
                 continue
             backend_user_id = obscured_user_id(user_id, backend_name)
             LOG.info(u'retiring user %s from %s', user_id, backend_name)
             try:
                 result = get_backend_provider(name=backend_name).retire_user(backend_user_id)
             except ProctoredBaseException:
                 LOG.exception(u'attempting to delete %s (%s) from %s', user_id, backend_user_id, backend_name)
                 result = False
             if result is not None:
                 results[backend_name] = result
                 if not result:
                     code = 500
             seen.add(backend_name)
     return Response(data=results, status=code)
Ejemplo n.º 2
0
 def post(self, request, user_id):  # pylint: disable=unused-argument
     """
     Deletes all user data for the particular user_id
     from all configured backends
     """
     if not request.user.has_perm('accounts.can_retire_user'):
         return Response(status=403)
     results = {}
     code = 200
     seen = set()
     attempts = ProctoredExamStudentAttempt.objects.filter(user_id=user_id).select_related('proctored_exam')
     if attempts:
         for attempt in attempts:
             backend_name = attempt.proctored_exam.backend
             if backend_name in seen or not attempt.taking_as_proctored:
                 continue
             backend_user_id = obscured_user_id(user_id, backend_name)
             LOG.info('retiring user %s from %s', user_id, backend_name)
             try:
                 result = get_backend_provider(name=backend_name).retire_user(backend_user_id)
             except ProctoredBaseException:
                 LOG.exception('attempting to delete %s (%s) from %s', user_id, backend_user_id, backend_name)
                 result = False
             if result is not None:
                 results[backend_name] = result
                 if not result:
                     code = 500
             seen.add(backend_name)
     return Response(data=results, status=code)
Ejemplo n.º 3
0
 def post(self, request, user_id):  # pylint: disable=unused-argument
     """
     Deletes all user data for the particular user_id
     from all configured backends
     """
     if not request.user.has_perm('accounts.can_retire_user'):
         return Response(status=403)
     from django.apps import apps
     choices = apps.get_app_config('edx_proctoring').get_backend_choices()
     results = {}
     code = 200
     if ProctoredExamStudentAttempt.objects.filter(user_id=user_id):
         for backend_name, verbose_name in choices:
             backend_user_id = obscured_user_id(user_id, backend_name)
             LOG.info('retiring user %s from %s', user_id, backend_name)
             try:
                 result = get_backend_provider(name=backend_name).retire_user(backend_user_id)
             except ProctoredBaseException:
                 LOG.exception('attempting to delete %s (%s) from %s', user_id, backend_user_id, verbose_name)
                 result = False
             if result is not None:
                 results[backend_name] = result
                 if not result:
                     code = 500
     return Response(data=results, status=code)
Ejemplo n.º 4
0
    def get(self, request, course_id, exam_id=None):
        """
        Redirect to dashboard for a given course and optional exam_id
        """
        exam = None
        attempt_id = None
        ext_exam_id = None
        show_configuration_dashboard = False

        if exam_id:
            exam = get_exam_by_id(exam_id)
            # the exam_id in the url is our database id (for ease of lookups)
            # but the backend needs its external id for the instructor dashboard
            ext_exam_id = exam['external_id']
            attempt_id = request.GET.get('attempt', None)

            # only show the configuration dashboard if an exam_id is passed in
            show_configuration_dashboard = request.GET.get('config', '').lower() == 'true'

        else:
            found_backend = None
            for exam in get_all_exams_for_course(course_id, True):
                exam_backend = exam['backend'] or settings.PROCTORING_BACKENDS.get('DEFAULT', None)
                if found_backend and exam_backend != found_backend:
                    # In this case, what are we supposed to do?!
                    # It should not be possible to get in this state, because
                    # course teams will be prevented from updating the backend after the course start date
                    error_message = "Multiple backends for course %r %r != %r" % (course_id,
                                                                                  found_backend,
                                                                                  exam['backend'])
                    return Response(data=error_message, status=400)
                else:
                    found_backend = exam_backend
        if exam is None:
            error = _('No exams in course {course_id}.').format(course_id=course_id)
        else:
            backend = get_backend_provider(exam)
            if backend:
                user = {
                    'id': obscured_user_id(request.user.id, exam['backend']),
                    'full_name': request.user.get_full_name(),
                    'email': request.user.email
                }

                url = backend.get_instructor_url(
                    exam['course_id'],
                    user,
                    exam_id=ext_exam_id,
                    attempt_id=attempt_id,
                    show_configuration_dashboard=show_configuration_dashboard
                )
                if url:
                    return redirect(url)
                else:
                    error = _('No instructor dashboard for {proctor_service}').format(
                        proctor_service=backend.verbose_name)
            else:
                error = _('No proctored exams in course {course_id}').format(course_id=course_id)
        return Response(data=error, status=404, headers={'X-Frame-Options': 'sameorigin'})
Ejemplo n.º 5
0
    def get(self, request, course_id, exam_id=None):
        """
        Redirect to dashboard for a given course and optional exam_id
        """
        exam = None
        attempt_id = None
        ext_exam_id = None
        show_configuration_dashboard = False

        if exam_id:
            exam = get_exam_by_id(exam_id)
            # the exam_id in the url is our database id (for ease of lookups)
            # but the backend needs its external id for the instructor dashboard
            ext_exam_id = exam['external_id']
            attempt_id = request.GET.get('attempt', None)

            # only show the configuration dashboard if an exam_id is passed in
            show_configuration_dashboard = request.GET.get('config', '').lower() == 'true'

        else:
            found_backend = None
            for exam in get_all_exams_for_course(course_id, True):
                exam_backend = exam['backend'] or settings.PROCTORING_BACKENDS.get('DEFAULT', None)
                if found_backend and exam_backend != found_backend:
                    # In this case, what are we supposed to do?!
                    # It should not be possible to get in this state, because
                    # course teams will be prevented from updating the backend after the course start date
                    error_message = "Multiple backends for course %r %r != %r" % (course_id,
                                                                                  found_backend,
                                                                                  exam['backend'])
                    return Response(data=error_message, status=400)
                else:
                    found_backend = exam_backend
        if exam is None:
            error = _('No exams in course {course_id}.').format(course_id=course_id)
        else:
            backend = get_backend_provider(exam)
            if backend:
                user = {
                    'id': obscured_user_id(request.user.id, exam['backend']),
                    'full_name': request.user.get_full_name(),
                    'email': request.user.email
                }

                url = backend.get_instructor_url(
                    exam['course_id'],
                    user,
                    exam_id=ext_exam_id,
                    attempt_id=attempt_id,
                    show_configuration_dashboard=show_configuration_dashboard
                )
                if url:
                    return redirect(url)
                else:
                    error = _('No instructor dashboard for {proctor_service}').format(
                        proctor_service=backend.verbose_name)
            else:
                error = _('No proctored exams in course {course_id}').format(course_id=course_id)
        return Response(data=error, status=404, headers={'X-Frame-Options': 'sameorigin'})
Ejemplo n.º 6
0
    def post(self, request):
        """
        Post callback handler
        """
        provider = get_backend_provider({'backend': 'software_secure'})

        # call down into the underlying provider code
        attempt_code = request.data.get('examMetaData', {}).get('examCode')
        attempt_obj, is_archived = locate_attempt_by_attempt_code(attempt_code)
        if not attempt_obj:
            # still can't find, error out
            err_msg = (u'Could not locate attempt_code: {attempt_code}'.format(
                attempt_code=attempt_code))
            raise StudentExamAttemptDoesNotExistsException(err_msg)
        serialized = ProctoredExamStudentAttemptSerializer(attempt_obj).data
        serialized['is_archived'] = is_archived
        self.make_review(serialized, request.data, backend=provider)
        return Response('OK')
Ejemplo n.º 7
0
    def post(self, request):
        """
        Post callback handler
        """
        provider = get_backend_provider({'backend': 'software_secure'})

        # call down into the underlying provider code
        attempt_code = request.data.get('examMetaData', {}).get('examCode')
        attempt_obj, is_archived = locate_attempt_by_attempt_code(attempt_code)
        if not attempt_obj:
            # still can't find, error out
            err_msg = (
                'Could not locate attempt_code: {attempt_code}'.format(attempt_code=attempt_code)
            )
            raise StudentExamAttemptDoesNotExistsException(err_msg)
        serialized = ProctoredExamStudentAttemptSerializer(attempt_obj).data
        serialized['is_archived'] = is_archived
        self.make_review(serialized,
                         request.data,
                         backend=provider)
        return Response('OK')
Ejemplo n.º 8
0
    def get(self, request, course_id, exam_id=None):
        """
        Redirect to dashboard for a given course and optional exam_id
        """
        exam = None
        backend = None
        ext_exam_id = None
        attempt_id = None
        show_configuration_dashboard = False

        if exam_id:
            exam = get_exam_by_id(exam_id)
            backend = get_backend_provider(exam=exam)
            # the exam_id in the url is our database id (for ease of lookups)
            # but the backend needs its external id for the instructor dashboard
            ext_exam_id = exam['external_id']
            attempt_id = request.GET.get('attempt', None)

            # only show the configuration dashboard if an exam_id is passed in
            show_configuration_dashboard = request.GET.get('config', '').lower() == 'true'
        else:
            existing_backend_name = None
            for exam in get_all_exams_for_course(course_id, True):
                if not exam.get('is_proctored'):
                    # We should only get backends of exams which are configured to be proctored
                    continue

                exam_backend_name = exam.get('backend')
                backend = get_backend_provider(name=exam_backend_name)
                if existing_backend_name and exam_backend_name != existing_backend_name:
                    # In this case, what are we supposed to do?!
                    # It should not be possible to get in this state, because
                    # course teams will be prevented from updating the backend after the course start date
                    error_message = u"Multiple backends for course %r %r != %r" % (
                        course_id,
                        existing_backend_name,
                        exam_backend_name
                    )
                    return Response(data=error_message, status=400)
                else:
                    existing_backend_name = exam_backend_name

        if not exam:
            return Response(
                data=_(u'No exams in course {course_id}.').format(course_id=course_id),
                status=404,
                headers={'X-Frame-Options': 'sameorigin'}
            )
        if not backend:
            return Response(
                data=_(u'No proctored exams in course {course_id}').format(course_id=course_id),
                status=404,
                headers={'X-Frame-Options': 'sameorigin'}
            )
        user = {
            'id': obscured_user_id(request.user.id, exam['backend']),
            'full_name': request.user.profile.name,
            'email': request.user.email
        }

        url = backend.get_instructor_url(
            exam['course_id'],
            user,
            exam_id=ext_exam_id,
            attempt_id=attempt_id,
            show_configuration_dashboard=show_configuration_dashboard
        )
        if not url:
            return Response(
                data=_(u'No instructor dashboard for {proctor_service}').format(
                    proctor_service=backend.verbose_name
                ),
                status=404,
                headers={'X-Frame-Options': 'sameorigin'}
            )
        return redirect(url)
Ejemplo n.º 9
0
    def make_review(self, attempt, data, backend=None):
        """
        Save the review and review comments
        """
        attempt_code = attempt['attempt_code']
        if not backend:
            backend = get_backend_provider(attempt['proctored_exam'])

        # this method should convert the payload into a normalized format
        backend_review = backend.on_review_callback(attempt, data)

        # do we already have a review for this attempt?!? We may not allow updates
        review = ProctoredExamSoftwareSecureReview.get_review_by_attempt_code(attempt_code)

        if review:
            if not constants.ALLOW_REVIEW_UPDATES:
                err_msg = (
                    u'We already have a review submitted regarding '
                    u'attempt_code {attempt_code}. We do not allow for updates!'.format(
                        attempt_code=attempt_code
                    )
                )
                raise ProctoredExamReviewAlreadyExists(err_msg)

            # we allow updates
            warn_msg = (
                u'We already have a review submitted from our proctoring provider regarding '
                u'attempt_code {attempt_code}. We have been configured to allow for '
                u'updates and will continue...'.format(
                    attempt_code=attempt_code
                )
            )
            LOG.warning(warn_msg)
        else:
            # this is first time we've received this attempt_code, so
            # make a new record in the review table
            review = ProctoredExamSoftwareSecureReview()

        # first, validate that the backend review status is valid
        ReviewStatus.validate(backend_review['status'])
        # For now, we'll convert the standard review status to the old
        # software secure review status.
        # In the future, the old data should be standardized.
        review.review_status = SoftwareSecureReviewStatus.from_standard_status.get(backend_review['status'])

        review.attempt_code = attempt_code
        review.raw_data = json.dumps(data)
        review.student_id = attempt['user']['id']
        review.exam_id = attempt['proctored_exam']['id']

        try:
            review.reviewed_by = get_user_model().objects.get(email=data['reviewed_by'])
        except (ObjectDoesNotExist, KeyError):
            review.reviewed_by = None

        # If the reviewing user is a user in the system (user may be None for automated reviews) and does
        # not have permission to submit a review, log a warning.
        course_id = attempt['proctored_exam']['course_id']
        if review.reviewed_by is not None and not is_user_course_or_global_staff(review.reviewed_by, course_id):
            LOG.warning(
                u'User %(user)s does not have the required permissions to submit '
                u'a review for attempt_code %(attempt_code)s.',
                {'user': review.reviewed_by, 'attempt_code': attempt_code}
            )

        review.save()

        # go through and populate all of the specific comments
        for comment in backend_review.get('comments', []):
            comment = ProctoredExamSoftwareSecureComment(
                review=review,
                start_time=comment.get('start', 0),
                stop_time=comment.get('stop', 0),
                duration=comment.get('duration', 0),
                comment=comment['comment'],
                status=comment['status']
            )
            comment.save()

        if review.should_notify:
            instructor_service = get_runtime_service('instructor')
            request = get_current_request()
            if instructor_service and request:
                course_id = attempt['proctored_exam']['course_id']
                exam_id = attempt['proctored_exam']['id']
                review_url = request.build_absolute_uri(
                    u'{}?attempt={}'.format(
                        reverse('edx_proctoring:instructor_dashboard_exam', args=[course_id, exam_id]),
                        attempt['external_id']
                    ))
                instructor_service.send_support_notification(
                    course_id=attempt['proctored_exam']['course_id'],
                    exam_name=attempt['proctored_exam']['exam_name'],
                    student_username=attempt['user']['username'],
                    review_status=review.review_status,
                    review_url=review_url,
                )
Ejemplo n.º 10
0
    def get(self, request):  # pylint: disable=unused-argument
        """
        HTTP GET Handler. Returns the status of the exam attempt.
        """

        exams = get_active_exams_for_user(request.user.id)

        if exams:
            exam_info = exams[0]

            exam = exam_info['exam']
            attempt = exam_info['attempt']

            provider = get_backend_provider(exam)

            time_remaining_seconds = get_time_remaining_for_attempt(attempt)

            proctoring_settings = getattr(settings, 'PROCTORING_SETTINGS', {})
            low_threshold_pct = proctoring_settings.get('low_threshold_pct', .2)
            critically_low_threshold_pct = proctoring_settings.get('critically_low_threshold_pct', .05)

            low_threshold = int(low_threshold_pct * float(attempt['allowed_time_limit_mins']) * 60)
            critically_low_threshold = int(
                critically_low_threshold_pct * float(attempt['allowed_time_limit_mins']) * 60
            )

            exam_url_path = ''
            try:
                # resolve the LMS url, note we can't assume we're running in
                # a same process as the LMS
                exam_url_path = reverse('jump_to', args=[exam['course_id'], exam['content_id']])
            except NoReverseMatch:
                LOG.exception(u"Can't find exam url for course %s", exam['course_id'])

            response_dict = {
                'in_timed_exam': True,
                'taking_as_proctored': attempt['taking_as_proctored'],
                'exam_type': (
                    _('a timed exam') if not attempt['taking_as_proctored'] else
                    (_('a proctored exam') if not attempt['is_sample_attempt'] else
                     (_('an onboarding exam') if provider.supports_onboarding else _('a practice exam')))
                ),
                'exam_display_name': exam['exam_name'],
                'exam_url_path': exam_url_path,
                'time_remaining_seconds': time_remaining_seconds,
                'low_threshold_sec': low_threshold,
                'critically_low_threshold_sec': critically_low_threshold,
                'course_id': exam['course_id'],
                'attempt_id': attempt['id'],
                'accessibility_time_string': _(u'you have {remaining_time} remaining').format(
                    remaining_time=humanized_time(int(round(time_remaining_seconds / 60.0, 0)))
                ),
                'attempt_status': attempt['status'],
                'exam_started_poll_url': reverse(
                    'edx_proctoring:proctored_exam.attempt',
                    args=[attempt['id']]
                ),

            }

            if provider:
                response_dict['desktop_application_js_url'] = provider.get_javascript()
                response_dict['ping_interval'] = provider.ping_interval
            else:
                response_dict['desktop_application_js_url'] = ''

        else:
            response_dict = {
                'in_timed_exam': False,
                'is_proctored': False
            }

        return Response(data=response_dict, status=status.HTTP_200_OK)
Ejemplo n.º 11
0
    def make_review(self, attempt, data, backend=None):
        """
        Save the review and review comments
        """
        attempt_code = attempt['attempt_code']
        if not backend:
            backend = get_backend_provider(attempt['proctored_exam'])

        # this method should convert the payload into a normalized format
        backend_review = backend.on_review_callback(attempt, data)

        # do we already have a review for this attempt?!? We may not allow updates
        review = ProctoredExamSoftwareSecureReview.get_review_by_attempt_code(attempt_code)

        if review:
            if not constants.ALLOW_REVIEW_UPDATES:
                err_msg = (
                    'We already have a review submitted regarding '
                    'attempt_code {attempt_code}. We do not allow for updates!'.format(
                        attempt_code=attempt_code
                    )
                )
                raise ProctoredExamReviewAlreadyExists(err_msg)

            # we allow updates
            warn_msg = (
                'We already have a review submitted from our proctoring provider regarding '
                'attempt_code {attempt_code}. We have been configured to allow for '
                'updates and will continue...'.format(
                    attempt_code=attempt_code
                )
            )
            LOG.warning(warn_msg)
        else:
            # this is first time we've received this attempt_code, so
            # make a new record in the review table
            review = ProctoredExamSoftwareSecureReview()

        # first, validate that the backend review status is valid
        ReviewStatus.validate(backend_review['status'])
        # For now, we'll convert the standard review status to the old
        # software secure review status.
        # In the future, the old data should be standardized.
        review.review_status = SoftwareSecureReviewStatus.from_standard_status.get(backend_review['status'])

        review.attempt_code = attempt_code
        review.raw_data = json.dumps(data)
        review.student_id = attempt['user']['id']
        review.exam_id = attempt['proctored_exam']['id']

        try:
            review.reviewed_by = get_user_model().objects.get(email=data['reviewed_by'])
        except (ObjectDoesNotExist, KeyError):
            review.reviewed_by = None

        # If the reviewing user is a user in the system (user may be None for automated reviews) and does
        # not have permission to submit a review, log a warning.
        course_id = attempt['proctored_exam']['course_id']
        if review.reviewed_by is not None and not is_user_course_or_global_staff(review.reviewed_by, course_id):
            LOG.warning(
                'User %(user)s does not have the required permissions to submit '
                'a review for attempt_code %(attempt_code)s.',
                {'user': review.reviewed_by, 'attempt_code': attempt_code}
            )

        review.save()

        # go through and populate all of the specific comments
        for comment in backend_review.get('comments', []):
            comment = ProctoredExamSoftwareSecureComment(
                review=review,
                start_time=comment.get('start', 0),
                stop_time=comment.get('stop', 0),
                duration=comment.get('duration', 0),
                comment=comment['comment'],
                status=comment['status']
            )
            comment.save()

        if review.should_notify:
            instructor_service = get_runtime_service('instructor')
            request = get_current_request()
            if instructor_service and request:
                course_id = attempt['proctored_exam']['course_id']
                exam_id = attempt['proctored_exam']['id']
                review_url = request.build_absolute_uri(
                    u'{}?attempt={}'.format(
                        reverse('edx_proctoring:instructor_dashboard_exam', args=[course_id, exam_id]),
                        attempt['external_id']
                    ))
                instructor_service.send_support_notification(
                    course_id=attempt['proctored_exam']['course_id'],
                    exam_name=attempt['proctored_exam']['exam_name'],
                    student_username=attempt['user']['username'],
                    review_status=review.review_status,
                    review_url=review_url,
                )
Ejemplo n.º 12
0
    def get(self, request):  # pylint: disable=unused-argument
        """
        HTTP GET Handler. Returns the status of the exam attempt.
        """

        exams = get_active_exams_for_user(request.user.id)

        if exams:
            exam_info = exams[0]

            exam = exam_info['exam']
            attempt = exam_info['attempt']

            provider = get_backend_provider(exam)

            time_remaining_seconds = get_time_remaining_for_attempt(attempt)

            proctoring_settings = getattr(settings, 'PROCTORING_SETTINGS', {})
            low_threshold_pct = proctoring_settings.get('low_threshold_pct', .2)
            critically_low_threshold_pct = proctoring_settings.get('critically_low_threshold_pct', .05)

            low_threshold = int(low_threshold_pct * float(attempt['allowed_time_limit_mins']) * 60)
            critically_low_threshold = int(
                critically_low_threshold_pct * float(attempt['allowed_time_limit_mins']) * 60
            )

            exam_url_path = ''
            try:
                # resolve the LMS url, note we can't assume we're running in
                # a same process as the LMS
                exam_url_path = reverse('jump_to', args=[exam['course_id'], exam['content_id']])
            except NoReverseMatch:
                LOG.exception("Can't find exam url for course %s", exam['course_id'])

            response_dict = {
                'in_timed_exam': True,
                'taking_as_proctored': attempt['taking_as_proctored'],
                'exam_type': (
                    _('a timed exam') if not attempt['taking_as_proctored'] else
                    (_('a proctored exam') if not attempt['is_sample_attempt'] else
                     (_('an onboarding exam') if provider.supports_onboarding else _('a practice exam')))
                ),
                'exam_display_name': exam['exam_name'],
                'exam_url_path': exam_url_path,
                'time_remaining_seconds': time_remaining_seconds,
                'low_threshold_sec': low_threshold,
                'critically_low_threshold_sec': critically_low_threshold,
                'course_id': exam['course_id'],
                'attempt_id': attempt['id'],
                'accessibility_time_string': _('you have {remaining_time} remaining').format(
                    remaining_time=humanized_time(int(round(time_remaining_seconds / 60.0, 0)))
                ),
                'attempt_status': attempt['status'],
                'exam_started_poll_url': reverse(
                    'edx_proctoring:proctored_exam.attempt',
                    args=[attempt['id']]
                ),

            }

            if provider:
                response_dict['desktop_application_js_url'] = provider.get_javascript()
                response_dict['ping_interval'] = provider.ping_interval
            else:
                response_dict['desktop_application_js_url'] = ''

        else:
            response_dict = {
                'in_timed_exam': False,
                'is_proctored': False
            }

        return Response(data=response_dict, status=status.HTTP_200_OK)
Ejemplo n.º 13
0
    def make_review(self, attempt, data, backend=None):
        """
        Save the review and review comments
        """
        attempt_code = attempt['attempt_code']
        if not backend:
            backend = get_backend_provider(attempt['proctored_exam'])

        # this method should convert the payload into a normalized format
        backend_review = backend.on_review_callback(attempt, data)

        # do we already have a review for this attempt?!? We may not allow updates
        review = ProctoredExamSoftwareSecureReview.get_review_by_attempt_code(attempt_code)

        if review:
            if not constants.ALLOW_REVIEW_UPDATES:
                err_msg = (
                    'We already have a review submitted regarding '
                    'attempt_code {attempt_code}. We do not allow for updates!'.format(
                        attempt_code=attempt_code
                    )
                )
                raise ProctoredExamReviewAlreadyExists(err_msg)

            # we allow updates
            warn_msg = (
                'We already have a review submitted from our proctoring provider regarding '
                'attempt_code {attempt_code}. We have been configured to allow for '
                'updates and will continue...'.format(
                    attempt_code=attempt_code
                )
            )
            LOG.warning(warn_msg)
        else:
            # this is first time we've received this attempt_code, so
            # make a new record in the review table
            review = ProctoredExamSoftwareSecureReview()

        # first, validate that the backend review status is valid
        ReviewStatus.validate(backend_review['status'])
        # For now, we'll convert the standard review status to the old
        # software secure review status.
        # In the future, the old data should be standardized.
        review.review_status = SoftwareSecureReviewStatus.from_standard_status.get(backend_review['status'])

        review.attempt_code = attempt_code
        review.raw_data = json.dumps(backend_review)
        review.student_id = attempt['user']['id']
        review.exam_id = attempt['proctored_exam']['id']
        # set reviewed_by to None because it was reviewed by our 3rd party
        # service provider, not a user in our database
        review.reviewed_by = None

        review.save()

        # go through and populate all of the specific comments
        for comment in backend_review.get('comments', []):
            comment = ProctoredExamSoftwareSecureComment(
                review=review,
                start_time=comment.get('start', 0),
                stop_time=comment.get('stop', 0),
                duration=comment.get('duration', 0),
                comment=comment['comment'],
                status=comment['status']
            )
            comment.save()