import logging from xmodule.modulestore import search from xmodule.modulestore.django import modulestore, ModuleI18nService from xmodule.modulestore.exceptions import ItemNotFoundError, NoPathToItem from xmodule.open_ended_grading_classes.controller_query_service import ControllerQueryService from xmodule.open_ended_grading_classes.grading_service_module import GradingServiceError from django.utils.translation import ugettext as _ from django.conf import settings from edxmako.shortcuts import render_to_string log = logging.getLogger(__name__) GRADER_DISPLAY_NAMES = { 'ML': _("AI Assessment"), 'PE': _("Peer Assessment"), 'NA': _("Not yet available"), 'BC': _("Automatic Checker"), 'IN': _("Instructor Assessment"), } STUDENT_ERROR_MESSAGE = _("Error occurred while contacting the grading service. Please notify course staff.") STAFF_ERROR_MESSAGE = _("Error occurred while contacting the grading service. Please notify your edX point of contact.") def generate_problem_url(problem_url_parts, base_course_url): """ From a list of problem url parts generated by search.path_to_location and a base course url, generates a url to a problem @param problem_url_parts: Output of search.path_to_location @param base_course_url: Base url of a given course @return: A path to the problem """ problem_url = base_course_url + "/" for i, part in enumerate(problem_url_parts): if part is not None: # This is the course_key. We need to turn it into its deprecated # form. if i == 0: part = part.to_deprecated_string() # This is placed between the course id and the rest of the url. if i == 1: problem_url += "courseware/" problem_url += part + "/" return problem_url def does_location_exist(usage_key): """ Checks to see if a valid module exists at a given location (ie has not been deleted) course_id - string course id location - string location """ try: search.path_to_location(modulestore(), usage_key) return True except ItemNotFoundError: # If the problem cannot be found at the location received from the grading controller server, # it has been deleted by the course author. return False except NoPathToItem: # If the problem can be found, but there is no path to it, then we assume it is a draft. # Log a warning in any case. log.warn("Got an unexpected NoPathToItem error in staff grading with location %s. " "This is ok if it is a draft; ensure that the location is valid.", usage_key) return False def create_controller_query_service(): """ Return an instance of a service that can query edX ORA. """ return ControllerQueryService(settings.OPEN_ENDED_GRADING_INTERFACE, render_to_string) class StudentProblemList(object): """ Get a list of problems that the student has attempted from ORA. Add in metadata as needed. """ def __init__(self, course_id, user_id): """ @param course_id: The id of a course object. Get using course.id. @param user_id: The anonymous id of the user, from the unique_id_for_user function. """ self.course_id = course_id self.user_id = user_id # We want to append this string to all of our error messages. self.course_error_ending = _("for course {0} and student {1}.").format(self.course_id, user_id) # This is our generic error message. self.error_text = STUDENT_ERROR_MESSAGE self.success = False # Create a service to query edX ORA. self.controller_qs = create_controller_query_service() def fetch_from_grading_service(self): """ Fetch a list of problems that the student has answered from ORA. Handle various error conditions. @return: A boolean success indicator. """ # In the case of multiple calls, ensure that success is false initially. self.success = False try: #Get list of all open ended problems that the grading server knows about problem_list_dict = self.controller_qs.get_grading_status_list(self.course_id, self.user_id) except GradingServiceError: log.error("Problem contacting open ended grading service " + self.course_error_ending) return self.success except ValueError: log.error("Problem with results from external grading service for open ended" + self.course_error_ending) return self.success success = problem_list_dict['success'] if 'error' in problem_list_dict: self.error_text = problem_list_dict['error'] return success if 'problem_list' not in problem_list_dict: log.error("Did not receive a problem list in ORA response" + self.course_error_ending) return success self.problem_list = problem_list_dict['problem_list'] self.success = True return self.success def add_problem_data(self, base_course_url): """ Add metadata to problems. @param base_course_url: the base url for any course. Can get with reverse('course') @return: A list of valid problems in the course and their appended data. """ # Our list of valid problems. valid_problems = [] if not self.success or not isinstance(self.problem_list, list): log.error("Called add_problem_data without a valid problem list" + self.course_error_ending) return valid_problems # Iterate through all of our problems and add data. for problem in self.problem_list: try: # Try to load the problem. usage_key = self.course_id.make_usage_key_from_deprecated_string(problem['location']) problem_url_parts = search.path_to_location(modulestore(), usage_key) except (ItemNotFoundError, NoPathToItem): # If the problem cannot be found at the location received from the grading controller server, # it has been deleted by the course author. We should not display it. error_message = "Could not find module for course {0} at location {1}".format(self.course_id, problem['location']) log.error(error_message) continue # Get the problem url in the courseware. problem_url = generate_problem_url(problem_url_parts, base_course_url) # Map the grader name from ORA to a human readable version. grader_type_display_name = GRADER_DISPLAY_NAMES.get(problem['grader_type'], "edX Assessment") problem['actual_url'] = problem_url problem['grader_type_display_name'] = grader_type_display_name valid_problems.append(problem) return valid_problems