<commit_before>"""
This module provides views that proxy to the staff grading backend service.
"""

import json
import logging

from django.conf import settings
from django.http import HttpResponse, Http404
from django.utils.translation import ugettext as _

from xmodule.course_module import CourseDescriptor
from xmodule.open_ended_grading_classes.grading_service_module import GradingService, GradingServiceError
from xmodule.modulestore.django import ModuleI18nService

from courseware.access import has_access
from lms.lib.xblock.runtime import LmsModuleSystem
from edxmako.shortcuts import render_to_string
from student.models import unique_id_for_user
from util.json_request import expect_json

from open_ended_grading.utils import does_location_exist

log = logging.getLogger(__name__)

STAFF_ERROR_MESSAGE = _(
    u'Could not contact the external grading server. Please contact the '
    u'development team at {email}.'
).format(
    email=u'<a href="mailto:{tech_support_email}>{tech_support_email}</a>'.format(
        tech_support_email=settings.TECH_SUPPORT_EMAIL
    )
)
MAX_ALLOWED_FEEDBACK_LENGTH = 5000


class MockStaffGradingService(object):
    """
    A simple mockup of a staff grading service, testing.
    """

    def __init__(self):
        self.cnt = 0

    def get_next(self, course_id, location, grader_id):
        self.cnt += 1
        return {'success': True,
                'submission_id': self.cnt,
                'submission': 'Test submission {cnt}'.format(cnt=self.cnt),
                'num_graded': 3,
                'min_for_ml': 5,
                'num_pending': 4,
                'prompt': 'This is a fake prompt',
                'ml_error_info': 'ML info',
                'max_score': 2 + self.cnt % 3,
                'rubric': 'A rubric'}

    def get_problem_list(self, course_id, grader_id):
        self.cnt += 1
        return {'success': True,
                 'problem_list': [
                     json.dumps({'location': 'i4x://MITx/3.091x/problem/open_ended_demo1',
                                 'problem_name': "Problem 1", 'num_graded': 3, 'num_pending': 5,
                                 'min_for_ml': 10}),
                     json.dumps({'location': 'i4x://MITx/3.091x/problem/open_ended_demo2',
                                 'problem_name': "Problem 2", 'num_graded': 1, 'num_pending': 5,
                                 'min_for_ml': 10})
                 ]}

    def save_grade(self, course_id, grader_id, submission_id, score, feedback, skipped, rubric_scores,
                   submission_flagged):
        return self.get_next(course_id, 'fake location', grader_id)


class StaffGradingService(GradingService):
    """
    Interface to staff grading backend.
    """

    def __init__(self, config):
        config['system'] = LmsModuleSystem(
            static_url='/static',
            track_function=None,
            get_module=None,
            render_template=render_to_string,
            replace_urls=None,
            descriptor_runtime=None,
            services={
                'i18n': ModuleI18nService(),
            },
        )
        super(StaffGradingService, self).__init__(config)
        self.url = config['url'] + config['staff_grading']
        self.login_url = self.url + '/login/'
        self.get_next_url = self.url + '/get_next_submission/'
        self.save_grade_url = self.url + '/save_grade/'
        self.get_problem_list_url = self.url + '/get_problem_list/'
        self.get_notifications_url = self.url + "/get_notifications/"

    def get_problem_list(self, course_id, grader_id):
        """
        Get the list of problems for a given course.

        Args:
            course_id: course id that we want the problems of
            grader_id: who is grading this?  The anonymous user_id of the grader.

        Returns:
            dict with the response from the service.  (Deliberately not
            writing out the fields here--see the docs on the staff_grading view
            in the grading_controller repo)

        Raises:
            GradingServiceError: something went wrong with the connection.
        """
        params = {'course_id': course_id, 'grader_id': grader_id}
        return self.get(self.get_problem_list_url, params).json()

    def get_next(self, course_id, location, grader_id):
        """
        Get the next thing to grade.

        Args:
            course_id: the course that this problem belongs to
            location: location of the problem that we are grading and would like the
                next submission for
            grader_id: who is grading this?  The anonymous user_id of the grader.

        Returns:
            dict with the response from the service.  (Deliberately not
            writing out the fields here--see the docs on the staff_grading view
            in the grading_controller repo)

        Raises:
            GradingServiceError: something went wrong with the connection.
        """
        response = self.get(self.get_next_url,
                            params={'location': location,
                                    'grader_id': grader_id})
        return self._render_rubric(response.json())

    def save_grade(self, course_id, grader_id, submission_id, score, feedback, skipped, rubric_scores,
                   submission_flagged):
        """
        Save a score and feedback for a submission.

        Returns:
            dict with keys
                'success': bool
                'error': error msg, if something went wrong.

        Raises:
            GradingServiceError if there's a problem connecting.
        """
        data = {'course_id': course_id,
                'submission_id': submission_id,
                'score': score,
                'feedback': feedback,
                'grader_id': grader_id,
                'skipped': skipped,
                'rubric_scores': rubric_scores,
                'rubric_scores_complete': True,
                'submission_flagged': submission_flagged}

        response = self.post(self.save_grade_url, data=data)
        return self._render_rubric(response.json())

    def get_notifications(self, course_id):
        params = {'course_id': course_id}
        return self.get(self.get_notifications_url, params).json()


# don't initialize until staff_grading_service() is called--means that just
# importing this file doesn't create objects that may not have the right config
_service = None


def staff_grading_service():
    """
    Return a staff grading service instance--if settings.MOCK_STAFF_GRADING is True,
    returns a mock one, otherwise a real one.

    Caches the result, so changing the setting after the first call to this<commit_msg>Record metrics about the staff grading service<commit_after>"""
This module provides views that proxy to the staff grading backend service.
"""

import json
import logging

from django.conf import settings
from django.http import HttpResponse, Http404
from django.utils.translation import ugettext as _

from xmodule.course_module import CourseDescriptor
from xmodule.open_ended_grading_classes.grading_service_module import GradingService, GradingServiceError
from xmodule.modulestore.django import ModuleI18nService

from courseware.access import has_access
from lms.lib.xblock.runtime import LmsModuleSystem
from edxmako.shortcuts import render_to_string
from student.models import unique_id_for_user
from util.json_request import expect_json

from open_ended_grading.utils import does_location_exist

log = logging.getLogger(__name__)

STAFF_ERROR_MESSAGE = _(
    u'Could not contact the external grading server. Please contact the '
    u'development team at {email}.'
).format(
    email=u'<a href="mailto:{tech_support_email}>{tech_support_email}</a>'.format(
        tech_support_email=settings.TECH_SUPPORT_EMAIL
    )
)
MAX_ALLOWED_FEEDBACK_LENGTH = 5000


class MockStaffGradingService(object):
    """
    A simple mockup of a staff grading service, testing.
    """

    def __init__(self):
        self.cnt = 0

    def get_next(self, course_id, location, grader_id):
        self.cnt += 1
        return {'success': True,
                'submission_id': self.cnt,
                'submission': 'Test submission {cnt}'.format(cnt=self.cnt),
                'num_graded': 3,
                'min_for_ml': 5,
                'num_pending': 4,
                'prompt': 'This is a fake prompt',
                'ml_error_info': 'ML info',
                'max_score': 2 + self.cnt % 3,
                'rubric': 'A rubric'}

    def get_problem_list(self, course_id, grader_id):
        self.cnt += 1
        return {'success': True,
                 'problem_list': [
                     json.dumps({'location': 'i4x://MITx/3.091x/problem/open_ended_demo1',
                                 'problem_name': "Problem 1", 'num_graded': 3, 'num_pending': 5,
                                 'min_for_ml': 10}),
                     json.dumps({'location': 'i4x://MITx/3.091x/problem/open_ended_demo2',
                                 'problem_name': "Problem 2", 'num_graded': 1, 'num_pending': 5,
                                 'min_for_ml': 10})
                 ]}

    def save_grade(self, course_id, grader_id, submission_id, score, feedback, skipped, rubric_scores,
                   submission_flagged):
        return self.get_next(course_id, 'fake location', grader_id)


class StaffGradingService(GradingService):
    """
    Interface to staff grading backend.
    """

    METRIC_NAME = 'edxapp.open_ended_grading.staff_grading_service'

    def __init__(self, config):
        config['system'] = LmsModuleSystem(
            static_url='/static',
            track_function=None,
            get_module=None,
            render_template=render_to_string,
            replace_urls=None,
            descriptor_runtime=None,
            services={
                'i18n': ModuleI18nService(),
            },
        )
        super(StaffGradingService, self).__init__(config)
        self.url = config['url'] + config['staff_grading']
        self.login_url = self.url + '/login/'
        self.get_next_url = self.url + '/get_next_submission/'
        self.save_grade_url = self.url + '/save_grade/'
        self.get_problem_list_url = self.url + '/get_problem_list/'
        self.get_notifications_url = self.url + "/get_notifications/"

    def get_problem_list(self, course_id, grader_id):
        """
        Get the list of problems for a given course.

        Args:
            course_id: course id that we want the problems of
            grader_id: who is grading this?  The anonymous user_id of the grader.

        Returns:
            dict with the response from the service.  (Deliberately not
            writing out the fields here--see the docs on the staff_grading view
            in the grading_controller repo)

        Raises:
            GradingServiceError: something went wrong with the connection.
        """
        params = {'course_id': course_id, 'grader_id': grader_id}
        result = self.get(self.get_problem_list_url, params).json()
        tags = [u'course_id:{}'.format(course_id)]
        self._record_result('get_problem_list', result, tags)
        dog_stats_api.histogram(
            self._metric_name('get_problem_list.result.length'),
            len(result.get('problem_list', []))
        )
        return result

    def get_next(self, course_id, location, grader_id):
        """
        Get the next thing to grade.

        Args:
            course_id: the course that this problem belongs to
            location: location of the problem that we are grading and would like the
                next submission for
            grader_id: who is grading this?  The anonymous user_id of the grader.

        Returns:
            dict with the response from the service.  (Deliberately not
            writing out the fields here--see the docs on the staff_grading view
            in the grading_controller repo)

        Raises:
            GradingServiceError: something went wrong with the connection.
        """
        response = self.get(self.get_next_url,
                            params={'location': location,
                                    'grader_id': grader_id})
        result = self._render_rubric(response.json())
        tags = [u'course_id:{}'.format(course_id)]
        self._record_result('get_next', result, tags)
        return result

    def save_grade(self, course_id, grader_id, submission_id, score, feedback, skipped, rubric_scores,
                   submission_flagged):
        """
        Save a score and feedback for a submission.

        Returns:
            dict with keys
                'success': bool
                'error': error msg, if something went wrong.

        Raises:
            GradingServiceError if there's a problem connecting.
        """
        data = {'course_id': course_id,
                'submission_id': submission_id,
                'score': score,
                'feedback': feedback,
                'grader_id': grader_id,
                'skipped': skipped,
                'rubric_scores': rubric_scores,
                'rubric_scores_complete': True,
                'submission_flagged': submission_flagged}

        response = self.post(self.save_grade_url, data=data)
        result = self._render_rubric(response.json())
        tags = [u'course_id:{}'.format(course_id)]
        self._record_result('save_grade', result, tags)
        return result

    def get_notifications(self, course_id):
        params = {'course_id': course_id}
        result = self.get(self.get_notifications_url, params).json()
        tags = [
            u'course_id:{}'.format(course_id),
            u'staff_needs_to_grade:{}'.format(result.get('staff_needs_to_grade'))
        ]
        self._record_result('get_notifications', result, tags)
        return result


# don't initialize until staff_grading_service() is called--means that just
# importing this file doesn't create objects that may not have the right config
_service = None


def staff_grading_service():
    """
    Return a staff grading service instance--if settings.MOCK_STAFF_GRADING is True,
    returns a mock one, otherwise a real one.

    Caches the result, so changing the setting after the first call to this
{'fraction_non_alphanumeric': 0.08871866295264624, 'fraction_numerical': 0.005988857938718663, 'mean_word_length': 2.2832647462277094, 'pattern_counts': {'":': 0, '<': 7, '<?xml version=': 0, '>': 7, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 10, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5009541', 'n_tokens_mistral': 4175, 'n_tokens_neox': 3926, 'n_words': 1081}