def save_scores(user, puzzle_scores): score_responses = [] for score in puzzle_scores: log.debug("score: %s", score) # expected keys ScoreType, PuzzleID (int), # BestScore (energy), CurrentScore (Energy), ScoreVersion (int) puzzle_id = score['PuzzleID'] best_score = score['BestScore'] current_score = score['CurrentScore'] score_version = score['ScoreVersion'] # SetPlayerPuzzleScoreResponse object # Score entries are unique on user/unique_user_id/puzzle_id/score_version try: obj = Score.objects.get(user=user, unique_user_id=unique_id_for_user(user), puzzle_id=puzzle_id, score_version=score_version) obj.current_score = current_score obj.best_score = best_score except Score.DoesNotExist: obj = Score(user=user, unique_user_id=unique_id_for_user(user), puzzle_id=puzzle_id, current_score=current_score, best_score=best_score, score_version=score_version) obj.save() score_responses.append({'PuzzleID': puzzle_id, 'Status': 'Success'}) return {"OperationID": "SetPlayerPuzzleScores", "Value": score_responses}
def setUp(self): self.factory = RequestFactory() self.url = reverse('foldit_ops') pwd = 'abc' self.user = User.objects.create_user('testuser', '*****@*****.**', pwd) self.user2 = User.objects.create_user('testuser2', '*****@*****.**', pwd) self.unique_user_id = unique_id_for_user(self.user) self.unique_user_id2 = unique_id_for_user(self.user2) now = datetime.now(UTC) self.tomorrow = now + timedelta(days=1) self.yesterday = now - timedelta(days=1) UserProfile.objects.create(user=self.user) UserProfile.objects.create(user=self.user2)
def _is_embargoed_by_profile_country(self, user, course_id="", course_is_embargoed=False): """ Check whether the user is embargoed based on the country code in the user's profile. Args: user (User): The user attempting to access courseware. Keyword Args: course_id (unicode): The course the user is trying to access. course_is_embargoed (boolean): Whether the course the user is accessing has been embargoed. Returns: A unicode message if the user is embargoed, otherwise `None` """ cache_key = u'user.{user_id}.profile.country'.format(user_id=user.id) profile_country = cache.get(cache_key) if profile_country is None: profile = getattr(user, 'profile', None) if profile is not None and profile.country.code is not None: profile_country = profile.country.code.upper() else: profile_country = "" cache.set(cache_key, profile_country) if profile_country in self._embargoed_countries: return self.REASONS['profile_country'].format( user_id=unique_id_for_user(user), profile_country=profile_country, from_course=self._from_course_msg(course_id, course_is_embargoed) ) else: return None
def get_problem_list(request, course_id): """ Get all the problems for the given course id Returns a json dict with the following keys: success: bool problem_list: a list containing json dicts with the following keys: each dict represents a different problem in the course location: the location of the problem problem_name: the name of the problem num_graded: the number of responses that have been graded num_pending: the number of responses that are sitting in the queue min_for_ml: the number of responses that need to be graded before the ml can be run """ _check_access(request.user, course_id) try: response = staff_grading_service().get_problem_list( course_id, unique_id_for_user(request.user)) return HttpResponse(response, mimetype="application/json") except GradingServiceError: # This is a dev_facing_error log.exception("Error from staff grading service in open ended grading. server url: {0}" .format(staff_grading_service().url)) # This is a staff_facing_error return HttpResponse(json.dumps({'success': False, 'error': STAFF_ERROR_MESSAGE}))
def save_complete(user, puzzles_complete): """ Returned list of PuzzleIDs should be in sorted order (I don't think client cares, but tests do) """ for complete in puzzles_complete: log.debug("Puzzle complete: %s", complete) puzzle_id = complete['PuzzleID'] puzzle_set = complete['Set'] puzzle_subset = complete['SubSet'] # create if not there PuzzleComplete.objects.get_or_create( user=user, unique_user_id=unique_id_for_user(user), puzzle_id=puzzle_id, puzzle_set=puzzle_set, puzzle_subset=puzzle_subset) # List of all puzzle ids of intro-level puzzles completed ever, including on this # request # TODO: this is just in this request... complete_responses = list(pc.puzzle_id for pc in PuzzleComplete.objects.filter(user=user)) return {"OperationID": "SetPuzzlesComplete", "Value": complete_responses}
def staff_grading_notifications(course, user): staff_gs = StaffGradingService(settings.OPEN_ENDED_GRADING_INTERFACE) pending_grading = False img_path = "" course_id = course.id student_id = unique_id_for_user(user) notification_type = "staff" success, notification_dict = get_value_from_cache(student_id, course_id, notification_type) if success: return notification_dict try: notifications = json.loads(staff_gs.get_notifications(course_id)) if notifications["success"]: if notifications["staff_needs_to_grade"]: pending_grading = True except: # Non catastrophic error, so no real action notifications = {} # This is a dev_facing_error log.info( "Problem with getting notifications from staff grading service for course {0} user {1}.".format( course_id, student_id ) ) if pending_grading: img_path = "/static/images/grading_notification.png" notification_dict = {"pending_grading": pending_grading, "img_path": img_path, "response": notifications} set_value_in_cache(student_id, course_id, notification_type, notification_dict) return notification_dict
def staff_grading_notifications(course, user): staff_gs = StaffGradingService(settings.OPEN_ENDED_GRADING_INTERFACE) pending_grading = False img_path = "" course_id = course.id student_id = unique_id_for_user(user) notification_type = "staff" success, notification_dict = get_value_from_cache(student_id, course_id, notification_type) if success: return notification_dict try: notifications = json.loads(staff_gs.get_notifications(course_id)) if notifications['success']: if notifications['staff_needs_to_grade']: pending_grading = True except: #Non catastrophic error, so no real action notifications = {} #This is a dev_facing_error log.info( "Problem with getting notifications from staff grading service for course {0} user {1}.".format(course_id, student_id)) if pending_grading: img_path = "/static/images/grading_notification.png" notification_dict = {'pending_grading': pending_grading, 'img_path': img_path, 'response': notifications} set_value_in_cache(student_id, course_id, notification_type, notification_dict) return notification_dict
def _is_embargoed_by_profile_country(self, user, course_id="", course_is_embargoed=False): """ Check whether the user is embargoed based on the country code in the user's profile. Args: user (User): The user attempting to access courseware. Keyword Args: course_id (unicode): The course the user is trying to access. course_is_embargoed (boolean): Whether the course the user is accessing has been embargoed. Returns: A unicode message if the user is embargoed, otherwise `None` """ cache_key = u'user.{user_id}.profile.country'.format(user_id=user.id) profile_country = cache.get(cache_key) if profile_country is None: profile = getattr(user, 'profile', None) if profile is not None: profile_country = profile.country.code.upper() else: profile_country = "" cache.set(cache_key, profile_country) if profile_country in self._embargoed_countries: return self.REASONS['profile_country'].format( user_id=unique_id_for_user(user), profile_country=profile_country, from_course=self._from_course_msg(course_id, course_is_embargoed) ) else: return None
def peer_grading_notifications(course, user): peer_gs = peer_grading_service.PeerGradingService(settings.OPEN_ENDED_GRADING_INTERFACE, render_to_string) pending_grading = False img_path = "" course_id = course.id student_id = unique_id_for_user(user) notification_type = "peer" success, notification_dict = get_value_from_cache(student_id, course_id, notification_type) if success: return notification_dict try: notifications = json.loads(peer_gs.get_notifications(course_id, student_id)) if notifications['success']: if notifications['student_needs_to_peer_grade']: pending_grading = True except: #Non catastrophic error, so no real action notifications = {} #This is a dev_facing_error log.info( "Problem with getting notifications from peer grading service for course {0} user {1}.".format(course_id, student_id)) if pending_grading: img_path = "/static/images/grading_notification.png" notification_dict = {'pending_grading': pending_grading, 'img_path': img_path, 'response': notifications} set_value_in_cache(student_id, course_id, notification_type, notification_dict) return notification_dict
def get_anon_ids(request, course_id): # pylint: disable=W0613 """ Respond with 2-column CSV output of user-id, anonymized-user-id """ # TODO: the User.objects query and CSV generation here could be # centralized into analytics. Currently analytics has similar functionality # but not quite what's needed. def csv_response(filename, header, rows): """Returns a CSV http response for the given header and rows (excel/utf-8).""" response = HttpResponse(mimetype='text/csv') response['Content-Disposition'] = 'attachment; filename={0}'.format(filename) writer = csv.writer(response, dialect='excel', quotechar='"', quoting=csv.QUOTE_ALL) # In practice, there should not be non-ascii data in this query, # but trying to do the right thing anyway. encoded = [unicode(s).encode('utf-8') for s in header] writer.writerow(encoded) for row in rows: encoded = [unicode(s).encode('utf-8') for s in row] writer.writerow(encoded) return response students = User.objects.filter( courseenrollment__course_id=course_id, ).order_by('id') header = ['User ID', 'Anonymized user ID'] rows = [[s.id, unique_id_for_user(s)] for s in students] return csv_response(course_id.replace('/', '-') + '-anon-ids.csv', header, rows)
def peer_grading_notifications(course, user): system = LmsModuleSystem(track_function=None, get_module=None, render_template=render_to_string, replace_urls=None) peer_gs = peer_grading_service.PeerGradingService(settings.OPEN_ENDED_GRADING_INTERFACE, system) pending_grading = False img_path = "" course_id = course.id student_id = unique_id_for_user(user) notification_type = "peer" success, notification_dict = get_value_from_cache(student_id, course_id, notification_type) if success: return notification_dict try: notifications = json.loads(peer_gs.get_notifications(course_id, student_id)) if notifications["success"]: if notifications["student_needs_to_peer_grade"]: pending_grading = True except: # Non catastrophic error, so no real action notifications = {} # This is a dev_facing_error log.info( "Problem with getting notifications from peer grading service for course {0} user {1}.".format( course_id, student_id ) ) if pending_grading: img_path = "/static/images/grading_notification.png" notification_dict = {"pending_grading": pending_grading, "img_path": img_path, "response": notifications} set_value_in_cache(student_id, course_id, notification_type, notification_dict) return notification_dict
def handle(self, *args, **options): if len(args) != 1: raise CommandError("Usage: unique_id_mapping %s" % " ".join(("<%s>" % arg for arg in Command.args))) course_id = args[0] # Generate the output filename from the course ID. # Change slashes to dashes first, and then append .csv extension. output_filename = course_id.replace('/', '-') + ".csv" # Figure out which students are enrolled in the course students = User.objects.filter(courseenrollment__course_id=course_id) if len(students) == 0: self.stdout.write("No students enrolled in %s" % course_id) return # Write mapping to output file in CSV format with a simple header try: with open(output_filename, 'wb') as output_file: csv_writer = csv.writer(output_file) csv_writer.writerow(("User ID", "Anonymized user ID")) for student in students: csv_writer.writerow((student.id, unique_id_for_user(student))) except IOError: raise CommandError("Error writing to file: %s" % output_filename)
def get_blank_lti(request, course_id): # pylint: disable=unused-argument """ Respond with CSV output - ID - email - grade (blank) - max_grade (blank) - comments (blank) """ course_id = CourseKey.from_string(course_id) students = User.objects.filter( courseenrollment__course_id=course_id, ).order_by('id') header = [ 'ID', 'Anonymized User ID', 'email', 'grade', 'max_grade', 'comments', ] encoded_header = [unicode(s).encode('utf-8') for s in header] rows = [[ student.id, unique_id_for_user(student, save=False), student.email, '', '', '', ] for student in students] csv_filename = "{course_id}-blank-grade-submission.csv".format( course_id=unicode(course_id).replace('/', '-'), ) return create_csv_response(csv_filename, encoded_header, rows)
def get_anon_ids(request, course_id): # pylint: disable=W0613 """ Respond with 2-column CSV output of user-id, anonymized-user-id """ # TODO: the User.objects query and CSV generation here could be # centralized into analytics. Currently analytics has similar functionality # but not quite what's needed. course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) def csv_response(filename, header, rows): """Returns a CSV http response for the given header and rows (excel/utf-8).""" response = HttpResponse(mimetype='text/csv') response['Content-Disposition'] = 'attachment; filename={0}'.format(filename) writer = csv.writer(response, dialect='excel', quotechar='"', quoting=csv.QUOTE_ALL) # In practice, there should not be non-ascii data in this query, # but trying to do the right thing anyway. encoded = [unicode(s).encode('utf-8') for s in header] writer.writerow(encoded) for row in rows: encoded = [unicode(s).encode('utf-8') for s in row] writer.writerow(encoded) return response students = User.objects.filter( courseenrollment__course_id=course_id, ).order_by('id') header = ['User ID', 'Anonymized user ID', 'Course Specific Anonymized user ID'] rows = [[s.id, unique_id_for_user(s), anonymous_id_for_user(s, course_id)] for s in students] return csv_response(course_id.to_deprecated_string().replace('/', '-') + '-anon-ids.csv', header, rows)
def handle(self, *args, **options): if len(args) != 1: raise CommandError("Usage: unique_id_mapping %s" % " ".join( ("<%s>" % arg for arg in Command.args))) course_id = args[0] # Generate the output filename from the course ID. # Change slashes to dashes first, and then append .csv extension. output_filename = course_id.replace('/', '-') + ".csv" # Figure out which students are enrolled in the course students = User.objects.filter(courseenrollment__course_id=course_id) if len(students) == 0: self.stdout.write("No students enrolled in %s" % course_id) return # Write mapping to output file in CSV format with a simple header try: with open(output_filename, 'wb') as output_file: csv_writer = csv.writer(output_file) csv_writer.writerow(("User ID", "Anonymized user ID")) for student in students: csv_writer.writerow( (student.id, unique_id_for_user(student))) except IOError: raise CommandError("Error writing to file: %s" % output_filename)
def get_anon_ids(request, course_id): # pylint: disable=W0613 """ Respond with 2-column CSV output of user-id, anonymized-user-id """ # TODO: the User.objects query and CSV generation here could be # centralized into analytics. Currently analytics has similar functionality # but not quite what's needed. def csv_response(filename, header, rows): """Returns a CSV http response for the given header and rows (excel/utf-8).""" response = HttpResponse(mimetype="text/csv") response["Content-Disposition"] = "attachment; filename={0}".format(filename) writer = csv.writer(response, dialect="excel", quotechar='"', quoting=csv.QUOTE_ALL) # In practice, there should not be non-ascii data in this query, # but trying to do the right thing anyway. encoded = [unicode(s).encode("utf-8") for s in header] writer.writerow(encoded) for row in rows: encoded = [unicode(s).encode("utf-8") for s in row] writer.writerow(encoded) return response students = User.objects.filter(courseenrollment__course_id=course_id).order_by("id") header = ["User ID", "Anonymized user ID"] rows = [[s.id, unique_id_for_user(s)] for s in students] return csv_response(course_id.replace("/", "-") + "-anon-ids.csv", header, rows)
def get_next(request, course_id): """ Get the next thing to grade for course_id and with the location specified in the request. 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. 'message': if there was no submission available, but nothing went wrong, there will be a message field. 'error': if success is False, will have an error message with more info. """ _check_access(request.user, course_id) required = set(["location"]) if request.method != "POST": raise Http404 actual = set(request.POST.keys()) missing = required - actual if len(missing) > 0: return _err_response("Missing required keys {0}".format(", ".join(missing))) grader_id = unique_id_for_user(request.user) p = request.POST location = p["location"] return HttpResponse(_get_next(course_id, grader_id, location), mimetype="application/json")
def flagged_problem_list(request, course_id): ''' Show a student problem list ''' course = get_course_with_access(request.user, course_id, 'staff') student_id = unique_id_for_user(request.user) # call problem list service success = False error_text = "" problem_list = [] base_course_url = reverse('courses') # Make a service that can query edX ORA. controller_qs = create_controller_query_service() try: problem_list_json = controller_qs.get_flagged_problem_list(course_id) problem_list_dict = json.loads(problem_list_json) success = problem_list_dict['success'] if 'error' in problem_list_dict: error_text = problem_list_dict['error'] problem_list = [] else: problem_list = problem_list_dict['flagged_submissions'] except GradingServiceError: #This is a staff_facing_error error_text = STAFF_ERROR_MESSAGE #This is a dev_facing_error log.error( "Could not get flagged problem list from external grading service for open ended." ) success = False # catch error if if the json loads fails except ValueError: #This is a staff_facing_error error_text = STAFF_ERROR_MESSAGE #This is a dev_facing_error log.error( "Could not parse problem list from external grading service response." ) success = False ajax_url = _reverse_with_slash('open_ended_flagged_problems', course_id) context = { 'course': course, 'course_id': course_id, 'ajax_url': ajax_url, 'success': success, 'problem_list': problem_list, 'error_text': error_text, # Checked above 'staff_access': True, } return render_to_response( 'open_ended_problems/open_ended_flagged_problems.html', context)
def test_process_survey_link(self): username = "******" user = Mock(username=username) id = unique_id_for_user(user) link1 = "http://www.mysurvey.com" self.assertEqual(process_survey_link(link1, user), link1) link2 = "http://www.mysurvey.com?unique={UNIQUE_ID}" link2_expected = "http://www.mysurvey.com?unique={UNIQUE_ID}".format(UNIQUE_ID=id) self.assertEqual(process_survey_link(link2, user), link2_expected)
def test_process_survey_link(self): username = "******" user = Mock(username=username) user_id = unique_id_for_user(user) link1 = "http://www.mysurvey.com" self.assertEqual(process_survey_link(link1, user), link1) link2 = "http://www.mysurvey.com?unique={UNIQUE_ID}" link2_expected = "http://www.mysurvey.com?unique={UNIQUE_ID}".format(UNIQUE_ID=user_id) self.assertEqual(process_survey_link(link2, user), link2_expected)
def get_problem_list(request, course_id): """ Get all the problems for the given course id Returns a json dict with the following keys: success: bool problem_list: a list containing json dicts with the following keys: each dict represents a different problem in the course location: the location of the problem problem_name: the name of the problem num_graded: the number of responses that have been graded num_pending: the number of responses that are sitting in the queue min_for_ml: the number of responses that need to be graded before the ml can be run """ _check_access(request.user, course_id) try: response = staff_grading_service().get_problem_list( course_id, unique_id_for_user(request.user)) response = json.loads(response) problem_list = response['problem_list'] valid_problem_list = [] for i in xrange(0, len(problem_list)): #Needed to ensure that the 'location' key can be accessed try: problem_list[i] = json.loads(problem_list[i]) except Exception: pass if does_location_exist(course_id, problem_list[i]['location']): valid_problem_list.append(problem_list[i]) response['problem_list'] = valid_problem_list response = json.dumps(response) return HttpResponse(response, mimetype="application/json") except GradingServiceError: #This is a dev_facing_error log.exception("Error from staff grading service in open " "ended grading. server url: {0}".format( staff_grading_service().url)) #This is a staff_facing_error return HttpResponse( json.dumps({ 'success': False, 'error': STAFF_ERROR_MESSAGE }))
def test_SetPlayerPuzzlesComplete_level_complete(self): # pylint: disable=invalid-name """Check that the level complete function works""" puzzles = [{ "PuzzleID": 13, "Set": 1, "SubSet": 2 }, { "PuzzleID": 53524, "Set": 1, "SubSet": 1 }] response = self.make_puzzles_complete_request(puzzles) self.assertEqual(response.content, self.set_puzzle_complete_response([13, 53524])) puzzles = [{ "PuzzleID": 14, "Set": 1, "SubSet": 3 }, { "PuzzleID": 15, "Set": 1, "SubSet": 1 }] response = self.make_puzzles_complete_request(puzzles) self.assertEqual( response.content, self.set_puzzle_complete_response([13, 14, 15, 53524])) is_complete = partial(PuzzleComplete.is_level_complete, unique_id_for_user(self.user)) self.assertTrue(is_complete(1, 1)) self.assertTrue(is_complete(1, 3)) self.assertTrue(is_complete(1, 2)) self.assertFalse(is_complete(4, 5)) puzzles = [{"PuzzleID": 74, "Set": 4, "SubSet": 5}] response = self.make_puzzles_complete_request(puzzles) self.assertTrue(is_complete(4, 5)) # Now check due dates self.assertTrue(is_complete(1, 1, due=self.tomorrow)) self.assertFalse(is_complete(1, 1, due=self.yesterday))
def flagged_problem_list(request, course_id): ''' Show a student problem list ''' course = get_course_with_access(request.user, course_id, 'staff') student_id = unique_id_for_user(request.user) # call problem list service success = False error_text = "" problem_list = [] base_course_url = reverse('courses') # Make a service that can query edX ORA. controller_qs = create_controller_query_service() try: problem_list_json = controller_qs.get_flagged_problem_list(course_id) problem_list_dict = json.loads(problem_list_json) success = problem_list_dict['success'] if 'error' in problem_list_dict: error_text = problem_list_dict['error'] problem_list = [] else: problem_list = problem_list_dict['flagged_submissions'] except GradingServiceError: #This is a staff_facing_error error_text = STAFF_ERROR_MESSAGE #This is a dev_facing_error log.error("Could not get flagged problem list from external grading service for open ended.") success = False # catch error if if the json loads fails except ValueError: #This is a staff_facing_error error_text = STAFF_ERROR_MESSAGE #This is a dev_facing_error log.error("Could not parse problem list from external grading service response.") success = False ajax_url = _reverse_with_slash('open_ended_flagged_problems', course_id) context = { 'course': course, 'course_id': course_id, 'ajax_url': ajax_url, 'success': success, 'problem_list': problem_list, 'error_text': error_text, # Checked above 'staff_access': True, } return render_to_response('open_ended_problems/open_ended_flagged_problems.html', context)
def save_scores(user, puzzle_scores): score_responses = [] for score in puzzle_scores: log.debug("score: %s", score) # expected keys ScoreType, PuzzleID (int), # BestScore (energy), CurrentScore (Energy), ScoreVersion (int) puzzle_id = score['PuzzleID'] best_score = score['BestScore'] current_score = score['CurrentScore'] score_version = score['ScoreVersion'] # SetPlayerPuzzleScoreResponse object # Score entries are unique on # user/unique_user_id/puzzle_id/score_version try: obj = Score.objects.get( user=user, unique_user_id=unique_id_for_user(user), puzzle_id=puzzle_id, score_version=score_version) obj.current_score = current_score obj.best_score = best_score except Score.DoesNotExist: obj = Score( user=user, unique_user_id=unique_id_for_user(user), puzzle_id=puzzle_id, current_score=current_score, best_score=best_score, score_version=score_version) obj.save() score_responses.append({'PuzzleID': puzzle_id, 'Status': 'Success'}) return {"OperationID": "SetPlayerPuzzleScores", "Value": score_responses}
def peer_grading_notifications(course, user): system = LmsModuleSystem( track_function=None, get_module=None, render_template=render_to_string, replace_urls=None, descriptor_runtime=None, services={ 'i18n': ModuleI18nService(), }, ) peer_gs = peer_grading_service.PeerGradingService( settings.OPEN_ENDED_GRADING_INTERFACE, system) pending_grading = False img_path = "" course_id = course.id student_id = unique_id_for_user(user) notification_type = "peer" success, notification_dict = get_value_from_cache(student_id, course_id, notification_type) if success: return notification_dict try: notifications = json.loads( peer_gs.get_notifications(course_id, student_id)) if notifications['success']: if notifications['student_needs_to_peer_grade']: pending_grading = True except: #Non catastrophic error, so no real action notifications = {} #This is a dev_facing_error log.info( "Problem with getting notifications from peer grading service for course {0} user {1}." .format(course_id, student_id)) if pending_grading: img_path = "/static/images/grading_notification.png" notification_dict = { 'pending_grading': pending_grading, 'img_path': img_path, 'response': notifications } set_value_in_cache(student_id, course_id, notification_type, notification_dict) return notification_dict
def flagged_problem_list(request, course_id): """ Show a student problem list """ course = get_course_with_access(request.user, course_id, "staff") student_id = unique_id_for_user(request.user) # call problem list service success = False error_text = "" problem_list = [] base_course_url = reverse("courses") try: problem_list_json = controller_qs.get_flagged_problem_list(course_id) problem_list_dict = json.loads(problem_list_json) success = problem_list_dict["success"] if "error" in problem_list_dict: error_text = problem_list_dict["error"] problem_list = [] else: problem_list = problem_list_dict["flagged_submissions"] except GradingServiceError: # This is a staff_facing_error error_text = STAFF_ERROR_MESSAGE # This is a dev_facing_error log.error("Could not get flagged problem list from external grading service for open ended.") success = False # catch error if if the json loads fails except ValueError: # This is a staff_facing_error error_text = STAFF_ERROR_MESSAGE # This is a dev_facing_error log.error("Could not parse problem list from external grading service response.") success = False ajax_url = _reverse_with_slash("open_ended_flagged_problems", course_id) context = { "course": course, "course_id": course_id, "ajax_url": ajax_url, "success": success, "problem_list": problem_list, "error_text": error_text, # Checked above "staff_access": True, } return render_to_response("open_ended_problems/open_ended_flagged_problems.html", context)
def make(svalue): """ Given a User value entry `svalue`, extracts the student's email and fullname, and provides a unique id for the user. Returns a dictionary with keys 'EMAIL', 'FULLNAME', and 'EDX_ID'. """ fake_user = FakeUser(svalue["user_id"], svalue["user__username"], lambda: True) entry = { "EMAIL": svalue["user__email"], "FULLNAME": svalue["name"].title(), "EDX_ID": unique_id_for_user(fake_user), } return entry
def make(svalue): """ Given a User value entry `svalue`, extracts the student's email and fullname, and provides a unique id for the user. Returns a dictionary with keys 'EMAIL', 'FULLNAME', and 'EDX_ID'. """ fake_user = FakeUser(svalue['user_id'], svalue['user__username'], lambda: True) entry = { 'EMAIL': svalue['user__email'], 'FULLNAME': svalue['name'].title(), 'EDX_ID': unique_id_for_user(fake_user) } return entry
def student_problem_list(request, course_id): """ Show a list of problems they have attempted to a student. Fetch the list from the grading controller server and append some data. @param request: The request object for this view. @param course_id: The id of the course to get the problem list for. @return: Renders an HTML problem list table. """ assert isinstance(course_id, basestring) course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) # Load the course. Don't catch any errors here, as we want them to be loud. course = get_course_with_access(request.user, 'load', course_key) # The anonymous student id is needed for communication with ORA. student_id = unique_id_for_user(request.user) base_course_url = reverse('courses') error_text = "" student_problem_list = StudentProblemList(course_key, student_id) # Get the problem list from ORA. success = student_problem_list.fetch_from_grading_service() # If we fetched the problem list properly, add in additional problem data. if success: # Add in links to problems. valid_problems = student_problem_list.add_problem_data(base_course_url) else: # Get an error message to show to the student. valid_problems = [] error_text = student_problem_list.error_text ajax_url = _reverse_with_slash('open_ended_problems', course_key) context = { 'course': course, 'course_id': course_key.to_deprecated_string(), 'ajax_url': ajax_url, 'success': success, 'problem_list': valid_problems, 'error_text': error_text, # Checked above 'staff_access': False, } return render_to_response('open_ended_problems/open_ended_problems.html', context)
def student_problem_list(request, course_id): """ Show a list of problems they have attempted to a student. Fetch the list from the grading controller server and append some data. @param request: The request object for this view. @param course_id: The id of the course to get the problem list for. @return: Renders an HTML problem list table. """ assert isinstance(course_id, basestring) course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) # Load the course. Don't catch any errors here, as we want them to be loud. course = get_course_with_access(request.user, "load", course_key) # The anonymous student id is needed for communication with ORA. student_id = unique_id_for_user(request.user) base_course_url = reverse("courses") error_text = "" student_problem_list = StudentProblemList(course_key, student_id) # Get the problem list from ORA. success = student_problem_list.fetch_from_grading_service() # If we fetched the problem list properly, add in additional problem data. if success: # Add in links to problems. valid_problems = student_problem_list.add_problem_data(base_course_url) else: # Get an error message to show to the student. valid_problems = [] error_text = student_problem_list.error_text ajax_url = _reverse_with_slash("open_ended_problems", course_key) context = { "course": course, "course_id": course_key.to_deprecated_string(), "ajax_url": ajax_url, "success": success, "problem_list": valid_problems, "error_text": error_text, # Checked above "staff_access": False, } return render_to_response("open_ended_problems/open_ended_problems.html", context)
def student_problem_list(request, course_id): """ Show a list of problems they have attempted to a student. Fetch the list from the grading controller server and append some data. @param request: The request object for this view. @param course_id: The id of the course to get the problem list for. @return: Renders an HTML problem list table. """ # Load the course. Don't catch any errors here, as we want them to be loud. course = get_course_with_access(request.user, course_id, 'load') # The anonymous student id is needed for communication with ORA. student_id = unique_id_for_user(request.user) base_course_url = reverse('courses') error_text = "" student_problem_list = StudentProblemList(course_id, student_id) # Get the problem list from ORA. success = student_problem_list.fetch_from_grading_service() # If we fetched the problem list properly, add in additional problem data. if success: # Add in links to problems. valid_problems = student_problem_list.add_problem_data(base_course_url) else: # Get an error message to show to the student. valid_problems = [] error_text = student_problem_list.error_text ajax_url = _reverse_with_slash('open_ended_problems', course_id) context = { 'course': course, 'course_id': course_id, 'ajax_url': ajax_url, 'success': success, 'problem_list': valid_problems, 'error_text': error_text, # Checked above 'staff_access': False, } return render_to_response('open_ended_problems/open_ended_problems.html', context)
def test_get_problem_list(self): """ Test to see if the StudentProblemList class can get and parse a problem list from ORA. Mock the get_grading_status_list function using StudentProblemListMockQuery. """ # Initialize a StudentProblemList object. student_problem_list = utils.StudentProblemList(self.course.id, unique_id_for_user(self.user)) # Get the initial problem list from ORA. success = student_problem_list.fetch_from_grading_service() # Should be successful, and we should have three problems. See mock class for details. self.assertTrue(success) self.assertEqual(len(student_problem_list.problem_list), 3) # See if the problem locations are valid. valid_problems = student_problem_list.add_problem_data(reverse('courses')) # One location is invalid, so we should now have two. self.assertEqual(len(valid_problems), 2) # Ensure that human names are being set properly. self.assertEqual(valid_problems[0]['grader_type_display_name'], "Instructor Assessment")
def test_SetPlayerPuzzlesComplete_level_complete(self): # pylint: disable=invalid-name """Check that the level complete function works""" puzzles = [ {"PuzzleID": 13, "Set": 1, "SubSet": 2}, {"PuzzleID": 53524, "Set": 1, "SubSet": 1} ] response = self.make_puzzles_complete_request(puzzles) self.assertEqual(response.content, self.set_puzzle_complete_response([13, 53524])) puzzles = [ {"PuzzleID": 14, "Set": 1, "SubSet": 3}, {"PuzzleID": 15, "Set": 1, "SubSet": 1} ] response = self.make_puzzles_complete_request(puzzles) self.assertEqual(response.content, self.set_puzzle_complete_response([13, 14, 15, 53524])) is_complete = partial( PuzzleComplete.is_level_complete, unique_id_for_user(self.user)) self.assertTrue(is_complete(1, 1)) self.assertTrue(is_complete(1, 3)) self.assertTrue(is_complete(1, 2)) self.assertFalse(is_complete(4, 5)) puzzles = [{"PuzzleID": 74, "Set": 4, "SubSet": 5}] response = self.make_puzzles_complete_request(puzzles) self.assertTrue(is_complete(4, 5)) # Now check due dates self.assertTrue(is_complete(1, 1, due=self.tomorrow)) self.assertFalse(is_complete(1, 1, due=self.yesterday))
def get_next(request, course_id): """ Get the next thing to grade for course_id and with the location specified in the request. 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. 'message': if there was no submission available, but nothing went wrong, there will be a message field. 'error': if success is False, will have an error message with more info. """ assert (isinstance(course_id, basestring)) course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) _check_access(request.user, course_key) required = set(['location']) if request.method != 'POST': raise Http404 actual = set(request.POST.keys()) missing = required - actual if len(missing) > 0: return _err_response('Missing required keys {0}'.format( ', '.join(missing))) grader_id = unique_id_for_user(request.user) p = request.POST location = course_key.make_usage_key_from_deprecated_string(p['location']) return HttpResponse(json.dumps(_get_next(course_key, grader_id, location)), mimetype="application/json")
def get_next(request, course_id): """ Get the next thing to grade for course_id and with the location specified in the request. 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. 'message': if there was no submission available, but nothing went wrong, there will be a message field. 'error': if success is False, will have an error message with more info. """ assert(isinstance(course_id, basestring)) course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) _check_access(request.user, course_key) required = set(['location']) if request.method != 'POST': raise Http404 actual = set(request.POST.keys()) missing = required - actual if len(missing) > 0: return _err_response('Missing required keys {0}'.format( ', '.join(missing))) grader_id = unique_id_for_user(request.user) p = request.POST location = course_key.make_usage_key_from_deprecated_string(p['location']) return HttpResponse(json.dumps(_get_next(course_key, grader_id, location)), mimetype="application/json")
def get_module_for_descriptor_internal(user, descriptor, field_data_cache, course_id, track_function, xqueue_callback_url_prefix, position=None, wrap_xmodule_display=True, grade_bucket_type=None, static_asset_path=''): """ Actually implement get_module, without requiring a request. See get_module() docstring for further details. """ # Short circuit--if the user shouldn't have access, bail without doing any work if not has_access(user, descriptor, 'load', course_id): return None # Setup system context for module instance ajax_url = reverse( 'modx_dispatch', kwargs=dict(course_id=course_id, location=descriptor.location.url(), dispatch=''), ) # Intended use is as {ajax_url}/{dispatch_command}, so get rid of the trailing slash. ajax_url = ajax_url.rstrip('/') def make_xqueue_callback(dispatch='score_update'): # Fully qualified callback URL for external queueing system relative_xqueue_callback_url = reverse( 'xqueue_callback', kwargs=dict(course_id=course_id, userid=str(user.id), mod_id=descriptor.location.url(), dispatch=dispatch), ) return xqueue_callback_url_prefix + relative_xqueue_callback_url # Default queuename is course-specific and is derived from the course that # contains the current module. # TODO: Queuename should be derived from 'course_settings.json' of each course xqueue_default_queuename = descriptor.location.org + '-' + descriptor.location.course xqueue = { 'interface': xqueue_interface, 'construct_callback': make_xqueue_callback, 'default_queuename': xqueue_default_queuename.replace(' ', '_'), 'waittime': settings.XQUEUE_WAITTIME_BETWEEN_REQUESTS } # This is a hacky way to pass settings to the combined open ended xmodule # It needs an S3 interface to upload images to S3 # It needs the open ended grading interface in order to get peer grading to be done # this first checks to see if the descriptor is the correct one, and only sends settings if it is # Get descriptor metadata fields indicating needs for various settings needs_open_ended_interface = getattr(descriptor, "needs_open_ended_interface", False) needs_s3_interface = getattr(descriptor, "needs_s3_interface", False) # Initialize interfaces to None open_ended_grading_interface = None s3_interface = None # Create interfaces if needed if needs_open_ended_interface: open_ended_grading_interface = settings.OPEN_ENDED_GRADING_INTERFACE open_ended_grading_interface[ 'mock_peer_grading'] = settings.MOCK_PEER_GRADING open_ended_grading_interface[ 'mock_staff_grading'] = settings.MOCK_STAFF_GRADING if needs_s3_interface: s3_interface = { 'access_key': getattr(settings, 'AWS_ACCESS_KEY_ID', ''), 'secret_access_key': getattr(settings, 'AWS_SECRET_ACCESS_KEY', ''), 'storage_bucket_name': getattr(settings, 'AWS_STORAGE_BUCKET_NAME', 'openended') } def inner_get_module(descriptor): """ Delegate to get_module_for_descriptor_internal() with all values except `descriptor` set. Because it does an access check, it may return None. """ # TODO: fix this so that make_xqueue_callback uses the descriptor passed into # inner_get_module, not the parent's callback. Add it as an argument.... return get_module_for_descriptor_internal( user, descriptor, field_data_cache, course_id, track_function, make_xqueue_callback, position, wrap_xmodule_display, grade_bucket_type, static_asset_path) def xblock_field_data(descriptor): student_data = DbModel(DjangoKeyValueStore(field_data_cache)) return lms_field_data(descriptor._field_data, student_data) def publish(event): """A function that allows XModules to publish events. This only supports grade changes right now.""" if event.get('event_name') != 'grade': return # Construct the key for the module key = KeyValueStore.Key(scope=Scope.user_state, student_id=user.id, block_scope_id=descriptor.location, field_name='grade') student_module = field_data_cache.find_or_create(key) # Update the grades student_module.grade = event.get('value') student_module.max_grade = event.get('max_value') # Save all changes to the underlying KeyValueStore student_module.save() # Bin score into range and increment stats score_bucket = get_score_bucket(student_module.grade, student_module.max_grade) org, course_num, run = course_id.split("/") tags = [ "org:{0}".format(org), "course:{0}".format(course_num), "run:{0}".format(run), "score_bucket:{0}".format(score_bucket) ] if grade_bucket_type is not None: tags.append('type:%s' % grade_bucket_type) statsd.increment("lms.courseware.question_answered", tags=tags) # TODO (cpennington): When modules are shared between courses, the static # prefix is going to have to be specific to the module, not the directory # that the xml was loaded from system = ModuleSystem( track_function=track_function, render_template=render_to_string, ajax_url=ajax_url, xqueue=xqueue, # TODO (cpennington): Figure out how to share info between systems filestore=descriptor.system.resources_fs, get_module=inner_get_module, user=user, # TODO (cpennington): This should be removed when all html from # a module is coming through get_html and is therefore covered # by the replace_static_urls code below replace_urls=partial( static_replace.replace_static_urls, data_directory=getattr(descriptor, 'data_dir', None), course_id=course_id, static_asset_path=static_asset_path or descriptor.static_asset_path, ), replace_course_urls=partial(static_replace.replace_course_urls, course_id=course_id), replace_jump_to_id_urls=partial(static_replace.replace_jump_to_id_urls, course_id=course_id, jump_to_id_base_url=reverse( 'jump_to_id', kwargs={ 'course_id': course_id, 'module_id': '' })), node_path=settings.NODE_PATH, xblock_field_data=xblock_field_data, publish=publish, anonymous_student_id=unique_id_for_user(user), course_id=course_id, open_ended_grading_interface=open_ended_grading_interface, s3_interface=s3_interface, cache=cache, can_execute_unsafe_code=(lambda: can_execute_unsafe_code(course_id)), # TODO: When we merge the descriptor and module systems, we can stop reaching into the mixologist (cpennington) mixins=descriptor.system.mixologist._mixins, ) # pass position specified in URL to module through ModuleSystem system.set('position', position) system.set('DEBUG', settings.DEBUG) if settings.MITX_FEATURES.get('ENABLE_PSYCHOMETRICS'): system.set( 'psychometrics_handler', # set callback for updating PsychometricsData make_psychometrics_data_update_handler(course_id, user, descriptor.location.url())) try: module = descriptor.xmodule(system) except: log.exception( "Error creating module from descriptor {0}".format(descriptor)) # make an ErrorDescriptor -- assuming that the descriptor's system is ok if has_access(user, descriptor.location, 'staff', course_id): err_descriptor_class = ErrorDescriptor else: err_descriptor_class = NonStaffErrorDescriptor err_descriptor = err_descriptor_class.from_descriptor( descriptor, error_msg=exc_info_to_str(sys.exc_info())) # Make an error module return err_descriptor.xmodule(system) system.set('user_is_staff', has_access(user, descriptor.location, 'staff', course_id)) _get_html = module.get_html if wrap_xmodule_display is True: _get_html = wrap_xmodule(module.get_html, module, 'xmodule_display.html') module.get_html = replace_static_urls(_get_html, getattr(descriptor, 'data_dir', None), course_id=course_id, static_asset_path=static_asset_path or descriptor.static_asset_path) # Allow URLs of the form '/course/' refer to the root of multicourse directory # hierarchy of this course module.get_html = replace_course_urls(module.get_html, course_id) # this will rewrite intra-courseware links # that use the shorthand /jump_to_id/<id>. This is very helpful # for studio authored courses (compared to the /course/... format) since it is # is durable with respect to moves and the author doesn't need to # know the hierarchy # NOTE: module_id is empty string here. The 'module_id' will get assigned in the replacement # function, we just need to specify something to get the reverse() to work module.get_html = replace_jump_to_id_urls( module.get_html, course_id, reverse('jump_to_id', kwargs={ 'course_id': course_id, 'module_id': '' })) if settings.MITX_FEATURES.get('DISPLAY_HISTOGRAMS_TO_STAFF'): if has_access(user, module, 'staff', course_id): module.get_html = add_histogram(module.get_html, module, user) # force the module to save after rendering module.get_html = save_module(module.get_html, module) return module
def student_problem_list(request, course_id): ''' Show a student problem list to a student. Fetch the list from the grading controller server, get some metadata, and then show it to the student. ''' course = get_course_with_access(request.user, course_id, 'load') student_id = unique_id_for_user(request.user) # call problem list service success = False error_text = "" problem_list = [] base_course_url = reverse('courses') list_to_remove = [] try: #Get list of all open ended problems that the grading server knows about problem_list_json = controller_qs.get_grading_status_list( course_id, unique_id_for_user(request.user)) problem_list_dict = json.loads(problem_list_json) success = problem_list_dict['success'] if 'error' in problem_list_dict: error_text = problem_list_dict['error'] problem_list = [] else: problem_list = problem_list_dict['problem_list'] #A list of problems to remove (problems that can't be found in the course) for i in xrange(0, len(problem_list)): try: #Try to load each problem in the courseware to get links to them problem_url_parts = search.path_to_location( modulestore(), course.id, problem_list[i]['location']) 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. #Continue with the rest of the location to construct the list error_message = "Could not find module for course {0} at location {1}".format( course.id, problem_list[i]['location']) log.error(error_message) #Mark the problem for removal from the list list_to_remove.append(i) continue problem_url = generate_problem_url(problem_url_parts, base_course_url) problem_list[i].update({'actual_url': problem_url}) eta_available = problem_list[i]['eta_available'] if isinstance(eta_available, basestring): eta_available = (eta_available.lower() == "true") eta_string = "N/A" if eta_available: try: eta_string = convert_seconds_to_human_readable( int(problem_list[i]['eta'])) except: #This is a student_facing_error eta_string = "Error getting ETA." problem_list[i].update({'eta_string': eta_string}) except GradingServiceError: #This is a student_facing_error error_text = STUDENT_ERROR_MESSAGE #This is a dev facing error log.error("Problem contacting open ended grading service.") success = False # catch error if if the json loads fails except ValueError: #This is a student facing error error_text = STUDENT_ERROR_MESSAGE #This is a dev_facing_error log.error( "Problem with results from external grading service for open ended." ) success = False #Remove problems that cannot be found in the courseware from the list problem_list = [ problem_list[i] for i in xrange(0, len(problem_list)) if i not in list_to_remove ] ajax_url = _reverse_with_slash('open_ended_problems', course_id) return render_to_response( 'open_ended_problems/open_ended_problems.html', { 'course': course, 'course_id': course_id, 'ajax_url': ajax_url, 'success': success, 'problem_list': problem_list, 'error_text': error_text, # Checked above 'staff_access': False, })
def process_survey_link(survey_link, user): """ If {UNIQUE_ID} appears in the link, replace it with a unique id for the user. Currently, this is sha1(user.username). Otherwise, return survey_link. """ return survey_link.format(UNIQUE_ID=unique_id_for_user(user))
def combined_notifications(course, user): """ Show notifications to a given user for a given course. Get notifications from the cache if possible, or from the grading controller server if not. @param course: The course object for which we are getting notifications @param user: The user object for which we are getting notifications @return: A dictionary with boolean pending_grading (true if there is pending grading), img_path (for notification image), and response (actual response from grading controller server). """ # Set up return values so that we can return them for error cases pending_grading = False img_path = "" notifications = {} notification_dict = { 'pending_grading': pending_grading, 'img_path': img_path, 'response': notifications } # We don't want to show anonymous users anything. if not user.is_authenticated(): return notification_dict # Define a mock modulesystem system = ModuleSystem(ajax_url=None, track_function=None, get_module=None, render_template=render_to_string, replace_urls=None, xblock_model_data={}) # Initialize controller query service using our mock system controller_qs = ControllerQueryService( settings.OPEN_ENDED_GRADING_INTERFACE, system) student_id = unique_id_for_user(user) user_is_staff = has_access(user, course, 'staff') course_id = course.id notification_type = "combined" # See if we have a stored value in the cache success, notification_dict = get_value_from_cache(student_id, course_id, notification_type) if success: return notification_dict # Get the time of the last login of the user last_login = user.last_login # Find the modules they have seen since they logged in last_module_seen = StudentModule.objects.filter( student=user, course_id=course_id, modified__gt=last_login).values('modified').order_by('-modified') last_module_seen_count = last_module_seen.count() if last_module_seen_count > 0: # The last time they viewed an updated notification (last module seen # minus how long notifications are cached) last_time_viewed = last_module_seen[0]['modified'] - datetime.timedelta( seconds=(NOTIFICATION_CACHE_TIME + 60)) else: # If they have not seen any modules since they logged in, then don't # refresh return { 'pending_grading': False, 'img_path': img_path, 'response': notifications } try: # Get the notifications from the grading controller controller_response = controller_qs.check_combined_notifications( course.id, student_id, user_is_staff, last_time_viewed) notifications = json.loads(controller_response) if notifications['success']: if notifications['overall_need_to_check']: pending_grading = True except: # Non catastrophic error, so no real action # This is a dev_facing_error log.exception( "Problem with getting notifications from controller query service for course {0} user {1}." .format(course_id, student_id)) if pending_grading: img_path = "/static/images/grading_notification.png" notification_dict = { 'pending_grading': pending_grading, 'img_path': img_path, 'response': notifications } # Store the notifications in the cache set_value_in_cache(student_id, course_id, notification_type, notification_dict) return notification_dict
def combined_notifications(course, user): """ Show notifications to a given user for a given course. Get notifications from the cache if possible, or from the grading controller server if not. @param course: The course object for which we are getting notifications @param user: The user object for which we are getting notifications @return: A dictionary with boolean pending_grading (true if there is pending grading), img_path (for notification image), and response (actual response from grading controller server). """ #Set up return values so that we can return them for error cases pending_grading = False img_path = "" notifications={} notification_dict = {'pending_grading': pending_grading, 'img_path': img_path, 'response': notifications} #We don't want to show anonymous users anything. if not user.is_authenticated(): return notification_dict #Define a mock modulesystem system = ModuleSystem( ajax_url=None, track_function=None, get_module = None, render_template=render_to_string, replace_urls=None, xblock_model_data= {} ) #Initialize controller query service using our mock system controller_qs = ControllerQueryService(settings.OPEN_ENDED_GRADING_INTERFACE, system) student_id = unique_id_for_user(user) user_is_staff = has_access(user, course, 'staff') course_id = course.id notification_type = "combined" #See if we have a stored value in the cache success, notification_dict = get_value_from_cache(student_id, course_id, notification_type) if success: return notification_dict #Get the time of the last login of the user last_login = user.last_login #Find the modules they have seen since they logged in last_module_seen = StudentModule.objects.filter(student=user, course_id=course_id, modified__gt=last_login).values('modified').order_by( '-modified') last_module_seen_count = last_module_seen.count() if last_module_seen_count > 0: #The last time they viewed an updated notification (last module seen minus how long notifications are cached) last_time_viewed = last_module_seen[0]['modified'] - datetime.timedelta(seconds=(NOTIFICATION_CACHE_TIME + 60)) else: #If they have not seen any modules since they logged in, then don't refresh return {'pending_grading': False, 'img_path': img_path, 'response': notifications} try: #Get the notifications from the grading controller controller_response = controller_qs.check_combined_notifications(course.id, student_id, user_is_staff, last_time_viewed) notifications = json.loads(controller_response) if notifications['success']: if notifications['overall_need_to_check']: pending_grading = True except: #Non catastrophic error, so no real action #This is a dev_facing_error log.exception( "Problem with getting notifications from controller query service for course {0} user {1}.".format( course_id, student_id)) if pending_grading: img_path = "/static/images/grading_notification.png" notification_dict = {'pending_grading': pending_grading, 'img_path': img_path, 'response': notifications} #Store the notifications in the cache set_value_in_cache(student_id, course_id, notification_type, notification_dict) return notification_dict
def combined_notifications(course, user): """ Show notifications to a given user for a given course. Get notifications from the cache if possible, or from the grading controller server if not. @param course: The course object for which we are getting notifications @param user: The user object for which we are getting notifications @return: A dictionary with boolean pending_grading (true if there is pending grading), img_path (for notification image), and response (actual response from grading controller server). """ #Set up return values so that we can return them for error cases pending_grading = False img_path = "" notifications = {} notification_dict = { 'pending_grading': pending_grading, 'img_path': img_path, 'response': notifications } #We don't want to show anonymous users anything. if not user.is_authenticated(): return notification_dict #Define a mock modulesystem system = LmsModuleSystem( static_url="/static", track_function=None, get_module=None, render_template=render_to_string, replace_urls=None, descriptor_runtime=None, services={ 'i18n': ModuleI18nService(), }, ) #Initialize controller query service using our mock system controller_qs = ControllerQueryService( settings.OPEN_ENDED_GRADING_INTERFACE, system) student_id = unique_id_for_user(user) user_is_staff = has_access(user, course, 'staff') course_id = course.id notification_type = "combined" #See if we have a stored value in the cache success, notification_dict = get_value_from_cache(student_id, course_id, notification_type) if success: return notification_dict #Get the time of the last login of the user last_login = user.last_login last_time_viewed = last_login - datetime.timedelta( seconds=(NOTIFICATION_CACHE_TIME + 60)) try: #Get the notifications from the grading controller controller_response = controller_qs.check_combined_notifications( course.id, student_id, user_is_staff, last_time_viewed) notifications = json.loads(controller_response) if notifications.get('success'): if (notifications.get('staff_needs_to_grade') or notifications.get('student_needs_to_peer_grade')): pending_grading = True except: #Non catastrophic error, so no real action #This is a dev_facing_error log.exception( u"Problem with getting notifications from controller query service for course {0} user {1}." .format(course_id, student_id)) if pending_grading: img_path = "/static/images/grading_notification.png" notification_dict = { 'pending_grading': pending_grading, 'img_path': img_path, 'response': notifications } #Store the notifications in the cache set_value_in_cache(student_id, course_id, notification_type, notification_dict) return notification_dict
def get_module_for_descriptor(user, request, descriptor, model_data_cache, course_id, position=None, wrap_xmodule_display=True, grade_bucket_type=None): """ Actually implement get_module. See docstring there for details. """ # allow course staff to masquerade as student if has_access(user, descriptor, 'staff', course_id): setup_masquerade(request, True) # Short circuit--if the user shouldn't have access, bail without doing any work if not has_access(user, descriptor, 'load', course_id): return None # Setup system context for module instance ajax_url = reverse( 'modx_dispatch', kwargs=dict(course_id=course_id, location=descriptor.location.url(), dispatch=''), ) # Intended use is as {ajax_url}/{dispatch_command}, so get rid of the trailing slash. ajax_url = ajax_url.rstrip('/') def make_xqueue_callback(dispatch='score_update'): # Fully qualified callback URL for external queueing system xqueue_callback_url = '{proto}://{host}'.format( host=request.get_host(), proto=request.META.get('HTTP_X_FORWARDED_PROTO', 'https' if request.is_secure() else 'http')) xqueue_callback_url = settings.XQUEUE_INTERFACE.get( 'callback_url', xqueue_callback_url) # allow override xqueue_callback_url += reverse( 'xqueue_callback', kwargs=dict(course_id=course_id, userid=str(user.id), id=descriptor.location.url(), dispatch=dispatch), ) return xqueue_callback_url # Default queuename is course-specific and is derived from the course that # contains the current module. # TODO: Queuename should be derived from 'course_settings.json' of each course xqueue_default_queuename = descriptor.location.org + '-' + descriptor.location.course xqueue = { 'interface': xqueue_interface, 'construct_callback': make_xqueue_callback, 'default_queuename': xqueue_default_queuename.replace(' ', '_'), 'waittime': settings.XQUEUE_WAITTIME_BETWEEN_REQUESTS } #This is a hacky way to pass settings to the combined open ended xmodule #It needs an S3 interface to upload images to S3 #It needs the open ended grading interface in order to get peer grading to be done #this first checks to see if the descriptor is the correct one, and only sends settings if it is #Get descriptor metadata fields indicating needs for various settings needs_open_ended_interface = getattr(descriptor, "needs_open_ended_interface", False) needs_s3_interface = getattr(descriptor, "needs_s3_interface", False) #Initialize interfaces to None open_ended_grading_interface = None s3_interface = None #Create interfaces if needed if needs_open_ended_interface: open_ended_grading_interface = settings.OPEN_ENDED_GRADING_INTERFACE open_ended_grading_interface[ 'mock_peer_grading'] = settings.MOCK_PEER_GRADING open_ended_grading_interface[ 'mock_staff_grading'] = settings.MOCK_STAFF_GRADING if needs_s3_interface: s3_interface = { 'access_key': getattr(settings, 'AWS_ACCESS_KEY_ID', ''), 'secret_access_key': getattr(settings, 'AWS_SECRET_ACCESS_KEY', ''), 'storage_bucket_name': getattr(settings, 'AWS_STORAGE_BUCKET_NAME', 'openended') } def inner_get_module(descriptor): """ Delegate to get_module. It does an access check, so may return None """ return get_module_for_descriptor(user, request, descriptor, model_data_cache, course_id, position) def xblock_model_data(descriptor): return DbModel( LmsKeyValueStore(descriptor._model_data, model_data_cache), descriptor.module_class, user.id, LmsUsage(descriptor.location, descriptor.location)) def publish(event): if event.get('event_name') != 'grade': return student_module, created = StudentModule.objects.get_or_create( course_id=course_id, student=user, module_type=descriptor.location.category, module_state_key=descriptor.location.url(), defaults={'state': '{}'}, ) student_module.grade = event.get('value') student_module.max_grade = event.get('max_value') student_module.save() #Bin score into range and increment stats score_bucket = get_score_bucket(student_module.grade, student_module.max_grade) org, course_num, run = course_id.split("/") tags = [ "org:{0}".format(org), "course:{0}".format(course_num), "run:{0}".format(run), "score_bucket:{0}".format(score_bucket) ] if grade_bucket_type is not None: tags.append('type:%s' % grade_bucket_type) statsd.increment("lms.courseware.question_answered", tags=tags) def can_execute_unsafe_code(): # To decide if we can run unsafe code, we check the course id against # a list of regexes configured on the server. for regex in settings.COURSES_WITH_UNSAFE_CODE: if re.match(regex, course_id): return True return False # TODO (cpennington): When modules are shared between courses, the static # prefix is going to have to be specific to the module, not the directory # that the xml was loaded from system = ModuleSystem( track_function=make_track_function(request), render_template=render_to_string, ajax_url=ajax_url, xqueue=xqueue, # TODO (cpennington): Figure out how to share info between systems filestore=descriptor.system.resources_fs, get_module=inner_get_module, user=user, # TODO (cpennington): This should be removed when all html from # a module is coming through get_html and is therefore covered # by the replace_static_urls code below replace_urls=partial( static_replace.replace_static_urls, data_directory=getattr(descriptor, 'data_dir', None), course_namespace=descriptor.location._replace(category=None, name=None), ), node_path=settings.NODE_PATH, xblock_model_data=xblock_model_data, publish=publish, anonymous_student_id=unique_id_for_user(user), course_id=course_id, open_ended_grading_interface=open_ended_grading_interface, s3_interface=s3_interface, cache=cache, can_execute_unsafe_code=can_execute_unsafe_code, ) # pass position specified in URL to module through ModuleSystem system.set('position', position) system.set('DEBUG', settings.DEBUG) if settings.MITX_FEATURES.get('ENABLE_PSYCHOMETRICS'): system.set( 'psychometrics_handler', # set callback for updating PsychometricsData make_psychometrics_data_update_handler(course_id, user, descriptor.location.url())) try: module = descriptor.xmodule(system) except: log.exception( "Error creating module from descriptor {0}".format(descriptor)) # make an ErrorDescriptor -- assuming that the descriptor's system is ok if has_access(user, descriptor.location, 'staff', course_id): err_descriptor_class = ErrorDescriptor else: err_descriptor_class = NonStaffErrorDescriptor err_descriptor = err_descriptor_class.from_xml( str(descriptor), descriptor.system, org=descriptor.location.org, course=descriptor.location.course, error_msg=exc_info_to_str(sys.exc_info())) # Make an error module return err_descriptor.xmodule(system) system.set('user_is_staff', has_access(user, descriptor.location, 'staff', course_id)) _get_html = module.get_html if wrap_xmodule_display == True: _get_html = wrap_xmodule(module.get_html, module, 'xmodule_display.html') module.get_html = replace_static_urls( _get_html, getattr(descriptor, 'data_dir', None), course_namespace=module.location._replace(category=None, name=None)) # Allow URLs of the form '/course/' refer to the root of multicourse directory # hierarchy of this course module.get_html = replace_course_urls(module.get_html, course_id) if settings.MITX_FEATURES.get('DISPLAY_HISTOGRAMS_TO_STAFF'): if has_access(user, module, 'staff', course_id): module.get_html = add_histogram(module.get_html, module, user) return module
def get_module_for_descriptor_internal(user, descriptor, model_data_cache, course_id, track_function, xqueue_callback_url_prefix, position=None, wrap_xmodule_display=True, grade_bucket_type=None): """ Actually implement get_module, without requiring a request. See get_module() docstring for further details. """ # Short circuit--if the user shouldn't have access, bail without doing any work if not has_access(user, descriptor, 'load', course_id): return None # Setup system context for module instance ajax_url = reverse('modx_dispatch', kwargs=dict(course_id=course_id, location=descriptor.location.url(), dispatch=''), ) # Intended use is as {ajax_url}/{dispatch_command}, so get rid of the trailing slash. ajax_url = ajax_url.rstrip('/') def make_xqueue_callback(dispatch='score_update'): # Fully qualified callback URL for external queueing system relative_xqueue_callback_url = reverse('xqueue_callback', kwargs=dict(course_id=course_id, userid=str(user.id), mod_id=descriptor.location.url(), dispatch=dispatch), ) return xqueue_callback_url_prefix + relative_xqueue_callback_url # Default queuename is course-specific and is derived from the course that # contains the current module. # TODO: Queuename should be derived from 'course_settings.json' of each course xqueue_default_queuename = descriptor.location.org + '-' + descriptor.location.course xqueue = {'interface': xqueue_interface, 'construct_callback': make_xqueue_callback, 'default_queuename': xqueue_default_queuename.replace(' ', '_'), 'waittime': settings.XQUEUE_WAITTIME_BETWEEN_REQUESTS } # This is a hacky way to pass settings to the combined open ended xmodule # It needs an S3 interface to upload images to S3 # It needs the open ended grading interface in order to get peer grading to be done # this first checks to see if the descriptor is the correct one, and only sends settings if it is # Get descriptor metadata fields indicating needs for various settings needs_open_ended_interface = getattr(descriptor, "needs_open_ended_interface", False) needs_s3_interface = getattr(descriptor, "needs_s3_interface", False) # Initialize interfaces to None open_ended_grading_interface = None s3_interface = None # Create interfaces if needed if needs_open_ended_interface: open_ended_grading_interface = settings.OPEN_ENDED_GRADING_INTERFACE open_ended_grading_interface['mock_peer_grading'] = settings.MOCK_PEER_GRADING open_ended_grading_interface['mock_staff_grading'] = settings.MOCK_STAFF_GRADING if needs_s3_interface: s3_interface = { 'access_key': getattr(settings, 'AWS_ACCESS_KEY_ID', ''), 'secret_access_key': getattr(settings, 'AWS_SECRET_ACCESS_KEY', ''), 'storage_bucket_name': getattr(settings, 'AWS_STORAGE_BUCKET_NAME', 'openended') } def inner_get_module(descriptor): """ Delegate to get_module_for_descriptor_internal() with all values except `descriptor` set. Because it does an access check, it may return None. """ # TODO: fix this so that make_xqueue_callback uses the descriptor passed into # inner_get_module, not the parent's callback. Add it as an argument.... return get_module_for_descriptor_internal(user, descriptor, model_data_cache, course_id, track_function, make_xqueue_callback, position, wrap_xmodule_display, grade_bucket_type) def xblock_model_data(descriptor): return DbModel( LmsKeyValueStore(descriptor._model_data, model_data_cache), descriptor.module_class, user.id, LmsUsage(descriptor.location, descriptor.location) ) def publish(event): if event.get('event_name') != 'grade': return student_module, created = StudentModule.objects.get_or_create( course_id=course_id, student=user, module_type=descriptor.location.category, module_state_key=descriptor.location.url(), defaults={'state': '{}'}, ) student_module.grade = event.get('value') student_module.max_grade = event.get('max_value') student_module.save() # Bin score into range and increment stats score_bucket = get_score_bucket(student_module.grade, student_module.max_grade) org, course_num, run = course_id.split("/") tags = ["org:{0}".format(org), "course:{0}".format(course_num), "run:{0}".format(run), "score_bucket:{0}".format(score_bucket)] if grade_bucket_type is not None: tags.append('type:%s' % grade_bucket_type) statsd.increment("lms.courseware.question_answered", tags=tags) # TODO (cpennington): When modules are shared between courses, the static # prefix is going to have to be specific to the module, not the directory # that the xml was loaded from system = ModuleSystem(track_function=track_function, render_template=render_to_string, ajax_url=ajax_url, xqueue=xqueue, # TODO (cpennington): Figure out how to share info between systems filestore=descriptor.system.resources_fs, get_module=inner_get_module, user=user, # TODO (cpennington): This should be removed when all html from # a module is coming through get_html and is therefore covered # by the replace_static_urls code below replace_urls=partial( static_replace.replace_static_urls, data_directory=getattr(descriptor, 'data_dir', None), course_namespace=descriptor.location._replace(category=None, name=None), ), node_path=settings.NODE_PATH, xblock_model_data=xblock_model_data, publish=publish, anonymous_student_id=unique_id_for_user(user), course_id=course_id, open_ended_grading_interface=open_ended_grading_interface, s3_interface=s3_interface, cache=cache, can_execute_unsafe_code=(lambda: can_execute_unsafe_code(course_id)), ) # pass position specified in URL to module through ModuleSystem system.set('position', position) system.set('DEBUG', settings.DEBUG) if settings.MITX_FEATURES.get('ENABLE_PSYCHOMETRICS'): system.set('psychometrics_handler', # set callback for updating PsychometricsData make_psychometrics_data_update_handler(course_id, user, descriptor.location.url())) try: module = descriptor.xmodule(system) except: log.exception("Error creating module from descriptor {0}".format(descriptor)) # make an ErrorDescriptor -- assuming that the descriptor's system is ok if has_access(user, descriptor.location, 'staff', course_id): err_descriptor_class = ErrorDescriptor else: err_descriptor_class = NonStaffErrorDescriptor err_descriptor = err_descriptor_class.from_descriptor( descriptor, error_msg=exc_info_to_str(sys.exc_info()) ) # Make an error module return err_descriptor.xmodule(system) system.set('user_is_staff', has_access(user, descriptor.location, 'staff', course_id)) _get_html = module.get_html if wrap_xmodule_display == True: _get_html = wrap_xmodule(module.get_html, module, 'xmodule_display.html') module.get_html = replace_static_urls( _get_html, getattr(descriptor, 'data_dir', None), course_namespace=module.location._replace(category=None, name=None)) # Allow URLs of the form '/course/' refer to the root of multicourse directory # hierarchy of this course module.get_html = replace_course_urls(module.get_html, course_id) if settings.MITX_FEATURES.get('DISPLAY_HISTOGRAMS_TO_STAFF'): if has_access(user, module, 'staff', course_id): module.get_html = add_histogram(module.get_html, module, user) return module
def combined_notifications(course, user): """ Show notifications to a given user for a given course. Get notifications from the cache if possible, or from the grading controller server if not. @param course: The course object for which we are getting notifications @param user: The user object for which we are getting notifications @return: A dictionary with boolean pending_grading (true if there is pending grading), img_path (for notification image), and response (actual response from grading controller server). """ #Set up return values so that we can return them for error cases pending_grading = False img_path = "" notifications = {} notification_dict = {'pending_grading': pending_grading, 'img_path': img_path, 'response': notifications} #We don't want to show anonymous users anything. if not user.is_authenticated(): return notification_dict #Initialize controller query service using our mock system controller_qs = ControllerQueryService(settings.OPEN_ENDED_GRADING_INTERFACE, render_to_string) student_id = unique_id_for_user(user) user_is_staff = has_access(user, 'staff', course) course_id = course.id notification_type = "combined" #See if we have a stored value in the cache success, notification_dict = get_value_from_cache(student_id, course_id, notification_type) if success: return notification_dict #Get the time of the last login of the user last_login = user.last_login last_time_viewed = last_login - datetime.timedelta(seconds=(NOTIFICATION_CACHE_TIME + 60)) try: #Get the notifications from the grading controller notifications = controller_qs.check_combined_notifications( course.id, student_id, user_is_staff, last_time_viewed, ) if notifications.get('success'): if (notifications.get('staff_needs_to_grade') or notifications.get('student_needs_to_peer_grade')): pending_grading = True except: #Non catastrophic error, so no real action #This is a dev_facing_error log.exception( u"Problem with getting notifications from controller query service for course {0} user {1}.".format( course_id, student_id)) if pending_grading: img_path = "/static/images/grading_notification.png" notification_dict = {'pending_grading': pending_grading, 'img_path': img_path, 'response': notifications} #Store the notifications in the cache set_value_in_cache(student_id, course_id, notification_type, notification_dict) return notification_dict
def get_module_for_descriptor_internal( user, descriptor, field_data_cache, course_id, track_function, xqueue_callback_url_prefix, position=None, wrap_xmodule_display=True, grade_bucket_type=None, static_asset_path="", ): """ Actually implement get_module, without requiring a request. See get_module() docstring for further details. """ # Short circuit--if the user shouldn't have access, bail without doing any work if not has_access(user, descriptor, "load", course_id): return None # Setup system context for module instance ajax_url = reverse( "modx_dispatch", kwargs=dict(course_id=course_id, location=descriptor.location.url(), dispatch="") ) # Intended use is as {ajax_url}/{dispatch_command}, so get rid of the trailing slash. ajax_url = ajax_url.rstrip("/") def make_xqueue_callback(dispatch="score_update"): # Fully qualified callback URL for external queueing system relative_xqueue_callback_url = reverse( "xqueue_callback", kwargs=dict(course_id=course_id, userid=str(user.id), mod_id=descriptor.location.url(), dispatch=dispatch), ) return xqueue_callback_url_prefix + relative_xqueue_callback_url # Default queuename is course-specific and is derived from the course that # contains the current module. # TODO: Queuename should be derived from 'course_settings.json' of each course xqueue_default_queuename = descriptor.location.org + "-" + descriptor.location.course xqueue = { "interface": xqueue_interface, "construct_callback": make_xqueue_callback, "default_queuename": xqueue_default_queuename.replace(" ", "_"), "waittime": settings.XQUEUE_WAITTIME_BETWEEN_REQUESTS, } # This is a hacky way to pass settings to the combined open ended xmodule # It needs an S3 interface to upload images to S3 # It needs the open ended grading interface in order to get peer grading to be done # this first checks to see if the descriptor is the correct one, and only sends settings if it is # Get descriptor metadata fields indicating needs for various settings needs_open_ended_interface = getattr(descriptor, "needs_open_ended_interface", False) needs_s3_interface = getattr(descriptor, "needs_s3_interface", False) # Initialize interfaces to None open_ended_grading_interface = None s3_interface = None # Create interfaces if needed if needs_open_ended_interface: open_ended_grading_interface = settings.OPEN_ENDED_GRADING_INTERFACE open_ended_grading_interface["mock_peer_grading"] = settings.MOCK_PEER_GRADING open_ended_grading_interface["mock_staff_grading"] = settings.MOCK_STAFF_GRADING if needs_s3_interface: s3_interface = { "access_key": getattr(settings, "AWS_ACCESS_KEY_ID", ""), "secret_access_key": getattr(settings, "AWS_SECRET_ACCESS_KEY", ""), "storage_bucket_name": getattr(settings, "AWS_STORAGE_BUCKET_NAME", ""), } def inner_get_module(descriptor): """ Delegate to get_module_for_descriptor_internal() with all values except `descriptor` set. Because it does an access check, it may return None. """ # TODO: fix this so that make_xqueue_callback uses the descriptor passed into # inner_get_module, not the parent's callback. Add it as an argument.... return get_module_for_descriptor_internal( user, descriptor, field_data_cache, course_id, track_function, make_xqueue_callback, position, wrap_xmodule_display, grade_bucket_type, static_asset_path, ) def xblock_field_data(descriptor): student_data = DbModel(DjangoKeyValueStore(field_data_cache)) return lms_field_data(descriptor._field_data, student_data) def publish(event): """A function that allows XModules to publish events. This only supports grade changes right now.""" if event.get("event_name") != "grade": return # Construct the key for the module key = KeyValueStore.Key( scope=Scope.user_state, student_id=user.id, block_scope_id=descriptor.location, field_name="grade" ) student_module = field_data_cache.find_or_create(key) # Update the grades student_module.grade = event.get("value") student_module.max_grade = event.get("max_value") # Save all changes to the underlying KeyValueStore student_module.save() # Bin score into range and increment stats score_bucket = get_score_bucket(student_module.grade, student_module.max_grade) org, course_num, run = course_id.split("/") tags = [ "org:{0}".format(org), "course:{0}".format(course_num), "run:{0}".format(run), "score_bucket:{0}".format(score_bucket), ] if grade_bucket_type is not None: tags.append("type:%s" % grade_bucket_type) statsd.increment("lms.courseware.question_answered", tags=tags) # TODO (cpennington): When modules are shared between courses, the static # prefix is going to have to be specific to the module, not the directory # that the xml was loaded from system = ModuleSystem( track_function=track_function, render_template=render_to_string, ajax_url=ajax_url, xqueue=xqueue, # TODO (cpennington): Figure out how to share info between systems filestore=descriptor.system.resources_fs, get_module=inner_get_module, user=user, # TODO (cpennington): This should be removed when all html from # a module is coming through get_html and is therefore covered # by the replace_static_urls code below replace_urls=partial( static_replace.replace_static_urls, data_directory=getattr(descriptor, "data_dir", None), course_id=course_id, static_asset_path=static_asset_path or descriptor.static_asset_path, ), replace_course_urls=partial(static_replace.replace_course_urls, course_id=course_id), replace_jump_to_id_urls=partial( static_replace.replace_jump_to_id_urls, course_id=course_id, jump_to_id_base_url=reverse("jump_to_id", kwargs={"course_id": course_id, "module_id": ""}), ), node_path=settings.NODE_PATH, xblock_field_data=xblock_field_data, publish=publish, anonymous_student_id=unique_id_for_user(user), course_id=course_id, open_ended_grading_interface=open_ended_grading_interface, s3_interface=s3_interface, cache=cache, can_execute_unsafe_code=(lambda: can_execute_unsafe_code(course_id)), # TODO: When we merge the descriptor and module systems, we can stop reaching into the mixologist (cpennington) mixins=descriptor.system.mixologist._mixins, ) # pass position specified in URL to module through ModuleSystem system.set("position", position) system.set("DEBUG", settings.DEBUG) if settings.MITX_FEATURES.get("ENABLE_PSYCHOMETRICS"): system.set( "psychometrics_handler", # set callback for updating PsychometricsData make_psychometrics_data_update_handler(course_id, user, descriptor.location.url()), ) try: module = descriptor.xmodule(system) except: log.exception("Error creating module from descriptor {0}".format(descriptor)) # make an ErrorDescriptor -- assuming that the descriptor's system is ok if has_access(user, descriptor.location, "staff", course_id): err_descriptor_class = ErrorDescriptor else: err_descriptor_class = NonStaffErrorDescriptor err_descriptor = err_descriptor_class.from_descriptor(descriptor, error_msg=exc_info_to_str(sys.exc_info())) # Make an error module return err_descriptor.xmodule(system) system.set("user_is_staff", has_access(user, descriptor.location, "staff", course_id)) _get_html = module.get_html if wrap_xmodule_display is True: _get_html = wrap_xmodule(module.get_html, module, "xmodule_display.html") module.get_html = replace_static_urls( _get_html, getattr(descriptor, "data_dir", None), course_id=course_id, static_asset_path=static_asset_path or descriptor.static_asset_path, ) # Allow URLs of the form '/course/' refer to the root of multicourse directory # hierarchy of this course module.get_html = replace_course_urls(module.get_html, course_id) # this will rewrite intra-courseware links # that use the shorthand /jump_to_id/<id>. This is very helpful # for studio authored courses (compared to the /course/... format) since it is # is durable with respect to moves and the author doesn't need to # know the hierarchy # NOTE: module_id is empty string here. The 'module_id' will get assigned in the replacement # function, we just need to specify something to get the reverse() to work module.get_html = replace_jump_to_id_urls( module.get_html, course_id, reverse("jump_to_id", kwargs={"course_id": course_id, "module_id": ""}) ) if settings.MITX_FEATURES.get("DISPLAY_HISTOGRAMS_TO_STAFF"): if has_access(user, module, "staff", course_id): module.get_html = add_histogram(module.get_html, module, user) # force the module to save after rendering module.get_html = save_module(module.get_html, module) return module
def save_grade(request, course_id): """ Save the grade and feedback for a submission, and, if all goes well, return the next thing to grade. Expects the following POST parameters: 'score': int 'feedback': string 'submission_id': int Returns the same thing as get_next, except that additional error messages are possible if something goes wrong with saving the grade. """ _check_access(request.user, course_id) if request.method != 'POST': raise Http404 p = request.POST required = set([ 'score', 'feedback', 'submission_id', 'location', 'submission_flagged' ]) skipped = 'skipped' in p #If the instructor has skipped grading the submission, then there will not be any rubric scores. #Only add in the rubric scores if the instructor has not skipped. if not skipped: required |= set(['rubric_scores[]']) actual = set(p.keys()) missing = required - actual if len(missing) > 0: return _err_response('Missing required keys {0}'.format( ', '.join(missing))) grader_id = unique_id_for_user(request.user) location = p['location'] try: result_json = staff_grading_service().save_grade( course_id, grader_id, p['submission_id'], p['score'], p['feedback'], skipped, p.getlist('rubric_scores[]'), p['submission_flagged']) except GradingServiceError: #This is a dev_facing_error log.exception( "Error saving grade in the staff grading interface in open ended grading. Request: {0} Course ID: {1}" .format(request, course_id)) #This is a staff_facing_error return _err_response(STAFF_ERROR_MESSAGE) try: result = json.loads(result_json) except ValueError: #This is a dev_facing_error log.exception( "save_grade returned broken json in the staff grading interface in open ended grading: {0}" .format(result_json)) #This is a staff_facing_error return _err_response(STAFF_ERROR_MESSAGE) if not result.get('success', False): #This is a dev_facing_error log.warning( 'Got success=False from staff grading service in open ended grading. Response: {0}' .format(result_json)) return _err_response(STAFF_ERROR_MESSAGE) # Ok, save_grade seemed to work. Get the next submission to grade. return HttpResponse(_get_next(course_id, grader_id, location), mimetype="application/json")
def get_problem_list(request, course_id): """ Get all the problems for the given course id Returns a json dict with the following keys: success: bool problem_list: a list containing json dicts with the following keys: each dict represents a different problem in the course location: the location of the problem problem_name: the name of the problem num_graded: the number of responses that have been graded num_pending: the number of responses that are sitting in the queue min_for_ml: the number of responses that need to be graded before the ml can be run 'error': if success is False, will have an error message with more info. """ assert(isinstance(course_id, basestring)) course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) _check_access(request.user, course_key) try: response = staff_grading_service().get_problem_list(course_key, unique_id_for_user(request.user)) # If 'problem_list' is in the response, then we got a list of problems from the ORA server. # If it is not, then ORA could not find any problems. if 'problem_list' in response: problem_list = response['problem_list'] else: problem_list = [] # Make an error messages to reflect that we could not find anything to grade. response['error'] = _( u'Cannot find any open response problems in this course. ' u'Have you submitted answers to any open response assessment questions? ' u'If not, please do so and return to this page.' ) valid_problem_list = [] for i in xrange(0, len(problem_list)): # Needed to ensure that the 'location' key can be accessed. try: problem_list[i] = json.loads(problem_list[i]) except Exception: pass if does_location_exist(course_key.make_usage_key_from_deprecated_string(problem_list[i]['location'])): valid_problem_list.append(problem_list[i]) response['problem_list'] = valid_problem_list response = json.dumps(response) return HttpResponse(response, mimetype="application/json") except GradingServiceError: #This is a dev_facing_error log.exception( "Error from staff grading service in open " "ended grading. server url: {0}".format(staff_grading_service().url) ) #This is a staff_facing_error return HttpResponse(json.dumps({'success': False, 'error': STAFF_ERROR_MESSAGE}))
def student_problem_list(request, course_id): ''' Show a student problem list to a student. Fetch the list from the grading controller server, get some metadata, and then show it to the student. ''' course = get_course_with_access(request.user, course_id, 'load') student_id = unique_id_for_user(request.user) # call problem list service success = False error_text = "" problem_list = [] base_course_url = reverse('courses') list_to_remove = [] try: #Get list of all open ended problems that the grading server knows about problem_list_json = controller_qs.get_grading_status_list(course_id, unique_id_for_user(request.user)) problem_list_dict = json.loads(problem_list_json) success = problem_list_dict['success'] if 'error' in problem_list_dict: error_text = problem_list_dict['error'] problem_list = [] else: problem_list = problem_list_dict['problem_list'] #A list of problems to remove (problems that can't be found in the course) for i in xrange(0, len(problem_list)): try: #Try to load each problem in the courseware to get links to them problem_url_parts = search.path_to_location(modulestore(), course.id, problem_list[i]['location']) 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. #Continue with the rest of the location to construct the list error_message = "Could not find module for course {0} at location {1}".format(course.id, problem_list[i][ 'location']) log.error(error_message) #Mark the problem for removal from the list list_to_remove.append(i) continue problem_url = generate_problem_url(problem_url_parts, base_course_url) problem_list[i].update({'actual_url': problem_url}) eta_available = problem_list[i]['eta_available'] if isinstance(eta_available, basestring): eta_available = (eta_available.lower() == "true") eta_string = "N/A" if eta_available: try: eta_string = convert_seconds_to_human_readable(int(problem_list[i]['eta'])) except: #This is a student_facing_error eta_string = "Error getting ETA." problem_list[i].update({'eta_string': eta_string}) except GradingServiceError: #This is a student_facing_error error_text = STUDENT_ERROR_MESSAGE #This is a dev facing error log.error("Problem contacting open ended grading service.") success = False # catch error if if the json loads fails except ValueError: #This is a student facing error error_text = STUDENT_ERROR_MESSAGE #This is a dev_facing_error log.error("Problem with results from external grading service for open ended.") success = False #Remove problems that cannot be found in the courseware from the list problem_list = [problem_list[i] for i in xrange(0, len(problem_list)) if i not in list_to_remove] ajax_url = _reverse_with_slash('open_ended_problems', course_id) return render_to_response('open_ended_problems/open_ended_problems.html', { 'course': course, 'course_id': course_id, 'ajax_url': ajax_url, 'success': success, 'problem_list': problem_list, 'error_text': error_text, # Checked above 'staff_access': False, })
def get_problem_list(request, course_id): """ Get all the problems for the given course id Returns a json dict with the following keys: success: bool problem_list: a list containing json dicts with the following keys: each dict represents a different problem in the course location: the location of the problem problem_name: the name of the problem num_graded: the number of responses that have been graded num_pending: the number of responses that are sitting in the queue min_for_ml: the number of responses that need to be graded before the ml can be run 'error': if success is False, will have an error message with more info. """ _check_access(request.user, course_id) try: response = staff_grading_service().get_problem_list( course_id, unique_id_for_user(request.user)) response = json.loads(response) # If 'problem_list' is in the response, then we got a list of problems from the ORA server. # If it is not, then ORA could not find any problems. if 'problem_list' in response: problem_list = response['problem_list'] else: problem_list = [] # Make an error messages to reflect that we could not find anything to grade. response['error'] = ( "Cannot find any open response problems in this course. " "Have you submitted answers to any open response assessment questions? " "If not, please do so and return to this page.") valid_problem_list = [] for i in xrange(0, len(problem_list)): # Needed to ensure that the 'location' key can be accessed. try: problem_list[i] = json.loads(problem_list[i]) except Exception: pass if does_location_exist(course_id, problem_list[i]['location']): valid_problem_list.append(problem_list[i]) response['problem_list'] = valid_problem_list response = json.dumps(response) return HttpResponse(response, mimetype="application/json") except GradingServiceError: #This is a dev_facing_error log.exception("Error from staff grading service in open " "ended grading. server url: {0}".format( staff_grading_service().url)) #This is a staff_facing_error return HttpResponse( json.dumps({ 'success': False, 'error': STAFF_ERROR_MESSAGE }))
def save_grade(request, course_id): """ Save the grade and feedback for a submission, and, if all goes well, return the next thing to grade. Expects the following POST parameters: 'score': int 'feedback': string 'submission_id': int Returns the same thing as get_next, except that additional error messages are possible if something goes wrong with saving the grade. """ course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) _check_access(request.user, course_key) if request.method != 'POST': raise Http404 p = request.POST required = set(['score', 'feedback', 'submission_id', 'location', 'submission_flagged']) skipped = 'skipped' in p #If the instructor has skipped grading the submission, then there will not be any rubric scores. #Only add in the rubric scores if the instructor has not skipped. if not skipped: required.add('rubric_scores[]') actual = set(p.keys()) missing = required - actual if len(missing) > 0: return _err_response('Missing required keys {0}'.format( ', '.join(missing))) success, message = check_feedback_length(p) if not success: return _err_response(message) grader_id = unique_id_for_user(request.user) location = course_key.make_usage_key_from_deprecated_string(p['location']) try: result = staff_grading_service().save_grade(course_key, grader_id, p['submission_id'], p['score'], p['feedback'], skipped, p.getlist('rubric_scores[]'), p['submission_flagged']) except GradingServiceError: #This is a dev_facing_error log.exception( "Error saving grade in the staff grading interface in open ended grading. Request: {0} Course ID: {1}".format( request, course_id)) #This is a staff_facing_error return _err_response(STAFF_ERROR_MESSAGE) except ValueError: #This is a dev_facing_error log.exception( "save_grade returned broken json in the staff grading interface in open ended grading: {0}".format( result_json)) #This is a staff_facing_error return _err_response(STAFF_ERROR_MESSAGE) if not result.get('success', False): #This is a dev_facing_error log.warning( 'Got success=False from staff grading service in open ended grading. Response: {0}'.format(result_json)) return _err_response(STAFF_ERROR_MESSAGE) # Ok, save_grade seemed to work. Get the next submission to grade. return HttpResponse(json.dumps(_get_next(course_id, grader_id, location)), mimetype="application/json")