def reset_student_attempts(request, course_id): """ Resets a students attempts counter or starts a task to reset all students attempts counters. Optionally deletes student state for a problem. Limited to staff access. Some sub-methods limited to instructor access. Takes some of the following query paremeters - problem_to_reset is a urlname of a problem - student_email is an email - all_students is a boolean requires instructor access mutually exclusive with delete_module mutually exclusive with delete_module - delete_module is a boolean requires instructor access mutually exclusive with all_students """ course = get_course_with_access( request.user, course_id, 'staff', depth=None ) problem_to_reset = strip_if_string(request.GET.get('problem_to_reset')) student_email = strip_if_string(request.GET.get('student_email')) all_students = request.GET.get('all_students', False) in ['true', 'True', True] delete_module = request.GET.get('delete_module', False) in ['true', 'True', True] # parameter combinations if all_students and student_email: return HttpResponseBadRequest( "all_students and student_email are mutually exclusive." ) if all_students and delete_module: return HttpResponseBadRequest( "all_students and delete_module are mutually exclusive." ) # instructor authorization if all_students or delete_module: if not has_access(request.user, course, 'instructor'): return HttpResponseForbidden("Requires instructor access.") module_state_key = _msk_from_problem_urlname(course_id, problem_to_reset) response_payload = {} response_payload['problem_to_reset'] = problem_to_reset if student_email: try: student = User.objects.get(email=student_email) enrollment.reset_student_attempts(course_id, student, module_state_key, delete_module=delete_module) except StudentModule.DoesNotExist: return HttpResponseBadRequest("Module does not exist.") elif all_students: instructor_task.api.submit_reset_problem_attempts_for_all_students(request, course_id, module_state_key) response_payload['task'] = 'created' else: return HttpResponseBadRequest() return JsonResponse(response_payload)
def remove_user_from_role(request, username_or_email, role, group_title, event_name): """ Look up the given user by username (if no '@') or email (otherwise), and remove them from the supplied role. Arguments: request: django request--used for tracking log username_or_email: who to remove. Decide if it's an email by presense of an '@' role: A student.roles.AccessRole group_title: what to call this group in messages to user--e.g. "beta-testers". event_name: what to call this event when logging to tracking logs. Returns: html to insert in the message field """ username_or_email = strip_if_string(username_or_email) try: user = _user_from_name_or_email(username_or_email) except User.DoesNotExist: return u'<font color="red">Error: unknown username or email "{0}"</font>'.format(username_or_email) role.remove_users(user) # Deal with historical event names if event_name in ("staff", "beta-tester"): track.views.server_track( request, "add-or-remove-user-group", {"event_name": event_name, "user": unicode(user), "event": "remove"}, page="idashboard", ) else: track.views.server_track(request, "remove-instructor", {"instructor": unicode(user)}, page="idashboard") return '<font color="green">Removed {0} from {1}</font>'.format(user, group_title)
def list_instructor_tasks(request, course_id): """ List instructor tasks. Limited to instructor access. Takes optional query paremeters. - With no arguments, lists running tasks. - `problem_urlname` lists task history for problem - `problem_urlname` and `student_email` lists task history for problem AND student (intersection) """ problem_urlname = strip_if_string(request.GET.get('problem_urlname', False)) student_email = strip_if_string(request.GET.get('student_email', False)) if student_email and not problem_urlname: return HttpResponseBadRequest( "student_email must accompany problem_urlname") if problem_urlname: module_state_key = _msk_from_problem_urlname(course_id, problem_urlname) if student_email: student = User.objects.get(email=student_email) tasks = instructor_task.api.get_instructor_task_history( course_id, module_state_key, student) else: tasks = instructor_task.api.get_instructor_task_history( course_id, module_state_key) else: tasks = instructor_task.api.get_running_instructor_tasks(course_id) def extract_task_features(task): """ Convert task to dict for json rendering """ features = [ 'task_type', 'task_input', 'task_id', 'requester', 'created', 'task_state' ] return dict( (feature, str(getattr(task, feature))) for feature in features) response_payload = { 'tasks': map(extract_task_features, tasks), } return JsonResponse(response_payload)
def update_forum_role_membership(request, course_id): """ Modify user's forum role. The requesting user must be at least staff. Staff forum admins can access all roles EXCEPT for FORUM_ROLE_ADMINISTRATOR which is limited to instructors. No one can revoke an instructors FORUM_ROLE_ADMINISTRATOR status. Query parameters: - `email` is the target users email - `rolename` is one of [FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA] - `action` is one of ['allow', 'revoke'] """ course = get_course_by_id(course_id) has_instructor_access = has_access(request.user, course, 'instructor') has_forum_admin = has_forum_access(request.user, course_id, FORUM_ROLE_ADMINISTRATOR) email = strip_if_string(request.GET.get('email')) rolename = request.GET.get('rolename') action = request.GET.get('action') # default roles require either (staff & forum admin) or (instructor) if not (has_forum_admin or has_instructor_access): return HttpResponseBadRequest( "Operation requires staff & forum admin or instructor access") # EXCEPT FORUM_ROLE_ADMINISTRATOR requires (instructor) if rolename == FORUM_ROLE_ADMINISTRATOR and not has_instructor_access: return HttpResponseBadRequest("Operation requires instructor access.") if not rolename in [ FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA ]: return HttpResponseBadRequest( "Unrecognized rolename '{}'.".format(rolename)) user = User.objects.get(email=email) target_is_instructor = has_access(user, course, 'instructor') # cannot revoke instructor if target_is_instructor and action == 'revoke' and rolename == FORUM_ROLE_ADMINISTRATOR: return HttpResponseBadRequest( "Cannot revoke instructor forum admin privelages.") try: update_forum_role(course_id, user, rolename, action) except Role.DoesNotExist: return HttpResponseBadRequest("Role does not exist.") response_payload = { 'course_id': course_id, 'action': action, } return JsonResponse(response_payload)
def rescore_problem(request, course_id): """ Starts a background process a students attempts counter. Optionally deletes student state for a problem. Limited to instructor access. Takes either of the following query paremeters - problem_to_reset is a urlname of a problem - student_email is an email - all_students is a boolean all_students and student_email cannot both be present. """ problem_to_reset = strip_if_string(request.GET.get('problem_to_reset')) student_email = strip_if_string(request.GET.get('student_email', False)) all_students = request.GET.get('all_students') in ['true', 'True', True] if not (problem_to_reset and (all_students or student_email)): return HttpResponseBadRequest("Missing query parameters.") if all_students and student_email: return HttpResponseBadRequest( "Cannot rescore with all_students and student_email.") module_state_key = _msk_from_problem_urlname(course_id, problem_to_reset) response_payload = {} response_payload['problem_to_reset'] = problem_to_reset if student_email: response_payload['student_email'] = student_email student = User.objects.get(email=student_email) instructor_task.api.submit_rescore_problem_for_student( request, course_id, module_state_key, student) response_payload['task'] = 'created' elif all_students: instructor_task.api.submit_rescore_problem_for_all_students( request, course_id, module_state_key) response_payload['task'] = 'created' else: return HttpResponseBadRequest() return JsonResponse(response_payload)
def rescore_problem(request, course_id): """ Starts a background process a students attempts counter. Optionally deletes student state for a problem. Limited to instructor access. Takes either of the following query paremeters - problem_to_reset is a urlname of a problem - student_email is an email - all_students is a boolean all_students and student_email cannot both be present. """ problem_to_reset = strip_if_string(request.GET.get('problem_to_reset')) student_email = strip_if_string(request.GET.get('student_email', False)) all_students = request.GET.get('all_students') in ['true', 'True', True] if not (problem_to_reset and (all_students or student_email)): return HttpResponseBadRequest("Missing query parameters.") if all_students and student_email: return HttpResponseBadRequest( "Cannot rescore with all_students and student_email." ) module_state_key = _msk_from_problem_urlname(course_id, problem_to_reset) response_payload = {} response_payload['problem_to_reset'] = problem_to_reset if student_email: response_payload['student_email'] = student_email student = User.objects.get(email=student_email) instructor_task.api.submit_rescore_problem_for_student(request, course_id, module_state_key, student) response_payload['task'] = 'created' elif all_students: instructor_task.api.submit_rescore_problem_for_all_students(request, course_id, module_state_key) response_payload['task'] = 'created' else: return HttpResponseBadRequest() return JsonResponse(response_payload)
def update_forum_role_membership(request, course_id): """ Modify user's forum role. The requesting user must be at least staff. Staff forum admins can access all roles EXCEPT for FORUM_ROLE_ADMINISTRATOR which is limited to instructors. No one can revoke an instructors FORUM_ROLE_ADMINISTRATOR status. Query parameters: - `email` is the target users email - `rolename` is one of [FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA] - `action` is one of ['allow', 'revoke'] """ course = get_course_by_id(course_id) has_instructor_access = has_access(request.user, course, 'instructor') has_forum_admin = has_forum_access( request.user, course_id, FORUM_ROLE_ADMINISTRATOR ) email = strip_if_string(request.GET.get('email')) rolename = request.GET.get('rolename') action = request.GET.get('action') # default roles require either (staff & forum admin) or (instructor) if not (has_forum_admin or has_instructor_access): return HttpResponseBadRequest( "Operation requires staff & forum admin or instructor access" ) # EXCEPT FORUM_ROLE_ADMINISTRATOR requires (instructor) if rolename == FORUM_ROLE_ADMINISTRATOR and not has_instructor_access: return HttpResponseBadRequest("Operation requires instructor access.") if not rolename in [FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA]: return HttpResponseBadRequest("Unrecognized rolename '{}'.".format(rolename)) user = User.objects.get(email=email) target_is_instructor = has_access(user, course, 'instructor') # cannot revoke instructor if target_is_instructor and action == 'revoke' and rolename == FORUM_ROLE_ADMINISTRATOR: return HttpResponseBadRequest("Cannot revoke instructor forum admin privelages.") try: access.update_forum_role_membership(course_id, user, rolename, action) except Role.DoesNotExist: return HttpResponseBadRequest("Role does not exist.") response_payload = { 'course_id': course_id, 'action': action, } return JsonResponse(response_payload)
def _user_from_name_or_email(username_or_email): """ Return the `django.contrib.auth.User` with the supplied username or email. If `username_or_email` contains an `@` it is treated as an email, otherwise it is treated as the username """ username_or_email = strip_if_string(username_or_email) if '@' in username_or_email: return User.objects.get(email=username_or_email) else: return User.objects.get(username=username_or_email)
def list_instructor_tasks(request, course_id): """ List instructor tasks. Limited to instructor access. Takes optional query paremeters. - With no arguments, lists running tasks. - `problem_urlname` lists task history for problem - `problem_urlname` and `student_email` lists task history for problem AND student (intersection) """ problem_urlname = strip_if_string(request.GET.get('problem_urlname', False)) student_email = strip_if_string(request.GET.get('student_email', False)) if student_email and not problem_urlname: return HttpResponseBadRequest( "student_email must accompany problem_urlname" ) if problem_urlname: module_state_key = _msk_from_problem_urlname(course_id, problem_urlname) if student_email: student = User.objects.get(email=student_email) tasks = instructor_task.api.get_instructor_task_history(course_id, module_state_key, student) else: tasks = instructor_task.api.get_instructor_task_history(course_id, module_state_key) else: tasks = instructor_task.api.get_running_instructor_tasks(course_id) def extract_task_features(task): """ Convert task to dict for json rendering """ features = ['task_type', 'task_input', 'task_id', 'requester', 'created', 'task_state'] return dict((feature, str(getattr(task, feature))) for feature in features) response_payload = { 'tasks': map(extract_task_features, tasks), } return JsonResponse(response_payload)
def modify_access(request, course_id): """ Modify staff/instructor access of other user. Requires instructor access. NOTE: instructors cannot remove their own instructor access. Query parameters: email is the target users email rolename is one of ['instructor', 'staff', 'beta'] action is one of ['allow', 'revoke'] """ course = get_course_with_access( request.user, course_id, 'instructor', depth=None ) email = strip_if_string(request.GET.get('email')) rolename = request.GET.get('rolename') action = request.GET.get('action') if not rolename in ['instructor', 'staff', 'beta']: return HttpResponseBadRequest( "unknown rolename '{}'".format(rolename) ) user = User.objects.get(email=email) # disallow instructors from removing their own instructor access. if rolename == 'instructor' and user == request.user and action != 'allow': return HttpResponseBadRequest( "An instructor cannot remove their own instructor access." ) if action == 'allow': access.allow_access(course, user, rolename) elif action == 'revoke': access.revoke_access(course, user, rolename) else: return HttpResponseBadRequest("unrecognized action '{}'".format(action)) response_payload = { 'email': email, 'rolename': rolename, 'action': action, 'success': 'yes', } return JsonResponse(response_payload)
def get_student_progress_url(request, course_id): """ Get the progress url of a student. Limited to staff access. Takes query paremeter student_email and if the student exists returns e.g. { 'progress_url': '/../...' } """ student_email = strip_if_string(request.GET.get('student_email')) user = User.objects.get(email=student_email) progress_url = reverse('student_progress', kwargs={'course_id': course_id, 'student_id': user.id}) response_payload = { 'course_id': course_id, 'progress_url': progress_url, } return JsonResponse(response_payload)
def list_instructor_tasks(request, course_id): """ List instructor tasks. Takes optional query paremeters. - With no arguments, lists running tasks. - `problem_urlname` lists task history for problem - `problem_urlname` and `unique_student_identifier` lists task history for problem AND student (intersection) """ problem_urlname = strip_if_string(request.GET.get('problem_urlname', False)) student = request.GET.get('unique_student_identifier', None) if student is not None: student = get_student_from_identifier(student) if student and not problem_urlname: return HttpResponseBadRequest( "unique_student_identifier must accompany problem_urlname") if problem_urlname: module_state_key = _msk_from_problem_urlname(course_id, problem_urlname) if student: # Specifying for a single student's history on this problem tasks = instructor_task.api.get_instructor_task_history( course_id, module_state_key, student) else: # Specifying for single problem's history tasks = instructor_task.api.get_instructor_task_history( course_id, module_state_key) else: # If no problem or student, just get currently running tasks tasks = instructor_task.api.get_running_instructor_tasks(course_id) response_payload = { 'tasks': map(extract_task_features, tasks), } return JsonResponse(response_payload)
def remove_user_from_role(request, username_or_email, role, group_title, event_name): """ Look up the given user by username (if no '@') or email (otherwise), and remove them from the supplied role. Arguments: request: django request--used for tracking log username_or_email: who to remove. Decide if it's an email by presense of an '@' role: A student.roles.AccessRole group_title: what to call this group in messages to user--e.g. "beta-testers". event_name: what to call this event when logging to tracking logs. Returns: html to insert in the message field """ username_or_email = strip_if_string(username_or_email) try: user = _user_from_name_or_email(username_or_email) except User.DoesNotExist: return u'<font color="red">Error: unknown username or email "{0}"</font>'.format(username_or_email) role.remove_users(user) # Deal with historical event names if event_name in ('staff', 'beta-tester'): track.views.server_track( request, "add-or-remove-user-group", { "event_name": event_name, "user": unicode(user), "event": "remove" }, page="idashboard" ) else: track.views.server_track(request, "remove-instructor", {"instructor": unicode(user)}, page="idashboard") return '<font color="green">Removed {0} from {1}</font>'.format(user, group_title)
def get_student_progress_url(request, course_id): """ Get the progress url of a student. Limited to staff access. Takes query paremeter student_email and if the student exists returns e.g. { 'progress_url': '/../...' } """ student_email = strip_if_string(request.GET.get('student_email')) user = User.objects.get(email=student_email) progress_url = reverse('student_progress', kwargs={ 'course_id': course_id, 'student_id': user.id }) response_payload = { 'course_id': course_id, 'progress_url': progress_url, } return JsonResponse(response_payload)
def list_instructor_tasks(request, course_id): """ List instructor tasks. Takes optional query paremeters. - With no arguments, lists running tasks. - `problem_urlname` lists task history for problem - `problem_urlname` and `unique_student_identifier` lists task history for problem AND student (intersection) """ problem_urlname = strip_if_string(request.GET.get('problem_urlname', False)) student = request.GET.get('unique_student_identifier', None) if student is not None: student = get_student_from_identifier(student) if student and not problem_urlname: return HttpResponseBadRequest( "unique_student_identifier must accompany problem_urlname" ) if problem_urlname: module_state_key = _msk_from_problem_urlname(course_id, problem_urlname) if student: # Specifying for a single student's history on this problem tasks = instructor_task.api.get_instructor_task_history(course_id, module_state_key, student) else: # Specifying for single problem's history tasks = instructor_task.api.get_instructor_task_history(course_id, module_state_key) else: # If no problem or student, just get currently running tasks tasks = instructor_task.api.get_running_instructor_tasks(course_id) response_payload = { 'tasks': map(extract_task_features, tasks), } return JsonResponse(response_payload)
def list_instructor_tasks(request, course_id): """ List instructor tasks. Limited to instructor access. Takes optional query paremeters. - With no arguments, lists running tasks. - `problem_urlname` lists task history for problem - `problem_urlname` and `unique_student_identifier` lists task history for problem AND student (intersection) """ problem_urlname = strip_if_string(request.GET.get('problem_urlname', False)) student = request.GET.get('unique_student_identifier', None) if student is not None: student = get_student_from_identifier(student) if student and not problem_urlname: return HttpResponseBadRequest( "unique_student_identifier must accompany problem_urlname" ) if problem_urlname: module_state_key = _msk_from_problem_urlname(course_id, problem_urlname) if student: tasks = instructor_task.api.get_instructor_task_history(course_id, module_state_key, student) else: tasks = instructor_task.api.get_instructor_task_history(course_id, module_state_key) else: tasks = instructor_task.api.get_running_instructor_tasks(course_id) def extract_task_features(task): """ Convert task to dict for json rendering. Expects tasks have the following features: * task_type (str, type of task) * task_input (dict, input(s) to the task) * task_id (str, celery id of the task) * requester (str, username who submitted the task) * task_state (str, state of task eg PROGRESS, COMPLETED) * created (datetime, when the task was completed) * task_output (optional) """ # Pull out information from the task features = ['task_type', 'task_input', 'task_id', 'requester', 'task_state'] task_feature_dict = {feature: str(getattr(task, feature)) for feature in features} # Some information (created, duration, status, task message) require additional formatting task_feature_dict['created'] = task.created.isoformat() # Get duration info, if known duration_sec = 'unknown' if hasattr(task, 'task_output') and task.task_output is not None: try: task_output = json.loads(task.task_output) except ValueError: log.error("Could not parse task output as valid json; task output: %s", task.task_output) else: if 'duration_ms' in task_output: duration_sec = int(task_output['duration_ms'] / 1000.0) task_feature_dict['duration_sec'] = duration_sec # Get progress status message & success information success, task_message = get_task_completion_info(task) status = _("Complete") if success else _("Incomplete") task_feature_dict['status'] = status task_feature_dict['task_message'] = task_message return task_feature_dict response_payload = { 'tasks': map(extract_task_features, tasks), } return JsonResponse(response_payload)
def list_instructor_tasks(request, course_id): """ List instructor tasks. Limited to instructor access. Takes optional query paremeters. - With no arguments, lists running tasks. - `problem_urlname` lists task history for problem - `problem_urlname` and `unique_student_identifier` lists task history for problem AND student (intersection) """ problem_urlname = strip_if_string(request.GET.get('problem_urlname', False)) student = request.GET.get('unique_student_identifier', None) if student is not None: student = get_student_from_identifier(student) if student and not problem_urlname: return HttpResponseBadRequest( "unique_student_identifier must accompany problem_urlname") if problem_urlname: module_state_key = _msk_from_problem_urlname(course_id, problem_urlname) if student: tasks = instructor_task.api.get_instructor_task_history( course_id, module_state_key, student) else: tasks = instructor_task.api.get_instructor_task_history( course_id, module_state_key) else: tasks = instructor_task.api.get_running_instructor_tasks(course_id) def extract_task_features(task): """ Convert task to dict for json rendering. Expects tasks have the following features: * task_type (str, type of task) * task_input (dict, input(s) to the task) * task_id (str, celery id of the task) * requester (str, username who submitted the task) * task_state (str, state of task eg PROGRESS, COMPLETED) * created (datetime, when the task was completed) * task_output (optional) """ # Pull out information from the task features = [ 'task_type', 'task_input', 'task_id', 'requester', 'task_state' ] task_feature_dict = { feature: str(getattr(task, feature)) for feature in features } # Some information (created, duration, status, task message) require additional formatting task_feature_dict['created'] = task.created.isoformat() # Get duration info, if known duration_sec = 'unknown' if hasattr(task, 'task_output') and task.task_output is not None: try: task_output = json.loads(task.task_output) except ValueError: log.error( "Could not parse task output as valid json; task output: %s", task.task_output) else: if 'duration_ms' in task_output: duration_sec = int(task_output['duration_ms'] / 1000.0) task_feature_dict['duration_sec'] = duration_sec # Get progress status message & success information success, task_message = get_task_completion_info(task) status = _("Complete") if success else _("Incomplete") task_feature_dict['status'] = status task_feature_dict['task_message'] = task_message return task_feature_dict response_payload = { 'tasks': map(extract_task_features, tasks), } return JsonResponse(response_payload)
def reset_student_attempts(request, course_id): """ Resets a students attempts counter or starts a task to reset all students attempts counters. Optionally deletes student state for a problem. Limited to staff access. Some sub-methods limited to instructor access. Takes some of the following query paremeters - problem_to_reset is a urlname of a problem - unique_student_identifier is an email or username - all_students is a boolean requires instructor access mutually exclusive with delete_module mutually exclusive with delete_module - delete_module is a boolean requires instructor access mutually exclusive with all_students """ course = get_course_with_access(request.user, course_id, 'staff', depth=None) problem_to_reset = strip_if_string(request.GET.get('problem_to_reset')) student_identifier = request.GET.get('unique_student_identifier', None) student = None if student_identifier is not None: student = get_student_from_identifier(student_identifier) all_students = request.GET.get('all_students', False) in ['true', 'True', True] delete_module = request.GET.get('delete_module', False) in ['true', 'True', True] # parameter combinations if all_students and student: return HttpResponseBadRequest( "all_students and unique_student_identifier are mutually exclusive." ) if all_students and delete_module: return HttpResponseBadRequest( "all_students and delete_module are mutually exclusive.") # instructor authorization if all_students or delete_module: if not has_access(request.user, course, 'instructor'): return HttpResponseForbidden("Requires instructor access.") module_state_key = _msk_from_problem_urlname(course_id, problem_to_reset) response_payload = {} response_payload['problem_to_reset'] = problem_to_reset if student: try: enrollment.reset_student_attempts(course_id, student, module_state_key, delete_module=delete_module) except StudentModule.DoesNotExist: return HttpResponseBadRequest("Module does not exist.") response_payload['student'] = student_identifier elif all_students: instructor_task.api.submit_reset_problem_attempts_for_all_students( request, course_id, module_state_key) response_payload['task'] = 'created' response_payload['student'] = 'All Students' else: return HttpResponseBadRequest() return JsonResponse(response_payload)