Make course ids and usage ids opaque to LMS and Studio [partial commit]
This commit adds the non-courseware lms/djangoapps and lms/lib. These keys are now objects with a limited interface, and the particular internal representation is managed by the data storage layer (the modulestore). For the LMS, there should be no outward-facing changes to the system. The keys are, for now, a change to internal representation only. For Studio, the new serialized form of the keys is used in urls, to allow for further migration in the future. Co-Author: Andy Armstrong <andya@edx.org> Co-Author: Christina Roberts <christina@edx.org> Co-Author: David Baumgold <db@edx.org> Co-Author: Diana Huang <dkh@edx.org> Co-Author: Don Mitchell <dmitchell@edx.org> Co-Author: Julia Hansbrough <julia@edx.org> Co-Author: Nimisha Asthagiri <nasthagiri@edx.org> Co-Author: Sarina Canelake <sarina@edx.org> [LMS-2370]
This commit is contained in:
@@ -68,6 +68,9 @@ from .tools import (
|
||||
strip_if_string,
|
||||
)
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
from xmodule.modulestore.keys import UsageKey
|
||||
from opaque_keys import InvalidKeyError
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -191,9 +194,9 @@ def require_level(level):
|
||||
def decorator(func): # pylint: disable=C0111
|
||||
def wrapped(*args, **kwargs): # pylint: disable=C0111
|
||||
request = args[0]
|
||||
course = get_course_by_id(kwargs['course_id'])
|
||||
course = get_course_by_id(SlashSeparatedCourseKey.from_deprecated_string(kwargs['course_id']))
|
||||
|
||||
if has_access(request.user, course, level):
|
||||
if has_access(request.user, level, course):
|
||||
return func(*args, **kwargs)
|
||||
else:
|
||||
return HttpResponseForbidden()
|
||||
@@ -242,6 +245,7 @@ def students_update_enrollment(request, course_id):
|
||||
]
|
||||
}
|
||||
"""
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
|
||||
action = request.GET.get('action')
|
||||
identifiers_raw = request.GET.get('identifiers')
|
||||
@@ -331,6 +335,7 @@ def bulk_beta_modify_access(request, course_id):
|
||||
anything split_input_list can handle.
|
||||
- action is one of ['add', 'remove']
|
||||
"""
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
action = request.GET.get('action')
|
||||
identifiers_raw = request.GET.get('identifiers')
|
||||
identifiers = _split_input_list(identifiers_raw)
|
||||
@@ -413,8 +418,9 @@ def modify_access(request, course_id):
|
||||
rolename is one of ['instructor', 'staff', 'beta']
|
||||
action is one of ['allow', 'revoke']
|
||||
"""
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
course = get_course_with_access(
|
||||
request.user, course_id, 'instructor', depth=None
|
||||
request.user, 'instructor', course_id, depth=None
|
||||
)
|
||||
try:
|
||||
user = get_student_from_identifier(request.GET.get('unique_student_identifier'))
|
||||
@@ -494,8 +500,9 @@ def list_course_role_members(request, course_id):
|
||||
]
|
||||
}
|
||||
"""
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
course = get_course_with_access(
|
||||
request.user, course_id, 'instructor', depth=None
|
||||
request.user, 'instructor', course_id, depth=None
|
||||
)
|
||||
|
||||
rolename = request.GET.get('rolename')
|
||||
@@ -513,7 +520,7 @@ def list_course_role_members(request, course_id):
|
||||
}
|
||||
|
||||
response_payload = {
|
||||
'course_id': course_id,
|
||||
'course_id': course_id.to_deprecated_string(),
|
||||
rolename: map(extract_user_info, list_with_level(
|
||||
course, rolename
|
||||
)),
|
||||
@@ -528,13 +535,14 @@ def get_grading_config(request, course_id):
|
||||
"""
|
||||
Respond with json which contains a html formatted grade summary.
|
||||
"""
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
course = get_course_with_access(
|
||||
request.user, course_id, 'staff', depth=None
|
||||
request.user, 'staff', course_id, depth=None
|
||||
)
|
||||
grading_config_summary = analytics.basic.dump_grading_context(course)
|
||||
|
||||
response_payload = {
|
||||
'course_id': course_id,
|
||||
'course_id': course_id.to_deprecated_string(),
|
||||
'grading_config_summary': grading_config_summary,
|
||||
}
|
||||
return JsonResponse(response_payload)
|
||||
@@ -552,6 +560,8 @@ def get_students_features(request, course_id, csv=False): # pylint: disable=W06
|
||||
|
||||
TO DO accept requests for different attribute sets.
|
||||
"""
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
|
||||
available_features = analytics.basic.AVAILABLE_FEATURES
|
||||
query_features = [
|
||||
'username', 'name', 'email', 'language', 'location', 'year_of_birth',
|
||||
@@ -578,7 +588,7 @@ def get_students_features(request, course_id, csv=False): # pylint: disable=W06
|
||||
|
||||
if not csv:
|
||||
response_payload = {
|
||||
'course_id': course_id,
|
||||
'course_id': course_id.to_deprecated_string(),
|
||||
'students': student_data,
|
||||
'students_count': len(student_data),
|
||||
'queried_features': query_features,
|
||||
@@ -601,6 +611,7 @@ def get_anon_ids(request, course_id): # pylint: disable=W0613
|
||||
# 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')
|
||||
@@ -620,7 +631,7 @@ def get_anon_ids(request, course_id): # pylint: disable=W0613
|
||||
).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)
|
||||
return csv_response(course_id.to_deprecated_string().replace('/', '-') + '-anon-ids.csv', header, rows)
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@@ -635,6 +646,7 @@ def get_distribution(request, course_id):
|
||||
empty response['feature_results'] object.
|
||||
A list of available will be available in the response['available_features']
|
||||
"""
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
feature = request.GET.get('feature')
|
||||
# alternate notations of None
|
||||
if feature in (None, 'null', ''):
|
||||
@@ -650,7 +662,7 @@ def get_distribution(request, course_id):
|
||||
))
|
||||
|
||||
response_payload = {
|
||||
'course_id': course_id,
|
||||
'course_id': course_id.to_deprecated_string(),
|
||||
'queried_feature': feature,
|
||||
'available_features': available_features,
|
||||
'feature_display_names': analytics.distributions.DISPLAY_NAMES,
|
||||
@@ -689,12 +701,13 @@ def get_student_progress_url(request, course_id):
|
||||
'progress_url': '/../...'
|
||||
}
|
||||
"""
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
user = get_student_from_identifier(request.GET.get('unique_student_identifier'))
|
||||
|
||||
progress_url = reverse('student_progress', kwargs={'course_id': course_id, 'student_id': user.id})
|
||||
progress_url = reverse('student_progress', kwargs={'course_id': course_id.to_deprecated_string(), 'student_id': user.id})
|
||||
|
||||
response_payload = {
|
||||
'course_id': course_id,
|
||||
'course_id': course_id.to_deprecated_string(),
|
||||
'progress_url': progress_url,
|
||||
}
|
||||
return JsonResponse(response_payload)
|
||||
@@ -725,8 +738,9 @@ def reset_student_attempts(request, course_id):
|
||||
requires instructor access
|
||||
mutually exclusive with all_students
|
||||
"""
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
course = get_course_with_access(
|
||||
request.user, course_id, 'staff', depth=None
|
||||
request.user, 'staff', course_id, depth=None
|
||||
)
|
||||
|
||||
problem_to_reset = strip_if_string(request.GET.get('problem_to_reset'))
|
||||
@@ -749,10 +763,13 @@ def reset_student_attempts(request, course_id):
|
||||
|
||||
# instructor authorization
|
||||
if all_students or delete_module:
|
||||
if not has_access(request.user, course, 'instructor'):
|
||||
if not has_access(request.user, 'instructor', course):
|
||||
return HttpResponseForbidden("Requires instructor access.")
|
||||
|
||||
module_state_key = _msk_from_problem_urlname(course_id, problem_to_reset)
|
||||
try:
|
||||
module_state_key = UsageKey.from_string(problem_to_reset)
|
||||
except InvalidKeyError:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
response_payload = {}
|
||||
response_payload['problem_to_reset'] = problem_to_reset
|
||||
@@ -768,7 +785,7 @@ def reset_student_attempts(request, course_id):
|
||||
return HttpResponse(error_msg, status=500)
|
||||
response_payload['student'] = student_identifier
|
||||
elif all_students:
|
||||
instructor_task.api.submit_reset_problem_attempts_for_all_students(request, course_id, module_state_key)
|
||||
instructor_task.api.submit_reset_problem_attempts_for_all_students(request, module_state_key)
|
||||
response_payload['task'] = 'created'
|
||||
response_payload['student'] = 'All Students'
|
||||
else:
|
||||
@@ -794,6 +811,7 @@ def rescore_problem(request, course_id):
|
||||
|
||||
all_students and unique_student_identifier cannot both be present.
|
||||
"""
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
problem_to_reset = strip_if_string(request.GET.get('problem_to_reset'))
|
||||
student_identifier = request.GET.get('unique_student_identifier', None)
|
||||
student = None
|
||||
@@ -810,17 +828,20 @@ def rescore_problem(request, course_id):
|
||||
"Cannot rescore with all_students and unique_student_identifier."
|
||||
)
|
||||
|
||||
module_state_key = _msk_from_problem_urlname(course_id, problem_to_reset)
|
||||
try:
|
||||
module_state_key = UsageKey.from_string(problem_to_reset)
|
||||
except InvalidKeyError:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
response_payload = {}
|
||||
response_payload['problem_to_reset'] = problem_to_reset
|
||||
|
||||
if student:
|
||||
response_payload['student'] = student_identifier
|
||||
instructor_task.api.submit_rescore_problem_for_student(request, course_id, module_state_key, student)
|
||||
instructor_task.api.submit_rescore_problem_for_student(request, 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)
|
||||
instructor_task.api.submit_rescore_problem_for_all_students(request, module_state_key)
|
||||
response_payload['task'] = 'created'
|
||||
else:
|
||||
return HttpResponseBadRequest()
|
||||
@@ -874,6 +895,7 @@ def list_background_email_tasks(request, course_id): # pylint: disable=unused-a
|
||||
"""
|
||||
List background email tasks.
|
||||
"""
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
task_type = 'bulk_course_email'
|
||||
# Specifying for the history of a single task type
|
||||
tasks = instructor_task.api.get_instructor_task_history(course_id, task_type=task_type)
|
||||
@@ -893,22 +915,26 @@ def list_instructor_tasks(request, course_id):
|
||||
|
||||
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
|
||||
- `problem_location_str` lists task history for problem
|
||||
- `problem_location_str` and `unique_student_identifier` lists task
|
||||
history for problem AND student (intersection)
|
||||
"""
|
||||
problem_urlname = strip_if_string(request.GET.get('problem_urlname', False))
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
problem_location_str = strip_if_string(request.GET.get('problem_location_str', 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:
|
||||
if student and not problem_location_str:
|
||||
return HttpResponseBadRequest(
|
||||
"unique_student_identifier must accompany problem_urlname"
|
||||
"unique_student_identifier must accompany problem_location_str"
|
||||
)
|
||||
|
||||
if problem_urlname:
|
||||
module_state_key = _msk_from_problem_urlname(course_id, problem_urlname)
|
||||
if problem_location_str:
|
||||
try:
|
||||
module_state_key = UsageKey.from_string(problem_location_str)
|
||||
except InvalidKeyError:
|
||||
return HttpResponseBadRequest()
|
||||
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)
|
||||
@@ -932,6 +958,7 @@ def list_report_downloads(_request, course_id):
|
||||
"""
|
||||
List grade CSV files that are available for download for this course.
|
||||
"""
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
report_store = ReportStore.from_config()
|
||||
|
||||
response_payload = {
|
||||
@@ -950,8 +977,9 @@ def calculate_grades_csv(request, course_id):
|
||||
"""
|
||||
AlreadyRunningError is raised if the course's grades are already being updated.
|
||||
"""
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
try:
|
||||
instructor_task.api.submit_calculate_grades_csv(request, course_id)
|
||||
instructor_task.api.submit_calculate_grades_csv(request, course_key)
|
||||
success_status = _("Your grade report is being generated! You can view the status of the generation task in the 'Pending Instructor Tasks' section.")
|
||||
return JsonResponse({"status": success_status})
|
||||
except AlreadyRunningError:
|
||||
@@ -976,8 +1004,9 @@ def list_forum_members(request, course_id):
|
||||
|
||||
Takes query parameter `rolename`.
|
||||
"""
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
course = get_course_by_id(course_id)
|
||||
has_instructor_access = has_access(request.user, course, 'instructor')
|
||||
has_instructor_access = has_access(request.user, 'instructor', course)
|
||||
has_forum_admin = has_forum_access(
|
||||
request.user, course_id, FORUM_ROLE_ADMINISTRATOR
|
||||
)
|
||||
@@ -1016,7 +1045,7 @@ def list_forum_members(request, course_id):
|
||||
}
|
||||
|
||||
response_payload = {
|
||||
'course_id': course_id,
|
||||
'course_id': course_id.to_deprecated_string(),
|
||||
rolename: map(extract_user_info, users),
|
||||
}
|
||||
return JsonResponse(response_payload)
|
||||
@@ -1036,6 +1065,7 @@ def send_email(request, course_id):
|
||||
- 'subject' specifies email's subject
|
||||
- 'message' specifies email's content
|
||||
"""
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
send_to = request.POST.get("send_to")
|
||||
subject = request.POST.get("subject")
|
||||
message = request.POST.get("message")
|
||||
@@ -1047,8 +1077,7 @@ def send_email(request, course_id):
|
||||
|
||||
# Submit the task, so that the correct InstructorTask object gets created (for monitoring purposes)
|
||||
instructor_task.api.submit_bulk_course_email(request, course_id, email.id) # pylint: disable=E1101
|
||||
|
||||
response_payload = {'course_id': course_id}
|
||||
response_payload = {'course_id': course_id.to_deprecated_string()}
|
||||
return JsonResponse(response_payload)
|
||||
|
||||
|
||||
@@ -1075,8 +1104,9 @@ def update_forum_role_membership(request, course_id):
|
||||
- `rolename` is one of [FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA]
|
||||
- `action` is one of ['allow', 'revoke']
|
||||
"""
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
course = get_course_by_id(course_id)
|
||||
has_instructor_access = has_access(request.user, course, 'instructor')
|
||||
has_instructor_access = has_access(request.user, 'instructor', course)
|
||||
has_forum_admin = has_forum_access(
|
||||
request.user, course_id, FORUM_ROLE_ADMINISTRATOR
|
||||
)
|
||||
@@ -1101,7 +1131,7 @@ def update_forum_role_membership(request, course_id):
|
||||
))
|
||||
|
||||
user = get_student_from_identifier(unique_student_identifier)
|
||||
target_is_instructor = has_access(user, course, 'instructor')
|
||||
target_is_instructor = has_access(user, 'instructor', course)
|
||||
# cannot revoke instructor
|
||||
if target_is_instructor and action == 'revoke' and rolename == FORUM_ROLE_ADMINISTRATOR:
|
||||
return HttpResponseBadRequest("Cannot revoke instructor forum admin privileges.")
|
||||
@@ -1112,7 +1142,7 @@ def update_forum_role_membership(request, course_id):
|
||||
return HttpResponseBadRequest("Role does not exist.")
|
||||
|
||||
response_payload = {
|
||||
'course_id': course_id,
|
||||
'course_id': course_id.to_deprecated_string(),
|
||||
'action': action,
|
||||
}
|
||||
return JsonResponse(response_payload)
|
||||
@@ -1131,6 +1161,7 @@ def proxy_legacy_analytics(request, course_id):
|
||||
|
||||
`aname` is a query parameter specifying which analytic to query.
|
||||
"""
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
analytics_name = request.GET.get('aname')
|
||||
|
||||
# abort if misconfigured
|
||||
@@ -1140,7 +1171,7 @@ def proxy_legacy_analytics(request, course_id):
|
||||
url = "{}get?aname={}&course_id={}&apikey={}".format(
|
||||
settings.ANALYTICS_SERVER_URL,
|
||||
analytics_name,
|
||||
course_id,
|
||||
course_id.to_deprecated_string(),
|
||||
settings.ANALYTICS_API_KEY,
|
||||
)
|
||||
|
||||
@@ -1175,9 +1206,9 @@ def _display_unit(unit):
|
||||
"""
|
||||
name = getattr(unit, 'display_name', None)
|
||||
if name:
|
||||
return u'{0} ({1})'.format(name, unit.location.url())
|
||||
return u'{0} ({1})'.format(name, unit.location.to_deprecated_string())
|
||||
else:
|
||||
return unit.location.url()
|
||||
return unit.location.to_deprecated_string()
|
||||
|
||||
|
||||
@handle_dashboard_error
|
||||
@@ -1189,7 +1220,7 @@ def change_due_date(request, course_id):
|
||||
"""
|
||||
Grants a due date extension to a student for a particular unit.
|
||||
"""
|
||||
course = get_course_by_id(course_id)
|
||||
course = get_course_by_id(SlashSeparatedCourseKey.from_deprecated_string(course_id))
|
||||
student = get_student_from_identifier(request.GET.get('student'))
|
||||
unit = find_unit(course, request.GET.get('url'))
|
||||
due_date = parse_datetime(request.GET.get('due_datetime'))
|
||||
@@ -1210,7 +1241,7 @@ def reset_due_date(request, course_id):
|
||||
"""
|
||||
Rescinds a due date extension for a student on a particular unit.
|
||||
"""
|
||||
course = get_course_by_id(course_id)
|
||||
course = get_course_by_id(SlashSeparatedCourseKey.from_deprecated_string(course_id))
|
||||
student = get_student_from_identifier(request.GET.get('student'))
|
||||
unit = find_unit(course, request.GET.get('url'))
|
||||
set_due_date_extension(course, unit, student, None)
|
||||
@@ -1230,7 +1261,7 @@ def show_unit_extensions(request, course_id):
|
||||
"""
|
||||
Shows all of the students which have due date extensions for the given unit.
|
||||
"""
|
||||
course = get_course_by_id(course_id)
|
||||
course = get_course_by_id(SlashSeparatedCourseKey.from_deprecated_string(course_id))
|
||||
unit = find_unit(course, request.GET.get('url'))
|
||||
return JsonResponse(dump_module_extensions(course, unit))
|
||||
|
||||
@@ -1246,7 +1277,7 @@ def show_student_extensions(request, course_id):
|
||||
particular course.
|
||||
"""
|
||||
student = get_student_from_identifier(request.GET.get('student'))
|
||||
course = get_course_by_id(course_id)
|
||||
course = get_course_by_id(SlashSeparatedCourseKey.from_deprecated_string(course_id))
|
||||
return JsonResponse(dump_student_extensions(course, student))
|
||||
|
||||
|
||||
@@ -1267,23 +1298,3 @@ def _split_input_list(str_list):
|
||||
new_list = [s for s in new_list if s != '']
|
||||
|
||||
return new_list
|
||||
|
||||
|
||||
def _msk_from_problem_urlname(course_id, urlname):
|
||||
"""
|
||||
Convert a 'problem urlname' (name that instructor's input into dashboard)
|
||||
to a module state key (db field)
|
||||
"""
|
||||
if urlname.endswith(".xml"):
|
||||
urlname = urlname[:-4]
|
||||
|
||||
# Combined open ended problems also have state that can be deleted. However,
|
||||
# prepending "problem" will only allow capa problems to be reset.
|
||||
# Get around this for xblock problems.
|
||||
if "/" not in urlname:
|
||||
urlname = "problem/" + urlname
|
||||
|
||||
parts = Location.parse_course_id(course_id)
|
||||
parts['urlname'] = urlname
|
||||
module_state_key = u"i4x://{org}/{course}/{urlname}".format(**parts)
|
||||
return module_state_key
|
||||
|
||||
Reference in New Issue
Block a user