diff --git a/common/djangoapps/contentserver/middleware.py b/common/djangoapps/contentserver/middleware.py index b9c14cd537aa44b4cfe75e6eb6be6f6bed35b88a..056a1ce45983b46b142b5903c9c1aade11c16c2d 100644 --- a/common/djangoapps/contentserver/middleware.py +++ b/common/djangoapps/contentserver/middleware.py @@ -4,10 +4,12 @@ from student.models import CourseEnrollment from xmodule.contentstore.django import contentstore from xmodule.contentstore.content import StaticContent, XASSET_LOCATION_TAG -from xmodule.modulestore import InvalidLocationError +from xmodule.modulestore import InvalidLocationError, InvalidKeyError from cache_toolbox.core import get_cached_content, set_cached_content from xmodule.exceptions import NotFoundError +# TODO: Soon as we have a reasonable way to serialize/deserialize AssetKeys, we need +# to change this file so instead of using course_id_partial, we're just using asset keys class StaticContentServer(object): def process_request(self, request): @@ -15,7 +17,7 @@ class StaticContentServer(object): if request.path.startswith('/' + XASSET_LOCATION_TAG + '/'): try: loc = StaticContent.get_location_from_path(request.path) - except InvalidLocationError: + except (InvalidLocationError, InvalidKeyError): # return a 'Bad Request' to browser as we have a malformed Location response = HttpResponse() response.status_code = 400 @@ -47,9 +49,9 @@ class StaticContentServer(object): if getattr(content, "locked", False): if not hasattr(request, "user") or not request.user.is_authenticated(): return HttpResponseForbidden('Unauthorized') - course_partial_id = "/".join([loc.org, loc.course]) if not request.user.is_staff and not CourseEnrollment.is_enrolled_by_partial( - request.user, course_partial_id): + request.user, loc.course_key + ): return HttpResponseForbidden('Unauthorized') # convert over the DB persistent last modified timestamp to a HTTP compatible diff --git a/common/djangoapps/contentserver/tests/test.py b/common/djangoapps/contentserver/tests/test.py index 334b10ca00c0eeaee249c2fd571d13060ca572c0..21666b6f2330b1f2f5d9c67d2ede3c04b2cbbdaf 100644 --- a/common/djangoapps/contentserver/tests/test.py +++ b/common/djangoapps/contentserver/tests/test.py @@ -15,9 +15,9 @@ from django.test.utils import override_settings from student.models import CourseEnrollment from xmodule.contentstore.django import contentstore, _CONTENTSTORE -from xmodule.modulestore import Location from xmodule.contentstore.content import StaticContent from xmodule.modulestore.django import modulestore +from xmodule.modulestore.locations import SlashSeparatedCourseKey from xmodule.modulestore.tests.django_utils import (studio_store_config, ModuleStoreTestCase) from xmodule.modulestore.xml_importer import import_from_xml @@ -47,18 +47,20 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): self.client = Client() self.contentstore = contentstore() - # A locked asset - self.loc_locked = Location('c4x', 'edX', 'toy', 'asset', 'sample_static.txt') - self.url_locked = StaticContent.get_url_path_from_location(self.loc_locked) - - # An unlocked asset - self.loc_unlocked = Location('c4x', 'edX', 'toy', 'asset', 'another_static.txt') - self.url_unlocked = StaticContent.get_url_path_from_location(self.loc_unlocked) + self.course_key = SlashSeparatedCourseKey('edX', 'toy', '2012_Fall') import_from_xml(modulestore('direct'), 'common/test/data/', ['toy'], static_content_store=self.contentstore, verbose=True) - self.contentstore.set_attr(self.loc_locked, 'locked', True) + # A locked asset + self.locked_asset = self.course_key.make_asset_key('asset', 'sample_static.txt') + self.url_locked = self.locked_asset.to_deprecated_string() + + # An unlocked asset + self.unlocked_asset = self.course_key.make_asset_key('asset', 'another_static.txt') + self.url_unlocked = self.unlocked_asset.to_deprecated_string() + + self.contentstore.set_attr(self.locked_asset, 'locked', True) # Create user self.usr = 'testuser' @@ -114,10 +116,8 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): Test that locked assets behave appropriately in case user is logged in and registered for the course. """ - # pylint: disable=E1101 - course_id = "/".join([self.loc_locked.org, self.loc_locked.course, '2012_Fall']) - CourseEnrollment.enroll(self.user, course_id) - self.assertTrue(CourseEnrollment.is_enrolled(self.user, course_id)) + CourseEnrollment.enroll(self.user, self.course_key) + self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course_key)) self.client.login(username=self.usr, password=self.pwd) resp = self.client.get(self.url_locked) @@ -127,9 +127,6 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): """ Test that locked assets behave appropriately in case user is staff. """ - # pylint: disable=E1101 - course_id = "/".join([self.loc_locked.org, self.loc_locked.course, '2012_Fall']) - self.client.login(username=self.staff_usr, password=self.staff_pwd) resp = self.client.get(self.url_locked) self.assertEqual(resp.status_code, 200) # pylint: disable=E1103 diff --git a/common/djangoapps/course_groups/cohorts.py b/common/djangoapps/course_groups/cohorts.py index 7e6925879ff6ae465ad16881e7440e4201b5e9e9..380bc30c5f3860934f64ce8a8d525a412f31b299 100644 --- a/common/djangoapps/course_groups/cohorts.py +++ b/common/djangoapps/course_groups/cohorts.py @@ -32,30 +32,30 @@ def local_random(): return _local_random -def is_course_cohorted(course_id): +def is_course_cohorted(course_key): """ - Given a course id, return a boolean for whether or not the course is + Given a course key, return a boolean for whether or not the course is cohorted. Raises: Http404 if the course doesn't exist. """ - return courses.get_course_by_id(course_id).is_cohorted + return courses.get_course_by_id(course_key).is_cohorted -def get_cohort_id(user, course_id): +def get_cohort_id(user, course_key): """ - Given a course id and a user, return the id of the cohort that user is + Given a course key and a user, return the id of the cohort that user is assigned to in that course. If they don't have a cohort, return None. """ - cohort = get_cohort(user, course_id) + cohort = get_cohort(user, course_key) return None if cohort is None else cohort.id -def is_commentable_cohorted(course_id, commentable_id): +def is_commentable_cohorted(course_key, commentable_id): """ Args: - course_id: string + course_key: CourseKey commentable_id: string Returns: @@ -64,7 +64,7 @@ def is_commentable_cohorted(course_id, commentable_id): Raises: Http404 if the course doesn't exist. """ - course = courses.get_course_by_id(course_id) + course = courses.get_course_by_id(course_key) if not course.is_cohorted: # this is the easy case :) @@ -77,18 +77,18 @@ def is_commentable_cohorted(course_id, commentable_id): # inline discussions are cohorted by default ans = True - log.debug(u"is_commentable_cohorted({0}, {1}) = {2}".format(course_id, - commentable_id, - ans)) + log.debug(u"is_commentable_cohorted({0}, {1}) = {2}".format( + course_key, commentable_id, ans + )) return ans -def get_cohorted_commentables(course_id): +def get_cohorted_commentables(course_key): """ - Given a course_id return a list of strings representing cohorted commentables + Given a course_key return a list of strings representing cohorted commentables """ - course = courses.get_course_by_id(course_id) + course = courses.get_course_by_id(course_key) if not course.is_cohorted: # this is the easy case :) @@ -99,34 +99,34 @@ def get_cohorted_commentables(course_id): return ans -def get_cohort(user, course_id): +def get_cohort(user, course_key): """ - Given a django User and a course_id, return the user's cohort in that + Given a django User and a CourseKey, return the user's cohort in that cohort. Arguments: user: a Django User object. - course_id: string in the format 'org/course/run' + course_key: CourseKey Returns: A CourseUserGroup object if the course is cohorted and the User has a cohort, else None. Raises: - ValueError if the course_id doesn't exist. + ValueError if the CourseKey doesn't exist. """ # First check whether the course is cohorted (users shouldn't be in a cohort # in non-cohorted courses, but settings can change after course starts) try: - course = courses.get_course_by_id(course_id) + course = courses.get_course_by_id(course_key) except Http404: - raise ValueError("Invalid course_id") + raise ValueError("Invalid course_key") if not course.is_cohorted: return None try: - return CourseUserGroup.objects.get(course_id=course_id, + return CourseUserGroup.objects.get(course_id=course_key, group_type=CourseUserGroup.COHORT, users__id=user.id) except CourseUserGroup.DoesNotExist: @@ -142,72 +142,81 @@ def get_cohort(user, course_id): # Nowhere to put user log.warning("Course %s is auto-cohorted, but there are no" " auto_cohort_groups specified", - course_id) + course_key) return None # Put user in a random group, creating it if needed group_name = local_random().choice(choices) group, created = CourseUserGroup.objects.get_or_create( - course_id=course_id, + course_id=course_key, group_type=CourseUserGroup.COHORT, - name=group_name) + name=group_name + ) user.course_groups.add(group) return group -def get_course_cohorts(course_id): +def get_course_cohorts(course_key): """ Get a list of all the cohorts in the given course. Arguments: - course_id: string in the format 'org/course/run' + course_key: CourseKey Returns: A list of CourseUserGroup objects. Empty if there are no cohorts. Does not check whether the course is cohorted. """ - return list(CourseUserGroup.objects.filter(course_id=course_id, - group_type=CourseUserGroup.COHORT)) + return list(CourseUserGroup.objects.filter( + course_id=course_key, + group_type=CourseUserGroup.COHORT + )) ### Helpers for cohort management views -def get_cohort_by_name(course_id, name): +def get_cohort_by_name(course_key, name): """ Return the CourseUserGroup object for the given cohort. Raises DoesNotExist it isn't present. """ - return CourseUserGroup.objects.get(course_id=course_id, - group_type=CourseUserGroup.COHORT, - name=name) + return CourseUserGroup.objects.get( + course_id=course_key, + group_type=CourseUserGroup.COHORT, + name=name + ) -def get_cohort_by_id(course_id, cohort_id): +def get_cohort_by_id(course_key, cohort_id): """ Return the CourseUserGroup object for the given cohort. Raises DoesNotExist - it isn't present. Uses the course_id for extra validation... + it isn't present. Uses the course_key for extra validation... """ - return CourseUserGroup.objects.get(course_id=course_id, - group_type=CourseUserGroup.COHORT, - id=cohort_id) + return CourseUserGroup.objects.get( + course_id=course_key, + group_type=CourseUserGroup.COHORT, + id=cohort_id + ) -def add_cohort(course_id, name): +def add_cohort(course_key, name): """ Add a cohort to a course. Raises ValueError if a cohort of the same name already exists. """ - log.debug("Adding cohort %s to %s", name, course_id) - if CourseUserGroup.objects.filter(course_id=course_id, + log.debug("Adding cohort %s to %s", name, course_key) + if CourseUserGroup.objects.filter(course_id=course_key, group_type=CourseUserGroup.COHORT, name=name).exists(): raise ValueError("Can't create two cohorts with the same name") - return CourseUserGroup.objects.create(course_id=course_id, - group_type=CourseUserGroup.COHORT, - name=name) + return CourseUserGroup.objects.create( + course_id=course_key, + group_type=CourseUserGroup.COHORT, + name=name + ) class CohortConflict(Exception): @@ -237,9 +246,10 @@ def add_user_to_cohort(cohort, username_or_email): previous_cohort = None course_cohorts = CourseUserGroup.objects.filter( - course_id=cohort.course_id, + course_id=cohort.course_key, users__id=user.id, - group_type=CourseUserGroup.COHORT) + group_type=CourseUserGroup.COHORT + ) if course_cohorts.exists(): if course_cohorts[0] == cohort: raise ValueError("User {0} already present in cohort {1}".format( @@ -253,21 +263,21 @@ def add_user_to_cohort(cohort, username_or_email): return (user, previous_cohort) -def get_course_cohort_names(course_id): +def get_course_cohort_names(course_key): """ Return a list of the cohort names in a course. """ - return [c.name for c in get_course_cohorts(course_id)] + return [c.name for c in get_course_cohorts(course_key)] -def delete_empty_cohort(course_id, name): +def delete_empty_cohort(course_key, name): """ Remove an empty cohort. Raise ValueError if cohort is not empty. """ - cohort = get_cohort_by_name(course_id, name) + cohort = get_cohort_by_name(course_key, name) if cohort.users.exists(): raise ValueError( "Can't delete non-empty cohort {0} in course {1}".format( - name, course_id)) + name, course_key)) cohort.delete() diff --git a/common/djangoapps/course_groups/models.py b/common/djangoapps/course_groups/models.py index 8bab17493be0e8150e7a962b9c87abfeab80f719..52c22b6e9ac3be6bd2a44862b7e4d8eb4b0a9ff9 100644 --- a/common/djangoapps/course_groups/models.py +++ b/common/djangoapps/course_groups/models.py @@ -2,6 +2,7 @@ import logging from django.contrib.auth.models import User from django.db import models +from xmodule_django.models import CourseKeyField log = logging.getLogger(__name__) @@ -23,7 +24,8 @@ class CourseUserGroup(models.Model): # Note: groups associated with particular runs of a course. E.g. Fall 2012 and Spring # 2013 versions of 6.00x will have separate groups. - course_id = models.CharField(max_length=255, db_index=True, + # TODO change field name to course_key + course_id = CourseKeyField(max_length=255, db_index=True, help_text="Which course is this group associated with?") # For now, only have group type 'cohort', but adding a type field to support diff --git a/common/djangoapps/course_groups/tests/test_cohorts.py b/common/djangoapps/course_groups/tests/test_cohorts.py index a17df56a714dcf920403e6c0dadb1d36ef0fa147..4901bdc94a43e93af9e55578271241a459b02963 100644 --- a/common/djangoapps/course_groups/tests/test_cohorts.py +++ b/common/djangoapps/course_groups/tests/test_cohorts.py @@ -9,6 +9,7 @@ from course_groups.cohorts import (get_cohort, get_course_cohorts, is_commentable_cohorted, get_cohort_by_name) from xmodule.modulestore.django import modulestore, clear_existing_modulestores +from xmodule.modulestore.locations import SlashSeparatedCourseKey from xmodule.modulestore.tests.django_utils import mixed_store_config @@ -84,13 +85,14 @@ class TestCohorts(django.test.TestCase): Make sure that course is reloaded every time--clear out the modulestore. """ clear_existing_modulestores() + self.toy_course_key = SlashSeparatedCourseKey("edX", "toy", "2012_Fall") def test_get_cohort(self): """ Make sure get_cohort() does the right thing when the course is cohorted """ - course = modulestore().get_course("edX/toy/2012_Fall") - self.assertEqual(course.id, "edX/toy/2012_Fall") + course = modulestore().get_course(self.toy_course_key) + self.assertEqual(course.id, self.toy_course_key) self.assertFalse(course.is_cohorted) user = User.objects.create(username="test", email="a@b.com") @@ -120,8 +122,7 @@ class TestCohorts(django.test.TestCase): """ Make sure get_cohort() does the right thing when the course is auto_cohorted """ - course = modulestore().get_course("edX/toy/2012_Fall") - self.assertEqual(course.id, "edX/toy/2012_Fall") + course = modulestore().get_course(self.toy_course_key) self.assertFalse(course.is_cohorted) user1 = User.objects.create(username="test", email="a@b.com") @@ -168,8 +169,7 @@ class TestCohorts(django.test.TestCase): """ Make sure get_cohort() randomizes properly. """ - course = modulestore().get_course("edX/toy/2012_Fall") - self.assertEqual(course.id, "edX/toy/2012_Fall") + course = modulestore().get_course(self.toy_course_key) self.assertFalse(course.is_cohorted) groups = ["group_{0}".format(n) for n in range(5)] @@ -194,26 +194,26 @@ class TestCohorts(django.test.TestCase): self.assertLess(num_users, 50) def test_get_course_cohorts(self): - course1_id = 'a/b/c' - course2_id = 'e/f/g' + course1_key = SlashSeparatedCourseKey('a', 'b', 'c') + course2_key = SlashSeparatedCourseKey('e', 'f', 'g') # add some cohorts to course 1 cohort = CourseUserGroup.objects.create(name="TestCohort", - course_id=course1_id, + course_id=course1_key, group_type=CourseUserGroup.COHORT) cohort = CourseUserGroup.objects.create(name="TestCohort2", - course_id=course1_id, + course_id=course1_key, group_type=CourseUserGroup.COHORT) # second course should have no cohorts - self.assertEqual(get_course_cohorts(course2_id), []) + self.assertEqual(get_course_cohorts(course2_key), []) - cohorts = sorted([c.name for c in get_course_cohorts(course1_id)]) + cohorts = sorted([c.name for c in get_course_cohorts(course1_key)]) self.assertEqual(cohorts, ['TestCohort', 'TestCohort2']) def test_is_commentable_cohorted(self): - course = modulestore().get_course("edX/toy/2012_Fall") + course = modulestore().get_course(self.toy_course_key) self.assertFalse(course.is_cohorted) def to_id(name): diff --git a/common/djangoapps/course_groups/views.py b/common/djangoapps/course_groups/views.py index 9dc9cf523c0918ce592cf52c3c8a294a26c0b3f7..a9a058a539f82a9a3a70833c91f989c886d44035 100644 --- a/common/djangoapps/course_groups/views.py +++ b/common/djangoapps/course_groups/views.py @@ -33,25 +33,25 @@ def split_by_comma_and_whitespace(s): @ensure_csrf_cookie -def list_cohorts(request, course_id): +def list_cohorts(request, course_key): """ Return json dump of dict: {'success': True, 'cohorts': [{'name': name, 'id': id}, ...]} """ - get_course_with_access(request.user, course_id, 'staff') + get_course_with_access(request.user, 'staff', course_key) all_cohorts = [{'name': c.name, 'id': c.id} - for c in cohorts.get_course_cohorts(course_id)] + for c in cohorts.get_course_cohorts(course_key)] return json_http_response({'success': True, - 'cohorts': all_cohorts}) + 'cohorts': all_cohorts}) @ensure_csrf_cookie @require_POST -def add_cohort(request, course_id): +def add_cohort(request, course_key): """ Return json of dict: {'success': True, @@ -63,7 +63,7 @@ def add_cohort(request, course_id): {'success': False, 'msg': error_msg} if there's an error """ - get_course_with_access(request.user, course_id, 'staff') + get_course_with_access(request.user, 'staff', course_key) name = request.POST.get("name") if not name: @@ -71,7 +71,7 @@ def add_cohort(request, course_id): 'msg': "No name specified"}) try: - cohort = cohorts.add_cohort(course_id, name) + cohort = cohorts.add_cohort(course_key, name) except ValueError as err: return json_http_response({'success': False, 'msg': str(err)}) @@ -84,7 +84,7 @@ def add_cohort(request, course_id): @ensure_csrf_cookie -def users_in_cohort(request, course_id, cohort_id): +def users_in_cohort(request, course_key, cohort_id): """ Return users in the cohort. Show up to 100 per page, and page using the 'page' GET attribute in the call. Format: @@ -97,11 +97,11 @@ def users_in_cohort(request, course_id, cohort_id): 'users': [{'username': ..., 'email': ..., 'name': ...}] } """ - get_course_with_access(request.user, course_id, 'staff') + get_course_with_access(request.user, 'staff', course_key) # this will error if called with a non-int cohort_id. That's ok--it # shoudn't happen for valid clients. - cohort = cohorts.get_cohort_by_id(course_id, int(cohort_id)) + cohort = cohorts.get_cohort_by_id(course_key, int(cohort_id)) paginator = Paginator(cohort.users.all(), 100) page = request.GET.get('page') @@ -119,17 +119,17 @@ def users_in_cohort(request, course_id, cohort_id): user_info = [{'username': u.username, 'email': u.email, 'name': '{0} {1}'.format(u.first_name, u.last_name)} - for u in users] + for u in users] return json_http_response({'success': True, - 'page': page, - 'num_pages': paginator.num_pages, - 'users': user_info}) + 'page': page, + 'num_pages': paginator.num_pages, + 'users': user_info}) @ensure_csrf_cookie @require_POST -def add_users_to_cohort(request, course_id, cohort_id): +def add_users_to_cohort(request, course_key, cohort_id): """ Return json dict of: @@ -144,9 +144,9 @@ def add_users_to_cohort(request, course_id, cohort_id): 'present': [str1, str2, ...], # already there 'unknown': [str1, str2, ...]} """ - get_course_with_access(request.user, course_id, 'staff') + get_course_with_access(request.user, 'staff', course_key) - cohort = cohorts.get_cohort_by_id(course_id, cohort_id) + cohort = cohorts.get_cohort_by_id(course_key, cohort_id) users = request.POST.get('users', '') added = [] @@ -175,15 +175,15 @@ def add_users_to_cohort(request, course_id, cohort_id): unknown.append(username_or_email) return json_http_response({'success': True, - 'added': added, - 'changed': changed, - 'present': present, - 'unknown': unknown}) + 'added': added, + 'changed': changed, + 'present': present, + 'unknown': unknown}) @ensure_csrf_cookie @require_POST -def remove_user_from_cohort(request, course_id, cohort_id): +def remove_user_from_cohort(request, course_key, cohort_id): """ Expects 'username': username in POST data. @@ -193,14 +193,14 @@ def remove_user_from_cohort(request, course_id, cohort_id): {'success': False, 'msg': error_msg} """ - get_course_with_access(request.user, course_id, 'staff') + get_course_with_access(request.user, 'staff', course_key) username = request.POST.get('username') if username is None: return json_http_response({'success': False, - 'msg': 'No username specified'}) + 'msg': 'No username specified'}) - cohort = cohorts.get_cohort_by_id(course_id, cohort_id) + cohort = cohorts.get_cohort_by_id(course_key, cohort_id) try: user = User.objects.get(username=username) cohort.users.remove(user) @@ -208,16 +208,18 @@ def remove_user_from_cohort(request, course_id, cohort_id): except User.DoesNotExist: log.debug('no user') return json_http_response({'success': False, - 'msg': "No user '{0}'".format(username)}) + 'msg': "No user '{0}'".format(username)}) -def debug_cohort_mgmt(request, course_id): +def debug_cohort_mgmt(request, course_key): """ Debugging view for dev. """ # add staff check to make sure it's safe if it's accidentally deployed. - get_course_with_access(request.user, course_id, 'staff') + get_course_with_access(request.user, 'staff', course_key) - context = {'cohorts_ajax_url': reverse('cohorts', - kwargs={'course_id': course_id})} + context = {'cohorts_ajax_url': reverse( + 'cohorts', + kwargs={'course_id': course_key.to_deprecated_string()} + )} return render_to_response('/course_groups/debug.html', context) diff --git a/common/djangoapps/course_modes/models.py b/common/djangoapps/course_modes/models.py index ae9fac5ed429985297a2eab4581368a79f108979..2657f877f63b291a32a3b5082971821a207bd14c 100644 --- a/common/djangoapps/course_modes/models.py +++ b/common/djangoapps/course_modes/models.py @@ -9,6 +9,8 @@ from collections import namedtuple from django.utils.translation import ugettext as _ from django.db.models import Q +from xmodule_django.models import CourseKeyField + Mode = namedtuple('Mode', ['slug', 'name', 'min_price', 'suggested_prices', 'currency', 'expiration_datetime']) class CourseMode(models.Model): @@ -17,7 +19,7 @@ class CourseMode(models.Model): """ # the course that this mode is attached to - course_id = models.CharField(max_length=255, db_index=True) + course_id = CourseKeyField(max_length=255, db_index=True) # the reference to this mode that can be used by Enrollments to generate # similar behavior for the same slug across courses diff --git a/common/djangoapps/course_modes/tests/test_models.py b/common/djangoapps/course_modes/tests/test_models.py index 5336b3e5fab74a5ad66b893b112017049ed94b40..c29a9e7ec3d368c84ae59d4d56e1e51916992842 100644 --- a/common/djangoapps/course_modes/tests/test_models.py +++ b/common/djangoapps/course_modes/tests/test_models.py @@ -8,6 +8,7 @@ Replace this with more appropriate tests for your application. from datetime import datetime, timedelta import pytz +from xmodule.modulestore.locations import SlashSeparatedCourseKey from django.test import TestCase from course_modes.models import CourseMode, Mode @@ -18,7 +19,7 @@ class CourseModeModelTest(TestCase): """ def setUp(self): - self.course_id = 'TestCourse' + self.course_key = SlashSeparatedCourseKey('Test', 'TestCourse', 'TestCourseRun') CourseMode.objects.all().delete() def create_mode(self, mode_slug, mode_name, min_price=0, suggested_prices='', currency='usd'): @@ -26,7 +27,7 @@ class CourseModeModelTest(TestCase): Create a new course mode """ return CourseMode.objects.get_or_create( - course_id=self.course_id, + course_id=self.course_key, mode_display_name=mode_name, mode_slug=mode_slug, min_price=min_price, @@ -39,7 +40,7 @@ class CourseModeModelTest(TestCase): If we can't find any modes, we should get back the default mode """ # shouldn't be able to find a corresponding course - modes = CourseMode.modes_for_course(self.course_id) + modes = CourseMode.modes_for_course(self.course_key) self.assertEqual([CourseMode.DEFAULT_MODE], modes) def test_nodes_for_course_single(self): @@ -48,13 +49,13 @@ class CourseModeModelTest(TestCase): """ self.create_mode('verified', 'Verified Certificate') - modes = CourseMode.modes_for_course(self.course_id) + modes = CourseMode.modes_for_course(self.course_key) mode = Mode(u'verified', u'Verified Certificate', 0, '', 'usd', None) self.assertEqual([mode], modes) - modes_dict = CourseMode.modes_for_course_dict(self.course_id) + modes_dict = CourseMode.modes_for_course_dict(self.course_key) self.assertEqual(modes_dict['verified'], mode) - self.assertEqual(CourseMode.mode_for_course(self.course_id, 'verified'), + self.assertEqual(CourseMode.mode_for_course(self.course_key, 'verified'), mode) def test_modes_for_course_multiple(self): @@ -67,18 +68,18 @@ class CourseModeModelTest(TestCase): for mode in set_modes: self.create_mode(mode.slug, mode.name, mode.min_price, mode.suggested_prices) - modes = CourseMode.modes_for_course(self.course_id) + modes = CourseMode.modes_for_course(self.course_key) self.assertEqual(modes, set_modes) - self.assertEqual(mode1, CourseMode.mode_for_course(self.course_id, u'honor')) - self.assertEqual(mode2, CourseMode.mode_for_course(self.course_id, u'verified')) - self.assertIsNone(CourseMode.mode_for_course(self.course_id, 'DNE')) + self.assertEqual(mode1, CourseMode.mode_for_course(self.course_key, u'honor')) + self.assertEqual(mode2, CourseMode.mode_for_course(self.course_key, u'verified')) + self.assertIsNone(CourseMode.mode_for_course(self.course_key, 'DNE')) def test_min_course_price_for_currency(self): """ Get the min course price for a course according to currency """ # no modes, should get 0 - self.assertEqual(0, CourseMode.min_course_price_for_currency(self.course_id, 'usd')) + self.assertEqual(0, CourseMode.min_course_price_for_currency(self.course_key, 'usd')) # create some modes mode1 = Mode(u'honor', u'Honor Code Certificate', 10, '', 'usd', None) @@ -88,27 +89,27 @@ class CourseModeModelTest(TestCase): for mode in set_modes: self.create_mode(mode.slug, mode.name, mode.min_price, mode.suggested_prices, mode.currency) - self.assertEqual(10, CourseMode.min_course_price_for_currency(self.course_id, 'usd')) - self.assertEqual(80, CourseMode.min_course_price_for_currency(self.course_id, 'cny')) + self.assertEqual(10, CourseMode.min_course_price_for_currency(self.course_key, 'usd')) + self.assertEqual(80, CourseMode.min_course_price_for_currency(self.course_key, 'cny')) def test_modes_for_course_expired(self): expired_mode, _status = self.create_mode('verified', 'Verified Certificate') expired_mode.expiration_datetime = datetime.now(pytz.UTC) + timedelta(days=-1) expired_mode.save() - modes = CourseMode.modes_for_course(self.course_id) + modes = CourseMode.modes_for_course(self.course_key) self.assertEqual([CourseMode.DEFAULT_MODE], modes) mode1 = Mode(u'honor', u'Honor Code Certificate', 0, '', 'usd', None) self.create_mode(mode1.slug, mode1.name, mode1.min_price, mode1.suggested_prices) - modes = CourseMode.modes_for_course(self.course_id) + modes = CourseMode.modes_for_course(self.course_key) self.assertEqual([mode1], modes) expiration_datetime = datetime.now(pytz.UTC) + timedelta(days=1) expired_mode.expiration_datetime = expiration_datetime expired_mode.save() expired_mode_value = Mode(u'verified', u'Verified Certificate', 0, '', 'usd', expiration_datetime) - modes = CourseMode.modes_for_course(self.course_id) + modes = CourseMode.modes_for_course(self.course_key) self.assertEqual([expired_mode_value, mode1], modes) - modes = CourseMode.modes_for_course('second_test_course') + modes = CourseMode.modes_for_course(SlashSeparatedCourseKey('TestOrg', 'TestCourse', 'TestRun')) self.assertEqual([CourseMode.DEFAULT_MODE], modes) diff --git a/common/djangoapps/course_modes/views.py b/common/djangoapps/course_modes/views.py index 8ca43a17bb514556ee6190d49ef3d79cd32b9409..14bbac303699fd12432c2cd8f0c6d64ef8b7601f 100644 --- a/common/djangoapps/course_modes/views.py +++ b/common/djangoapps/course_modes/views.py @@ -20,6 +20,7 @@ from courseware.access import has_access from student.models import CourseEnrollment from student.views import course_from_id from verify_student.models import SoftwareSecurePhotoVerification +from xmodule.modulestore.locations import SlashSeparatedCourseKey class ChooseModeView(View): @@ -35,7 +36,9 @@ class ChooseModeView(View): def get(self, request, course_id, error=None): """ Displays the course mode choice page """ - enrollment_mode = CourseEnrollment.enrollment_mode_for_user(request.user, course_id) + course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) + + enrollment_mode = CourseEnrollment.enrollment_mode_for_user(request.user, course_key) upgrade = request.GET.get('upgrade', False) request.session['attempting_upgrade'] = upgrade @@ -47,13 +50,13 @@ class ChooseModeView(View): if enrollment_mode is not None and upgrade is False: return redirect(reverse('dashboard')) - modes = CourseMode.modes_for_course_dict(course_id) + modes = CourseMode.modes_for_course_dict(course_key) donation_for_course = request.session.get("donation_for_course", {}) - chosen_price = donation_for_course.get(course_id, None) + chosen_price = donation_for_course.get(course_key, None) - course = course_from_id(course_id) + course = course_from_id(course_key) context = { - "course_id": course_id, + "course_modes_choose_url": reverse("course_modes_choose", kwargs={'course_id': course_key.to_deprecated_string()}), "modes": modes, "course_name": course.display_name_with_default, "course_org": course.display_org_with_default, @@ -72,25 +75,26 @@ class ChooseModeView(View): @method_decorator(login_required) def post(self, request, course_id): """ Takes the form submission from the page and parses it """ + course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) user = request.user # This is a bit redundant with logic in student.views.change_enrollement, # but I don't really have the time to refactor it more nicely and test. - course = course_from_id(course_id) - if not has_access(user, course, 'enroll'): + course = course_from_id(course_key) + if not has_access(user, 'enroll', course): error_msg = _("Enrollment is closed") - return self.get(request, course_id, error=error_msg) + return self.get(request, course_key, error=error_msg) upgrade = request.GET.get('upgrade', False) requested_mode = self.get_requested_mode(request.POST) - allowed_modes = CourseMode.modes_for_course_dict(course_id) + allowed_modes = CourseMode.modes_for_course_dict(course_key) if requested_mode not in allowed_modes: return HttpResponseBadRequest(_("Enrollment mode not supported")) if requested_mode in ("audit", "honor"): - CourseEnrollment.enroll(user, course_id, requested_mode) + CourseEnrollment.enroll(user, course_key, requested_mode) return redirect('dashboard') mode_info = allowed_modes[requested_mode] @@ -104,25 +108,25 @@ class ChooseModeView(View): amount_value = decimal.Decimal(amount).quantize(decimal.Decimal('.01'), rounding=decimal.ROUND_DOWN) except decimal.InvalidOperation: error_msg = _("Invalid amount selected.") - return self.get(request, course_id, error=error_msg) + return self.get(request, course_key, error=error_msg) # Check for minimum pricing if amount_value < mode_info.min_price: error_msg = _("No selected price or selected price is too low.") - return self.get(request, course_id, error=error_msg) + return self.get(request, course_key, error=error_msg) donation_for_course = request.session.get("donation_for_course", {}) - donation_for_course[course_id] = amount_value + donation_for_course[course_key] = amount_value request.session["donation_for_course"] = donation_for_course if SoftwareSecurePhotoVerification.user_has_valid_or_pending(request.user): return redirect( reverse('verify_student_verified', - kwargs={'course_id': course_id}) + "?upgrade={}".format(upgrade) + kwargs={'course_id': course_key.to_deprecated_string()}) + "?upgrade={}".format(upgrade) ) return redirect( reverse('verify_student_show_requirements', - kwargs={'course_id': course_id}) + "?upgrade={}".format(upgrade)) + kwargs={'course_id': course_key.to_deprecated_string()}) + "?upgrade={}".format(upgrade)) def get_requested_mode(self, request_dict): """ diff --git a/common/djangoapps/django_comment_common/models.py b/common/djangoapps/django_comment_common/models.py index 0479b7ab28cd10c113e81e6fb443eec40bed6c7e..67e33fb9ebb64a39bfb3e3f504c4341445409fd9 100644 --- a/common/djangoapps/django_comment_common/models.py +++ b/common/djangoapps/django_comment_common/models.py @@ -9,7 +9,8 @@ from django.utils.translation import ugettext_noop from student.models import CourseEnrollment from xmodule.modulestore.django import modulestore -from xmodule.course_module import CourseDescriptor +from xmodule.modulestore.exceptions import ItemNotFoundError +from xmodule_django.models import CourseKeyField, NoneToEmptyManager FORUM_ROLE_ADMINISTRATOR = ugettext_noop('Administrator') FORUM_ROLE_MODERATOR = ugettext_noop('Moderator') @@ -48,16 +49,20 @@ def assign_default_role(course_id, user): class Role(models.Model): + + objects = NoneToEmptyManager() + name = models.CharField(max_length=30, null=False, blank=False) users = models.ManyToManyField(User, related_name="roles") - course_id = models.CharField(max_length=255, blank=True, db_index=True) + course_id = CourseKeyField(max_length=255, blank=True, db_index=True) class Meta: # use existing table that was originally created from django_comment_client app db_table = 'django_comment_client_role' def __unicode__(self): - return self.name + " for " + (self.course_id if self.course_id else "all courses") + # pylint: disable=no-member + return self.name + " for " + (self.course_id.to_deprecated_string() if self.course_id else "all courses") def inherit_permissions(self, role): # TODO the name of this method is a little bit confusing, # since it's one-off and doesn't handle inheritance later @@ -71,8 +76,9 @@ class Role(models.Model): self.permissions.add(Permission.objects.get_or_create(name=permission)[0]) def has_permission(self, permission): - course_loc = CourseDescriptor.id_to_location(self.course_id) - course = modulestore().get_instance(self.course_id, course_loc) + course = modulestore().get_course(self.course_id) + if course is None: + raise ItemNotFoundError(self.course_id) if self.name == FORUM_ROLE_STUDENT and \ (permission.startswith('edit') or permission.startswith('update') or permission.startswith('create')) and \ (not course.forum_posts_allowed): diff --git a/common/djangoapps/django_comment_common/tests.py b/common/djangoapps/django_comment_common/tests.py index fd776c75d322731a54ad4406e8b7a59699f3a598..308a1c93906c31bececda87d50bb57c81c395b7d 100644 --- a/common/djangoapps/django_comment_common/tests.py +++ b/common/djangoapps/django_comment_common/tests.py @@ -1,5 +1,6 @@ from django.test import TestCase +from xmodule.modulestore.locations import SlashSeparatedCourseKey from django_comment_common.models import Role from student.models import CourseEnrollment, User @@ -21,13 +22,13 @@ class RoleAssignmentTest(TestCase): "hacky", "hacky@fake.edx.org" ) - self.course_id = "edX/Fake101/2012" - CourseEnrollment.enroll(self.staff_user, self.course_id) - CourseEnrollment.enroll(self.student_user, self.course_id) + self.course_key = SlashSeparatedCourseKey("edX", "Fake101", "2012") + CourseEnrollment.enroll(self.staff_user, self.course_key) + CourseEnrollment.enroll(self.student_user, self.course_key) def test_enrollment_auto_role_creation(self): student_role = Role.objects.get( - course_id=self.course_id, + course_id=self.course_key, name="Student" ) diff --git a/common/djangoapps/django_comment_common/utils.py b/common/djangoapps/django_comment_common/utils.py index 75da2453dc6bc244d8eb2ea4993ba71da8d5cc6c..9650401b65ab95dbdf9a2c6b5c82765ae765ed28 100644 --- a/common/djangoapps/django_comment_common/utils.py +++ b/common/djangoapps/django_comment_common/utils.py @@ -10,27 +10,27 @@ _MODERATOR_ROLE_PERMISSIONS = ["edit_content", "delete_thread", "openclose_threa _ADMINISTRATOR_ROLE_PERMISSIONS = ["manage_moderator"] -def _save_forum_role(course_id, name): +def _save_forum_role(course_key, name): """ - Save and Update 'course_id' for all roles which are already created to keep course_id same - as actual passed course id + Save and Update 'course_key' for all roles which are already created to keep course_id same + as actual passed course key """ - role, created = Role.objects.get_or_create(name=name, course_id=course_id) + role, created = Role.objects.get_or_create(name=name, course_id=course_key) if created is False: - role.course_id = course_id + role.course_id = course_key role.save() return role -def seed_permissions_roles(course_id): +def seed_permissions_roles(course_key): """ Create and assign permissions for forum roles """ - administrator_role = _save_forum_role(course_id, "Administrator") - moderator_role = _save_forum_role(course_id, "Moderator") - community_ta_role = _save_forum_role(course_id, "Community TA") - student_role = _save_forum_role(course_id, "Student") + administrator_role = _save_forum_role(course_key, "Administrator") + moderator_role = _save_forum_role(course_key, "Moderator") + community_ta_role = _save_forum_role(course_key, "Community TA") + student_role = _save_forum_role(course_key, "Student") for per in _STUDENT_ROLE_PERMISSIONS: student_role.add_permission(per) diff --git a/common/djangoapps/embargo/forms.py b/common/djangoapps/embargo/forms.py index 09ea11445be7a6b89177e79dd7f4add7599b01aa..ce7a6a00da09d89e179ec620bb3e8720aaac6834 100644 --- a/common/djangoapps/embargo/forms.py +++ b/common/djangoapps/embargo/forms.py @@ -10,6 +10,8 @@ from embargo.fixtures.country_codes import COUNTRY_CODES import socket from xmodule.modulestore.django import modulestore +from opaque_keys import InvalidKeyError +from xmodule.modulestore.locations import SlashSeparatedCourseKey class EmbargoedCourseForm(forms.ModelForm): # pylint: disable=incomplete-protocol @@ -20,19 +22,29 @@ class EmbargoedCourseForm(forms.ModelForm): # pylint: disable=incomplete-protoc def clean_course_id(self): """Validate the course id""" - course_id = self.cleaned_data["course_id"] + + cleaned_id = self.cleaned_data["course_id"] + + try: + course_id = SlashSeparatedCourseKey.from_deprecated_string(cleaned_id) + + except InvalidKeyError: + msg = 'COURSE NOT FOUND' + msg += u' --- Entered course id was: "{0}". '.format(cleaned_id) + msg += 'Please recheck that you have supplied a valid course id.' + raise forms.ValidationError(msg) # Try to get the course. If this returns None, it's not a real course try: course = modulestore().get_course(course_id) except ValueError: msg = 'COURSE NOT FOUND' - msg += u' --- Entered course id was: "{0}". '.format(course_id) + msg += u' --- Entered course id was: "{0}". '.format(course_id.to_deprecated_string()) msg += 'Please recheck that you have supplied a valid course id.' raise forms.ValidationError(msg) if not course: msg = 'COURSE NOT FOUND' - msg += u' --- Entered course id was: "{0}". '.format(course_id) + msg += u' --- Entered course id was: "{0}". '.format(course_id.to_deprecated_string()) msg += 'Please recheck that you have supplied a valid course id.' raise forms.ValidationError(msg) diff --git a/common/djangoapps/embargo/models.py b/common/djangoapps/embargo/models.py index 4ee66138590b19c559fefc2686731b6ae2b7cd18..fb85c34713ff125bf7d868721bad85a6ccd82e07 100644 --- a/common/djangoapps/embargo/models.py +++ b/common/djangoapps/embargo/models.py @@ -13,14 +13,17 @@ file and check it in at the same time as your model changes. To do that, from django.db import models from config_models.models import ConfigurationModel +from xmodule_django.models import CourseKeyField, NoneToEmptyManager class EmbargoedCourse(models.Model): """ Enable course embargo on a course-by-course basis. """ + objects = NoneToEmptyManager() + # The course to embargo - course_id = models.CharField(max_length=255, db_index=True, unique=True) + course_id = CourseKeyField(max_length=255, db_index=True, unique=True) # Whether or not to embargo embargoed = models.BooleanField(default=False) @@ -42,7 +45,8 @@ class EmbargoedCourse(models.Model): not_em = "Not " if self.embargoed: not_em = "" - return u"Course '{}' is {}Embargoed".format(self.course_id, not_em) + # pylint: disable=no-member + return u"Course '{}' is {}Embargoed".format(self.course_id.to_deprecated_string(), not_em) class EmbargoedState(ConfigurationModel): diff --git a/common/djangoapps/embargo/tests/test_forms.py b/common/djangoapps/embargo/tests/test_forms.py index cea030c23d607243ceac36f2f897129622bf12e3..8dc5dbbb806a7c876535250cc2f717d84502a80d 100644 --- a/common/djangoapps/embargo/tests/test_forms.py +++ b/common/djangoapps/embargo/tests/test_forms.py @@ -22,8 +22,8 @@ class EmbargoCourseFormTest(ModuleStoreTestCase): def setUp(self): self.course = CourseFactory.create() - self.true_form_data = {'course_id': self.course.id, 'embargoed': True} - self.false_form_data = {'course_id': self.course.id, 'embargoed': False} + self.true_form_data = {'course_id': self.course.id.to_deprecated_string(), 'embargoed': True} + self.false_form_data = {'course_id': self.course.id.to_deprecated_string(), 'embargoed': False} def test_embargo_course(self): self.assertFalse(EmbargoedCourse.is_embargoed(self.course.id)) @@ -62,7 +62,7 @@ class EmbargoCourseFormTest(ModuleStoreTestCase): def test_form_typo(self): # Munge course id - bad_id = self.course.id + '_typo' + bad_id = self.course.id.to_deprecated_string() + '_typo' form_data = {'course_id': bad_id, 'embargoed': True} form = EmbargoedCourseForm(data=form_data) @@ -79,7 +79,7 @@ class EmbargoCourseFormTest(ModuleStoreTestCase): def test_invalid_location(self): # Munge course id - bad_id = self.course.id.split('/')[-1] + bad_id = self.course.id.to_deprecated_string().split('/')[-1] form_data = {'course_id': bad_id, 'embargoed': True} form = EmbargoedCourseForm(data=form_data) diff --git a/common/djangoapps/embargo/tests/test_middleware.py b/common/djangoapps/embargo/tests/test_middleware.py index c3f418c9057a2a4deface6069f55aa0c188e59c3..cd09b2cbd9376f59924d01383c40f1c9184a52f6 100644 --- a/common/djangoapps/embargo/tests/test_middleware.py +++ b/common/djangoapps/embargo/tests/test_middleware.py @@ -32,8 +32,8 @@ class EmbargoMiddlewareTests(TestCase): self.embargo_course.save() self.regular_course = CourseFactory.create(org="Regular") self.regular_course.save() - self.embargoed_page = '/courses/' + self.embargo_course.id + '/info' - self.regular_page = '/courses/' + self.regular_course.id + '/info' + self.embargoed_page = '/courses/' + self.embargo_course.id.to_deprecated_string() + '/info' + self.regular_page = '/courses/' + self.regular_course.id.to_deprecated_string() + '/info' EmbargoedCourse(course_id=self.embargo_course.id, embargoed=True).save() EmbargoedState( embargoed_countries="cu, ir, Sy, SD", diff --git a/common/djangoapps/embargo/tests/test_models.py b/common/djangoapps/embargo/tests/test_models.py index 12c66295b8a15c7dae896f8ec56cad0fa5d8fbb0..37a8d48a2eceab04fc15200e9b4b88f82d8f234f 100644 --- a/common/djangoapps/embargo/tests/test_models.py +++ b/common/djangoapps/embargo/tests/test_models.py @@ -1,13 +1,14 @@ """Test of models for embargo middleware app""" from django.test import TestCase +from xmodule.modulestore.locations import SlashSeparatedCourseKey from embargo.models import EmbargoedCourse, EmbargoedState, IPFilter class EmbargoModelsTest(TestCase): """Test each of the 3 models in embargo.models""" def test_course_embargo(self): - course_id = 'abc/123/doremi' + course_id = SlashSeparatedCourseKey('abc', '123', 'doremi') # Test that course is not authorized by default self.assertFalse(EmbargoedCourse.is_embargoed(course_id)) diff --git a/common/djangoapps/external_auth/tests/test_shib.py b/common/djangoapps/external_auth/tests/test_shib.py index c8f38563de3541c178a572ece67bf2bd800ffdbd..0250ecc1a00ef464e23363719d09e44dd33061ae 100644 --- a/common/djangoapps/external_auth/tests/test_shib.py +++ b/common/djangoapps/external_auth/tests/test_shib.py @@ -19,6 +19,7 @@ from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, mixed_store_config from xmodule.modulestore.inheritance import own_metadata from xmodule.modulestore.django import editable_modulestore +from xmodule.modulestore.locations import SlashSeparatedCourseKey from external_auth.models import ExternalAuthMap from external_auth.views import shib_login, course_specific_login, course_specific_register, _flatten_to_ascii @@ -340,8 +341,8 @@ class ShibSPTest(ModuleStoreTestCase): '?course_id=MITx/999/course/Robot_Super_Course' + '&enrollment_action=enroll') - login_response = course_specific_login(login_request, 'MITx/999/Robot_Super_Course') - reg_response = course_specific_register(login_request, 'MITx/999/Robot_Super_Course') + login_response = course_specific_login(login_request, SlashSeparatedCourseKey('MITx', '999', 'Robot_Super_Course')) + reg_response = course_specific_register(login_request, SlashSeparatedCourseKey('MITx', '999', 'Robot_Super_Course')) if "shib" in domain: self.assertIsInstance(login_response, HttpResponseRedirect) @@ -375,8 +376,8 @@ class ShibSPTest(ModuleStoreTestCase): '?course_id=DNE/DNE/DNE/Robot_Super_Course' + '&enrollment_action=enroll') - login_response = course_specific_login(login_request, 'DNE/DNE/DNE') - reg_response = course_specific_register(login_request, 'DNE/DNE/DNE') + login_response = course_specific_login(login_request, SlashSeparatedCourseKey('DNE', 'DNE', 'DNE')) + reg_response = course_specific_register(login_request, SlashSeparatedCourseKey('DNE', 'DNE', 'DNE')) self.assertIsInstance(login_response, HttpResponseRedirect) self.assertEqual(login_response['Location'], @@ -436,7 +437,7 @@ class ShibSPTest(ModuleStoreTestCase): for student in [shib_student, other_ext_student, int_student]: request = self.request_factory.post('/change_enrollment') request.POST.update({'enrollment_action': 'enroll', - 'course_id': course.id}) + 'course_id': course.id.to_deprecated_string()}) request.user = student response = change_enrollment(request) # If course is not limited or student has correct shib extauth then enrollment should be allowed @@ -476,7 +477,7 @@ class ShibSPTest(ModuleStoreTestCase): self.assertFalse(CourseEnrollment.is_enrolled(student, course.id)) self.client.logout() request_kwargs = {'path': '/shib-login/', - 'data': {'enrollment_action': 'enroll', 'course_id': course.id, 'next': '/testredirect'}, + 'data': {'enrollment_action': 'enroll', 'course_id': course.id.to_deprecated_string(), 'next': '/testredirect'}, 'follow': False, 'REMOTE_USER': 'testuser@stanford.edu', 'Shib-Identity-Provider': 'https://idp.stanford.edu/'} diff --git a/common/djangoapps/external_auth/tests/test_ssl.py b/common/djangoapps/external_auth/tests/test_ssl.py index cd9940bf7e6d2a7b1baa991fb1dd4f2429930af2..24d4dc3bd6ff0ed2ba686b959c46dc89f75e1a88 100644 --- a/common/djangoapps/external_auth/tests/test_ssl.py +++ b/common/djangoapps/external_auth/tests/test_ssl.py @@ -3,8 +3,6 @@ Provides unit tests for SSL based authentication portions of the external_auth app. """ -import logging -import StringIO import unittest from django.conf import settings @@ -22,7 +20,7 @@ from edxmako.middleware import MakoMiddleware from external_auth.models import ExternalAuthMap import external_auth.views from student.tests.factories import UserFactory -from xmodule.modulestore.exceptions import InsufficientSpecificationError +from opaque_keys import InvalidKeyError FEATURES_WITH_SSL_AUTH = settings.FEATURES.copy() FEATURES_WITH_SSL_AUTH['AUTH_USE_CERTIFICATES'] = True @@ -193,18 +191,23 @@ class SSLClientTest(TestCase): This tests to make sure when immediate signup is on that the user doesn't get presented with the registration page. """ - # Expect a NotImplementError from course page as we don't have anything else built - with self.assertRaisesRegexp(InsufficientSpecificationError, - 'Must provide one of url, version_guid, package_id'): + # Expect an InvalidKeyError from course page as we don't have anything else built + with self.assertRaisesRegexp( + InvalidKeyError, + "<class 'xmodule.modulestore.keys.CourseKey'>: None" + ): self.client.get( reverse('signup'), follow=True, - SSL_CLIENT_S_DN=self.AUTH_DN.format(self.USER_NAME, self.USER_EMAIL)) + SSL_CLIENT_S_DN=self.AUTH_DN.format(self.USER_NAME, self.USER_EMAIL) + ) # assert that we are logged in self.assertIn(SESSION_KEY, self.client.session) # Now that we are logged in, make sure we don't see the registration page - with self.assertRaisesRegexp(InsufficientSpecificationError, - 'Must provide one of url, version_guid, package_id'): + with self.assertRaisesRegexp( + InvalidKeyError, + "<class 'xmodule.modulestore.keys.CourseKey'>: None" + ): self.client.get(reverse('signup'), follow=True) @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') @@ -228,7 +231,6 @@ class SSLClientTest(TestCase): self.assertIn(reverse('dashboard'), response['location']) self.assertIn(SESSION_KEY, self.client.session) - @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') @override_settings(FEATURES=FEATURES_WITH_SSL_AUTH_IMMEDIATE_SIGNUP) def test_ssl_bad_eamap(self): diff --git a/common/djangoapps/external_auth/views.py b/common/djangoapps/external_auth/views.py index 0f3baf7b9226136193cf407abe3b76eab48d0389..5a88a5a0191cbb5a0d0a35cc08bdd25c8e69fc9a 100644 --- a/common/djangoapps/external_auth/views.py +++ b/common/djangoapps/external_auth/views.py @@ -576,9 +576,8 @@ def course_specific_login(request, course_id): Dispatcher function for selecting the specific login method required by the course """ - try: - course = course_from_id(course_id) - except ItemNotFoundError: + course = student.views.course_from_id(course_id) + if not course: # couldn't find the course, will just return vanilla signin page return _redirect_with_get_querydict('signin_user', request.GET) @@ -595,9 +594,9 @@ def course_specific_register(request, course_id): Dispatcher function for selecting the specific registration method required by the course """ - try: - course = course_from_id(course_id) - except ItemNotFoundError: + course = student.views.course_from_id(course_id) + + if not course: # couldn't find the course, will just return vanilla registration page return _redirect_with_get_querydict('register_user', request.GET) @@ -934,9 +933,3 @@ def provider_xrds(request): # custom XRDS header necessary for discovery process response['X-XRDS-Location'] = get_xrds_url('xrds', request) return response - - -def course_from_id(course_id): - """Return the CourseDescriptor corresponding to this course_id""" - course_loc = CourseDescriptor.id_to_location(course_id) - return modulestore().get_instance(course_id, course_loc) diff --git a/common/djangoapps/heartbeat/views.py b/common/djangoapps/heartbeat/views.py index 0cee7116b467418793785f02368e26232abdfd20..9825436e7e51919e0199d62beaa672dca4a5b6e5 100644 --- a/common/djangoapps/heartbeat/views.py +++ b/common/djangoapps/heartbeat/views.py @@ -13,6 +13,6 @@ def heartbeat(request): """ output = { 'date': datetime.now(UTC).isoformat(), - 'courses': [course.location.url() for course in modulestore().get_courses()], + 'courses': [course.location.to_deprecated_string() for course in modulestore().get_courses()], } return HttpResponse(json.dumps(output, indent=4)) diff --git a/common/djangoapps/reverification/models.py b/common/djangoapps/reverification/models.py index 53b2b659c94fcde0a1d4fdf1017ae25be6cd1712..c7a07ee19aa2e267c6dddd06adc66790f754bdec 100644 --- a/common/djangoapps/reverification/models.py +++ b/common/djangoapps/reverification/models.py @@ -7,6 +7,7 @@ import pytz from django.core.exceptions import ValidationError from django.db import models from util.validate_on_save import ValidateOnSaveMixin +from xmodule_django.models import CourseKeyField class MidcourseReverificationWindow(ValidateOnSaveMixin, models.Model): @@ -17,7 +18,7 @@ class MidcourseReverificationWindow(ValidateOnSaveMixin, models.Model): overlapping time ranges. This is enforced by this class's clean() method. """ # the course that this window is attached to - course_id = models.CharField(max_length=255, db_index=True) + course_id = CourseKeyField(max_length=255, db_index=True) start_date = models.DateTimeField(default=None, null=True, blank=True) end_date = models.DateTimeField(default=None, null=True, blank=True) diff --git a/common/djangoapps/reverification/tests/factories.py b/common/djangoapps/reverification/tests/factories.py index 5a0452b7f7074c6784323913fcc70707a4e5e26f..65669c6b0852c794418dd25ee0784aa37846344b 100644 --- a/common/djangoapps/reverification/tests/factories.py +++ b/common/djangoapps/reverification/tests/factories.py @@ -5,6 +5,7 @@ from reverification.models import MidcourseReverificationWindow from factory.django import DjangoModelFactory import pytz from datetime import timedelta, datetime +from xmodule.modulestore.locations import SlashSeparatedCourseKey # Factories don't have __init__ methods, and are self documenting @@ -13,7 +14,7 @@ class MidcourseReverificationWindowFactory(DjangoModelFactory): """ Creates a generic MidcourseReverificationWindow. """ FACTORY_FOR = MidcourseReverificationWindow - course_id = u'MITx/999/Robot_Super_Course' + course_id = SlashSeparatedCourseKey.from_deprecated_string(u'MITx/999/Robot_Super_Course') # By default this factory creates a window that is currently open start_date = datetime.now(pytz.UTC) - timedelta(days=100) end_date = datetime.now(pytz.UTC) + timedelta(days=100) diff --git a/common/djangoapps/static_replace/__init__.py b/common/djangoapps/static_replace/__init__.py index 29dcf7455b030ac19fc58d8d598753a54202c4c2..56f31b2e36728bde6d51e11540607999d8016492 100644 --- a/common/djangoapps/static_replace/__init__.py +++ b/common/djangoapps/static_replace/__init__.py @@ -72,7 +72,7 @@ def replace_jump_to_id_urls(text, course_id, jump_to_id_base_url): return re.sub(_url_replace_regex('/jump_to_id/'), replace_jump_to_id_url, text) -def replace_course_urls(text, course_id): +def replace_course_urls(text, course_key): """ Replace /course/$stuff urls with /courses/$course_id/$stuff urls @@ -82,6 +82,8 @@ def replace_course_urls(text, course_id): returns: text with the links replaced """ + course_id = course_key.to_deprecated_string() + def replace_course_url(match): quote = match.group('quote') rest = match.group('rest') diff --git a/common/djangoapps/static_replace/test/test_static_replace.py b/common/djangoapps/static_replace/test/test_static_replace.py index 1e7521c3a932ac41de63392b8010acd02a93651d..d631b1ffe84990d50e07e1a75376c8c0de96ea6e 100644 --- a/common/djangoapps/static_replace/test/test_static_replace.py +++ b/common/djangoapps/static_replace/test/test_static_replace.py @@ -4,12 +4,13 @@ from nose.tools import assert_equals, assert_true, assert_false # pylint: disab from static_replace import (replace_static_urls, replace_course_urls, _url_replace_regex) from mock import patch, Mock -from xmodule.modulestore import Location + +from xmodule.modulestore.locations import SlashSeparatedCourseKey from xmodule.modulestore.mongo import MongoModuleStore from xmodule.modulestore.xml import XMLModuleStore DATA_DIRECTORY = 'data_dir' -COURSE_ID = 'org/course/run' +COURSE_KEY = SlashSeparatedCourseKey('org', 'course', 'run') STATIC_SOURCE = '"/static/file.png"' @@ -21,8 +22,8 @@ def test_multi_replace(): replace_static_urls(replace_static_urls(STATIC_SOURCE, DATA_DIRECTORY), DATA_DIRECTORY) ) assert_equals( - replace_course_urls(course_source, COURSE_ID), - replace_course_urls(replace_course_urls(course_source, COURSE_ID), COURSE_ID) + replace_course_urls(course_source, COURSE_KEY), + replace_course_urls(replace_course_urls(course_source, COURSE_KEY), COURSE_KEY) ) @@ -59,10 +60,10 @@ def test_mongo_filestore(mock_modulestore, mock_static_content): # Namespace => content url assert_equals( '"' + mock_static_content.convert_legacy_static_url_with_course_id.return_value + '"', - replace_static_urls(STATIC_SOURCE, DATA_DIRECTORY, course_id=COURSE_ID) + replace_static_urls(STATIC_SOURCE, DATA_DIRECTORY, course_id=COURSE_KEY) ) - mock_static_content.convert_legacy_static_url_with_course_id.assert_called_once_with('file.png', COURSE_ID) + mock_static_content.convert_legacy_static_url_with_course_id.assert_called_once_with('file.png', COURSE_KEY) @patch('static_replace.settings') @@ -101,7 +102,7 @@ def test_static_url_with_query(mock_modulestore, mock_storage): pre_text = 'EMBED src ="/static/LAlec04_controller.swf?csConfigFile=/c4x/org/course/asset/LAlec04_config.xml"' post_text = 'EMBED src ="/c4x/org/course/asset/LAlec04_controller.swf?csConfigFile=/c4x/org/course/asset/LAlec04_config.xml"' - assert_equals(post_text, replace_static_urls(pre_text, DATA_DIRECTORY, COURSE_ID)) + assert_equals(post_text, replace_static_urls(pre_text, DATA_DIRECTORY, COURSE_KEY)) def test_regex(): diff --git a/common/djangoapps/student/auth.py b/common/djangoapps/student/auth.py index 97583f69dc300af65906cfd4565549cecf78dccf..505ad9d4dad9e687b8218ab4dd56c31e937dd8f6 100644 --- a/common/djangoapps/student/auth.py +++ b/common/djangoapps/student/auth.py @@ -35,7 +35,7 @@ def has_access(user, role): return True # if not, then check inferred permissions if (isinstance(role, (CourseStaffRole, CourseBetaTesterRole)) and - CourseInstructorRole(role.location).has_user(user)): + CourseInstructorRole(role.course_key).has_user(user)): return True return False @@ -81,6 +81,6 @@ def _check_caller_authority(caller, role): if isinstance(role, (GlobalStaff, CourseCreatorRole)): raise PermissionDenied elif isinstance(role, CourseRole): # instructors can change the roles w/in their course - if not has_access(caller, CourseInstructorRole(role.location)): + if not has_access(caller, CourseInstructorRole(role.course_key)): raise PermissionDenied diff --git a/common/djangoapps/student/management/commands/anonymized_id_mapping.py b/common/djangoapps/student/management/commands/anonymized_id_mapping.py index d27c306d8da5aec0bc680492daa8e281dbeee89d..75b61e45ba82c88665158e54a6d5154eb4ea59b8 100644 --- a/common/djangoapps/student/management/commands/anonymized_id_mapping.py +++ b/common/djangoapps/student/management/commands/anonymized_id_mapping.py @@ -59,7 +59,7 @@ class Command(BaseCommand): for student in students: csv_writer.writerow(( student.id, - anonymous_id_for_user(student, ''), + anonymous_id_for_user(student, None), anonymous_id_for_user(student, course_id) )) except IOError: diff --git a/common/djangoapps/student/management/commands/create_user.py b/common/djangoapps/student/management/commands/create_user.py index bf848004ce98603195d1619caa5d5de19af529ae..9ba00cabdaf8eb4a82a5dff28750a719fd60f089 100644 --- a/common/djangoapps/student/management/commands/create_user.py +++ b/common/djangoapps/student/management/commands/create_user.py @@ -5,6 +5,9 @@ from django.contrib.auth.models import User from django.core.management.base import BaseCommand from django.utils import translation +from opaque_keys import InvalidKeyError +from xmodule.modulestore.keys import CourseKey +from xmodule.modulestore.locations import SlashSeparatedCourseKey from student.models import CourseEnrollment, Registration, create_comments_service_user from student.views import _do_create_account, AccountValidationError from track.management.tracked_command import TrackedCommand @@ -68,6 +71,15 @@ class Command(TrackedCommand): if not name: name = options['email'].split('@')[0] + # parse out the course into a coursekey + if options['course']: + try: + course = CourseKey.from_string(options['course']) + # if it's not a new-style course key, parse it from an old-style + # course key + except InvalidKeyError: + course = SlashSeparatedCourseKey.from_deprecated_string(options['course']) + post_data = { 'username': username, 'email': options['email'], @@ -93,5 +105,5 @@ class Command(TrackedCommand): print e.message user = User.objects.get(email=options['email']) if options['course']: - CourseEnrollment.enroll(user, options['course'], mode=options['mode']) + CourseEnrollment.enroll(user, course, mode=options['mode']) translation.deactivate() diff --git a/common/djangoapps/student/migrations/0034_auto__add_courseaccessrole.py b/common/djangoapps/student/migrations/0034_auto__add_courseaccessrole.py new file mode 100644 index 0000000000000000000000000000000000000000..d6267ecb017ae003750eeb8307127598b01b0db8 --- /dev/null +++ b/common/djangoapps/student/migrations/0034_auto__add_courseaccessrole.py @@ -0,0 +1,189 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'CourseAccessRole' + db.create_table('student_courseaccessrole', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])), + ('org', self.gf('django.db.models.fields.CharField')(db_index=True, max_length=64, blank=True)), + ('course_id', self.gf('xmodule_django.models.CourseKeyField')(db_index=True, max_length=255, blank=True)), + ('role', self.gf('django.db.models.fields.CharField')(max_length=64, db_index=True)), + )) + db.send_create_signal('student', ['CourseAccessRole']) + + # Adding unique constraint on 'CourseAccessRole', fields ['user', 'org', 'course_id', 'role'] + db.create_unique('student_courseaccessrole', ['user_id', 'org', 'course_id', 'role']) + + + # Changing field 'AnonymousUserId.course_id' + db.alter_column('student_anonymoususerid', 'course_id', self.gf('xmodule_django.models.CourseKeyField')(max_length=255)) + + # Changing field 'CourseEnrollment.course_id' + db.alter_column('student_courseenrollment', 'course_id', self.gf('xmodule_django.models.CourseKeyField')(max_length=255)) + + # Changing field 'CourseEnrollmentAllowed.course_id' + db.alter_column('student_courseenrollmentallowed', 'course_id', self.gf('xmodule_django.models.CourseKeyField')(max_length=255)) + + def backwards(self, orm): + # Removing unique constraint on 'CourseAccessRole', fields ['user', 'org', 'course_id', 'role'] + db.delete_unique('student_courseaccessrole', ['user_id', 'org', 'course_id', 'role']) + + # Deleting model 'CourseAccessRole' + db.delete_table('student_courseaccessrole') + + + # Changing field 'AnonymousUserId.course_id' + db.alter_column('student_anonymoususerid', 'course_id', self.gf('django.db.models.fields.CharField')(max_length=255)) + + # Changing field 'CourseEnrollment.course_id' + db.alter_column('student_courseenrollment', 'course_id', self.gf('django.db.models.fields.CharField')(max_length=255)) + + # Changing field 'CourseEnrollmentAllowed.course_id' + db.alter_column('student_courseenrollmentallowed', 'course_id', self.gf('django.db.models.fields.CharField')(max_length=255)) + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'student.anonymoususerid': { + 'Meta': {'object_name': 'AnonymousUserId'}, + 'anonymous_user_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}), + 'course_id': ('xmodule_django.models.CourseKeyField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'student.courseaccessrole': { + 'Meta': {'unique_together': "(('user', 'org', 'course_id', 'role'),)", 'object_name': 'CourseAccessRole'}, + 'course_id': ('xmodule_django.models.CourseKeyField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'org': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '64', 'blank': 'True'}), + 'role': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'student.courseenrollment': { + 'Meta': {'ordering': "('user', 'course_id')", 'unique_together': "(('user', 'course_id'),)", 'object_name': 'CourseEnrollment'}, + 'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'mode': ('django.db.models.fields.CharField', [], {'default': "'honor'", 'max_length': '100'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'student.courseenrollmentallowed': { + 'Meta': {'unique_together': "(('email', 'course_id'),)", 'object_name': 'CourseEnrollmentAllowed'}, + 'auto_enroll': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}), + 'email': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'student.loginfailures': { + 'Meta': {'object_name': 'LoginFailures'}, + 'failure_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'lockout_until': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'student.passwordhistory': { + 'Meta': {'object_name': 'PasswordHistory'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'time_set': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'student.pendingemailchange': { + 'Meta': {'object_name': 'PendingEmailChange'}, + 'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'new_email': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'}) + }, + 'student.pendingnamechange': { + 'Meta': {'object_name': 'PendingNameChange'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'new_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'rationale': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'}) + }, + 'student.registration': { + 'Meta': {'object_name': 'Registration', 'db_table': "'auth_registration'"}, + 'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True'}) + }, + 'student.userprofile': { + 'Meta': {'object_name': 'UserProfile', 'db_table': "'auth_userprofile'"}, + 'allow_certificate': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'city': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'null': 'True', 'blank': 'True'}), + 'courseware': ('django.db.models.fields.CharField', [], {'default': "'course.xml'", 'max_length': '255', 'blank': 'True'}), + 'gender': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}), + 'goals': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'language': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), + 'level_of_education': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}), + 'location': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), + 'mailing_address': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'meta': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'profile'", 'unique': 'True', 'to': "orm['auth.User']"}), + 'year_of_birth': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}) + }, + 'student.userstanding': { + 'Meta': {'object_name': 'UserStanding'}, + 'account_status': ('django.db.models.fields.CharField', [], {'max_length': '31', 'blank': 'True'}), + 'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'standing_last_changed_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'standing'", 'unique': 'True', 'to': "orm['auth.User']"}) + }, + 'student.usertestgroup': { + 'Meta': {'object_name': 'UserTestGroup'}, + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}), + 'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'db_index': 'True', 'symmetrical': 'False'}) + } + } + + complete_apps = ['student'] \ No newline at end of file diff --git a/common/djangoapps/student/tests/test_authz.py b/common/djangoapps/student/tests/test_authz.py index dee2eb84f4bbeddcd37d8ad2f821e91540f5d996..0d6929c19031fedc6a9c7c3f286fddcda30de49c 100644 --- a/common/djangoapps/student/tests/test_authz.py +++ b/common/djangoapps/student/tests/test_authz.py @@ -5,12 +5,12 @@ import mock from django.test import TestCase from django.contrib.auth.models import User, AnonymousUser -from xmodule.modulestore import Location from django.core.exceptions import PermissionDenied from student.roles import CourseInstructorRole, CourseStaffRole, CourseCreatorRole from student.tests.factories import AdminFactory from student.auth import has_access, add_users, remove_users +from xmodule.modulestore.locations import SlashSeparatedCourseKey class CreatorGroupTest(TestCase): @@ -137,54 +137,54 @@ class CourseGroupTest(TestCase): self.global_admin = AdminFactory() self.creator = User.objects.create_user('testcreator', 'testcreator+courses@edx.org', 'foo') self.staff = User.objects.create_user('teststaff', 'teststaff+courses@edx.org', 'foo') - self.location = Location('i4x', 'mitX', '101', 'course', 'test') + self.course_key = SlashSeparatedCourseKey('mitX', '101', 'test') def test_add_user_to_course_group(self): """ Tests adding user to course group (happy path). """ # Create groups for a new course (and assign instructor role to the creator). - self.assertFalse(has_access(self.creator, CourseInstructorRole(self.location))) - add_users(self.global_admin, CourseInstructorRole(self.location), self.creator) - add_users(self.global_admin, CourseStaffRole(self.location), self.creator) - self.assertTrue(has_access(self.creator, CourseInstructorRole(self.location))) + self.assertFalse(has_access(self.creator, CourseInstructorRole(self.course_key))) + add_users(self.global_admin, CourseInstructorRole(self.course_key), self.creator) + add_users(self.global_admin, CourseStaffRole(self.course_key), self.creator) + self.assertTrue(has_access(self.creator, CourseInstructorRole(self.course_key))) # Add another user to the staff role. - self.assertFalse(has_access(self.staff, CourseStaffRole(self.location))) - add_users(self.creator, CourseStaffRole(self.location), self.staff) - self.assertTrue(has_access(self.staff, CourseStaffRole(self.location))) + self.assertFalse(has_access(self.staff, CourseStaffRole(self.course_key))) + add_users(self.creator, CourseStaffRole(self.course_key), self.staff) + self.assertTrue(has_access(self.staff, CourseStaffRole(self.course_key))) def test_add_user_to_course_group_permission_denied(self): """ Verifies PermissionDenied if caller of add_user_to_course_group is not instructor role. """ - add_users(self.global_admin, CourseInstructorRole(self.location), self.creator) - add_users(self.global_admin, CourseStaffRole(self.location), self.creator) + add_users(self.global_admin, CourseInstructorRole(self.course_key), self.creator) + add_users(self.global_admin, CourseStaffRole(self.course_key), self.creator) with self.assertRaises(PermissionDenied): - add_users(self.staff, CourseStaffRole(self.location), self.staff) + add_users(self.staff, CourseStaffRole(self.course_key), self.staff) def test_remove_user_from_course_group(self): """ Tests removing user from course group (happy path). """ - add_users(self.global_admin, CourseInstructorRole(self.location), self.creator) - add_users(self.global_admin, CourseStaffRole(self.location), self.creator) + add_users(self.global_admin, CourseInstructorRole(self.course_key), self.creator) + add_users(self.global_admin, CourseStaffRole(self.course_key), self.creator) - add_users(self.creator, CourseStaffRole(self.location), self.staff) - self.assertTrue(has_access(self.staff, CourseStaffRole(self.location))) + add_users(self.creator, CourseStaffRole(self.course_key), self.staff) + self.assertTrue(has_access(self.staff, CourseStaffRole(self.course_key))) - remove_users(self.creator, CourseStaffRole(self.location), self.staff) - self.assertFalse(has_access(self.staff, CourseStaffRole(self.location))) + remove_users(self.creator, CourseStaffRole(self.course_key), self.staff) + self.assertFalse(has_access(self.staff, CourseStaffRole(self.course_key))) - remove_users(self.creator, CourseInstructorRole(self.location), self.creator) - self.assertFalse(has_access(self.creator, CourseInstructorRole(self.location))) + remove_users(self.creator, CourseInstructorRole(self.course_key), self.creator) + self.assertFalse(has_access(self.creator, CourseInstructorRole(self.course_key))) def test_remove_user_from_course_group_permission_denied(self): """ Verifies PermissionDenied if caller of remove_user_from_course_group is not instructor role. """ - add_users(self.global_admin, CourseInstructorRole(self.location), self.creator) + add_users(self.global_admin, CourseInstructorRole(self.course_key), self.creator) another_staff = User.objects.create_user('another', 'teststaff+anothercourses@edx.org', 'foo') - add_users(self.global_admin, CourseStaffRole(self.location), self.creator, self.staff, another_staff) + add_users(self.global_admin, CourseStaffRole(self.course_key), self.creator, self.staff, another_staff) with self.assertRaises(PermissionDenied): - remove_users(self.staff, CourseStaffRole(self.location), another_staff) + remove_users(self.staff, CourseStaffRole(self.course_key), another_staff) diff --git a/common/djangoapps/student/tests/test_auto_auth.py b/common/djangoapps/student/tests/test_auto_auth.py index 67834127431a93efbf035891cdd9990f3d1d325e..6228962d0b858b99a2ba51f919ec4000bf1b0647 100644 --- a/common/djangoapps/student/tests/test_auto_auth.py +++ b/common/djangoapps/student/tests/test_auto_auth.py @@ -6,6 +6,7 @@ from django_comment_common.models import ( from django_comment_common.utils import seed_permissions_roles from student.models import CourseEnrollment, UserProfile from util.testing import UrlResetMixin +from xmodule.modulestore.locations import SlashSeparatedCourseKey from mock import patch @@ -23,6 +24,8 @@ class AutoAuthEnabledTestCase(UrlResetMixin, TestCase): super(AutoAuthEnabledTestCase, self).setUp() self.url = '/auto_auth' self.client = Client() + self.course_id = 'edX/Test101/2014_Spring' + self.course_key = SlashSeparatedCourseKey.from_deprecated_string(self.course_id) def test_create_user(self): """ @@ -83,43 +86,39 @@ class AutoAuthEnabledTestCase(UrlResetMixin, TestCase): def test_course_enrollment(self): # Create a user and enroll in a course - course_id = "edX/Test101/2014_Spring" - self._auto_auth(username='test', course_id=course_id) + self._auto_auth(username='test', course_id=self.course_id) # Check that a course enrollment was created for the user self.assertEqual(CourseEnrollment.objects.count(), 1) - enrollment = CourseEnrollment.objects.get(course_id=course_id) + enrollment = CourseEnrollment.objects.get(course_id=self.course_key) self.assertEqual(enrollment.user.username, "test") def test_double_enrollment(self): # Create a user and enroll in a course - course_id = "edX/Test101/2014_Spring" - self._auto_auth(username='test', course_id=course_id) + self._auto_auth(username='test', course_id=self.course_id) # Make the same call again, re-enrolling the student in the same course - self._auto_auth(username='test', course_id=course_id) + self._auto_auth(username='test', course_id=self.course_id) # Check that only one course enrollment was created for the user self.assertEqual(CourseEnrollment.objects.count(), 1) - enrollment = CourseEnrollment.objects.get(course_id=course_id) + enrollment = CourseEnrollment.objects.get(course_id=self.course_key) self.assertEqual(enrollment.user.username, "test") def test_set_roles(self): - - course_id = "edX/Test101/2014_Spring" - seed_permissions_roles(course_id) - course_roles = dict((r.name, r) for r in Role.objects.filter(course_id=course_id)) + seed_permissions_roles(self.course_key) + course_roles = dict((r.name, r) for r in Role.objects.filter(course_id=self.course_key)) self.assertEqual(len(course_roles), 4) # sanity check # Student role is assigned by default on course enrollment. - self._auto_auth(username='a_student', course_id=course_id) + self._auto_auth(username='a_student', course_id=self.course_id) user = User.objects.get(username='a_student') user_roles = user.roles.all() self.assertEqual(len(user_roles), 1) self.assertEqual(user_roles[0], course_roles[FORUM_ROLE_STUDENT]) - self._auto_auth(username='a_moderator', course_id=course_id, roles='Moderator') + self._auto_auth(username='a_moderator', course_id=self.course_id, roles='Moderator') user = User.objects.get(username='a_moderator') user_roles = user.roles.all() self.assertEqual( @@ -128,7 +127,7 @@ class AutoAuthEnabledTestCase(UrlResetMixin, TestCase): course_roles[FORUM_ROLE_MODERATOR]])) # check multiple roles work. - self._auto_auth(username='an_admin', course_id=course_id, + self._auto_auth(username='an_admin', course_id=self.course_id, roles='{},{}'.format(FORUM_ROLE_MODERATOR, FORUM_ROLE_ADMINISTRATOR)) user = User.objects.get(username='an_admin') user_roles = user.roles.all() diff --git a/common/djangoapps/student/tests/test_bulk_email_settings.py b/common/djangoapps/student/tests/test_bulk_email_settings.py index 6b903d9979058b7d47a97f0fa47e06f1e14e1af6..f164aea76186e40d57bbb943b1b6e630fd8720d5 100644 --- a/common/djangoapps/student/tests/test_bulk_email_settings.py +++ b/common/djangoapps/student/tests/test_bulk_email_settings.py @@ -15,6 +15,7 @@ from student.tests.factories import UserFactory, CourseEnrollmentFactory from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE +from xmodule.modulestore.locations import SlashSeparatedCourseKey from bulk_email.models import CourseAuthorization @@ -100,7 +101,10 @@ class TestStudentDashboardEmailViewXMLBacked(ModuleStoreTestCase): # Create student account student = UserFactory.create() - CourseEnrollmentFactory.create(user=student, course_id=self.course_name) + CourseEnrollmentFactory.create( + user=student, + course_id=SlashSeparatedCourseKey.from_deprecated_string(self.course_name) + ) self.client.login(username=student.username, password="test") try: diff --git a/common/djangoapps/student/tests/test_login.py b/common/djangoapps/student/tests/test_login.py index 34083c90b47dd7cb82fc45db69ae130a940ff22c..00fc67282ae476ccfa9f8d974ebad28267490f06 100644 --- a/common/djangoapps/student/tests/test_login.py +++ b/common/djangoapps/student/tests/test_login.py @@ -20,6 +20,7 @@ from xmodule.modulestore.inheritance import own_metadata from xmodule.modulestore.django import editable_modulestore from external_auth.models import ExternalAuthMap +from xmodule.modulestore.locations import SlashSeparatedCourseKey TEST_DATA_MIXED_MODULESTORE = mixed_store_config(settings.COMMON_TEST_DATA_ROOT, {}) @@ -275,7 +276,10 @@ class UtilFnTest(TestCase): COURSE_ID = u'org/num/run' # pylint: disable=C0103 COURSE_URL = u'/courses/{}/otherstuff'.format(COURSE_ID) # pylint: disable=C0103 NON_COURSE_URL = u'/blahblah' # pylint: disable=C0103 - self.assertEqual(_parse_course_id_from_string(COURSE_URL), COURSE_ID) + self.assertEqual( + _parse_course_id_from_string(COURSE_URL), + SlashSeparatedCourseKey.from_deprecated_string(COURSE_ID) + ) self.assertIsNone(_parse_course_id_from_string(NON_COURSE_URL)) @@ -320,7 +324,7 @@ class ExternalAuthShibTest(ModuleStoreTestCase): """ Tests the _get_course_enrollment_domain utility function """ - self.assertIsNone(_get_course_enrollment_domain("I/DONT/EXIST")) + self.assertIsNone(_get_course_enrollment_domain(SlashSeparatedCourseKey("I", "DONT", "EXIST"))) self.assertIsNone(_get_course_enrollment_domain(self.course.id)) self.assertEqual(self.shib_course.enrollment_domain, _get_course_enrollment_domain(self.shib_course.id)) @@ -340,7 +344,7 @@ class ExternalAuthShibTest(ModuleStoreTestCase): Tests the redirects when visiting course-specific URL with @login_required. Should vary by course depending on its enrollment_domain """ - TARGET_URL = reverse('courseware', args=[self.course.id]) # pylint: disable=C0103 + TARGET_URL = reverse('courseware', args=[self.course.id.to_deprecated_string()]) # pylint: disable=C0103 noshib_response = self.client.get(TARGET_URL, follow=True) self.assertEqual(noshib_response.redirect_chain[-1], ('http://testserver/accounts/login?next={url}'.format(url=TARGET_URL), 302)) @@ -348,7 +352,7 @@ class ExternalAuthShibTest(ModuleStoreTestCase): .format(platform_name=settings.PLATFORM_NAME))) self.assertEqual(noshib_response.status_code, 200) - TARGET_URL_SHIB = reverse('courseware', args=[self.shib_course.id]) # pylint: disable=C0103 + TARGET_URL_SHIB = reverse('courseware', args=[self.shib_course.id.to_deprecated_string()]) # pylint: disable=C0103 shib_response = self.client.get(**{'path': TARGET_URL_SHIB, 'follow': True, 'REMOTE_USER': self.extauth.external_id, diff --git a/common/djangoapps/student/tests/test_userstanding.py b/common/djangoapps/student/tests/test_userstanding.py index 19fe957ab76694e88666d4ed5942a7a68e5babd5..c6f9cdcbc7e48395c83d6a62c6b0d5168db2e2d9 100644 --- a/common/djangoapps/student/tests/test_userstanding.py +++ b/common/djangoapps/student/tests/test_userstanding.py @@ -53,7 +53,7 @@ class UserStandingTest(TestCase): try: self.some_url = reverse('dashboard') except NoReverseMatch: - self.some_url = '/course' + self.some_url = '/course/' # since it's only possible to disable accounts from lms, we're going # to skip tests for cms diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 34c30c45035afba565fff00a4994892a067fd7a8..61affc3d1f0b366f5c7a12e209b0deaebbaa3419 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -55,6 +55,7 @@ from dark_lang.models import DarkLangConfig from xmodule.course_module import CourseDescriptor from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.django import modulestore +from xmodule.modulestore.locations import SlashSeparatedCourseKey from xmodule.modulestore import XML_MODULESTORE_TYPE, Location from collections import namedtuple @@ -132,8 +133,7 @@ def index(request, extra_context={}, user=AnonymousUser()): def course_from_id(course_id): """Return the CourseDescriptor corresponding to this course_id""" - course_loc = CourseDescriptor.id_to_location(course_id) - return modulestore().get_instance(course_id, course_loc) + return modulestore().get_course(course_id) day_pattern = re.compile(r'\s\d+,\s') multimonth_pattern = re.compile(r'\s?\-\s?\S+\s') @@ -269,8 +269,8 @@ def get_course_enrollment_pairs(user, course_org_filter, org_filter_out_set): a student's dashboard. """ for enrollment in CourseEnrollment.enrollments_for_user(user): - try: - course = course_from_id(enrollment.course_id) + course = course_from_id(enrollment.course_id) + if course: # if we are in a Microsite, then filter out anything that is not # attributed (by ORG) to that Microsite @@ -282,7 +282,7 @@ def get_course_enrollment_pairs(user, course_org_filter, org_filter_out_set): continue yield (course, enrollment) - except ItemNotFoundError: + else: log.error("User {0} enrolled in non-existent course {1}" .format(user.username, enrollment.course_id)) @@ -478,13 +478,13 @@ def dashboard(request): # Global staff can see what courses errored on their dashboard staff_access = False errored_courses = {} - if has_access(user, 'global', 'staff'): + if has_access(user, 'staff', 'global'): # Show any courses that errored on load staff_access = True errored_courses = modulestore().get_errored_courses() show_courseware_links_for = frozenset(course.id for course, _enrollment in course_enrollment_pairs - if has_access(request.user, course, 'load')) + if has_access(request.user, 'load', course)) course_modes = {course.id: complete_course_mode_info(course.id, enrollment) for course, enrollment in course_enrollment_pairs} cert_statuses = {course.id: cert_info(request.user, course) for course, _enrollment in course_enrollment_pairs} @@ -617,10 +617,11 @@ def change_enrollment(request): user = request.user action = request.POST.get("enrollment_action") - course_id = request.POST.get("course_id") - if course_id is None: + if 'course_id' not in request.POST: return HttpResponseBadRequest(_("Course id not specified")) + course_id = SlashSeparatedCourseKey.from_deprecated_string(request.POST.get("course_id")) + if not user.is_authenticated(): return HttpResponseForbidden() @@ -634,7 +635,7 @@ def change_enrollment(request): .format(user.username, course_id)) return HttpResponseBadRequest(_("Course id is invalid")) - if not has_access(user, course, 'enroll'): + if not has_access(user, 'enroll', course): return HttpResponseBadRequest(_("Enrollment is closed")) # see if we have already filled up all allowed enrollments @@ -648,7 +649,7 @@ def change_enrollment(request): available_modes = CourseMode.modes_for_course(course_id) if len(available_modes) > 1: return HttpResponse( - reverse("course_modes_choose", kwargs={'course_id': course_id}) + reverse("course_modes_choose", kwargs={'course_id': course_id.to_deprecated_string()}) ) current_mode = available_modes[0] @@ -664,7 +665,7 @@ def change_enrollment(request): # the user to the shopping cart page always, where they can reasonably discern the status of their cart, # whether things got added, etc - shoppingcart.views.add_course_to_cart(request, course_id) + shoppingcart.views.add_course_to_cart(request, course_id.to_deprecated_string()) return HttpResponse( reverse("shoppingcart.views.show_cart") ) @@ -686,7 +687,7 @@ def _parse_course_id_from_string(input_str): """ m_obj = re.match(r'^/courses/(?P<course_id>[^/]+/[^/]+/[^/]+)', input_str) if m_obj: - return m_obj.group('course_id') + return SlashSeparatedCourseKey.from_deprecated_string(m_obj.group('course_id')) return None @@ -696,12 +697,12 @@ def _get_course_enrollment_domain(course_id): @param course_id: @return: """ - try: - course = course_from_id(course_id) - return course.enrollment_domain - except ItemNotFoundError: + course = course_from_id(course_id) + if course is None: return None + return course.enrollment_domain + @ensure_csrf_cookie def accounts_login(request): @@ -1378,6 +1379,9 @@ def auto_auth(request): full_name = request.GET.get('full_name', username) is_staff = request.GET.get('staff', None) course_id = request.GET.get('course_id', None) + course_key = None + if course_id: + course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) role_names = [v.strip() for v in request.GET.get('roles', '').split(',') if v.strip()] # Get or create the user object @@ -1413,12 +1417,12 @@ def auto_auth(request): reg.save() # Enroll the user in a course - if course_id is not None: - CourseEnrollment.enroll(user, course_id) + if course_key is not None: + CourseEnrollment.enroll(user, course_key) # Apply the roles for role_name in role_names: - role = Role.objects.get(name=role_name, course_id=course_id) + role = Role.objects.get(name=role_name, course_id=course_key) user.roles.add(role) # Log in as the user @@ -1865,15 +1869,16 @@ def change_email_settings(request): user = request.user course_id = request.POST.get("course_id") + course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) receive_emails = request.POST.get("receive_emails") if receive_emails: - optout_object = Optout.objects.filter(user=user, course_id=course_id) + optout_object = Optout.objects.filter(user=user, course_id=course_key) if optout_object: optout_object.delete() log.info(u"User {0} ({1}) opted in to receive emails from course {2}".format(user.username, user.email, course_id)) track.views.server_track(request, "change-email-settings", {"receive_emails": "yes", "course": course_id}, page='dashboard') else: - Optout.objects.get_or_create(user=user, course_id=course_id) + Optout.objects.get_or_create(user=user, course_id=course_key) log.info(u"User {0} ({1}) opted out of receiving emails from course {2}".format(user.username, user.email, course_id)) track.views.server_track(request, "change-email-settings", {"receive_emails": "no", "course": course_id}, page='dashboard') @@ -1889,7 +1894,7 @@ def token(request): the token was issued. This will be stored with the user along with the id for identification purposes in the backend. ''' - course_id = request.GET.get("course_id") + course_id = SlashSeparatedCourseKey.from_deprecated_string(request.GET.get("course_id")) course = course_from_id(course_id) dtnow = datetime.datetime.now() dtutcnow = datetime.datetime.utcnow() diff --git a/common/djangoapps/track/contexts.py b/common/djangoapps/track/contexts.py index 070ac10ebdea35c8d7325c472d99668455718520..bb8673480e3c2ea26445338c63c21ee2287081b6 100644 --- a/common/djangoapps/track/contexts.py +++ b/common/djangoapps/track/contexts.py @@ -1,7 +1,8 @@ """Generates common contexts""" import logging -from xmodule.course_module import CourseDescriptor +from xmodule.modulestore.locations import SlashSeparatedCourseKey +from opaque_keys import InvalidKeyError from util.request import COURSE_REGEX log = logging.getLogger(__name__) @@ -9,15 +10,24 @@ log = logging.getLogger(__name__) def course_context_from_url(url): """ - Extracts the course_id from the given `url` and passes it on to + Extracts the course_context from the given `url` and passes it on to `course_context_from_course_id()`. """ url = url or '' match = COURSE_REGEX.match(url) - course_id = '' + course_id = None if match: - course_id = match.group('course_id') or '' + course_id_string = match.group('course_id') + try: + course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id_string) + except InvalidKeyError: + log.warning( + 'unable to parse course_id "{course_id}"'.format( + course_id=course_id_string + ), + exc_info=True + ) return course_context_from_course_id(course_id) @@ -34,23 +44,12 @@ def course_context_from_course_id(course_id): } """ - - course_id = course_id or '' - context = { - 'course_id': course_id, - 'org_id': '' + if course_id is None: + return {'course_id': '', 'org_id': ''} + + # TODO: Make this accept any CourseKey, and serialize it using .to_string + assert(isinstance(course_id, SlashSeparatedCourseKey)) + return { + 'course_id': course_id.to_deprecated_string(), + 'org_id': course_id.org, } - - if course_id: - try: - location = CourseDescriptor.id_to_location(course_id) - context['org_id'] = location.org - except ValueError: - log.warning( - 'Unable to parse course_id "{course_id}"'.format( - course_id=course_id - ), - exc_info=True - ) - - return context diff --git a/common/djangoapps/user_api/middleware.py b/common/djangoapps/user_api/middleware.py index 3a33db7143598aba42c038c97dc631c6a6e632ef..97387e50072a01ee11660eec0e20ecfbe00ad789 100644 --- a/common/djangoapps/user_api/middleware.py +++ b/common/djangoapps/user_api/middleware.py @@ -5,6 +5,7 @@ Adds user's tags to tracking event context. from track.contexts import COURSE_REGEX from eventtracking import tracker from user_api.models import UserCourseTag +from xmodule.modulestore.locations import SlashSeparatedCourseKey class UserTagsEventContextMiddleware(object): @@ -19,6 +20,7 @@ class UserTagsEventContextMiddleware(object): course_id = None if match: course_id = match.group('course_id') + course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) context = {} @@ -29,7 +31,7 @@ class UserTagsEventContextMiddleware(object): context['course_user_tags'] = dict( UserCourseTag.objects.filter( user=request.user.pk, - course_id=course_id + course_id=course_key, ).values_list('key', 'value') ) else: diff --git a/common/djangoapps/user_api/models.py b/common/djangoapps/user_api/models.py index 76b8cd5053f4f0ce349962ba66ab7047b80d18fa..e5a31db820c1a35662aa88fa058ae91e832c9e1e 100644 --- a/common/djangoapps/user_api/models.py +++ b/common/djangoapps/user_api/models.py @@ -2,6 +2,8 @@ from django.contrib.auth.models import User from django.core.validators import RegexValidator from django.db import models +from xmodule_django.models import CourseKeyField + class UserPreference(models.Model): """A user's preference, stored as generic text to be processed by client""" @@ -44,7 +46,7 @@ class UserCourseTag(models.Model): """ user = models.ForeignKey(User, db_index=True, related_name="+") key = models.CharField(max_length=255, db_index=True) - course_id = models.CharField(max_length=255, db_index=True) + course_id = CourseKeyField(max_length=255, db_index=True) value = models.TextField() class Meta: # pylint: disable=missing-docstring diff --git a/common/djangoapps/user_api/tests/factories.py b/common/djangoapps/user_api/tests/factories.py index 535e888a59b99a033a13367bb007d5ecc889e2bc..e5bd4debac6b663b39659c81235f69004bafb786 100644 --- a/common/djangoapps/user_api/tests/factories.py +++ b/common/djangoapps/user_api/tests/factories.py @@ -3,6 +3,7 @@ from factory.django import DjangoModelFactory from factory import SubFactory from student.tests.factories import UserFactory from user_api.models import UserPreference, UserCourseTag +from xmodule.modulestore.locations import SlashSeparatedCourseKey # Factories don't have __init__ methods, and are self documenting # pylint: disable=W0232, C0111 @@ -18,6 +19,6 @@ class UserCourseTagFactory(DjangoModelFactory): FACTORY_FOR = UserCourseTag user = SubFactory(UserFactory) - course_id = 'org/course/run' + course_id = SlashSeparatedCourseKey('org', 'course', 'run') key = None value = None diff --git a/common/djangoapps/user_api/tests/test_user_service.py b/common/djangoapps/user_api/tests/test_user_service.py index f63f702bcbb2c01305032edc1a775a757cf58322..63be81b049d60351cee64e626c38c4b918b00486 100644 --- a/common/djangoapps/user_api/tests/test_user_service.py +++ b/common/djangoapps/user_api/tests/test_user_service.py @@ -5,6 +5,7 @@ from django.test import TestCase from student.tests.factories import UserFactory from user_api import user_service +from xmodule.modulestore.locations import SlashSeparatedCourseKey class TestUserService(TestCase): @@ -13,7 +14,7 @@ class TestUserService(TestCase): """ def setUp(self): self.user = UserFactory.create() - self.course_id = 'test_org/test_course_number/test_run' + self.course_id = SlashSeparatedCourseKey('test_org', 'test_course_number', 'test_run') self.test_key = 'test_key' def test_get_set_course_tag(self): diff --git a/common/djangoapps/util/request.py b/common/djangoapps/util/request.py index 813a3347d37f3493586121907a62b4209b0ca9d4..b3b369cff4ec47e6d29abd9b7385d943901b6f06 100644 --- a/common/djangoapps/util/request.py +++ b/common/djangoapps/util/request.py @@ -3,6 +3,7 @@ import re from django.conf import settings from microsite_configuration import microsite +from xmodule.modulestore.locations import SlashSeparatedCourseKey COURSE_REGEX = re.compile(r'^.*?/courses/(?P<course_id>[^/]+/[^/]+/[^/]+)') @@ -26,11 +27,17 @@ def course_id_from_url(url): """ Extracts the course_id from the given `url`. """ - url = url or '' + if not url: + return None match = COURSE_REGEX.match(url) - course_id = '' - if match: - course_id = match.group('course_id') or '' - return course_id + if match is None: + return None + + course_id = match.group('course_id') + + if course_id is None: + return None + + return SlashSeparatedCourseKey.from_deprecated_string(course_id) diff --git a/common/djangoapps/xmodule_modifiers.py b/common/djangoapps/xmodule_modifiers.py index 77d590777f7306100110208c457ad0538924e48f..be9b98e4ba265680b6c8ad1162e3325514e4b855 100644 --- a/common/djangoapps/xmodule_modifiers.py +++ b/common/djangoapps/xmodule_modifiers.py @@ -18,7 +18,7 @@ from xmodule.vertical_module import VerticalModule from xmodule.x_module import shim_xmodule_js, XModuleDescriptor, XModule from lms.lib.xblock.runtime import quote_slashes from xmodule.modulestore import MONGO_MODULESTORE_TYPE -from xmodule.modulestore.django import modulestore, loc_mapper +from xmodule.modulestore.django import modulestore log = logging.getLogger(__name__) @@ -33,7 +33,7 @@ def wrap_fragment(fragment, new_content): return wrapper_frag -def wrap_xblock(runtime_class, block, view, frag, context, display_name_only=False, extra_data=None): # pylint: disable=unused-argument +def wrap_xblock(runtime_class, block, view, frag, context, usage_id_serializer, display_name_only=False, extra_data=None): # pylint: disable=unused-argument """ Wraps the results of rendering an XBlock view in a standard <section> with identifying data so that the appropriate javascript module can be loaded onto it. @@ -43,6 +43,8 @@ def wrap_xblock(runtime_class, block, view, frag, context, display_name_only=Fal :param view: The name of the view that rendered the fragment being wrapped :param frag: The :class:`Fragment` to be wrapped :param context: The context passed to the view being rendered + :param usage_id_serializer: A function to serialize the block's usage_id for use by the + front-end Javascript Runtime. :param display_name_only: If true, don't render the fragment content at all. Instead, just render the `display_name` of `block` :param extra_data: A dictionary with extra data values to be set on the wrapper @@ -74,13 +76,14 @@ def wrap_xblock(runtime_class, block, view, frag, context, display_name_only=Fal data['runtime-class'] = runtime_class data['runtime-version'] = frag.js_init_version data['block-type'] = block.scope_ids.block_type - data['usage-id'] = quote_slashes(unicode(block.scope_ids.usage_id)) + data['usage-id'] = usage_id_serializer(block.scope_ids.usage_id) template_context = { 'content': block.display_name if display_name_only else frag.content, 'classes': css_classes, 'display_name': block.display_name_with_default, - 'data_attributes': u' '.join(u'data-{}="{}"'.format(key, value) for key, value in data.items()), + 'data_attributes': u' '.join(u'data-{}="{}"'.format(key, value) + for key, value in data.iteritems()), } return wrap_fragment(frag, render_to_string('xblock_wrapper.html', template_context)) @@ -145,7 +148,7 @@ def grade_histogram(module_id): WHERE courseware_studentmodule.module_id=%s GROUP BY courseware_studentmodule.grade""" # Passing module_id this way prevents sql-injection. - cursor.execute(q, [module_id]) + cursor.execute(q, [module_id.to_deprecated_string()]) grades = list(cursor.fetchall()) grades.sort(key=lambda x: x[0]) # Add ORDER BY to sql query? @@ -167,14 +170,14 @@ def add_staff_markup(user, block, view, frag, context): # pylint: disable=unuse # TODO: make this more general, eg use an XModule attribute instead if isinstance(block, VerticalModule): # check that the course is a mongo backed Studio course before doing work - is_mongo_course = modulestore().get_modulestore_type(block.course_id) == MONGO_MODULESTORE_TYPE + is_mongo_course = modulestore().get_modulestore_type(block.location.course_key) == MONGO_MODULESTORE_TYPE is_studio_course = block.course_edit_method == "Studio" if is_studio_course and is_mongo_course: - # get relative url/location of unit in Studio - locator = loc_mapper().translate_location(block.course_id, block.location, False, True) - # build edit link to unit in CMS - edit_link = "//" + settings.CMS_BASE + locator.url_reverse('unit', '') + # build edit link to unit in CMS. Can't use reverse here as lms doesn't load cms's urls.py + # reverse for contentstore.views.unit_handler + edit_link = "//" + settings.CMS_BASE + '/unit/' + unicode(block.location) + # return edit link in rendered HTML for display return wrap_fragment(frag, render_to_string("edit_unit_link.html", {'frag_content': frag.content, 'edit_link': edit_link})) else: @@ -183,7 +186,7 @@ def add_staff_markup(user, block, view, frag, context): # pylint: disable=unuse if isinstance(block, SequenceModule): return frag - block_id = block.id + block_id = block.location if block.has_score and settings.FEATURES.get('DISPLAY_HISTOGRAMS_TO_STAFF'): histogram = grade_histogram(block_id) render_histogram = len(histogram) > 0