class PeerGradingModule(PeerGradingFields, XModule):
    """
    PeerGradingModule.__init__ takes the same arguments as xmodule.x_module:XModule.__init__
    """
    _VERSION = 1

    js = {
        'coffee': [
            resource_string(__name__, 'js/src/peergrading/peer_grading.coffee'),
            resource_string(__name__, 'js/src/peergrading/peer_grading_problem.coffee'),
            resource_string(__name__, 'js/src/collapsible.coffee'),
            resource_string(__name__, 'js/src/javascript_loader.coffee'),
        ]
    }
    js_module_name = "PeerGrading"

    css = {'scss': [resource_string(__name__, 'css/combinedopenended/display.scss')]}

    def __init__(self, *args, **kwargs):
        super(PeerGradingModule, self).__init__(*args, **kwargs)

        # Copy this to a new variable so that we can edit it if needed.
        # We need to edit it if the linked module cannot be found, so
        # we can revert to panel model.
        self.use_for_single_location_local = self.use_for_single_location

        # We need to set the location here so the child modules can use it.
        self.runtime.set('location', self.location)
        if (self.runtime.open_ended_grading_interface):
            self.peer_gs = PeerGradingService(self.system.open_ended_grading_interface, self.system)
        else:
            self.peer_gs = MockPeerGradingService()

        if self.use_for_single_location_local:
            linked_descriptors = self.descriptor.get_required_module_descriptors()
            if len(linked_descriptors) == 0:
                error_msg = "Peer grading module {0} is trying to use single problem mode without "
                "a location specified.".format(self.location)
                log.error(error_msg)
                # Change module over to panel mode from single problem mode.
                self.use_for_single_location_local = False
            else:
                self.linked_problem = self.system.get_module(linked_descriptors[0])

        try:
            self.timeinfo = TimeInfo(
                get_extended_due_date(self), self.graceperiod)
        except Exception:
            log.error("Error parsing due date information in location {0}".format(self.location))
            raise

        self.display_due_date = self.timeinfo.display_due_date

        try:
            self.student_data_for_location = json.loads(self.student_data_for_location)
        except Exception:
            pass

    @property
    def ajax_url(self):
        """
        Returns the `ajax_url` from the system, with any trailing '/' stripped off.
        """
        ajax_url = self.system.ajax_url
        if not ajax_url.endswith("/"):
            ajax_url += "/"
        return ajax_url

    def closed(self):
        return self._closed(self.timeinfo)

    def _closed(self, timeinfo):
        if timeinfo.close_date is not None and datetime.now(UTC()) > timeinfo.close_date:
            return True
        return False

    def _err_response(self, msg):
        """
        Return a HttpResponse with a json dump with success=False, and the given error message.
        """
        return {'success': False, 'error': msg}

    def _check_required(self, data, required):
        actual = set(data.keys())
        missing = required - actual
        if len(missing) > 0:
            return False, "Missing required keys: {0}".format(', '.join(missing))
        else:
            return True, ""

    def get_html(self):
        """
         Needs to be implemented by inheritors.  Renders the HTML that students see.
        @return:
        """
        if self.closed():
            return self.peer_grading_closed()
        if not self.use_for_single_location_local:
            return self.peer_grading()
        else:
            return self.peer_grading_problem({'location': self.link_to_location})['html']

    def handle_ajax(self, dispatch, data):
        """
        Needs to be implemented by child modules.  Handles AJAX events.
        @return:
        """
        handlers = {
            'get_next_submission': self.get_next_submission,
            'show_calibration_essay': self.show_calibration_essay,
            'is_student_calibrated': self.is_student_calibrated,
            'save_grade': self.save_grade,
            'save_calibration_essay': self.save_calibration_essay,
            'problem': self.peer_grading_problem,
        }

        if dispatch not in handlers:
            # This is a dev_facing_error
            log.error("Cannot find {0} in handlers in handle_ajax function for open_ended_module.py".format(dispatch))
            # This is a dev_facing_error
            return json.dumps({'error': 'Error handling action.  Please try again.', 'success': False})

        d = handlers[dispatch](data)

        return json.dumps(d, cls=ComplexEncoder)

    def query_data_for_location(self, location):
        student_id = self.system.anonymous_student_id
        success = False
        response = {}

        try:
            response = self.peer_gs.get_data_for_location(location, student_id)
            count_graded = response['count_graded']
            count_required = response['count_required']
            success = True
        except GradingServiceError:
            # This is a dev_facing_error
            log.exception("Error getting location data from controller for location {0}, student {1}"
            .format(location, student_id))

        return success, response

    def get_progress(self):
        pass

    def get_score(self):
        max_score = None
        score = None
        weight = self.weight

        #The old default was None, so set to 1 if it is the old default weight
        if weight is None:
            weight = 1
        score_dict = {
            'score': score,
            'total': max_score,
        }
        if not self.use_for_single_location_local or not self.graded:
            return score_dict

        try:
            count_graded = self.student_data_for_location['count_graded']
            count_required = self.student_data_for_location['count_required']
        except:
            success, response = self.query_data_for_location(self.location)
            if not success:
                log.exception(
                    "No instance data found and could not get data from controller for loc {0} student {1}".format(
                        self.system.location.url(), self.system.anonymous_student_id
                    ))
                return None
            count_graded = response['count_graded']
            count_required = response['count_required']
            if count_required > 0 and count_graded >= count_required:
                # Ensures that once a student receives a final score for peer grading, that it does not change.
                self.student_data_for_location = response

        score = int(count_graded >= count_required and count_graded > 0) * float(weight)
        total = float(weight)
        score_dict['score'] = score
        score_dict['total'] = total

        return score_dict

    def max_score(self):
        ''' Maximum score. Two notes:

            * This is generic; in abstract, a problem could be 3/5 points on one
              randomization, and 5/7 on another
        '''
        max_grade = None
        if self.use_for_single_location_local and self.graded:
            max_grade = self.weight
        return max_grade

    def get_next_submission(self, data):
        """
        Makes a call to the grading controller for the next essay that should be graded
        Returns a json dict with the following keys:

        'success': bool

        'submission_id': a unique identifier for the submission, to be passed back
                         with the grade.

        'submission': the submission, rendered as read-only html for grading

        'rubric': the rubric, also rendered as html.

        'submission_key': a key associated with the submission for validation reasons

        'error': if success is False, will have an error message with more info.
        """
        required = set(['location'])
        success, message = self._check_required(data, required)
        if not success:
            return self._err_response(message)
        grader_id = self.system.anonymous_student_id
        location = data['location']

        try:
            response = self.peer_gs.get_next_submission(location, grader_id)
            return response
        except GradingServiceError:
            # This is a dev_facing_error
            log.exception("Error getting next submission.  server url: {0}  location: {1}, grader_id: {2}"
            .format(self.peer_gs.url, location, grader_id))
            # This is a student_facing_error
            return {'success': False,
                    'error': EXTERNAL_GRADER_NO_CONTACT_ERROR}

    def save_grade(self, data):
        """
        Saves the grade of a given submission.
        Input:
            The request should have the following keys:
            location - problem location
            submission_id - id associated with this submission
            submission_key - submission key given for validation purposes
            score - the grade that was given to the submission
            feedback - the feedback from the student
        Returns
            A json object with the following keys:
            success: bool indicating whether the save was a success
            error: if there was an error in the submission, this is the error message
        """

        required = ['location', 'submission_id', 'submission_key', 'score', 'feedback', 'submission_flagged', 'answer_unknown']
        if data.get("submission_flagged", False) in ["false", False, "False", "FALSE"]:
            required.append("rubric_scores[]")
        success, message = self._check_required(data, set(required))
        if not success:
            return self._err_response(message)

        success, message = self._check_feedback_length(data)
        if not success:
            return self._err_response(message)

        data_dict = {k:data.get(k) for k in required}
        if 'rubric_scores[]' in required:
            data_dict['rubric_scores'] = data.getall('rubric_scores[]')
        data_dict['grader_id'] = self.system.anonymous_student_id

        try:
            response = self.peer_gs.save_grade(**data_dict)
            success, location_data = self.query_data_for_location(data_dict['location'])
            #Don't check for success above because the response = statement will raise the same Exception as the one
            #that will cause success to be false.
            response.update({'required_done' : False})
            if 'count_graded' in location_data and 'count_required' in location_data and int(location_data['count_graded'])>=int(location_data['count_required']):
                response['required_done'] = True
            return response
        except GradingServiceError:
            # This is a dev_facing_error
            log.exception("""Error saving grade to open ended grading service.  server url: {0}"""
            .format(self.peer_gs.url)
            )
            # This is a student_facing_error
            return {
                'success': False,
                'error': EXTERNAL_GRADER_NO_CONTACT_ERROR
            }

    def is_student_calibrated(self, data):
        """
        Calls the grading controller to see if the given student is calibrated
        on the given problem

        Input:
            In the request, we need the following arguments:
            location - problem location

        Returns:
            Json object with the following keys
            success - bool indicating whether or not the call was successful
            calibrated - true if the grader has fully calibrated and can now move on to grading
                       - false if the grader is still working on calibration problems
            total_calibrated_on_so_far - the number of calibration essays for this problem
                that this grader has graded
        """

        required = set(['location'])
        success, message = self._check_required(data, required)
        if not success:
            return self._err_response(message)
        grader_id = self.system.anonymous_student_id

        location = data['location']

        try:
            response = self.peer_gs.is_student_calibrated(location, grader_id)
            return response
        except GradingServiceError:
            # This is a dev_facing_error
            log.exception("Error from open ended grading service.  server url: {0}, grader_id: {0}, location: {1}"
            .format(self.peer_gs.url, grader_id, location))
            # This is a student_facing_error
            return {
                'success': False,
                'error': EXTERNAL_GRADER_NO_CONTACT_ERROR
            }

    def show_calibration_essay(self, data):
        """
        Fetch the next calibration essay from the grading controller and return it
        Inputs:
            In the request
            location - problem location

        Returns:
            A json dict with the following keys
            'success': bool

            'submission_id': a unique identifier for the submission, to be passed back
                             with the grade.

            'submission': the submission, rendered as read-only html for grading

            'rubric': the rubric, also rendered as html.

            'submission_key': a key associated with the submission for validation reasons

            'error': if success is False, will have an error message with more info.

        """

        required = set(['location'])
        success, message = self._check_required(data, required)
        if not success:
            return self._err_response(message)

        grader_id = self.system.anonymous_student_id

        location = data['location']
        try:
            response = self.peer_gs.show_calibration_essay(location, grader_id)
            return response
        except GradingServiceError:
            # This is a dev_facing_error
            log.exception("Error from open ended grading service.  server url: {0}, location: {0}"
            .format(self.peer_gs.url, location))
            # This is a student_facing_error
            return {'success': False,
                    'error': EXTERNAL_GRADER_NO_CONTACT_ERROR}
        # if we can't parse the rubric into HTML,
        except etree.XMLSyntaxError:
            # This is a dev_facing_error
            log.exception("Cannot parse rubric string.")
            # This is a student_facing_error
            return {'success': False,
                    'error': 'Error displaying submission.  Please notify course staff.'}

    def save_calibration_essay(self, data):
        """
        Saves the grader's grade of a given calibration.
        Input:
            The request should have the following keys:
            location - problem location
            submission_id - id associated with this submission
            submission_key - submission key given for validation purposes
            score - the grade that was given to the submission
            feedback - the feedback from the student
        Returns
            A json object with the following keys:
            success: bool indicating whether the save was a success
            error: if there was an error in the submission, this is the error message
            actual_score: the score that the instructor gave to this calibration essay

        """

        required = set(['location', 'submission_id', 'submission_key', 'score', 'feedback', 'rubric_scores[]'])
        success, message = self._check_required(data, required)
        if not success:
            return self._err_response(message)

        data_dict = {k:data.get(k) for k in required}
        data_dict['rubric_scores'] = data.getall('rubric_scores[]')
        data_dict['student_id'] = self.system.anonymous_student_id
        data_dict['calibration_essay_id'] = data_dict['submission_id']

        try:
            response = self.peer_gs.save_calibration_essay(**data_dict)
            if 'actual_rubric' in response:
                rubric_renderer = combined_open_ended_rubric.CombinedOpenEndedRubric(self.system, True)
                response['actual_rubric'] = rubric_renderer.render_rubric(response['actual_rubric'])['html']
            return response
        except GradingServiceError:
            # This is a dev_facing_error
            log.exception("Error saving calibration grade")
            # This is a student_facing_error
            return self._err_response('There was an error saving your score.  Please notify course staff.')

    def peer_grading_closed(self):
        '''
        Show the Peer grading closed template
        '''
        html = self.system.render_template('peer_grading/peer_grading_closed.html', {
            'use_for_single_location': self.use_for_single_location_local
        })
        return html

    def _find_corresponding_module_for_location(self, location):
        """
        Find the peer grading module that exists at the given location.
        """
        try:
            return self.descriptor.system.load_item(location)
        except ItemNotFoundError:
            # The linked problem doesn't exist.
            log.error("Problem {0} does not exist in this course.".format(location))
            raise
        except NoPathToItem:
            # The linked problem does not have a path to it (ie is in a draft or other strange state).
            log.error("Cannot find a path to problem {0} in this course.".format(location))
            raise

    def peer_grading(self, _data=None):
        '''
        Show a peer grading interface
        '''

        # call problem list service
        success = False
        error_text = ""
        problem_list = []
        try:
            problem_list_json = self.peer_gs.get_problem_list(self.course_id, self.system.anonymous_student_id)
            problem_list_dict = problem_list_json
            success = problem_list_dict['success']
            if 'error' in problem_list_dict:
                error_text = problem_list_dict['error']

            problem_list = problem_list_dict['problem_list']

        except GradingServiceError:
            # This is a student_facing_error
            error_text = EXTERNAL_GRADER_NO_CONTACT_ERROR
            log.error(error_text)
            success = False
        # catch error if if the json loads fails
        except ValueError:
            # This is a student_facing_error
            error_text = "Could not get list of problems to peer grade.  Please notify course staff."
            log.error(error_text)
            success = False
        except Exception:
            log.exception("Could not contact peer grading service.")
            success = False

        good_problem_list = []
        for problem in problem_list:
            problem_location = Location(problem['location'])
            try:
                descriptor = self._find_corresponding_module_for_location(problem_location)
            except (NoPathToItem, ItemNotFoundError):
                continue
            if descriptor:
                problem['due'] = get_extended_due_date(descriptor)
                grace_period = descriptor.graceperiod
                try:
                    problem_timeinfo = TimeInfo(problem['due'], grace_period)
                except Exception:
                    log.error("Malformed due date or grace period string for location {0}".format(problem_location))
                    raise
                if self._closed(problem_timeinfo):
                    problem['closed'] = True
                else:
                    problem['closed'] = False
            else:
                # if we can't find the due date, assume that it doesn't have one
                problem['due'] = None
                problem['closed'] = False
            good_problem_list.append(problem)

        ajax_url = self.ajax_url
        html = self.system.render_template('peer_grading/peer_grading.html', {
            'course_id': self.course_id,
            'ajax_url': ajax_url,
            'success': success,
            'problem_list': good_problem_list,
            'error_text': error_text,
            # Checked above
            'staff_access': False,
            'use_single_location': self.use_for_single_location_local,
        })

        return html

    def peer_grading_problem(self, data=None):
        '''
        Show individual problem interface
        '''
        if data is None or data.get('location') is None:
            if not self.use_for_single_location_local:
                # This is an error case, because it must be set to use a single location to be called without get parameters
                # This is a dev_facing_error
                log.error(
                    "Peer grading problem in peer_grading_module called with no get parameters, but use_for_single_location is False.")
                return {'html': "", 'success': False}
            problem_location = Location(self.link_to_location)

        elif data.get('location') is not None:
            problem_location = Location(data.get('location'))

        module = self._find_corresponding_module_for_location(problem_location)

        ajax_url = self.ajax_url
        html = self.system.render_template('peer_grading/peer_grading_problem.html', {
            'view_html': '',
            'problem_location': problem_location,
            'course_id': self.course_id,
            'ajax_url': ajax_url,
            # Checked above
            'staff_access': False,
            'use_single_location': self.use_for_single_location_local,
        })

        return {'html': html, 'success': True}

    def get_instance_state(self):
        """
        Returns the current instance state.  The module can be recreated from the instance state.
        Input: None
        Output: A dictionary containing the instance state.
        """

        state = {
            'student_data_for_location': self.student_data_for_location,
        }

        return json.dumps(state)

    def _check_feedback_length(self, data):
        feedback = data.get("feedback")
        if feedback and len(feedback) > MAX_ALLOWED_FEEDBACK_LENGTH:
            return False, "Feedback is too long, Max length is {0} characters.".format(
                MAX_ALLOWED_FEEDBACK_LENGTH
            )
        else:
            return True, ""
Beispiel #2
0
class PeerGradingModule(PeerGradingFields, XModule):
    """
    PeerGradingModule.__init__ takes the same arguments as xmodule.x_module:XModule.__init__
    """
    _VERSION = 1

    js = {'coffee': [resource_string(__name__, 'js/src/peergrading/peer_grading.coffee'),
                     resource_string(__name__, 'js/src/peergrading/peer_grading_problem.coffee'),
                     resource_string(__name__, 'js/src/collapsible.coffee'),
                     resource_string(__name__, 'js/src/javascript_loader.coffee'),
    ]}
    js_module_name = "PeerGrading"

    css = {'scss': [resource_string(__name__, 'css/combinedopenended/display.scss')]}

    def __init__(self, *args, **kwargs):
        super(PeerGradingModule, self).__init__(*args, **kwargs)

        #We need to set the location here so the child modules can use it
        self.runtime.set('location', self.location)
        if (self.system.open_ended_grading_interface):
            self.peer_gs = PeerGradingService(self.system.open_ended_grading_interface, self.system)
        else:
            self.peer_gs = MockPeerGradingService()

        if self.use_for_single_location:
            try:
                self.linked_problem = modulestore().get_instance(self.system.course_id, self.link_to_location)
            except:
                log.error("Linked location {0} for peer grading module {1} does not exist".format(
                    self.link_to_location, self.location))
                raise
            due_date = self.linked_problem._model_data.get('peer_grading_due', None)
            if due_date:
                self._model_data['due'] = due_date

        try:
            self.timeinfo = TimeInfo(self.due_date, self.grace_period_string)
        except:
            log.error("Error parsing due date information in location {0}".format(location))
            raise

        self.display_due_date = self.timeinfo.display_due_date

        try:
            self.student_data_for_location = json.loads(self.student_data_for_location)
        except:
            pass

        self.ajax_url = self.system.ajax_url
        if not self.ajax_url.endswith("/"):
            self.ajax_url = self.ajax_url + "/"

        # Integer could return None, so keep this check.
        if not isinstance(self.max_grade, int):
            raise TypeError("max_grade needs to be an integer.")

    def closed(self):
        return self._closed(self.timeinfo)

    def _closed(self, timeinfo):
        if timeinfo.close_date is not None and datetime.now(UTC()) > timeinfo.close_date:
            return True
        return False


    def _err_response(self, msg):
        """
        Return a HttpResponse with a json dump with success=False, and the given error message.
        """
        return {'success': False, 'error': msg}

    def _check_required(self, data, required):
        actual = set(data.keys())
        missing = required - actual
        if len(missing) > 0:
            return False, "Missing required keys: {0}".format(', '.join(missing))
        else:
            return True, ""

    def get_html(self):
        """
         Needs to be implemented by inheritors.  Renders the HTML that students see.
        @return:
        """
        if self.closed():
            return self.peer_grading_closed()
        if not self.use_for_single_location:
            return self.peer_grading()
        else:
            return self.peer_grading_problem({'location': self.link_to_location})['html']

    def handle_ajax(self, dispatch, data):
        """
        Needs to be implemented by child modules.  Handles AJAX events.
        @return:
        """
        handlers = {
            'get_next_submission': self.get_next_submission,
            'show_calibration_essay': self.show_calibration_essay,
            'is_student_calibrated': self.is_student_calibrated,
            'save_grade': self.save_grade,
            'save_calibration_essay': self.save_calibration_essay,
            'problem': self.peer_grading_problem,
        }

        if dispatch not in handlers:
            # This is a dev_facing_error
            log.error("Cannot find {0} in handlers in handle_ajax function for open_ended_module.py".format(dispatch))
            # This is a dev_facing_error
            return json.dumps({'error': 'Error handling action.  Please try again.', 'success': False})

        d = handlers[dispatch](data)

        return json.dumps(d, cls=ComplexEncoder)

    def query_data_for_location(self):
        student_id = self.system.anonymous_student_id
        location = self.link_to_location
        success = False
        response = {}

        try:
            response = self.peer_gs.get_data_for_location(location, student_id)
            count_graded = response['count_graded']
            count_required = response['count_required']
            success = True
        except GradingServiceError:
            # This is a dev_facing_error
            log.exception("Error getting location data from controller for location {0}, student {1}"
            .format(location, student_id))

        return success, response

    def get_progress(self):
        pass

    def get_score(self):
        max_score = None
        score = None
        score_dict = {
            'score': score,
            'total': max_score,
        }
        if not self.use_for_single_location or not self.is_graded:
            return score_dict

        try:
            count_graded = self.student_data_for_location['count_graded']
            count_required = self.student_data_for_location['count_required']
        except:
            success, response = self.query_data_for_location()
            if not success:
                log.exception(
                    "No instance data found and could not get data from controller for loc {0} student {1}".format(
                        self.system.location.url(), self.system.anonymous_student_id
                    ))
                return None
            count_graded = response['count_graded']
            count_required = response['count_required']
            if count_required > 0 and count_graded >= count_required:
                # Ensures that once a student receives a final score for peer grading, that it does not change.
                self.student_data_for_location = response

        if self.weight is not None:
            score = int(count_graded >= count_required and count_graded > 0) * float(self.weight)
            total = self.max_grade * float(self.weight)
            score_dict['score'] = score
            score_dict['total'] = total

        return score_dict

    def max_score(self):
        ''' Maximum score. Two notes:

            * This is generic; in abstract, a problem could be 3/5 points on one
              randomization, and 5/7 on another
        '''
        max_grade = None
        if self.use_for_single_location and self.is_graded:
            max_grade = self.max_grade
        return max_grade

    def get_next_submission(self, data):
        """
        Makes a call to the grading controller for the next essay that should be graded
        Returns a json dict with the following keys:

        'success': bool

        'submission_id': a unique identifier for the submission, to be passed back
                         with the grade.

        'submission': the submission, rendered as read-only html for grading

        'rubric': the rubric, also rendered as html.

        'submission_key': a key associated with the submission for validation reasons

        'error': if success is False, will have an error message with more info.
        """
        required = set(['location'])
        success, message = self._check_required(data, required)
        if not success:
            return self._err_response(message)
        grader_id = self.system.anonymous_student_id
        location = data['location']

        try:
            response = self.peer_gs.get_next_submission(location, grader_id)
            return response
        except GradingServiceError:
            # This is a dev_facing_error
            log.exception("Error getting next submission.  server url: {0}  location: {1}, grader_id: {2}"
            .format(self.peer_gs.url, location, grader_id))
            # This is a student_facing_error
            return {'success': False,
                    'error': EXTERNAL_GRADER_NO_CONTACT_ERROR}

    def save_grade(self, data):
        """
        Saves the grade of a given submission.
        Input:
            The request should have the following keys:
            location - problem location
            submission_id - id associated with this submission
            submission_key - submission key given for validation purposes
            score - the grade that was given to the submission
            feedback - the feedback from the student
        Returns
            A json object with the following keys:
            success: bool indicating whether the save was a success
            error: if there was an error in the submission, this is the error message
        """

        required = set(['location', 'submission_id', 'submission_key', 'score', 'feedback', 'rubric_scores[]',
                        'submission_flagged'])
        success, message = self._check_required(data, required)
        if not success:
            return self._err_response(message)
        grader_id = self.system.anonymous_student_id

        location = data.get('location')
        submission_id = data.get('submission_id')
        score = data.get('score')
        feedback = data.get('feedback')
        submission_key = data.get('submission_key')
        rubric_scores = data.getlist('rubric_scores[]')
        submission_flagged = data.get('submission_flagged')

        try:
            response = self.peer_gs.save_grade(location, grader_id, submission_id,
                                               score, feedback, submission_key, rubric_scores, submission_flagged)
            return response
        except GradingServiceError:
            # This is a dev_facing_error
            log.exception("""Error saving grade to open ended grading service.  server url: {0}, location: {1}, submission_id:{2},
                            submission_key: {3}, score: {4}"""
            .format(self.peer_gs.url,
                    location, submission_id, submission_key, score)
            )
            # This is a student_facing_error
            return {
                'success': False,
                'error': EXTERNAL_GRADER_NO_CONTACT_ERROR
            }

    def is_student_calibrated(self, data):
        """
        Calls the grading controller to see if the given student is calibrated
        on the given problem

        Input:
            In the request, we need the following arguments:
            location - problem location

        Returns:
            Json object with the following keys
            success - bool indicating whether or not the call was successful
            calibrated - true if the grader has fully calibrated and can now move on to grading
                       - false if the grader is still working on calibration problems
            total_calibrated_on_so_far - the number of calibration essays for this problem
                that this grader has graded
        """

        required = set(['location'])
        success, message = self._check_required(data, required)
        if not success:
            return self._err_response(message)
        grader_id = self.system.anonymous_student_id

        location = data['location']

        try:
            response = self.peer_gs.is_student_calibrated(location, grader_id)
            return response
        except GradingServiceError:
            # This is a dev_facing_error
            log.exception("Error from open ended grading service.  server url: {0}, grader_id: {0}, location: {1}"
            .format(self.peer_gs.url, grader_id, location))
            # This is a student_facing_error
            return {
                'success': False,
                'error': EXTERNAL_GRADER_NO_CONTACT_ERROR
            }

    def show_calibration_essay(self, data):
        """
        Fetch the next calibration essay from the grading controller and return it
        Inputs:
            In the request
            location - problem location

        Returns:
            A json dict with the following keys
            'success': bool

            'submission_id': a unique identifier for the submission, to be passed back
                             with the grade.

            'submission': the submission, rendered as read-only html for grading

            'rubric': the rubric, also rendered as html.

            'submission_key': a key associated with the submission for validation reasons

            'error': if success is False, will have an error message with more info.

        """

        required = set(['location'])
        success, message = self._check_required(data, required)
        if not success:
            return self._err_response(message)

        grader_id = self.system.anonymous_student_id

        location = data['location']
        try:
            response = self.peer_gs.show_calibration_essay(location, grader_id)
            return response
        except GradingServiceError:
            # This is a dev_facing_error
            log.exception("Error from open ended grading service.  server url: {0}, location: {0}"
            .format(self.peer_gs.url, location))
            # This is a student_facing_error
            return {'success': False,
                    'error': EXTERNAL_GRADER_NO_CONTACT_ERROR}
        # if we can't parse the rubric into HTML,
        except etree.XMLSyntaxError:
            # This is a dev_facing_error
            log.exception("Cannot parse rubric string.")
            # This is a student_facing_error
            return {'success': False,
                    'error': 'Error displaying submission.  Please notify course staff.'}

    def save_calibration_essay(self, data):
        """
        Saves the grader's grade of a given calibration.
        Input:
            The request should have the following keys:
            location - problem location
            submission_id - id associated with this submission
            submission_key - submission key given for validation purposes
            score - the grade that was given to the submission
            feedback - the feedback from the student
        Returns
            A json object with the following keys:
            success: bool indicating whether the save was a success
            error: if there was an error in the submission, this is the error message
            actual_score: the score that the instructor gave to this calibration essay

        """

        required = set(['location', 'submission_id', 'submission_key', 'score', 'feedback', 'rubric_scores[]'])
        success, message = self._check_required(data, required)
        if not success:
            return self._err_response(message)
        grader_id = self.system.anonymous_student_id

        location = data.get('location')
        calibration_essay_id = data.get('submission_id')
        submission_key = data.get('submission_key')
        score = data.get('score')
        feedback = data.get('feedback')
        rubric_scores = data.getlist('rubric_scores[]')

        try:
            response = self.peer_gs.save_calibration_essay(location, grader_id, calibration_essay_id,
                                                           submission_key, score, feedback, rubric_scores)
            if 'actual_rubric' in response:
                rubric_renderer = combined_open_ended_rubric.CombinedOpenEndedRubric(self.system, True)
                response['actual_rubric'] = rubric_renderer.render_rubric(response['actual_rubric'])['html']
            return response
        except GradingServiceError:
            # This is a dev_facing_error
            log.exception(
                "Error saving calibration grade, location: {0}, submission_key: {1}, grader_id: {2}".format(
                    location, submission_key, grader_id))
            # This is a student_facing_error
            return self._err_response('There was an error saving your score.  Please notify course staff.')

    def peer_grading_closed(self):
        '''
        Show the Peer grading closed template
        '''
        html = self.system.render_template('peer_grading/peer_grading_closed.html', {
            'use_for_single_location': self.use_for_single_location
        })
        return html

    def peer_grading(self, _data=None):
        '''
        Show a peer grading interface
        '''

        # call problem list service
        success = False
        error_text = ""
        problem_list = []
        try:
            problem_list_json = self.peer_gs.get_problem_list(self.system.course_id, self.system.anonymous_student_id)
            problem_list_dict = problem_list_json
            success = problem_list_dict['success']
            if 'error' in problem_list_dict:
                error_text = problem_list_dict['error']

            problem_list = problem_list_dict['problem_list']

        except GradingServiceError:
            # This is a student_facing_error
            error_text = EXTERNAL_GRADER_NO_CONTACT_ERROR
            log.error(error_text)
            success = False
        # catch error if if the json loads fails
        except ValueError:
            # This is a student_facing_error
            error_text = "Could not get list of problems to peer grade.  Please notify course staff."
            log.error(error_text)
            success = False
        except:
            log.exception("Could not contact peer grading service.")
            success = False


        def _find_corresponding_module_for_location(location):
            '''
            find the peer grading module that links to the given location
            '''
            try:
                return modulestore().get_instance(self.system.course_id, location)
            except:
                # the linked problem doesn't exist
                log.error("Problem {0} does not exist in this course".format(location))
                raise

        for problem in problem_list:
            problem_location = problem['location']
            descriptor = _find_corresponding_module_for_location(problem_location)
            if descriptor:
                problem['due'] = descriptor._model_data.get('peer_grading_due', None)
                grace_period_string = descriptor._model_data.get('graceperiod', None)
                try:
                    problem_timeinfo = TimeInfo(problem['due'], grace_period_string)
                except:
                    log.error("Malformed due date or grace period string for location {0}".format(problem_location))
                    raise
                if self._closed(problem_timeinfo):
                    problem['closed'] = True
                else:
                    problem['closed'] = False
            else:
                # if we can't find the due date, assume that it doesn't have one
                problem['due'] = None
                problem['closed'] = False

        ajax_url = self.ajax_url
        html = self.system.render_template('peer_grading/peer_grading.html', {
            'course_id': self.system.course_id,
            'ajax_url': ajax_url,
            'success': success,
            'problem_list': problem_list,
            'error_text': error_text,
            # Checked above
            'staff_access': False,
            'use_single_location': self.use_for_single_location,
        })

        return html

    def peer_grading_problem(self, data=None):
        '''
        Show individual problem interface
        '''
        if data is None or data.get('location') is None:
            if not self.use_for_single_location:
                # This is an error case, because it must be set to use a single location to be called without get parameters
                # This is a dev_facing_error
                log.error(
                    "Peer grading problem in peer_grading_module called with no get parameters, but use_for_single_location is False.")
                return {'html': "", 'success': False}
            problem_location = self.link_to_location

        elif data.get('location') is not None:
            problem_location = data.get('location')

        ajax_url = self.ajax_url
        html = self.system.render_template('peer_grading/peer_grading_problem.html', {
            'view_html': '',
            'problem_location': problem_location,
            'course_id': self.system.course_id,
            'ajax_url': ajax_url,
            # Checked above
            'staff_access': False,
            'use_single_location': self.use_for_single_location,
        })

        return {'html': html, 'success': True}

    def get_instance_state(self):
        """
        Returns the current instance state.  The module can be recreated from the instance state.
        Input: None
        Output: A dictionary containing the instance state.
        """

        state = {
            'student_data_for_location': self.student_data_for_location,
        }

        return json.dumps(state)
class PeerGradingModule(PeerGradingFields, XModule):
    """
    PeerGradingModule.__init__ takes the same arguments as xmodule.x_module:XModule.__init__
    """
    _VERSION = 1

    js = {
        'coffee': [
            resource_string(__name__,
                            'js/src/peergrading/peer_grading.coffee'),
            resource_string(__name__,
                            'js/src/peergrading/peer_grading_problem.coffee'),
            resource_string(__name__, 'js/src/javascript_loader.coffee'),
        ],
        'js': [
            resource_string(__name__, 'js/src/collapsible.js'),
        ]
    }
    js_module_name = "PeerGrading"

    css = {
        'scss':
        [resource_string(__name__, 'css/combinedopenended/display.scss')]
    }

    def __init__(self, *args, **kwargs):
        super(PeerGradingModule, self).__init__(*args, **kwargs)

        # Copy this to a new variable so that we can edit it if needed.
        # We need to edit it if the linked module cannot be found, so
        # we can revert to panel model.
        self.use_for_single_location_local = self.use_for_single_location

        # We need to set the location here so the child modules can use it.
        self.runtime.set('location', self.location)
        if (self.runtime.open_ended_grading_interface):
            self.peer_gs = PeerGradingService(
                self.system.open_ended_grading_interface, self.system)
        else:
            self.peer_gs = MockPeerGradingService()

        if self.use_for_single_location_local:
            linked_descriptors = self.descriptor.get_required_module_descriptors(
            )
            if len(linked_descriptors) == 0:
                error_msg = "Peer grading module {0} is trying to use single problem mode without "
                "a location specified.".format(self.location)
                log.error(error_msg)
                # Change module over to panel mode from single problem mode.
                self.use_for_single_location_local = False
            else:
                self.linked_problem = self.system.get_module(
                    linked_descriptors[0])

        try:
            self.timeinfo = TimeInfo(get_extended_due_date(self),
                                     self.graceperiod)
        except Exception:
            log.error(
                "Error parsing due date information in location {0}".format(
                    self.location))
            raise

        self.display_due_date = self.timeinfo.display_due_date

        try:
            self.student_data_for_location = json.loads(
                self.student_data_for_location)
        except Exception:
            pass

    @property
    def ajax_url(self):
        """
        Returns the `ajax_url` from the system, with any trailing '/' stripped off.
        """
        ajax_url = self.system.ajax_url
        if not ajax_url.endswith("/"):
            ajax_url += "/"
        return ajax_url

    def closed(self):
        return self._closed(self.timeinfo)

    def _closed(self, timeinfo):
        if timeinfo.close_date is not None and datetime.now(
                UTC()) > timeinfo.close_date:
            return True
        return False

    def _err_response(self, msg):
        """
        Return a HttpResponse with a json dump with success=False, and the given error message.
        """
        return {'success': False, 'error': msg}

    def _check_required(self, data, required):
        actual = set(data.keys())
        missing = required - actual
        if len(missing) > 0:
            return False, "Missing required keys: {0}".format(
                ', '.join(missing))
        else:
            return True, ""

    def get_html(self):
        """
         Needs to be implemented by inheritors.  Renders the HTML that students see.
        @return:
        """
        if self.closed():
            return self.peer_grading_closed()
        if not self.use_for_single_location_local:
            return self.peer_grading()
        else:
            return self.peer_grading_problem(
                {'location': self.link_to_location})['html']

    def handle_ajax(self, dispatch, data):
        """
        Needs to be implemented by child modules.  Handles AJAX events.
        @return:
        """
        handlers = {
            'get_next_submission': self.get_next_submission,
            'show_calibration_essay': self.show_calibration_essay,
            'is_student_calibrated': self.is_student_calibrated,
            'save_grade': self.save_grade,
            'save_calibration_essay': self.save_calibration_essay,
            'problem': self.peer_grading_problem,
        }

        if dispatch not in handlers:
            # This is a dev_facing_error
            log.error(
                "Cannot find {0} in handlers in handle_ajax function for open_ended_module.py"
                .format(dispatch))
            # This is a dev_facing_error
            return json.dumps({
                'error': 'Error handling action.  Please try again.',
                'success': False
            })

        d = handlers[dispatch](data)

        return json.dumps(d, cls=ComplexEncoder)

    def query_data_for_location(self, location):
        student_id = self.system.anonymous_student_id
        success = False
        response = {}

        try:
            response = self.peer_gs.get_data_for_location(location, student_id)
            count_graded = response['count_graded']
            count_required = response['count_required']
            success = True
        except GradingServiceError:
            # This is a dev_facing_error
            log.exception(
                "Error getting location data from controller for location {0}, student {1}"
                .format(location, student_id))

        return success, response

    def get_progress(self):
        pass

    def get_score(self):
        max_score = None
        score = None
        weight = self.weight

        #The old default was None, so set to 1 if it is the old default weight
        if weight is None:
            weight = 1
        score_dict = {
            'score': score,
            'total': max_score,
        }
        if not self.use_for_single_location_local or not self.graded:
            return score_dict

        try:
            count_graded = self.student_data_for_location['count_graded']
            count_required = self.student_data_for_location['count_required']
        except:
            success, response = self.query_data_for_location(
                self.link_to_location)
            if not success:
                log.exception(
                    "No instance data found and could not get data from controller for loc {0} student {1}"
                    .format(self.system.location.url(),
                            self.system.anonymous_student_id))
                return None
            count_graded = response['count_graded']
            count_required = response['count_required']
            if count_required > 0 and count_graded >= count_required:
                # Ensures that once a student receives a final score for peer grading, that it does not change.
                self.student_data_for_location = response

        score = int(count_graded >= count_required
                    and count_graded > 0) * float(weight)
        total = float(weight)
        score_dict['score'] = score
        score_dict['total'] = total

        return score_dict

    def max_score(self):
        ''' Maximum score. Two notes:

            * This is generic; in abstract, a problem could be 3/5 points on one
              randomization, and 5/7 on another
        '''
        max_grade = None
        if self.use_for_single_location_local and self.graded:
            max_grade = self.weight
        return max_grade

    def get_next_submission(self, data):
        """
        Makes a call to the grading controller for the next essay that should be graded
        Returns a json dict with the following keys:

        'success': bool

        'submission_id': a unique identifier for the submission, to be passed back
                         with the grade.

        'submission': the submission, rendered as read-only html for grading

        'rubric': the rubric, also rendered as html.

        'submission_key': a key associated with the submission for validation reasons

        'error': if success is False, will have an error message with more info.
        """
        required = set(['location'])
        success, message = self._check_required(data, required)
        if not success:
            return self._err_response(message)
        grader_id = self.system.anonymous_student_id
        location = data['location']

        try:
            response = self.peer_gs.get_next_submission(location, grader_id)
            return response
        except GradingServiceError:
            # This is a dev_facing_error
            log.exception(
                "Error getting next submission.  server url: {0}  location: {1}, grader_id: {2}"
                .format(self.peer_gs.url, location, grader_id))
            # This is a student_facing_error
            return {
                'success': False,
                'error': EXTERNAL_GRADER_NO_CONTACT_ERROR
            }

    def save_grade(self, data):
        """
        Saves the grade of a given submission.
        Input:
            The request should have the following keys:
            location - problem location
            submission_id - id associated with this submission
            submission_key - submission key given for validation purposes
            score - the grade that was given to the submission
            feedback - the feedback from the student
        Returns
            A json object with the following keys:
            success: bool indicating whether the save was a success
            error: if there was an error in the submission, this is the error message
        """

        required = [
            'location', 'submission_id', 'submission_key', 'score', 'feedback',
            'submission_flagged', 'answer_unknown'
        ]
        if data.get("submission_flagged",
                    False) in ["false", False, "False", "FALSE"]:
            required.append("rubric_scores[]")
        success, message = self._check_required(data, set(required))
        if not success:
            return self._err_response(message)

        success, message = self._check_feedback_length(data)
        if not success:
            return self._err_response(message)

        data_dict = {k: data.get(k) for k in required}
        if 'rubric_scores[]' in required:
            data_dict['rubric_scores'] = data.getall('rubric_scores[]')
        data_dict['grader_id'] = self.system.anonymous_student_id

        try:
            response = self.peer_gs.save_grade(**data_dict)
            success, location_data = self.query_data_for_location(
                data_dict['location'])
            #Don't check for success above because the response = statement will raise the same Exception as the one
            #that will cause success to be false.
            response.update({'required_done': False})
            if 'count_graded' in location_data and 'count_required' in location_data and int(
                    location_data['count_graded']) >= int(
                        location_data['count_required']):
                response['required_done'] = True
            return response
        except GradingServiceError:
            # This is a dev_facing_error
            log.exception(
                """Error saving grade to open ended grading service.  server url: {0}"""
                .format(self.peer_gs.url))
            # This is a student_facing_error
            return {
                'success': False,
                'error': EXTERNAL_GRADER_NO_CONTACT_ERROR
            }

    def is_student_calibrated(self, data):
        """
        Calls the grading controller to see if the given student is calibrated
        on the given problem

        Input:
            In the request, we need the following arguments:
            location - problem location

        Returns:
            Json object with the following keys
            success - bool indicating whether or not the call was successful
            calibrated - true if the grader has fully calibrated and can now move on to grading
                       - false if the grader is still working on calibration problems
            total_calibrated_on_so_far - the number of calibration essays for this problem
                that this grader has graded
        """

        required = set(['location'])
        success, message = self._check_required(data, required)
        if not success:
            return self._err_response(message)
        grader_id = self.system.anonymous_student_id

        location = data['location']

        try:
            response = self.peer_gs.is_student_calibrated(location, grader_id)
            return response
        except GradingServiceError:
            # This is a dev_facing_error
            log.exception(
                "Error from open ended grading service.  server url: {0}, grader_id: {0}, location: {1}"
                .format(self.peer_gs.url, grader_id, location))
            # This is a student_facing_error
            return {
                'success': False,
                'error': EXTERNAL_GRADER_NO_CONTACT_ERROR
            }

    def show_calibration_essay(self, data):
        """
        Fetch the next calibration essay from the grading controller and return it
        Inputs:
            In the request
            location - problem location

        Returns:
            A json dict with the following keys
            'success': bool

            'submission_id': a unique identifier for the submission, to be passed back
                             with the grade.

            'submission': the submission, rendered as read-only html for grading

            'rubric': the rubric, also rendered as html.

            'submission_key': a key associated with the submission for validation reasons

            'error': if success is False, will have an error message with more info.

        """

        required = set(['location'])
        success, message = self._check_required(data, required)
        if not success:
            return self._err_response(message)

        grader_id = self.system.anonymous_student_id

        location = data['location']
        try:
            response = self.peer_gs.show_calibration_essay(location, grader_id)
            return response
        except GradingServiceError:
            # This is a dev_facing_error
            log.exception(
                "Error from open ended grading service.  server url: {0}, location: {0}"
                .format(self.peer_gs.url, location))
            # This is a student_facing_error
            return {
                'success': False,
                'error': EXTERNAL_GRADER_NO_CONTACT_ERROR
            }
        # if we can't parse the rubric into HTML,
        except etree.XMLSyntaxError:
            # This is a dev_facing_error
            log.exception("Cannot parse rubric string.")
            # This is a student_facing_error
            return {
                'success':
                False,
                'error':
                'Error displaying submission.  Please notify course staff.'
            }

    def save_calibration_essay(self, data):
        """
        Saves the grader's grade of a given calibration.
        Input:
            The request should have the following keys:
            location - problem location
            submission_id - id associated with this submission
            submission_key - submission key given for validation purposes
            score - the grade that was given to the submission
            feedback - the feedback from the student
        Returns
            A json object with the following keys:
            success: bool indicating whether the save was a success
            error: if there was an error in the submission, this is the error message
            actual_score: the score that the instructor gave to this calibration essay

        """

        required = set([
            'location', 'submission_id', 'submission_key', 'score', 'feedback',
            'rubric_scores[]'
        ])
        success, message = self._check_required(data, required)
        if not success:
            return self._err_response(message)

        data_dict = {k: data.get(k) for k in required}
        data_dict['rubric_scores'] = data.getall('rubric_scores[]')
        data_dict['student_id'] = self.system.anonymous_student_id
        data_dict['calibration_essay_id'] = data_dict['submission_id']

        try:
            response = self.peer_gs.save_calibration_essay(**data_dict)
            if 'actual_rubric' in response:
                rubric_renderer = combined_open_ended_rubric.CombinedOpenEndedRubric(
                    self.system, True)
                response['actual_rubric'] = rubric_renderer.render_rubric(
                    response['actual_rubric'])['html']
            return response
        except GradingServiceError:
            # This is a dev_facing_error
            log.exception("Error saving calibration grade")
            # This is a student_facing_error
            return self._err_response(
                'There was an error saving your score.  Please notify course staff.'
            )

    def peer_grading_closed(self):
        '''
        Show the Peer grading closed template
        '''
        html = self.system.render_template(
            'peer_grading/peer_grading_closed.html',
            {'use_for_single_location': self.use_for_single_location_local})
        return html

    def _find_corresponding_module_for_location(self, location):
        """
        Find the peer grading module that exists at the given location.
        """
        try:
            return self.descriptor.system.load_item(location)
        except ItemNotFoundError:
            # The linked problem doesn't exist.
            log.error(
                "Problem {0} does not exist in this course.".format(location))
            raise
        except NoPathToItem:
            # The linked problem does not have a path to it (ie is in a draft or other strange state).
            log.error(
                "Cannot find a path to problem {0} in this course.".format(
                    location))
            raise

    def peer_grading(self, _data=None):
        '''
        Show a peer grading interface
        '''

        # call problem list service
        success = False
        error_text = ""
        problem_list = []
        try:
            problem_list_dict = self.peer_gs.get_problem_list(
                self.course_id, self.system.anonymous_student_id)
            success = problem_list_dict['success']
            if 'error' in problem_list_dict:
                error_text = problem_list_dict['error']

            problem_list = problem_list_dict['problem_list']

        except GradingServiceError:
            # This is a student_facing_error
            error_text = EXTERNAL_GRADER_NO_CONTACT_ERROR
            log.error(error_text)
            success = False
        # catch error if if the json loads fails
        except ValueError:
            # This is a student_facing_error
            error_text = "Could not get list of problems to peer grade.  Please notify course staff."
            log.error(error_text)
            success = False
        except Exception:
            log.exception("Could not contact peer grading service.")
            success = False

        good_problem_list = []
        for problem in problem_list:
            problem_location = Location(problem['location'])
            try:
                descriptor = self._find_corresponding_module_for_location(
                    problem_location)
            except (NoPathToItem, ItemNotFoundError):
                continue
            if descriptor:
                problem['due'] = get_extended_due_date(descriptor)
                grace_period = descriptor.graceperiod
                try:
                    problem_timeinfo = TimeInfo(problem['due'], grace_period)
                except Exception:
                    log.error(
                        "Malformed due date or grace period string for location {0}"
                        .format(problem_location))
                    raise
                if self._closed(problem_timeinfo):
                    problem['closed'] = True
                else:
                    problem['closed'] = False
            else:
                # if we can't find the due date, assume that it doesn't have one
                problem['due'] = None
                problem['closed'] = False
            good_problem_list.append(problem)

        ajax_url = self.ajax_url
        html = self.system.render_template(
            'peer_grading/peer_grading.html',
            {
                'course_id': self.course_id,
                'ajax_url': ajax_url,
                'success': success,
                'problem_list': good_problem_list,
                'error_text': error_text,
                # Checked above
                'staff_access': False,
                'use_single_location': self.use_for_single_location_local,
            })

        return html

    def peer_grading_problem(self, data=None):
        '''
        Show individual problem interface
        '''
        if data is None or data.get('location') is None:
            if not self.use_for_single_location_local:
                # This is an error case, because it must be set to use a single location to be called without get parameters
                # This is a dev_facing_error
                log.error(
                    "Peer grading problem in peer_grading_module called with no get parameters, but use_for_single_location is False."
                )
                return {'html': "", 'success': False}
            problem_location = Location(self.link_to_location)

        elif data.get('location') is not None:
            problem_location = Location(data.get('location'))

        module = self._find_corresponding_module_for_location(problem_location)

        ajax_url = self.ajax_url
        html = self.system.render_template(
            'peer_grading/peer_grading_problem.html',
            {
                'view_html': '',
                'problem_location': problem_location,
                'course_id': self.course_id,
                'ajax_url': ajax_url,
                # Checked above
                'staff_access': False,
                'use_single_location': self.use_for_single_location_local,
            })

        return {'html': html, 'success': True}

    def get_instance_state(self):
        """
        Returns the current instance state.  The module can be recreated from the instance state.
        Input: None
        Output: A dictionary containing the instance state.
        """

        state = {
            'student_data_for_location': self.student_data_for_location,
        }

        return json.dumps(state)

    def _check_feedback_length(self, data):
        feedback = data.get("feedback")
        if feedback and len(feedback) > MAX_ALLOWED_FEEDBACK_LENGTH:
            return False, "Feedback is too long, Max length is {0} characters.".format(
                MAX_ALLOWED_FEEDBACK_LENGTH)
        else:
            return True, ""
class PeerGradingModule(PeerGradingFields, XModule):
    _VERSION = 1

    js = {
        "coffee": [
            resource_string(__name__, "js/src/peergrading/peer_grading.coffee"),
            resource_string(__name__, "js/src/peergrading/peer_grading_problem.coffee"),
            resource_string(__name__, "js/src/collapsible.coffee"),
            resource_string(__name__, "js/src/javascript_loader.coffee"),
        ]
    }
    js_module_name = "PeerGrading"

    css = {"scss": [resource_string(__name__, "css/combinedopenended/display.scss")]}

    def __init__(self, system, location, descriptor, model_data):
        XModule.__init__(self, system, location, descriptor, model_data)

        # We need to set the location here so the child modules can use it
        system.set("location", location)
        self.system = system
        if self.system.open_ended_grading_interface:
            self.peer_gs = PeerGradingService(self.system.open_ended_grading_interface, self.system)
        else:
            self.peer_gs = MockPeerGradingService()

        if self.use_for_single_location in TRUE_DICT:
            try:
                self.linked_problem = modulestore().get_instance(self.system.course_id, self.link_to_location)
            except:
                log.error(
                    "Linked location {0} for peer grading module {1} does not exist".format(
                        self.link_to_location, self.location
                    )
                )
                raise
            due_date = self.linked_problem._model_data.get("peer_grading_due", None)
            if due_date:
                self._model_data["due"] = due_date

        try:
            self.timeinfo = TimeInfo(self.due_date, self.grace_period_string)
        except:
            log.error("Error parsing due date information in location {0}".format(location))
            raise

        self.display_due_date = self.timeinfo.display_due_date

        try:
            self.student_data_for_location = json.loads(self.student_data_for_location)
        except:
            pass

        self.ajax_url = self.system.ajax_url
        if not self.ajax_url.endswith("/"):
            self.ajax_url = self.ajax_url + "/"

        # StringyInteger could return None, so keep this check.
        if not isinstance(self.max_grade, int):
            raise TypeError("max_grade needs to be an integer.")

    def closed(self):
        return self._closed(self.timeinfo)

    def _closed(self, timeinfo):
        if timeinfo.close_date is not None and datetime.now(UTC()) > timeinfo.close_date:
            return True
        return False

    def _err_response(self, msg):
        """
        Return a HttpResponse with a json dump with success=False, and the given error message.
        """
        return {"success": False, "error": msg}

    def _check_required(self, get, required):
        actual = set(get.keys())
        missing = required - actual
        if len(missing) > 0:
            return False, "Missing required keys: {0}".format(", ".join(missing))
        else:
            return True, ""

    def get_html(self):
        """
         Needs to be implemented by inheritors.  Renders the HTML that students see.
        @return:
        """
        if self.closed():
            return self.peer_grading_closed()
        if self.use_for_single_location not in TRUE_DICT:
            return self.peer_grading()
        else:
            return self.peer_grading_problem({"location": self.link_to_location})["html"]

    def handle_ajax(self, dispatch, get):
        """
        Needs to be implemented by child modules.  Handles AJAX events.
        @return:
        """
        handlers = {
            "get_next_submission": self.get_next_submission,
            "show_calibration_essay": self.show_calibration_essay,
            "is_student_calibrated": self.is_student_calibrated,
            "save_grade": self.save_grade,
            "save_calibration_essay": self.save_calibration_essay,
            "problem": self.peer_grading_problem,
        }

        if dispatch not in handlers:
            # This is a dev_facing_error
            log.error("Cannot find {0} in handlers in handle_ajax function for open_ended_module.py".format(dispatch))
            # This is a dev_facing_error
            return json.dumps({"error": "Error handling action.  Please try again.", "success": False})

        d = handlers[dispatch](get)

        return json.dumps(d, cls=ComplexEncoder)

    def query_data_for_location(self):
        student_id = self.system.anonymous_student_id
        location = self.link_to_location
        success = False
        response = {}

        try:
            response = self.peer_gs.get_data_for_location(location, student_id)
            count_graded = response["count_graded"]
            count_required = response["count_required"]
            success = True
        except GradingServiceError:
            # This is a dev_facing_error
            log.exception(
                "Error getting location data from controller for location {0}, student {1}".format(location, student_id)
            )

        return success, response

    def get_progress(self):
        pass

    def get_score(self):
        max_score = None
        score = None
        score_dict = {"score": score, "total": max_score}
        if self.use_for_single_location not in TRUE_DICT or self.is_graded not in TRUE_DICT:
            return score_dict

        try:
            count_graded = self.student_data_for_location["count_graded"]
            count_required = self.student_data_for_location["count_required"]
        except:
            success, response = self.query_data_for_location()
            if not success:
                log.exception(
                    "No instance data found and could not get data from controller for loc {0} student {1}".format(
                        self.system.location.url(), self.system.anonymous_student_id
                    )
                )
                return None
            count_graded = response["count_graded"]
            count_required = response["count_required"]
            if count_required > 0 and count_graded >= count_required:
                # Ensures that once a student receives a final score for peer grading, that it does not change.
                self.student_data_for_location = response

        if self.weight is not None:
            score = int(count_graded >= count_required and count_graded > 0) * float(self.weight)
            total = self.max_grade * float(self.weight)
            score_dict["score"] = score
            score_dict["total"] = total

        return score_dict

    def max_score(self):
        """ Maximum score. Two notes:

            * This is generic; in abstract, a problem could be 3/5 points on one
              randomization, and 5/7 on another
        """
        max_grade = None
        if self.use_for_single_location in TRUE_DICT and self.is_graded in TRUE_DICT:
            max_grade = self.max_grade
        return max_grade

    def get_next_submission(self, get):
        """
        Makes a call to the grading controller for the next essay that should be graded
        Returns a json dict with the following keys:

        'success': bool

        'submission_id': a unique identifier for the submission, to be passed back
                         with the grade.

        'submission': the submission, rendered as read-only html for grading

        'rubric': the rubric, also rendered as html.

        'submission_key': a key associated with the submission for validation reasons

        'error': if success is False, will have an error message with more info.
        """
        required = set(["location"])
        success, message = self._check_required(get, required)
        if not success:
            return self._err_response(message)
        grader_id = self.system.anonymous_student_id
        location = get["location"]

        try:
            response = self.peer_gs.get_next_submission(location, grader_id)
            return response
        except GradingServiceError:
            # This is a dev_facing_error
            log.exception(
                "Error getting next submission.  server url: {0}  location: {1}, grader_id: {2}".format(
                    self.peer_gs.url, location, grader_id
                )
            )
            # This is a student_facing_error
            return {"success": False, "error": EXTERNAL_GRADER_NO_CONTACT_ERROR}

    def save_grade(self, get):
        """
        Saves the grade of a given submission.
        Input:
            The request should have the following keys:
            location - problem location
            submission_id - id associated with this submission
            submission_key - submission key given for validation purposes
            score - the grade that was given to the submission
            feedback - the feedback from the student
        Returns
            A json object with the following keys:
            success: bool indicating whether the save was a success
            error: if there was an error in the submission, this is the error message
        """

        required = set(
            [
                "location",
                "submission_id",
                "submission_key",
                "score",
                "feedback",
                "rubric_scores[]",
                "submission_flagged",
            ]
        )
        success, message = self._check_required(get, required)
        if not success:
            return self._err_response(message)
        grader_id = self.system.anonymous_student_id

        location = get.get("location")
        submission_id = get.get("submission_id")
        score = get.get("score")
        feedback = get.get("feedback")
        submission_key = get.get("submission_key")
        rubric_scores = get.getlist("rubric_scores[]")
        submission_flagged = get.get("submission_flagged")

        try:
            response = self.peer_gs.save_grade(
                location, grader_id, submission_id, score, feedback, submission_key, rubric_scores, submission_flagged
            )
            return response
        except GradingServiceError:
            # This is a dev_facing_error
            log.exception(
                """Error saving grade to open ended grading service.  server url: {0}, location: {1}, submission_id:{2},
                            submission_key: {3}, score: {4}""".format(
                    self.peer_gs.url, location, submission_id, submission_key, score
                )
            )
            # This is a student_facing_error
            return {"success": False, "error": EXTERNAL_GRADER_NO_CONTACT_ERROR}

    def is_student_calibrated(self, get):
        """
        Calls the grading controller to see if the given student is calibrated
        on the given problem

        Input:
            In the request, we need the following arguments:
            location - problem location

        Returns:
            Json object with the following keys
            success - bool indicating whether or not the call was successful
            calibrated - true if the grader has fully calibrated and can now move on to grading
                       - false if the grader is still working on calibration problems
            total_calibrated_on_so_far - the number of calibration essays for this problem
                that this grader has graded
        """

        required = set(["location"])
        success, message = self._check_required(get, required)
        if not success:
            return self._err_response(message)
        grader_id = self.system.anonymous_student_id

        location = get["location"]

        try:
            response = self.peer_gs.is_student_calibrated(location, grader_id)
            return response
        except GradingServiceError:
            # This is a dev_facing_error
            log.exception(
                "Error from open ended grading service.  server url: {0}, grader_id: {0}, location: {1}".format(
                    self.peer_gs.url, grader_id, location
                )
            )
            # This is a student_facing_error
            return {"success": False, "error": EXTERNAL_GRADER_NO_CONTACT_ERROR}

    def show_calibration_essay(self, get):
        """
        Fetch the next calibration essay from the grading controller and return it
        Inputs:
            In the request
            location - problem location

        Returns:
            A json dict with the following keys
            'success': bool

            'submission_id': a unique identifier for the submission, to be passed back
                             with the grade.

            'submission': the submission, rendered as read-only html for grading

            'rubric': the rubric, also rendered as html.

            'submission_key': a key associated with the submission for validation reasons

            'error': if success is False, will have an error message with more info.

        """

        required = set(["location"])
        success, message = self._check_required(get, required)
        if not success:
            return self._err_response(message)

        grader_id = self.system.anonymous_student_id

        location = get["location"]
        try:
            response = self.peer_gs.show_calibration_essay(location, grader_id)
            return response
        except GradingServiceError:
            # This is a dev_facing_error
            log.exception(
                "Error from open ended grading service.  server url: {0}, location: {0}".format(
                    self.peer_gs.url, location
                )
            )
            # This is a student_facing_error
            return {"success": False, "error": EXTERNAL_GRADER_NO_CONTACT_ERROR}
        # if we can't parse the rubric into HTML,
        except etree.XMLSyntaxError:
            # This is a dev_facing_error
            log.exception("Cannot parse rubric string.")
            # This is a student_facing_error
            return {"success": False, "error": "Error displaying submission.  Please notify course staff."}

    def save_calibration_essay(self, get):
        """
        Saves the grader's grade of a given calibration.
        Input:
            The request should have the following keys:
            location - problem location
            submission_id - id associated with this submission
            submission_key - submission key given for validation purposes
            score - the grade that was given to the submission
            feedback - the feedback from the student
        Returns
            A json object with the following keys:
            success: bool indicating whether the save was a success
            error: if there was an error in the submission, this is the error message
            actual_score: the score that the instructor gave to this calibration essay

        """

        required = set(["location", "submission_id", "submission_key", "score", "feedback", "rubric_scores[]"])
        success, message = self._check_required(get, required)
        if not success:
            return self._err_response(message)
        grader_id = self.system.anonymous_student_id

        location = get.get("location")
        calibration_essay_id = get.get("submission_id")
        submission_key = get.get("submission_key")
        score = get.get("score")
        feedback = get.get("feedback")
        rubric_scores = get.getlist("rubric_scores[]")

        try:
            response = self.peer_gs.save_calibration_essay(
                location, grader_id, calibration_essay_id, submission_key, score, feedback, rubric_scores
            )
            if "actual_rubric" in response:
                rubric_renderer = combined_open_ended_rubric.CombinedOpenEndedRubric(self.system, True)
                response["actual_rubric"] = rubric_renderer.render_rubric(response["actual_rubric"])["html"]
            return response
        except GradingServiceError:
            # This is a dev_facing_error
            log.exception(
                "Error saving calibration grade, location: {0}, submission_key: {1}, grader_id: {2}".format(
                    location, submission_key, grader_id
                )
            )
            # This is a student_facing_error
            return self._err_response("There was an error saving your score.  Please notify course staff.")

    def peer_grading_closed(self):
        """
        Show the Peer grading closed template
        """
        html = self.system.render_template(
            "peer_grading/peer_grading_closed.html", {"use_for_single_location": self.use_for_single_location}
        )
        return html

    def peer_grading(self, get=None):
        """
        Show a peer grading interface
        """

        # call problem list service
        success = False
        error_text = ""
        problem_list = []
        try:
            problem_list_json = self.peer_gs.get_problem_list(self.system.course_id, self.system.anonymous_student_id)
            problem_list_dict = problem_list_json
            success = problem_list_dict["success"]
            if "error" in problem_list_dict:
                error_text = problem_list_dict["error"]

            problem_list = problem_list_dict["problem_list"]

        except GradingServiceError:
            # This is a student_facing_error
            error_text = EXTERNAL_GRADER_NO_CONTACT_ERROR
            log.error(error_text)
            success = False
        # catch error if if the json loads fails
        except ValueError:
            # This is a student_facing_error
            error_text = "Could not get list of problems to peer grade.  Please notify course staff."
            log.error(error_text)
            success = False
        except:
            log.exception("Could not contact peer grading service.")
            success = False

        def _find_corresponding_module_for_location(location):
            """
            find the peer grading module that links to the given location
            """
            try:
                return modulestore().get_instance(self.system.course_id, location)
            except:
                # the linked problem doesn't exist
                log.error("Problem {0} does not exist in this course".format(location))
                raise

        for problem in problem_list:
            problem_location = problem["location"]
            descriptor = _find_corresponding_module_for_location(problem_location)
            if descriptor:
                problem["due"] = descriptor._model_data.get("peer_grading_due", None)
                grace_period_string = descriptor._model_data.get("graceperiod", None)
                try:
                    problem_timeinfo = TimeInfo(problem["due"], grace_period_string)
                except:
                    log.error("Malformed due date or grace period string for location {0}".format(problem_location))
                    raise
                if self._closed(problem_timeinfo):
                    problem["closed"] = True
                else:
                    problem["closed"] = False
            else:
                # if we can't find the due date, assume that it doesn't have one
                problem["due"] = None
                problem["closed"] = False

        ajax_url = self.ajax_url
        html = self.system.render_template(
            "peer_grading/peer_grading.html",
            {
                "course_id": self.system.course_id,
                "ajax_url": ajax_url,
                "success": success,
                "problem_list": problem_list,
                "error_text": error_text,
                # Checked above
                "staff_access": False,
                "use_single_location": self.use_for_single_location,
            },
        )

        return html

    def peer_grading_problem(self, get=None):
        """
        Show individual problem interface
        """
        if get is None or get.get("location") is None:
            if self.use_for_single_location not in TRUE_DICT:
                # This is an error case, because it must be set to use a single location to be called without get parameters
                # This is a dev_facing_error
                log.error(
                    "Peer grading problem in peer_grading_module called with no get parameters, but use_for_single_location is False."
                )
                return {"html": "", "success": False}
            problem_location = self.link_to_location

        elif get.get("location") is not None:
            problem_location = get.get("location")

        ajax_url = self.ajax_url
        html = self.system.render_template(
            "peer_grading/peer_grading_problem.html",
            {
                "view_html": "",
                "problem_location": problem_location,
                "course_id": self.system.course_id,
                "ajax_url": ajax_url,
                # Checked above
                "staff_access": False,
                "use_single_location": self.use_for_single_location,
            },
        )

        return {"html": html, "success": True}

    def get_instance_state(self):
        """
        Returns the current instance state.  The module can be recreated from the instance state.
        Input: None
        Output: A dictionary containing the instance state.
        """

        state = {"student_data_for_location": self.student_data_for_location}

        return json.dumps(state)