diff --git a/lms/djangoapps/analytics/basic.py b/lms/djangoapps/analytics/basic.py index c4682f0594..d812b10a0b 100644 --- a/lms/djangoapps/analytics/basic.py +++ b/lms/djangoapps/analytics/basic.py @@ -70,7 +70,7 @@ def dump_grading_context(course): subgrader.index = 1 graders[subgrader.type] = subgrader msg += hbar - msg += "Listing grading context for course %s\n" % course.id + msg += "Listing grading context for course %s\n" % course.id.to_deprecated_string() gcontext = course.grading_context msg += "graded sections:\n" diff --git a/lms/djangoapps/analytics/tests/test_basic.py b/lms/djangoapps/analytics/tests/test_basic.py index 91d6ed45e9..8629aaf294 100644 --- a/lms/djangoapps/analytics/tests/test_basic.py +++ b/lms/djangoapps/analytics/tests/test_basic.py @@ -5,6 +5,7 @@ Tests for instructor.basic from django.test import TestCase from student.models import CourseEnrollment from student.tests.factories import UserFactory +from xmodule.modulestore.locations import SlashSeparatedCourseKey from analytics.basic import enrolled_students_features, AVAILABLE_FEATURES, STUDENT_FEATURES, PROFILE_FEATURES @@ -13,14 +14,14 @@ class TestAnalyticsBasic(TestCase): """ Test basic analytics functions. """ def setUp(self): - self.course_id = 'some/robot/course/id' + self.course_key = SlashSeparatedCourseKey('robot', 'course', 'id') self.users = tuple(UserFactory() for _ in xrange(30)) - self.ces = tuple(CourseEnrollment.enroll(user, self.course_id) + self.ces = tuple(CourseEnrollment.enroll(user, self.course_key) for user in self.users) def test_enrolled_students_features_username(self): self.assertIn('username', AVAILABLE_FEATURES) - userreports = enrolled_students_features(self.course_id, ['username']) + userreports = enrolled_students_features(self.course_key, ['username']) self.assertEqual(len(userreports), len(self.users)) for userreport in userreports: self.assertEqual(userreport.keys(), ['username']) @@ -30,7 +31,7 @@ class TestAnalyticsBasic(TestCase): query_features = ('username', 'name', 'email') for feature in query_features: self.assertIn(feature, AVAILABLE_FEATURES) - userreports = enrolled_students_features(self.course_id, query_features) + userreports = enrolled_students_features(self.course_key, query_features) self.assertEqual(len(userreports), len(self.users)) for userreport in userreports: self.assertEqual(set(userreport.keys()), set(query_features)) diff --git a/lms/djangoapps/analytics/tests/test_distributions.py b/lms/djangoapps/analytics/tests/test_distributions.py index 6d314e4a49..1e2f588758 100644 --- a/lms/djangoapps/analytics/tests/test_distributions.py +++ b/lms/djangoapps/analytics/tests/test_distributions.py @@ -4,6 +4,7 @@ from django.test import TestCase from nose.tools import raises from student.models import CourseEnrollment from student.tests.factories import UserFactory +from xmodule.modulestore.locations import SlashSeparatedCourseKey from analytics.distributions import profile_distribution, AVAILABLE_PROFILE_FEATURES @@ -12,7 +13,7 @@ class TestAnalyticsDistributions(TestCase): '''Test analytics distribution gathering.''' def setUp(self): - self.course_id = 'some/robot/course/id' + self.course_id = SlashSeparatedCourseKey('robot', 'course', 'id') self.users = [UserFactory( profile__gender=['m', 'f', 'o'][i % 3], @@ -53,7 +54,7 @@ class TestAnalyticsDistributionsNoData(TestCase): '''Test analytics distribution gathering.''' def setUp(self): - self.course_id = 'some/robot/course/id' + self.course_id = SlashSeparatedCourseKey('robot', 'course', 'id') self.users = [UserFactory( profile__year_of_birth=i + 1930, diff --git a/lms/djangoapps/bulk_email/forms.py b/lms/djangoapps/bulk_email/forms.py index bc68cf5aa0..92c47cdcb0 100644 --- a/lms/djangoapps/bulk_email/forms.py +++ b/lms/djangoapps/bulk_email/forms.py @@ -8,9 +8,10 @@ from django.core.exceptions import ValidationError from bulk_email.models import CourseEmailTemplate, COURSE_EMAIL_MESSAGE_BODY_TAG, CourseAuthorization -from courseware.courses import get_course_by_id +from opaque_keys import InvalidKeyError from xmodule.modulestore import XML_MODULESTORE_TYPE from xmodule.modulestore.django import modulestore +from xmodule.modulestore.locations import SlashSeparatedCourseKey log = logging.getLogger(__name__) @@ -57,22 +58,26 @@ class CourseAuthorizationAdminForm(forms.ModelForm): # pylint: disable=R0924 def clean_course_id(self): """Validate the course id""" - course_id = self.cleaned_data["course_id"] + cleaned_id = self.cleaned_data["course_id"] try: - # Just try to get the course descriptor. - # If we can do that, it's a real course. - get_course_by_id(course_id, depth=1) - except Exception as exc: - msg = 'Error encountered ({0})'.format(str(exc).capitalize()) - msg += u' --- Entered course id was: "{0}". '.format(course_id) - msg += 'Please recheck that you have supplied a course id in the format: ORG/COURSE/RUN' + course_id = SlashSeparatedCourseKey.from_deprecated_string(cleaned_id) + except InvalidKeyError: + msg = u'Course id invalid.' + 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) + + if not modulestore().has_course(course_id): + msg = u'COURSE NOT FOUND' + 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) # Now, try and discern if it is a Studio course - HTML editor doesn't work with XML courses is_studio_course = modulestore().get_modulestore_type(course_id) != XML_MODULESTORE_TYPE if not is_studio_course: msg = "Course Email feature is only available for courses authored in Studio. " - msg += '"{0}" appears to be an XML backed course.'.format(course_id) + msg += '"{0}" appears to be an XML backed course.'.format(course_id.to_deprecated_string()) raise forms.ValidationError(msg) - return course_id + return course_id.to_deprecated_string() diff --git a/lms/djangoapps/bulk_email/models.py b/lms/djangoapps/bulk_email/models.py index 9580703ef2..f7b382c77a 100644 --- a/lms/djangoapps/bulk_email/models.py +++ b/lms/djangoapps/bulk_email/models.py @@ -19,6 +19,8 @@ from django.db import models, transaction from html_to_text import html_to_text from mail_utils import wrap_message +from xmodule_django.models import CourseKeyField + log = logging.getLogger(__name__) # Bulk email to_options - the send to options that users can @@ -63,7 +65,7 @@ class CourseEmail(Email): (SEND_TO_STAFF, 'Staff and instructors'), (SEND_TO_ALL, 'All') ) - course_id = models.CharField(max_length=255, db_index=True) + course_id = CourseKeyField(max_length=255, db_index=True) to_option = models.CharField(max_length=64, choices=TO_OPTION_CHOICES, default=SEND_TO_MYSELF) def __unicode__(self): @@ -127,7 +129,7 @@ class Optout(models.Model): # We need to first create the 'user' column with some sort of default in order to run the data migration, # and given the unique index, 'null' is the best default value. user = models.ForeignKey(User, db_index=True, null=True) - course_id = models.CharField(max_length=255, db_index=True) + course_id = CourseKeyField(max_length=255, db_index=True) class Meta: # pylint: disable=C0111 unique_together = ('user', 'course_id') @@ -220,7 +222,7 @@ class CourseAuthorization(models.Model): Enable the course email feature on a course-by-course basis. """ # The course that these features are attached to. - 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 enable instructor email email_enabled = models.BooleanField(default=False) @@ -247,4 +249,5 @@ class CourseAuthorization(models.Model): not_en = "Not " if self.email_enabled: not_en = "" - return u"Course '{}': Instructor Email {}Enabled".format(self.course_id, not_en) + # pylint: disable=no-member + return u"Course '{}': Instructor Email {}Enabled".format(self.course_id.to_deprecated_string(), not_en) diff --git a/lms/djangoapps/bulk_email/tasks.py b/lms/djangoapps/bulk_email/tasks.py index 530b8e12c2..8debf9eeb0 100644 --- a/lms/djangoapps/bulk_email/tasks.py +++ b/lms/djangoapps/bulk_email/tasks.py @@ -107,8 +107,8 @@ def _get_recipient_queryset(user_id, to_option, course_id, course_location): if to_option == SEND_TO_MYSELF: recipient_qset = User.objects.filter(id=user_id) else: - staff_qset = CourseStaffRole(course_location).users_with_role() - instructor_qset = CourseInstructorRole(course_location).users_with_role() + staff_qset = CourseStaffRole(course_id).users_with_role() + instructor_qset = CourseInstructorRole(course_id).users_with_role() recipient_qset = staff_qset | instructor_qset if to_option == SEND_TO_ALL: # We also require students to have activated their accounts to @@ -129,7 +129,7 @@ def _get_course_email_context(course): """ Returns context arguments to apply to all emails, independent of recipient. """ - course_id = course.id + course_id = course.id.to_deprecated_string() course_title = course.display_name course_url = 'https://{}{}'.format( settings.SITE_NAME, @@ -160,9 +160,9 @@ def perform_delegate_email_batches(entry_id, course_id, task_input, action_name) # Perfunctory check, since expansion is made for convenience of other task # code that doesn't need the entry_id. if course_id != entry.course_id: - format_msg = u"Course id conflict: explicit value {} does not match task value {}" - log.warning("Task %s: %s", task_id, format_msg.format(course_id, entry.course_id)) - raise ValueError("Course id conflict: explicit value does not match task value") + format_msg = u"Course id conflict: explicit value %r does not match task value %r" + log.warning("Task %s: " + format_msg, task_id, course_id, entry.course_id) + raise ValueError(format_msg % (course_id, entry.course_id)) # Fetch the CourseEmail. email_id = task_input['email_id'] @@ -188,16 +188,17 @@ def perform_delegate_email_batches(entry_id, course_id, task_input, action_name) # Sanity check that course for email_obj matches that of the task referencing it. if course_id != email_obj.course_id: - format_msg = u"Course id conflict: explicit value {} does not match email value {}" - log.warning("Task %s: %s", task_id, format_msg.format(course_id, entry.course_id)) - raise ValueError("Course id conflict: explicit value does not match email value") + format_msg = u"Course id conflict: explicit value %r does not match email value %r" + log.warning("Task %s: " + format_msg, task_id, course_id, email_obj.course_id) + raise ValueError(format_msg % (course_id, email_obj.course_id)) # Fetch the course object. - try: - course = get_course(course_id) - except ValueError: - log.exception("Task %s: course not found: %s", task_id, course_id) - raise + course = get_course(course_id) + + if course is None: + msg = "Task %s: course not found: %s" + log.error(msg, task_id, course_id) + raise ValueError(msg % (task_id, course_id)) # Get arguments that will be passed to every subtask. to_option = email_obj.to_option @@ -369,15 +370,14 @@ def _get_source_address(course_id, course_title): """ course_title_no_quotes = re.sub(r'"', '', course_title) - # The course_id is assumed to be in the form 'org/course_num/run', - # so pull out the course_num. Then make sure that it can be used + # For the email address, get the course. Then make sure that it can be used # in an email address, by substituting a '_' anywhere a non-(ascii, period, or dash) # character appears. - course_num = Location.parse_course_id(course_id)['course'] - invalid_chars = re.compile(r"[^\w.-]") - course_num = invalid_chars.sub('_', course_num) - - from_addr = u'"{0}" Course Staff <{1}-{2}>'.format(course_title_no_quotes, course_num, settings.BULK_EMAIL_DEFAULT_FROM_EMAIL) + from_addr = u'"{0}" Course Staff <{1}-{2}>'.format( + course_title_no_quotes, + re.sub(r"[^\w.-]", '_', course_id.course), + settings.BULK_EMAIL_DEFAULT_FROM_EMAIL + ) return from_addr diff --git a/lms/djangoapps/bulk_email/tests/test_course_optout.py b/lms/djangoapps/bulk_email/tests/test_course_optout.py index 5cd54686b1..e5ab357004 100644 --- a/lms/djangoapps/bulk_email/tests/test_course_optout.py +++ b/lms/djangoapps/bulk_email/tests/test_course_optout.py @@ -47,7 +47,7 @@ class TestOptoutCourseEmails(ModuleStoreTestCase): def navigate_to_email_view(self): """Navigate to the instructor dash's email view""" # Pull up email view on instructor dashboard - url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id}) + url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url) email_link = 'Email' # If this fails, it is likely because ENABLE_INSTRUCTOR_EMAIL is set to False @@ -69,7 +69,7 @@ class TestOptoutCourseEmails(ModuleStoreTestCase): url = reverse('change_email_settings') # This is a checkbox, so on the post of opting out (that is, an Un-check of the box), # the Post that is sent will not contain 'receive_emails' - response = self.client.post(url, {'course_id': self.course.id}) + response = self.client.post(url, {'course_id': self.course.id.to_deprecated_string()}) self.assertEquals(json.loads(response.content), {'success': True}) self.client.logout() @@ -77,7 +77,7 @@ class TestOptoutCourseEmails(ModuleStoreTestCase): self.client.login(username=self.instructor.username, password="test") self.navigate_to_email_view() - url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id}) + url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id.to_deprecated_string()}) test_email = { 'action': 'Send email', 'to_option': 'all', @@ -96,7 +96,7 @@ class TestOptoutCourseEmails(ModuleStoreTestCase): Make sure student receives course email after opting in. """ url = reverse('change_email_settings') - response = self.client.post(url, {'course_id': self.course.id, 'receive_emails': 'on'}) + response = self.client.post(url, {'course_id': self.course.id.to_deprecated_string(), 'receive_emails': 'on'}) self.assertEquals(json.loads(response.content), {'success': True}) self.client.logout() @@ -106,7 +106,7 @@ class TestOptoutCourseEmails(ModuleStoreTestCase): self.client.login(username=self.instructor.username, password="test") self.navigate_to_email_view() - url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id}) + url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id.to_deprecated_string()}) test_email = { 'action': 'Send email', 'to_option': 'all', diff --git a/lms/djangoapps/bulk_email/tests/test_email.py b/lms/djangoapps/bulk_email/tests/test_email.py index c96727dfcf..e4b2b2502a 100644 --- a/lms/djangoapps/bulk_email/tests/test_email.py +++ b/lms/djangoapps/bulk_email/tests/test_email.py @@ -51,10 +51,10 @@ class TestEmailSendFromDashboard(ModuleStoreTestCase): course_title = u"ẗëṡẗ title イ乇丂イ ᄊ乇丂丂ムg乇 キo尺 ムレレ тэѕт мэѕѕаБэ" self.course = CourseFactory.create(display_name=course_title) - self.instructor = InstructorFactory(course=self.course.location) + self.instructor = InstructorFactory(course=self.course.id) # Create staff - self.staff = [StaffFactory(course=self.course.location) + self.staff = [StaffFactory(course=self.course.id) for _ in xrange(STAFF_COUNT)] # Create students @@ -68,7 +68,7 @@ class TestEmailSendFromDashboard(ModuleStoreTestCase): self.client.login(username=self.instructor.username, password="test") # Pull up email view on instructor dashboard - self.url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id}) + self.url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(self.url) email_link = 'Email' # If this fails, it is likely because ENABLE_INSTRUCTOR_EMAIL is set to False diff --git a/lms/djangoapps/bulk_email/tests/test_err_handling.py b/lms/djangoapps/bulk_email/tests/test_err_handling.py index f9365c86ef..27a9fa4e10 100644 --- a/lms/djangoapps/bulk_email/tests/test_err_handling.py +++ b/lms/djangoapps/bulk_email/tests/test_err_handling.py @@ -17,6 +17,7 @@ from django.db import DatabaseError from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory +from xmodule.modulestore.locations import SlashSeparatedCourseKey from student.tests.factories import UserFactory, AdminFactory, CourseEnrollmentFactory from bulk_email.models import CourseEmail, SEND_TO_ALL @@ -51,7 +52,7 @@ class TestEmailErrors(ModuleStoreTestCase): # load initial content (since we don't run migrations as part of tests): call_command("loaddata", "course_email_template.json") - self.url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id}) + self.url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id.to_deprecated_string()}) def tearDown(self): patch.stopall() @@ -171,12 +172,13 @@ class TestEmailErrors(ModuleStoreTestCase): """ Tests exception when the course in the email doesn't exist """ - course_id = "I/DONT/EXIST" + course_id = SlashSeparatedCourseKey("I", "DONT", "EXIST") email = CourseEmail(course_id=course_id) email.save() entry = InstructorTask.create(course_id, "task_type", "task_key", "task_input", self.instructor) task_input = {"email_id": email.id} # pylint: disable=E1101 - with self.assertRaisesRegexp(ValueError, "Course not found"): + # (?i) is a regex for ignore case + with self.assertRaisesRegexp(ValueError, r"(?i)course not found"): perform_delegate_email_batches(entry.id, course_id, task_input, "action_name") # pylint: disable=E1101 def test_nonexistent_to_option(self): @@ -205,7 +207,7 @@ class TestEmailErrors(ModuleStoreTestCase): """ Tests exception when the course_id in CourseEmail is not the same as one explicitly passed in. """ - email = CourseEmail(course_id="bogus_course_id", to_option=SEND_TO_ALL) + email = CourseEmail(course_id=SlashSeparatedCourseKey("bogus", "course", "id"), to_option=SEND_TO_ALL) email.save() entry = InstructorTask.create(self.course.id, "task_type", "task_key", "task_input", self.instructor) task_input = {"email_id": email.id} # pylint: disable=E1101 diff --git a/lms/djangoapps/bulk_email/tests/test_forms.py b/lms/djangoapps/bulk_email/tests/test_forms.py index cd823a1a41..025c433e72 100644 --- a/lms/djangoapps/bulk_email/tests/test_forms.py +++ b/lms/djangoapps/bulk_email/tests/test_forms.py @@ -11,12 +11,13 @@ from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE from xmodule.modulestore.django import modulestore -from xmodule.modulestore import XML_MODULESTORE_TYPE, Location +from xmodule.modulestore import XML_MODULESTORE_TYPE from mock import patch from bulk_email.models import CourseAuthorization from bulk_email.forms import CourseAuthorizationAdminForm +from xmodule.modulestore.locations import SlashSeparatedCourseKey @override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) @@ -38,7 +39,7 @@ class CourseAuthorizationFormTest(ModuleStoreTestCase): # Initially course shouldn't be authorized self.assertFalse(CourseAuthorization.instructor_email_enabled(self.course.id)) # Test authorizing the course, which should totally work - form_data = {'course_id': self.course.id, 'email_enabled': True} + form_data = {'course_id': self.course.id.to_deprecated_string(), 'email_enabled': True} form = CourseAuthorizationAdminForm(data=form_data) # Validation should work self.assertTrue(form.is_valid()) @@ -51,7 +52,7 @@ class CourseAuthorizationFormTest(ModuleStoreTestCase): # Initially course shouldn't be authorized self.assertFalse(CourseAuthorization.instructor_email_enabled(self.course.id)) # Test authorizing the course, which should totally work - form_data = {'course_id': self.course.id, 'email_enabled': True} + form_data = {'course_id': self.course.id.to_deprecated_string(), 'email_enabled': True} form = CourseAuthorizationAdminForm(data=form_data) # Validation should work self.assertTrue(form.is_valid()) @@ -60,7 +61,7 @@ class CourseAuthorizationFormTest(ModuleStoreTestCase): self.assertTrue(CourseAuthorization.instructor_email_enabled(self.course.id)) # Now make a new course authorization with the same course id that tries to turn email off - form_data = {'course_id': self.course.id, 'email_enabled': False} + form_data = {'course_id': self.course.id.to_deprecated_string(), 'email_enabled': False} form = CourseAuthorizationAdminForm(data=form_data) # Validation should not work because course_id field is unique self.assertFalse(form.is_valid()) @@ -77,16 +78,31 @@ class CourseAuthorizationFormTest(ModuleStoreTestCase): @patch.dict(settings.FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True, 'REQUIRE_COURSE_EMAIL_AUTH': True}) def test_form_typo(self): # Munge course id - bad_id = self.course.id + '_typo' + bad_id = SlashSeparatedCourseKey(u'Broken{}'.format(self.course.id.org), '', self.course.id.run + '_typo') - form_data = {'course_id': bad_id, 'email_enabled': True} + form_data = {'course_id': bad_id.to_deprecated_string(), 'email_enabled': True} form = CourseAuthorizationAdminForm(data=form_data) # Validation shouldn't work self.assertFalse(form.is_valid()) - msg = u'Error encountered (Course not found.)' - msg += u' --- Entered course id was: "{0}". '.format(bad_id) - msg += 'Please recheck that you have supplied a course id in the format: ORG/COURSE/RUN' + msg = u'COURSE NOT FOUND' + msg += u' --- Entered course id was: "{0}". '.format(bad_id.to_deprecated_string()) + msg += 'Please recheck that you have supplied a valid course id.' + self.assertEquals(msg, form._errors['course_id'][0]) # pylint: disable=protected-access + + with self.assertRaisesRegexp(ValueError, "The CourseAuthorization could not be created because the data didn't validate."): + form.save() + + @patch.dict(settings.FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True, 'REQUIRE_COURSE_EMAIL_AUTH': True}) + def test_form_invalid_key(self): + form_data = {'course_id': "asd::**!@#$%^&*())//foobar!!", 'email_enabled': True} + form = CourseAuthorizationAdminForm(data=form_data) + # Validation shouldn't work + self.assertFalse(form.is_valid()) + + msg = u'Course id invalid.' + msg += u' --- Entered course id was: "asd::**!@#$%^&*())//foobar!!". ' + msg += 'Please recheck that you have supplied a valid course id.' self.assertEquals(msg, form._errors['course_id'][0]) # pylint: disable=protected-access with self.assertRaisesRegexp(ValueError, "The CourseAuthorization could not be created because the data didn't validate."): @@ -95,16 +111,14 @@ class CourseAuthorizationFormTest(ModuleStoreTestCase): @patch.dict(settings.FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True, 'REQUIRE_COURSE_EMAIL_AUTH': True}) def test_course_name_only(self): # Munge course id - common - bad_id = Location.parse_course_id(self.course.id)['name'] - - form_data = {'course_id': bad_id, 'email_enabled': True} + form_data = {'course_id': self.course.id.run, 'email_enabled': True} form = CourseAuthorizationAdminForm(data=form_data) # Validation shouldn't work self.assertFalse(form.is_valid()) error_msg = form._errors['course_id'][0] - self.assertIn(u'--- Entered course id was: "{0}". '.format(bad_id), error_msg) - self.assertIn(u'Please recheck that you have supplied a course id in the format: ORG/COURSE/RUN', error_msg) + self.assertIn(u'--- Entered course id was: "{0}". '.format(self.course.id.run), error_msg) + self.assertIn(u'Please recheck that you have supplied a valid course id.', error_msg) with self.assertRaisesRegexp(ValueError, "The CourseAuthorization could not be created because the data didn't validate."): form.save() @@ -116,17 +130,17 @@ class CourseAuthorizationXMLFormTest(ModuleStoreTestCase): @patch.dict(settings.FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True, 'REQUIRE_COURSE_EMAIL_AUTH': True}) def test_xml_course_authorization(self): - course_id = 'edX/toy/2012_Fall' + course_id = SlashSeparatedCourseKey('edX', 'toy', '2012_Fall') # Assert this is an XML course self.assertEqual(modulestore().get_modulestore_type(course_id), XML_MODULESTORE_TYPE) - form_data = {'course_id': course_id, 'email_enabled': True} + form_data = {'course_id': course_id.to_deprecated_string(), 'email_enabled': True} form = CourseAuthorizationAdminForm(data=form_data) # Validation shouldn't work self.assertFalse(form.is_valid()) msg = u"Course Email feature is only available for courses authored in Studio. " - msg += u'"{0}" appears to be an XML backed course.'.format(course_id) + msg += u'"{0}" appears to be an XML backed course.'.format(course_id.to_deprecated_string()) self.assertEquals(msg, form._errors['course_id'][0]) # pylint: disable=protected-access with self.assertRaisesRegexp(ValueError, "The CourseAuthorization could not be created because the data didn't validate."): diff --git a/lms/djangoapps/bulk_email/tests/test_models.py b/lms/djangoapps/bulk_email/tests/test_models.py index f812972fc6..c0a03f1ade 100644 --- a/lms/djangoapps/bulk_email/tests/test_models.py +++ b/lms/djangoapps/bulk_email/tests/test_models.py @@ -10,13 +10,14 @@ from student.tests.factories import UserFactory from mock import patch from bulk_email.models import CourseEmail, SEND_TO_STAFF, CourseEmailTemplate, CourseAuthorization +from xmodule.modulestore.locations import SlashSeparatedCourseKey class CourseEmailTest(TestCase): """Test the CourseEmail model.""" def test_creation(self): - course_id = 'abc/123/doremi' + course_id = SlashSeparatedCourseKey('abc', '123', 'doremi') sender = UserFactory.create() to_option = SEND_TO_STAFF subject = "dummy subject" @@ -29,7 +30,7 @@ class CourseEmailTest(TestCase): self.assertEquals(email.sender, sender) def test_bad_to_option(self): - course_id = 'abc/123/doremi' + course_id = SlashSeparatedCourseKey('abc', '123', 'doremi') sender = UserFactory.create() to_option = "fake" subject = "dummy subject" @@ -109,7 +110,7 @@ class CourseAuthorizationTest(TestCase): @patch.dict(settings.FEATURES, {'REQUIRE_COURSE_EMAIL_AUTH': True}) def test_creation_auth_on(self): - course_id = 'abc/123/doremi' + course_id = SlashSeparatedCourseKey('abc', '123', 'doremi') # Test that course is not authorized by default self.assertFalse(CourseAuthorization.instructor_email_enabled(course_id)) @@ -135,7 +136,7 @@ class CourseAuthorizationTest(TestCase): @patch.dict(settings.FEATURES, {'REQUIRE_COURSE_EMAIL_AUTH': False}) def test_creation_auth_off(self): - course_id = 'blahx/blah101/ehhhhhhh' + course_id = SlashSeparatedCourseKey('blahx', 'blah101', 'ehhhhhhh') # Test that course is authorized by default, since auth is turned off self.assertTrue(CourseAuthorization.instructor_email_enabled(course_id)) diff --git a/lms/djangoapps/bulk_email/tests/test_tasks.py b/lms/djangoapps/bulk_email/tests/test_tasks.py index b964f1cb23..872781f14c 100644 --- a/lms/djangoapps/bulk_email/tests/test_tasks.py +++ b/lms/djangoapps/bulk_email/tests/test_tasks.py @@ -35,6 +35,7 @@ from instructor_task.subtasks import update_subtask_status, SubtaskStatus from instructor_task.models import InstructorTask from instructor_task.tests.test_base import InstructorTaskCourseTestCase from instructor_task.tests.factories import InstructorTaskFactory +from xmodule.modulestore.locations import SlashSeparatedCourseKey class TestTaskFailure(Exception): @@ -119,7 +120,7 @@ class TestBulkEmailInstructorTask(InstructorTaskCourseTestCase): def test_email_undefined_course(self): # Check that we fail when passing in a course that doesn't exist. - task_entry = self._create_input_entry(course_id="bogus/course/id") + task_entry = self._create_input_entry(course_id=SlashSeparatedCourseKey("bogus", "course", "id")) with self.assertRaises(ValueError): self._run_task_with_mock_celery(send_bulk_course_email, task_entry.id, task_entry.task_id) diff --git a/lms/djangoapps/certificates/management/commands/cert_whitelist.py b/lms/djangoapps/certificates/management/commands/cert_whitelist.py index ce9e5699ab..0aa7d81ab7 100644 --- a/lms/djangoapps/certificates/management/commands/cert_whitelist.py +++ b/lms/djangoapps/certificates/management/commands/cert_whitelist.py @@ -1,5 +1,8 @@ from django.core.management.base import BaseCommand, CommandError from optparse import make_option +from opaque_keys import InvalidKeyError +from xmodule.modulestore.keys import CourseKey +from xmodule.modulestore.locations import SlashSeparatedCourseKey from certificates.models import CertificateWhitelist from django.contrib.auth.models import User @@ -48,6 +51,14 @@ class Command(BaseCommand): course_id = options['course_id'] if not course_id: raise CommandError("You must specify a course-id") + + # try to parse the serialized course key into a CourseKey + try: + course = CourseKey.from_string(course_id) + except InvalidKeyError: + log.warning("Course id %s could not be parsed as a CourseKey; falling back to SSCK.from_dep_str", course_id) + course = SlashSeparatedCourseKey.from_deprecated_string(course_id) + if options['add'] and options['del']: raise CommandError("Either remove or add a user, not both") @@ -60,14 +71,14 @@ class Command(BaseCommand): cert_whitelist, created = \ CertificateWhitelist.objects.get_or_create( - user=user, course_id=course_id) + user=user, course_id=course) if options['add']: cert_whitelist.whitelist = True elif options['del']: cert_whitelist.whitelist = False cert_whitelist.save() - whitelist = CertificateWhitelist.objects.filter(course_id=course_id) + whitelist = CertificateWhitelist.objects.filter(course_id=course) print "User whitelist for course {0}:\n{1}".format(course_id, '\n'.join(["{0} {1} {2}".format( u.user.username, u.user.email, u.whitelist) diff --git a/lms/djangoapps/certificates/management/commands/gen_cert_report.py b/lms/djangoapps/certificates/management/commands/gen_cert_report.py index 505552de29..951f20d8a2 100644 --- a/lms/djangoapps/certificates/management/commands/gen_cert_report.py +++ b/lms/djangoapps/certificates/management/commands/gen_cert_report.py @@ -40,8 +40,7 @@ class Command(BaseCommand): for course_id in [course # all courses in COURSE_LISTINGS for sub in settings.COURSE_LISTINGS for course in settings.COURSE_LISTINGS[sub]]: - course_loc = CourseDescriptor.id_to_location(course_id) - course = modulestore().get_instance(course_id, course_loc) + course = modulestore().get_course(course_id) if course.has_ended(): yield course_id diff --git a/lms/djangoapps/certificates/management/commands/regenerate_user.py b/lms/djangoapps/certificates/management/commands/regenerate_user.py index 325bb3dfb8..d1d5fbc4b5 100644 --- a/lms/djangoapps/certificates/management/commands/regenerate_user.py +++ b/lms/djangoapps/certificates/management/commands/regenerate_user.py @@ -62,7 +62,7 @@ class Command(BaseCommand): student = User.objects.get(username=user, courseenrollment__course_id=course_id) print "Fetching course data for {0}".format(course_id) - course = modulestore().get_instance(course_id, CourseDescriptor.id_to_location(course_id), depth=2) + course = modulestore().get_course(course_id, depth=2) if not options['noop']: # Add the certificate request to the queue diff --git a/lms/djangoapps/certificates/management/commands/ungenerated_certs.py b/lms/djangoapps/certificates/management/commands/ungenerated_certs.py index 728a7d6489..0a86c102c2 100644 --- a/lms/djangoapps/certificates/management/commands/ungenerated_certs.py +++ b/lms/djangoapps/certificates/management/commands/ungenerated_certs.py @@ -1,9 +1,12 @@ -from django.core.management.base import BaseCommand +from django.core.management.base import BaseCommand, CommandError from certificates.models import certificate_status_for_student from certificates.queue import XQueueCertInterface from django.contrib.auth.models import User from optparse import make_option from django.conf import settings +from opaque_keys import InvalidKeyError +from xmodule.modulestore.keys import CourseKey +from xmodule.modulestore.locations import SlashSeparatedCourseKey from xmodule.course_module import CourseDescriptor from xmodule.modulestore.django import modulestore from certificates.models import CertificateStatuses @@ -66,25 +69,23 @@ class Command(BaseCommand): STATUS_INTERVAL = 500 if options['course']: - ended_courses = [options['course']] + # try to parse out the course from the serialized form + try: + course = CourseKey.from_string(options['course']) + except InvalidKeyError: + log.warning("Course id %s could not be parsed as a CourseKey; falling back to SSCK.from_dep_str", course_id) + course = SlashSeparatedCourseKey.from_deprecated_string(options['course']) + ended_courses = [course] else: - # Find all courses that have ended - ended_courses = [] - for course_id in [course # all courses in COURSE_LISTINGS - for sub in settings.COURSE_LISTINGS - for course in settings.COURSE_LISTINGS[sub]]: - course_loc = CourseDescriptor.id_to_location(course_id) - course = modulestore().get_instance(course_id, course_loc) - if course.has_ended(): - ended_courses.append(course_id) + raise CommandError("You must specify a course") - for course_id in ended_courses: + for course_key in ended_courses: # prefetch all chapters/sequentials by saying depth=2 - course = modulestore().get_instance(course_id, CourseDescriptor.id_to_location(course_id), depth=2) + course = modulestore().get_course(course_key, depth=2) - print "Fetching enrolled students for {0}".format(course_id) + print "Fetching enrolled students for {0}".format(course_key.to_deprecated_string()) enrolled_students = User.objects.filter( - courseenrollment__course_id=course_id) + courseenrollment__course_id=course_key) xq = XQueueCertInterface() if options['insecure']: @@ -108,9 +109,9 @@ class Command(BaseCommand): start = datetime.datetime.now(UTC) if certificate_status_for_student( - student, course_id)['status'] in valid_statuses: + student, course_key)['status'] in valid_statuses: if not options['noop']: # Add the certificate request to the queue - ret = xq.add_cert(student, course_id, course=course) + ret = xq.add_cert(student, course_key, course=course) if ret == 'generating': print '{0} - {1}'.format(student, ret) diff --git a/lms/djangoapps/certificates/models.py b/lms/djangoapps/certificates/models.py index eb6ca407a0..71ab9ffcf4 100644 --- a/lms/djangoapps/certificates/models.py +++ b/lms/djangoapps/certificates/models.py @@ -2,6 +2,7 @@ from django.contrib.auth.models import User from django.db import models from datetime import datetime from model_utils import Choices +from xmodule_django.models import CourseKeyField, NoneToEmptyManager """ Certificates are created for a student and an offering of a course. @@ -71,14 +72,17 @@ class CertificateWhitelist(models.Model): embargoed country restriction list (allow_certificate set to False in userprofile). """ + + objects = NoneToEmptyManager() + user = models.ForeignKey(User) - course_id = models.CharField(max_length=255, blank=True, default='') + course_id = CourseKeyField(max_length=255, blank=True, default=None) whitelist = models.BooleanField(default=0) class GeneratedCertificate(models.Model): user = models.ForeignKey(User) - course_id = models.CharField(max_length=255, blank=True, default='') + course_id = CourseKeyField(max_length=255, blank=True, default=None) verify_uuid = models.CharField(max_length=32, blank=True, default='') download_uuid = models.CharField(max_length=32, blank=True, default='') download_url = models.CharField(max_length=128, blank=True, default='') diff --git a/lms/djangoapps/certificates/queue.py b/lms/djangoapps/certificates/queue.py index f593d58fb3..06267758fd 100644 --- a/lms/djangoapps/certificates/queue.py +++ b/lms/djangoapps/certificates/queue.py @@ -130,7 +130,7 @@ class XQueueCertInterface(object): Arguments: student - User.object - course_id - courseenrollment.course_id (string) + course_id - courseenrollment.course_id (CourseKey) forced_grade - a string indicating a grade parameter to pass with the certificate request. If this is given, grading will be skipped. @@ -181,16 +181,15 @@ class XQueueCertInterface(object): mode_is_verified = (enrollment_mode == GeneratedCertificate.MODES.verified) user_is_verified = SoftwareSecurePhotoVerification.user_is_verified(student) user_is_reverified = SoftwareSecurePhotoVerification.user_is_reverified_for_all(course_id, student) - course_id_dict = Location.parse_course_id(course_id) cert_mode = enrollment_mode if (mode_is_verified and user_is_verified and user_is_reverified): - template_pdf = "certificate-template-{org}-{course}-verified.pdf".format(**course_id_dict) + template_pdf = "certificate-template-{id.org}-{id.course}-verified.pdf".format(id=course_id) elif (mode_is_verified and not (user_is_verified and user_is_reverified)): - template_pdf = "certificate-template-{org}-{course}.pdf".format(**course_id_dict) + template_pdf = "certificate-template-{id.org}-{id.course}.pdf".format(id=course_id) cert_mode = GeneratedCertificate.MODES.honor else: # honor code and audit students - template_pdf = "certificate-template-{org}-{course}.pdf".format(**course_id_dict) + template_pdf = "certificate-template-{id.org}-{id.course}.pdf".format(id=course_id) if forced_grade: grade['grade'] = forced_grade @@ -219,7 +218,7 @@ class XQueueCertInterface(object): contents = { 'action': 'create', 'username': student.username, - 'course_id': course_id, + 'course_id': course_id.to_deprecated_string(), 'name': profile.name, 'grade': grade['grade'], 'template_pdf': template_pdf, diff --git a/lms/djangoapps/certificates/views.py b/lms/djangoapps/certificates/views.py index 11f567a58f..993aad50a5 100644 --- a/lms/djangoapps/certificates/views.py +++ b/lms/djangoapps/certificates/views.py @@ -5,6 +5,7 @@ from django.views.decorators.csrf import csrf_exempt from django.http import HttpResponse import json from dogapi import dog_stats_api +from xmodule.modulestore.locations import SlashSeparatedCourseKey from capa.xqueue_interface import XQUEUE_METRIC_NAME logger = logging.getLogger(__name__) @@ -27,21 +28,23 @@ def update_certificate(request): xqueue_header = json.loads(request.POST.get('xqueue_header')) try: + course_key = SlashSeparatedCourseKey.from_deprecated_string(xqueue_body['course_id']) + cert = GeneratedCertificate.objects.get( - user__username=xqueue_body['username'], - course_id=xqueue_body['course_id'], - key=xqueue_header['lms_key']) + user__username=xqueue_body['username'], + course_id=course_key, + key=xqueue_header['lms_key']) except GeneratedCertificate.DoesNotExist: logger.critical('Unable to lookup certificate\n' - 'xqueue_body: {0}\n' - 'xqueue_header: {1}'.format( - xqueue_body, xqueue_header)) + 'xqueue_body: {0}\n' + 'xqueue_header: {1}'.format( + xqueue_body, xqueue_header)) return HttpResponse(json.dumps({ - 'return_code': 1, - 'content': 'unable to lookup key'}), - mimetype='application/json') + 'return_code': 1, + 'content': 'unable to lookup key'}), + mimetype='application/json') if 'error' in xqueue_body: cert.status = status.error diff --git a/lms/djangoapps/class_dashboard/dashboard_data.py b/lms/djangoapps/class_dashboard/dashboard_data.py index 209d647faf..a5c9322df5 100644 --- a/lms/djangoapps/class_dashboard/dashboard_data.py +++ b/lms/djangoapps/class_dashboard/dashboard_data.py @@ -7,11 +7,12 @@ from courseware import models from django.db.models import Count from django.utils.translation import ugettext as _ -from xmodule.course_module import CourseDescriptor from xmodule.modulestore.django import modulestore from xmodule.modulestore.inheritance import own_metadata from analytics.csvs import create_csv_response +from xmodule.modulestore import Location + # Used to limit the length of list displayed to the screen. MAX_SCREEN_LIST_LENGTH = 250 @@ -31,13 +32,13 @@ def get_problem_grade_distribution(course_id): course_id__exact=course_id, grade__isnull=False, module_type__exact="problem", - ).values('module_state_key', 'grade', 'max_grade').annotate(count_grade=Count('grade')) + ).values('module_id', 'grade', 'max_grade').annotate(count_grade=Count('grade')) prob_grade_distrib = {} # Loop through resultset building data for each problem for row in db_query: - curr_problem = row['module_state_key'] + curr_problem = course_id.make_usage_key_from_deprecated_string(row['module_id']) # Build set of grade distributions for each problem that has student responses if curr_problem in prob_grade_distrib: @@ -69,12 +70,13 @@ def get_sequential_open_distrib(course_id): db_query = models.StudentModule.objects.filter( course_id__exact=course_id, module_type__exact="sequential", - ).values('module_state_key').annotate(count_sequential=Count('module_state_key')) + ).values('module_id').annotate(count_sequential=Count('module_id')) # Build set of "opened" data for each subsection that has "opened" data sequential_open_distrib = {} for row in db_query: - sequential_open_distrib[row['module_state_key']] = row['count_sequential'] + row_loc = course_id.make_usage_key_from_deprecated_string(row['module_id']) + sequential_open_distrib[row_loc] = row['count_sequential'] return sequential_open_distrib @@ -85,7 +87,7 @@ def get_problem_set_grade_distrib(course_id, problem_set): `course_id` the course ID for the course interested in - `problem_set` an array of strings representing problem module_id's. + `problem_set` an array of UsageKeys representing problem module_id's. Requests from the database the a count of each grade for each problem in the `problem_set`. @@ -99,24 +101,25 @@ def get_problem_set_grade_distrib(course_id, problem_set): course_id__exact=course_id, grade__isnull=False, module_type__exact="problem", - module_state_key__in=problem_set, + module_id__in=problem_set, ).values( - 'module_state_key', + 'module_id', 'grade', 'max_grade', - ).annotate(count_grade=Count('grade')).order_by('module_state_key', 'grade') + ).annotate(count_grade=Count('grade')).order_by('module_id', 'grade') prob_grade_distrib = {} # Loop through resultset building data for each problem for row in db_query: - if row['module_state_key'] not in prob_grade_distrib: - prob_grade_distrib[row['module_state_key']] = { + row_loc = course_id.make_usage_key_from_deprecated_string(row['module_id']) + if row_loc not in prob_grade_distrib: + prob_grade_distrib[row_loc] = { 'max_grade': 0, 'grade_distrib': [], } - curr_grade_distrib = prob_grade_distrib[row['module_state_key']] + curr_grade_distrib = prob_grade_distrib[row_loc] curr_grade_distrib['grade_distrib'].append((row['grade'], row['count_grade'])) if curr_grade_distrib['max_grade'] < row['max_grade']: @@ -140,7 +143,7 @@ def get_d3_problem_grade_distrib(course_id): d3_data = [] # Retrieve course object down to problems - course = modulestore().get_instance(course_id, CourseDescriptor.id_to_location(course_id), depth=4) + course = modulestore().get_course(course_id, depth=4) # Iterate through sections, subsections, units, problems for section in course.get_children(): @@ -165,10 +168,10 @@ def get_d3_problem_grade_distrib(course_id): label = "P{0}.{1}.{2}".format(c_subsection, c_unit, c_problem) # Only problems in prob_grade_distrib have had a student submission. - if child.location.url() in prob_grade_distrib: + if child.location in prob_grade_distrib: # Get max_grade, grade_distribution for this problem - problem_info = prob_grade_distrib[child.location.url()] + problem_info = prob_grade_distrib[child.location] # Get problem_name for tooltip problem_name = own_metadata(child).get('display_name', '') @@ -197,7 +200,7 @@ def get_d3_problem_grade_distrib(course_id): 'color': percent, 'value': count_grade, 'tooltip': tooltip, - 'module_url': child.location.url(), + 'module_url': child.location.to_deprecated_string(), }) problem = { @@ -227,7 +230,7 @@ def get_d3_sequential_open_distrib(course_id): d3_data = [] # Retrieve course object down to subsection - course = modulestore().get_instance(course_id, CourseDescriptor.id_to_location(course_id), depth=2) + course = modulestore().get_course(course_id, depth=2) # Iterate through sections, subsections for section in course.get_children(): @@ -242,8 +245,8 @@ def get_d3_sequential_open_distrib(course_id): subsection_name = own_metadata(subsection).get('display_name', '') num_students = 0 - if subsection.location.url() in sequential_open_distrib: - num_students = sequential_open_distrib[subsection.location.url()] + if subsection.location in sequential_open_distrib: + num_students = sequential_open_distrib[subsection.location] stack_data = [] tooltip = _("{num_students} student(s) opened Subsection {subsection_num}: {subsection_name}").format( @@ -256,7 +259,7 @@ def get_d3_sequential_open_distrib(course_id): 'color': 0, 'value': num_students, 'tooltip': tooltip, - 'module_url': subsection.location.url(), + 'module_url': subsection.location.to_deprecated_string(), }) subsection = { 'xValue': "SS {0}".format(c_subsection), @@ -294,7 +297,7 @@ def get_d3_section_grade_distrib(course_id, section): """ # Retrieve course object down to problems - course = modulestore().get_instance(course_id, CourseDescriptor.id_to_location(course_id), depth=4) + course = modulestore().get_course(course_id, depth=4) problem_set = [] problem_info = {} @@ -308,9 +311,9 @@ def get_d3_section_grade_distrib(course_id, section): for child in unit.get_children(): if (child.location.category == 'problem'): c_problem += 1 - problem_set.append(child.location.url()) - problem_info[child.location.url()] = { - 'id': child.location.url(), + problem_set.append(child.location) + problem_info[child.location] = { + 'id': child.location.to_deprecated_string(), 'x_value': "P{0}.{1}.{2}".format(c_subsection, c_unit, c_problem), 'display_name': own_metadata(child).get('display_name', ''), } @@ -366,7 +369,7 @@ def get_section_display_name(course_id): The ith string in the array is the display name of the ith section in the course. """ - course = modulestore().get_instance(course_id, CourseDescriptor.id_to_location(course_id), depth=4) + course = modulestore().get_course(course_id, depth=4) section_display_name = [""] * len(course.get_children()) i = 0 @@ -386,7 +389,7 @@ def get_array_section_has_problem(course_id): The ith value in the array is true if the ith section in the course contains problems and false otherwise. """ - course = modulestore().get_instance(course_id, CourseDescriptor.id_to_location(course_id), depth=4) + course = modulestore().get_course(course_id, depth=4) b_section_has_problem = [False] * len(course.get_children()) i = 0 @@ -415,12 +418,12 @@ def get_students_opened_subsection(request, csv=False): If 'csv' is True, returns a header array, and an array of arrays in the format: student names, usernames for CSV download. """ - module_id = request.GET.get('module_id') + module_id = Location.from_deprecated_string(request.GET.get('module_id')) csv = request.GET.get('csv') # Query for "opened a subsection" students students = models.StudentModule.objects.select_related('student').filter( - module_state_key__exact=module_id, + module_id__exact=module_id, module_type__exact='sequential', ).values('student__username', 'student__profile__name').order_by('student__profile__name') @@ -465,12 +468,12 @@ def get_students_problem_grades(request, csv=False): If 'csv' is True, returns a header array, and an array of arrays in the format: student names, usernames, grades, percents for CSV download. """ - module_id = request.GET.get('module_id') + module_id = Location.from_deprecated_string(request.GET.get('module_id')) csv = request.GET.get('csv') # Query for "problem grades" students students = models.StudentModule.objects.select_related('student').filter( - module_state_key__exact=module_id, + module_id__exact=module_id, module_type__exact='problem', grade__isnull=False, ).values('student__username', 'student__profile__name', 'grade', 'max_grade').order_by('student__profile__name') diff --git a/lms/djangoapps/linkedin/management/commands/tests/__init__.py b/lms/djangoapps/class_dashboard/tests/__init__.py similarity index 100% rename from lms/djangoapps/linkedin/management/commands/tests/__init__.py rename to lms/djangoapps/class_dashboard/tests/__init__.py diff --git a/lms/djangoapps/class_dashboard/test/test_dashboard_data.py b/lms/djangoapps/class_dashboard/tests/test_dashboard_data.py similarity index 93% rename from lms/djangoapps/class_dashboard/test/test_dashboard_data.py rename to lms/djangoapps/class_dashboard/tests/test_dashboard_data.py index e53d373b75..048722787a 100644 --- a/lms/djangoapps/class_dashboard/test/test_dashboard_data.py +++ b/lms/djangoapps/class_dashboard/tests/test_dashboard_data.py @@ -14,7 +14,6 @@ from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE from courseware.tests.factories import StudentModuleFactory from student.tests.factories import UserFactory, CourseEnrollmentFactory, AdminFactory from capa.tests.response_xml_factory import StringResponseXMLFactory -from xmodule.modulestore import Location from class_dashboard.dashboard_data import (get_problem_grade_distribution, get_sequential_open_distrib, get_problem_set_grade_distrib, get_d3_problem_grade_distrib, @@ -82,7 +81,7 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase): max_grade=1 if i < j else 0.5, student=user, course_id=self.course.id, - module_state_key=Location(self.item.location).url(), + module_state_key=self.item.location, state=json.dumps({'attempts': self.attempts}), ) @@ -90,7 +89,7 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase): StudentModuleFactory.create( course_id=self.course.id, module_type='sequential', - module_state_key=Location(self.item.location).url(), + module_state_key=self.item.location, ) def test_get_problem_grade_distribution(self): @@ -156,7 +155,7 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase): def test_get_students_problem_grades(self): - attributes = '?module_id=' + self.item.location.url() + attributes = '?module_id=' + self.item.location.to_deprecated_string() request = self.request_factory.get(reverse('get_students_problem_grades') + attributes) response = get_students_problem_grades(request) @@ -174,7 +173,7 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase): def test_get_students_problem_grades_max(self): with patch('class_dashboard.dashboard_data.MAX_SCREEN_LIST_LENGTH', 2): - attributes = '?module_id=' + self.item.location.url() + attributes = '?module_id=' + self.item.location.to_deprecated_string() request = self.request_factory.get(reverse('get_students_problem_grades') + attributes) response = get_students_problem_grades(request) @@ -188,7 +187,7 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase): def test_get_students_problem_grades_csv(self): tooltip = 'P1.2.1 Q1 - 3382 Students (100%: 1/1 questions)' - attributes = '?module_id=' + self.item.location.url() + '&tooltip=' + tooltip + '&csv=true' + attributes = '?module_id=' + self.item.location.to_deprecated_string() + '&tooltip=' + tooltip + '&csv=true' request = self.request_factory.get(reverse('get_students_problem_grades') + attributes) response = get_students_problem_grades(request) @@ -208,7 +207,7 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase): def test_get_students_opened_subsection(self): - attributes = '?module_id=' + self.item.location.url() + attributes = '?module_id=' + self.item.location.to_deprecated_string() request = self.request_factory.get(reverse('get_students_opened_subsection') + attributes) response = get_students_opened_subsection(request) @@ -221,7 +220,7 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase): with patch('class_dashboard.dashboard_data.MAX_SCREEN_LIST_LENGTH', 2): - attributes = '?module_id=' + self.item.location.url() + attributes = '?module_id=' + self.item.location.to_deprecated_string() request = self.request_factory.get(reverse('get_students_opened_subsection') + attributes) response = get_students_opened_subsection(request) @@ -235,7 +234,7 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase): def test_get_students_opened_subsection_csv(self): tooltip = '4162 student(s) opened Subsection 5: Relational Algebra Exercises' - attributes = '?module_id=' + self.item.location.url() + '&tooltip=' + tooltip + '&csv=true' + attributes = '?module_id=' + self.item.location.to_deprecated_string() + '&tooltip=' + tooltip + '&csv=true' request = self.request_factory.get(reverse('get_students_opened_subsection') + attributes) response = get_students_opened_subsection(request) @@ -255,7 +254,7 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase): def test_dashboard(self): - url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id}) + url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.post( url, { diff --git a/lms/djangoapps/class_dashboard/test/test_views.py b/lms/djangoapps/class_dashboard/tests/test_views.py similarity index 100% rename from lms/djangoapps/class_dashboard/test/test_views.py rename to lms/djangoapps/class_dashboard/tests/test_views.py diff --git a/lms/djangoapps/class_dashboard/views.py b/lms/djangoapps/class_dashboard/views.py index 0b8de65855..142e993c96 100644 --- a/lms/djangoapps/class_dashboard/views.py +++ b/lms/djangoapps/class_dashboard/views.py @@ -19,8 +19,8 @@ def has_instructor_access_for_class(user, course_id): Returns true if the `user` is an instructor for the course. """ - course = get_course_with_access(user, course_id, 'staff', depth=None) - return has_access(user, course, 'staff') + course = get_course_with_access(user, 'staff', course_id, depth=None) + return has_access(user, 'staff', course) def all_sequential_open_distrib(request, course_id): diff --git a/lms/djangoapps/course_wiki/middleware.py b/lms/djangoapps/course_wiki/middleware.py index 996677c8ef..0d276e1cfc 100644 --- a/lms/djangoapps/course_wiki/middleware.py +++ b/lms/djangoapps/course_wiki/middleware.py @@ -29,8 +29,8 @@ class WikiAccessMiddleware(object): if course_id: # See if we are able to view the course. If we are, redirect to it try: - course = get_course_with_access(request.user, course_id, 'load') - return redirect("/courses/{course_id}/wiki/{path}".format(course_id=course.id, path=wiki_path)) + _course = get_course_with_access(request.user, 'load', course_id) + return redirect("/courses/{course_id}/wiki/{path}".format(course_id=course_id.to_deprecated_string(), path=wiki_path)) except Http404: # Even though we came from the course, we can't see it. So don't worry about it. pass @@ -44,22 +44,23 @@ class WikiAccessMiddleware(object): if not view_func.__module__.startswith('wiki.'): return - course_id = course_id_from_url(request.path) - wiki_path = request.path.split('/wiki/', 1)[1] - # wiki pages are login required if not request.user.is_authenticated(): return redirect(reverse('accounts_login'), next=request.path) + course_id = course_id_from_url(request.path) + wiki_path = request.path.partition('/wiki/')[2] + if course_id: # This is a /courses/org/name/run/wiki request - # HACK: django-wiki monkeypatches the django reverse function to enable urls to be rewritten - url_prefix = "/courses/{0}".format(course_id) - reverse._transform_url = lambda url: url_prefix + url # pylint: disable=protected-access + course_path = "/courses/{}".format(course_id.to_deprecated_string()) + # HACK: django-wiki monkeypatches the reverse function to enable + # urls to be rewritten + reverse._transform_url = lambda url: course_path + url # pylint: disable=protected-access # Authorization Check # Let's see if user is enrolled or the course allows for public access try: - course = get_course_with_access(request.user, course_id, 'load') + course = get_course_with_access(request.user, 'load', course_id) except Http404: # course does not exist. redirect to root wiki. # clearing the referrer will cause process_response not to redirect @@ -69,11 +70,11 @@ class WikiAccessMiddleware(object): if not course.allow_public_wiki_access: is_enrolled = CourseEnrollment.is_enrolled(request.user, course.id) - is_staff = has_access(request.user, course, 'staff') + is_staff = has_access(request.user, 'staff', course) if not (is_enrolled or is_staff): # if a user is logged in, but not authorized to see a page, # we'll redirect them to the course about page - return redirect('about_course', course_id) + return redirect('about_course', course_id.to_deprecated_string()) # set the course onto here so that the wiki template can show the course navigation request.course = course else: diff --git a/lms/djangoapps/course_wiki/tests/test_access.py b/lms/djangoapps/course_wiki/tests/test_access.py index 5d56c228d8..d9bc5923ed 100644 --- a/lms/djangoapps/course_wiki/tests/test_access.py +++ b/lms/djangoapps/course_wiki/tests/test_access.py @@ -4,7 +4,6 @@ Tests for wiki permissions from django.contrib.auth.models import Group from student.tests.factories import UserFactory -from xmodule.modulestore.django import loc_mapper from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase @@ -48,12 +47,9 @@ class TestWikiAccessBase(ModuleStoreTestCase): def create_staff_for_course(self, course): """Creates and returns users with instructor and staff access to course.""" - course_locator = loc_mapper().translate_location(course.id, course.location) return [ - InstructorFactory(course=course.location), # Creates instructor_org/number/run role name - StaffFactory(course=course.location), # Creates staff_org/number/run role name - InstructorFactory(course=course_locator), # Creates instructor_org.number.run role name - StaffFactory(course=course_locator), # Creates staff_org.number.run role name + InstructorFactory(course=course.id), # Creates instructor_org/number/run role name + StaffFactory(course=course.id), # Creates staff_org/number/run role name ] diff --git a/lms/djangoapps/course_wiki/tests/test_middleware.py b/lms/djangoapps/course_wiki/tests/test_middleware.py index bd861352c2..f2cd27e8b7 100644 --- a/lms/djangoapps/course_wiki/tests/test_middleware.py +++ b/lms/djangoapps/course_wiki/tests/test_middleware.py @@ -23,7 +23,7 @@ class TestWikiAccessMiddleware(ModuleStoreTestCase): self.wiki = get_or_create_root() self.course_math101 = CourseFactory.create(org='edx', number='math101', display_name='2014', metadata={'use_unique_wiki_id': 'false'}) - self.course_math101_instructor = InstructorFactory(course=self.course_math101.location, username='instructor', password='secret') + self.course_math101_instructor = InstructorFactory(course=self.course_math101.id, username='instructor', password='secret') self.wiki_math101 = URLPath.create_article(self.wiki, 'math101', title='math101') self.client = Client() diff --git a/lms/djangoapps/course_wiki/tests/tests.py b/lms/djangoapps/course_wiki/tests/tests.py index 3bad49f773..8252e49bbb 100644 --- a/lms/djangoapps/course_wiki/tests/tests.py +++ b/lms/djangoapps/course_wiki/tests/tests.py @@ -4,7 +4,7 @@ from django.test.utils import override_settings from courseware.tests.tests import LoginEnrollmentTestCase from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE from xmodule.modulestore.django import modulestore -from xmodule.modulestore.tests.factories import CourseFactory +from xmodule.modulestore.locations import SlashSeparatedCourseKey from mock import patch @@ -15,7 +15,7 @@ class WikiRedirectTestCase(LoginEnrollmentTestCase): def setUp(self): # Load the toy course - self.toy = modulestore().get_course('edX/toy/2012_Fall') + self.toy = modulestore().get_course(SlashSeparatedCourseKey('edX', 'toy', '2012_Fall')) # Create two accounts self.student = 'view@test.com' @@ -43,7 +43,7 @@ class WikiRedirectTestCase(LoginEnrollmentTestCase): self.enroll(self.toy) - referer = reverse("progress", kwargs={'course_id': self.toy.id}) + referer = reverse("progress", kwargs={'course_id': self.toy.id.to_deprecated_string()}) destination = reverse("wiki:get", kwargs={'path': 'some/fake/wiki/page/'}) redirected_to = referer.replace("progress", "wiki/some/fake/wiki/page/") @@ -72,7 +72,7 @@ class WikiRedirectTestCase(LoginEnrollmentTestCase): self.enroll(self.toy) - referer = reverse("progress", kwargs={'course_id': self.toy.id}) + referer = reverse("progress", kwargs={'course_id': self.toy.id.to_deprecated_string()}) destination = reverse("wiki:get", kwargs={'path': 'some/fake/wiki/page/'}) resp = self.client.get(destination, HTTP_REFERER=referer) @@ -84,8 +84,8 @@ class WikiRedirectTestCase(LoginEnrollmentTestCase): The user must be enrolled in the course to see the page. """ - course_wiki_home = reverse('course_wiki', kwargs={'course_id': course.id}) - referer = reverse("progress", kwargs={'course_id': self.toy.id}) + course_wiki_home = reverse('course_wiki', kwargs={'course_id': course.id.to_deprecated_string()}) + referer = reverse("progress", kwargs={'course_id': self.toy.id.to_deprecated_string()}) resp = self.client.get(course_wiki_home, follow=True, HTTP_REFERER=referer) @@ -117,8 +117,7 @@ class WikiRedirectTestCase(LoginEnrollmentTestCase): self.create_course_page(self.toy) course_wiki_page = reverse('wiki:get', kwargs={'path': self.toy.wiki_slug + '/'}) - - referer = reverse("courseware", kwargs={'course_id': self.toy.id}) + referer = reverse("courseware", kwargs={'course_id': self.toy.id.to_deprecated_string()}) resp = self.client.get(course_wiki_page, follow=True, HTTP_REFERER=referer) @@ -137,7 +136,7 @@ class WikiRedirectTestCase(LoginEnrollmentTestCase): self.login(self.student, self.password) course_wiki_page = reverse('wiki:get', kwargs={'path': self.toy.wiki_slug + '/'}) - referer = reverse("courseware", kwargs={'course_id': self.toy.id}) + referer = reverse("courseware", kwargs={'course_id': self.toy.id.to_deprecated_string()}) # When not enrolled, we should get a 302 resp = self.client.get(course_wiki_page, follow=False, HTTP_REFERER=referer) @@ -147,7 +146,7 @@ class WikiRedirectTestCase(LoginEnrollmentTestCase): resp = self.client.get(course_wiki_page, follow=True, HTTP_REFERER=referer) target_url, __ = resp.redirect_chain[-1] self.assertTrue( - target_url.endswith(reverse('about_course', args=[self.toy.id])) + target_url.endswith(reverse('about_course', args=[self.toy.id.to_deprecated_string()])) ) @patch.dict("django.conf.settings.FEATURES", {'ALLOW_WIKI_ROOT_ACCESS': True}) diff --git a/lms/djangoapps/course_wiki/utils.py b/lms/djangoapps/course_wiki/utils.py index 331894b81d..4945bf37b6 100644 --- a/lms/djangoapps/course_wiki/utils.py +++ b/lms/djangoapps/course_wiki/utils.py @@ -33,12 +33,12 @@ def user_is_article_course_staff(user, article): # course numbered '202_' or '202' and so we need to consider both. courses = modulestore.django.modulestore().get_courses_for_wiki(wiki_slug) - if any(courseware.access.has_access(user, course, 'staff', course.course_id) for course in courses): + if any(courseware.access.has_access(user, 'staff', course, course.course_key) for course in courses): return True if (wiki_slug.endswith('_') and slug_is_numerical(wiki_slug[:-1])): courses = modulestore.django.modulestore().get_courses_for_wiki(wiki_slug[:-1]) - if any(courseware.access.has_access(user, course, 'staff', course.course_id) for course in courses): + if any(courseware.access.has_access(user, 'staff', course, course.course_key) for course in courses): return True return False diff --git a/lms/djangoapps/course_wiki/views.py b/lms/djangoapps/course_wiki/views.py index 724125a716..73d18bc378 100644 --- a/lms/djangoapps/course_wiki/views.py +++ b/lms/djangoapps/course_wiki/views.py @@ -16,6 +16,7 @@ from wiki.models import URLPath, Article from courseware.courses import get_course_by_id from course_wiki.utils import course_wiki_slug +from xmodule.modulestore.locations import SlashSeparatedCourseKey log = logging.getLogger(__name__) @@ -35,7 +36,7 @@ def course_wiki_redirect(request, course_id): # pylint: disable=W0613 as it's home page. A course's wiki must be an article on the root (for example, "/6.002x") to keep things simple. """ - course = get_course_by_id(course_id) + course = get_course_by_id(SlashSeparatedCourseKey.from_deprecated_string(course_id)) course_slug = course_wiki_slug(course) valid_slug = True diff --git a/lms/djangoapps/dashboard/git_import.py b/lms/djangoapps/dashboard/git_import.py index a3391665fd..8f3963debf 100644 --- a/lms/djangoapps/dashboard/git_import.py +++ b/lms/djangoapps/dashboard/git_import.py @@ -17,7 +17,7 @@ from django.utils.translation import ugettext as _ import mongoengine from dashboard.models import CourseImportLog -from xmodule.modulestore import Location +from xmodule.modulestore.keys import CourseKey log = logging.getLogger(__name__) @@ -222,19 +222,16 @@ def add_repo(repo, rdir_in, branch=None): logger.setLevel(logging.NOTSET) logger.removeHandler(import_log_handler) - course_id = 'unknown' + course_key = None location = 'unknown' # extract course ID from output of import-command-run and make symlink # this is needed in order for custom course scripts to work - match = re.search('(?ms)===> IMPORTING course to location (\S+)', - ret_import) + match = re.search(r'(?ms)===> IMPORTING course (\S+)', ret_import) if match: - location = Location(match.group(1)) - log.debug('location = {0}'.format(location)) - course_id = location.course_id - - cdir = '{0}/{1}'.format(GIT_REPO_DIR, location.course) + course_id = match.group(1) + course_key = CourseKey.from_string(course_id) + cdir = '{0}/{1}'.format(GIT_REPO_DIR, course_key.course) log.debug('Studio course dir = {0}'.format(cdir)) if os.path.exists(cdir) and not os.path.islink(cdir): @@ -267,8 +264,8 @@ def add_repo(repo, rdir_in, branch=None): log.exception('Unable to connect to mongodb to save log, please ' 'check MONGODB_LOG settings') cil = CourseImportLog( - course_id=course_id, - location=unicode(location), + course_id=course_key, + location=location, repo_dir=rdir, created=timezone.now(), import_log=ret_import, diff --git a/lms/djangoapps/dashboard/management/commands/git_add_course.py b/lms/djangoapps/dashboard/management/commands/git_add_course.py index 58092fe5c6..da02a438a6 100644 --- a/lms/djangoapps/dashboard/management/commands/git_add_course.py +++ b/lms/djangoapps/dashboard/management/commands/git_add_course.py @@ -2,19 +2,13 @@ Script for importing courseware from git/xml into a mongo modulestore """ -import os -import re -import StringIO -import subprocess import logging -from django.core import management from django.core.management.base import BaseCommand, CommandError from django.utils.translation import ugettext as _ import dashboard.git_import from dashboard.git_import import GitImportError -from dashboard.models import CourseImportLog from xmodule.modulestore.django import modulestore from xmodule.modulestore.xml import XMLModuleStore diff --git a/lms/djangoapps/dashboard/management/commands/tests/test_git_add_course.py b/lms/djangoapps/dashboard/management/commands/tests/test_git_add_course.py index f88b8dd431..a1e76fad2b 100644 --- a/lms/djangoapps/dashboard/management/commands/tests/test_git_add_course.py +++ b/lms/djangoapps/dashboard/management/commands/tests/test_git_add_course.py @@ -17,10 +17,12 @@ from django.test.utils import override_settings from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE from xmodule.contentstore.django import contentstore from xmodule.modulestore.django import modulestore +from xmodule.modulestore.locations import SlashSeparatedCourseKey from xmodule.modulestore.store_utilities import delete_course from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase import dashboard.git_import as git_import from dashboard.git_import import GitImportError +from xmodule.modulestore.locations import SlashSeparatedCourseKey TEST_MONGODB_LOG = { 'host': 'localhost', @@ -45,7 +47,7 @@ class TestGitAddCourse(ModuleStoreTestCase): TEST_REPO = 'https://github.com/mitocw/edx4edx_lite.git' TEST_COURSE = 'MITx/edx4edx/edx4edx' TEST_BRANCH = 'testing_do_not_delete' - TEST_BRANCH_COURSE = 'MITx/edx4edx_branch/edx4edx' + TEST_BRANCH_COURSE = SlashSeparatedCourseKey('MITx', 'edx4edx_branch', 'edx4edx') GIT_REPO_DIR = getattr(settings, 'GIT_REPO_DIR') def assertCommandFailureRegexp(self, regex, *args): @@ -162,14 +164,14 @@ class TestGitAddCourse(ModuleStoreTestCase): # Delete to test branching back to master delete_course(def_ms, contentstore(), - def_ms.get_course(self.TEST_BRANCH_COURSE).location, + self.TEST_BRANCH_COURSE, True) self.assertIsNone(def_ms.get_course(self.TEST_BRANCH_COURSE)) git_import.add_repo(self.TEST_REPO, repo_dir / 'edx4edx_lite', 'master') self.assertIsNone(def_ms.get_course(self.TEST_BRANCH_COURSE)) - self.assertIsNotNone(def_ms.get_course(self.TEST_COURSE)) + self.assertIsNotNone(def_ms.get_course(SlashSeparatedCourseKey.from_deprecated_string(self.TEST_COURSE))) def test_branch_exceptions(self): """ diff --git a/lms/djangoapps/dashboard/models.py b/lms/djangoapps/dashboard/models.py index 096288f6d4..988214287d 100644 --- a/lms/djangoapps/dashboard/models.py +++ b/lms/djangoapps/dashboard/models.py @@ -1,13 +1,15 @@ """Models for dashboard application""" import mongoengine +from xmodule.modulestore.mongoengine_fields import CourseKeyField class CourseImportLog(mongoengine.Document): """Mongoengine model for git log""" # pylint: disable=R0924 - course_id = mongoengine.StringField(max_length=128) + course_id = CourseKeyField(max_length=128) + # NOTE: this location is not a Location object but a pathname location = mongoengine.StringField(max_length=168) import_log = mongoengine.StringField(max_length=20 * 65535) git_log = mongoengine.StringField(max_length=65535) diff --git a/lms/djangoapps/dashboard/sysadmin.py b/lms/djangoapps/dashboard/sysadmin.py index c036dc2c0e..9366122e6c 100644 --- a/lms/djangoapps/dashboard/sysadmin.py +++ b/lms/djangoapps/dashboard/sysadmin.py @@ -42,6 +42,7 @@ from xmodule.modulestore import XML_MODULESTORE_TYPE from xmodule.modulestore.django import modulestore from xmodule.modulestore.store_utilities import delete_course from xmodule.modulestore.xml import XMLModuleStore +from xmodule.modulestore.locations import SlashSeparatedCourseKey log = logging.getLogger(__name__) @@ -78,7 +79,7 @@ class SysadminDashboardView(TemplateView): """ Get an iterable list of courses.""" courses = self.def_ms.get_courses() - courses = dict([c.id, c] for c in courses) # no course directory + courses = dict([c.id.to_deprecated_string(), c] for c in courses) # no course directory return courses @@ -258,7 +259,7 @@ class Users(SysadminDashboardView): self.msg += u'
    ' for (cdir, course) in courses.items(): self.msg += u'
  1. {0} ({1})
  2. '.format( - escape(cdir), course.location.url()) + escape(cdir), course.location.to_deprecated_string()) self.msg += u'
' def get(self, request): @@ -469,7 +470,7 @@ class Courses(SysadminDashboardView): course = self.def_ms.courses[os.path.abspath(gdir)] msg += _('Loaded course {0} {1}
Errors:').format( cdir, course.display_name) - errors = self.def_ms.get_item_errors(course.location) + errors = self.def_ms.get_course_errors(course.id) if not errors: msg += u'None' else: @@ -489,9 +490,7 @@ class Courses(SysadminDashboardView): courses = self.get_courses() for (cdir, course) in courses.items(): - gdir = cdir - if '/' in cdir: - gdir = cdir.rsplit('/', 1)[1] + gdir = cdir.run data.append([course.display_name, cdir] + self.git_info_for_course(gdir)) @@ -535,13 +534,14 @@ class Courses(SysadminDashboardView): elif action == 'del_course': course_id = request.POST.get('course_id', '').strip() + course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) course_found = False - if course_id in courses: + if course_key in courses: course_found = True - course = courses[course_id] + course = courses[course_key] else: try: - course = get_course_by_id(course_id) + course = get_course_by_id(course_key) course_found = True except Exception, err: # pylint: disable=broad-except self.msg += _('Error - cannot get course with ID ' @@ -549,7 +549,7 @@ class Courses(SysadminDashboardView): course_id, escape(str(err)) ) - is_xml_course = (modulestore().get_modulestore_type(course_id) == XML_MODULESTORE_TYPE) + is_xml_course = (modulestore().get_modulestore_type(course_key) == XML_MODULESTORE_TYPE) if course_found and is_xml_course: cdir = course.data_dir self.def_ms.courses.pop(cdir) @@ -567,14 +567,13 @@ class Courses(SysadminDashboardView): elif course_found and not is_xml_course: # delete course that is stored with mongodb backend - loc = course.location content_store = contentstore() commit = True - delete_course(self.def_ms, content_store, loc, commit) + delete_course(self.def_ms, content_store, course.id, commit) # don't delete user permission groups, though self.msg += \ - u"{0} {1} = {2} ({3})".format( - _('Deleted'), loc, course.id, course.display_name) + u"{0} {1} ({2})".format( + _('Deleted'), course.id.to_deprecated_string(), course.display_name) datatable = self.make_datatable() context = { @@ -606,9 +605,9 @@ class Staffing(SysadminDashboardView): datum = [course.display_name, course.id] datum += [CourseEnrollment.objects.filter( course_id=course.id).count()] - datum += [CourseStaffRole(course.location).users_with_role().count()] + datum += [CourseStaffRole(course.id).users_with_role().count()] datum += [','.join([x.username for x in CourseInstructorRole( - course.location).users_with_role()])] + course.id).users_with_role()])] data.append(datum) datatable = dict(header=[_('Course Name'), _('course_id'), @@ -640,7 +639,7 @@ class Staffing(SysadminDashboardView): for (cdir, course) in courses.items(): # pylint: disable=unused-variable for role in roles: - for user in role(course.location).users_with_role(): + for user in role(course.id).users_with_role(): datum = [course.id, role, user.username, user.email, user.profile.name] data.append(datum) @@ -667,6 +666,8 @@ class GitLogs(TemplateView): """Shows logs of imports that happened as a result of a git import""" course_id = kwargs.get('course_id') + if course_id: + course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) # Set mongodb defaults even if it isn't defined in settings mongo_db = { @@ -709,16 +710,15 @@ class GitLogs(TemplateView): # Allow only course team, instructors, and staff if not (request.user.is_staff or - CourseInstructorRole(course.location).has_user(request.user) or - CourseStaffRole(course.location).has_user(request.user)): + CourseInstructorRole(course.id).has_user(request.user) or + CourseStaffRole(course.id).has_user(request.user)): raise Http404 log.debug('course_id={0}'.format(course_id)) - cilset = CourseImportLog.objects.filter( - course_id=course_id).order_by('-created') + cilset = CourseImportLog.objects.filter(course_id=course_id).order_by('-created') log.debug('cilset length={0}'.format(len(cilset))) mdb.disconnect() context = {'cilset': cilset, - 'course_id': course_id, + 'course_id': course_id.to_deprecated_string() if course_id else None, 'error_msg': error_msg} return render_to_response(self.template_name, context) diff --git a/lms/djangoapps/dashboard/tests/test_sysadmin.py b/lms/djangoapps/dashboard/tests/test_sysadmin.py index ca10f7d7e6..ea6c799ba1 100644 --- a/lms/djangoapps/dashboard/tests/test_sysadmin.py +++ b/lms/djangoapps/dashboard/tests/test_sysadmin.py @@ -13,7 +13,6 @@ from django.contrib.auth.models import User from django.core.urlresolvers import reverse from django.test.client import Client from django.test.utils import override_settings -from django.utils.html import escape from django.utils.translation import ugettext as _ import mongoengine @@ -27,6 +26,7 @@ from student.tests.factories import UserFactory from xmodule.modulestore.django import modulestore from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.xml import XMLModuleStore +from xmodule.modulestore.locations import SlashSeparatedCourseKey TEST_MONGODB_LOG = { @@ -47,7 +47,7 @@ class SysadminBaseTestCase(ModuleStoreTestCase): TEST_REPO = 'https://github.com/mitocw/edx4edx_lite.git' TEST_BRANCH = 'testing_do_not_delete' - TEST_BRANCH_COURSE = 'MITx/edx4edx_branch/edx4edx' + TEST_BRANCH_COURSE = SlashSeparatedCourseKey('MITx', 'edx4edx_branch', 'edx4edx') def setUp(self): """Setup test case by adding primary user.""" @@ -79,12 +79,16 @@ class SysadminBaseTestCase(ModuleStoreTestCase): course = def_ms.courses.get(course_path, None) except AttributeError: # Using mongo store - course = def_ms.get_course('MITx/edx4edx/edx4edx') + course = def_ms.get_course(SlashSeparatedCourseKey('MITx', 'edx4edx', 'edx4edx')) # Delete git loaded course - response = self.client.post(reverse('sysadmin_courses'), - {'course_id': course.id, - 'action': 'del_course', }) + response = self.client.post( + reverse('sysadmin_courses'), + { + 'course_id': course.id.to_deprecated_string(), + 'action': 'del_course', + } + ) self.addCleanup(self._rm_glob, '{0}_deleted_*'.format(course_path)) return response @@ -321,7 +325,7 @@ class TestSysadmin(SysadminBaseTestCase): course = def_ms.courses.get('{0}/edx4edx_lite'.format( os.path.abspath(settings.DATA_DIR)), None) self.assertIsNotNone(course) - self.assertIn(self.TEST_BRANCH_COURSE, course.location.course_id) + self.assertEqual(self.TEST_BRANCH_COURSE, course.id) self._rm_edx4edx() # Try and delete a non-existent course @@ -363,8 +367,8 @@ class TestSysadmin(SysadminBaseTestCase): self._add_edx4edx() def_ms = modulestore() - course = def_ms.get_course('MITx/edx4edx/edx4edx') - CourseStaffRole(course.location).add_users(self.user) + course = def_ms.get_course(SlashSeparatedCourseKey('MITx', 'edx4edx', 'edx4edx')) + CourseStaffRole(course.id).add_users(self.user) response = self.client.post(reverse('sysadmin_staffing'), {'action': 'get_staff_csv', }) @@ -447,11 +451,11 @@ class TestSysAdminMongoCourseImport(SysadminBaseTestCase): self.assertFalse(isinstance(def_ms, XMLModuleStore)) self._add_edx4edx() - course = def_ms.get_course('MITx/edx4edx/edx4edx') + course = def_ms.get_course(SlashSeparatedCourseKey('MITx', 'edx4edx', 'edx4edx')) self.assertIsNotNone(course) self._rm_edx4edx() - course = def_ms.get_course('MITx/edx4edx/edx4edx') + course = def_ms.get_course(SlashSeparatedCourseKey('MITx', 'edx4edx', 'edx4edx')) self.assertIsNone(course) def test_gitlogs(self): @@ -472,7 +476,7 @@ class TestSysAdminMongoCourseImport(SysadminBaseTestCase): reverse('gitlogs_detail', kwargs={ 'course_id': 'MITx/edx4edx/edx4edx'})) - self.assertIn('======> IMPORTING course to location', + self.assertIn('======> IMPORTING course', response.content) self._rm_edx4edx() @@ -505,23 +509,25 @@ class TestSysAdminMongoCourseImport(SysadminBaseTestCase): self.assertEqual(response.status_code, 404) # Or specific logs response = self.client.get(reverse('gitlogs_detail', kwargs={ - 'course_id': 'MITx/edx4edx/edx4edx'})) + 'course_id': 'MITx/edx4edx/edx4edx' + })) self.assertEqual(response.status_code, 404) # Add user as staff in course team def_ms = modulestore() - course = def_ms.get_course('MITx/edx4edx/edx4edx') - CourseStaffRole(course.location).add_users(self.user) + course = def_ms.get_course(SlashSeparatedCourseKey('MITx', 'edx4edx', 'edx4edx')) + CourseStaffRole(course.id).add_users(self.user) - self.assertTrue(CourseStaffRole(course.location).has_user(self.user)) + self.assertTrue(CourseStaffRole(course.id).has_user(self.user)) logged_in = self.client.login(username=self.user.username, password='foo') self.assertTrue(logged_in) response = self.client.get( reverse('gitlogs_detail', kwargs={ - 'course_id': 'MITx/edx4edx/edx4edx'})) - self.assertIn('======> IMPORTING course to location', + 'course_id': 'MITx/edx4edx/edx4edx' + })) + self.assertIn('======> IMPORTING course', response.content) self._rm_edx4edx() diff --git a/lms/djangoapps/debug/management/commands/dump_xml_courses.py b/lms/djangoapps/debug/management/commands/dump_xml_courses.py index 571ba59aa9..7f5a4cf630 100644 --- a/lms/djangoapps/debug/management/commands/dump_xml_courses.py +++ b/lms/djangoapps/debug/management/commands/dump_xml_courses.py @@ -42,7 +42,7 @@ class Command(BaseCommand): for course_id, course_modules in xml_module_store.modules.iteritems(): course_path = course_id.replace('/', '_') for location, descriptor in course_modules.iteritems(): - location_path = location.url().replace('/', '_') + location_path = location.to_deprecated_string().replace('/', '_') data = {} for field_name, field in descriptor.fields.iteritems(): try: diff --git a/lms/djangoapps/django_comment_client/base/tests.py b/lms/djangoapps/django_comment_client/base/tests.py index 8739c54edb..f05a245fb2 100644 --- a/lms/djangoapps/django_comment_client/base/tests.py +++ b/lms/djangoapps/django_comment_client/base/tests.py @@ -47,7 +47,7 @@ class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin): display_name='Robot Super Course') self.course_id = self.course.id # seed the forums permissions and roles - call_command('seed_permissions_roles', self.course_id) + call_command('seed_permissions_roles', self.course_id.to_deprecated_string()) # Patch the comment client user save method so it does not try # to create a new cc user when creating a django user @@ -106,7 +106,7 @@ class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin): "title": ["Hello"] } url = reverse('create_thread', kwargs={'commentable_id': 'i4x-MITx-999-course-Robot_Super_Course', - 'course_id': self.course_id}) + 'course_id': self.course_id.to_deprecated_string()}) response = self.client.post(url, data=thread) assert_true(mock_request.called) mock_request.assert_called_with( @@ -117,7 +117,8 @@ class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin): 'anonymous_to_peers': False, 'user_id': 1, 'title': u'Hello', 'commentable_id': u'i4x-MITx-999-course-Robot_Super_Course', - 'anonymous': False, 'course_id': u'MITx/999/Robot_Super_Course', + 'anonymous': False, + 'course_id': u'MITx/999/Robot_Super_Course', }, params={'request_id': ANY}, headers=ANY, @@ -134,7 +135,7 @@ class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin): request = RequestFactory().post("dummy_url", {"id": test_comment_id}) request.user = self.student request.view_name = "delete_comment" - response = views.delete_comment(request, course_id=self.course.id, comment_id=test_comment_id) + response = views.delete_comment(request, course_id=self.course.id.to_deprecated_string(), comment_id=test_comment_id) self.assertEqual(response.status_code, 200) self.assertTrue(mock_request.called) @@ -172,7 +173,7 @@ class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin): def test_create_thread_no_title(self, mock_request): self._test_request_error( "create_thread", - {"commentable_id": "dummy", "course_id": self.course_id}, + {"commentable_id": "dummy", "course_id": self.course_id.to_deprecated_string()}, {"body": "foo"}, mock_request ) @@ -180,7 +181,7 @@ class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin): def test_create_thread_empty_title(self, mock_request): self._test_request_error( "create_thread", - {"commentable_id": "dummy", "course_id": self.course_id}, + {"commentable_id": "dummy", "course_id": self.course_id.to_deprecated_string()}, {"body": "foo", "title": " "}, mock_request ) @@ -188,7 +189,7 @@ class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin): def test_create_thread_no_body(self, mock_request): self._test_request_error( "create_thread", - {"commentable_id": "dummy", "course_id": self.course_id}, + {"commentable_id": "dummy", "course_id": self.course_id.to_deprecated_string()}, {"title": "foo"}, mock_request ) @@ -196,7 +197,7 @@ class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin): def test_create_thread_empty_body(self, mock_request): self._test_request_error( "create_thread", - {"commentable_id": "dummy", "course_id": self.course_id}, + {"commentable_id": "dummy", "course_id": self.course_id.to_deprecated_string()}, {"body": " ", "title": "foo"}, mock_request ) @@ -204,7 +205,7 @@ class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin): def test_update_thread_no_title(self, mock_request): self._test_request_error( "update_thread", - {"thread_id": "dummy", "course_id": self.course_id}, + {"thread_id": "dummy", "course_id": self.course_id.to_deprecated_string()}, {"body": "foo"}, mock_request ) @@ -212,7 +213,7 @@ class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin): def test_update_thread_empty_title(self, mock_request): self._test_request_error( "update_thread", - {"thread_id": "dummy", "course_id": self.course_id}, + {"thread_id": "dummy", "course_id": self.course_id.to_deprecated_string()}, {"body": "foo", "title": " "}, mock_request ) @@ -220,7 +221,7 @@ class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin): def test_update_thread_no_body(self, mock_request): self._test_request_error( "update_thread", - {"thread_id": "dummy", "course_id": self.course_id}, + {"thread_id": "dummy", "course_id": self.course_id.to_deprecated_string()}, {"title": "foo"}, mock_request ) @@ -228,7 +229,7 @@ class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin): def test_update_thread_empty_body(self, mock_request): self._test_request_error( "update_thread", - {"thread_id": "dummy", "course_id": self.course_id}, + {"thread_id": "dummy", "course_id": self.course_id.to_deprecated_string()}, {"body": " ", "title": "foo"}, mock_request ) @@ -236,7 +237,7 @@ class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin): def test_create_comment_no_body(self, mock_request): self._test_request_error( "create_comment", - {"thread_id": "dummy", "course_id": self.course_id}, + {"thread_id": "dummy", "course_id": self.course_id.to_deprecated_string()}, {}, mock_request ) @@ -244,7 +245,7 @@ class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin): def test_create_comment_empty_body(self, mock_request): self._test_request_error( "create_comment", - {"thread_id": "dummy", "course_id": self.course_id}, + {"thread_id": "dummy", "course_id": self.course_id.to_deprecated_string()}, {"body": " "}, mock_request ) @@ -252,7 +253,7 @@ class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin): def test_create_sub_comment_no_body(self, mock_request): self._test_request_error( "create_sub_comment", - {"comment_id": "dummy", "course_id": self.course_id}, + {"comment_id": "dummy", "course_id": self.course_id.to_deprecated_string()}, {}, mock_request ) @@ -260,7 +261,7 @@ class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin): def test_create_sub_comment_empty_body(self, mock_request): self._test_request_error( "create_sub_comment", - {"comment_id": "dummy", "course_id": self.course_id}, + {"comment_id": "dummy", "course_id": self.course_id.to_deprecated_string()}, {"body": " "}, mock_request ) @@ -268,7 +269,7 @@ class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin): def test_update_comment_no_body(self, mock_request): self._test_request_error( "update_comment", - {"comment_id": "dummy", "course_id": self.course_id}, + {"comment_id": "dummy", "course_id": self.course_id.to_deprecated_string()}, {}, mock_request ) @@ -276,7 +277,7 @@ class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin): def test_update_comment_empty_body(self, mock_request): self._test_request_error( "update_comment", - {"comment_id": "dummy", "course_id": self.course_id}, + {"comment_id": "dummy", "course_id": self.course_id.to_deprecated_string()}, {"body": " "}, mock_request ) @@ -289,7 +290,7 @@ class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin): response = self.client.post( reverse( "update_comment", - kwargs={"course_id": self.course_id, "comment_id": comment_id} + kwargs={"course_id": self.course_id.to_deprecated_string(), "comment_id": comment_id} ), data={"body": updated_body} ) @@ -334,7 +335,7 @@ class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin): "read": False, "comments_count": 0, }) - url = reverse('flag_abuse_for_thread', kwargs={'thread_id': '518d4237b023791dca00000d', 'course_id': self.course_id}) + url = reverse('flag_abuse_for_thread', kwargs={'thread_id': '518d4237b023791dca00000d', 'course_id': self.course_id.to_deprecated_string()}) response = self.client.post(url) assert_true(mock_request.called) @@ -403,7 +404,7 @@ class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin): "read": False, "comments_count": 0 }) - url = reverse('un_flag_abuse_for_thread', kwargs={'thread_id': '518d4237b023791dca00000d', 'course_id': self.course_id}) + url = reverse('un_flag_abuse_for_thread', kwargs={'thread_id': '518d4237b023791dca00000d', 'course_id': self.course_id.to_deprecated_string()}) response = self.client.post(url) assert_true(mock_request.called) @@ -466,7 +467,7 @@ class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin): "type": "comment", "endorsed": False }) - url = reverse('flag_abuse_for_comment', kwargs={'comment_id': '518d4237b023791dca00000d', 'course_id': self.course_id}) + url = reverse('flag_abuse_for_comment', kwargs={'comment_id': '518d4237b023791dca00000d', 'course_id': self.course_id.to_deprecated_string()}) response = self.client.post(url) assert_true(mock_request.called) @@ -529,7 +530,7 @@ class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin): "type": "comment", "endorsed": False }) - url = reverse('un_flag_abuse_for_comment', kwargs={'comment_id': '518d4237b023791dca00000d', 'course_id': self.course_id}) + url = reverse('un_flag_abuse_for_comment', kwargs={'comment_id': '518d4237b023791dca00000d', 'course_id': self.course_id.to_deprecated_string()}) response = self.client.post(url) assert_true(mock_request.called) @@ -586,7 +587,7 @@ class ViewPermissionsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSet self._set_mock_request_data(mock_request, {}) self.client.login(username=self.student.username, password=self.password) response = self.client.post( - reverse("pin_thread", kwargs={"course_id": self.course.id, "thread_id": "dummy"}) + reverse("pin_thread", kwargs={"course_id": self.course.id.to_deprecated_string(), "thread_id": "dummy"}) ) self.assertEqual(response.status_code, 401) @@ -594,7 +595,7 @@ class ViewPermissionsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSet self._set_mock_request_data(mock_request, {}) self.client.login(username=self.moderator.username, password=self.password) response = self.client.post( - reverse("pin_thread", kwargs={"course_id": self.course.id, "thread_id": "dummy"}) + reverse("pin_thread", kwargs={"course_id": self.course.id.to_deprecated_string(), "thread_id": "dummy"}) ) self.assertEqual(response.status_code, 200) @@ -602,7 +603,7 @@ class ViewPermissionsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSet self._set_mock_request_data(mock_request, {}) self.client.login(username=self.student.username, password=self.password) response = self.client.post( - reverse("un_pin_thread", kwargs={"course_id": self.course.id, "thread_id": "dummy"}) + reverse("un_pin_thread", kwargs={"course_id": self.course.id.to_deprecated_string(), "thread_id": "dummy"}) ) self.assertEqual(response.status_code, 401) @@ -610,7 +611,7 @@ class ViewPermissionsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSet self._set_mock_request_data(mock_request, {}) self.client.login(username=self.moderator.username, password=self.password) response = self.client.post( - reverse("un_pin_thread", kwargs={"course_id": self.course.id, "thread_id": "dummy"}) + reverse("un_pin_thread", kwargs={"course_id": self.course.id.to_deprecated_string(), "thread_id": "dummy"}) ) self.assertEqual(response.status_code, 200) @@ -629,7 +630,7 @@ class CreateThreadUnicodeTestCase(ModuleStoreTestCase, UnicodeTestMixin, MockReq request = RequestFactory().post("dummy_url", {"body": text, "title": text}) request.user = self.student request.view_name = "create_thread" - response = views.create_thread(request, course_id=self.course.id, commentable_id="test_commentable") + response = views.create_thread(request, course_id=self.course.id.to_deprecated_string(), commentable_id="test_commentable") self.assertEqual(response.status_code, 200) self.assertTrue(mock_request.called) @@ -654,7 +655,7 @@ class UpdateThreadUnicodeTestCase(ModuleStoreTestCase, UnicodeTestMixin, MockReq request = RequestFactory().post("dummy_url", {"body": text, "title": text}) request.user = self.student request.view_name = "update_thread" - response = views.update_thread(request, course_id=self.course.id, thread_id="dummy_thread_id") + response = views.update_thread(request, course_id=self.course.id.to_deprecated_string(), thread_id="dummy_thread_id") self.assertEqual(response.status_code, 200) self.assertTrue(mock_request.called) @@ -678,7 +679,7 @@ class CreateCommentUnicodeTestCase(ModuleStoreTestCase, UnicodeTestMixin, MockRe request = RequestFactory().post("dummy_url", {"body": text}) request.user = self.student request.view_name = "create_comment" - response = views.create_comment(request, course_id=self.course.id, thread_id="dummy_thread_id") + response = views.create_comment(request, course_id=self.course.id.to_deprecated_string(), thread_id="dummy_thread_id") self.assertEqual(response.status_code, 200) self.assertTrue(mock_request.called) @@ -702,7 +703,7 @@ class UpdateCommentUnicodeTestCase(ModuleStoreTestCase, UnicodeTestMixin, MockRe request = RequestFactory().post("dummy_url", {"body": text}) request.user = self.student request.view_name = "update_comment" - response = views.update_comment(request, course_id=self.course.id, comment_id="dummy_comment_id") + response = views.update_comment(request, course_id=self.course.id.to_deprecated_string(), comment_id="dummy_comment_id") self.assertEqual(response.status_code, 200) self.assertTrue(mock_request.called) @@ -726,7 +727,7 @@ class CreateSubCommentUnicodeTestCase(ModuleStoreTestCase, UnicodeTestMixin, Moc request = RequestFactory().post("dummy_url", {"body": text}) request.user = self.student request.view_name = "create_sub_comment" - response = views.create_sub_comment(request, course_id=self.course.id, comment_id="dummy_comment_id") + response = views.create_sub_comment(request, course_id=self.course.id.to_deprecated_string(), comment_id="dummy_comment_id") self.assertEqual(response.status_code, 200) self.assertTrue(mock_request.called) diff --git a/lms/djangoapps/django_comment_client/base/views.py b/lms/djangoapps/django_comment_client/base/views.py index cca662ab9e..98afcb9e8f 100644 --- a/lms/djangoapps/django_comment_client/base/views.py +++ b/lms/djangoapps/django_comment_client/base/views.py @@ -1,6 +1,5 @@ import time import random -import os import os.path import logging import urlparse @@ -13,12 +12,11 @@ import django_comment_client.settings as cc_settings from django.core import exceptions from django.contrib.auth.decorators import login_required -from django.views.decorators.http import require_POST, require_GET +from django.views.decorators.http import require_POST from django.views.decorators import csrf from django.core.files.storage import get_storage_class from django.utils.translation import ugettext as _ -from edxmako.shortcuts import render_to_string from courseware.courses import get_course_with_access, get_course_by_id from course_groups.cohorts import get_cohort_id, is_commentable_cohorted @@ -26,6 +24,8 @@ from django_comment_client.utils import JsonResponse, JsonError, extract, add_co from django_comment_client.permissions import check_permissions_by_view, cached_has_permission from courseware.access import has_access +from xmodule.modulestore.locations import SlashSeparatedCourseKey +from xmodule.modulestore.keys import CourseKey log = logging.getLogger(__name__) @@ -41,7 +41,8 @@ def permitted(fn): else: content = None return content - if check_permissions_by_view(request.user, kwargs['course_id'], fetch_content(), request.view_name): + course_key = SlashSeparatedCourseKey.from_deprecated_string(kwargs['course_id']) + if check_permissions_by_view(request.user, course_key, fetch_content(), request.view_name): return fn(request, *args, **kwargs) else: return JsonError("unauthorized", status=401) @@ -49,10 +50,6 @@ def permitted(fn): def ajax_content_response(request, course_id, content): - context = { - 'course_id': course_id, - 'content': content, - } user_info = cc.User.from_django_user(request.user).to_dict() annotated_content_info = utils.get_annotated_content_info(course_id, content, request.user, user_info) return JsonResponse({ @@ -70,7 +67,8 @@ def create_thread(request, course_id, commentable_id): """ log.debug("Creating new thread in %r, id %r", course_id, commentable_id) - course = get_course_with_access(request.user, course_id, 'load') + course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) + course = get_course_with_access(request.user, 'load', course_id) post = request.POST if course.allow_anonymous: @@ -93,7 +91,7 @@ def create_thread(request, course_id, commentable_id): 'anonymous': anonymous, 'anonymous_to_peers': anonymous_to_peers, 'commentable_id': commentable_id, - 'course_id': course_id, + 'course_id': course_id.to_deprecated_string(), 'user_id': request.user.id, }) @@ -152,23 +150,24 @@ def update_thread(request, course_id, thread_id): thread.update_attributes(**extract(request.POST, ['body', 'title'])) thread.save() if request.is_ajax(): - return ajax_content_response(request, course_id, thread.to_dict()) + return ajax_content_response(request, SlashSeparatedCourseKey.from_deprecated_string(course_id), thread.to_dict()) else: return JsonResponse(utils.safe_content(thread.to_dict())) -def _create_comment(request, course_id, thread_id=None, parent_id=None): +def _create_comment(request, course_key, thread_id=None, parent_id=None): """ given a course_id, thread_id, and parent_id, create a comment, called from create_comment to do the actual creation """ + assert isinstance(course_key, CourseKey) post = request.POST if 'body' not in post or not post['body'].strip(): return JsonError(_("Body can't be empty")) comment = cc.Comment(**extract(post, ['body'])) - course = get_course_with_access(request.user, course_id, 'load') + course = get_course_with_access(request.user, 'load', course_key) if course.allow_anonymous: anonymous = post.get('anonymous', 'false').lower() == 'true' else: @@ -183,7 +182,7 @@ def _create_comment(request, course_id, thread_id=None, parent_id=None): 'anonymous': anonymous, 'anonymous_to_peers': anonymous_to_peers, 'user_id': request.user.id, - 'course_id': course_id, + 'course_id': course_key, 'thread_id': thread_id, 'parent_id': parent_id, }) @@ -192,7 +191,7 @@ def _create_comment(request, course_id, thread_id=None, parent_id=None): user = cc.User.from_django_user(request.user) user.follow(comment.thread) if request.is_ajax(): - return ajax_content_response(request, course_id, comment.to_dict()) + return ajax_content_response(request, course_key, comment.to_dict()) else: return JsonResponse(utils.safe_content(comment.to_dict())) @@ -208,7 +207,7 @@ def create_comment(request, course_id, thread_id): if cc_settings.MAX_COMMENT_DEPTH is not None: if cc_settings.MAX_COMMENT_DEPTH < 0: return JsonError(_("Comment level too deep")) - return _create_comment(request, course_id, thread_id=thread_id) + return _create_comment(request, SlashSeparatedCourseKey.from_deprecated_string(course_id), thread_id=thread_id) @require_POST @@ -238,7 +237,7 @@ def update_comment(request, course_id, comment_id): comment.update_attributes(**extract(request.POST, ['body'])) comment.save() if request.is_ajax(): - return ajax_content_response(request, course_id, comment.to_dict()) + return ajax_content_response(request, SlashSeparatedCourseKey.from_deprecated_string(course_id), comment.to_dict()) else: return JsonResponse(utils.safe_content(comment.to_dict())) @@ -271,7 +270,7 @@ def openclose_thread(request, course_id, thread_id): thread = thread.to_dict() return JsonResponse({ 'content': utils.safe_content(thread), - 'ability': utils.get_ability(course_id, thread, request.user), + 'ability': utils.get_ability(SlashSeparatedCourseKey.from_deprecated_string(course_id), thread, request.user), }) @@ -286,7 +285,7 @@ def create_sub_comment(request, course_id, comment_id): if cc_settings.MAX_COMMENT_DEPTH is not None: if cc_settings.MAX_COMMENT_DEPTH <= cc.Comment.find(comment_id).depth: return JsonError(_("Comment level too deep")) - return _create_comment(request, course_id, parent_id=comment_id) + return _create_comment(request, SlashSeparatedCourseKey.from_deprecated_string(course_id), parent_id=comment_id) @require_POST @@ -366,10 +365,11 @@ def un_flag_abuse_for_thread(request, course_id, thread_id): ajax only """ user = cc.User.from_django_user(request.user) + course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) course = get_course_by_id(course_id) thread = cc.Thread.find(thread_id) - removeAll = cached_has_permission(request.user, 'openclose_thread', course_id) or has_access(request.user, course, 'staff') - thread.unFlagAbuse(user, thread, removeAll) + remove_all = cached_has_permission(request.user, 'openclose_thread', course_id) or has_access(request.user, 'staff', course) + thread.unFlagAbuse(user, thread, remove_all) return JsonResponse(utils.safe_content(thread.to_dict())) @@ -396,10 +396,11 @@ def un_flag_abuse_for_comment(request, course_id, comment_id): ajax only """ user = cc.User.from_django_user(request.user) - course = get_course_by_id(course_id) - removeAll = cached_has_permission(request.user, 'openclose_thread', course_id) or has_access(request.user, course, 'staff') + course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) + course = get_course_by_id(course_key) + remove_all = cached_has_permission(request.user, 'openclose_thread', course_key) or has_access(request.user, 'staff', course) comment = cc.Comment.find(comment_id) - comment.unFlagAbuse(user, comment, removeAll) + comment.unFlagAbuse(user, comment, remove_all) return JsonResponse(utils.safe_content(comment.to_dict())) diff --git a/lms/djangoapps/django_comment_client/forum/tests.py b/lms/djangoapps/django_comment_client/forum/tests.py index fb32c94118..f66246fad4 100644 --- a/lms/djangoapps/django_comment_client/forum/tests.py +++ b/lms/djangoapps/django_comment_client/forum/tests.py @@ -64,7 +64,7 @@ class ViewsExceptionTestCase(UrlResetMixin, ModuleStoreTestCase): mock_from_django_user.return_value = Mock() url = reverse('django_comment_client.forum.views.user_profile', - kwargs={'course_id': self.course.id, 'user_id': '12345'}) # There is no user 12345 + kwargs={'course_id': self.course.id.to_deprecated_string(), 'user_id': '12345'}) # There is no user 12345 self.response = self.client.get(url) self.assertEqual(self.response.status_code, 404) @@ -81,7 +81,7 @@ class ViewsExceptionTestCase(UrlResetMixin, ModuleStoreTestCase): mock_from_django_user.return_value = Mock() url = reverse('django_comment_client.forum.views.followed_threads', - kwargs={'course_id': self.course.id, 'user_id': '12345'}) # There is no user 12345 + kwargs={'course_id': self.course.id.to_deprecated_string(), 'user_id': '12345'}) # There is no user 12345 self.response = self.client.get(url) self.assertEqual(self.response.status_code, 404) @@ -173,7 +173,7 @@ class SingleThreadTestCase(ModuleStoreTestCase): request.user = self.student response = views.single_thread( request, - self.course.id, + self.course.id.to_deprecated_string(), "dummy_discussion_id", "test_thread_id" ) @@ -208,7 +208,7 @@ class SingleThreadTestCase(ModuleStoreTestCase): request.user = self.student response = views.single_thread( request, - self.course.id, + self.course.id.to_deprecated_string(), "dummy_discussion_id", "test_thread_id" ) @@ -237,7 +237,7 @@ class SingleThreadTestCase(ModuleStoreTestCase): request = RequestFactory().post("dummy_url") response = views.single_thread( request, - self.course.id, + self.course.id.to_deprecated_string(), "dummy_discussion_id", "dummy_thread_id" ) @@ -252,7 +252,7 @@ class SingleThreadTestCase(ModuleStoreTestCase): Http404, views.single_thread, request, - self.course.id, + self.course.id.to_deprecated_string(), "test_discussion_id", "test_thread_id" ) @@ -277,7 +277,7 @@ class UserProfileTestCase(ModuleStoreTestCase): request.user = self.student response = views.user_profile( request, - self.course.id, + self.course.id.to_deprecated_string(), self.profiled_user.id ) mock_request.assert_any_call( @@ -342,7 +342,7 @@ class UserProfileTestCase(ModuleStoreTestCase): with self.assertRaises(Http404): response = views.user_profile( request, - self.course.id, + self.course.id.to_deprecated_string(), -999 ) @@ -362,7 +362,7 @@ class UserProfileTestCase(ModuleStoreTestCase): request.user = self.student response = views.user_profile( request, - self.course.id, + self.course.id.to_deprecated_string(), self.profiled_user.id ) self.assertEqual(response.status_code, 405) @@ -406,7 +406,7 @@ class CommentsServiceRequestHeadersTestCase(UrlResetMixin, ModuleStoreTestCase): reverse( "django_comment_client.forum.views.single_thread", kwargs={ - "course_id": self.course.id, + "course_id": self.course.id.to_deprecated_string(), "discussion_id": "dummy", "thread_id": thread_id, } @@ -422,7 +422,7 @@ class CommentsServiceRequestHeadersTestCase(UrlResetMixin, ModuleStoreTestCase): self.client.get( reverse( "django_comment_client.forum.views.forum_form_discussion", - kwargs={"course_id": self.course.id} + kwargs={"course_id": self.course.id.to_deprecated_string()} ), ) self.assert_all_calls_have_header(mock_request, "X-Edx-Api-Key", "test_api_key") @@ -441,7 +441,7 @@ class InlineDiscussionUnicodeTestCase(ModuleStoreTestCase, UnicodeTestMixin): request = RequestFactory().get("dummy_url") request.user = self.student - response = views.inline_discussion(request, self.course.id, "dummy_discussion_id") + response = views.inline_discussion(request, self.course.id.to_deprecated_string(), "dummy_discussion_id") self.assertEqual(response.status_code, 200) response_data = json.loads(response.content) self.assertEqual(response_data["discussion_data"][0]["title"], text) @@ -462,7 +462,7 @@ class ForumFormDiscussionUnicodeTestCase(ModuleStoreTestCase, UnicodeTestMixin): request.user = self.student request.META["HTTP_X_REQUESTED_WITH"] = "XMLHttpRequest" # so request.is_ajax() == True - response = views.forum_form_discussion(request, self.course.id) + response = views.forum_form_discussion(request, self.course.id.to_deprecated_string()) self.assertEqual(response.status_code, 200) response_data = json.loads(response.content) self.assertEqual(response_data["discussion_data"][0]["title"], text) @@ -484,7 +484,7 @@ class SingleThreadUnicodeTestCase(ModuleStoreTestCase, UnicodeTestMixin): request.user = self.student request.META["HTTP_X_REQUESTED_WITH"] = "XMLHttpRequest" # so request.is_ajax() == True - response = views.single_thread(request, self.course.id, "dummy_discussion_id", thread_id) + response = views.single_thread(request, self.course.id.to_deprecated_string(), "dummy_discussion_id", thread_id) self.assertEqual(response.status_code, 200) response_data = json.loads(response.content) self.assertEqual(response_data["content"]["title"], text) @@ -505,7 +505,7 @@ class UserProfileUnicodeTestCase(ModuleStoreTestCase, UnicodeTestMixin): request.user = self.student request.META["HTTP_X_REQUESTED_WITH"] = "XMLHttpRequest" # so request.is_ajax() == True - response = views.user_profile(request, self.course.id, str(self.student.id)) + response = views.user_profile(request, self.course.id.to_deprecated_string(), str(self.student.id)) self.assertEqual(response.status_code, 200) response_data = json.loads(response.content) self.assertEqual(response_data["discussion_data"][0]["title"], text) @@ -526,7 +526,7 @@ class FollowedThreadsUnicodeTestCase(ModuleStoreTestCase, UnicodeTestMixin): request.user = self.student request.META["HTTP_X_REQUESTED_WITH"] = "XMLHttpRequest" # so request.is_ajax() == True - response = views.followed_threads(request, self.course.id, str(self.student.id)) + response = views.followed_threads(request, self.course.id.to_deprecated_string(), str(self.student.id)) self.assertEqual(response.status_code, 200) response_data = json.loads(response.content) self.assertEqual(response_data["discussion_data"][0]["title"], text) diff --git a/lms/djangoapps/django_comment_client/forum/views.py b/lms/djangoapps/django_comment_client/forum/views.py index 4a29d8e5b9..957629e3e0 100644 --- a/lms/djangoapps/django_comment_client/forum/views.py +++ b/lms/djangoapps/django_comment_client/forum/views.py @@ -21,6 +21,8 @@ from django_comment_client.utils import (merge_dict, extract, strip_none, add_co import django_comment_client.utils as utils import lms.lib.comment_client as cc +from xmodule.modulestore.locations import SlashSeparatedCourseKey + THREADS_PER_PAGE = 20 INLINE_THREADS_PER_PAGE = 20 PAGES_NEARBY_DELTA = 2 @@ -41,7 +43,7 @@ def get_threads(request, course_id, discussion_id=None, per_page=THREADS_PER_PAG 'sort_order': 'desc', 'text': '', 'commentable_id': discussion_id, - 'course_id': course_id, + 'course_id': course_id.to_deprecated_string(), 'user_id': request.user.id, } @@ -111,8 +113,9 @@ def inline_discussion(request, course_id, discussion_id): Renders JSON for DiscussionModules """ nr_transaction = newrelic.agent.current_transaction() + course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) - course = get_course_with_access(request.user, course_id, 'load_forum') + course = get_course_with_access(request.user, 'load_forum', course_id) threads, query_params = get_threads(request, course_id, discussion_id, per_page=INLINE_THREADS_PER_PAGE) cc_user = cc.User.from_django_user(request.user) @@ -166,9 +169,10 @@ def forum_form_discussion(request, course_id): """ Renders the main Discussion page, potentially filtered by a search query """ + course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) nr_transaction = newrelic.agent.current_transaction() - course = get_course_with_access(request.user, course_id, 'load_forum') + course = get_course_with_access(request.user, 'load_forum', course_id) with newrelic.agent.FunctionTrace(nr_transaction, "get_discussion_category_map"): category_map = utils.get_discussion_category_map(course) @@ -206,13 +210,13 @@ def forum_form_discussion(request, course_id): 'csrf': csrf(request)['csrf_token'], 'course': course, #'recent_active_threads': recent_active_threads, - 'staff_access': has_access(request.user, course, 'staff'), + 'staff_access': has_access(request.user, 'staff', course), 'threads': saxutils.escape(json.dumps(threads), escapedict), 'thread_pages': query_params['num_pages'], 'user_info': saxutils.escape(json.dumps(user_info), escapedict), - 'flag_moderator': cached_has_permission(request.user, 'openclose_thread', course.id) or has_access(request.user, course, 'staff'), + 'flag_moderator': cached_has_permission(request.user, 'openclose_thread', course.id) or has_access(request.user, 'staff', course), 'annotated_content_info': saxutils.escape(json.dumps(annotated_content_info), escapedict), - 'course_id': course.id, + 'course_id': course.id.to_deprecated_string(), 'category_map': category_map, 'roles': saxutils.escape(json.dumps(utils.get_role_ids(course_id)), escapedict), 'is_moderator': cached_has_permission(request.user, "see_all_cohorts", course_id), @@ -228,9 +232,10 @@ def forum_form_discussion(request, course_id): @require_GET @login_required def single_thread(request, course_id, discussion_id, thread_id): + course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) nr_transaction = newrelic.agent.current_transaction() - course = get_course_with_access(request.user, course_id, 'load_forum') + course = get_course_with_access(request.user, 'load_forum', course_id) cc_user = cc.User.from_django_user(request.user) user_info = cc_user.to_dict() @@ -267,7 +272,7 @@ def single_thread(request, course_id, discussion_id, thread_id): threads, query_params = get_threads(request, course_id) threads.append(thread.to_dict()) - course = get_course_with_access(request.user, course_id, 'load_forum') + course = get_course_with_access(request.user, 'load_forum', course_id) with newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"): add_courseware_context(threads, course) @@ -298,7 +303,7 @@ def single_thread(request, course_id, discussion_id, thread_id): 'annotated_content_info': saxutils.escape(json.dumps(annotated_content_info), escapedict), 'course': course, #'recent_active_threads': recent_active_threads, - 'course_id': course.id, # TODO: Why pass both course and course.id to template? + 'course_id': course.id.to_deprecated_string(), # TODO: Why pass both course and course.id to template? 'thread_id': thread_id, 'threads': saxutils.escape(json.dumps(threads), escapedict), 'category_map': category_map, @@ -306,7 +311,7 @@ def single_thread(request, course_id, discussion_id, thread_id): 'thread_pages': query_params['num_pages'], 'is_course_cohorted': is_course_cohorted(course_id), 'is_moderator': cached_has_permission(request.user, "see_all_cohorts", course_id), - 'flag_moderator': cached_has_permission(request.user, 'openclose_thread', course.id) or has_access(request.user, course, 'staff'), + 'flag_moderator': cached_has_permission(request.user, 'openclose_thread', course.id) or has_access(request.user, 'staff', course), 'cohorts': cohorts, 'user_cohort': get_cohort_id(request.user, course_id), 'cohorted_commentables': cohorted_commentables @@ -317,10 +322,11 @@ def single_thread(request, course_id, discussion_id, thread_id): @require_GET @login_required def user_profile(request, course_id, user_id): + course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) nr_transaction = newrelic.agent.current_transaction() #TODO: Allow sorting? - course = get_course_with_access(request.user, course_id, 'load_forum') + course = get_course_with_access(request.user, 'load_forum', course_id) try: profiled_user = cc.User(id=user_id, course_id=course_id) @@ -365,9 +371,10 @@ def user_profile(request, course_id, user_id): @login_required def followed_threads(request, course_id, user_id): + course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) nr_transaction = newrelic.agent.current_transaction() - course = get_course_with_access(request.user, course_id, 'load_forum') + course = get_course_with_access(request.user, 'load_forum', course_id) try: profiled_user = cc.User(id=user_id, course_id=course_id) diff --git a/lms/djangoapps/django_comment_client/management/commands/seed_permissions_roles.py b/lms/djangoapps/django_comment_client/management/commands/seed_permissions_roles.py index 1073d7dbcf..92a82e7946 100644 --- a/lms/djangoapps/django_comment_client/management/commands/seed_permissions_roles.py +++ b/lms/djangoapps/django_comment_client/management/commands/seed_permissions_roles.py @@ -1,5 +1,6 @@ from django.core.management.base import BaseCommand, CommandError from django_comment_common.utils import seed_permissions_roles +from xmodule.modulestore.locations import SlashSeparatedCourseKey class Command(BaseCommand): @@ -11,6 +12,6 @@ class Command(BaseCommand): raise CommandError("Please provide a course id") if len(args) > 1: raise CommandError("Too many arguments") - course_id = args[0] + course_id = SlashSeparatedCourseKey.from_deprecated_string(args[0]) seed_permissions_roles(course_id) diff --git a/lms/djangoapps/django_comment_client/tests/test_models.py b/lms/djangoapps/django_comment_client/tests/test_models.py index 6d46df113a..376be6d3ed 100644 --- a/lms/djangoapps/django_comment_client/tests/test_models.py +++ b/lms/djangoapps/django_comment_client/tests/test_models.py @@ -1,13 +1,15 @@ import django_comment_common.models as models from django.test import TestCase +from xmodule.modulestore.locations import SlashSeparatedCourseKey + class RoleClassTestCase(TestCase): def setUp(self): # For course ID, syntax edx/classname/classdate is important # because xmodel.course_module.id_to_location looks for a string to split - self.course_id = "edX/toy/2012_Fall" + self.course_id = SlashSeparatedCourseKey("edX", "toy", "2012_Fall") self.student_role = models.Role.objects.get_or_create(name="Student", course_id=self.course_id)[0] self.student_role.add_permission("delete_thread") @@ -15,7 +17,7 @@ class RoleClassTestCase(TestCase): course_id=self.course_id)[0] self.TA_role = models.Role.objects.get_or_create(name="Community TA", course_id=self.course_id)[0] - self.course_id_2 = "edx/6.002x/2012_Fall" + self.course_id_2 = SlashSeparatedCourseKey("edx", "6.002x", "2012_Fall") self.TA_role_2 = models.Role.objects.get_or_create(name="Community TA", course_id=self.course_id_2)[0] diff --git a/lms/djangoapps/django_comment_client/tests/test_utils.py b/lms/djangoapps/django_comment_client/tests/test_utils.py index 0ad65c4881..f9bd599bb2 100644 --- a/lms/djangoapps/django_comment_client/tests/test_utils.py +++ b/lms/djangoapps/django_comment_client/tests/test_utils.py @@ -41,9 +41,11 @@ class DictionaryTestCase(TestCase): self.assertEqual(utils.merge_dict(d1, d2), expected) +@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) class AccessUtilsTestCase(TestCase): def setUp(self): - self.course_id = 'edX/toy/2012_Fall' + self.course = CourseFactory.create() + self.course_id = self.course.id self.student_role = RoleFactory(name='Student', course_id=self.course_id) self.moderator_role = RoleFactory(name='Moderator', course_id=self.course_id) self.community_ta_role = RoleFactory(name='Community TA', course_id=self.course_id) @@ -121,8 +123,8 @@ class CoursewareContextTestCase(ModuleStoreTestCase): reverse( "jump_to", kwargs={ - "course_id": self.course.location.course_id, - "location": discussion.location + "course_id": self.course.id.to_deprecated_string(), + "location": discussion.location.to_deprecated_string() } ) ) diff --git a/lms/djangoapps/django_comment_client/utils.py b/lms/djangoapps/django_comment_client/utils.py index a0ff410ac4..1e5d3e9c9d 100644 --- a/lms/djangoapps/django_comment_client/utils.py +++ b/lms/djangoapps/django_comment_client/utils.py @@ -15,8 +15,9 @@ from edxmako import lookup_template import pystache_custom as pystache from xmodule.modulestore.django import modulestore -from xmodule.modulestore import Location from django.utils.timezone import UTC +from xmodule.modulestore.locations import i4xEncoder, SlashSeparatedCourseKey +import json log = logging.getLogger(__name__) @@ -55,10 +56,7 @@ def has_forum_access(uname, course_id, rolename): def _get_discussion_modules(course): - all_modules = modulestore().get_items( - Location('i4x', course.location.org, course.location.course, 'discussion', None), - course_id=course.id - ) + all_modules = modulestore().get_items(course.id, category='discussion') def has_required_keys(module): for key in ('discussion_id', 'discussion_category', 'discussion_target'): @@ -198,7 +196,7 @@ def get_discussion_category_map(course): class JsonResponse(HttpResponse): def __init__(self, data=None): - content = simplejson.dumps(data) + content = json.dumps(data, cls=i4xEncoder) super(JsonResponse, self).__init__(content, mimetype='application/json; charset=utf-8') @@ -311,12 +309,16 @@ def render_mustache(template_name, dictionary, *args, **kwargs): def permalink(content): + if isinstance(content['course_id'], SlashSeparatedCourseKey): + course_id = content['course_id'].to_deprecated_string() + else: + course_id = content['course_id'] if content['type'] == 'thread': return reverse('django_comment_client.forum.views.single_thread', - args=[content['course_id'], content['commentable_id'], content['id']]) + args=[course_id, content['commentable_id'], content['id']]) else: return reverse('django_comment_client.forum.views.single_thread', - args=[content['course_id'], content['commentable_id'], content['thread_id']]) + '#' + content['id'] + args=[course_id, content['commentable_id'], content['thread_id']]) + '#' + content['id'] def extend_content(content): @@ -344,10 +346,10 @@ def add_courseware_context(content_list, course): for content in content_list: commentable_id = content['commentable_id'] if commentable_id in id_map: - location = id_map[commentable_id]["location"].url() + location = id_map[commentable_id]["location"].to_deprecated_string() title = id_map[commentable_id]["title"] - url = reverse('jump_to', kwargs={"course_id": course.location.course_id, + url = reverse('jump_to', kwargs={"course_id": course.id.to_deprecated_string(), "location": location}) content.update({"courseware_url": url, "courseware_title": title}) diff --git a/lms/djangoapps/foldit/tests.py b/lms/djangoapps/foldit/tests.py index 4e30fe7007..a5992ae6a4 100644 --- a/lms/djangoapps/foldit/tests.py +++ b/lms/djangoapps/foldit/tests.py @@ -8,11 +8,12 @@ from django.core.urlresolvers import reverse from foldit.views import foldit_ops, verify_code from foldit.models import PuzzleComplete, Score -from student.models import unique_id_for_user -from student.tests.factories import CourseEnrollmentFactory, UserFactory +from student.models import unique_id_for_user, CourseEnrollment +from student.tests.factories import UserFactory from datetime import datetime, timedelta from pytz import UTC +from xmodule.modulestore.locations import SlashSeparatedCourseKey log = logging.getLogger(__name__) @@ -23,18 +24,14 @@ class FolditTestCase(TestCase): self.factory = RequestFactory() self.url = reverse('foldit_ops') - self.course_id = 'course/id/1' - self.course_id2 = 'course/id/2' + self.course_id = SlashSeparatedCourseKey('course', 'id', '1') + self.course_id2 = SlashSeparatedCourseKey('course', 'id', '2') self.user = UserFactory.create() self.user2 = UserFactory.create() - self.course_enrollment = CourseEnrollmentFactory.create( - user=self.user, course_id=self.course_id - ) - self.course_enrollment2 = CourseEnrollmentFactory.create( - user=self.user2, course_id=self.course_id2 - ) + CourseEnrollment.enroll(self.user, self.course_id) + CourseEnrollment.enroll(self.user2, self.course_id2) now = datetime.now(UTC) self.tomorrow = now + timedelta(days=1) diff --git a/lms/djangoapps/instructor/access.py b/lms/djangoapps/instructor/access.py index e513365bb5..168814256e 100644 --- a/lms/djangoapps/instructor/access.py +++ b/lms/djangoapps/instructor/access.py @@ -31,7 +31,7 @@ def list_with_level(course, level): There could be other levels specific to the course. If there is no Group for that course-level, returns an empty list """ - return ROLES[level](course.location).users_with_role() + return ROLES[level](course.id).users_with_role() def allow_access(course, user, level): @@ -63,7 +63,7 @@ def _change_access(course, user, level, action): """ try: - role = ROLES[level](course.location) + role = ROLES[level](course.id) except KeyError: raise ValueError("unrecognized level '{}'".format(level)) diff --git a/lms/djangoapps/instructor/enrollment.py b/lms/djangoapps/instructor/enrollment.py index 1651e91a18..1cc4d28c66 100644 --- a/lms/djangoapps/instructor/enrollment.py +++ b/lms/djangoapps/instructor/enrollment.py @@ -86,7 +86,6 @@ def enroll_email(course_id, student_email, auto_enroll=False, email_students=Fal returns two EmailEnrollmentState's representing state before and after the action. """ - previous_state = EmailEnrollmentState(course_id, student_email) if previous_state.user: @@ -121,7 +120,6 @@ def unenroll_email(course_id, student_email, email_students=False, email_params= returns two EmailEnrollmentState's representing state before and after the action. """ - previous_state = EmailEnrollmentState(course_id, student_email) if previous_state.enrollment: @@ -200,7 +198,7 @@ def reset_student_attempts(course_id, student, module_state_key, delete_module=F module_to_reset = StudentModule.objects.get( student_id=student.id, course_id=course_id, - module_state_key=module_state_key + module_id=module_state_key ) if delete_module: @@ -237,13 +235,15 @@ def get_email_params(course, auto_enroll): 'SITE_NAME', settings.SITE_NAME ) + # TODO: Use request.build_absolute_uri rather than 'https://{}{}'.format + # and check with the Services team that this works well with microsites registration_url = u'https://{}{}'.format( stripped_site_name, reverse('student.views.register_user') ) course_url = u'https://{}{}'.format( stripped_site_name, - reverse('course_root', kwargs={'course_id': course.id}) + reverse('course_root', kwargs={'course_id': course.id.to_deprecated_string()}) ) # We can't get the url to the course's About page if the marketing site is enabled. @@ -251,7 +251,7 @@ def get_email_params(course, auto_enroll): if not settings.FEATURES.get('ENABLE_MKTG_SITE', False): course_about_url = u'https://{}{}'.format( stripped_site_name, - reverse('about_course', kwargs={'course_id': course.id}) + reverse('about_course', kwargs={'course_id': course.id.to_deprecated_string()}) ) is_shib_course = uses_shib(course) diff --git a/lms/djangoapps/instructor/features/bulk_email.py b/lms/djangoapps/instructor/features/bulk_email.py index 821e6c9f73..d17fff4ab9 100644 --- a/lms/djangoapps/instructor/features/bulk_email.py +++ b/lms/djangoapps/instructor/features/bulk_email.py @@ -29,23 +29,23 @@ def make_populated_course(step): # pylint: disable=unused-argument number='888', display_name='Bulk Email Test Course' ) - world.bulk_email_course_id = 'edx/888/Bulk_Email_Test_Course' + world.bulk_email_course_id = course.id try: # See if we've defined the instructor & staff user yet world.bulk_email_instructor except AttributeError: # Make & register an instructor for the course - world.bulk_email_instructor = InstructorFactory(course=course.location) + world.bulk_email_instructor = InstructorFactory(course=world.bulk_email_course_id) world.enroll_user(world.bulk_email_instructor, world.bulk_email_course_id) # Make & register a staff member - world.bulk_email_staff = StaffFactory(course=course.location) + world.bulk_email_staff = StaffFactory(course=course.id) world.enroll_user(world.bulk_email_staff, world.bulk_email_course_id) # Make & register a student - world.register_by_course_id( - 'edx/888/Bulk_Email_Test_Course', + world.register_by_course_key( + course.id, username='student', password='test', is_staff=False diff --git a/lms/djangoapps/instructor/features/common.py b/lms/djangoapps/instructor/features/common.py index d6c23a1518..c469264bdd 100644 --- a/lms/djangoapps/instructor/features/common.py +++ b/lms/djangoapps/instructor/features/common.py @@ -43,12 +43,12 @@ def i_am_staff_or_instructor(step, role): # pylint: disable=unused-argument display_name='Test Course' ) - world.course_id = 'edx/999/Test_Course' + world.course_id = course.id world.role = 'instructor' # Log in as the an instructor or staff for the course if role == 'instructor': # Make & register an instructor for the course - world.instructor = InstructorFactory(course=course.location) + world.instructor = InstructorFactory(course=world.course_id) world.enroll_user(world.instructor, world.course_id) world.log_in( @@ -61,7 +61,7 @@ def i_am_staff_or_instructor(step, role): # pylint: disable=unused-argument else: world.role = 'staff' # Make & register a staff member - world.staff = StaffFactory(course=course.location) + world.staff = StaffFactory(course=world.course_id) world.enroll_user(world.staff, world.course_id) world.log_in( diff --git a/lms/djangoapps/instructor/hint_manager.py b/lms/djangoapps/instructor/hint_manager.py index c546b668c0..30604da34b 100644 --- a/lms/djangoapps/instructor/hint_manager.py +++ b/lms/djangoapps/instructor/hint_manager.py @@ -19,8 +19,9 @@ from courseware.courses import get_course_with_access from courseware.models import XModuleUserStateSummaryField import courseware.module_render as module_render import courseware.model_data as model_data -from xmodule.modulestore import Location from xmodule.modulestore.django import modulestore +from xmodule.modulestore.locations import SlashSeparatedCourseKey +from xmodule.modulestore.exceptions import ItemNotFoundError @ensure_csrf_cookie @@ -28,13 +29,14 @@ def hint_manager(request, course_id): """ The URL landing function for all calls to the hint manager, both POST and GET. """ + course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) try: - get_course_with_access(request.user, course_id, 'staff', depth=None) + get_course_with_access(request.user, 'staff', course_key, depth=None) except Http404: out = 'Sorry, but students are not allowed to access the hint manager!' return HttpResponse(out) if request.method == 'GET': - out = get_hints(request, course_id, 'mod_queue') + out = get_hints(request, course_key, 'mod_queue') out.update({'error': ''}) return render_to_response('instructor/hint_manager.html', out) field = request.POST['field'] @@ -52,10 +54,10 @@ def hint_manager(request, course_id): } # Do the operation requested, and collect any error messages. - error_text = switch_dict[request.POST['op']](request, course_id, field) + error_text = switch_dict[request.POST['op']](request, course_key, field) if error_text is None: error_text = '' - render_dict = get_hints(request, course_id, field) + render_dict = get_hints(request, course_key, field) render_dict.update({'error': error_text}) rendered_html = render_to_string('instructor/hint_manager_inner.html', render_dict) return HttpResponse(json.dumps({'success': True, 'contents': rendered_html})) @@ -86,13 +88,13 @@ def get_hints(request, course_id, field): other_field = 'mod_queue' field_label = 'Approved Hints' other_field_label = 'Hints Awaiting Moderation' - # The course_id is of the form school/number/classname. # We want to use the course_id to find all matching usage_id's. # To do this, just take the school/number part - leave off the classname. - course_id_dict = Location.parse_course_id(course_id) - chopped_id = u'{org}/{course}'.format(**course_id_dict) - chopped_id = re.escape(chopped_id) - all_hints = XModuleUserStateSummaryField.objects.filter(field_name=field, usage_id__regex=chopped_id) + # FIXME: we need to figure out how to do this with opaque keys + all_hints = XModuleUserStateSummaryField.objects.filter( + field_name=field, + usage_id__regex=re.escape(u'{0.org}/{0.course}'.format(course_id)), + ) # big_out_dict[problem id] = [[answer, {pk: [hint, votes]}], sorted by answer] # big_out_dict maps a problem id to a list of [answer, hints] pairs, sorted in order of answer. big_out_dict = {} @@ -101,8 +103,8 @@ def get_hints(request, course_id, field): id_to_name = {} for hints_by_problem in all_hints: - loc = Location(hints_by_problem.usage_id) - name = location_to_problem_name(course_id, loc) + hints_by_problem.usage_id = hints_by_problem.usage_id.map_into_course(course_id) + name = location_to_problem_name(course_id, hints_by_problem.usage_id) if name is None: continue id_to_name[hints_by_problem.usage_id] = name @@ -138,9 +140,9 @@ def location_to_problem_name(course_id, loc): problem it wraps around. Return None if the hinter no longer exists. """ try: - descriptor = modulestore().get_items(loc, course_id=course_id)[0] + descriptor = modulestore().get_item(loc) return descriptor.get_children()[0].display_name - except IndexError: + except ItemNotFoundError: # Sometimes, the problem is no longer in the course. Just # don't include said problem. return None @@ -164,9 +166,10 @@ def delete_hints(request, course_id, field): if key == 'op' or key == 'field': continue problem_id, answer, pk = request.POST.getlist(key) + problem_key = course_id.make_usage_key_from_deprecated_string(problem_id) # Can be optimized - sort the delete list by problem_id, and load each problem # from the database only once. - this_problem = XModuleUserStateSummaryField.objects.get(field_name=field, usage_id=problem_id) + this_problem = XModuleUserStateSummaryField.objects.get(field_name=field, usage_id=problem_key) problem_dict = json.loads(this_problem.value) del problem_dict[answer][pk] this_problem.value = json.dumps(problem_dict) @@ -191,7 +194,8 @@ def change_votes(request, course_id, field): if key == 'op' or key == 'field': continue problem_id, answer, pk, new_votes = request.POST.getlist(key) - this_problem = XModuleUserStateSummaryField.objects.get(field_name=field, usage_id=problem_id) + problem_key = course_id.make_usage_key_from_deprecated_string(problem_id) + this_problem = XModuleUserStateSummaryField.objects.get(field_name=field, usage_id=problem_key) problem_dict = json.loads(this_problem.value) # problem_dict[answer][pk] points to a [hint_text, #votes] pair. problem_dict[answer][pk][1] = int(new_votes) @@ -210,23 +214,27 @@ def add_hint(request, course_id, field): """ problem_id = request.POST['problem'] + problem_key = course_id.make_usage_key_from_deprecated_string(problem_id) answer = request.POST['answer'] hint_text = request.POST['hint'] # Validate the answer. This requires initializing the xmodules, which # is annoying. - loc = Location(problem_id) - descriptors = modulestore().get_items(loc, course_id=course_id) + try: + descriptor = modulestore().get_item(problem_key) + descriptors = [descriptor] + except ItemNotFoundError: + descriptors = [] field_data_cache = model_data.FieldDataCache(descriptors, course_id, request.user) - hinter_module = module_render.get_module(request.user, request, loc, field_data_cache, course_id) + hinter_module = module_render.get_module(request.user, request, problem_key, field_data_cache, course_id) if not hinter_module.validate_answer(answer): # Invalid answer. Don't add it to the database, or else the # hinter will crash when we encounter it. return 'Error - the answer you specified is not properly formatted: ' + str(answer) - this_problem = XModuleUserStateSummaryField.objects.get(field_name=field, usage_id=problem_id) + this_problem = XModuleUserStateSummaryField.objects.get(field_name=field, usage_id=problem_key) - hint_pk_entry = XModuleUserStateSummaryField.objects.get(field_name='hint_pk', usage_id=problem_id) + hint_pk_entry = XModuleUserStateSummaryField.objects.get(field_name='hint_pk', usage_id=problem_key) this_pk = int(hint_pk_entry.value) hint_pk_entry.value = this_pk + 1 hint_pk_entry.save() @@ -253,16 +261,17 @@ def approve(request, course_id, field): if key == 'op' or key == 'field': continue problem_id, answer, pk = request.POST.getlist(key) + problem_key = course_id.make_usage_key_from_deprecated_string(problem_id) # Can be optimized - sort the delete list by problem_id, and load each problem # from the database only once. - problem_in_mod = XModuleUserStateSummaryField.objects.get(field_name=field, usage_id=problem_id) + problem_in_mod = XModuleUserStateSummaryField.objects.get(field_name=field, usage_id=problem_key) problem_dict = json.loads(problem_in_mod.value) hint_to_move = problem_dict[answer][pk] del problem_dict[answer][pk] problem_in_mod.value = json.dumps(problem_dict) problem_in_mod.save() - problem_in_hints = XModuleUserStateSummaryField.objects.get(field_name='hints', usage_id=problem_id) + problem_in_hints = XModuleUserStateSummaryField.objects.get(field_name='hints', usage_id=problem_key) problem_dict = json.loads(problem_in_hints.value) if answer not in problem_dict: problem_dict[answer] = {} diff --git a/lms/djangoapps/instructor/management/commands/openended_post.py b/lms/djangoapps/instructor/management/commands/openended_post.py index 12bd4fda55..a2a28ee07a 100644 --- a/lms/djangoapps/instructor/management/commands/openended_post.py +++ b/lms/djangoapps/instructor/management/commands/openended_post.py @@ -6,6 +6,8 @@ from django.core.management.base import BaseCommand from optparse import make_option from xmodule.modulestore.django import modulestore +from xmodule.modulestore.keys import UsageKey +from xmodule.modulestore.locations import SlashSeparatedCourseKey from xmodule.open_ended_grading_classes.openendedchild import OpenEndedChild from xmodule.open_ended_grading_classes.open_ended_module import OpenEndedModule @@ -37,8 +39,8 @@ class Command(BaseCommand): task_number = options['task_number'] if len(args) == 4: - course_id = args[0] - location = args[1] + course_id = SlashSeparatedCourseKey.from_deprecated_string(args[0]) + location = course_id.make_usage_key_from_deprecated_string(args[1]) students_ids = [line.strip() for line in open(args[2])] hostname = args[3] else: @@ -51,7 +53,7 @@ class Command(BaseCommand): print err return - descriptor = modulestore().get_instance(course.id, location, depth=0) + descriptor = modulestore().get_item(location, depth=0) if descriptor is None: print "Location not found in course" return @@ -76,7 +78,7 @@ def post_submission_for_student(student, course, location, task_number, dry_run= request.host = hostname try: - module = get_module_for_student(student, course, location, request=request) + module = get_module_for_student(student, location, request=request) if module is None: print " WARNING: No state found." return False diff --git a/lms/djangoapps/instructor/management/commands/openended_stats.py b/lms/djangoapps/instructor/management/commands/openended_stats.py index 5fd619b484..0cca4446a8 100644 --- a/lms/djangoapps/instructor/management/commands/openended_stats.py +++ b/lms/djangoapps/instructor/management/commands/openended_stats.py @@ -9,6 +9,8 @@ from optparse import make_option from xmodule.modulestore import Location from xmodule.modulestore.django import modulestore +from xmodule.modulestore.keys import UsageKey +from xmodule.modulestore.locations import SlashSeparatedCourseKey from xmodule.open_ended_grading_classes.openendedchild import OpenEndedChild from courseware.courses import get_course @@ -37,8 +39,8 @@ class Command(BaseCommand): task_number = options['task_number'] if len(args) == 2: - course_id = args[0] - location = args[1] + course_id = SlashSeparatedCourseKey.from_deprecated_string(args[0]) + usage_key = UsageKey.from_string(args[1]) else: print self.help return @@ -49,16 +51,16 @@ class Command(BaseCommand): print err return - descriptor = modulestore().get_instance(course.id, location, depth=0) + descriptor = modulestore().get_item(usage_key, depth=0) if descriptor is None: - print "Location {0} not found in course".format(location) + print "Location {0} not found in course".format(usage_key) return try: enrolled_students = CourseEnrollment.users_enrolled_in(course_id) print "Total students enrolled in {0}: {1}".format(course_id, enrolled_students.count()) - calculate_task_statistics(enrolled_students, course, location, task_number) + calculate_task_statistics(enrolled_students, course, usage_key, task_number) except KeyboardInterrupt: print "\nOperation Cancelled" @@ -79,7 +81,7 @@ def calculate_task_statistics(students, course, location, task_number, write_to_ students_with_graded_submissions = [] # pylint: disable=invalid-name students_with_no_state = [] - student_modules = StudentModule.objects.filter(module_state_key=location, student__in=students).order_by('student') + student_modules = StudentModule.objects.filter(module_id=location, student__in=students).order_by('student') print "Total student modules: {0}".format(student_modules.count()) for index, student_module in enumerate(student_modules): @@ -89,7 +91,7 @@ def calculate_task_statistics(students, course, location, task_number, write_to_ student = student_module.student print "{0}:{1}".format(student.id, student.username) - module = get_module_for_student(student, course, location) + module = get_module_for_student(student, location) if module is None: print " WARNING: No state found" students_with_no_state.append(student) @@ -113,8 +115,6 @@ def calculate_task_statistics(students, course, location, task_number, write_to_ elif task_state == OpenEndedChild.POST_ASSESSMENT or task_state == OpenEndedChild.DONE: students_with_graded_submissions.append(student) - location = Location(location) - print "----------------------------------" print "Time: {0}".format(time.strftime("%Y %b %d %H:%M:%S +0000", time.gmtime())) print "Course: {0}".format(course.id) @@ -132,7 +132,7 @@ def calculate_task_statistics(students, course, location, task_number, write_to_ with open('{0}.{1}.csv'.format(filename, time_stamp), 'wb') as csv_file: writer = csv.writer(csv_file, delimiter=' ', quoting=csv.QUOTE_MINIMAL) for student in students_with_ungraded_submissions: - writer.writerow(("ungraded", student.id, anonymous_id_for_user(student, ''), student.username)) + writer.writerow(("ungraded", student.id, anonymous_id_for_user(student, None), student.username)) for student in students_with_graded_submissions: - writer.writerow(("graded", student.id, anonymous_id_for_user(student, ''), student.username)) + writer.writerow(("graded", student.id, anonymous_id_for_user(student, None), student.username)) return stats diff --git a/lms/djangoapps/instructor/management/tests/test_openended_commands.py b/lms/djangoapps/instructor/management/tests/test_openended_commands.py index 48b09583cb..b4c50ae816 100644 --- a/lms/djangoapps/instructor/management/tests/test_openended_commands.py +++ b/lms/djangoapps/instructor/management/tests/test_openended_commands.py @@ -24,14 +24,16 @@ from instructor.management.commands.openended_post import post_submission_for_st from instructor.management.commands.openended_stats import calculate_task_statistics from instructor.utils import get_module_for_student +from xmodule.modulestore.locations import SlashSeparatedCourseKey + @override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) class OpenEndedPostTest(ModuleStoreTestCase): """Test the openended_post management command.""" def setUp(self): - self.course_id = "edX/open_ended/2012_Fall" - self.problem_location = Location(["i4x", "edX", "open_ended", "combinedopenended", "SampleQuestion"]) + self.course_id = SlashSeparatedCourseKey("edX", "open_ended", "2012_Fall") + self.problem_location = Location("edX", "open_ended", "2012_Fall", "combinedopenended", "SampleQuestion") self.self_assessment_task_number = 0 self.open_ended_task_number = 1 @@ -41,7 +43,7 @@ class OpenEndedPostTest(ModuleStoreTestCase): StudentModuleFactory.create( course_id=self.course_id, - module_state_key=self.problem_location, + module_id=self.problem_location, student=self.student_on_initial, grade=0, max_grade=1, @@ -50,7 +52,7 @@ class OpenEndedPostTest(ModuleStoreTestCase): StudentModuleFactory.create( course_id=self.course_id, - module_state_key=self.problem_location, + module_id=self.problem_location, student=self.student_on_accessing, grade=0, max_grade=1, @@ -59,7 +61,7 @@ class OpenEndedPostTest(ModuleStoreTestCase): StudentModuleFactory.create( course_id=self.course_id, - module_state_key=self.problem_location, + module_id=self.problem_location, student=self.student_on_post_assessment, grade=0, max_grade=1, @@ -67,7 +69,7 @@ class OpenEndedPostTest(ModuleStoreTestCase): ) def test_post_submission_for_student_on_initial(self): - course = get_course_with_access(self.student_on_initial, self.course_id, 'load') + course = get_course_with_access(self.student_on_initial, 'load', self.course_id) dry_run_result = post_submission_for_student(self.student_on_initial, course, self.problem_location, self.open_ended_task_number, dry_run=True) self.assertFalse(dry_run_result) @@ -76,7 +78,7 @@ class OpenEndedPostTest(ModuleStoreTestCase): self.assertFalse(result) def test_post_submission_for_student_on_accessing(self): - course = get_course_with_access(self.student_on_accessing, self.course_id, 'load') + course = get_course_with_access(self.student_on_accessing, 'load', self.course_id) dry_run_result = post_submission_for_student(self.student_on_accessing, course, self.problem_location, self.open_ended_task_number, dry_run=True) self.assertFalse(dry_run_result) @@ -84,11 +86,11 @@ class OpenEndedPostTest(ModuleStoreTestCase): with patch('capa.xqueue_interface.XQueueInterface.send_to_queue') as mock_send_to_queue: mock_send_to_queue.return_value = (0, "Successfully queued") - module = get_module_for_student(self.student_on_accessing, course, self.problem_location) + module = get_module_for_student(self.student_on_accessing, self.problem_location) task = module.child_module.get_task_number(self.open_ended_task_number) student_response = "Here is an answer." - student_anonymous_id = anonymous_id_for_user(self.student_on_accessing, '') + student_anonymous_id = anonymous_id_for_user(self.student_on_accessing, None) submission_time = datetime.strftime(datetime.now(UTC), xqueue_interface.dateformat) result = post_submission_for_student(self.student_on_accessing, course, self.problem_location, self.open_ended_task_number, dry_run=False) @@ -102,7 +104,7 @@ class OpenEndedPostTest(ModuleStoreTestCase): self.assertGreaterEqual(body_arg_student_info['submission_time'], submission_time) def test_post_submission_for_student_on_post_assessment(self): - course = get_course_with_access(self.student_on_post_assessment, self.course_id, 'load') + course = get_course_with_access(self.student_on_post_assessment, 'load', self.course_id) dry_run_result = post_submission_for_student(self.student_on_post_assessment, course, self.problem_location, self.open_ended_task_number, dry_run=True) self.assertFalse(dry_run_result) @@ -111,7 +113,7 @@ class OpenEndedPostTest(ModuleStoreTestCase): self.assertFalse(result) def test_post_submission_for_student_invalid_task(self): - course = get_course_with_access(self.student_on_accessing, self.course_id, 'load') + course = get_course_with_access(self.student_on_accessing, 'load', self.course_id) result = post_submission_for_student(self.student_on_accessing, course, self.problem_location, self.self_assessment_task_number, dry_run=False) self.assertFalse(result) @@ -126,8 +128,8 @@ class OpenEndedStatsTest(ModuleStoreTestCase): """Test the openended_stats management command.""" def setUp(self): - self.course_id = "edX/open_ended/2012_Fall" - self.problem_location = Location(["i4x", "edX", "open_ended", "combinedopenended", "SampleQuestion"]) + self.course_id = SlashSeparatedCourseKey("edX", "open_ended", "2012_Fall") + self.problem_location = Location("edX", "open_ended", "2012_Fall", "combinedopenended", "SampleQuestion") self.task_number = 1 self.invalid_task_number = 3 @@ -137,7 +139,7 @@ class OpenEndedStatsTest(ModuleStoreTestCase): StudentModuleFactory.create( course_id=self.course_id, - module_state_key=self.problem_location, + module_id=self.problem_location, student=self.student_on_initial, grade=0, max_grade=1, @@ -146,7 +148,7 @@ class OpenEndedStatsTest(ModuleStoreTestCase): StudentModuleFactory.create( course_id=self.course_id, - module_state_key=self.problem_location, + module_id=self.problem_location, student=self.student_on_accessing, grade=0, max_grade=1, @@ -155,7 +157,7 @@ class OpenEndedStatsTest(ModuleStoreTestCase): StudentModuleFactory.create( course_id=self.course_id, - module_state_key=self.problem_location, + module_id=self.problem_location, student=self.student_on_post_assessment, grade=0, max_grade=1, @@ -165,7 +167,7 @@ class OpenEndedStatsTest(ModuleStoreTestCase): self.students = [self.student_on_initial, self.student_on_accessing, self.student_on_post_assessment] def test_calculate_task_statistics(self): - course = get_course_with_access(self.student_on_accessing, self.course_id, 'load') + course = get_course_with_access(self.student_on_accessing, 'load', self.course_id) stats = calculate_task_statistics(self.students, course, self.problem_location, self.task_number, write_to_file=False) self.assertEqual(stats[OpenEndedChild.INITIAL], 1) self.assertEqual(stats[OpenEndedChild.ASSESSING], 1) diff --git a/lms/djangoapps/instructor/tests/test_access.py b/lms/djangoapps/instructor/tests/test_access.py index 8ee2a9d985..cd25179be7 100644 --- a/lms/djangoapps/instructor/tests/test_access.py +++ b/lms/djangoapps/instructor/tests/test_access.py @@ -50,19 +50,19 @@ class TestInstructorAccessAllow(ModuleStoreTestCase): def test_allow(self): user = UserFactory() allow_access(self.course, user, 'staff') - self.assertTrue(CourseStaffRole(self.course.location).has_user(user)) + self.assertTrue(CourseStaffRole(self.course.id).has_user(user)) def test_allow_twice(self): user = UserFactory() allow_access(self.course, user, 'staff') allow_access(self.course, user, 'staff') - self.assertTrue(CourseStaffRole(self.course.location).has_user(user)) + self.assertTrue(CourseStaffRole(self.course.id).has_user(user)) def test_allow_beta(self): """ Test allow beta against list beta. """ user = UserFactory() allow_access(self.course, user, 'beta') - self.assertTrue(CourseBetaTesterRole(self.course.location).has_user(user)) + self.assertTrue(CourseBetaTesterRole(self.course.id).has_user(user)) @raises(ValueError) def test_allow_badlevel(self): @@ -91,17 +91,17 @@ class TestInstructorAccessRevoke(ModuleStoreTestCase): def test_revoke(self): user = self.staff[0] revoke_access(self.course, user, 'staff') - self.assertFalse(CourseStaffRole(self.course.location).has_user(user)) + self.assertFalse(CourseStaffRole(self.course.id).has_user(user)) def test_revoke_twice(self): user = self.staff[0] revoke_access(self.course, user, 'staff') - self.assertFalse(CourseStaffRole(self.course.location).has_user(user)) + self.assertFalse(CourseStaffRole(self.course.id).has_user(user)) def test_revoke_beta(self): user = self.beta_testers[0] revoke_access(self.course, user, 'beta') - self.assertFalse(CourseBetaTesterRole(self.course.location).has_user(user)) + self.assertFalse(CourseBetaTesterRole(self.course.id).has_user(user)) @raises(ValueError) def test_revoke_badrolename(self): diff --git a/lms/djangoapps/instructor/tests/test_api.py b/lms/djangoapps/instructor/tests/test_api.py index daf46a3db5..12295ffcc5 100644 --- a/lms/djangoapps/instructor/tests/test_api.py +++ b/lms/djangoapps/instructor/tests/test_api.py @@ -12,12 +12,14 @@ from django.test import TestCase from nose.tools import raises from mock import Mock, patch from django.test.utils import override_settings +from django.conf import settings from django.core.urlresolvers import reverse from django.http import HttpRequest, HttpResponse from django_comment_common.models import FORUM_ROLE_COMMUNITY_TA, Role from django_comment_common.utils import seed_permissions_roles from django.core import mail from django.utils.timezone import utc +from django.test import RequestFactory from django.contrib.auth.models import User from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE @@ -27,6 +29,7 @@ from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from student.tests.factories import UserFactory from courseware.tests.factories import StaffFactory, InstructorFactory, BetaTesterFactory from student.roles import CourseBetaTesterRole +from microsite_configuration import microsite from student.models import CourseEnrollment, CourseEnrollmentAllowed from courseware.models import StudentModule @@ -35,10 +38,11 @@ from courseware.models import StudentModule import instructor_task.api from instructor.access import allow_access import instructor.views.api -from instructor.views.api import _split_input_list, _msk_from_problem_urlname, common_exceptions_400 +from instructor.views.api import _split_input_list, common_exceptions_400 from instructor_task.api_helper import AlreadyRunningError +from xmodule.modulestore.locations import SlashSeparatedCourseKey -from .test_tools import get_extended_due +from .test_tools import msk_from_problem_urlname, get_extended_due @common_exceptions_400 @@ -108,14 +112,15 @@ class TestInstructorAPIDenyLevels(ModuleStoreTestCase, LoginEnrollmentTestCase): self.user = UserFactory.create() CourseEnrollment.enroll(self.user, self.course.id) - self.problem_urlname = 'robot-some-problem-urlname' + self.problem_location = msk_from_problem_urlname( + self.course.id, + 'robot-some-problem-urlname' + ) + self.problem_urlname = str(self.problem_location) _module = StudentModule.objects.create( student=self.user, course_id=self.course.id, - module_state_key=_msk_from_problem_urlname( - self.course.id, - self.problem_urlname - ), + module_id=self.problem_location, state=json.dumps({'attempts': 10}), ) @@ -153,13 +158,11 @@ class TestInstructorAPIDenyLevels(ModuleStoreTestCase, LoginEnrollmentTestCase): status_code: expected HTTP status code response msg: message to display if assertion fails. """ - url = reverse(endpoint, kwargs={'course_id': self.course.id}) - if endpoint in 'send_email': + url = reverse(endpoint, kwargs={'course_id': self.course.id.to_deprecated_string()}) + if endpoint in ['send_email']: response = self.client.post(url, args) else: response = self.client.get(url, args) - print endpoint - print response self.assertEqual( response.status_code, status_code, @@ -192,7 +195,7 @@ class TestInstructorAPIDenyLevels(ModuleStoreTestCase, LoginEnrollmentTestCase): """ Ensure that a staff member can't access instructor endpoints. """ - staff_member = StaffFactory(course=self.course.location) + staff_member = StaffFactory(course=self.course.id) CourseEnrollment.enroll(staff_member, self.course.id) self.client.login(username=staff_member.username, password='test') # Try to promote to forums admin - not working @@ -221,7 +224,7 @@ class TestInstructorAPIDenyLevels(ModuleStoreTestCase, LoginEnrollmentTestCase): """ Ensure that an instructor member can access all endpoints. """ - inst = InstructorFactory(course=self.course.location) + inst = InstructorFactory(course=self.course.id) CourseEnrollment.enroll(inst, self.course.id) self.client.login(username=inst.username, password='test') @@ -257,8 +260,9 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase): job of test_enrollment. This tests the response and action switch. """ def setUp(self): + self.request = RequestFactory().request() self.course = CourseFactory.create() - self.instructor = InstructorFactory(course=self.course.location) + self.instructor = InstructorFactory(course=self.course.id) self.client.login(username=self.instructor.username, password='test') self.enrolled_student = UserFactory(username='EnrolledStudent', first_name='Enrolled', last_name='Student') @@ -276,26 +280,41 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase): self.notregistered_email = 'robot-not-an-email-yet@robot.org' self.assertEqual(User.objects.filter(email=self.notregistered_email).count(), 0) + # Email URL values + self.site_name = microsite.get_value( + 'SITE_NAME', + settings.SITE_NAME + ) + self.registration_url = 'https://{}/register'.format(self.site_name) + self.about_url = 'https://{}/courses/MITx/999/Robot_Super_Course/about'.format(self.site_name) + self.course_url = 'https://{}/courses/MITx/999/Robot_Super_Course/'.format(self.site_name) + # uncomment to enable enable printing of large diffs # from failed assertions in the event of a test failure. # (comment because pylint C0103) # self.maxDiff = None + def tearDown(self): + """ + Undo all patches. + """ + patch.stopall() + def test_missing_params(self): """ Test missing all query parameters. """ - url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id}) + url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url) self.assertEqual(response.status_code, 400) def test_bad_action(self): """ Test with an invalid action. """ action = 'robot-not-an-action' - url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id}) + url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url, {'identifiers': self.enrolled_student.email, 'action': action}) self.assertEqual(response.status_code, 400) def test_invalid_email(self): - url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id}) + url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url, {'identifiers': 'percivaloctavius@', 'action': 'enroll', 'email_students': False}) self.assertEqual(response.status_code, 200) @@ -315,7 +334,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase): self.assertEqual(res_json, expected) def test_invalid_username(self): - url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id}) + url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url, {'identifiers': 'percivaloctavius', 'action': 'enroll', 'email_students': False}) self.assertEqual(response.status_code, 200) @@ -335,7 +354,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase): self.assertEqual(res_json, expected) def test_enroll_with_username(self): - url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id}) + url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url, {'identifiers': self.notenrolled_student.username, 'action': 'enroll', 'email_students': False}) self.assertEqual(response.status_code, 200) @@ -366,7 +385,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase): self.assertEqual(res_json, expected) def test_enroll_without_email(self): - url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id}) + url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url, {'identifiers': self.notenrolled_student.email, 'action': 'enroll', 'email_students': False}) print "type(self.notenrolled_student.email): {}".format(type(self.notenrolled_student.email)) self.assertEqual(response.status_code, 200) @@ -405,7 +424,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase): self.assertEqual(len(mail.outbox), 0) def test_enroll_with_email(self): - url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id}) + url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url, {'identifiers': self.notenrolled_student.email, 'action': 'enroll', 'email_students': True}) print "type(self.notenrolled_student.email): {}".format(type(self.notenrolled_student.email)) self.assertEqual(response.status_code, 200) @@ -452,12 +471,14 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase): "at edx.org by a member of the course staff. " "The course should now appear on your edx.org dashboard.\n\n" "To start accessing course materials, please visit " - "https://edx.org/courses/MITx/999/Robot_Super_Course/\n\n----\n" - "This email was automatically sent from edx.org to NotEnrolled Student" + "{course_url}\n\n----\n" + "This email was automatically sent from edx.org to NotEnrolled Student".format( + course_url=self.course_url + ) ) def test_enroll_with_email_not_registered(self): - url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id}) + url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url, {'identifiers': self.notregistered_email, 'action': 'enroll', 'email_students': True}) self.assertEqual(response.status_code, 200) @@ -470,18 +491,20 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase): self.assertEqual( mail.outbox[0].body, "Dear student,\n\nYou have been invited to join Robot Super Course at edx.org by a member of the course staff.\n\n" - "To finish your registration, please visit https://edx.org/register and fill out the registration form " + "To finish your registration, please visit {registration_url} and fill out the registration form " "making sure to use robot-not-an-email-yet@robot.org in the E-mail field.\n" "Once you have registered and activated your account, " - "visit https://edx.org/courses/MITx/999/Robot_Super_Course/about to join the course.\n\n----\n" - "This email was automatically sent from edx.org to robot-not-an-email-yet@robot.org" + "visit {about_url} to join the course.\n\n----\n" + "This email was automatically sent from edx.org to robot-not-an-email-yet@robot.org".format( + registration_url=self.registration_url, about_url=self.about_url + ) ) + @patch.dict(settings.FEATURES, {'ENABLE_MKTG_SITE': True}) def test_enroll_email_not_registered_mktgsite(self): - url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id}) # Try with marketing site enabled - with patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': True}): - response = self.client.get(url, {'identifiers': self.notregistered_email, 'action': 'enroll', 'email_students': True}) + url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id.to_deprecated_string()}) + response = self.client.get(url, {'identifiers': self.notregistered_email, 'action': 'enroll', 'email_students': True}) self.assertEqual(response.status_code, 200) self.assertEqual( @@ -494,7 +517,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase): ) def test_enroll_with_email_not_registered_autoenroll(self): - url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id}) + url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url, {'identifiers': self.notregistered_email, 'action': 'enroll', 'email_students': True, 'auto_enroll': True}) print "type(self.notregistered_email): {}".format(type(self.notregistered_email)) self.assertEqual(response.status_code, 200) @@ -508,14 +531,16 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase): self.assertEqual( mail.outbox[0].body, "Dear student,\n\nYou have been invited to join Robot Super Course at edx.org by a member of the course staff.\n\n" - "To finish your registration, please visit https://edx.org/register and fill out the registration form " + "To finish your registration, please visit {registration_url} and fill out the registration form " "making sure to use robot-not-an-email-yet@robot.org in the E-mail field.\n" "Once you have registered and activated your account, you will see Robot Super Course listed on your dashboard.\n\n----\n" - "This email was automatically sent from edx.org to robot-not-an-email-yet@robot.org" + "This email was automatically sent from edx.org to robot-not-an-email-yet@robot.org".format( + registration_url=self.registration_url + ) ) def test_unenroll_without_email(self): - url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id}) + url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url, {'identifiers': self.enrolled_student.email, 'action': 'unenroll', 'email_students': False}) print "type(self.enrolled_student.email): {}".format(type(self.enrolled_student.email)) self.assertEqual(response.status_code, 200) @@ -554,7 +579,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase): self.assertEqual(len(mail.outbox), 0) def test_unenroll_with_email(self): - url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id}) + url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url, {'identifiers': self.enrolled_student.email, 'action': 'unenroll', 'email_students': True}) print "type(self.enrolled_student.email): {}".format(type(self.enrolled_student.email)) self.assertEqual(response.status_code, 200) @@ -605,7 +630,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase): ) def test_unenroll_with_email_allowed_student(self): - url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id}) + url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url, {'identifiers': self.allowed_email, 'action': 'unenroll', 'email_students': True}) print "type(self.allowed_email): {}".format(type(self.allowed_email)) self.assertEqual(response.status_code, 200) @@ -653,7 +678,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase): def test_enroll_with_email_not_registered_with_shib(self, mock_uses_shib): mock_uses_shib.return_value = True - url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id}) + url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url, {'identifiers': self.notregistered_email, 'action': 'enroll', 'email_students': True}) self.assertEqual(response.status_code, 200) @@ -667,15 +692,19 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase): self.assertEqual( mail.outbox[0].body, "Dear student,\n\nYou have been invited to join Robot Super Course at edx.org by a member of the course staff.\n\n" - "To access the course visit https://edx.org/courses/MITx/999/Robot_Super_Course/about and register for the course.\n\n----\n" - "This email was automatically sent from edx.org to robot-not-an-email-yet@robot.org" + "To access the course visit {about_url} and register for the course.\n\n----\n" + "This email was automatically sent from edx.org to robot-not-an-email-yet@robot.org".format( + about_url=self.about_url + ) ) @patch('instructor.enrollment.uses_shib') + @patch.dict(settings.FEATURES, {'ENABLE_MKTG_SITE': True}) def test_enroll_email_not_registered_shib_mktgsite(self, mock_uses_shib): + # Try with marketing site enabled and shib on mock_uses_shib.return_value = True - url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id}) + url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id.to_deprecated_string()}) # Try with marketing site enabled with patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': True}): response = self.client.get(url, {'identifiers': self.notregistered_email, 'action': 'enroll', 'email_students': True}) @@ -692,7 +721,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase): mock_uses_shib.return_value = True - url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id}) + url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url, {'identifiers': self.notregistered_email, 'action': 'enroll', 'email_students': True, 'auto_enroll': True}) print "type(self.notregistered_email): {}".format(type(self.notregistered_email)) self.assertEqual(response.status_code, 200) @@ -707,8 +736,10 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase): self.assertEqual( mail.outbox[0].body, "Dear student,\n\nYou have been invited to join Robot Super Course at edx.org by a member of the course staff.\n\n" - "To access the course visit https://edx.org/courses/MITx/999/Robot_Super_Course/ and login.\n\n----\n" - "This email was automatically sent from edx.org to robot-not-an-email-yet@robot.org" + "To access the course visit {course_url} and login.\n\n----\n" + "This email was automatically sent from edx.org to robot-not-an-email-yet@robot.org".format( + course_url=self.course_url + ) ) @@ -719,20 +750,30 @@ class TestInstructorAPIBulkBetaEnrollment(ModuleStoreTestCase, LoginEnrollmentTe """ def setUp(self): self.course = CourseFactory.create() - self.instructor = InstructorFactory(course=self.course.location) + self.instructor = InstructorFactory(course=self.course.id) self.client.login(username=self.instructor.username, password='test') - self.beta_tester = BetaTesterFactory(course=self.course.location) + self.beta_tester = BetaTesterFactory(course=self.course.id) CourseEnrollment.enroll( self.beta_tester, self.course.id ) + self.assertTrue(CourseBetaTesterRole(self.course.id).has_user(self.beta_tester)) self.notenrolled_student = UserFactory(username='NotEnrolledStudent') self.notregistered_email = 'robot-not-an-email-yet@robot.org' self.assertEqual(User.objects.filter(email=self.notregistered_email).count(), 0) + self.request = RequestFactory().request() + + # Email URL values + self.site_name = microsite.get_value( + 'SITE_NAME', + settings.SITE_NAME + ) + self.about_url = 'https://{}/courses/MITx/999/Robot_Super_Course/about'.format(self.site_name) + # uncomment to enable enable printing of large diffs # from failed assertions in the event of a test failure. # (comment because pylint C0103) @@ -740,14 +781,14 @@ class TestInstructorAPIBulkBetaEnrollment(ModuleStoreTestCase, LoginEnrollmentTe def test_missing_params(self): """ Test missing all query parameters. """ - url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id}) + url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url) self.assertEqual(response.status_code, 400) def test_bad_action(self): """ Test with an invalid action. """ action = 'robot-not-an-action' - url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id}) + url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url, {'identifiers': self.beta_tester.email, 'action': action}) self.assertEqual(response.status_code, 400) @@ -763,7 +804,7 @@ class TestInstructorAPIBulkBetaEnrollment(ModuleStoreTestCase, LoginEnrollmentTe Additionally asserts no email was sent. """ self.assertEqual(response.status_code, 200) - self.assertTrue(CourseBetaTesterRole(self.course.location).has_user(self.notenrolled_student)) + self.assertTrue(CourseBetaTesterRole(self.course.id).has_user(self.notenrolled_student)) # test the response data expected = { "action": "add", @@ -783,35 +824,35 @@ class TestInstructorAPIBulkBetaEnrollment(ModuleStoreTestCase, LoginEnrollmentTe self.assertEqual(len(mail.outbox), 0) def test_add_notenrolled_email(self): - url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id}) + url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url, {'identifiers': self.notenrolled_student.email, 'action': 'add', 'email_students': False}) self.add_notenrolled(response, self.notenrolled_student.email) self.assertFalse(CourseEnrollment.is_enrolled(self.notenrolled_student, self.course.id)) def test_add_notenrolled_email_autoenroll(self): - url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id}) + url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url, {'identifiers': self.notenrolled_student.email, 'action': 'add', 'email_students': False, 'auto_enroll': True}) self.add_notenrolled(response, self.notenrolled_student.email) self.assertTrue(CourseEnrollment.is_enrolled(self.notenrolled_student, self.course.id)) def test_add_notenrolled_username(self): - url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id}) + url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url, {'identifiers': self.notenrolled_student.username, 'action': 'add', 'email_students': False}) self.add_notenrolled(response, self.notenrolled_student.username) self.assertFalse(CourseEnrollment.is_enrolled(self.notenrolled_student, self.course.id)) def test_add_notenrolled_username_autoenroll(self): - url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id}) + url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url, {'identifiers': self.notenrolled_student.username, 'action': 'add', 'email_students': False, 'auto_enroll': True}) self.add_notenrolled(response, self.notenrolled_student.username) self.assertTrue(CourseEnrollment.is_enrolled(self.notenrolled_student, self.course.id)) def test_add_notenrolled_with_email(self): - url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id}) + url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url, {'identifiers': self.notenrolled_student.email, 'action': 'add', 'email_students': True}) self.assertEqual(response.status_code, 200) - self.assertTrue(CourseBetaTesterRole(self.course.location).has_user(self.notenrolled_student)) + self.assertTrue(CourseBetaTesterRole(self.course.id).has_user(self.notenrolled_student)) # test the response data expected = { "action": "add", @@ -837,23 +878,24 @@ class TestInstructorAPIBulkBetaEnrollment(ModuleStoreTestCase, LoginEnrollmentTe mail.outbox[0].body, u"Dear {0}\n\nYou have been invited to be a beta tester " "for Robot Super Course at edx.org by a member of the course staff.\n\n" - "Visit https://edx.org/courses/MITx/999/Robot_Super_Course/about to join " + "Visit {1} to join " "the course and begin the beta test.\n\n----\n" - "This email was automatically sent from edx.org to {1}".format( + "This email was automatically sent from edx.org to {2}".format( self.notenrolled_student.profile.name, + self.about_url, self.notenrolled_student.email ) ) def test_add_notenrolled_with_email_autoenroll(self): - url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id}) + url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get( url, {'identifiers': self.notenrolled_student.email, 'action': 'add', 'email_students': True, 'auto_enroll': True} ) self.assertEqual(response.status_code, 200) - self.assertTrue(CourseBetaTesterRole(self.course.location).has_user(self.notenrolled_student)) + self.assertTrue(CourseBetaTesterRole(self.course.id).has_user(self.notenrolled_student)) # test the response data expected = { "action": "add", @@ -887,11 +929,11 @@ class TestInstructorAPIBulkBetaEnrollment(ModuleStoreTestCase, LoginEnrollmentTe ) ) + @patch.dict(settings.FEATURES, {'ENABLE_MKTG_SITE': True}) def test_add_notenrolled_email_mktgsite(self): - url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id}) # Try with marketing site enabled - with patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': True}): - response = self.client.get(url, {'identifiers': self.notenrolled_student.email, 'action': 'add', 'email_students': True}) + url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id.to_deprecated_string()}) + response = self.client.get(url, {'identifiers': self.notenrolled_student.email, 'action': 'add', 'email_students': True}) self.assertEqual(response.status_code, 200) self.assertEqual( @@ -907,7 +949,7 @@ class TestInstructorAPIBulkBetaEnrollment(ModuleStoreTestCase, LoginEnrollmentTe def test_enroll_with_email_not_registered(self): # User doesn't exist - url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id}) + url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url, {'identifiers': self.notregistered_email, 'action': 'add', 'email_students': True}) self.assertEqual(response.status_code, 200) # test the response data @@ -928,11 +970,15 @@ class TestInstructorAPIBulkBetaEnrollment(ModuleStoreTestCase, LoginEnrollmentTe self.assertEqual(len(mail.outbox), 0) def test_remove_without_email(self): - url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id}) + url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url, {'identifiers': self.beta_tester.email, 'action': 'remove', 'email_students': False}) self.assertEqual(response.status_code, 200) - self.assertFalse(CourseBetaTesterRole(self.course.location).has_user(self.beta_tester)) + # Works around a caching bug which supposedly can't happen in prod. The instance here is not == + # the instance fetched from the email above which had its cache cleared + if hasattr(self.beta_tester, '_roles'): + del self.beta_tester._roles + self.assertFalse(CourseBetaTesterRole(self.course.id).has_user(self.beta_tester)) # test the response data expected = { @@ -952,11 +998,15 @@ class TestInstructorAPIBulkBetaEnrollment(ModuleStoreTestCase, LoginEnrollmentTe self.assertEqual(len(mail.outbox), 0) def test_remove_with_email(self): - url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id}) + url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url, {'identifiers': self.beta_tester.email, 'action': 'remove', 'email_students': True}) self.assertEqual(response.status_code, 200) - self.assertFalse(CourseBetaTesterRole(self.course.location).has_user(self.beta_tester)) + # Works around a caching bug which supposedly can't happen in prod. The instance here is not == + # the instance fetched from the email above which had its cache cleared + if hasattr(self.beta_tester, '_roles'): + del self.beta_tester._roles + self.assertFalse(CourseBetaTesterRole(self.course.id).has_user(self.beta_tester)) # test the response data expected = { @@ -1005,24 +1055,22 @@ class TestInstructorAPILevelsAccess(ModuleStoreTestCase, LoginEnrollmentTestCase """ def setUp(self): self.course = CourseFactory.create() - self.instructor = InstructorFactory(course=self.course.location) + self.instructor = InstructorFactory(course=self.course.id) self.client.login(username=self.instructor.username, password='test') - self.other_instructor = UserFactory() - allow_access(self.course, self.other_instructor, 'instructor') - self.other_staff = UserFactory() - allow_access(self.course, self.other_staff, 'staff') + self.other_instructor = InstructorFactory(course=self.course.id) + self.other_staff = StaffFactory(course=self.course.id) self.other_user = UserFactory() def test_modify_access_noparams(self): """ Test missing all query parameters. """ - url = reverse('modify_access', kwargs={'course_id': self.course.id}) + url = reverse('modify_access', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url) self.assertEqual(response.status_code, 400) def test_modify_access_bad_action(self): """ Test with an invalid action parameter. """ - url = reverse('modify_access', kwargs={'course_id': self.course.id}) + url = reverse('modify_access', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url, { 'unique_student_identifier': self.other_staff.email, 'rolename': 'staff', @@ -1032,7 +1080,7 @@ class TestInstructorAPILevelsAccess(ModuleStoreTestCase, LoginEnrollmentTestCase def test_modify_access_bad_role(self): """ Test with an invalid action parameter. """ - url = reverse('modify_access', kwargs={'course_id': self.course.id}) + url = reverse('modify_access', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url, { 'unique_student_identifier': self.other_staff.email, 'rolename': 'robot-not-a-roll', @@ -1041,16 +1089,16 @@ class TestInstructorAPILevelsAccess(ModuleStoreTestCase, LoginEnrollmentTestCase self.assertEqual(response.status_code, 400) def test_modify_access_allow(self): - url = reverse('modify_access', kwargs={'course_id': self.course.id}) + url = reverse('modify_access', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url, { - 'unique_student_identifier': self.other_instructor.email, + 'unique_student_identifier': self.other_user.email, 'rolename': 'staff', 'action': 'allow', }) self.assertEqual(response.status_code, 200) def test_modify_access_allow_with_uname(self): - url = reverse('modify_access', kwargs={'course_id': self.course.id}) + url = reverse('modify_access', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url, { 'unique_student_identifier': self.other_instructor.username, 'rolename': 'staff', @@ -1059,7 +1107,7 @@ class TestInstructorAPILevelsAccess(ModuleStoreTestCase, LoginEnrollmentTestCase self.assertEqual(response.status_code, 200) def test_modify_access_revoke(self): - url = reverse('modify_access', kwargs={'course_id': self.course.id}) + url = reverse('modify_access', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url, { 'unique_student_identifier': self.other_staff.email, 'rolename': 'staff', @@ -1068,7 +1116,7 @@ class TestInstructorAPILevelsAccess(ModuleStoreTestCase, LoginEnrollmentTestCase self.assertEqual(response.status_code, 200) def test_modify_access_revoke_with_username(self): - url = reverse('modify_access', kwargs={'course_id': self.course.id}) + url = reverse('modify_access', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url, { 'unique_student_identifier': self.other_staff.username, 'rolename': 'staff', @@ -1077,7 +1125,7 @@ class TestInstructorAPILevelsAccess(ModuleStoreTestCase, LoginEnrollmentTestCase self.assertEqual(response.status_code, 200) def test_modify_access_with_fake_user(self): - url = reverse('modify_access', kwargs={'course_id': self.course.id}) + url = reverse('modify_access', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url, { 'unique_student_identifier': 'GandalfTheGrey', 'rolename': 'staff', @@ -1094,7 +1142,7 @@ class TestInstructorAPILevelsAccess(ModuleStoreTestCase, LoginEnrollmentTestCase def test_modify_access_with_inactive_user(self): self.other_user.is_active = False self.other_user.save() # pylint: disable=no-member - url = reverse('modify_access', kwargs={'course_id': self.course.id}) + url = reverse('modify_access', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url, { 'unique_student_identifier': self.other_user.username, 'rolename': 'beta', @@ -1110,7 +1158,7 @@ class TestInstructorAPILevelsAccess(ModuleStoreTestCase, LoginEnrollmentTestCase def test_modify_access_revoke_not_allowed(self): """ Test revoking access that a user does not have. """ - url = reverse('modify_access', kwargs={'course_id': self.course.id}) + url = reverse('modify_access', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url, { 'unique_student_identifier': self.other_staff.email, 'rolename': 'instructor', @@ -1122,7 +1170,7 @@ class TestInstructorAPILevelsAccess(ModuleStoreTestCase, LoginEnrollmentTestCase """ Test that an instructor cannot remove instructor privelages from themself. """ - url = reverse('modify_access', kwargs={'course_id': self.course.id}) + url = reverse('modify_access', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url, { 'unique_student_identifier': self.instructor.email, 'rolename': 'instructor', @@ -1141,30 +1189,28 @@ class TestInstructorAPILevelsAccess(ModuleStoreTestCase, LoginEnrollmentTestCase def test_list_course_role_members_noparams(self): """ Test missing all query parameters. """ - url = reverse('list_course_role_members', kwargs={'course_id': self.course.id}) + url = reverse('list_course_role_members', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url) self.assertEqual(response.status_code, 400) def test_list_course_role_members_bad_rolename(self): """ Test with an invalid rolename parameter. """ - url = reverse('list_course_role_members', kwargs={'course_id': self.course.id}) + url = reverse('list_course_role_members', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url, { 'rolename': 'robot-not-a-rolename', }) - print response self.assertEqual(response.status_code, 400) def test_list_course_role_members_staff(self): - url = reverse('list_course_role_members', kwargs={'course_id': self.course.id}) + url = reverse('list_course_role_members', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url, { 'rolename': 'staff', }) - print response self.assertEqual(response.status_code, 200) # check response content expected = { - 'course_id': self.course.id, + 'course_id': self.course.id.to_deprecated_string(), 'staff': [ { 'username': self.other_staff.username, @@ -1178,16 +1224,15 @@ class TestInstructorAPILevelsAccess(ModuleStoreTestCase, LoginEnrollmentTestCase self.assertEqual(res_json, expected) def test_list_course_role_members_beta(self): - url = reverse('list_course_role_members', kwargs={'course_id': self.course.id}) + url = reverse('list_course_role_members', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url, { 'rolename': 'beta', }) - print response self.assertEqual(response.status_code, 200) # check response content expected = { - 'course_id': self.course.id, + 'course_id': self.course.id.to_deprecated_string(), 'beta': [] } res_json = json.loads(response.content) @@ -1225,7 +1270,7 @@ class TestInstructorAPILevelsAccess(ModuleStoreTestCase, LoginEnrollmentTestCase Get unique_student_identifier, rolename and action and update forum role. """ - url = reverse('update_forum_role_membership', kwargs={'course_id': self.course.id}) + url = reverse('update_forum_role_membership', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get( url, { @@ -1253,7 +1298,7 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa """ def setUp(self): self.course = CourseFactory.create() - self.instructor = InstructorFactory(course=self.course.location) + self.instructor = InstructorFactory(course=self.course.id) self.client.login(username=self.instructor.username, password='test') self.students = [UserFactory() for _ in xrange(6)] @@ -1265,7 +1310,7 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa Test that some minimum of information is formatted correctly in the response to get_students_features. """ - url = reverse('get_students_features', kwargs={'course_id': self.course.id}) + url = reverse('get_students_features', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url, {}) res_json = json.loads(response.content) self.assertIn('students', res_json) @@ -1281,7 +1326,7 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa """ Test the CSV output for the anonymized user ids. """ - url = reverse('get_anon_ids', kwargs={'course_id': self.course.id}) + url = reverse('get_anon_ids', kwargs={'course_id': self.course.id.to_deprecated_string()}) with patch('instructor.views.api.unique_id_for_user') as mock_unique: mock_unique.return_value = '42' response = self.client.get(url, {}) @@ -1291,7 +1336,7 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa self.assertTrue(body.endswith('"7","42"\n')) def test_list_report_downloads(self): - url = reverse('list_report_downloads', kwargs={'course_id': self.course.id}) + url = reverse('list_report_downloads', kwargs={'course_id': self.course.id.to_deprecated_string()}) with patch('instructor_task.models.LocalFSReportStore.links_for') as mock_links_for: mock_links_for.return_value = [ ('mock_file_name_1', 'https://1.mock.url'), @@ -1317,7 +1362,7 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa self.assertEqual(res_json, expected_response) def test_calculate_grades_csv_success(self): - url = reverse('calculate_grades_csv', kwargs={'course_id': self.course.id}) + url = reverse('calculate_grades_csv', kwargs={'course_id': self.course.id.to_deprecated_string()}) with patch('instructor_task.api.submit_calculate_grades_csv') as mock_cal_grades: mock_cal_grades.return_value = True @@ -1326,7 +1371,7 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa self.assertIn(success_status, response.content) def test_calculate_grades_csv_already_running(self): - url = reverse('calculate_grades_csv', kwargs={'course_id': self.course.id}) + url = reverse('calculate_grades_csv', kwargs={'course_id': self.course.id.to_deprecated_string()}) with patch('instructor_task.api.submit_calculate_grades_csv') as mock_cal_grades: mock_cal_grades.side_effect = AlreadyRunningError() @@ -1339,7 +1384,7 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa Test that some minimum of information is formatted correctly in the response to get_students_features. """ - url = reverse('get_students_features', kwargs={'course_id': self.course.id}) + url = reverse('get_students_features', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url + '/csv', {}) self.assertEqual(response['Content-Type'], 'text/csv') @@ -1348,13 +1393,13 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa Test that get_distribution lists available features when supplied no feature parameter. """ - url = reverse('get_distribution', kwargs={'course_id': self.course.id}) + url = reverse('get_distribution', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url) self.assertEqual(response.status_code, 200) res_json = json.loads(response.content) self.assertEqual(type(res_json['available_features']), list) - url = reverse('get_distribution', kwargs={'course_id': self.course.id}) + url = reverse('get_distribution', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url + u'?feature=') self.assertEqual(response.status_code, 200) res_json = json.loads(response.content) @@ -1365,7 +1410,7 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa Test that get_distribution fails gracefully with an unavailable feature. """ - url = reverse('get_distribution', kwargs={'course_id': self.course.id}) + url = reverse('get_distribution', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url, {'feature': 'robot-not-a-real-feature'}) self.assertEqual(response.status_code, 400) @@ -1374,11 +1419,10 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa Test that get_distribution fails gracefully with an unavailable feature. """ - url = reverse('get_distribution', kwargs={'course_id': self.course.id}) + url = reverse('get_distribution', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url, {'feature': 'gender'}) self.assertEqual(response.status_code, 200) res_json = json.loads(response.content) - print res_json self.assertEqual(res_json['feature_results']['data']['m'], 6) self.assertEqual(res_json['feature_results']['choices_display_names']['m'], 'Male') self.assertEqual(res_json['feature_results']['data']['no_data'], 0) @@ -1386,39 +1430,35 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa def test_get_student_progress_url(self): """ Test that progress_url is in the successful response. """ - url = reverse('get_student_progress_url', kwargs={'course_id': self.course.id}) + url = reverse('get_student_progress_url', kwargs={'course_id': self.course.id.to_deprecated_string()}) url += "?unique_student_identifier={}".format( quote(self.students[0].email.encode("utf-8")) ) - print url response = self.client.get(url) - print response self.assertEqual(response.status_code, 200) res_json = json.loads(response.content) self.assertIn('progress_url', res_json) def test_get_student_progress_url_from_uname(self): """ Test that progress_url is in the successful response. """ - url = reverse('get_student_progress_url', kwargs={'course_id': self.course.id}) + url = reverse('get_student_progress_url', kwargs={'course_id': self.course.id.to_deprecated_string()}) url += "?unique_student_identifier={}".format( quote(self.students[0].username.encode("utf-8")) ) - print url response = self.client.get(url) - print response self.assertEqual(response.status_code, 200) res_json = json.loads(response.content) self.assertIn('progress_url', res_json) def test_get_student_progress_url_noparams(self): """ Test that the endpoint 404's without the required query params. """ - url = reverse('get_student_progress_url', kwargs={'course_id': self.course.id}) + url = reverse('get_student_progress_url', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url) self.assertEqual(response.status_code, 400) def test_get_student_progress_url_nostudent(self): """ Test that the endpoint 400's when requesting an unknown email. """ - url = reverse('get_student_progress_url', kwargs={'course_id': self.course.id}) + url = reverse('get_student_progress_url', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url) self.assertEqual(response.status_code, 400) @@ -1434,42 +1474,42 @@ class TestInstructorAPIRegradeTask(ModuleStoreTestCase, LoginEnrollmentTestCase) """ def setUp(self): self.course = CourseFactory.create() - self.instructor = InstructorFactory(course=self.course.location) + self.instructor = InstructorFactory(course=self.course.id) self.client.login(username=self.instructor.username, password='test') self.student = UserFactory() CourseEnrollment.enroll(self.student, self.course.id) - self.problem_urlname = 'robot-some-problem-urlname' + self.problem_location = msk_from_problem_urlname( + self.course.id, + 'robot-some-problem-urlname' + ) + self.problem_urlname = str(self.problem_location) + self.module_to_reset = StudentModule.objects.create( student=self.student, course_id=self.course.id, - module_state_key=_msk_from_problem_urlname( - self.course.id, - self.problem_urlname - ), + module_id=self.problem_location, state=json.dumps({'attempts': 10}), ) def test_reset_student_attempts_deletall(self): """ Make sure no one can delete all students state on a problem. """ - url = reverse('reset_student_attempts', kwargs={'course_id': self.course.id}) + url = reverse('reset_student_attempts', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url, { 'problem_to_reset': self.problem_urlname, 'all_students': True, 'delete_module': True, }) - print response.content self.assertEqual(response.status_code, 400) def test_reset_student_attempts_single(self): """ Test reset single student attempts. """ - url = reverse('reset_student_attempts', kwargs={'course_id': self.course.id}) + url = reverse('reset_student_attempts', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url, { 'problem_to_reset': self.problem_urlname, 'unique_student_identifier': self.student.email, }) - print response.content self.assertEqual(response.status_code, 200) # make sure problem attempts have been reset. changed_module = StudentModule.objects.get(pk=self.module_to_reset.pk) @@ -1482,89 +1522,82 @@ class TestInstructorAPIRegradeTask(ModuleStoreTestCase, LoginEnrollmentTestCase) @patch.object(instructor_task.api, 'submit_reset_problem_attempts_for_all_students') def test_reset_student_attempts_all(self, act): """ Test reset all student attempts. """ - url = reverse('reset_student_attempts', kwargs={'course_id': self.course.id}) + url = reverse('reset_student_attempts', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url, { 'problem_to_reset': self.problem_urlname, 'all_students': True, }) - print response.content self.assertEqual(response.status_code, 200) self.assertTrue(act.called) def test_reset_student_attempts_missingmodule(self): """ Test reset for non-existant problem. """ - url = reverse('reset_student_attempts', kwargs={'course_id': self.course.id}) + url = reverse('reset_student_attempts', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url, { 'problem_to_reset': 'robot-not-a-real-module', 'unique_student_identifier': self.student.email, }) - print response.content self.assertEqual(response.status_code, 400) def test_reset_student_attempts_delete(self): """ Test delete single student state. """ - url = reverse('reset_student_attempts', kwargs={'course_id': self.course.id}) + url = reverse('reset_student_attempts', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url, { 'problem_to_reset': self.problem_urlname, 'unique_student_identifier': self.student.email, 'delete_module': True, }) - print response.content self.assertEqual(response.status_code, 200) # make sure the module has been deleted self.assertEqual( StudentModule.objects.filter( student=self.module_to_reset.student, course_id=self.module_to_reset.course_id, - # module_state_key=self.module_to_reset.module_state_key, + # module_id=self.module_to_reset.module_id, ).count(), 0 ) def test_reset_student_attempts_nonsense(self): """ Test failure with both unique_student_identifier and all_students. """ - url = reverse('reset_student_attempts', kwargs={'course_id': self.course.id}) + url = reverse('reset_student_attempts', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url, { 'problem_to_reset': self.problem_urlname, 'unique_student_identifier': self.student.email, 'all_students': True, }) - print response.content self.assertEqual(response.status_code, 400) @patch.object(instructor_task.api, 'submit_rescore_problem_for_student') def test_rescore_problem_single(self, act): """ Test rescoring of a single student. """ - url = reverse('rescore_problem', kwargs={'course_id': self.course.id}) + url = reverse('rescore_problem', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url, { 'problem_to_reset': self.problem_urlname, 'unique_student_identifier': self.student.email, }) - print response.content self.assertEqual(response.status_code, 200) self.assertTrue(act.called) @patch.object(instructor_task.api, 'submit_rescore_problem_for_student') def test_rescore_problem_single_from_uname(self, act): """ Test rescoring of a single student. """ - url = reverse('rescore_problem', kwargs={'course_id': self.course.id}) + url = reverse('rescore_problem', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url, { 'problem_to_reset': self.problem_urlname, 'unique_student_identifier': self.student.username, }) - print response.content self.assertEqual(response.status_code, 200) self.assertTrue(act.called) @patch.object(instructor_task.api, 'submit_rescore_problem_for_all_students') def test_rescore_problem_all(self, act): """ Test rescoring for all students. """ - url = reverse('rescore_problem', kwargs={'course_id': self.course.id}) + url = reverse('rescore_problem', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url, { 'problem_to_reset': self.problem_urlname, 'all_students': True, }) - print response.content self.assertEqual(response.status_code, 200) self.assertTrue(act.called) @@ -1578,7 +1611,7 @@ class TestInstructorSendEmail(ModuleStoreTestCase, LoginEnrollmentTestCase): """ def setUp(self): self.course = CourseFactory.create() - self.instructor = InstructorFactory(course=self.course.location) + self.instructor = InstructorFactory(course=self.course.id) self.client.login(username=self.instructor.username, password='test') test_subject = u'\u1234 test subject' test_message = u'\u6824 test message' @@ -1589,13 +1622,13 @@ class TestInstructorSendEmail(ModuleStoreTestCase, LoginEnrollmentTestCase): } def test_send_email_as_logged_in_instructor(self): - url = reverse('send_email', kwargs={'course_id': self.course.id}) + url = reverse('send_email', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.post(url, self.full_test_message) self.assertEqual(response.status_code, 200) def test_send_email_but_not_logged_in(self): self.client.logout() - url = reverse('send_email', kwargs={'course_id': self.course.id}) + url = reverse('send_email', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.post(url, self.full_test_message) self.assertEqual(response.status_code, 403) @@ -1603,7 +1636,7 @@ class TestInstructorSendEmail(ModuleStoreTestCase, LoginEnrollmentTestCase): self.client.logout() student = UserFactory() self.client.login(username=student.username, password='test') - url = reverse('send_email', kwargs={'course_id': self.course.id}) + url = reverse('send_email', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.post(url, self.full_test_message) self.assertEqual(response.status_code, 403) @@ -1613,7 +1646,7 @@ class TestInstructorSendEmail(ModuleStoreTestCase, LoginEnrollmentTestCase): self.assertNotEqual(response.status_code, 200) def test_send_email_no_sendto(self): - url = reverse('send_email', kwargs={'course_id': self.course.id}) + url = reverse('send_email', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.post(url, { 'subject': 'test subject', 'message': 'test message', @@ -1621,7 +1654,7 @@ class TestInstructorSendEmail(ModuleStoreTestCase, LoginEnrollmentTestCase): self.assertEqual(response.status_code, 400) def test_send_email_no_subject(self): - url = reverse('send_email', kwargs={'course_id': self.course.id}) + url = reverse('send_email', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.post(url, { 'send_to': 'staff', 'message': 'test message', @@ -1629,7 +1662,7 @@ class TestInstructorSendEmail(ModuleStoreTestCase, LoginEnrollmentTestCase): self.assertEqual(response.status_code, 400) def test_send_email_no_message(self): - url = reverse('send_email', kwargs={'course_id': self.course.id}) + url = reverse('send_email', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.post(url, { 'send_to': 'staff', 'subject': 'test subject', @@ -1700,20 +1733,22 @@ class TestInstructorAPITaskLists(ModuleStoreTestCase, LoginEnrollmentTestCase): def setUp(self): self.course = CourseFactory.create() - self.instructor = InstructorFactory(course=self.course.location) + self.instructor = InstructorFactory(course=self.course.id) self.client.login(username=self.instructor.username, password='test') self.student = UserFactory() CourseEnrollment.enroll(self.student, self.course.id) - self.problem_urlname = 'robot-some-problem-urlname' + self.problem_location = msk_from_problem_urlname( + self.course.id, + 'robot-some-problem-urlname' + ) + self.problem_urlname = str(self.problem_location) + self.module = StudentModule.objects.create( student=self.student, course_id=self.course.id, - module_state_key=_msk_from_problem_urlname( - self.course.id, - self.problem_urlname - ), + module_id=self.problem_location, state=json.dumps({'attempts': 10}), ) mock_factory = MockCompletionInfo() @@ -1730,7 +1765,7 @@ class TestInstructorAPITaskLists(ModuleStoreTestCase, LoginEnrollmentTestCase): def test_list_instructor_tasks_running(self, act): """ Test list of all running tasks. """ act.return_value = self.tasks - url = reverse('list_instructor_tasks', kwargs={'course_id': self.course.id}) + url = reverse('list_instructor_tasks', kwargs={'course_id': self.course.id.to_deprecated_string()}) mock_factory = MockCompletionInfo() with patch('instructor.views.api.get_task_completion_info') as mock_completion_info: mock_completion_info.side_effect = mock_factory.mock_get_task_completion_info @@ -1749,7 +1784,7 @@ class TestInstructorAPITaskLists(ModuleStoreTestCase, LoginEnrollmentTestCase): def test_list_background_email_tasks(self, act): """Test list of background email tasks.""" act.return_value = self.tasks - url = reverse('list_background_email_tasks', kwargs={'course_id': self.course.id}) + url = reverse('list_background_email_tasks', kwargs={'course_id': self.course.id.to_deprecated_string()}) mock_factory = MockCompletionInfo() with patch('instructor.views.api.get_task_completion_info') as mock_completion_info: mock_completion_info.side_effect = mock_factory.mock_get_task_completion_info @@ -1768,12 +1803,12 @@ class TestInstructorAPITaskLists(ModuleStoreTestCase, LoginEnrollmentTestCase): def test_list_instructor_tasks_problem(self, act): """ Test list task history for problem. """ act.return_value = self.tasks - url = reverse('list_instructor_tasks', kwargs={'course_id': self.course.id}) + url = reverse('list_instructor_tasks', kwargs={'course_id': self.course.id.to_deprecated_string()}) mock_factory = MockCompletionInfo() with patch('instructor.views.api.get_task_completion_info') as mock_completion_info: mock_completion_info.side_effect = mock_factory.mock_get_task_completion_info response = self.client.get(url, { - 'problem_urlname': self.problem_urlname, + 'problem_location_str': self.problem_urlname, }) self.assertEqual(response.status_code, 200) @@ -1789,12 +1824,12 @@ class TestInstructorAPITaskLists(ModuleStoreTestCase, LoginEnrollmentTestCase): def test_list_instructor_tasks_problem_student(self, act): """ Test list task history for problem AND student. """ act.return_value = self.tasks - url = reverse('list_instructor_tasks', kwargs={'course_id': self.course.id}) + url = reverse('list_instructor_tasks', kwargs={'course_id': self.course.id.to_deprecated_string()}) mock_factory = MockCompletionInfo() with patch('instructor.views.api.get_task_completion_info') as mock_completion_info: mock_completion_info.side_effect = mock_factory.mock_get_task_completion_info response = self.client.get(url, { - 'problem_urlname': self.problem_urlname, + 'problem_location_str': self.problem_urlname, 'unique_student_identifier': self.student.email, }) self.assertEqual(response.status_code, 200) @@ -1831,7 +1866,7 @@ class TestInstructorAPIAnalyticsProxy(ModuleStoreTestCase, LoginEnrollmentTestCa def setUp(self): self.course = CourseFactory.create() - self.instructor = InstructorFactory(course=self.course.location) + self.instructor = InstructorFactory(course=self.course.id) self.client.login(username=self.instructor.username, password='test') @patch.object(instructor.views.api.requests, 'get') @@ -1839,18 +1874,17 @@ class TestInstructorAPIAnalyticsProxy(ModuleStoreTestCase, LoginEnrollmentTestCa """ Test legacy analytics proxy url generation. """ act.return_value = self.FakeProxyResponse() - url = reverse('proxy_legacy_analytics', kwargs={'course_id': self.course.id}) + url = reverse('proxy_legacy_analytics', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url, { 'aname': 'ProblemGradeDistribution' }) - print response.content self.assertEqual(response.status_code, 200) # check request url - expected_url = "{url}get?aname={aname}&course_id={course_id}&apikey={api_key}".format( + expected_url = "{url}get?aname={aname}&course_id={course_id!s}&apikey={api_key}".format( url="http://robotanalyticsserver.netbot:900/", aname="ProblemGradeDistribution", - course_id=self.course.id, + course_id=self.course.id.to_deprecated_string(), api_key="robot_api_key", ) act.assert_called_once_with(expected_url) @@ -1862,11 +1896,10 @@ class TestInstructorAPIAnalyticsProxy(ModuleStoreTestCase, LoginEnrollmentTestCa """ act.return_value = self.FakeProxyResponse() - url = reverse('proxy_legacy_analytics', kwargs={'course_id': self.course.id}) + url = reverse('proxy_legacy_analytics', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url, { 'aname': 'ProblemGradeDistribution' }) - print response.content self.assertEqual(response.status_code, 200) # check response @@ -1879,11 +1912,10 @@ class TestInstructorAPIAnalyticsProxy(ModuleStoreTestCase, LoginEnrollmentTestCa """ Test proxy when server reponds with failure. """ act.return_value = self.FakeBadProxyResponse() - url = reverse('proxy_legacy_analytics', kwargs={'course_id': self.course.id}) + url = reverse('proxy_legacy_analytics', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url, { 'aname': 'ProblemGradeDistribution' }) - print response.content self.assertEqual(response.status_code, 500) @patch.object(instructor.views.api.requests, 'get') @@ -1891,9 +1923,8 @@ class TestInstructorAPIAnalyticsProxy(ModuleStoreTestCase, LoginEnrollmentTestCa """ Test proxy when missing the aname query parameter. """ act.return_value = self.FakeProxyResponse() - url = reverse('proxy_legacy_analytics', kwargs={'course_id': self.course.id}) + url = reverse('proxy_legacy_analytics', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url, {}) - print response.content self.assertEqual(response.status_code, 400) self.assertFalse(act.called) @@ -1917,31 +1948,15 @@ class TestInstructorAPIHelpers(TestCase): self.assertEqual(_split_input_list(scary_unistuff), [scary_unistuff]) def test_msk_from_problem_urlname(self): - course_id = 'RobotU/Robots101/3001_Spring' - capa_urlname = 'capa_urlname' - capa_urlname_xml = 'capa_urlname.xml' - xblock_urlname = 'notaproblem/someothername' - xblock_urlname_xml = 'notaproblem/someothername.xml' - - capa_msk = 'i4x://RobotU/Robots101/problem/capa_urlname' - xblock_msk = 'i4x://RobotU/Robots101/notaproblem/someothername' - - for urlname in [capa_urlname, capa_urlname_xml]: - self.assertEqual( - _msk_from_problem_urlname(course_id, urlname), - capa_msk - ) - - for urlname in [xblock_urlname, xblock_urlname_xml]: - self.assertEqual( - _msk_from_problem_urlname(course_id, urlname), - xblock_msk - ) + course_id = SlashSeparatedCourseKey('MITx', '6.002x', '2013_Spring') + name = 'L2Node1' + output = 'i4x://MITx/6.002x/problem/L2Node1' + self.assertEqual(msk_from_problem_urlname(course_id, name).to_deprecated_string(), output) @raises(ValueError) def test_msk_from_problem_urlname_error(self): args = ('notagoodcourse', 'L2Node1') - _msk_from_problem_urlname(*args) + msk_from_problem_urlname(*args) @override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) @@ -1959,60 +1974,60 @@ class TestDueDateExtensions(ModuleStoreTestCase, LoginEnrollmentTestCase): week1 = ItemFactory.create(due=due) week2 = ItemFactory.create(due=due) week3 = ItemFactory.create(due=due) - course.children = [week1.location.url(), week2.location.url(), - week3.location.url()] + course.children = [week1.location.to_deprecated_string(), week2.location.to_deprecated_string(), + week3.location.to_deprecated_string()] homework = ItemFactory.create( parent_location=week1.location, due=due ) - week1.children = [homework.location.url()] + week1.children = [homework.location.to_deprecated_string()] user1 = UserFactory.create() StudentModule( state='{}', student_id=user1.id, course_id=course.id, - module_state_key=week1.location.url()).save() + module_id=week1.location).save() StudentModule( state='{}', student_id=user1.id, course_id=course.id, - module_state_key=week2.location.url()).save() + module_id=week2.location).save() StudentModule( state='{}', student_id=user1.id, course_id=course.id, - module_state_key=week3.location.url()).save() + module_id=week3.location).save() StudentModule( state='{}', student_id=user1.id, course_id=course.id, - module_state_key=homework.location.url()).save() + module_id=homework.location).save() user2 = UserFactory.create() StudentModule( state='{}', student_id=user2.id, course_id=course.id, - module_state_key=week1.location.url()).save() + module_id=week1.location).save() StudentModule( state='{}', student_id=user2.id, course_id=course.id, - module_state_key=homework.location.url()).save() + module_id=homework.location).save() user3 = UserFactory.create() StudentModule( state='{}', student_id=user3.id, course_id=course.id, - module_state_key=week1.location.url()).save() + module_id=week1.location).save() StudentModule( state='{}', student_id=user3.id, course_id=course.id, - module_state_key=homework.location.url()).save() + module_id=homework.location).save() self.course = course self.week1 = week1 @@ -2021,14 +2036,14 @@ class TestDueDateExtensions(ModuleStoreTestCase, LoginEnrollmentTestCase): self.user1 = user1 self.user2 = user2 - self.instructor = InstructorFactory(course=course.location) + self.instructor = InstructorFactory(course=course.id) self.client.login(username=self.instructor.username, password='test') def test_change_due_date(self): - url = reverse('change_due_date', kwargs={'course_id': self.course.id}) + url = reverse('change_due_date', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url, { 'student': self.user1.username, - 'url': self.week1.location.url(), + 'url': self.week1.location.to_deprecated_string(), 'due_datetime': '12/30/2013 00:00' }) self.assertEqual(response.status_code, 200, response.content) @@ -2037,10 +2052,10 @@ class TestDueDateExtensions(ModuleStoreTestCase, LoginEnrollmentTestCase): def test_reset_date(self): self.test_change_due_date() - url = reverse('reset_due_date', kwargs={'course_id': self.course.id}) + url = reverse('reset_due_date', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url, { 'student': self.user1.username, - 'url': self.week1.location.url(), + 'url': self.week1.location.to_deprecated_string(), }) self.assertEqual(response.status_code, 200, response.content) self.assertEqual(None, @@ -2049,8 +2064,8 @@ class TestDueDateExtensions(ModuleStoreTestCase, LoginEnrollmentTestCase): def test_show_unit_extensions(self): self.test_change_due_date() url = reverse('show_unit_extensions', - kwargs={'course_id': self.course.id}) - response = self.client.get(url, {'url': self.week1.location.url()}) + kwargs={'course_id': self.course.id.to_deprecated_string()}) + response = self.client.get(url, {'url': self.week1.location.to_deprecated_string()}) self.assertEqual(response.status_code, 200, response.content) self.assertEqual(json.loads(response.content), { u'data': [{u'Extended Due Date': u'2013-12-30 00:00', @@ -2063,7 +2078,7 @@ class TestDueDateExtensions(ModuleStoreTestCase, LoginEnrollmentTestCase): def test_show_student_extensions(self): self.test_change_due_date() url = reverse('show_student_extensions', - kwargs={'course_id': self.course.id}) + kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url, {'student': self.user1.username}) self.assertEqual(response.status_code, 200, response.content) self.assertEqual(json.loads(response.content), { diff --git a/lms/djangoapps/instructor/tests/test_email.py b/lms/djangoapps/instructor/tests/test_email.py index 6a2b71f2e8..e584b312be 100644 --- a/lms/djangoapps/instructor/tests/test_email.py +++ b/lms/djangoapps/instructor/tests/test_email.py @@ -18,6 +18,7 @@ from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE from mock import patch from bulk_email.models import CourseAuthorization +from xmodule.modulestore.locations import SlashSeparatedCourseKey @override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) @@ -34,7 +35,7 @@ class TestNewInstructorDashboardEmailViewMongoBacked(ModuleStoreTestCase): self.client.login(username=instructor.username, password="test") # URL for instructor dash - self.url = reverse('instructor_dashboard_2', kwargs={'course_id': self.course.id}) + self.url = reverse('instructor_dashboard_2', kwargs={'course_id': self.course.id.to_deprecated_string()}) # URL for email view self.email_link = 'Email' @@ -115,14 +116,14 @@ class TestNewInstructorDashboardEmailViewXMLBacked(ModuleStoreTestCase): Check for email view on the new instructor dashboard """ def setUp(self): - self.course_name = 'edX/toy/2012_Fall' + self.course_key = SlashSeparatedCourseKey('edX', 'toy', '2012_Fall') # Create instructor account instructor = AdminFactory.create() self.client.login(username=instructor.username, password="test") # URL for instructor dash - self.url = reverse('instructor_dashboard_2', kwargs={'course_id': self.course_name}) + self.url = reverse('instructor_dashboard_2', kwargs={'course_id': self.course_key.to_deprecated_string()}) # URL for email view self.email_link = 'Email' diff --git a/lms/djangoapps/instructor/tests/test_enrollment.py b/lms/djangoapps/instructor/tests/test_enrollment.py index b34fd64517..8a9a3efe03 100644 --- a/lms/djangoapps/instructor/tests/test_enrollment.py +++ b/lms/djangoapps/instructor/tests/test_enrollment.py @@ -9,6 +9,7 @@ from courseware.models import StudentModule from django.conf import settings from django.test import TestCase from django.test.utils import override_settings +from django.test.client import RequestFactory from student.tests.factories import UserFactory from xmodule.modulestore.tests.factories import CourseFactory from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE @@ -22,15 +23,17 @@ from instructor.enrollment import ( send_beta_role_email, unenroll_email ) +from xmodule.modulestore.locations import SlashSeparatedCourseKey from submissions import api as sub_api from student.models import anonymous_id_for_user +from .test_tools import msk_from_problem_urlname class TestSettableEnrollmentState(TestCase): """ Test the basis class for enrollment tests. """ def setUp(self): - self.course_id = 'robot:/a/fake/c::rse/id' + self.course_key = SlashSeparatedCourseKey('Robot', 'fAKE', 'C-%-se-%-ID') def test_mes_create(self): """ @@ -43,8 +46,8 @@ class TestSettableEnrollmentState(TestCase): auto_enroll=False ) # enrollment objects - eobjs = mes.create_user(self.course_id) - ees = EmailEnrollmentState(self.course_id, eobjs.email) + eobjs = mes.create_user(self.course_key) + ees = EmailEnrollmentState(self.course_key, eobjs.email) self.assertEqual(mes, ees) @@ -60,7 +63,7 @@ class TestEnrollmentChangeBase(TestCase): __metaclass__ = ABCMeta def setUp(self): - self.course_id = 'robot:/a/fake/c::rse/id' + self.course_key = SlashSeparatedCourseKey('Robot', 'fAKE', 'C-%-se-%-ID') def _run_state_change_test(self, before_ideal, after_ideal, action): """ @@ -74,8 +77,8 @@ class TestEnrollmentChangeBase(TestCase): """ # initialize & check before print "checking initialization..." - eobjs = before_ideal.create_user(self.course_id) - before = EmailEnrollmentState(self.course_id, eobjs.email) + eobjs = before_ideal.create_user(self.course_key) + before = EmailEnrollmentState(self.course_key, eobjs.email) self.assertEqual(before, before_ideal) # do action @@ -84,7 +87,7 @@ class TestEnrollmentChangeBase(TestCase): # check after print "checking effects..." - after = EmailEnrollmentState(self.course_id, eobjs.email) + after = EmailEnrollmentState(self.course_key, eobjs.email) self.assertEqual(after, after_ideal) @@ -105,7 +108,7 @@ class TestInstructorEnrollDB(TestEnrollmentChangeBase): auto_enroll=False ) - action = lambda email: enroll_email(self.course_id, email) + action = lambda email: enroll_email(self.course_key, email) return self._run_state_change_test(before_ideal, after_ideal, action) @@ -124,7 +127,7 @@ class TestInstructorEnrollDB(TestEnrollmentChangeBase): auto_enroll=False, ) - action = lambda email: enroll_email(self.course_id, email) + action = lambda email: enroll_email(self.course_key, email) return self._run_state_change_test(before_ideal, after_ideal, action) @@ -143,7 +146,7 @@ class TestInstructorEnrollDB(TestEnrollmentChangeBase): auto_enroll=False, ) - action = lambda email: enroll_email(self.course_id, email) + action = lambda email: enroll_email(self.course_key, email) return self._run_state_change_test(before_ideal, after_ideal, action) @@ -162,7 +165,7 @@ class TestInstructorEnrollDB(TestEnrollmentChangeBase): auto_enroll=False, ) - action = lambda email: enroll_email(self.course_id, email) + action = lambda email: enroll_email(self.course_key, email) return self._run_state_change_test(before_ideal, after_ideal, action) @@ -181,7 +184,7 @@ class TestInstructorEnrollDB(TestEnrollmentChangeBase): auto_enroll=True, ) - action = lambda email: enroll_email(self.course_id, email, auto_enroll=True) + action = lambda email: enroll_email(self.course_key, email, auto_enroll=True) return self._run_state_change_test(before_ideal, after_ideal, action) @@ -200,7 +203,7 @@ class TestInstructorEnrollDB(TestEnrollmentChangeBase): auto_enroll=False, ) - action = lambda email: enroll_email(self.course_id, email, auto_enroll=False) + action = lambda email: enroll_email(self.course_key, email, auto_enroll=False) return self._run_state_change_test(before_ideal, after_ideal, action) @@ -222,7 +225,7 @@ class TestInstructorUnenrollDB(TestEnrollmentChangeBase): auto_enroll=False ) - action = lambda email: unenroll_email(self.course_id, email) + action = lambda email: unenroll_email(self.course_key, email) return self._run_state_change_test(before_ideal, after_ideal, action) @@ -241,7 +244,7 @@ class TestInstructorUnenrollDB(TestEnrollmentChangeBase): auto_enroll=False ) - action = lambda email: unenroll_email(self.course_id, email) + action = lambda email: unenroll_email(self.course_key, email) return self._run_state_change_test(before_ideal, after_ideal, action) @@ -260,7 +263,7 @@ class TestInstructorUnenrollDB(TestEnrollmentChangeBase): auto_enroll=False ) - action = lambda email: unenroll_email(self.course_id, email) + action = lambda email: unenroll_email(self.course_key, email) return self._run_state_change_test(before_ideal, after_ideal, action) @@ -279,58 +282,66 @@ class TestInstructorUnenrollDB(TestEnrollmentChangeBase): auto_enroll=False ) - action = lambda email: unenroll_email(self.course_id, email) + action = lambda email: unenroll_email(self.course_key, email) return self._run_state_change_test(before_ideal, after_ideal, action) +@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) class TestInstructorEnrollmentStudentModule(TestCase): """ Test student module manipulations. """ def setUp(self): - self.course_id = 'robot:/a/fake/c::rse/id' + self.course_key = SlashSeparatedCourseKey('fake', 'course', 'id') def test_reset_student_attempts(self): user = UserFactory() - msk = 'robot/module/state/key' + msk = self.course_key.make_usage_key('dummy', 'module') original_state = json.dumps({'attempts': 32, 'otherstuff': 'alsorobots'}) - module = StudentModule.objects.create(student=user, course_id=self.course_id, module_state_key=msk, state=original_state) + StudentModule.objects.create(student=user, course_id=self.course_key, module_id=msk, state=original_state) # lambda to reload the module state from the database - module = lambda: StudentModule.objects.get(student=user, course_id=self.course_id, module_state_key=msk) + module = lambda: StudentModule.objects.get(student=user, course_id=self.course_key, module_id=msk) self.assertEqual(json.loads(module().state)['attempts'], 32) - reset_student_attempts(self.course_id, user, msk) + reset_student_attempts(self.course_key, user, msk) self.assertEqual(json.loads(module().state)['attempts'], 0) def test_delete_student_attempts(self): user = UserFactory() - msk = 'robot/module/state/key' + msk = self.course_key.make_usage_key('dummy', 'module') original_state = json.dumps({'attempts': 32, 'otherstuff': 'alsorobots'}) - StudentModule.objects.create(student=user, course_id=self.course_id, module_state_key=msk, state=original_state) - self.assertEqual(StudentModule.objects.filter(student=user, course_id=self.course_id, module_state_key=msk).count(), 1) - reset_student_attempts(self.course_id, user, msk, delete_module=True) - self.assertEqual(StudentModule.objects.filter(student=user, course_id=self.course_id, module_state_key=msk).count(), 0) + StudentModule.objects.create(student=user, course_id=self.course_key, module_id=msk, state=original_state) + self.assertEqual(StudentModule.objects.filter(student=user, course_id=self.course_key, module_id=msk).count(), 1) + reset_student_attempts(self.course_key, user, msk, delete_module=True) + self.assertEqual(StudentModule.objects.filter(student=user, course_id=self.course_key, module_id=msk).count(), 0) def test_delete_submission_scores(self): user = UserFactory() - course_id = 'ora2/1/1' - item_id = 'i4x://ora2/1/openassessment/b3dce2586c9c4876b73e7f390e42ef8f' + course = CourseFactory.create() + problem_location = msk_from_problem_urlname( + course.id, + 'b3dce2586c9c4876b73e7f390e42ef8f', + block_type='openassessment' + ) # Create a student module for the user StudentModule.objects.create( - student=user, course_id=course_id, module_state_key=item_id, state=json.dumps({}) + student=user, + course_id=course.id, + module_state_key=problem_location, + state=json.dumps({}) ) # Create a submission and score for the student using the submissions API student_item = { - 'student_id': anonymous_id_for_user(user, course_id), - 'course_id': course_id, - 'item_id': item_id, + 'student_id': anonymous_id_for_user(user, course.id), + 'course_id': course.id, + 'item_id': problem_location, 'item_type': 'openassessment' } submission = sub_api.create_submission(student_item, 'test answer') sub_api.set_score(submission['uuid'], 1, 2) # Delete student state using the instructor dash - reset_student_attempts(course_id, user, item_id, delete_module=True) + reset_student_attempts(course.id, user, problem_location, delete_module=True) # Verify that the student's scores have been reset in the submissions API score = sub_api.get_score(student_item) @@ -436,7 +447,7 @@ class TestGetEmailParams(TestCase): site = settings.SITE_NAME self.course_url = u'https://{}/courses/{}/'.format( site, - self.course.id + self.course.id.to_deprecated_string() ) self.course_about_url = self.course_url + 'about' self.registration_url = u'https://{}/register'.format( diff --git a/lms/djangoapps/instructor/tests/test_hint_manager.py b/lms/djangoapps/instructor/tests/test_hint_manager.py index 09788a23b2..3080abbeb4 100644 --- a/lms/djangoapps/instructor/tests/test_hint_manager.py +++ b/lms/djangoapps/instructor/tests/test_hint_manager.py @@ -26,8 +26,8 @@ class HintManagerTest(ModuleStoreTestCase): self.user = UserFactory.create(username='robot', email='robot@edx.org', password='test', is_staff=True) self.c = Client() self.c.login(username='robot', password='test') - self.problem_id = 'i4x://Me/19.002/crowdsource_hinter/crowdsource_hinter_001' - self.course_id = 'Me/19.002/test_course' + self.course_id = self.course.id + self.problem_id = self.course_id.make_usage_key('crowdsource_hinter', 'crowdsource_hinter_001') UserStateSummaryFactory.create(field_name='hints', usage_id=self.problem_id, value=json.dumps({'1.0': {'1': ['Hint 1', 2], @@ -60,7 +60,7 @@ class HintManagerTest(ModuleStoreTestCase): """ Makes sure that staff can access the hint management view. """ - out = self.c.get('/courses/Me/19.002/test_course/hint_manager') + out = self.c.get(self.url) print out self.assertTrue('Hints Awaiting Moderation' in out.content) @@ -115,7 +115,7 @@ class HintManagerTest(ModuleStoreTestCase): request = RequestFactory() post = request.post(self.url, {'field': 'hints', 'op': 'delete hints', - 1: [self.problem_id, '1.0', '1']}) + 1: [self.problem_id.to_deprecated_string(), '1.0', '1']}) view.delete_hints(post, self.course_id, 'hints') problem_hints = XModuleUserStateSummaryField.objects.get(field_name='hints', usage_id=self.problem_id).value self.assertTrue('1' not in json.loads(problem_hints)['1.0']) @@ -127,7 +127,7 @@ class HintManagerTest(ModuleStoreTestCase): request = RequestFactory() post = request.post(self.url, {'field': 'hints', 'op': 'change votes', - 1: [self.problem_id, '1.0', '1', 5]}) + 1: [self.problem_id.to_deprecated_string(), '1.0', '1', 5]}) view.change_votes(post, self.course_id, 'hints') problem_hints = XModuleUserStateSummaryField.objects.get(field_name='hints', usage_id=self.problem_id).value # hints[answer][hint_pk (string)] = [hint text, vote count] @@ -146,7 +146,7 @@ class HintManagerTest(ModuleStoreTestCase): request = RequestFactory() post = request.post(self.url, {'field': 'mod_queue', 'op': 'add hint', - 'problem': self.problem_id, + 'problem': self.problem_id.to_deprecated_string(), 'answer': '3.14', 'hint': 'This is a new hint.'}) post.user = 'fake user' @@ -167,7 +167,7 @@ class HintManagerTest(ModuleStoreTestCase): request = RequestFactory() post = request.post(self.url, {'field': 'mod_queue', 'op': 'add hint', - 'problem': self.problem_id, + 'problem': self.problem_id.to_deprecated_string(), 'answer': 'fish', 'hint': 'This is a new hint.'}) post.user = 'fake user' @@ -185,7 +185,7 @@ class HintManagerTest(ModuleStoreTestCase): request = RequestFactory() post = request.post(self.url, {'field': 'mod_queue', 'op': 'approve', - 1: [self.problem_id, '2.0', '2']}) + 1: [self.problem_id.to_deprecated_string(), '2.0', '2']}) view.approve(post, self.course_id, 'mod_queue') problem_hints = XModuleUserStateSummaryField.objects.get(field_name='mod_queue', usage_id=self.problem_id).value self.assertTrue('2.0' not in json.loads(problem_hints) or len(json.loads(problem_hints)['2.0']) == 0) diff --git a/lms/djangoapps/instructor/tests/test_legacy_anon_csv.py b/lms/djangoapps/instructor/tests/test_legacy_anon_csv.py index b3a329ee05..c73a7be12d 100644 --- a/lms/djangoapps/instructor/tests/test_legacy_anon_csv.py +++ b/lms/djangoapps/instructor/tests/test_legacy_anon_csv.py @@ -20,6 +20,7 @@ from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE from student.roles import CourseStaffRole from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.django import modulestore, clear_existing_modulestores +from xmodule.modulestore.locations import SlashSeparatedCourseKey from mock import patch @@ -33,7 +34,7 @@ class TestInstructorDashboardAnonCSV(ModuleStoreTestCase, LoginEnrollmentTestCas # Note -- I copied this setUp from a similar test def setUp(self): clear_existing_modulestores() - self.toy = modulestore().get_course("edX/toy/2012_Fall") + self.toy = modulestore().get_course(SlashSeparatedCourseKey("edX", "toy", "2012_Fall")) # Create two accounts self.student = 'view@test.com' @@ -44,7 +45,7 @@ class TestInstructorDashboardAnonCSV(ModuleStoreTestCase, LoginEnrollmentTestCas self.activate_user(self.student) self.activate_user(self.instructor) - CourseStaffRole(self.toy.location).add_users(User.objects.get(email=self.instructor)) + CourseStaffRole(self.toy.id).add_users(User.objects.get(email=self.instructor)) self.logout() self.login(self.instructor, self.password) @@ -52,7 +53,7 @@ class TestInstructorDashboardAnonCSV(ModuleStoreTestCase, LoginEnrollmentTestCas def test_download_anon_csv(self): course = self.toy - url = reverse('instructor_dashboard', kwargs={'course_id': course.id}) + url = reverse('instructor_dashboard', kwargs={'course_id': course.id.to_deprecated_string()}) with patch('instructor.views.legacy.unique_id_for_user') as mock_unique: mock_unique.return_value = 42 diff --git a/lms/djangoapps/instructor/tests/test_legacy_download_csv.py b/lms/djangoapps/instructor/tests/test_legacy_download_csv.py index a03f029ff3..ae6e98e0eb 100644 --- a/lms/djangoapps/instructor/tests/test_legacy_download_csv.py +++ b/lms/djangoapps/instructor/tests/test_legacy_download_csv.py @@ -20,6 +20,7 @@ from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE from student.roles import CourseStaffRole from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.django import modulestore, clear_existing_modulestores +from xmodule.modulestore.locations import SlashSeparatedCourseKey @override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) @@ -30,7 +31,7 @@ class TestInstructorDashboardGradeDownloadCSV(ModuleStoreTestCase, LoginEnrollme def setUp(self): clear_existing_modulestores() - self.toy = modulestore().get_course("edX/toy/2012_Fall") + self.toy = modulestore().get_course(SlashSeparatedCourseKey("edX", "toy", "2012_Fall")) # Create two accounts self.student = 'view@test.com' @@ -41,7 +42,7 @@ class TestInstructorDashboardGradeDownloadCSV(ModuleStoreTestCase, LoginEnrollme self.activate_user(self.student) self.activate_user(self.instructor) - CourseStaffRole(self.toy.location).add_users(User.objects.get(email=self.instructor)) + CourseStaffRole(self.toy.id).add_users(User.objects.get(email=self.instructor)) self.logout() self.login(self.instructor, self.password) @@ -49,7 +50,7 @@ class TestInstructorDashboardGradeDownloadCSV(ModuleStoreTestCase, LoginEnrollme def test_download_grades_csv(self): course = self.toy - url = reverse('instructor_dashboard', kwargs={'course_id': course.id}) + url = reverse('instructor_dashboard', kwargs={'course_id': course.id.to_deprecated_string()}) msg = "url = {0}\n".format(url) response = self.client.post(url, {'action': 'Download CSV of all student grades for this course'}) msg += "instructor dashboard download csv grades: response = '{0}'\n".format(response) @@ -58,7 +59,7 @@ class TestInstructorDashboardGradeDownloadCSV(ModuleStoreTestCase, LoginEnrollme cdisp = response['Content-Disposition'] msg += "Content-Disposition = '%s'\n" % cdisp - self.assertEqual(cdisp, 'attachment; filename=grades_{0}.csv'.format(course.id), msg) + self.assertEqual(cdisp, 'attachment; filename=grades_{0}.csv'.format(course.id.to_deprecated_string()), msg) body = response.content.replace('\r', '') msg += "body = '{0}'\n".format(body) diff --git a/lms/djangoapps/instructor/tests/test_legacy_email.py b/lms/djangoapps/instructor/tests/test_legacy_email.py index c91d1cce2f..e818561f85 100644 --- a/lms/djangoapps/instructor/tests/test_legacy_email.py +++ b/lms/djangoapps/instructor/tests/test_legacy_email.py @@ -32,7 +32,7 @@ class TestInstructorDashboardEmailView(ModuleStoreTestCase): self.client.login(username=instructor.username, password="test") # URL for instructor dash - self.url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id}) + self.url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id.to_deprecated_string()}) # URL for email view self.email_link = 'Email' diff --git a/lms/djangoapps/instructor/tests/test_legacy_enrollment.py b/lms/djangoapps/instructor/tests/test_legacy_enrollment.py index 2a3b29c742..10a4528604 100644 --- a/lms/djangoapps/instructor/tests/test_legacy_enrollment.py +++ b/lms/djangoapps/instructor/tests/test_legacy_enrollment.py @@ -52,7 +52,7 @@ class TestInstructorEnrollsStudent(ModuleStoreTestCase, LoginEnrollmentTestCase) course = self.course # Run the Un-enroll students command - url = reverse('instructor_dashboard', kwargs={'course_id': course.id}) + url = reverse('instructor_dashboard', kwargs={'course_id': course.id.to_deprecated_string()}) response = self.client.post( url, { @@ -84,7 +84,7 @@ class TestInstructorEnrollsStudent(ModuleStoreTestCase, LoginEnrollmentTestCase) course = self.course # Run the Enroll students command - url = reverse('instructor_dashboard', kwargs={'course_id': course.id}) + url = reverse('instructor_dashboard', kwargs={'course_id': course.id.to_deprecated_string()}) response = self.client.post(url, {'action': 'Enroll multiple students', 'multiple_students': 'student1_1@test.com, student1_2@test.com', 'auto_enroll': 'on'}) # Check the page output @@ -129,7 +129,7 @@ class TestInstructorEnrollsStudent(ModuleStoreTestCase, LoginEnrollmentTestCase) course = self.course - url = reverse('instructor_dashboard', kwargs={'course_id': course.id}) + url = reverse('instructor_dashboard', kwargs={'course_id': course.id.to_deprecated_string()}) response = self.client.post(url, {'action': 'Enroll multiple students', 'multiple_students': 'student0@test.com', 'auto_enroll': 'on'}) self.assertContains(response, 'student0@test.com') self.assertContains(response, 'already enrolled') @@ -142,7 +142,7 @@ class TestInstructorEnrollsStudent(ModuleStoreTestCase, LoginEnrollmentTestCase) course = self.course # Run the Enroll students command - url = reverse('instructor_dashboard', kwargs={'course_id': course.id}) + url = reverse('instructor_dashboard', kwargs={'course_id': course.id.to_deprecated_string()}) response = self.client.post(url, {'action': 'Enroll multiple students', 'multiple_students': 'student2_1@test.com, student2_2@test.com'}) # Check the page output @@ -199,7 +199,7 @@ class TestInstructorEnrollsStudent(ModuleStoreTestCase, LoginEnrollmentTestCase) # Create activated, but not enrolled, user UserFactory.create(username="student3_0", email="student3_0@test.com", first_name='Autoenrolled') - url = reverse('instructor_dashboard', kwargs={'course_id': course.id}) + url = reverse('instructor_dashboard', kwargs={'course_id': course.id.to_deprecated_string()}) response = self.client.post(url, {'action': 'Enroll multiple students', 'multiple_students': 'student3_0@test.com, student3_1@test.com, student3_2@test.com', 'auto_enroll': 'on', 'email_students': 'on'}) # Check the page output @@ -254,7 +254,7 @@ class TestInstructorEnrollsStudent(ModuleStoreTestCase, LoginEnrollmentTestCase) cea = CourseEnrollmentAllowed(email='student4_0@test.com', course_id=course.id) cea.save() - url = reverse('instructor_dashboard', kwargs={'course_id': course.id}) + url = reverse('instructor_dashboard', kwargs={'course_id': course.id.to_deprecated_string()}) response = self.client.post(url, {'action': 'Unenroll multiple students', 'multiple_students': 'student4_0@test.com, student2@test.com, student3@test.com', 'email_students': 'on'}) # Check the page output @@ -301,7 +301,7 @@ class TestInstructorEnrollsStudent(ModuleStoreTestCase, LoginEnrollmentTestCase) # Create activated, but not enrolled, user UserFactory.create(username="student5_0", email="student5_0@test.com", first_name="ShibTest", last_name="Enrolled") - url = reverse('instructor_dashboard', kwargs={'course_id': course.id}) + url = reverse('instructor_dashboard', kwargs={'course_id': course.id.to_deprecated_string()}) response = self.client.post(url, {'action': 'Enroll multiple students', 'multiple_students': 'student5_0@test.com, student5_1@test.com', 'auto_enroll': 'on', 'email_students': 'on'}) # Check the page output diff --git a/lms/djangoapps/instructor/tests/test_legacy_forum_admin.py b/lms/djangoapps/instructor/tests/test_legacy_forum_admin.py index 2da8d18d73..09c60a300d 100644 --- a/lms/djangoapps/instructor/tests/test_legacy_forum_admin.py +++ b/lms/djangoapps/instructor/tests/test_legacy_forum_admin.py @@ -17,6 +17,7 @@ from courseware.tests.helpers import LoginEnrollmentTestCase from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE from student.roles import CourseStaffRole from xmodule.modulestore.django import modulestore, clear_existing_modulestores +from xmodule.modulestore.locations import SlashSeparatedCourseKey from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase @@ -42,7 +43,7 @@ class TestInstructorDashboardForumAdmin(ModuleStoreTestCase, LoginEnrollmentTest clear_existing_modulestores() courses = modulestore().get_courses() - self.course_id = "edX/toy/2012_Fall" + self.course_id = SlashSeparatedCourseKey("edX", "toy", "2012_Fall") self.toy = modulestore().get_course(self.course_id) # Create two accounts @@ -54,7 +55,7 @@ class TestInstructorDashboardForumAdmin(ModuleStoreTestCase, LoginEnrollmentTest self.activate_user(self.student) self.activate_user(self.instructor) - CourseStaffRole(self.toy.location).add_users(User.objects.get(email=self.instructor)) + CourseStaffRole(self.toy.id).add_users(User.objects.get(email=self.instructor)) self.logout() self.login(self.instructor, self.password) @@ -67,7 +68,7 @@ class TestInstructorDashboardForumAdmin(ModuleStoreTestCase, LoginEnrollmentTest def test_add_forum_admin_users_for_unknown_user(self): course = self.toy - url = reverse('instructor_dashboard', kwargs={'course_id': course.id}) + url = reverse('instructor_dashboard', kwargs={'course_id': course.id.to_deprecated_string()}) username = 'unknown' for action in ['Add', 'Remove']: for rolename in FORUM_ROLES: @@ -76,7 +77,7 @@ class TestInstructorDashboardForumAdmin(ModuleStoreTestCase, LoginEnrollmentTest def test_add_forum_admin_users_for_missing_roles(self): course = self.toy - url = reverse('instructor_dashboard', kwargs={'course_id': course.id}) + url = reverse('instructor_dashboard', kwargs={'course_id': course.id.to_deprecated_string()}) username = 'u1' for action in ['Add', 'Remove']: for rolename in FORUM_ROLES: @@ -86,7 +87,7 @@ class TestInstructorDashboardForumAdmin(ModuleStoreTestCase, LoginEnrollmentTest def test_remove_forum_admin_users_for_missing_users(self): course = self.toy self.initialize_roles(course.id) - url = reverse('instructor_dashboard', kwargs={'course_id': course.id}) + url = reverse('instructor_dashboard', kwargs={'course_id': course.id.to_deprecated_string()}) username = 'u1' action = 'Remove' for rolename in FORUM_ROLES: @@ -96,20 +97,20 @@ class TestInstructorDashboardForumAdmin(ModuleStoreTestCase, LoginEnrollmentTest def test_add_and_remove_forum_admin_users(self): course = self.toy self.initialize_roles(course.id) - url = reverse('instructor_dashboard', kwargs={'course_id': course.id}) + url = reverse('instructor_dashboard', kwargs={'course_id': course.id.to_deprecated_string()}) username = 'u2' for rolename in FORUM_ROLES: response = self.client.post(url, {'action': action_name('Add', rolename), FORUM_ADMIN_USER[rolename]: username}) - self.assertTrue(response.content.find('Added "{0}" to "{1}" forum role = "{2}"'.format(username, course.id, rolename)) >= 0) + self.assertContains(response, 'Added "{0}" to "{1}" forum role = "{2}"'.format(username, course.id.to_deprecated_string(), rolename)) self.assertTrue(has_forum_access(username, course.id, rolename)) response = self.client.post(url, {'action': action_name('Remove', rolename), FORUM_ADMIN_USER[rolename]: username}) - self.assertTrue(response.content.find('Removed "{0}" from "{1}" forum role = "{2}"'.format(username, course.id, rolename)) >= 0) + self.assertContains(response, 'Removed "{0}" from "{1}" forum role = "{2}"'.format(username, course.id.to_deprecated_string(), rolename)) self.assertFalse(has_forum_access(username, course.id, rolename)) def test_add_and_read_forum_admin_users(self): course = self.toy self.initialize_roles(course.id) - url = reverse('instructor_dashboard', kwargs={'course_id': course.id}) + url = reverse('instructor_dashboard', kwargs={'course_id': course.id.to_deprecated_string()}) username = 'u2' for rolename in FORUM_ROLES: # perform an add, and follow with a second identical add: @@ -121,7 +122,7 @@ class TestInstructorDashboardForumAdmin(ModuleStoreTestCase, LoginEnrollmentTest def test_add_nonstaff_forum_admin_users(self): course = self.toy self.initialize_roles(course.id) - url = reverse('instructor_dashboard', kwargs={'course_id': course.id}) + url = reverse('instructor_dashboard', kwargs={'course_id': course.id.to_deprecated_string()}) username = 'u1' rolename = FORUM_ROLE_ADMINISTRATOR response = self.client.post(url, {'action': action_name('Add', rolename), FORUM_ADMIN_USER[rolename]: username}) @@ -130,7 +131,7 @@ class TestInstructorDashboardForumAdmin(ModuleStoreTestCase, LoginEnrollmentTest def test_list_forum_admin_users(self): course = self.toy self.initialize_roles(course.id) - url = reverse('instructor_dashboard', kwargs={'course_id': course.id}) + url = reverse('instructor_dashboard', kwargs={'course_id': course.id.to_deprecated_string()}) username = 'u2' added_roles = [FORUM_ROLE_STUDENT] # u2 is already added as a student to the discussion forums self.assertTrue(has_forum_access(username, course.id, 'Student')) diff --git a/lms/djangoapps/instructor/tests/test_legacy_gradebook.py b/lms/djangoapps/instructor/tests/test_legacy_gradebook.py index bca7528a96..871ced01db 100644 --- a/lms/djangoapps/instructor/tests/test_legacy_gradebook.py +++ b/lms/djangoapps/instructor/tests/test_legacy_gradebook.py @@ -11,6 +11,7 @@ from courseware.tests.tests import TEST_DATA_MIXED_MODULESTORE from capa.tests.response_xml_factory import StringResponseXMLFactory from courseware.tests.factories import StudentModuleFactory from xmodule.modulestore import Location +from xmodule.modulestore.locations import SlashSeparatedCourseKey from xmodule.modulestore.django import modulestore @@ -64,10 +65,13 @@ class TestGradebook(ModuleStoreTestCase): max_grade=1, student=user, course_id=self.course.id, - module_state_key=Location(item.location).url() + module_id=item.location ) - self.response = self.client.get(reverse('gradebook', args=(self.course.id,))) + self.response = self.client.get(reverse( + 'gradebook', + args=(self.course.id.to_deprecated_string(),) + )) def test_response_code(self): self.assertEquals(self.response.status_code, 200) diff --git a/lms/djangoapps/instructor/tests/test_legacy_raw_download_csv.py b/lms/djangoapps/instructor/tests/test_legacy_raw_download_csv.py index cb7aa803d0..36f7c34b6c 100644 --- a/lms/djangoapps/instructor/tests/test_legacy_raw_download_csv.py +++ b/lms/djangoapps/instructor/tests/test_legacy_raw_download_csv.py @@ -24,7 +24,7 @@ class TestRawGradeCSV(TestSubmittingProblems): self.instructor = 'view2@test.com' self.create_account('u2', self.instructor, self.password) self.activate_user(self.instructor) - CourseStaffRole(self.course.location).add_users(User.objects.get(email=self.instructor)) + CourseStaffRole(self.course.id).add_users(User.objects.get(email=self.instructor)) self.logout() self.login(self.instructor, self.password) self.enroll(self.course) @@ -45,7 +45,7 @@ class TestRawGradeCSV(TestSubmittingProblems): resp = self.submit_question_answer('p2', {'2_1': 'Correct'}) self.assertEqual(resp.status_code, 200) - url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id}) + url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id.to_deprecated_string()}) msg = "url = {0}\n".format(url) response = self.client.post(url, {'action': 'Download CSV of all RAW grades'}) msg += "instructor dashboard download raw csv grades: response = '{0}'\n".format(response) diff --git a/lms/djangoapps/instructor/tests/test_legacy_reset.py b/lms/djangoapps/instructor/tests/test_legacy_reset.py index ea259bc1fb..4687fa2f7e 100644 --- a/lms/djangoapps/instructor/tests/test_legacy_reset.py +++ b/lms/djangoapps/instructor/tests/test_legacy_reset.py @@ -16,6 +16,7 @@ from courseware.models import StudentModule from submissions import api as sub_api from student.models import anonymous_id_for_user +from .test_tools import msk_from_problem_urlname @override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) @@ -35,29 +36,36 @@ class InstructorResetStudentStateTest(ModuleStoreTestCase, LoginEnrollmentTestCa CourseEnrollmentFactory.create(user=self.student, course_id=self.course.id) def test_delete_student_state_resets_scores(self): - item_id = 'i4x://MITx/999/openassessment/b3dce2586c9c4876b73e7f390e42ef8f' + problem_location = msk_from_problem_urlname( + self.course.id, + 'b3dce2586c9c4876b73e7f390e42ef8f', + block_type='openassessment' + ) # Create a student module for the user StudentModule.objects.create( - student=self.student, course_id=self.course.id, module_state_key=item_id, state=json.dumps({}) + student=self.student, + course_id=self.course.id, + module_state_key=problem_location, + state=json.dumps({}) ) # Create a submission and score for the student using the submissions API student_item = { 'student_id': anonymous_id_for_user(self.student, self.course.id), 'course_id': self.course.id, - 'item_id': item_id, + 'item_id': problem_location, 'item_type': 'openassessment' } submission = sub_api.create_submission(student_item, 'test answer') sub_api.set_score(submission['uuid'], 1, 2) # Delete student state using the instructor dash - url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id}) + url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.post(url, { 'action': 'Delete student state for module', 'unique_student_identifier': self.student.email, - 'problem_for_student': 'openassessment/b3dce2586c9c4876b73e7f390e42ef8f', + 'problem_for_student': str(problem_location), }) self.assertEqual(response.status_code, 200) diff --git a/lms/djangoapps/instructor/tests/test_legacy_xss.py b/lms/djangoapps/instructor/tests/test_legacy_xss.py index d748876032..9c807b022c 100644 --- a/lms/djangoapps/instructor/tests/test_legacy_xss.py +++ b/lms/djangoapps/instructor/tests/test_legacy_xss.py @@ -48,7 +48,7 @@ class TestXss(ModuleStoreTestCase): ) req.user = self._instructor req.session = {} - resp = legacy.instructor_dashboard(req, self._course.id) + resp = legacy.instructor_dashboard(req, self._course.id.to_deprecated_string()) respUnicode = resp.content.decode(settings.DEFAULT_CHARSET) self.assertNotIn(self._evil_student.profile.name, respUnicode) self.assertIn(escape(self._evil_student.profile.name), respUnicode) diff --git a/lms/djangoapps/instructor/tests/test_tools.py b/lms/djangoapps/instructor/tests/test_tools.py index 6f5850f7ee..b3a8888d86 100644 --- a/lms/djangoapps/instructor/tests/test_tools.py +++ b/lms/djangoapps/instructor/tests/test_tools.py @@ -17,6 +17,7 @@ from student.tests.factories import UserFactory from xmodule.fields import Date from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory +from xmodule.modulestore.keys import CourseKey from ..views import tools @@ -86,10 +87,8 @@ class TestFindUnit(ModuleStoreTestCase): Fixtures. """ course = CourseFactory.create() - week1 = ItemFactory.create() - homework = ItemFactory.create(parent_location=week1.location) - week1.children.append(homework.location) - course.children.append(week1.location) + week1 = ItemFactory.create(parent=course) + homework = ItemFactory.create(parent=week1) self.course = course self.homework = homework @@ -98,7 +97,7 @@ class TestFindUnit(ModuleStoreTestCase): """ Test finding a nested unit. """ - url = self.homework.location.url() + url = self.homework.location.to_deprecated_string() self.assertEqual(tools.find_unit(self.course, url), self.homework) def test_find_unit_notfound(self): @@ -121,15 +120,13 @@ class TestGetUnitsWithDueDate(ModuleStoreTestCase): """ due = datetime.datetime(2010, 5, 12, 2, 42, tzinfo=utc) course = CourseFactory.create() - week1 = ItemFactory.create(due=due) - week2 = ItemFactory.create(due=due) - course.children = [week1.location.url(), week2.location.url()] + week1 = ItemFactory.create(due=due, parent=course) + week2 = ItemFactory.create(due=due, parent=course) homework = ItemFactory.create( - parent_location=week1.location, + parent=week1, due=due ) - week1.children = [homework.location.url()] self.course = course self.week1 = week1 @@ -139,7 +136,7 @@ class TestGetUnitsWithDueDate(ModuleStoreTestCase): def urls(seq): "URLs for sequence of nodes." - return sorted(i.location.url() for i in seq) + return sorted(i.location.to_deprecated_string() for i in seq) self.assertEquals( urls(tools.get_units_with_due_date(self.course)), @@ -156,7 +153,7 @@ class TestTitleOrUrl(unittest.TestCase): def test_url(self): unit = mock.Mock(display_name=None) - unit.location.url.return_value = 'test:hello' + unit.location.to_deprecated_string.return_value = 'test:hello' self.assertEquals(tools.title_or_url(unit), 'test:hello') @@ -171,27 +168,25 @@ class TestSetDueDateExtension(ModuleStoreTestCase): """ due = datetime.datetime(2010, 5, 12, 2, 42, tzinfo=utc) course = CourseFactory.create() - week1 = ItemFactory.create(due=due) - week2 = ItemFactory.create(due=due) - course.children = [week1.location.url(), week2.location.url()] + week1 = ItemFactory.create(due=due, parent=course) + week2 = ItemFactory.create(due=due, parent=course) homework = ItemFactory.create( - parent_location=week1.location, + parent=week1, due=due ) - week1.children = [homework.location.url()] user = UserFactory.create() StudentModule( state='{}', student_id=user.id, course_id=course.id, - module_state_key=week1.location.url()).save() + module_state_key=week1.location).save() StudentModule( state='{}', student_id=user.id, course_id=course.id, - module_state_key=homework.location.url()).save() + module_state_key=homework.location).save() self.course = course self.week1 = week1 @@ -226,63 +221,60 @@ class TestDataDumps(ModuleStoreTestCase): """ due = datetime.datetime(2010, 5, 12, 2, 42, tzinfo=utc) course = CourseFactory.create() - week1 = ItemFactory.create(due=due) - week2 = ItemFactory.create(due=due) - week3 = ItemFactory.create(due=due) - course.children = [week1.location.url(), week2.location.url(), - week3.location.url()] + week1 = ItemFactory.create(due=due, parent=course) + week2 = ItemFactory.create(due=due, parent=course) + week3 = ItemFactory.create(due=due, parent=course) homework = ItemFactory.create( - parent_location=week1.location, + parent=week1, due=due ) - week1.children = [homework.location.url()] user1 = UserFactory.create() StudentModule( state='{}', student_id=user1.id, course_id=course.id, - module_state_key=week1.location.url()).save() + module_state_key=week1.location).save() StudentModule( state='{}', student_id=user1.id, course_id=course.id, - module_state_key=week2.location.url()).save() + module_state_key=week2.location).save() StudentModule( state='{}', student_id=user1.id, course_id=course.id, - module_state_key=week3.location.url()).save() + module_state_key=week3.location).save() StudentModule( state='{}', student_id=user1.id, course_id=course.id, - module_state_key=homework.location.url()).save() + module_state_key=homework.location).save() user2 = UserFactory.create() StudentModule( state='{}', student_id=user2.id, course_id=course.id, - module_state_key=week1.location.url()).save() + module_state_key=week1.location).save() StudentModule( state='{}', student_id=user2.id, course_id=course.id, - module_state_key=homework.location.url()).save() + module_state_key=homework.location).save() user3 = UserFactory.create() StudentModule( state='{}', student_id=user3.id, course_id=course.id, - module_state_key=week1.location.url()).save() + module_state_key=week1.location).save() StudentModule( state='{}', student_id=user3.id, course_id=course.id, - module_state_key=homework.location.url()).save() + module_state_key=homework.location).save() self.course = course self.week1 = week1 @@ -337,10 +329,22 @@ def get_extended_due(course, unit, student): student_module = StudentModule.objects.get( student_id=student.id, course_id=course.id, - module_state_key=unit.location.url() + module_id=unit.location ) state = json.loads(student_module.state) extended = state.get('extended_due', None) if extended: return DATE_FIELD.from_json(extended) + + +def msk_from_problem_urlname(course_id, urlname, block_type='problem'): + """ + Convert a 'problem urlname' to a module state key (db field) + """ + if not isinstance(course_id, CourseKey): + raise ValueError + if urlname.endswith(".xml"): + urlname = urlname[:-4] + + return course_id.make_usage_key(block_type, urlname) diff --git a/lms/djangoapps/instructor/utils.py b/lms/djangoapps/instructor/utils.py index 4445f9e34d..79ba39f078 100644 --- a/lms/djangoapps/instructor/utils.py +++ b/lms/djangoapps/instructor/utils.py @@ -27,12 +27,12 @@ class DummyRequest(object): return False -def get_module_for_student(student, course, location, request=None): +def get_module_for_student(student, usage_key, request=None): """Return the module for the (student, location) using a DummyRequest.""" if request is None: request = DummyRequest() request.user = student - descriptor = modulestore().get_instance(course.id, location, depth=0) - field_data_cache = FieldDataCache([descriptor], course.id, student) - return get_module(student, request, location, field_data_cache, course.id) + descriptor = modulestore().get_item(usage_key, depth=0) + field_data_cache = FieldDataCache([descriptor], usage_key.course_key, student) + return get_module(student, request, usage_key, field_data_cache) diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py index 293d141efe..b05de8f019 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -68,6 +68,9 @@ from .tools import ( strip_if_string, ) from xmodule.modulestore import Location +from xmodule.modulestore.locations import SlashSeparatedCourseKey +from xmodule.modulestore.keys import UsageKey +from opaque_keys import InvalidKeyError log = logging.getLogger(__name__) @@ -191,9 +194,9 @@ def require_level(level): def decorator(func): # pylint: disable=C0111 def wrapped(*args, **kwargs): # pylint: disable=C0111 request = args[0] - course = get_course_by_id(kwargs['course_id']) + course = get_course_by_id(SlashSeparatedCourseKey.from_deprecated_string(kwargs['course_id'])) - if has_access(request.user, course, level): + if has_access(request.user, level, course): return func(*args, **kwargs) else: return HttpResponseForbidden() @@ -242,6 +245,7 @@ def students_update_enrollment(request, course_id): ] } """ + course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) action = request.GET.get('action') identifiers_raw = request.GET.get('identifiers') @@ -331,6 +335,7 @@ def bulk_beta_modify_access(request, course_id): anything split_input_list can handle. - action is one of ['add', 'remove'] """ + course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) action = request.GET.get('action') identifiers_raw = request.GET.get('identifiers') identifiers = _split_input_list(identifiers_raw) @@ -413,8 +418,9 @@ def modify_access(request, course_id): rolename is one of ['instructor', 'staff', 'beta'] action is one of ['allow', 'revoke'] """ + course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) course = get_course_with_access( - request.user, course_id, 'instructor', depth=None + request.user, 'instructor', course_id, depth=None ) try: user = get_student_from_identifier(request.GET.get('unique_student_identifier')) @@ -494,8 +500,9 @@ def list_course_role_members(request, course_id): ] } """ + course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) course = get_course_with_access( - request.user, course_id, 'instructor', depth=None + request.user, 'instructor', course_id, depth=None ) rolename = request.GET.get('rolename') @@ -513,7 +520,7 @@ def list_course_role_members(request, course_id): } response_payload = { - 'course_id': course_id, + 'course_id': course_id.to_deprecated_string(), rolename: map(extract_user_info, list_with_level( course, rolename )), @@ -528,13 +535,14 @@ def get_grading_config(request, course_id): """ Respond with json which contains a html formatted grade summary. """ + course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) course = get_course_with_access( - request.user, course_id, 'staff', depth=None + request.user, 'staff', course_id, depth=None ) grading_config_summary = analytics.basic.dump_grading_context(course) response_payload = { - 'course_id': course_id, + 'course_id': course_id.to_deprecated_string(), 'grading_config_summary': grading_config_summary, } return JsonResponse(response_payload) @@ -552,6 +560,8 @@ def get_students_features(request, course_id, csv=False): # pylint: disable=W06 TO DO accept requests for different attribute sets. """ + course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) + available_features = analytics.basic.AVAILABLE_FEATURES query_features = [ 'username', 'name', 'email', 'language', 'location', 'year_of_birth', @@ -578,7 +588,7 @@ def get_students_features(request, course_id, csv=False): # pylint: disable=W06 if not csv: response_payload = { - 'course_id': course_id, + 'course_id': course_id.to_deprecated_string(), 'students': student_data, 'students_count': len(student_data), 'queried_features': query_features, @@ -601,6 +611,7 @@ def get_anon_ids(request, course_id): # pylint: disable=W0613 # TODO: the User.objects query and CSV generation here could be # centralized into analytics. Currently analytics has similar functionality # but not quite what's needed. + course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) def csv_response(filename, header, rows): """Returns a CSV http response for the given header and rows (excel/utf-8).""" response = HttpResponse(mimetype='text/csv') @@ -620,7 +631,7 @@ def get_anon_ids(request, course_id): # pylint: disable=W0613 ).order_by('id') header = ['User ID', 'Anonymized user ID'] rows = [[s.id, unique_id_for_user(s)] for s in students] - return csv_response(course_id.replace('/', '-') + '-anon-ids.csv', header, rows) + return csv_response(course_id.to_deprecated_string().replace('/', '-') + '-anon-ids.csv', header, rows) @ensure_csrf_cookie @@ -635,6 +646,7 @@ def get_distribution(request, course_id): empty response['feature_results'] object. A list of available will be available in the response['available_features'] """ + course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) feature = request.GET.get('feature') # alternate notations of None if feature in (None, 'null', ''): @@ -650,7 +662,7 @@ def get_distribution(request, course_id): )) response_payload = { - 'course_id': course_id, + 'course_id': course_id.to_deprecated_string(), 'queried_feature': feature, 'available_features': available_features, 'feature_display_names': analytics.distributions.DISPLAY_NAMES, @@ -689,12 +701,13 @@ def get_student_progress_url(request, course_id): 'progress_url': '/../...' } """ + course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) user = get_student_from_identifier(request.GET.get('unique_student_identifier')) - progress_url = reverse('student_progress', kwargs={'course_id': course_id, 'student_id': user.id}) + progress_url = reverse('student_progress', kwargs={'course_id': course_id.to_deprecated_string(), 'student_id': user.id}) response_payload = { - 'course_id': course_id, + 'course_id': course_id.to_deprecated_string(), 'progress_url': progress_url, } return JsonResponse(response_payload) @@ -725,8 +738,9 @@ def reset_student_attempts(request, course_id): requires instructor access mutually exclusive with all_students """ + course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) course = get_course_with_access( - request.user, course_id, 'staff', depth=None + request.user, 'staff', course_id, depth=None ) problem_to_reset = strip_if_string(request.GET.get('problem_to_reset')) @@ -749,10 +763,13 @@ def reset_student_attempts(request, course_id): # instructor authorization if all_students or delete_module: - if not has_access(request.user, course, 'instructor'): + if not has_access(request.user, 'instructor', course): return HttpResponseForbidden("Requires instructor access.") - module_state_key = _msk_from_problem_urlname(course_id, problem_to_reset) + try: + module_state_key = UsageKey.from_string(problem_to_reset) + except InvalidKeyError: + return HttpResponseBadRequest() response_payload = {} response_payload['problem_to_reset'] = problem_to_reset @@ -768,7 +785,7 @@ def reset_student_attempts(request, course_id): return HttpResponse(error_msg, status=500) response_payload['student'] = student_identifier elif all_students: - instructor_task.api.submit_reset_problem_attempts_for_all_students(request, course_id, module_state_key) + instructor_task.api.submit_reset_problem_attempts_for_all_students(request, module_state_key) response_payload['task'] = 'created' response_payload['student'] = 'All Students' else: @@ -794,6 +811,7 @@ def rescore_problem(request, course_id): all_students and unique_student_identifier cannot both be present. """ + course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) problem_to_reset = strip_if_string(request.GET.get('problem_to_reset')) student_identifier = request.GET.get('unique_student_identifier', None) student = None @@ -810,17 +828,20 @@ def rescore_problem(request, course_id): "Cannot rescore with all_students and unique_student_identifier." ) - module_state_key = _msk_from_problem_urlname(course_id, problem_to_reset) + try: + module_state_key = UsageKey.from_string(problem_to_reset) + except InvalidKeyError: + return HttpResponseBadRequest() response_payload = {} response_payload['problem_to_reset'] = problem_to_reset if student: response_payload['student'] = student_identifier - instructor_task.api.submit_rescore_problem_for_student(request, course_id, module_state_key, student) + instructor_task.api.submit_rescore_problem_for_student(request, module_state_key, student) response_payload['task'] = 'created' elif all_students: - instructor_task.api.submit_rescore_problem_for_all_students(request, course_id, module_state_key) + instructor_task.api.submit_rescore_problem_for_all_students(request, module_state_key) response_payload['task'] = 'created' else: return HttpResponseBadRequest() @@ -874,6 +895,7 @@ def list_background_email_tasks(request, course_id): # pylint: disable=unused-a """ List background email tasks. """ + course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) task_type = 'bulk_course_email' # Specifying for the history of a single task type tasks = instructor_task.api.get_instructor_task_history(course_id, task_type=task_type) @@ -893,22 +915,26 @@ def list_instructor_tasks(request, course_id): Takes optional query paremeters. - With no arguments, lists running tasks. - - `problem_urlname` lists task history for problem - - `problem_urlname` and `unique_student_identifier` lists task + - `problem_location_str` lists task history for problem + - `problem_location_str` and `unique_student_identifier` lists task history for problem AND student (intersection) """ - problem_urlname = strip_if_string(request.GET.get('problem_urlname', False)) + course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) + problem_location_str = strip_if_string(request.GET.get('problem_location_str', False)) student = request.GET.get('unique_student_identifier', None) if student is not None: student = get_student_from_identifier(student) - if student and not problem_urlname: + if student and not problem_location_str: return HttpResponseBadRequest( - "unique_student_identifier must accompany problem_urlname" + "unique_student_identifier must accompany problem_location_str" ) - if problem_urlname: - module_state_key = _msk_from_problem_urlname(course_id, problem_urlname) + if problem_location_str: + try: + module_state_key = UsageKey.from_string(problem_location_str) + except InvalidKeyError: + return HttpResponseBadRequest() if student: # Specifying for a single student's history on this problem tasks = instructor_task.api.get_instructor_task_history(course_id, module_state_key, student) @@ -932,6 +958,7 @@ def list_report_downloads(_request, course_id): """ List grade CSV files that are available for download for this course. """ + course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) report_store = ReportStore.from_config() response_payload = { @@ -950,8 +977,9 @@ def calculate_grades_csv(request, course_id): """ AlreadyRunningError is raised if the course's grades are already being updated. """ + course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) try: - instructor_task.api.submit_calculate_grades_csv(request, course_id) + instructor_task.api.submit_calculate_grades_csv(request, course_key) success_status = _("Your grade report is being generated! You can view the status of the generation task in the 'Pending Instructor Tasks' section.") return JsonResponse({"status": success_status}) except AlreadyRunningError: @@ -976,8 +1004,9 @@ def list_forum_members(request, course_id): Takes query parameter `rolename`. """ + course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) course = get_course_by_id(course_id) - has_instructor_access = has_access(request.user, course, 'instructor') + has_instructor_access = has_access(request.user, 'instructor', course) has_forum_admin = has_forum_access( request.user, course_id, FORUM_ROLE_ADMINISTRATOR ) @@ -1016,7 +1045,7 @@ def list_forum_members(request, course_id): } response_payload = { - 'course_id': course_id, + 'course_id': course_id.to_deprecated_string(), rolename: map(extract_user_info, users), } return JsonResponse(response_payload) @@ -1036,6 +1065,7 @@ def send_email(request, course_id): - 'subject' specifies email's subject - 'message' specifies email's content """ + course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) send_to = request.POST.get("send_to") subject = request.POST.get("subject") message = request.POST.get("message") @@ -1047,8 +1077,7 @@ def send_email(request, course_id): # Submit the task, so that the correct InstructorTask object gets created (for monitoring purposes) instructor_task.api.submit_bulk_course_email(request, course_id, email.id) # pylint: disable=E1101 - - response_payload = {'course_id': course_id} + response_payload = {'course_id': course_id.to_deprecated_string()} return JsonResponse(response_payload) @@ -1075,8 +1104,9 @@ def update_forum_role_membership(request, course_id): - `rolename` is one of [FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA] - `action` is one of ['allow', 'revoke'] """ + course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) course = get_course_by_id(course_id) - has_instructor_access = has_access(request.user, course, 'instructor') + has_instructor_access = has_access(request.user, 'instructor', course) has_forum_admin = has_forum_access( request.user, course_id, FORUM_ROLE_ADMINISTRATOR ) @@ -1101,7 +1131,7 @@ def update_forum_role_membership(request, course_id): )) user = get_student_from_identifier(unique_student_identifier) - target_is_instructor = has_access(user, course, 'instructor') + target_is_instructor = has_access(user, 'instructor', course) # cannot revoke instructor if target_is_instructor and action == 'revoke' and rolename == FORUM_ROLE_ADMINISTRATOR: return HttpResponseBadRequest("Cannot revoke instructor forum admin privileges.") @@ -1112,7 +1142,7 @@ def update_forum_role_membership(request, course_id): return HttpResponseBadRequest("Role does not exist.") response_payload = { - 'course_id': course_id, + 'course_id': course_id.to_deprecated_string(), 'action': action, } return JsonResponse(response_payload) @@ -1131,6 +1161,7 @@ def proxy_legacy_analytics(request, course_id): `aname` is a query parameter specifying which analytic to query. """ + course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) analytics_name = request.GET.get('aname') # abort if misconfigured @@ -1140,7 +1171,7 @@ def proxy_legacy_analytics(request, course_id): url = "{}get?aname={}&course_id={}&apikey={}".format( settings.ANALYTICS_SERVER_URL, analytics_name, - course_id, + course_id.to_deprecated_string(), settings.ANALYTICS_API_KEY, ) @@ -1175,9 +1206,9 @@ def _display_unit(unit): """ name = getattr(unit, 'display_name', None) if name: - return u'{0} ({1})'.format(name, unit.location.url()) + return u'{0} ({1})'.format(name, unit.location.to_deprecated_string()) else: - return unit.location.url() + return unit.location.to_deprecated_string() @handle_dashboard_error @@ -1189,7 +1220,7 @@ def change_due_date(request, course_id): """ Grants a due date extension to a student for a particular unit. """ - course = get_course_by_id(course_id) + course = get_course_by_id(SlashSeparatedCourseKey.from_deprecated_string(course_id)) student = get_student_from_identifier(request.GET.get('student')) unit = find_unit(course, request.GET.get('url')) due_date = parse_datetime(request.GET.get('due_datetime')) @@ -1210,7 +1241,7 @@ def reset_due_date(request, course_id): """ Rescinds a due date extension for a student on a particular unit. """ - course = get_course_by_id(course_id) + course = get_course_by_id(SlashSeparatedCourseKey.from_deprecated_string(course_id)) student = get_student_from_identifier(request.GET.get('student')) unit = find_unit(course, request.GET.get('url')) set_due_date_extension(course, unit, student, None) @@ -1230,7 +1261,7 @@ def show_unit_extensions(request, course_id): """ Shows all of the students which have due date extensions for the given unit. """ - course = get_course_by_id(course_id) + course = get_course_by_id(SlashSeparatedCourseKey.from_deprecated_string(course_id)) unit = find_unit(course, request.GET.get('url')) return JsonResponse(dump_module_extensions(course, unit)) @@ -1246,7 +1277,7 @@ def show_student_extensions(request, course_id): particular course. """ student = get_student_from_identifier(request.GET.get('student')) - course = get_course_by_id(course_id) + course = get_course_by_id(SlashSeparatedCourseKey.from_deprecated_string(course_id)) return JsonResponse(dump_student_extensions(course, student)) @@ -1267,23 +1298,3 @@ def _split_input_list(str_list): new_list = [s for s in new_list if s != ''] return new_list - - -def _msk_from_problem_urlname(course_id, urlname): - """ - Convert a 'problem urlname' (name that instructor's input into dashboard) - to a module state key (db field) - """ - if urlname.endswith(".xml"): - urlname = urlname[:-4] - - # Combined open ended problems also have state that can be deleted. However, - # prepending "problem" will only allow capa problems to be reset. - # Get around this for xblock problems. - if "/" not in urlname: - urlname = "problem/" + urlname - - parts = Location.parse_course_id(course_id) - parts['urlname'] = urlname - module_state_key = u"i4x://{org}/{course}/{urlname}".format(**parts) - return module_state_key diff --git a/lms/djangoapps/instructor/views/instructor_dashboard.py b/lms/djangoapps/instructor/views/instructor_dashboard.py index fef3e17be4..f38d8cfc69 100644 --- a/lms/djangoapps/instructor/views/instructor_dashboard.py +++ b/lms/djangoapps/instructor/views/instructor_dashboard.py @@ -11,9 +11,10 @@ from django.utils.html import escape from django.http import Http404 from django.conf import settings +from lms.lib.xblock.runtime import quote_slashes from xmodule_modifiers import wrap_xblock from xmodule.html_module import HtmlDescriptor -from xmodule.modulestore import XML_MODULESTORE_TYPE, Location +from xmodule.modulestore import XML_MODULESTORE_TYPE from xmodule.modulestore.django import modulestore from xblock.field_data import DictFieldData from xblock.fields import ScopeIds @@ -26,22 +27,23 @@ from bulk_email.models import CourseAuthorization from class_dashboard.dashboard_data import get_section_display_name, get_array_section_has_problem from .tools import get_units_with_due_date, title_or_url +from xmodule.modulestore.locations import SlashSeparatedCourseKey @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) def instructor_dashboard_2(request, course_id): """ Display the instructor dashboard for a course. """ - - course = get_course_by_id(course_id, depth=None) - is_studio_course = (modulestore().get_modulestore_type(course_id) != XML_MODULESTORE_TYPE) + course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) + course = get_course_by_id(course_key, depth=None) + is_studio_course = (modulestore().get_modulestore_type(course_key) != XML_MODULESTORE_TYPE) access = { 'admin': request.user.is_staff, - 'instructor': has_access(request.user, course, 'instructor'), - 'staff': has_access(request.user, course, 'staff'), + 'instructor': has_access(request.user, 'instructor', course), + 'staff': has_access(request.user, 'staff', course), 'forum_admin': has_forum_access( - request.user, course_id, FORUM_ROLE_ADMINISTRATOR + request.user, course_key, FORUM_ROLE_ADMINISTRATOR ), } @@ -49,23 +51,24 @@ def instructor_dashboard_2(request, course_id): raise Http404() sections = [ - _section_course_info(course_id, access), - _section_membership(course_id, access), - _section_student_admin(course_id, access), - _section_data_download(course_id, access), - _section_analytics(course_id, access), + _section_course_info(course_key, access), + _section_membership(course_key, access), + _section_student_admin(course_key, access), + _section_data_download(course_key, access), + _section_analytics(course_key, access), ] if (settings.FEATURES.get('INDIVIDUAL_DUE_DATES') and access['instructor']): sections.insert(3, _section_extensions(course)) # Gate access to course email by feature flag & by course-specific authorization - if settings.FEATURES['ENABLE_INSTRUCTOR_EMAIL'] and is_studio_course and CourseAuthorization.instructor_email_enabled(course_id): - sections.append(_section_send_email(course_id, access, course)) + if settings.FEATURES['ENABLE_INSTRUCTOR_EMAIL'] and \ + is_studio_course and CourseAuthorization.instructor_email_enabled(course_key): + sections.append(_section_send_email(course_key, access, course)) # Gate access to Metrics tab by featue flag and staff authorization if settings.FEATURES['CLASS_DASHBOARD'] and access['staff']: - sections.append(_section_metrics(course_id, access)) + sections.append(_section_metrics(course_key, access)) studio_url = None if is_studio_course: @@ -79,7 +82,7 @@ def instructor_dashboard_2(request, course_id): context = { 'course': course, - 'old_dashboard_url': reverse('instructor_dashboard', kwargs={'course_id': course_id}), + 'old_dashboard_url': reverse('instructor_dashboard', kwargs={'course_id': course_key.to_deprecated_string()}), 'studio_url': studio_url, 'sections': sections, 'disable_buttons': disable_buttons, @@ -101,25 +104,20 @@ section_display_name will be used to generate link titles in the nav bar. """ # pylint: disable=W0105 -def _section_course_info(course_id, access): +def _section_course_info(course_key, access): """ Provide data for the corresponding dashboard section """ - course = get_course_by_id(course_id, depth=None) - - course_id_dict = Location.parse_course_id(course_id) + course = get_course_by_id(course_key, depth=None) section_data = { 'section_key': 'course_info', 'section_display_name': _('Course Info'), 'access': access, - 'course_id': course_id, - 'course_org': course_id_dict['org'], - 'course_num': course_id_dict['course'], - 'course_name': course_id_dict['name'], + 'course_id': course_key, 'course_display_name': course.display_name, - 'enrollment_count': CourseEnrollment.num_enrolled_in(course_id), + 'enrollment_count': CourseEnrollment.num_enrolled_in(course_key), 'has_started': course.has_started(), 'has_ended': course.has_ended(), - 'list_instructor_tasks_url': reverse('list_instructor_tasks', kwargs={'course_id': course_id}), + 'list_instructor_tasks_url': reverse('list_instructor_tasks', kwargs={'course_id': course_key.to_deprecated_string()}), } try: @@ -127,44 +125,44 @@ def _section_course_info(course_id, access): section_data['grade_cutoffs'] = reduce(advance, course.grade_cutoffs.items(), "")[:-2] except Exception: section_data['grade_cutoffs'] = "Not Available" - # section_data['offline_grades'] = offline_grades_available(course_id) + # section_data['offline_grades'] = offline_grades_available(course_key) try: - section_data['course_errors'] = [(escape(a), '') for (a, _unused) in modulestore().get_item_errors(course.location)] + section_data['course_errors'] = [(escape(a), '') for (a, _unused) in modulestore().get_course_errors(course.id)] except Exception: section_data['course_errors'] = [('Error fetching errors', '')] return section_data -def _section_membership(course_id, access): +def _section_membership(course_key, access): """ Provide data for the corresponding dashboard section """ section_data = { 'section_key': 'membership', 'section_display_name': _('Membership'), 'access': access, - 'enroll_button_url': reverse('students_update_enrollment', kwargs={'course_id': course_id}), - 'unenroll_button_url': reverse('students_update_enrollment', kwargs={'course_id': course_id}), - 'modify_beta_testers_button_url': reverse('bulk_beta_modify_access', kwargs={'course_id': course_id}), - 'list_course_role_members_url': reverse('list_course_role_members', kwargs={'course_id': course_id}), - 'modify_access_url': reverse('modify_access', kwargs={'course_id': course_id}), - 'list_forum_members_url': reverse('list_forum_members', kwargs={'course_id': course_id}), - 'update_forum_role_membership_url': reverse('update_forum_role_membership', kwargs={'course_id': course_id}), + 'enroll_button_url': reverse('students_update_enrollment', kwargs={'course_id': course_key.to_deprecated_string()}), + 'unenroll_button_url': reverse('students_update_enrollment', kwargs={'course_id': course_key.to_deprecated_string()}), + 'modify_beta_testers_button_url': reverse('bulk_beta_modify_access', kwargs={'course_id': course_key.to_deprecated_string()}), + 'list_course_role_members_url': reverse('list_course_role_members', kwargs={'course_id': course_key.to_deprecated_string()}), + 'modify_access_url': reverse('modify_access', kwargs={'course_id': course_key.to_deprecated_string()}), + 'list_forum_members_url': reverse('list_forum_members', kwargs={'course_id': course_key.to_deprecated_string()}), + 'update_forum_role_membership_url': reverse('update_forum_role_membership', kwargs={'course_id': course_key.to_deprecated_string()}), } return section_data -def _section_student_admin(course_id, access): +def _section_student_admin(course_key, access): """ Provide data for the corresponding dashboard section """ section_data = { 'section_key': 'student_admin', 'section_display_name': _('Student Admin'), 'access': access, - 'get_student_progress_url_url': reverse('get_student_progress_url', kwargs={'course_id': course_id}), - 'enrollment_url': reverse('students_update_enrollment', kwargs={'course_id': course_id}), - 'reset_student_attempts_url': reverse('reset_student_attempts', kwargs={'course_id': course_id}), - 'rescore_problem_url': reverse('rescore_problem', kwargs={'course_id': course_id}), - 'list_instructor_tasks_url': reverse('list_instructor_tasks', kwargs={'course_id': course_id}), + 'get_student_progress_url_url': reverse('get_student_progress_url', kwargs={'course_id': course_key.to_deprecated_string()}), + 'enrollment_url': reverse('students_update_enrollment', kwargs={'course_id': course_key.to_deprecated_string()}), + 'reset_student_attempts_url': reverse('reset_student_attempts', kwargs={'course_id': course_key.to_deprecated_string()}), + 'rescore_problem_url': reverse('rescore_problem', kwargs={'course_id': course_key.to_deprecated_string()}), + 'list_instructor_tasks_url': reverse('list_instructor_tasks', kwargs={'course_id': course_key.to_deprecated_string()}), } return section_data @@ -174,74 +172,82 @@ def _section_extensions(course): section_data = { 'section_key': 'extensions', 'section_display_name': _('Extensions'), - 'units_with_due_dates': [(title_or_url(unit), unit.location.url()) + 'units_with_due_dates': [(title_or_url(unit), unit.location.to_deprecated_string()) for unit in get_units_with_due_date(course)], - 'change_due_date_url': reverse('change_due_date', kwargs={'course_id': course.id}), - 'reset_due_date_url': reverse('reset_due_date', kwargs={'course_id': course.id}), - 'show_unit_extensions_url': reverse('show_unit_extensions', kwargs={'course_id': course.id}), - 'show_student_extensions_url': reverse('show_student_extensions', kwargs={'course_id': course.id}), + 'change_due_date_url': reverse('change_due_date', kwargs={'course_id': course.id.to_deprecated_string()}), + 'reset_due_date_url': reverse('reset_due_date', kwargs={'course_id': course.id.to_deprecated_string()}), + 'show_unit_extensions_url': reverse('show_unit_extensions', kwargs={'course_id': course.id.to_deprecated_string()}), + 'show_student_extensions_url': reverse('show_student_extensions', kwargs={'course_id': course.id.to_deprecated_string()}), } return section_data -def _section_data_download(course_id, access): +def _section_data_download(course_key, access): """ Provide data for the corresponding dashboard section """ section_data = { 'section_key': 'data_download', 'section_display_name': _('Data Download'), 'access': access, - 'get_grading_config_url': reverse('get_grading_config', kwargs={'course_id': course_id}), - 'get_students_features_url': reverse('get_students_features', kwargs={'course_id': course_id}), - 'get_anon_ids_url': reverse('get_anon_ids', kwargs={'course_id': course_id}), - 'list_instructor_tasks_url': reverse('list_instructor_tasks', kwargs={'course_id': course_id}), - 'list_report_downloads_url': reverse('list_report_downloads', kwargs={'course_id': course_id}), - 'calculate_grades_csv_url': reverse('calculate_grades_csv', kwargs={'course_id': course_id}), + 'get_grading_config_url': reverse('get_grading_config', kwargs={'course_id': course_key.to_deprecated_string()}), + 'get_students_features_url': reverse('get_students_features', kwargs={'course_id': course_key.to_deprecated_string()}), + 'get_anon_ids_url': reverse('get_anon_ids', kwargs={'course_id': course_key.to_deprecated_string()}), + 'list_instructor_tasks_url': reverse('list_instructor_tasks', kwargs={'course_id': course_key.to_deprecated_string()}), + 'list_report_downloads_url': reverse('list_report_downloads', kwargs={'course_id': course_key.to_deprecated_string()}), + 'calculate_grades_csv_url': reverse('calculate_grades_csv', kwargs={'course_id': course_key.to_deprecated_string()}), } return section_data -def _section_send_email(course_id, access, course): +def _section_send_email(course_key, access, course): """ Provide data for the corresponding bulk email section """ html_module = HtmlDescriptor( course.system, DictFieldData({'data': ''}), - ScopeIds(None, None, None, 'i4x://dummy_org/dummy_course/html/dummy_name') + ScopeIds(None, None, None, course_key.make_usage_key('html', 'fake')) ) fragment = course.system.render(html_module, 'studio_view') - fragment = wrap_xblock('LmsRuntime', html_module, 'studio_view', fragment, None, extra_data={"course-id": course_id}) + fragment = wrap_xblock( + 'LmsRuntime', html_module, 'studio_view', fragment, None, + extra_data={"course-id": course_key.to_deprecated_string()}, + usage_id_serializer=lambda usage_id: quote_slashes(usage_id.to_deprecated_string()) + ) email_editor = fragment.content section_data = { 'section_key': 'send_email', 'section_display_name': _('Email'), 'access': access, - 'send_email': reverse('send_email', kwargs={'course_id': course_id}), + 'send_email': reverse('send_email', kwargs={'course_id': course_key.to_deprecated_string()}), 'editor': email_editor, - 'list_instructor_tasks_url': reverse('list_instructor_tasks', kwargs={'course_id': course_id}), - 'email_background_tasks_url': reverse('list_background_email_tasks', kwargs={'course_id': course_id}), + 'list_instructor_tasks_url': reverse( + 'list_instructor_tasks', kwargs={'course_id': course_key.to_deprecated_string()} + ), + 'email_background_tasks_url': reverse( + 'list_background_email_tasks', kwargs={'course_id': course_key.to_deprecated_string()} + ), } return section_data -def _section_analytics(course_id, access): +def _section_analytics(course_key, access): """ Provide data for the corresponding dashboard section """ section_data = { 'section_key': 'analytics', 'section_display_name': _('Analytics'), 'access': access, - 'get_distribution_url': reverse('get_distribution', kwargs={'course_id': course_id}), - 'proxy_legacy_analytics_url': reverse('proxy_legacy_analytics', kwargs={'course_id': course_id}), + 'get_distribution_url': reverse('get_distribution', kwargs={'course_id': course_key.to_deprecated_string()}), + 'proxy_legacy_analytics_url': reverse('proxy_legacy_analytics', kwargs={'course_id': course_key.to_deprecated_string()}), } return section_data -def _section_metrics(course_id, access): +def _section_metrics(course_key, access): """Provide data for the corresponding dashboard section """ section_data = { 'section_key': 'metrics', 'section_display_name': ('Metrics'), 'access': access, - 'sub_section_display_name': get_section_display_name(course_id), - 'section_has_problem': get_array_section_has_problem(course_id), + 'sub_section_display_name': get_section_display_name(course_key), + 'section_has_problem': get_array_section_has_problem(course_key), 'get_students_opened_subsection_url': reverse('get_students_opened_subsection'), 'get_students_problem_grades_url': reverse('get_students_problem_grades'), } diff --git a/lms/djangoapps/instructor/views/legacy.py b/lms/djangoapps/instructor/views/legacy.py index adf9eaa6c7..f10222cfb2 100644 --- a/lms/djangoapps/instructor/views/legacy.py +++ b/lms/djangoapps/instructor/views/legacy.py @@ -25,10 +25,14 @@ from django.utils import timezone from xmodule_modifiers import wrap_xblock import xmodule.graders as xmgraders -from xmodule.modulestore import XML_MODULESTORE_TYPE, Location +from xmodule.modulestore import XML_MODULESTORE_TYPE from xmodule.modulestore.django import modulestore +from xmodule.modulestore.locations import SlashSeparatedCourseKey from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.html_module import HtmlDescriptor +from xmodule.modulestore.keys import UsageKey +from opaque_keys import InvalidKeyError +from lms.lib.xblock.runtime import quote_slashes # Submissions is a Django app that is currently installed # from the edx-ora2 repo, although it will likely move in the future. @@ -69,6 +73,7 @@ from xblock.fields import ScopeIds from django.utils.translation import ugettext as _ from microsite_configuration import microsite +from xmodule.modulestore.locations import i4xEncoder log = logging.getLogger(__name__) @@ -91,11 +96,12 @@ def split_by_comma_and_whitespace(a_str): @cache_control(no_cache=True, no_store=True, must_revalidate=True) def instructor_dashboard(request, course_id): """Display the instructor dashboard for a course.""" - course = get_course_with_access(request.user, course_id, 'staff', depth=None) + course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) + course = get_course_with_access(request.user, 'staff', course_key, depth=None) - instructor_access = has_access(request.user, course, 'instructor') # an instructor can manage staff lists + instructor_access = has_access(request.user, 'instructor', course) # an instructor can manage staff lists - forum_admin_access = has_forum_access(request.user, course_id, FORUM_ROLE_ADMINISTRATOR) + forum_admin_access = has_forum_access(request.user, course_key, FORUM_ROLE_ADMINISTRATOR) msg = '' email_msg = '' @@ -115,7 +121,7 @@ def instructor_dashboard(request, course_id): else: idash_mode = request.session.get('idash_mode', 'Grades') - enrollment_number = CourseEnrollment.num_enrolled_in(course_id) + enrollment_number = CourseEnrollment.num_enrolled_in(course_key) # assemble some course statistics for output to instructor def get_course_stats_table(): @@ -131,7 +137,10 @@ def instructor_dashboard(request, course_id): if getattr(field.scope, 'user', False): continue - data.append([field.name, json.dumps(field.read_json(course))]) + data.append([ + field.name, + json.dumps(field.read_json(course), cls=i4xEncoder) + ]) datatable['data'] = data return datatable @@ -158,29 +167,6 @@ def instructor_dashboard(request, course_id): writer.writerow(encoded_row) return response - def get_module_url(urlname): - """ - Construct full URL for a module from its urlname. - - Form is either urlname or modulename/urlname. If no modulename - is provided, "problem" is assumed. - """ - # remove whitespace - urlname = strip_if_string(urlname) - - # tolerate an XML suffix in the urlname - if urlname[-4:] == ".xml": - urlname = urlname[:-4] - - # implement default - if '/' not in urlname: - urlname = "problem/" + urlname - - # complete the url using information about the current course: - parts = Location.parse_course_id(course_id) - parts['url'] = urlname - return u"i4x://{org}/{course}/{url}".format(**parts) - def get_student_from_identifier(unique_student_identifier): """Gets a student object using either an email address or username""" unique_student_identifier = strip_if_string(unique_student_identifier) @@ -216,52 +202,52 @@ def instructor_dashboard(request, course_id): track.views.server_track(request, "git-pull", {"directory": data_dir}, page="idashboard") if 'Reload course' in action: - log.debug('reloading {0} ({1})'.format(course_id, course)) + log.debug('reloading {0} ({1})'.format(course_key, course)) try: data_dir = course.data_dir modulestore().try_load_course(data_dir) msg += "

Course reloaded from {0}

".format(data_dir) track.views.server_track(request, "reload", {"directory": data_dir}, page="idashboard") - course_errors = modulestore().get_item_errors(course.location) + course_errors = modulestore().get_course_errors(course.id) msg += '' - except Exception as err: + except Exception as err: # pylint: disable=broad-except msg += '

Error: {0}

'.format(escape(err)) if action == 'Dump list of enrolled students' or action == 'List enrolled students': log.debug(action) - datatable = get_student_grade_summary_data(request, course, course_id, get_grades=False, use_offline=use_offline) - datatable['title'] = _('List of students enrolled in {course_id}').format(course_id=course_id) + datatable = get_student_grade_summary_data(request, course, course_key, get_grades=False, use_offline=use_offline) + datatable['title'] = _('List of students enrolled in {course_key}').format(course_key=course_key.to_deprecated_string()) track.views.server_track(request, "list-students", {}, page="idashboard") elif 'Dump Grades' in action: log.debug(action) - datatable = get_student_grade_summary_data(request, course, course_id, get_grades=True, use_offline=use_offline) - datatable['title'] = _('Summary Grades of students enrolled in {course_id}').format(course_id=course_id) + datatable = get_student_grade_summary_data(request, course, course_key, get_grades=True, use_offline=use_offline) + datatable['title'] = _('Summary Grades of students enrolled in {course_key}').format(course_key=course_key.to_deprecated_string()) track.views.server_track(request, "dump-grades", {}, page="idashboard") elif 'Dump all RAW grades' in action: log.debug(action) - datatable = get_student_grade_summary_data(request, course, course_id, get_grades=True, + datatable = get_student_grade_summary_data(request, course, course_key, get_grades=True, get_raw_scores=True, use_offline=use_offline) - datatable['title'] = _('Raw Grades of students enrolled in {course_id}').format(course_id=course_id) + datatable['title'] = _('Raw Grades of students enrolled in {course_key}').format(course_key=course_key) track.views.server_track(request, "dump-grades-raw", {}, page="idashboard") elif 'Download CSV of all student grades' in action: track.views.server_track(request, "dump-grades-csv", {}, page="idashboard") - return return_csv('grades_{0}.csv'.format(course_id), - get_student_grade_summary_data(request, course, course_id, use_offline=use_offline)) + return return_csv('grades_{0}.csv'.format(course_key.to_deprecated_string()), + get_student_grade_summary_data(request, course, course_key, use_offline=use_offline)) elif 'Download CSV of all RAW grades' in action: track.views.server_track(request, "dump-grades-csv-raw", {}, page="idashboard") - return return_csv('grades_{0}_raw.csv'.format(course_id), - get_student_grade_summary_data(request, course, course_id, get_raw_scores=True, use_offline=use_offline)) + return return_csv('grades_{0}_raw.csv'.format(course_key.to_deprecated_string()), + get_student_grade_summary_data(request, course, course_key, get_raw_scores=True, use_offline=use_offline)) elif 'Download CSV of answer distributions' in action: track.views.server_track(request, "dump-answer-dist-csv", {}, page="idashboard") - return return_csv('answer_dist_{0}.csv'.format(course_id), get_answers_distribution(request, course_id)) + return return_csv('answer_dist_{0}.csv'.format(course_key.to_deprecated_string()), get_answers_distribution(request, course_key)) elif 'Dump description of graded assignments configuration' in action: # what is "graded assignments configuration"? @@ -269,55 +255,72 @@ def instructor_dashboard(request, course_id): msg += dump_grading_context(course) elif "Rescore ALL students' problem submissions" in action: - problem_urlname = request.POST.get('problem_for_all_students', '') - problem_url = get_module_url(problem_urlname) + problem_location_str = strip_if_string(request.POST.get('problem_for_all_students', '')) try: - instructor_task = submit_rescore_problem_for_all_students(request, course_id, problem_url) + problem_location = UsageKey.from_string(problem_location_str) + instructor_task = submit_rescore_problem_for_all_students(request, problem_location) if instructor_task is None: msg += '{text}'.format( text=_('Failed to create a background task for rescoring "{problem_url}".').format( - problem_url=problem_url + problem_url=problem_location_str ) ) else: - track.views.server_track(request, "rescore-all-submissions", {"problem": problem_url, "course": course_id}, page="idashboard") - except ItemNotFoundError as err: + track.views.server_track( + request, + "rescore-all-submissions", + { + "problem": problem_location_str, + "course": course_key.to_deprecated_string() + }, + page="idashboard" + ) + + except (InvalidKeyError, ItemNotFoundError) as err: msg += '{text}'.format( text=_('Failed to create a background task for rescoring "{problem_url}": problem not found.').format( - problem_url=problem_url + problem_url=problem_location_str ) ) - except Exception as err: + except Exception as err: # pylint: disable=broad-except log.error("Encountered exception from rescore: {0}".format(err)) msg += '{text}'.format( text=_('Failed to create a background task for rescoring "{url}": {message}.').format( - url=problem_url, message=err.message + url=problem_location_str, message=err.message ) ) elif "Reset ALL students' attempts" in action: - problem_urlname = request.POST.get('problem_for_all_students', '') - problem_url = get_module_url(problem_urlname) + problem_location_str = strip_if_string(request.POST.get('problem_for_all_students', '')) try: - instructor_task = submit_reset_problem_attempts_for_all_students(request, course_id, problem_url) + problem_location = UsageKey.from_string(problem_location_str) + instructor_task = submit_reset_problem_attempts_for_all_students(request, problem_location) if instructor_task is None: msg += '{text}'.format( - text=_('Failed to create a background task for resetting "{problem_url}".').format(problem_url=problem_url) + text=_('Failed to create a background task for resetting "{problem_url}".').format(problem_url=problem_location_str) ) else: - track.views.server_track(request, "reset-all-attempts", {"problem": problem_url, "course": course_id}, page="idashboard") - except ItemNotFoundError as err: + track.views.server_track( + request, + "reset-all-attempts", + { + "problem": problem_location_str, + "course": course_key.to_deprecated_string() + }, + page="idashboard" + ) + except (InvalidKeyError, ItemNotFoundError) as err: log.error('Failure to reset: unknown problem "{0}"'.format(err)) msg += '{text}'.format( text=_('Failed to create a background task for resetting "{problem_url}": problem not found.').format( - problem_url=problem_url + problem_url=problem_location_str ) ) - except Exception as err: + except Exception as err: # pylint: disable=broad-except log.error("Encountered exception from reset: {0}".format(err)) msg += '{text}'.format( text=_('Failed to create a background task for resetting "{url}": {message}.').format( - url=problem_url, message=err.message + url=problem_location_str, message=err.message ) ) @@ -328,16 +331,32 @@ def instructor_dashboard(request, course_id): if student is None: msg += message else: - problem_urlname = request.POST.get('problem_for_student', '') - problem_url = get_module_url(problem_urlname) - message, datatable = get_background_task_table(course_id, problem_url, student) - msg += message + problem_location_str = strip_if_string(request.POST.get('problem_for_student', '')) + try: + problem_location = UsageKey.from_string(problem_location_str) + except InvalidKeyError: + msg += '{text}'.format( + text=_('Could not find problem location "{url}".').format( + url=problem_location_str + ) + ) + else: + message, datatable = get_background_task_table(course_key, problem_location, student) + msg += message elif "Show Background Task History" in action: - problem_urlname = request.POST.get('problem_for_all_students', '') - problem_url = get_module_url(problem_urlname) - message, datatable = get_background_task_table(course_id, problem_url) - msg += message + problem_location = strip_if_string(request.POST.get('problem_for_all_students', '')) + try: + problem_location = UsageKey.from_string(problem_location_str) + except InvalidKeyError: + msg += '{text}'.format( + text=_('Could not find problem location "{url}".').format( + url=problem_location_str + ) + ) + else: + message, datatable = get_background_task_table(course_key, problem_location) + msg += message elif ("Reset student's attempts" in action or "Delete student state for module" in action or @@ -346,119 +365,135 @@ def instructor_dashboard(request, course_id): unique_student_identifier = request.POST.get( 'unique_student_identifier', '' ) - problem_urlname = request.POST.get('problem_for_student', '') - module_state_key = get_module_url(problem_urlname) - # try to uniquely id student by email address or username - message, student = get_student_from_identifier(unique_student_identifier) - msg += message - student_module = None - if student is not None: - - # Reset the student's score in the submissions API - # Currently this is used only by open assessment (ORA 2) - # We need to do this *before* retrieving the `StudentModule` model, - # because it's possible for a score to exist even if no student module exists. - if "Delete student state for module" in action: - try: - sub_api.reset_score( - anonymous_id_for_user(student, course_id), - course_id, - module_state_key, - ) - except sub_api.SubmissionError: - # Trust the submissions API to log the error - error_msg = _("An error occurred while deleting the score.") - msg += "{err} ".format(err=error_msg) - - # find the module in question - try: - student_module = StudentModule.objects.get( - student_id=student.id, - course_id=course_id, - module_state_key=module_state_key + problem_location_str = strip_if_string(request.POST.get('problem_for_student', '')) + try: + module_state_key = UsageKey.from_string(problem_location_str) + except InvalidKeyError: + msg += '{text}'.format( + text=_('Could not find problem location "{url}".').format( + url=problem_location_str ) - msg += _("Found module. ") + ) + else: + # try to uniquely id student by email address or username + message, student = get_student_from_identifier(unique_student_identifier) + msg += message + student_module = None + if student is not None: + # Reset the student's score in the submissions API + # Currently this is used only by open assessment (ORA 2) + # We need to do this *before* retrieving the `StudentModule` model, + # because it's possible for a score to exist even if no student module exists. + if "Delete student state for module" in action: + try: + sub_api.reset_score( + anonymous_id_for_user(student, course_key), + course_key, + module_state_key, + ) + except sub_api.SubmissionError: + # Trust the submissions API to log the error + error_msg = _("An error occurred while deleting the score.") + msg += "{err} ".format(err=error_msg) - except StudentModule.DoesNotExist as err: - error_msg = _("Couldn't find module with that urlname: {url}. ").format(url=problem_urlname) - msg += "{err_msg} ({err})".format(err_msg=error_msg, err=err) - log.debug(error_msg) - - if student_module is not None: - if "Delete student state for module" in action: - # delete the state + # find the module in question try: - student_module.delete() + student_module = StudentModule.objects.get( + student_id=student.id, + course_id=course_key, + module_id=module_state_key + ) + msg += _("Found module. ") - msg += "{text}".format( - text=_("Deleted student module state for {state}!").format(state=module_state_key) - ) - event = { - "problem": module_state_key, - "student": unique_student_identifier, - "course": course_id - } - track.views.server_track( - request, - "delete-student-module-state", - event, - page="idashboard" - ) - except Exception as err: - error_msg = _("Failed to delete module state for {id}/{url}. ").format( - id=unique_student_identifier, url=problem_urlname - ) + except StudentModule.DoesNotExist as err: + error_msg = _("Couldn't find module with that urlname: {url}. ").format(url=problem_location_str) msg += "{err_msg} ({err})".format(err_msg=error_msg, err=err) - log.exception(error_msg) - elif "Reset student's attempts" in action: - # modify the problem's state - try: - # load the state json - problem_state = json.loads(student_module.state) - old_number_of_attempts = problem_state["attempts"] - problem_state["attempts"] = 0 - # save - student_module.state = json.dumps(problem_state) - student_module.save() - event = { - "old_attempts": old_number_of_attempts, - "student": unicode(student), - "problem": student_module.module_state_key, - "instructor": unicode(request.user), - "course": course_id - } - track.views.server_track(request, "reset-student-attempts", event, page="idashboard") - msg += "{text}".format( - text=_("Module state successfully reset!") - ) - except Exception as err: - error_msg = _("Couldn't reset module state for {id}/{url}. ").format( - id=unique_student_identifier, url=problem_urlname - ) - msg += "{err_msg} ({err})".format(err_msg=error_msg, err=err) - log.exception(error_msg) - else: - # "Rescore student's problem submission" case - try: - instructor_task = submit_rescore_problem_for_student(request, course_id, module_state_key, student) - if instructor_task is None: + log.debug(error_msg) + + if student_module is not None: + if "Delete student state for module" in action: + # delete the state + try: + student_module.delete() + + msg += "{text}".format( + text=_("Deleted student module state for {state}!").format(state=module_state_key) + ) + event = { + "problem": problem_location_str, + "student": unique_student_identifier, + "course": course_key.to_deprecated_string() + } + track.views.server_track( + request, + "delete-student-module-state", + event, + page="idashboard" + ) + except Exception as err: # pylint: disable=broad-except + error_msg = _("Failed to delete module state for {id}/{url}. ").format( + id=unique_student_identifier, url=problem_location_str + ) + msg += "{err_msg} ({err})".format(err_msg=error_msg, err=err) + log.exception(error_msg) + elif "Reset student's attempts" in action: + # modify the problem's state + try: + # load the state json + problem_state = json.loads(student_module.state) + old_number_of_attempts = problem_state["attempts"] + problem_state["attempts"] = 0 + # save + student_module.state = json.dumps(problem_state) + student_module.save() + event = { + "old_attempts": old_number_of_attempts, + "student": unicode(student), + "problem": student_module.module_state_key, + "instructor": unicode(request.user), + "course": course_key.to_deprecated_string() + } + track.views.server_track(request, "reset-student-attempts", event, page="idashboard") + msg += "{text}".format( + text=_("Module state successfully reset!") + ) + except Exception as err: # pylint: disable=broad-except + error_msg = _("Couldn't reset module state for {id}/{url}. ").format( + id=unique_student_identifier, url=problem_location_str + ) + msg += "{err_msg} ({err})".format(err_msg=error_msg, err=err) + log.exception(error_msg) + else: + # "Rescore student's problem submission" case + try: + instructor_task = submit_rescore_problem_for_student(request, module_state_key, student) + if instructor_task is None: + msg += '{text}'.format( + text=_('Failed to create a background task for rescoring "{key}" for student {id}.').format( + key=module_state_key, id=unique_student_identifier + ) + ) + else: + track.views.server_track( + request, + "rescore-student-submission", + { + "problem": module_state_key, + "student": unique_student_identifier, + "course": course_key.to_deprecated_string() + }, + page="idashboard" + ) + except Exception as err: # pylint: disable=broad-except msg += '{text}'.format( - text=_('Failed to create a background task for rescoring "{key}" for student {id}.').format( - key=module_state_key, id=unique_student_identifier + text=_('Failed to create a background task for rescoring "{key}": {id}.').format( + key=module_state_key, id=err.message ) ) - else: - track.views.server_track(request, "rescore-student-submission", {"problem": module_state_key, "student": unique_student_identifier, "course": course_id}, page="idashboard") - except Exception as err: - msg += '{text}'.format( - text=_('Failed to create a background task for rescoring "{key}": {id}.').format( - key=module_state_key, id=err.message + log.exception("Encountered exception from rescore: student '{0}' problem '{1}'".format( + unique_student_identifier, module_state_key + ) ) - ) - log.exception("Encountered exception from rescore: student '{0}' problem '{1}'".format( - unique_student_identifier, module_state_key - ) - ) elif "Get link to student's progress page" in action: unique_student_identifier = request.POST.get('unique_student_identifier', '') @@ -466,8 +501,20 @@ def instructor_dashboard(request, course_id): message, student = get_student_from_identifier(unique_student_identifier) msg += message if student is not None: - progress_url = reverse('student_progress', kwargs={'course_id': course_id, 'student_id': student.id}) - track.views.server_track(request, "get-student-progress-page", {"student": unicode(student), "instructor": unicode(request.user), "course": course_id}, page="idashboard") + progress_url = reverse('student_progress', kwargs={ + 'course_id': course_key.to_deprecated_string(), + 'student_id': student.id + }) + track.views.server_track( + request, + "get-student-progress-page", + { + "student": unicode(student), + "instructor": unicode(request.user), + "course": course_key.to_deprecated_string() + }, + page="idashboard" + ) msg += "{text}.".format( url=progress_url, text=_("Progress page for username: {username} with email address: {email}").format( @@ -484,7 +531,7 @@ def instructor_dashboard(request, course_id): elif action == 'List assignments available for this course': log.debug(action) - allgrades = get_student_grade_summary_data(request, course, course_id, get_grades=True, use_offline=use_offline) + allgrades = get_student_grade_summary_data(request, course, course_key, get_grades=True, use_offline=use_offline) assignments = [[x] for x in allgrades['assignments']] datatable = {'header': [_('Assignment Name')]} @@ -494,7 +541,7 @@ def instructor_dashboard(request, course_id): msg += 'assignments=
%s
' % assignments elif action == 'List enrolled students matching remote gradebook': - stud_data = get_student_grade_summary_data(request, course, course_id, get_grades=False, use_offline=use_offline) + stud_data = get_student_grade_summary_data(request, course, course_key, get_grades=False, use_offline=use_offline) msg2, rg_stud_data = _do_remote_gradebook(request.user, course, 'get-membership') datatable = {'header': ['Student email', 'Match?']} rg_students = [x['email'] for x in rg_stud_data['retdata']] @@ -513,7 +560,7 @@ def instructor_dashboard(request, course_id): if not aname: msg += "{text}".format(text=_("Please enter an assignment name")) else: - allgrades = get_student_grade_summary_data(request, course, course_id, get_grades=True, use_offline=use_offline) + allgrades = get_student_grade_summary_data(request, course, course_key, get_grades=True, use_offline=use_offline) if aname not in allgrades['assignments']: msg += "{text}".format( text=_("Invalid assignment name '{name}'").format(name=aname) @@ -522,12 +569,12 @@ def instructor_dashboard(request, course_id): aidx = allgrades['assignments'].index(aname) datatable = {'header': [_('External email'), aname]} ddata = [] - for x in allgrades['students']: # do one by one in case there is a student who has only partial grades + for student in allgrades['students']: # do one by one in case there is a student who has only partial grades try: - ddata.append([x.email, x.grades[aidx]]) + ddata.append([student.email, student.grades[aidx]]) except IndexError: log.debug('No grade for assignment {idx} ({name}) for student {email}'.format( - idx=aidx, name=aname, email=x.email) + idx=aidx, name=aname, email=student.email) ) datatable['data'] = ddata @@ -549,33 +596,33 @@ def instructor_dashboard(request, course_id): # Admin elif 'List course staff' in action: - role = CourseStaffRole(course.location) - datatable = _role_members_table(role, _("List of Staff"), course_id) + role = CourseStaffRole(course.id) + datatable = _role_members_table(role, _("List of Staff"), course_key) track.views.server_track(request, "list-staff", {}, page="idashboard") elif 'List course instructors' in action and GlobalStaff().has_user(request.user): - role = CourseInstructorRole(course.location) - datatable = _role_members_table(role, _("List of Instructors"), course_id) + role = CourseInstructorRole(course.id) + datatable = _role_members_table(role, _("List of Instructors"), course_key) track.views.server_track(request, "list-instructors", {}, page="idashboard") elif action == 'Add course staff': uname = request.POST['staffuser'] - role = CourseStaffRole(course.location) + role = CourseStaffRole(course.id) msg += add_user_to_role(request, uname, role, 'staff', 'staff') elif action == 'Add instructor' and request.user.is_staff: uname = request.POST['instructor'] - role = CourseInstructorRole(course.location) + role = CourseInstructorRole(course.id) msg += add_user_to_role(request, uname, role, 'instructor', 'instructor') elif action == 'Remove course staff': uname = request.POST['staffuser'] - role = CourseStaffRole(course.location) + role = CourseStaffRole(course.id) msg += remove_user_from_role(request, uname, role, 'staff', 'staff') elif action == 'Remove instructor' and request.user.is_staff: uname = request.POST['instructor'] - role = CourseInstructorRole(course.location) + role = CourseInstructorRole(course.id) msg += remove_user_from_role(request, uname, role, 'instructor', 'instructor') #---------------------------------------- @@ -583,20 +630,28 @@ def instructor_dashboard(request, course_id): elif 'Download CSV of all student profile data' in action: enrolled_students = User.objects.filter( - courseenrollment__course_id=course_id, + courseenrollment__course_id=course_key, courseenrollment__is_active=1, ).order_by('username').select_related("profile") profkeys = ['name', 'language', 'location', 'year_of_birth', 'gender', 'level_of_education', 'mailing_address', 'goals'] datatable = {'header': ['username', 'email'] + profkeys} - def getdat(u): - p = u.profile - return [u.username, u.email] + [getattr(p, x, '') for x in profkeys] + def getdat(user): + """ + Return a list of profile data for the given user. + """ + profile = user.profile + return [user.username, user.email] + [getattr(profile, xkey, '') for xkey in profkeys] datatable['data'] = [getdat(u) for u in enrolled_students] - datatable['title'] = _('Student profile data for course {course_id}').format(course_id = course_id) - return return_csv('profiledata_{course_id}.csv'.format(course_id = course_id), datatable) + datatable['title'] = _('Student profile data for course {course_id}').format( + course_id=course_key.to_deprecated_string() + ) + return return_csv( + 'profiledata_{course_id}.csv'.format(course_id=course_key.to_deprecated_string()), + datatable + ) elif 'Download CSV of all responses to problem' in action: problem_to_dump = request.POST.get('problem_to_dump', '') @@ -604,15 +659,14 @@ def instructor_dashboard(request, course_id): if problem_to_dump[-4:] == ".xml": problem_to_dump = problem_to_dump[:-4] try: - course_id_dict = Location.parse_course_id(course_id) - module_state_key = u"i4x://{org}/{course}/problem/{0}".format(problem_to_dump, **course_id_dict) + module_state_key = course_key.make_usage_key(block_type='problem', name=problem_to_dump) smdat = StudentModule.objects.filter( - course_id=course_id, + course_id=course_key, module_state_key=module_state_key ) smdat = smdat.order_by('student') msg += _("Found {num} records to dump.").format(num=smdat) - except Exception as err: + except Exception as err: # pylint: disable=broad-except msg += "{text}
{err}
".format( text=_("Couldn't find module with that urlname."), err=escape(err) @@ -622,37 +676,37 @@ def instructor_dashboard(request, course_id): if smdat: datatable = {'header': ['username', 'state']} datatable['data'] = [[x.student.username, x.state] for x in smdat] - datatable['title'] = _('Student state for problem {problem}').format(problem = problem_to_dump) - return return_csv('student_state_from_{problem}.csv'.format(problem = problem_to_dump), datatable) + datatable['title'] = _('Student state for problem {problem}').format(problem=problem_to_dump) + return return_csv('student_state_from_{problem}.csv'.format(problem=problem_to_dump), datatable) elif 'Download CSV of all student anonymized IDs' in action: students = User.objects.filter( - courseenrollment__course_id=course_id, + courseenrollment__course_id=course_key, ).order_by('id') datatable = {'header': ['User ID', 'Anonymized user ID']} datatable['data'] = [[s.id, unique_id_for_user(s)] for s in students] - return return_csv(course_id.replace('/', '-') + '-anon-ids.csv', datatable) + return return_csv(course_key.to_deprecated_string().replace('/', '-') + '-anon-ids.csv', datatable) #---------------------------------------- # Group management elif 'List beta testers' in action: - role = CourseBetaTesterRole(course.location) - datatable = _role_members_table(role, _("List of Beta Testers"), course_id) + role = CourseBetaTesterRole(course.id) + datatable = _role_members_table(role, _("List of Beta Testers"), course_key) track.views.server_track(request, "list-beta-testers", {}, page="idashboard") elif action == 'Add beta testers': users = request.POST['betausers'] log.debug("users: {0!r}".format(users)) - role = CourseBetaTesterRole(course.location) + role = CourseBetaTesterRole(course.id) for username_or_email in split_by_comma_and_whitespace(users): msg += "

{0}

".format( add_user_to_role(request, username_or_email, role, 'beta testers', 'beta-tester')) elif action == 'Remove beta testers': users = request.POST['betausers'] - role = CourseBetaTesterRole(course.location) + role = CourseBetaTesterRole(course.id) for username_or_email in split_by_comma_and_whitespace(users): msg += "

{0}

".format( remove_user_from_role(request, username_or_email, role, 'beta testers', 'beta-tester')) @@ -663,56 +717,85 @@ def instructor_dashboard(request, course_id): elif action == 'List course forum admins': rolename = FORUM_ROLE_ADMINISTRATOR datatable = {} - msg += _list_course_forum_members(course_id, rolename, datatable) - track.views.server_track(request, "list-forum-admins", {"course": course_id}, page="idashboard") + msg += _list_course_forum_members(course_key, rolename, datatable) + track.views.server_track( + request, "list-forum-admins", {"course": course_key.to_deprecated_string()}, page="idashboard" + ) elif action == 'Remove forum admin': uname = request.POST['forumadmin'] msg += _update_forum_role_membership(uname, course, FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_REMOVE) - track.views.server_track(request, "remove-forum-admin", {"username": uname, "course": course_id}, page="idashboard") + track.views.server_track( + request, "remove-forum-admin", {"username": uname, "course": course_key.to_deprecated_string()}, + page="idashboard" + ) elif action == 'Add forum admin': uname = request.POST['forumadmin'] msg += _update_forum_role_membership(uname, course, FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_ADD) - track.views.server_track(request, "add-forum-admin", {"username": uname, "course": course_id}, page="idashboard") + track.views.server_track( + request, "add-forum-admin", {"username": uname, "course": course_key.to_deprecated_string()}, + page="idashboard" + ) elif action == 'List course forum moderators': rolename = FORUM_ROLE_MODERATOR datatable = {} - msg += _list_course_forum_members(course_id, rolename, datatable) - track.views.server_track(request, "list-forum-mods", {"course": course_id}, page="idashboard") + msg += _list_course_forum_members(course_key, rolename, datatable) + track.views.server_track( + request, "list-forum-mods", {"course": course_key.to_deprecated_string()}, page="idashboard" + ) elif action == 'Remove forum moderator': uname = request.POST['forummoderator'] msg += _update_forum_role_membership(uname, course, FORUM_ROLE_MODERATOR, FORUM_ROLE_REMOVE) - track.views.server_track(request, "remove-forum-mod", {"username": uname, "course": course_id}, page="idashboard") + track.views.server_track( + request, "remove-forum-mod", {"username": uname, "course": course_key.to_deprecated_string()}, + page="idashboard" + ) elif action == 'Add forum moderator': uname = request.POST['forummoderator'] msg += _update_forum_role_membership(uname, course, FORUM_ROLE_MODERATOR, FORUM_ROLE_ADD) - track.views.server_track(request, "add-forum-mod", {"username": uname, "course": course_id}, page="idashboard") + track.views.server_track( + request, "add-forum-mod", {"username": uname, "course": course_key.to_deprecated_string()}, + page="idashboard" + ) elif action == 'List course forum community TAs': rolename = FORUM_ROLE_COMMUNITY_TA datatable = {} - msg += _list_course_forum_members(course_id, rolename, datatable) - track.views.server_track(request, "list-forum-community-TAs", {"course": course_id}, page="idashboard") + msg += _list_course_forum_members(course_key, rolename, datatable) + track.views.server_track( + request, "list-forum-community-TAs", {"course": course_key.to_deprecated_string()}, + page="idashboard" + ) elif action == 'Remove forum community TA': uname = request.POST['forummoderator'] msg += _update_forum_role_membership(uname, course, FORUM_ROLE_COMMUNITY_TA, FORUM_ROLE_REMOVE) - track.views.server_track(request, "remove-forum-community-TA", {"username": uname, "course": course_id}, page="idashboard") + track.views.server_track( + request, "remove-forum-community-TA", { + "username": uname, "course": course_key.to_deprecated_string() + }, + page="idashboard" + ) elif action == 'Add forum community TA': uname = request.POST['forummoderator'] msg += _update_forum_role_membership(uname, course, FORUM_ROLE_COMMUNITY_TA, FORUM_ROLE_ADD) - track.views.server_track(request, "add-forum-community-TA", {"username": uname, "course": course_id}, page="idashboard") + track.views.server_track( + request, "add-forum-community-TA", { + "username": uname, "course": course_key.to_deprecated_string() + }, + page="idashboard" + ) #---------------------------------------- # enrollment elif action == 'List students who may enroll but may not have yet signed up': - ceaset = CourseEnrollmentAllowed.objects.filter(course_id=course_id) + ceaset = CourseEnrollmentAllowed.objects.filter(course_id=course_key) datatable = {'header': ['StudentEmail']} datatable['data'] = [[x.email] for x in ceaset] datatable['title'] = action @@ -723,14 +806,14 @@ def instructor_dashboard(request, course_id): students = request.POST.get('multiple_students', '') auto_enroll = bool(request.POST.get('auto_enroll')) email_students = bool(request.POST.get('email_students')) - ret = _do_enroll_students(course, course_id, students, auto_enroll=auto_enroll, email_students=email_students, is_shib_course=is_shib_course) + ret = _do_enroll_students(course, course_key, students, auto_enroll=auto_enroll, email_students=email_students, is_shib_course=is_shib_course) datatable = ret['datatable'] elif action == 'Unenroll multiple students': students = request.POST.get('multiple_students', '') email_students = bool(request.POST.get('email_students')) - ret = _do_unenroll_students(course_id, students, email_students=email_students) + ret = _do_unenroll_students(course_key, students, email_students=email_students) datatable = ret['datatable'] elif action == 'List sections available in remote gradebook': @@ -749,7 +832,7 @@ def instructor_dashboard(request, course_id): if not 'List' in action: students = ','.join([x['email'] for x in datatable['retdata']]) overload = 'Overload' in action - ret = _do_enroll_students(course, course_id, students, overload=overload) + ret = _do_enroll_students(course, course_key, students, overload=overload) datatable = ret['datatable'] #---------------------------------------- @@ -764,12 +847,14 @@ def instructor_dashboard(request, course_id): # Create the CourseEmail object. This is saved immediately, so that # any transaction that has been pending up to this point will also be # committed. - email = CourseEmail.create(course_id, request.user, email_to_option, email_subject, html_message) + email = CourseEmail.create( + course_key.to_deprecated_string(), request.user, email_to_option, email_subject, html_message + ) # Submit the task, so that the correct InstructorTask object gets created (for monitoring purposes) - submit_bulk_course_email(request, course_id, email.id) # pylint: disable=E1101 + submit_bulk_course_email(request, course_key, email.id) # pylint: disable=E1101 - except Exception as err: + except Exception as err: # pylint: disable=broad-except # Catch any errors and deliver a message to the user error_msg = "Failed to send email! ({0})".format(err) msg += "" + error_msg + "" @@ -789,11 +874,11 @@ def instructor_dashboard(request, course_id): email_msg = '

{text}

'.format(text=text) elif "Show Background Email Task History" in action: - message, datatable = get_background_task_table(course_id, task_type='bulk_course_email') + message, datatable = get_background_task_table(course_key, task_type='bulk_course_email') msg += message elif "Show Background Email Task History" in action: - message, datatable = get_background_task_table(course_id, task_type='bulk_course_email') + message, datatable = get_background_task_table(course_key, task_type='bulk_course_email') msg += message #---------------------------------------- @@ -806,7 +891,7 @@ def instructor_dashboard(request, course_id): track.views.server_track(request, "psychometrics-histogram-generation", {"problem": unicode(problem)}, page="idashboard") if idash_mode == 'Psychometrics': - problems = psychoanalyze.problems_with_psychometric_data(course_id) + problems = psychoanalyze.problems_with_psychometric_data(course_key) #---------------------------------------- # analytics @@ -815,12 +900,12 @@ def instructor_dashboard(request, course_id): logs and swallows errors. """ url = settings.ANALYTICS_SERVER_URL + \ - u"get?aname={}&course_id={}&apikey={}".format(analytics_name, - course_id, - settings.ANALYTICS_API_KEY) + u"get?aname={}&course_id={}&apikey={}".format( + analytics_name, course_key.to_deprecated_string(), settings.ANALYTICS_API_KEY + ) try: res = requests.get(url) - except Exception: + except Exception: # pylint: disable=broad-except log.exception("Error trying to access analytics at %s", url) return None @@ -854,8 +939,8 @@ def instructor_dashboard(request, course_id): metrics_results = {} if settings.FEATURES.get('CLASS_DASHBOARD') and idash_mode == 'Metrics': - metrics_results['section_display_name'] = dashboard_data.get_section_display_name(course_id) - metrics_results['section_has_problem'] = dashboard_data.get_array_section_has_problem(course_id) + metrics_results['section_display_name'] = dashboard_data.get_section_display_name(course_key) + metrics_results['section_has_problem'] = dashboard_data.get_array_section_has_problem(course_key) #---------------------------------------- # offline grades? @@ -863,18 +948,18 @@ def instructor_dashboard(request, course_id): if use_offline: msg += "
{text}".format( text=_("Grades from {course_id}").format( - course_id=offline_grades_available(course_id) + course_id=offline_grades_available(course_key) ) ) # generate list of pending background tasks if settings.FEATURES.get('ENABLE_INSTRUCTOR_BACKGROUND_TASKS'): - instructor_tasks = get_running_instructor_tasks(course_id) + instructor_tasks = get_running_instructor_tasks(course_key) else: instructor_tasks = None # determine if this is a studio-backed course so we can provide a link to edit this course in studio - is_studio_course = modulestore().get_modulestore_type(course_id) != XML_MODULESTORE_TYPE + is_studio_course = modulestore().get_modulestore_type(course_key) != XML_MODULESTORE_TYPE studio_url = None if is_studio_course: studio_url = get_cms_course_link(course) @@ -885,10 +970,14 @@ def instructor_dashboard(request, course_id): html_module = HtmlDescriptor( course.system, DictFieldData({'data': html_message}), - ScopeIds(None, None, None, 'i4x://dummy_org/dummy_course/html/dummy_name') + ScopeIds(None, None, None, course_key.make_usage_key('html', 'dummy')) ) fragment = html_module.render('studio_view') - fragment = wrap_xblock('LmsRuntime', html_module, 'studio_view', fragment, None, extra_data={"course-id": course_id}) + fragment = wrap_xblock( + 'LmsRuntime', html_module, 'studio_view', fragment, None, + extra_data={"course-id": course_key.to_deprecated_string()}, + usage_id_serializer=lambda usage_id: quote_slashes(usage_id.to_deprecated_string()) + ) email_editor = fragment.content # Enable instructor email only if the following conditions are met: @@ -896,7 +985,7 @@ def instructor_dashboard(request, course_id): # 2. We have explicitly enabled email for the given course via django-admin # 3. It is NOT an XML course if settings.FEATURES['ENABLE_INSTRUCTOR_EMAIL'] and \ - CourseAuthorization.instructor_email_enabled(course_id) and is_studio_course: + CourseAuthorization.instructor_email_enabled(course_key) and is_studio_course: show_email_tab = True # display course stats only if there is no other table to display: @@ -931,12 +1020,12 @@ def instructor_dashboard(request, course_id): 'email_msg': email_msg, # email 'show_email_tab': show_email_tab, # email - 'problems': problems, # psychometrics - 'plots': plots, # psychometrics - 'course_errors': modulestore().get_item_errors(course.location), + 'problems': problems, # psychometrics + 'plots': plots, # psychometrics + 'course_errors': modulestore().get_course_errors(course.id), 'instructor_tasks': instructor_tasks, - 'offline_grade_log': offline_grades_available(course_id), - 'cohorts_ajax_url': reverse('cohorts', kwargs={'course_id': course_id}), + 'offline_grade_log': offline_grades_available(course_key), + 'cohorts_ajax_url': reverse('cohorts', kwargs={'course_id': course_key.to_deprecated_string()}), 'analytics_results': analytics_results, 'disable_buttons': disable_buttons, @@ -944,7 +1033,9 @@ def instructor_dashboard(request, course_id): } if settings.FEATURES.get('ENABLE_INSTRUCTOR_BETA_DASHBOARD'): - context['beta_dashboard_url'] = reverse('instructor_dashboard_2', kwargs={'course_id': course_id}) + context['beta_dashboard_url'] = reverse( + 'instructor_dashboard_2', kwargs={'course_id': course_key.to_deprecated_string()} + ) return render_to_response('courseware/instructor_dashboard.html', context) @@ -976,20 +1067,20 @@ def _do_remote_gradebook(user, course, action, args=None, files=None): try: resp = requests.post(rgurl, data=data, verify=False, files=files) retdict = json.loads(resp.content) - except Exception as err: - msg = _("Failed to communicate with gradebook server at {url}").format(url = rgurl) + "
" - msg += _("Error: {err}").format(err = err) - msg += "
resp={resp}".format(resp = resp.content) - msg += "
data={data}".format(data = data) + except Exception as err: # pylint: disable=broad-except + msg = _("Failed to communicate with gradebook server at {url}").format(url=rgurl) + "
" + msg += _("Error: {err}").format(err=err) + msg += "
resp={resp}".format(resp=resp.content) + msg += "
data={data}".format(data=data) return msg, {} - msg = '
{msg}
'.format(msg = retdict['msg'].replace('\n', '
')) - retdata = retdict['data'] # a list of dicts + msg = '
{msg}
'.format(msg=retdict['msg'].replace('\n', '
')) + retdata = retdict['data'] # a list of dicts if retdata: datatable = {'header': retdata[0].keys()} datatable['data'] = [x.values() for x in retdata] - datatable['title'] = _('Remote gradebook response for {action}').format(action = action) + datatable['title'] = _('Remote gradebook response for {action}').format(action=action) datatable['retdata'] = retdata else: datatable = {} @@ -997,28 +1088,32 @@ def _do_remote_gradebook(user, course, action, args=None, files=None): return msg, datatable -def _list_course_forum_members(course_id, rolename, datatable): +def _list_course_forum_members(course_key, rolename, datatable): """ Fills in datatable with forum membership information, for a given role, so that it will be displayed on instructor dashboard. - course_ID = the ID string for a course + course_ID = the CourseKey for a course rolename = one of "Administrator", "Moderator", "Community TA" Returns message status string to append to displayed message, if role is unknown. """ # make sure datatable is set up properly for display first, before checking for errors datatable['header'] = [_('Username'), _('Full name'), _('Roles')] - datatable['title'] = _('List of Forum {name}s in course {id}').format(name = rolename, id = course_id) + datatable['title'] = _('List of Forum {name}s in course {id}').format( + name=rolename, id=course_key.to_deprecated_string() + ) datatable['data'] = [] try: - role = Role.objects.get(name=rolename, course_id=course_id) + role = Role.objects.get(name=rolename, course_id=course_key) except Role.DoesNotExist: return '' + _('Error: unknown rolename "{rolename}"').format(rolename=rolename) + '' uset = role.users.all().order_by('username') msg = 'Role = {0}'.format(rolename) log.debug('role={0}'.format(rolename)) - datatable['data'] = [[x.username, x.profile.name, ', '.join([r.name for r in x.roles.filter(course_id=course_id).order_by('name')])] for x in uset] + datatable['data'] = [[x.username, x.profile.name, ', '.join([ + r.name for r in x.roles.filter(course_id=course_key).order_by('name') + ])] for x in uset] return msg @@ -1053,21 +1148,21 @@ def _update_forum_role_membership(uname, course, rolename, add_or_remove): msg = '' + _('Error: user "{username}" does not have rolename "{rolename}", cannot remove').format(username=uname, rolename=rolename) + '' else: user.roles.remove(role) - msg = '' + _('Removed "{username}" from "{course_id}" forum role = "{rolename}"').format(username=user, course_id=course.id, rolename=rolename) + '' + msg = '' + _('Removed "{username}" from "{course_id}" forum role = "{rolename}"').format(username=user, course_id=course.id.to_deprecated_string(), rolename=rolename) + '' else: if alreadyexists: msg = '' + _('Error: user "{username}" already has rolename "{rolename}", cannot add').format(username=uname, rolename=rolename) + '' else: - if (rolename == FORUM_ROLE_ADMINISTRATOR and not has_access(user, course, 'staff')): + if (rolename == FORUM_ROLE_ADMINISTRATOR and not has_access(user, 'staff', course)): msg = '' + _('Error: user "{username}" should first be added as staff before adding as a forum administrator, cannot add').format(username=uname) + '' else: user.roles.add(role) - msg = '' + _('Added "{username}" to "{course_id}" forum role = "{rolename}"').format(username=user, course_id=course.id, rolename=rolename) + '' + msg = '' + _('Added "{username}" to "{course_id}" forum role = "{rolename}"').format(username=user, course_id=course.id.to_deprecated_string(), rolename=rolename) + '' return msg -def _role_members_table(role, title, course_id): +def _role_members_table(role, title, course_key): """ Return a data table of usernames and names of users in group_name. @@ -1084,7 +1179,7 @@ def _role_members_table(role, title, course_id): uset = role.users_with_role() datatable = {'header': [_('Username'), _('Full name')]} datatable['data'] = [[x.username, x.profile.name] for x in uset] - datatable['title'] = _('{title} in course {course_id}').format(title=title, course_id=course_id) + datatable['title'] = _('{title} in course {course_key}').format(title=title, course_key=course_key.to_deprecated_string()) return datatable @@ -1252,12 +1347,12 @@ class GradeTable(object): return self.components.keys() -def get_student_grade_summary_data(request, course, course_id, get_grades=True, get_raw_scores=False, use_offline=False): +def get_student_grade_summary_data(request, course, course_key, get_grades=True, get_raw_scores=False, use_offline=False): ''' Return data arrays with student identity and grades for specified course. course = CourseDescriptor - course_id = course ID + course_key = course ID Note: both are passed in, only because instructor_dashboard already has them already. @@ -1271,7 +1366,7 @@ def get_student_grade_summary_data(request, course, course_id, get_grades=True, ''' enrolled_students = User.objects.filter( - courseenrollment__course_id=course_id, + courseenrollment__course_id=course_key, courseenrollment__is_active=1, ).prefetch_related("groups").order_by('username') @@ -1330,10 +1425,11 @@ def gradebook(request, course_id): - only displayed to course staff - shows students who are enrolled. """ - course = get_course_with_access(request.user, course_id, 'staff', depth=None) + course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) + course = get_course_with_access(request.user, 'staff', course_key, depth=None) enrolled_students = User.objects.filter( - courseenrollment__course_id=course_id, + courseenrollment__course_id=course_key, courseenrollment__is_active=1 ).order_by('username').select_related("profile") @@ -1351,7 +1447,7 @@ def gradebook(request, course_id): return render_to_response('courseware/gradebook.html', { 'students': student_info, 'course': course, - 'course_id': course_id, + 'course_id': course_key, # Checked above 'staff_access': True, 'ordered_grades': sorted(course.grade_cutoffs.items(), key=lambda i: i[1], reverse=True), @@ -1359,9 +1455,9 @@ def gradebook(request, course_id): @cache_control(no_cache=True, no_store=True, must_revalidate=True) -def grade_summary(request, course_id): +def grade_summary(request, course_key): """Display the grade summary for a course.""" - course = get_course_with_access(request.user, course_id, 'staff') + course = get_course_with_access(request.user, 'staff', course_key) # For now, just a page context = {'course': course, @@ -1372,12 +1468,12 @@ def grade_summary(request, course_id): #----------------------------------------------------------------------------- # enrollment -def _do_enroll_students(course, course_id, students, overload=False, auto_enroll=False, email_students=False, is_shib_course=False): +def _do_enroll_students(course, course_key, students, overload=False, auto_enroll=False, email_students=False, is_shib_course=False): """ Do the actual work of enrolling multiple students, presented as a string of emails separated by commas or returns `course` is course object - `course_id` id of course (a `str`) + `course_key` id of course (a CourseKey) `students` string of student emails separated by commas or returns (a `str`) `overload` un-enrolls all existing students (a `boolean`) `auto_enroll` is user input preference (a `boolean`) @@ -1387,15 +1483,15 @@ def _do_enroll_students(course, course_id, students, overload=False, auto_enroll new_students, new_students_lc = get_and_clean_student_list(students) status = dict([x, 'unprocessed'] for x in new_students) - if overload: # delete all but staff - todelete = CourseEnrollment.objects.filter(course_id=course_id) + if overload: # delete all but staff + todelete = CourseEnrollment.objects.filter(course_id=course_key) for ce in todelete: - if not has_access(ce.user, course, 'staff') and ce.user.email.lower() not in new_students_lc: + if not has_access(ce.user, 'staff', course) and ce.user.email.lower() not in new_students_lc: status[ce.user.email] = 'deleted' ce.deactivate() else: status[ce.user.email] = 'is staff' - ceaset = CourseEnrollmentAllowed.objects.filter(course_id=course_id) + ceaset = CourseEnrollmentAllowed.objects.filter(course_id=course_key) for cea in ceaset: status[cea.email] = 'removed from pending enrollment list' ceaset.delete() @@ -1405,20 +1501,22 @@ def _do_enroll_students(course, course_id, students, overload=False, auto_enroll 'SITE_NAME', settings.SITE_NAME ) + # TODO: Use request.build_absolute_uri rather than 'https://{}{}'.format + # and check with the Services team that this works well with microsites registration_url = 'https://{}{}'.format( stripped_site_name, reverse('student.views.register_user') ) course_url = 'https://{}{}'.format( stripped_site_name, - reverse('course_root', kwargs={'course_id': course_id}) + reverse('course_root', kwargs={'course_id': course_key.to_deprecated_string()}) ) # We can't get the url to the course's About page if the marketing site is enabled. course_about_url = None if not settings.FEATURES.get('ENABLE_MKTG_SITE', False): course_about_url = u'https://{}{}'.format( stripped_site_name, - reverse('about_course', kwargs={'course_id': course.id}) + reverse('about_course', kwargs={'course_id': course_key.to_deprecated_string()}) ) # Composition of email @@ -1438,7 +1536,7 @@ def _do_enroll_students(course, course_id, students, overload=False, auto_enroll except User.DoesNotExist: #Student not signed up yet, put in pending enrollment allowed table - cea = CourseEnrollmentAllowed.objects.filter(email=student, course_id=course_id) + cea = CourseEnrollmentAllowed.objects.filter(email=student, course_id=course_key) #If enrollmentallowed already exists, update auto_enroll flag to however it was set in UI #Will be 0 or 1 records as there is a unique key on email + course_id @@ -1450,32 +1548,32 @@ def _do_enroll_students(course, course_id, students, overload=False, auto_enroll continue #EnrollmentAllowed doesn't exist so create it - cea = CourseEnrollmentAllowed(email=student, course_id=course_id, auto_enroll=auto_enroll) + cea = CourseEnrollmentAllowed(email=student, course_id=course_key, auto_enroll=auto_enroll) cea.save() status[student] = 'user does not exist, enrollment allowed, pending with auto enrollment ' \ + ('on' if auto_enroll else 'off') if email_students: - #User is allowed to enroll but has not signed up yet + # User is allowed to enroll but has not signed up yet d['email_address'] = student d['message'] = 'allowed_enroll' send_mail_ret = send_mail_to_student(student, d) status[student] += (', email sent' if send_mail_ret else '') continue - #Student has already registered - if CourseEnrollment.is_enrolled(user, course_id): + # Student has already registered + if CourseEnrollment.is_enrolled(user, course_key): status[student] = 'already enrolled' continue try: - #Not enrolled yet - ce = CourseEnrollment.enroll(user, course_id) + # Not enrolled yet + CourseEnrollment.enroll(user, course_key) status[student] = 'added' if email_students: - #User enrolled for first time, populate dict with user specific info + # User enrolled for first time, populate dict with user specific info d['email_address'] = student d['full_name'] = user.profile.name d['message'] = 'enrolled_enroll' @@ -1499,11 +1597,11 @@ def _do_enroll_students(course, course_id, students, overload=False, auto_enroll #Unenrollment -def _do_unenroll_students(course_id, students, email_students=False): +def _do_unenroll_students(course_key, students, email_students=False): """ Do the actual work of un-enrolling multiple students, presented as a string of emails separated by commas or returns - `course_id` is id of course (a `str`) + `course_key` is id of course (a `str`) `students` is string of student emails separated by commas or returns (a `str`) `email_students` is user input preference (a `boolean`) """ @@ -1516,7 +1614,7 @@ def _do_unenroll_students(course_id, students, email_students=False): settings.SITE_NAME ) if email_students: - course = course_from_id(course_id) + course = course_from_id(course_key) #Composition of email d = {'site_name': stripped_site_name, 'course': course} @@ -1524,7 +1622,7 @@ def _do_unenroll_students(course_id, students, email_students=False): for student in old_students: isok = False - cea = CourseEnrollmentAllowed.objects.filter(course_id=course_id, email=student) + cea = CourseEnrollmentAllowed.objects.filter(course_id=course_key, email=student) #Will be 0 or 1 records as there is a unique key on email + course_id if cea: cea[0].delete() @@ -1545,9 +1643,9 @@ def _do_unenroll_students(course_id, students, email_students=False): continue #Will be 0 or 1 records as there is a unique key on user + course_id - if CourseEnrollment.is_enrolled(user, course_id): + if CourseEnrollment.is_enrolled(user, course_key): try: - CourseEnrollment.unenroll(user, course_id) + CourseEnrollment.unenroll(user, course_key) status[student] = "un-enrolled" if email_students: #User was enrolled @@ -1557,7 +1655,7 @@ def _do_unenroll_students(course_id, students, email_students=False): send_mail_ret = send_mail_to_student(student, d) status[student] += (', email sent' if send_mail_ret else '') - except Exception: + except Exception: # pylint: disable=broad-except if not isok: status[student] = "Error! Failed to un-enroll" @@ -1577,7 +1675,7 @@ def send_mail_to_student(student, param_dict): `param_dict` is a `dict` with keys [ `site_name`: name given to edX instance (a `str`) `registration_url`: url for registration (a `str`) - `course_id`: id of course (a `str`) + `course_key`: id of course (a CourseKey) `auto_enroll`: user input option (a `str`) `course_url`: url of course (a `str`) `email_address`: email of student (a `str`) @@ -1651,7 +1749,7 @@ def get_and_clean_student_list(students): # answer distribution -def get_answers_distribution(request, course_id): +def get_answers_distribution(request, course_key): """ Get the distribution of answers for all graded problems in the course. @@ -1659,7 +1757,7 @@ def get_answers_distribution(request, course_id): 'header': a header row 'data': a list of rows """ - course = get_course_with_access(request.user, course_id, 'staff') + course = get_course_with_access(request.user, 'staff', course_key) dist = grades.answer_distributions(course.id) @@ -1691,13 +1789,13 @@ def compute_course_stats(course): def walk(module): children = module.get_children() - category = module.__class__.__name__ # HtmlDescriptor, CapaDescriptor, ... + category = module.__class__.__name__ # HtmlDescriptor, CapaDescriptor, ... counts[category] += 1 for c in children: walk(c) walk(course) - stats = dict(counts) # number of each kind of module + stats = dict(counts) # number of each kind of module return stats @@ -1721,34 +1819,34 @@ def dump_grading_context(course): msg += "-----------------------------------------------------------------------------\n" msg += "Listing grading context for course %s\n" % course.id - gc = course.grading_context + gcontext = course.grading_context msg += "graded sections:\n" - msg += '%s\n' % gc['graded_sections'].keys() - for (gs, gsvals) in gc['graded_sections'].items(): - msg += "--> Section %s:\n" % (gs) + msg += '%s\n' % gcontext['graded_sections'].keys() + for (gsections, gsvals) in gcontext['graded_sections'].items(): + msg += "--> Section %s:\n" % (gsections) for sec in gsvals: - s = sec['section_descriptor'] - grade_format = getattr(s, 'grade_format', None) + sdesc = sec['section_descriptor'] + grade_format = getattr(sdesc, 'grade_format', None) aname = '' if grade_format in graders: - g = graders[grade_format] - aname = '%s %02d' % (g.short_label, g.index) - g.index += 1 - elif s.display_name in graders: - g = graders[s.display_name] - aname = '%s' % g.short_label + gfmt = graders[grade_format] + aname = '%s %02d' % (gfmt.short_label, gfmt.index) + gfmt.index += 1 + elif sdesc.display_name in graders: + gfmt = graders[sdesc.display_name] + aname = '%s' % gfmt.short_label notes = '' - if getattr(s, 'score_by_attempt', False): + if getattr(sdesc, 'score_by_attempt', False): notes = ', score by attempt!' msg += " %s (grade_format=%s, Assignment=%s%s)\n" % (s.display_name, grade_format, aname, notes) msg += "all descriptors:\n" - msg += "length=%d\n" % len(gc['all_descriptors']) + msg += "length=%d\n" % len(gcontext['all_descriptors']) msg = '
%s
' % msg.replace('<', '<') return msg -def get_background_task_table(course_id, problem_url=None, student=None, task_type=None): +def get_background_task_table(course_key, problem_url=None, student=None, task_type=None): """ Construct the "datatable" structure to represent background task history. @@ -1759,7 +1857,7 @@ def get_background_task_table(course_id, problem_url=None, student=None, task_ty Returns a tuple of (msg, datatable), where the msg is a possible error message, and the datatable is the datatable to be used for display. """ - history_entries = get_instructor_task_history(course_id, problem_url, student, task_type) + history_entries = get_instructor_task_history(course_key, problem_url, student, task_type) datatable = {} msg = "" # first check to see if there is any history at all @@ -1767,12 +1865,16 @@ def get_background_task_table(course_id, problem_url=None, student=None, task_ty # just won't find any entries.) if (history_entries.count()) == 0: if problem_url is None: - msg += 'Failed to find any background tasks for course "{course}".'.format(course=course_id) + msg += 'Failed to find any background tasks for course "{course}".'.format( + course=course_key.to_deprecated_string() + ) elif student is not None: template = '' + _('Failed to find any background tasks for course "{course}", module "{problem}" and student "{student}".') + '' - msg += template.format(course=course_id, problem=problem_url, student=student.username) + msg += template.format(course=course_key.to_deprecated_string(), problem=problem_url, student=student.username) else: - msg += '' + _('Failed to find any background tasks for course "{course}" and module "{problem}".').format(course=course_id, problem=problem_url) + '' + msg += '' + _('Failed to find any background tasks for course "{course}" and module "{problem}".').format( + course=course_key.to_deprecated_string(), problem=problem_url + ) + '' else: datatable['header'] = ["Task Type", "Task Id", @@ -1808,13 +1910,17 @@ def get_background_task_table(course_id, problem_url=None, student=None, task_ty datatable['data'].append(row) if problem_url is None: - datatable['title'] = "{course_id}".format(course_id=course_id) + datatable['title'] = "{course_id}".format(course_id=course_key.to_deprecated_string()) elif student is not None: - datatable['title'] = "{course_id} > {location} > {student}".format(course_id=course_id, - location=problem_url, - student=student.username) + datatable['title'] = "{course_id} > {location} > {student}".format( + course_id=course_key.to_deprecated_string(), + location=problem_url, + student=student.username + ) else: - datatable['title'] = "{course_id} > {location}".format(course_id=course_id, location=problem_url) + datatable['title'] = "{course_id} > {location}".format( + course_id=course_key.to_deprecated_string(), location=problem_url + ) return msg, datatable diff --git a/lms/djangoapps/instructor/views/tools.py b/lms/djangoapps/instructor/views/tools.py index 703610cb64..6b41be20d5 100644 --- a/lms/djangoapps/instructor/views/tools.py +++ b/lms/djangoapps/instructor/views/tools.py @@ -88,7 +88,7 @@ def find_unit(course, url): """ Find node in course tree for url. """ - if node.location.url() == url: + if node.location.to_deprecated_string() == url: return node for child in node.get_children(): found = find(child, url) @@ -132,7 +132,7 @@ def title_or_url(node): """ title = getattr(node, 'display_name', None) if not title: - title = node.location.url() + title = node.location.to_deprecated_string() return title @@ -148,7 +148,7 @@ def set_due_date_extension(course, unit, student, due_date): student_module = StudentModule.objects.get( student_id=student.id, course_id=course.id, - module_state_key=node.location.url() + module_id=node.location ) state = json.loads(student_module.state) @@ -173,7 +173,7 @@ def dump_module_extensions(course, unit): header = [_("Username"), _("Full Name"), _("Extended Due Date")] query = StudentModule.objects.filter( course_id=course.id, - module_state_key=unit.location.url()) + module_id=unit.location) for module in query: state = json.loads(module.state) extended_due = state.get("extended_due") @@ -202,7 +202,7 @@ def dump_student_extensions(course, student): data = [] header = [_("Unit"), _("Extended Due Date")] units = get_units_with_due_date(course) - units = dict([(u.location.url(), u) for u in units]) + units = dict([(u.location, u) for u in units]) query = StudentModule.objects.filter( course_id=course.id, student_id=student.id) diff --git a/lms/djangoapps/instructor_task/api.py b/lms/djangoapps/instructor_task/api.py index 73af3e4378..e91e58fa0f 100644 --- a/lms/djangoapps/instructor_task/api.py +++ b/lms/djangoapps/instructor_task/api.py @@ -38,14 +38,14 @@ def get_running_instructor_tasks(course_id): return instructor_tasks.order_by('-id') -def get_instructor_task_history(course_id, problem_url=None, student=None, task_type=None): +def get_instructor_task_history(course_id, usage_key=None, student=None, task_type=None): """ Returns a query of InstructorTask objects of historical tasks for a given course, that optionally match a particular problem, a student, and/or a task type. """ instructor_tasks = InstructorTask.objects.filter(course_id=course_id) - if problem_url is not None or student is not None: - _, task_key = encode_problem_and_student_input(problem_url, student) + if usage_key is not None or student is not None: + _, task_key = encode_problem_and_student_input(usage_key, student) instructor_tasks = instructor_tasks.filter(task_key=task_key) if task_type is not None: instructor_tasks = instructor_tasks.filter(task_type=task_type) @@ -53,7 +53,8 @@ def get_instructor_task_history(course_id, problem_url=None, student=None, task_ return instructor_tasks.order_by('-id') -def submit_rescore_problem_for_student(request, course_id, problem_url, student): +# Disabling invalid-name because this fn name is longer than 30 chars. +def submit_rescore_problem_for_student(request, usage_key, student): # pylint: disable=invalid-name """ Request a problem to be rescored as a background task. @@ -74,15 +75,15 @@ def submit_rescore_problem_for_student(request, course_id, problem_url, student) """ # check arguments: let exceptions return up to the caller. - check_arguments_for_rescoring(course_id, problem_url) + check_arguments_for_rescoring(usage_key) task_type = 'rescore_problem' task_class = rescore_problem - task_input, task_key = encode_problem_and_student_input(problem_url, student) - return submit_task(request, task_type, task_class, course_id, task_input, task_key) + task_input, task_key = encode_problem_and_student_input(usage_key, student) + return submit_task(request, task_type, task_class, usage_key.course_key, task_input, task_key) -def submit_rescore_problem_for_all_students(request, course_id, problem_url): +def submit_rescore_problem_for_all_students(request, usage_key): # pylint: disable=invalid-name """ Request a problem to be rescored as a background task. @@ -103,23 +104,22 @@ def submit_rescore_problem_for_all_students(request, course_id, problem_url): separate transaction. """ # check arguments: let exceptions return up to the caller. - check_arguments_for_rescoring(course_id, problem_url) + check_arguments_for_rescoring(usage_key) # check to see if task is already running, and reserve it otherwise task_type = 'rescore_problem' task_class = rescore_problem - task_input, task_key = encode_problem_and_student_input(problem_url) - return submit_task(request, task_type, task_class, course_id, task_input, task_key) + task_input, task_key = encode_problem_and_student_input(usage_key) + return submit_task(request, task_type, task_class, usage_key.course_key, task_input, task_key) -def submit_reset_problem_attempts_for_all_students(request, course_id, problem_url): +def submit_reset_problem_attempts_for_all_students(request, usage_key): # pylint: disable=invalid-name """ Request to have attempts reset for a problem as a background task. The problem's attempts will be reset for all students who have accessed the particular problem in a course. Parameters are the `course_id` and - the `problem_url`. The url must specify the location of the problem, - using i4x-type notation. + the `usage_key`, which must be a :class:`Location`. ItemNotFoundException is raised if the problem doesn't exist, or AlreadyRunningError if the problem is already being reset. @@ -131,25 +131,24 @@ def submit_reset_problem_attempts_for_all_students(request, course_id, problem_u save here. Any future database operations will take place in a separate transaction. """ - # check arguments: make sure that the problem_url is defined + # check arguments: make sure that the usage_key is defined # (since that's currently typed in). If the corresponding module descriptor doesn't exist, # an exception will be raised. Let it pass up to the caller. - modulestore().get_instance(course_id, problem_url) + modulestore().get_item(usage_key) task_type = 'reset_problem_attempts' task_class = reset_problem_attempts - task_input, task_key = encode_problem_and_student_input(problem_url) - return submit_task(request, task_type, task_class, course_id, task_input, task_key) + task_input, task_key = encode_problem_and_student_input(usage_key) + return submit_task(request, task_type, task_class, usage_key.course_key, task_input, task_key) -def submit_delete_problem_state_for_all_students(request, course_id, problem_url): +def submit_delete_problem_state_for_all_students(request, usage_key): # pylint: disable=invalid-name """ Request to have state deleted for a problem as a background task. The problem's state will be deleted for all students who have accessed the particular problem in a course. Parameters are the `course_id` and - the `problem_url`. The url must specify the location of the problem, - using i4x-type notation. + the `usage_key`, which must be a :class:`Location`. ItemNotFoundException is raised if the problem doesn't exist, or AlreadyRunningError if the particular problem's state is already being deleted. @@ -161,23 +160,23 @@ def submit_delete_problem_state_for_all_students(request, course_id, problem_url save here. Any future database operations will take place in a separate transaction. """ - # check arguments: make sure that the problem_url is defined + # check arguments: make sure that the usage_key is defined # (since that's currently typed in). If the corresponding module descriptor doesn't exist, # an exception will be raised. Let it pass up to the caller. - modulestore().get_instance(course_id, problem_url) + modulestore().get_item(usage_key) task_type = 'delete_problem_state' task_class = delete_problem_state - task_input, task_key = encode_problem_and_student_input(problem_url) - return submit_task(request, task_type, task_class, course_id, task_input, task_key) + task_input, task_key = encode_problem_and_student_input(usage_key) + return submit_task(request, task_type, task_class, usage_key.course_key, task_input, task_key) -def submit_bulk_course_email(request, course_id, email_id): +def submit_bulk_course_email(request, course_key, email_id): """ Request to have bulk email sent as a background task. The specified CourseEmail object will be sent be updated for all students who have enrolled - in a course. Parameters are the `course_id` and the `email_id`, the id of the CourseEmail object. + in a course. Parameters are the `course_key` and the `email_id`, the id of the CourseEmail object. AlreadyRunningError is raised if the same recipients are already being emailed with the same CourseEmail object. @@ -206,10 +205,10 @@ def submit_bulk_course_email(request, course_id, email_id): task_key_stub = "{email_id}_{to_option}".format(email_id=email_id, to_option=to_option) # create the key value by using MD5 hash: task_key = hashlib.md5(task_key_stub).hexdigest() - return submit_task(request, task_type, task_class, course_id, task_input, task_key) + return submit_task(request, task_type, task_class, course_key, task_input, task_key) -def submit_calculate_grades_csv(request, course_id): +def submit_calculate_grades_csv(request, course_key): """ AlreadyRunningError is raised if the course's grades are already being updated. """ @@ -218,4 +217,4 @@ def submit_calculate_grades_csv(request, course_id): task_input = {} task_key = "" - return submit_task(request, task_type, task_class, course_id, task_input, task_key) + return submit_task(request, task_type, task_class, course_key, task_input, task_key) diff --git a/lms/djangoapps/instructor_task/api_helper.py b/lms/djangoapps/instructor_task/api_helper.py index 606907cdae..6c9603bf38 100644 --- a/lms/djangoapps/instructor_task/api_helper.py +++ b/lms/djangoapps/instructor_task/api_helper.py @@ -1,3 +1,9 @@ +""" +Helper lib for instructor_tasks API. + +Includes methods to check args for rescoring task, encoding student input, +and task submission logic, including handling the Celery backend. +""" import hashlib import json import logging @@ -8,6 +14,7 @@ from celery.states import READY_STATES, SUCCESS, FAILURE, REVOKED from courseware.module_render import get_xqueue_callback_url_prefix from xmodule.modulestore.django import modulestore +from xmodule.modulestore.locations import Location from instructor_task.models import InstructorTask, PROGRESS @@ -21,11 +28,13 @@ class AlreadyRunningError(Exception): def _task_is_running(course_id, task_type, task_key): """Checks if a particular task is already running""" - runningTasks = InstructorTask.objects.filter(course_id=course_id, task_type=task_type, task_key=task_key) + running_tasks = InstructorTask.objects.filter( + course_id=course_id, task_type=task_type, task_key=task_key + ) # exclude states that are "ready" (i.e. not "running", e.g. failure, success, revoked): for state in READY_STATES: - runningTasks = runningTasks.exclude(task_state=state) - return len(runningTasks) > 0 + running_tasks = running_tasks.exclude(task_state=state) + return len(running_tasks) > 0 def _reserve_task(course_id, task_type, task_key, task_input, requester): @@ -229,34 +238,37 @@ def get_status_from_instructor_task(instructor_task): return status -def check_arguments_for_rescoring(course_id, problem_url): +def check_arguments_for_rescoring(usage_key): """ Do simple checks on the descriptor to confirm that it supports rescoring. - Confirms first that the problem_url is defined (since that's currently typed + Confirms first that the usage_key is defined (since that's currently typed in). An ItemNotFoundException is raised if the corresponding module descriptor doesn't exist. NotImplementedError is raised if the corresponding module doesn't support rescoring calls. """ - descriptor = modulestore().get_instance(course_id, problem_url) + descriptor = modulestore().get_item(usage_key) if not hasattr(descriptor, 'module_class') or not hasattr(descriptor.module_class, 'rescore_problem'): msg = "Specified module does not support rescoring." raise NotImplementedError(msg) -def encode_problem_and_student_input(problem_url, student=None): +def encode_problem_and_student_input(usage_key, student=None): # pylint: disable=invalid-name """ - Encode optional problem_url and optional student into task_key and task_input values. + Encode optional usage_key and optional student into task_key and task_input values. - `problem_url` is full URL of the problem. - `student` is the user object of the student + Args: + usage_key (Location): The usage_key identifying the problem. + student (User): the student affected """ + + assert isinstance(usage_key, Location) if student is not None: - task_input = {'problem_url': problem_url, 'student': student.username} - task_key_stub = "{student}_{problem}".format(student=student.id, problem=problem_url) + task_input = {'problem_url': usage_key.to_deprecated_string(), 'student': student.username} + task_key_stub = "{student}_{problem}".format(student=student.id, problem=usage_key.to_deprecated_string()) else: - task_input = {'problem_url': problem_url} - task_key_stub = "_{problem}".format(problem=problem_url) + task_input = {'problem_url': usage_key.to_deprecated_string()} + task_key_stub = "_{problem}".format(problem=usage_key.to_deprecated_string()) # create the key value by using MD5 hash: task_key = hashlib.md5(task_key_stub).hexdigest() @@ -264,11 +276,11 @@ def encode_problem_and_student_input(problem_url, student=None): return task_input, task_key -def submit_task(request, task_type, task_class, course_id, task_input, task_key): +def submit_task(request, task_type, task_class, course_key, task_input, task_key): """ Helper method to submit a task. - Reserves the requested task, based on the `course_id`, `task_type`, and `task_key`, + Reserves the requested task, based on the `course_key`, `task_type`, and `task_key`, checking to see if the task is already running. The `task_input` is also passed so that it can be stored in the resulting InstructorTask entry. Arguments are extracted from the `request` provided by the originating server request. Then the task is submitted to run @@ -285,7 +297,7 @@ def submit_task(request, task_type, task_class, course_id, task_input, task_key) """ # check to see if task is already running, and reserve it otherwise: - instructor_task = _reserve_task(course_id, task_type, task_key, task_input, request.user) + instructor_task = _reserve_task(course_key, task_type, task_key, task_input, request.user) # submit task: task_id = instructor_task.task_id diff --git a/lms/djangoapps/instructor_task/models.py b/lms/djangoapps/instructor_task/models.py index ede7252d86..d279028379 100644 --- a/lms/djangoapps/instructor_task/models.py +++ b/lms/djangoapps/instructor_task/models.py @@ -18,7 +18,6 @@ from uuid import uuid4 import csv import json import hashlib -import os import os.path import urllib @@ -29,6 +28,8 @@ from django.conf import settings from django.contrib.auth.models import User from django.db import models, transaction +from xmodule_django.models import CourseKeyField + # define custom states used by InstructorTask QUEUING = 'QUEUING' @@ -58,7 +59,7 @@ class InstructorTask(models.Model): `updated` stores date that entry was last modified """ task_type = models.CharField(max_length=50, db_index=True) - course_id = models.CharField(max_length=255, db_index=True) + course_id = CourseKeyField(max_length=255, db_index=True) task_key = models.CharField(max_length=255, db_index=True) task_input = models.CharField(max_length=255) task_id = models.CharField(max_length=255, db_index=True) # max_length from celery_taskmeta @@ -251,9 +252,9 @@ class S3ReportStore(ReportStore): ) def key_for(self, course_id, filename): - """Return the S3 key we would use to store and retrive the data for the + """Return the S3 key we would use to store and retrieve the data for the given filename.""" - hashed_course_id = hashlib.sha1(course_id) + hashed_course_id = hashlib.sha1(course_id.to_deprecated_string()) key = Key(self.bucket) key.key = "{}/{}/{}".format( @@ -360,7 +361,7 @@ class LocalFSReportStore(ReportStore): def path_to(self, course_id, filename): """Return the full path to a given file for a given course.""" - return os.path.join(self.root_path, urllib.quote(course_id, safe=''), filename) + return os.path.join(self.root_path, urllib.quote(course_id.to_deprecated_string(), safe=''), filename) def store(self, course_id, filename, buff): """ diff --git a/lms/djangoapps/instructor_task/subtasks.py b/lms/djangoapps/instructor_task/subtasks.py index 93b4fd27a8..88162bd49b 100644 --- a/lms/djangoapps/instructor_task/subtasks.py +++ b/lms/djangoapps/instructor_task/subtasks.py @@ -375,7 +375,7 @@ def check_subtask_is_valid(entry_id, current_task_id, new_subtask_status): format_str = "Unexpected task_id '{}': unable to find subtasks of instructor task '{}': rejecting task {}" msg = format_str.format(current_task_id, entry, new_subtask_status) TASK_LOG.warning(msg) - dog_stats_api.increment('instructor_task.subtask.duplicate.nosubtasks', tags=[entry.course_id]) + dog_stats_api.increment('instructor_task.subtask.duplicate.nosubtasks', tags=[_statsd_tag(entry.course_id)]) raise DuplicateTaskException(msg) # Confirm that the InstructorTask knows about this particular subtask. @@ -385,7 +385,7 @@ def check_subtask_is_valid(entry_id, current_task_id, new_subtask_status): format_str = "Unexpected task_id '{}': unable to find status for subtask of instructor task '{}': rejecting task {}" msg = format_str.format(current_task_id, entry, new_subtask_status) TASK_LOG.warning(msg) - dog_stats_api.increment('instructor_task.subtask.duplicate.unknown', tags=[entry.course_id]) + dog_stats_api.increment('instructor_task.subtask.duplicate.unknown', tags=[_statsd_tag(entry.course_id)]) raise DuplicateTaskException(msg) # Confirm that the InstructorTask doesn't think that this subtask has already been @@ -396,7 +396,7 @@ def check_subtask_is_valid(entry_id, current_task_id, new_subtask_status): format_str = "Unexpected task_id '{}': already completed - status {} for subtask of instructor task '{}': rejecting task {}" msg = format_str.format(current_task_id, subtask_status, entry, new_subtask_status) TASK_LOG.warning(msg) - dog_stats_api.increment('instructor_task.subtask.duplicate.completed', tags=[entry.course_id]) + dog_stats_api.increment('instructor_task.subtask.duplicate.completed', tags=[_statsd_tag(entry.course_id)]) raise DuplicateTaskException(msg) # Confirm that the InstructorTask doesn't think that this subtask is already being @@ -410,7 +410,7 @@ def check_subtask_is_valid(entry_id, current_task_id, new_subtask_status): format_str = "Unexpected task_id '{}': already retried - status {} for subtask of instructor task '{}': rejecting task {}" msg = format_str.format(current_task_id, subtask_status, entry, new_subtask_status) TASK_LOG.warning(msg) - dog_stats_api.increment('instructor_task.subtask.duplicate.retried', tags=[entry.course_id]) + dog_stats_api.increment('instructor_task.subtask.duplicate.retried', tags=[_statsd_tag(entry.course_id)]) raise DuplicateTaskException(msg) # Now we are ready to start working on this. Try to lock it. @@ -420,7 +420,7 @@ def check_subtask_is_valid(entry_id, current_task_id, new_subtask_status): format_str = "Unexpected task_id '{}': already being executed - for subtask of instructor task '{}'" msg = format_str.format(current_task_id, entry) TASK_LOG.warning(msg) - dog_stats_api.increment('instructor_task.subtask.duplicate.locked', tags=[entry.course_id]) + dog_stats_api.increment('instructor_task.subtask.duplicate.locked', tags=[_statsd_tag(entry.course_id)]) raise DuplicateTaskException(msg) @@ -552,3 +552,11 @@ def _update_subtask_status(entry_id, current_task_id, new_subtask_status): else: TASK_LOG.debug("about to commit....") transaction.commit() + + +def _statsd_tag(course_id): + """ + Calculate the tag we will use for DataDog. + """ + tag = unicode(course_id).encode('utf-8') + return tag[:200] diff --git a/lms/djangoapps/instructor_task/tasks_helper.py b/lms/djangoapps/instructor_task/tasks_helper.py index b322907d0e..e5e0be9397 100644 --- a/lms/djangoapps/instructor_task/tasks_helper.py +++ b/lms/djangoapps/instructor_task/tasks_helper.py @@ -244,15 +244,14 @@ def perform_module_state_update(update_fcn, filter_fcn, _entry_id, course_id, ta # get start time for task: start_time = time() - module_state_key = task_input.get('problem_url') + usage_key = course_id.make_usage_key_from_deprecated_string(task_input.get('problem_url')) student_identifier = task_input.get('student') # find the problem descriptor: - module_descriptor = modulestore().get_instance(course_id, module_state_key) + module_descriptor = modulestore().get_item(usage_key) # find the module in question - modules_to_update = StudentModule.objects.filter(course_id=course_id, - module_state_key=module_state_key) + modules_to_update = StudentModule.objects.filter(course_id=course_id, module_id=usage_key) # give the option of updating an individual student. If not specified, # then updates all students who have responded to a problem so far @@ -394,13 +393,13 @@ def rescore_problem_module_state(xmodule_instance_args, module_descriptor, stude # unpack the StudentModule: course_id = student_module.course_id student = student_module.student - module_state_key = student_module.module_state_key + usage_key = student_module.module_state_key instance = _get_module_instance_for_task(course_id, student, module_descriptor, xmodule_instance_args, grade_bucket_type='rescore') if instance is None: # Either permissions just changed, or someone is trying to be clever # and load something they shouldn't have access to. - msg = "No module {loc} for student {student}--access denied?".format(loc=module_state_key, + msg = "No module {loc} for student {student}--access denied?".format(loc=usage_key, student=student) TASK_LOG.debug(msg) raise UpdateProblemModuleStateError(msg) @@ -416,15 +415,15 @@ def rescore_problem_module_state(xmodule_instance_args, module_descriptor, stude if 'success' not in result: # don't consider these fatal, but false means that the individual call didn't complete: TASK_LOG.warning(u"error processing rescore call for course {course}, problem {loc} and student {student}: " - u"unexpected response {msg}".format(msg=result, course=course_id, loc=module_state_key, student=student)) + u"unexpected response {msg}".format(msg=result, course=course_id, loc=usage_key, student=student)) return UPDATE_STATUS_FAILED elif result['success'] not in ['correct', 'incorrect']: TASK_LOG.warning(u"error processing rescore call for course {course}, problem {loc} and student {student}: " - u"{msg}".format(msg=result['success'], course=course_id, loc=module_state_key, student=student)) + u"{msg}".format(msg=result['success'], course=course_id, loc=usage_key, student=student)) return UPDATE_STATUS_FAILED else: TASK_LOG.debug(u"successfully processed rescore call for course {course}, problem {loc} and student {student}: " - u"{msg}".format(msg=result['success'], course=course_id, loc=module_state_key, student=student)) + u"{msg}".format(msg=result['success'], course=course_id, loc=usage_key, student=student)) return UPDATE_STATUS_SUCCEEDED @@ -552,7 +551,7 @@ def push_grades_to_s3(_xmodule_instance_args, _entry_id, course_id, _task_input, # Generate parts of the file name timestamp_str = start_time.strftime("%Y-%m-%d-%H%M") - course_id_prefix = urllib.quote(course_id.replace("/", "_")) + course_id_prefix = urllib.quote(course_id.to_deprecated_string().replace("/", "_")) # Perform the actual upload report_store = ReportStore.from_config() diff --git a/lms/djangoapps/instructor_task/tests/factories.py b/lms/djangoapps/instructor_task/tests/factories.py index 8cb982560d..e1d37908f1 100644 --- a/lms/djangoapps/instructor_task/tests/factories.py +++ b/lms/djangoapps/instructor_task/tests/factories.py @@ -5,13 +5,14 @@ from factory.django import DjangoModelFactory from student.tests.factories import UserFactory as StudentUserFactory from instructor_task.models import InstructorTask from celery.states import PENDING +from xmodule.modulestore.locations import SlashSeparatedCourseKey class InstructorTaskFactory(DjangoModelFactory): FACTORY_FOR = InstructorTask task_type = 'rescore_problem' - course_id = "MITx/999/Robot_Super_Course" + course_id = SlashSeparatedCourseKey("MITx", "999", "Robot_Super_Course") task_input = json.dumps({}) task_key = None task_id = None diff --git a/lms/djangoapps/instructor_task/tests/test_api.py b/lms/djangoapps/instructor_task/tests/test_api.py index aa34e51872..dac66fdac1 100644 --- a/lms/djangoapps/instructor_task/tests/test_api.py +++ b/lms/djangoapps/instructor_task/tests/test_api.py @@ -22,7 +22,7 @@ from instructor_task.models import InstructorTask, PROGRESS from instructor_task.tests.test_base import (InstructorTaskTestCase, InstructorTaskCourseTestCase, InstructorTaskModuleTestCase, - TEST_COURSE_ID) + TEST_COURSE_KEY) class InstructorTaskReportTest(InstructorTaskTestCase): @@ -36,7 +36,7 @@ class InstructorTaskReportTest(InstructorTaskTestCase): self._create_failure_entry() self._create_success_entry() progress_task_ids = [self._create_progress_entry().task_id for _ in range(1, 5)] - task_ids = [instructor_task.task_id for instructor_task in get_running_instructor_tasks(TEST_COURSE_ID)] + task_ids = [instructor_task.task_id for instructor_task in get_running_instructor_tasks(TEST_COURSE_KEY)] self.assertEquals(set(task_ids), set(progress_task_ids)) def test_get_instructor_task_history(self): @@ -47,21 +47,21 @@ class InstructorTaskReportTest(InstructorTaskTestCase): expected_ids.append(self._create_success_entry().task_id) expected_ids.append(self._create_progress_entry().task_id) task_ids = [instructor_task.task_id for instructor_task - in get_instructor_task_history(TEST_COURSE_ID, problem_url=self.problem_url)] + in get_instructor_task_history(TEST_COURSE_KEY, usage_key=self.problem_url)] self.assertEquals(set(task_ids), set(expected_ids)) # make the same call using explicit task_type: task_ids = [instructor_task.task_id for instructor_task in get_instructor_task_history( - TEST_COURSE_ID, - problem_url=self.problem_url, + TEST_COURSE_KEY, + usage_key=self.problem_url, task_type='rescore_problem' )] self.assertEquals(set(task_ids), set(expected_ids)) # make the same call using a non-existent task_type: task_ids = [instructor_task.task_id for instructor_task in get_instructor_task_history( - TEST_COURSE_ID, - problem_url=self.problem_url, + TEST_COURSE_KEY, + usage_key=self.problem_url, task_type='dummy_type' )] self.assertEquals(set(task_ids), set()) @@ -81,25 +81,25 @@ class InstructorTaskModuleSubmitTest(InstructorTaskModuleTestCase): course_id = self.course.id request = None with self.assertRaises(ItemNotFoundError): - submit_rescore_problem_for_student(request, course_id, problem_url, self.student) + submit_rescore_problem_for_student(request, problem_url, self.student) with self.assertRaises(ItemNotFoundError): - submit_rescore_problem_for_all_students(request, course_id, problem_url) + submit_rescore_problem_for_all_students(request, problem_url) with self.assertRaises(ItemNotFoundError): - submit_reset_problem_attempts_for_all_students(request, course_id, problem_url) + submit_reset_problem_attempts_for_all_students(request, problem_url) with self.assertRaises(ItemNotFoundError): - submit_delete_problem_state_for_all_students(request, course_id, problem_url) + submit_delete_problem_state_for_all_students(request, problem_url) def test_submit_nonrescorable_modules(self): # confirm that a rescore of an existent but unscorable module returns an exception # (Note that it is easier to test a scoreable but non-rescorable module in test_tasks, # where we are creating real modules.) - problem_url = self.problem_section.location.url() + problem_url = self.problem_section.location course_id = self.course.id request = None with self.assertRaises(NotImplementedError): - submit_rescore_problem_for_student(request, course_id, problem_url, self.student) + submit_rescore_problem_for_student(request, problem_url, self.student) with self.assertRaises(NotImplementedError): - submit_rescore_problem_for_all_students(request, course_id, problem_url) + submit_rescore_problem_for_all_students(request, problem_url) def _test_submit_with_long_url(self, task_function, student=None): problem_url_name = 'x' * 255 @@ -107,9 +107,9 @@ class InstructorTaskModuleSubmitTest(InstructorTaskModuleTestCase): location = InstructorTaskModuleTestCase.problem_location(problem_url_name) with self.assertRaises(ValueError): if student is not None: - task_function(self.create_task_request(self.instructor), self.course.id, location, student) + task_function(self.create_task_request(self.instructor), location, student) else: - task_function(self.create_task_request(self.instructor), self.course.id, location) + task_function(self.create_task_request(self.instructor), location) def test_submit_rescore_all_with_long_url(self): self._test_submit_with_long_url(submit_rescore_problem_for_all_students) @@ -129,11 +129,9 @@ class InstructorTaskModuleSubmitTest(InstructorTaskModuleTestCase): self.define_option_problem(problem_url_name) location = InstructorTaskModuleTestCase.problem_location(problem_url_name) if student is not None: - instructor_task = task_function(self.create_task_request(self.instructor), - self.course.id, location, student) + instructor_task = task_function(self.create_task_request(self.instructor), location, student) else: - instructor_task = task_function(self.create_task_request(self.instructor), - self.course.id, location) + instructor_task = task_function(self.create_task_request(self.instructor), location) # test resubmitting, by updating the existing record: instructor_task = InstructorTask.objects.get(id=instructor_task.id) @@ -142,9 +140,9 @@ class InstructorTaskModuleSubmitTest(InstructorTaskModuleTestCase): with self.assertRaises(AlreadyRunningError): if student is not None: - task_function(self.create_task_request(self.instructor), self.course.id, location, student) + task_function(self.create_task_request(self.instructor), location, student) else: - task_function(self.create_task_request(self.instructor), self.course.id, location) + task_function(self.create_task_request(self.instructor), location) def test_submit_rescore_all(self): self._test_submit_task(submit_rescore_problem_for_all_students) diff --git a/lms/djangoapps/instructor_task/tests/test_base.py b/lms/djangoapps/instructor_task/tests/test_base.py index ea5eeb1690..59d5319a8c 100644 --- a/lms/djangoapps/instructor_task/tests/test_base.py +++ b/lms/djangoapps/instructor_task/tests/test_base.py @@ -16,6 +16,7 @@ from capa.tests.response_xml_factory import OptionResponseXMLFactory from xmodule.modulestore.django import editable_modulestore from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase +from xmodule.modulestore.locations import Location, SlashSeparatedCourseKey from student.tests.factories import CourseEnrollmentFactory, UserFactory from courseware.model_data import StudentModule @@ -28,10 +29,10 @@ from instructor_task.views import instructor_task_status TEST_COURSE_ORG = 'edx' -TEST_COURSE_NAME = 'test course' +TEST_COURSE_NAME = 'test_course' TEST_COURSE_NUMBER = '1.23x' +TEST_COURSE_KEY = SlashSeparatedCourseKey(TEST_COURSE_ORG, TEST_COURSE_NUMBER, TEST_COURSE_NAME) TEST_SECTION_NAME = "Problem" -TEST_COURSE_ID = 'edx/1.23x/test_course' TEST_FAILURE_MESSAGE = 'task failed horribly' TEST_FAILURE_EXCEPTION = 'RandomCauseError' @@ -54,9 +55,7 @@ class InstructorTaskTestCase(TestCase): """ Create an internal location for a test problem. """ - return "i4x://{org}/{number}/problem/{problem_url_name}".format(org='edx', - number='1.23x', - problem_url_name=problem_url_name) + return TEST_COURSE_KEY.make_usage_key('problem', problem_url_name) def _create_entry(self, task_state=QUEUING, task_output=None, student=None): """Creates a InstructorTask entry for testing.""" @@ -64,7 +63,7 @@ class InstructorTaskTestCase(TestCase): progress_json = json.dumps(task_output) if task_output is not None else None task_input, task_key = encode_problem_and_student_input(self.problem_url, student) - instructor_task = InstructorTaskFactory.create(course_id=TEST_COURSE_ID, + instructor_task = InstructorTaskFactory.create(course_id=TEST_COURSE_KEY, requester=self.instructor, task_input=json.dumps(task_input), task_key=task_key, @@ -180,11 +179,9 @@ class InstructorTaskModuleTestCase(InstructorTaskCourseTestCase): Create an internal location for a test problem. """ if "i4x:" in problem_url_name: - return problem_url_name + return Location.from_deprecated_string(problem_url_name) else: - return "i4x://{org}/{number}/problem/{problem_url_name}".format(org=TEST_COURSE_ORG, - number=TEST_COURSE_NUMBER, - problem_url_name=problem_url_name) + return TEST_COURSE_KEY.make_usage_key('problem', problem_url_name) def define_option_problem(self, problem_url_name): """Create the problem definition so the answer is Option 1""" @@ -195,6 +192,7 @@ class InstructorTaskModuleTestCase(InstructorTaskCourseTestCase): 'num_responses': 2} problem_xml = factory.build_xml(**factory_args) ItemFactory.create(parent_location=self.problem_section.location, + parent=self.problem_section, category="problem", display_name=str(problem_url_name), data=problem_xml) @@ -208,7 +206,7 @@ class InstructorTaskModuleTestCase(InstructorTaskCourseTestCase): 'num_responses': 2} problem_xml = factory.build_xml(**factory_args) location = InstructorTaskTestCase.problem_location(problem_url_name) - item = self.module_store.get_instance(self.course.id, location) + item = self.module_store.get_item(location) item.data = problem_xml self.module_store.update_item(item, '**replace_user**') @@ -217,5 +215,5 @@ class InstructorTaskModuleTestCase(InstructorTaskCourseTestCase): return StudentModule.objects.get(course_id=self.course.id, student=User.objects.get(username=username), module_type=descriptor.location.category, - module_state_key=descriptor.location.url(), + module_id=descriptor.location, ) diff --git a/lms/djangoapps/instructor_task/tests/test_integration.py b/lms/djangoapps/instructor_task/tests/test_integration.py index bd2ab25160..adfec85ee9 100644 --- a/lms/djangoapps/instructor_task/tests/test_integration.py +++ b/lms/djangoapps/instructor_task/tests/test_integration.py @@ -58,11 +58,12 @@ class TestIntegrationTask(InstructorTaskModuleTestCase): # on the right problem: self.login_username(username) # make ajax call: - modx_url = reverse('xblock_handler', - kwargs={'course_id': self.course.id, - 'usage_id': quote_slashes(InstructorTaskModuleTestCase.problem_location(problem_url_name)), - 'handler': 'xmodule_handler', - 'suffix': 'problem_check', }) + modx_url = reverse('xblock_handler', kwargs={ + 'course_id': self.course.id.to_deprecated_string(), + 'usage_id': quote_slashes(InstructorTaskModuleTestCase.problem_location(problem_url_name).to_deprecated_string()), + 'handler': 'xmodule_handler', + 'suffix': 'problem_check', + }) # we assume we have two responses, so assign them the correct identifiers. resp = self.client.post(modx_url, { @@ -79,7 +80,7 @@ class TestIntegrationTask(InstructorTaskModuleTestCase): self.assertEqual(instructor_task.task_type, task_type) task_input = json.loads(instructor_task.task_input) self.assertFalse('student' in task_input) - self.assertEqual(task_input['problem_url'], InstructorTaskModuleTestCase.problem_location(problem_url_name)) + self.assertEqual(task_input['problem_url'], InstructorTaskModuleTestCase.problem_location(problem_url_name).to_deprecated_string()) status = json.loads(instructor_task.task_output) self.assertEqual(status['exception'], 'ZeroDivisionError') self.assertEqual(status['message'], expected_message) @@ -112,11 +113,12 @@ class TestRescoringTask(TestIntegrationTask): # on the right problem: self.login_username(username) # make ajax call: - modx_url = reverse('xblock_handler', - kwargs={'course_id': self.course.id, - 'usage_id': quote_slashes(InstructorTaskModuleTestCase.problem_location(problem_url_name)), - 'handler': 'xmodule_handler', - 'suffix': 'problem_get', }) + modx_url = reverse('xblock_handler', kwargs={ + 'course_id': self.course.id.to_deprecated_string(), + 'usage_id': quote_slashes(InstructorTaskModuleTestCase.problem_location(problem_url_name).to_deprecated_string()), + 'handler': 'xmodule_handler', + 'suffix': 'problem_get', + }) resp = self.client.post(modx_url, {}) return resp @@ -142,12 +144,12 @@ class TestRescoringTask(TestIntegrationTask): def submit_rescore_all_student_answers(self, instructor, problem_url_name): """Submits the particular problem for rescoring""" - return submit_rescore_problem_for_all_students(self.create_task_request(instructor), self.course.id, + return submit_rescore_problem_for_all_students(self.create_task_request(instructor), InstructorTaskModuleTestCase.problem_location(problem_url_name)) def submit_rescore_one_student_answer(self, instructor, problem_url_name, student): """Submits the particular problem for rescoring for a particular student""" - return submit_rescore_problem_for_student(self.create_task_request(instructor), self.course.id, + return submit_rescore_problem_for_student(self.create_task_request(instructor), InstructorTaskModuleTestCase.problem_location(problem_url_name), student) @@ -157,7 +159,7 @@ class TestRescoringTask(TestIntegrationTask): problem_url_name = 'H1P1' self.define_option_problem(problem_url_name) location = InstructorTaskModuleTestCase.problem_location(problem_url_name) - descriptor = self.module_store.get_instance(self.course.id, location) + descriptor = self.module_store.get_item(location) # first store answers for each of the separate users: self.submit_student_answer('u1', problem_url_name, [OPTION_1, OPTION_1]) @@ -227,7 +229,7 @@ class TestRescoringTask(TestIntegrationTask): self.assertEqual(instructor_task.task_type, 'rescore_problem') task_input = json.loads(instructor_task.task_input) self.assertFalse('student' in task_input) - self.assertEqual(task_input['problem_url'], InstructorTaskModuleTestCase.problem_location(problem_url_name)) + self.assertEqual(task_input['problem_url'], InstructorTaskModuleTestCase.problem_location(problem_url_name).to_deprecated_string()) status = json.loads(instructor_task.task_output) self.assertEqual(status['attempted'], 1) self.assertEqual(status['succeeded'], 0) @@ -288,8 +290,8 @@ class TestRescoringTask(TestIntegrationTask): """ % ('!=' if redefine else '==')) problem_xml = factory.build_xml(script=script, cfn="check_func", expect="42", num_responses=1) if redefine: - descriptor = self.module_store.get_instance( - self.course.id, InstructorTaskModuleTestCase.problem_location(problem_url_name) + descriptor = self.module_store.get_item( + InstructorTaskModuleTestCase.problem_location(problem_url_name) ) descriptor.data = problem_xml self.module_store.update_item(descriptor, '**replace_user**') @@ -311,7 +313,7 @@ class TestRescoringTask(TestIntegrationTask): problem_url_name = 'H1P1' self.define_randomized_custom_response_problem(problem_url_name) location = InstructorTaskModuleTestCase.problem_location(problem_url_name) - descriptor = self.module_store.get_instance(self.course.id, location) + descriptor = self.module_store.get_item(location) # run with more than one user userlist = ['u1', 'u2', 'u3', 'u4'] for username in userlist: @@ -375,10 +377,10 @@ class TestResetAttemptsTask(TestIntegrationTask): state = json.loads(module.state) return state['attempts'] - def reset_problem_attempts(self, instructor, problem_url_name): + def reset_problem_attempts(self, instructor, location): """Submits the current problem for resetting""" - return submit_reset_problem_attempts_for_all_students(self.create_task_request(instructor), self.course.id, - InstructorTaskModuleTestCase.problem_location(problem_url_name)) + return submit_reset_problem_attempts_for_all_students(self.create_task_request(instructor), + location) def test_reset_attempts_on_problem(self): """Run reset-attempts scenario on option problem""" @@ -386,7 +388,7 @@ class TestResetAttemptsTask(TestIntegrationTask): problem_url_name = 'H1P1' self.define_option_problem(problem_url_name) location = InstructorTaskModuleTestCase.problem_location(problem_url_name) - descriptor = self.module_store.get_instance(self.course.id, location) + descriptor = self.module_store.get_item(location) num_attempts = 3 # first store answers for each of the separate users: for _ in range(num_attempts): @@ -396,7 +398,7 @@ class TestResetAttemptsTask(TestIntegrationTask): for username in self.userlist: self.assertEquals(self.get_num_attempts(username, descriptor), num_attempts) - self.reset_problem_attempts('instructor', problem_url_name) + self.reset_problem_attempts('instructor', location) for username in self.userlist: self.assertEquals(self.get_num_attempts(username, descriptor), 0) @@ -404,19 +406,20 @@ class TestResetAttemptsTask(TestIntegrationTask): def test_reset_failure(self): """Simulate a failure in resetting attempts on a problem""" problem_url_name = 'H1P1' + location = InstructorTaskModuleTestCase.problem_location(problem_url_name) self.define_option_problem(problem_url_name) self.submit_student_answer('u1', problem_url_name, [OPTION_1, OPTION_1]) expected_message = "bad things happened" with patch('courseware.models.StudentModule.save') as mock_save: mock_save.side_effect = ZeroDivisionError(expected_message) - instructor_task = self.reset_problem_attempts('instructor', problem_url_name) + instructor_task = self.reset_problem_attempts('instructor', location) self._assert_task_failure(instructor_task.id, 'reset_problem_attempts', problem_url_name, expected_message) def test_reset_non_problem(self): """confirm that a non-problem can still be successfully reset""" - problem_url_name = self.problem_section.location.url() - instructor_task = self.reset_problem_attempts('instructor', problem_url_name) + location = self.problem_section.location + instructor_task = self.reset_problem_attempts('instructor', location) instructor_task = InstructorTask.objects.get(id=instructor_task.id) self.assertEqual(instructor_task.task_state, SUCCESS) @@ -436,10 +439,9 @@ class TestDeleteProblemTask(TestIntegrationTask): self.create_student(username) self.logout() - def delete_problem_state(self, instructor, problem_url_name): + def delete_problem_state(self, instructor, location): """Submits the current problem for deletion""" - return submit_delete_problem_state_for_all_students(self.create_task_request(instructor), self.course.id, - InstructorTaskModuleTestCase.problem_location(problem_url_name)) + return submit_delete_problem_state_for_all_students(self.create_task_request(instructor), location) def test_delete_problem_state(self): """Run delete-state scenario on option problem""" @@ -447,7 +449,7 @@ class TestDeleteProblemTask(TestIntegrationTask): problem_url_name = 'H1P1' self.define_option_problem(problem_url_name) location = InstructorTaskModuleTestCase.problem_location(problem_url_name) - descriptor = self.module_store.get_instance(self.course.id, location) + descriptor = self.module_store.get_item(location) # first store answers for each of the separate users: for username in self.userlist: self.submit_student_answer(username, problem_url_name, [OPTION_1, OPTION_1]) @@ -455,7 +457,7 @@ class TestDeleteProblemTask(TestIntegrationTask): for username in self.userlist: self.assertTrue(self.get_student_module(username, descriptor) is not None) # run delete task: - self.delete_problem_state('instructor', problem_url_name) + self.delete_problem_state('instructor', location) # confirm that no state can be found: for username in self.userlist: with self.assertRaises(StudentModule.DoesNotExist): @@ -464,18 +466,19 @@ class TestDeleteProblemTask(TestIntegrationTask): def test_delete_failure(self): """Simulate a failure in deleting state of a problem""" problem_url_name = 'H1P1' + location = InstructorTaskModuleTestCase.problem_location(problem_url_name) self.define_option_problem(problem_url_name) self.submit_student_answer('u1', problem_url_name, [OPTION_1, OPTION_1]) expected_message = "bad things happened" with patch('courseware.models.StudentModule.delete') as mock_delete: mock_delete.side_effect = ZeroDivisionError(expected_message) - instructor_task = self.delete_problem_state('instructor', problem_url_name) + instructor_task = self.delete_problem_state('instructor', location) self._assert_task_failure(instructor_task.id, 'delete_problem_state', problem_url_name, expected_message) def test_delete_non_problem(self): """confirm that a non-problem can still be successfully deleted""" - problem_url_name = self.problem_section.location.url() - instructor_task = self.delete_problem_state('instructor', problem_url_name) + location = self.problem_section.location + instructor_task = self.delete_problem_state('instructor', location) instructor_task = InstructorTask.objects.get(id=instructor_task.id) self.assertEqual(instructor_task.task_state, SUCCESS) diff --git a/lms/djangoapps/instructor_task/tests/test_tasks.py b/lms/djangoapps/instructor_task/tests/test_tasks.py index 5aa5dbcb80..688e1bcc49 100644 --- a/lms/djangoapps/instructor_task/tests/test_tasks.py +++ b/lms/djangoapps/instructor_task/tests/test_tasks.py @@ -13,6 +13,7 @@ from mock import Mock, MagicMock, patch from celery.states import SUCCESS, FAILURE from xmodule.modulestore.exceptions import ItemNotFoundError +from xmodule.modulestore.locations import i4xEncoder from courseware.models import StudentModule from courseware.tests.factories import StudentModuleFactory @@ -37,21 +38,21 @@ class TestInstructorTasks(InstructorTaskModuleTestCase): super(InstructorTaskModuleTestCase, self).setUp() self.initialize_course() self.instructor = self.create_instructor('instructor') - self.problem_url = InstructorTaskModuleTestCase.problem_location(PROBLEM_URL_NAME) + self.location = InstructorTaskModuleTestCase.problem_location(PROBLEM_URL_NAME) def _create_input_entry(self, student_ident=None, use_problem_url=True, course_id=None): """Creates a InstructorTask entry for testing.""" task_id = str(uuid4()) task_input = {} if use_problem_url: - task_input['problem_url'] = self.problem_url + task_input['problem_url'] = self.location if student_ident is not None: task_input['student'] = student_ident course_id = course_id or self.course.id instructor_task = InstructorTaskFactory.create(course_id=course_id, requester=self.instructor, - task_input=json.dumps(task_input), + task_input=json.dumps(task_input, cls=i4xEncoder), task_key='dummy value', task_id=task_id) return instructor_task @@ -127,7 +128,7 @@ class TestInstructorTasks(InstructorTaskModuleTestCase): for student in students: CourseEnrollmentFactory.create(course_id=self.course.id, user=student) StudentModuleFactory.create(course_id=self.course.id, - module_state_key=self.problem_url, + module_id=self.location, student=student, grade=grade, max_grade=max_grade, @@ -139,7 +140,7 @@ class TestInstructorTasks(InstructorTaskModuleTestCase): for student in students: module = StudentModule.objects.get(course_id=self.course.id, student=student, - module_state_key=self.problem_url) + module_id=self.location) state = json.loads(module.state) self.assertEquals(state['attempts'], num_attempts) @@ -356,7 +357,7 @@ class TestResetAttemptsInstructorTask(TestInstructorTasks): for student in students: module = StudentModule.objects.get(course_id=self.course.id, student=student, - module_state_key=self.problem_url) + module_id=self.location) state = json.loads(module.state) self.assertEquals(state['attempts'], initial_attempts) @@ -382,7 +383,7 @@ class TestResetAttemptsInstructorTask(TestInstructorTasks): for index, student in enumerate(students): module = StudentModule.objects.get(course_id=self.course.id, student=student, - module_state_key=self.problem_url) + module_id=self.location) state = json.loads(module.state) if index == 3: self.assertEquals(state['attempts'], 0) @@ -429,11 +430,11 @@ class TestDeleteStateInstructorTask(TestInstructorTasks): for student in students: StudentModule.objects.get(course_id=self.course.id, student=student, - module_state_key=self.problem_url) + module_id=self.location) self._test_run_with_task(delete_problem_state, 'deleted', num_students) # confirm that no state can be found anymore: for student in students: with self.assertRaises(StudentModule.DoesNotExist): StudentModule.objects.get(course_id=self.course.id, student=student, - module_state_key=self.problem_url) + module_id=self.location) diff --git a/lms/djangoapps/licenses/models.py b/lms/djangoapps/licenses/models.py index f91048d269..6ec2c22d98 100644 --- a/lms/djangoapps/licenses/models.py +++ b/lms/djangoapps/licenses/models.py @@ -4,6 +4,8 @@ from django.db import models, transaction from student.models import User +from xmodule_django.models import CourseKeyField + log = logging.getLogger("edx.licenses") @@ -11,7 +13,7 @@ class CourseSoftware(models.Model): name = models.CharField(max_length=255) full_name = models.CharField(max_length=255) url = models.CharField(max_length=255) - course_id = models.CharField(max_length=255) + course_id = CourseKeyField(max_length=255) def __unicode__(self): return u'{0} for {1}'.format(self.name, self.course_id) diff --git a/lms/djangoapps/licenses/views.py b/lms/djangoapps/licenses/views.py index 657d6cd0c7..a47499e636 100644 --- a/lms/djangoapps/licenses/views.py +++ b/lms/djangoapps/licenses/views.py @@ -6,6 +6,7 @@ from collections import namedtuple, defaultdict from edxmako.shortcuts import render_to_string +from xmodule.modulestore.locations import SlashSeparatedCourseKey from django.contrib.auth.decorators import login_required from django.contrib.auth.models import User @@ -59,6 +60,7 @@ def user_software_license(request): if not match: raise Http404 course_id = match.groupdict().get('id', '') + course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) user_id = request.session.get('_auth_user_id') software_name = request.POST.get('software') @@ -66,7 +68,7 @@ def user_software_license(request): try: software = CourseSoftware.objects.get(name=software_name, - course_id=course_id) + course_id=course_key) except CourseSoftware.DoesNotExist: raise Http404 diff --git a/lms/djangoapps/linkedin/management/commands/tests/test_mailusers.py b/lms/djangoapps/linkedin/management/commands/tests/test_mailusers.py deleted file mode 100644 index 2c6c54ce7d..0000000000 --- a/lms/djangoapps/linkedin/management/commands/tests/test_mailusers.py +++ /dev/null @@ -1,237 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Test email scripts. -""" -from smtplib import SMTPDataError, SMTPServerDisconnected -import datetime -import json -import mock - -from boto.ses.exceptions import SESIllegalAddressError, SESIdentityNotVerifiedError -from certificates.models import GeneratedCertificate -from django.contrib.auth.models import User -from django.conf import settings -from django.test.utils import override_settings -from django.core import mail -from django.utils.timezone import utc -from django.test import TestCase - -from xmodule.modulestore.tests.factories import CourseFactory -from student.models import UserProfile -from xmodule.modulestore.tests.django_utils import mixed_store_config -from linkedin.models import LinkedIn -from linkedin.management.commands import linkedin_mailusers as mailusers -from linkedin.management.commands.linkedin_mailusers import MAX_ATTEMPTS - -MODULE = 'linkedin.management.commands.linkedin_mailusers.' - -TEST_DATA_MIXED_MODULESTORE = mixed_store_config(settings.COMMON_TEST_DATA_ROOT, {}) - - -@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) -class MailusersTests(TestCase): - """ - Test mail users command. - """ - - def setUp(self): - CourseFactory.create(org='TESTX', number='1', display_name='TEST1', - start=datetime.datetime(2010, 5, 12, 2, 42, tzinfo=utc), - end=datetime.datetime(2011, 5, 12, 2, 42, tzinfo=utc)) - CourseFactory.create(org='TESTX', number='2', display_name='TEST2', - start=datetime.datetime(2010, 5, 12, 2, 42, tzinfo=utc), - end=datetime.datetime(2011, 5, 12, 2, 42, tzinfo=utc)) - CourseFactory.create(org='TESTX', number='3', display_name='TEST3', - start=datetime.datetime(2010, 5, 12, 2, 42, tzinfo=utc), - end=datetime.datetime(2011, 5, 12, 2, 42, tzinfo=utc)) - - self.fred = fred = User(username='fred', email='fred@bedrock.gov') - fred.save() - UserProfile(user=fred, name='Fred Flintstone').save() - LinkedIn(user=fred, has_linkedin_account=True).save() - self.barney = barney = User( - username='barney', email='barney@bedrock.gov') - barney.save() - - LinkedIn(user=barney, has_linkedin_account=True).save() - UserProfile(user=barney, name='Barney Rubble').save() - - self.adam = adam = User( - username='adam', email='adam@adam.gov') - adam.save() - - LinkedIn(user=adam, has_linkedin_account=True).save() - UserProfile(user=adam, name='Adam (חיים פּלי)').save() - self.cert1 = cert1 = GeneratedCertificate( - status='downloadable', - user=fred, - course_id='TESTX/1/TEST1', - name='TestX/Intro101', - download_url='http://test.foo/test') - cert1.save() - cert2 = GeneratedCertificate( - status='downloadable', - user=fred, - course_id='TESTX/2/TEST2') - cert2.save() - cert3 = GeneratedCertificate( - status='downloadable', - user=barney, - course_id='TESTX/3/TEST3') - cert3.save() - cert5 = GeneratedCertificate( - status='downloadable', - user=adam, - course_id='TESTX/3/TEST3') - cert5.save() - - @mock.patch.dict('django.conf.settings.LINKEDIN_API', - {'EMAIL_WHITELIST': ['barney@bedrock.gov']}) - def test_mail_users_with_whitelist(self): - """ - Test emailing users. - """ - fut = mailusers.Command().handle - fut() - self.assertEqual( - json.loads(self.barney.linkedin.emailed_courses), ['TESTX/3/TEST3']) - self.assertEqual(len(mail.outbox), 1) - self.assertEqual( - mail.outbox[0].to, ['Barney Rubble ']) - - def test_mail_users_grandfather(self): - """ - Test sending grandfather emails. - """ - fut = mailusers.Command().handle - fut() - self.assertEqual( - json.loads(self.fred.linkedin.emailed_courses), ['TESTX/1/TEST1', 'TESTX/2/TEST2']) - self.assertEqual( - json.loads(self.barney.linkedin.emailed_courses), ['TESTX/3/TEST3']) - self.assertEqual( - json.loads(self.adam.linkedin.emailed_courses), ['TESTX/3/TEST3']) - self.assertEqual(len(mail.outbox), 3) - self.assertEqual( - mail.outbox[0].to, ['Fred Flintstone ']) - self.assertEqual( - mail.outbox[0].subject, 'Fred Flintstone, Add your Achievements to your LinkedIn Profile') - self.assertEqual( - mail.outbox[1].to, ['Barney Rubble ']) - self.assertEqual( - mail.outbox[1].subject, 'Barney Rubble, Add your Achievements to your LinkedIn Profile') - self.assertEqual( - mail.outbox[2].subject, u'Adam (חיים פּלי), Add your Achievements to your LinkedIn Profile') - - def test_mail_users_grandfather_mock(self): - """ - test that we aren't sending anything when in mock_run mode - """ - fut = mailusers.Command().handle - fut(mock_run=True) - self.assertEqual( - json.loads(self.fred.linkedin.emailed_courses), []) - self.assertEqual( - json.loads(self.barney.linkedin.emailed_courses), []) - self.assertEqual( - json.loads(self.adam.linkedin.emailed_courses), []) - self.assertEqual(len(mail.outbox), 0) - - def test_transaction_semantics(self): - fut = mailusers.Command().handle - with mock.patch('linkedin.management.commands.linkedin_mailusers.Command.send_grandfather_email', - return_value=True, side_effect=[True, KeyboardInterrupt]): - try: - fut() - except KeyboardInterrupt: - # expect that this will be uncaught - - # check that fred's emailed_courses were updated - self.assertEqual( - json.loads(self.fred.linkedin.emailed_courses), ['TESTX/1/TEST1', 'TESTX/2/TEST2'] - ) - - #check that we did not update barney - self.assertEqual( - json.loads(self.barney.linkedin.emailed_courses), [] - ) - - - def test_certificate_url(self): - self.cert1.created_date = datetime.datetime( - 2010, 8, 15, 0, 0, tzinfo=utc) - self.cert1.save() - fut = mailusers.Command().certificate_url - self.assertEqual( - fut(self.cert1), - 'http://www.linkedin.com/profile/guided?' - 'pfCertificationName=TEST1&' - 'pfAuthorityId=0000000&' - 'pfCertificationUrl=http%3A%2F%2Ftest.foo%2Ftest&pfLicenseNo=TESTX%2F1%2FTEST1&' - 'pfCertStartDate=201005&_mSplash=1&' - 'trk=eml-prof-edX-1-gf&startTask=CERTIFICATION_NAME&force=true') - - def assert_fred_worked(self): - self.assertEqual(json.loads(self.fred.linkedin.emailed_courses), ['TESTX/1/TEST1', 'TESTX/2/TEST2']) - - def assert_fred_failed(self): - self.assertEqual(json.loads(self.fred.linkedin.emailed_courses), []) - - def assert_barney_worked(self): - self.assertEqual(json.loads(self.barney.linkedin.emailed_courses), ['TESTX/3/TEST3']) - - def assert_barney_failed(self): - self.assertEqual(json.loads(self.barney.linkedin.emailed_courses),[]) - - def test_single_email_failure(self): - # Test error that will immediately fail a single user, but not the run - with mock.patch('django.core.mail.EmailMessage.send', side_effect=[SESIllegalAddressError, None]): - mailusers.Command().handle() - # Fred should fail with a send error, but we should still run Barney - self.assert_fred_failed() - self.assert_barney_worked() - - def test_limited_retry_errors_both_succeed(self): - errors = [ - SMTPServerDisconnected, SMTPServerDisconnected, SMTPServerDisconnected, None, - SMTPServerDisconnected, None - ] - with mock.patch('django.core.mail.EmailMessage.send', side_effect=errors): - mailusers.Command().handle() - self.assert_fred_worked() - self.assert_barney_worked() - - def test_limited_retry_errors_first_fails(self): - errors = (MAX_ATTEMPTS + 1) * [SMTPServerDisconnected] + [None] - with mock.patch('django.core.mail.EmailMessage.send', side_effect=errors): - mailusers.Command().handle() - self.assert_fred_failed() - self.assert_barney_worked() - - def test_limited_retry_errors_both_fail(self): - errors = (MAX_ATTEMPTS * 2) * [SMTPServerDisconnected] - with mock.patch('django.core.mail.EmailMessage.send', side_effect=errors): - mailusers.Command().handle() - self.assert_fred_failed() - self.assert_barney_failed() - - @mock.patch('time.sleep') - def test_infinite_retry_errors(self, sleep): - - def _raise_err(): - """Need this because SMTPDataError takes args""" - raise SMTPDataError("", "") - - errors = (MAX_ATTEMPTS * 2) * [_raise_err] + [None, None] - with mock.patch('django.core.mail.EmailMessage.send', side_effect=errors): - mailusers.Command().handle() - self.assert_fred_worked() - self.assert_barney_worked() - - def test_total_failure(self): - # If we get this error, we just stop, so neither user gets email. - errors = [SESIdentityNotVerifiedError] - with mock.patch('django.core.mail.EmailMessage.send', side_effect=errors): - mailusers.Command().handle() - self.assert_fred_failed() - self.assert_barney_failed() diff --git a/lms/djangoapps/lms_migration/migrate.py b/lms/djangoapps/lms_migration/migrate.py index d00030bd43..daa30d8645 100644 --- a/lms/djangoapps/lms_migration/migrate.py +++ b/lms/djangoapps/lms_migration/migrate.py @@ -121,7 +121,7 @@ def manage_modulestores(request, reload_dir=None, commit_id=None): settings.EDX_ROOT_URL, escape(cdir), escape(cdir), - course.location.url() + course.location.to_deprecated_string() ) html += '' diff --git a/lms/djangoapps/notes/api.py b/lms/djangoapps/notes/api.py index 1162a144c0..b8bc4f5402 100644 --- a/lms/djangoapps/notes/api.py +++ b/lms/djangoapps/notes/api.py @@ -1,3 +1,4 @@ +from xmodule.modulestore.locations import SlashSeparatedCourseKey from django.contrib.auth.decorators import login_required from django.http import HttpResponse, Http404 from django.core.exceptions import ValidationError @@ -34,11 +35,11 @@ ApiResponse = collections.namedtuple('ApiResponse', ['http_response', 'data']) # API requests are routed through api_request() using the resource map. -def api_enabled(request, course_id): +def api_enabled(request, course_key): ''' Returns True if the api is enabled for the course, otherwise False. ''' - course = _get_course(request, course_id) + course = _get_course(request, course_key) return notes_enabled_for_course(course) @@ -49,9 +50,11 @@ def api_request(request, course_id, **kwargs): Raises a 404 if the requested resource does not exist or notes are disabled for the course. ''' + assert isinstance(course_id, basestring) + course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) # Verify that the api should be accessible to this course - if not api_enabled(request, course_id): + if not api_enabled(request, course_key): log.debug('Notes are disabled for course: {0}'.format(course_id)) raise Http404 @@ -78,7 +81,7 @@ def api_request(request, course_id, **kwargs): log.debug('API request: {0} {1}'.format(resource_method, resource_name)) - api_response = module[func](request, course_id, **kwargs) + api_response = module[func](request, course_key, **kwargs) http_response = api_format(api_response) return http_response @@ -104,33 +107,33 @@ def api_format(api_response): return http_response -def _get_course(request, course_id): +def _get_course(request, course_key): ''' Helper function to load and return a user's course. ''' - return get_course_with_access(request.user, course_id, 'load') + return get_course_with_access(request.user, 'load', course_key) #----------------------------------------------------------------------# # API actions exposed via the resource map. -def index(request, course_id): +def index(request, course_key): ''' Returns a list of annotation objects. ''' MAX_LIMIT = API_SETTINGS.get('MAX_NOTE_LIMIT') - notes = Note.objects.order_by('id').filter(course_id=course_id, + notes = Note.objects.order_by('id').filter(course_id=course_key, user=request.user)[:MAX_LIMIT] return ApiResponse(http_response=HttpResponse(), data=[note.as_dict() for note in notes]) -def create(request, course_id): +def create(request, course_key): ''' Receives an annotation object to create and returns a 303 with the read location. ''' - note = Note(course_id=course_id, user=request.user) + note = Note(course_id=course_key, user=request.user) try: note.clean(request.body) @@ -145,7 +148,7 @@ def create(request, course_id): return ApiResponse(http_response=response, data=None) -def read(request, course_id, note_id): +def read(request, course_key, note_id): ''' Returns a single annotation object. ''' @@ -160,7 +163,7 @@ def read(request, course_id, note_id): return ApiResponse(http_response=HttpResponse(), data=note.as_dict()) -def update(request, course_id, note_id): +def update(request, course_key, note_id): ''' Updates an annotation object and returns a 303 with the read location. ''' @@ -203,7 +206,7 @@ def delete(request, course_id, note_id): return ApiResponse(http_response=HttpResponse('', status=204), data=None) -def search(request, course_id): +def search(request, course_key): ''' Returns a subset of annotation objects based on a search query. ''' @@ -228,7 +231,7 @@ def search(request, course_id): limit = MAX_LIMIT # set filters - filters = {'course_id': course_id, 'user': request.user} + filters = {'course_id': course_key, 'user': request.user} if uri != '': filters['uri'] = uri @@ -244,7 +247,7 @@ def search(request, course_id): return ApiResponse(http_response=HttpResponse(), data=result) -def root(request, course_id): +def root(request, course_key): ''' Returns version information about the API. ''' diff --git a/lms/djangoapps/notes/models.py b/lms/djangoapps/notes/models.py index aa2ec7a377..57e1591969 100644 --- a/lms/djangoapps/notes/models.py +++ b/lms/djangoapps/notes/models.py @@ -5,10 +5,12 @@ from django.core.exceptions import ValidationError from django.utils.html import strip_tags import json +from xmodule_django.models import CourseKeyField + class Note(models.Model): user = models.ForeignKey(User, db_index=True) - course_id = models.CharField(max_length=255, db_index=True) + course_id = CourseKeyField(max_length=255, db_index=True) uri = models.CharField(max_length=255, db_index=True) text = models.TextField(default="") quote = models.TextField(default="") @@ -56,7 +58,8 @@ class Note(models.Model): """ Returns the absolute url for the note object. """ - kwargs = {'course_id': self.course_id, 'note_id': str(self.pk)} + # pylint: disable=no-member + kwargs = {'course_id': self.course_id.to_deprecated_string(), 'note_id': str(self.pk)} return reverse('notes_api_note', kwargs=kwargs) def as_dict(self): diff --git a/lms/djangoapps/notes/tests.py b/lms/djangoapps/notes/tests.py index 21b5cd7b36..ea7aa4b37f 100644 --- a/lms/djangoapps/notes/tests.py +++ b/lms/djangoapps/notes/tests.py @@ -2,6 +2,7 @@ Unit tests for the notes app. """ +from xmodule.modulestore.locations import SlashSeparatedCourseKey from django.test import TestCase from django.test.client import Client from django.core.urlresolvers import reverse @@ -55,10 +56,10 @@ class ApiTest(TestCase): self.student = User.objects.create_user('student', 'student@test.com', self.password) self.student2 = User.objects.create_user('student2', 'student2@test.com', self.password) self.instructor = User.objects.create_user('instructor', 'instructor@test.com', self.password) - self.course_id = 'HarvardX/CB22x/The_Ancient_Greek_Hero' + self.course_key = SlashSeparatedCourseKey('HarvardX', 'CB22x', 'The_Ancient_Greek_Hero') self.note = { 'user': self.student, - 'course_id': self.course_id, + 'course_id': self.course_key, 'uri': '/', 'text': 'foo', 'quote': 'bar', @@ -87,7 +88,7 @@ class ApiTest(TestCase): self.client.login(username=username, password=password) def url(self, name, args={}): - args.update({'course_id': self.course_id}) + args.update({'course_id': self.course_key.to_deprecated_string()}) return reverse(name, kwargs=args) def create_notes(self, num_notes, create=True): @@ -343,10 +344,10 @@ class NoteTest(TestCase): def setUp(self): self.password = 'abc' self.student = User.objects.create_user('student', 'student@test.com', self.password) - self.course_id = 'HarvardX/CB22x/The_Ancient_Greek_Hero' + self.course_key = SlashSeparatedCourseKey('HarvardX', 'CB22x', 'The_Ancient_Greek_Hero') self.note = { 'user': self.student, - 'course_id': self.course_id, + 'course_id': self.course_key, 'uri': '/', 'text': 'foo', 'quote': 'bar', @@ -361,7 +362,7 @@ class NoteTest(TestCase): reference_note = models.Note(**self.note) body = reference_note.as_dict() - note = models.Note(course_id=self.course_id, user=self.student) + note = models.Note(course_id=self.course_key, user=self.student) try: note.clean(json.dumps(body)) self.assertEqual(note.uri, body['uri']) @@ -376,7 +377,7 @@ class NoteTest(TestCase): self.fail('a valid note should not raise an exception') def test_clean_invalid_note(self): - note = models.Note(course_id=self.course_id, user=self.student) + note = models.Note(course_id=self.course_key, user=self.student) for empty_type in (None, '', 0, []): with self.assertRaises(ValidationError): note.clean(None) @@ -389,7 +390,7 @@ class NoteTest(TestCase): })) def test_as_dict(self): - note = models.Note(course_id=self.course_id, user=self.student) + note = models.Note(course_id=self.course_key, user=self.student) d = note.as_dict() self.assertNotIsInstance(d, basestring) self.assertEqual(d['user_id'], self.student.id) diff --git a/lms/djangoapps/notes/views.py b/lms/djangoapps/notes/views.py index b6670a7e09..9aca136992 100644 --- a/lms/djangoapps/notes/views.py +++ b/lms/djangoapps/notes/views.py @@ -10,7 +10,7 @@ from notes.utils import notes_enabled_for_course def notes(request, course_id): ''' Displays the student's notes. ''' - course = get_course_with_access(request.user, course_id, 'load') + course = get_course_with_access(request.user, 'load', course_id) if not notes_enabled_for_course(course): raise Http404 diff --git a/lms/djangoapps/open_ended_grading/open_ended_notifications.py b/lms/djangoapps/open_ended_grading/open_ended_notifications.py index 9560f70a42..11993e15be 100644 --- a/lms/djangoapps/open_ended_grading/open_ended_notifications.py +++ b/lms/djangoapps/open_ended_grading/open_ended_notifications.py @@ -143,7 +143,7 @@ def combined_notifications(course, user): #Initialize controller query service using our mock system controller_qs = ControllerQueryService(settings.OPEN_ENDED_GRADING_INTERFACE, system) student_id = unique_id_for_user(user) - user_is_staff = has_access(user, course, 'staff') + user_is_staff = has_access(user, 'staff', course) course_id = course.id notification_type = "combined" diff --git a/lms/djangoapps/open_ended_grading/staff_grading_service.py b/lms/djangoapps/open_ended_grading/staff_grading_service.py index 4406c81849..dce62af596 100644 --- a/lms/djangoapps/open_ended_grading/staff_grading_service.py +++ b/lms/djangoapps/open_ended_grading/staff_grading_service.py @@ -10,6 +10,7 @@ from django.http import HttpResponse, Http404 from django.utils.translation import ugettext as _ from xmodule.course_module import CourseDescriptor +from xmodule.modulestore.locations import SlashSeparatedCourseKey from xmodule.open_ended_grading_classes.grading_service_module import GradingService, GradingServiceError from xmodule.modulestore.django import ModuleI18nService @@ -17,7 +18,6 @@ from courseware.access import has_access from lms.lib.xblock.runtime import LmsModuleSystem from edxmako.shortcuts import render_to_string from student.models import unique_id_for_user -from util.json_request import expect_json from open_ended_grading.utils import does_location_exist from dogapi import dog_stats_api @@ -233,8 +233,7 @@ def _check_access(user, course_id): """ Raise 404 if user doesn't have staff access to course_id """ - course_location = CourseDescriptor.id_to_location(course_id) - if not has_access(user, course_location, 'staff'): + if not has_access(user, 'staff', course_id): raise Http404 return @@ -261,7 +260,9 @@ def get_next(request, course_id): 'error': if success is False, will have an error message with more info. """ - _check_access(request.user, course_id) + assert(isinstance(course_id, basestring)) + course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) + _check_access(request.user, course_key) required = set(['location']) if request.method != 'POST': @@ -275,7 +276,7 @@ def get_next(request, course_id): p = request.POST location = p['location'] - return HttpResponse(json.dumps(_get_next(course_id, grader_id, location)), + return HttpResponse(json.dumps(_get_next(course_key, grader_id, location)), mimetype="application/json") def get_problem_list(request, course_id): @@ -301,9 +302,11 @@ def get_problem_list(request, course_id): 'error': if success is False, will have an error message with more info. """ - _check_access(request.user, course_id) + assert(isinstance(course_id, basestring)) + course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) + _check_access(request.user, course_key) try: - response = staff_grading_service().get_problem_list(course_id, unique_id_for_user(request.user)) + response = staff_grading_service().get_problem_list(course_key, unique_id_for_user(request.user)) # If 'problem_list' is in the response, then we got a list of problems from the ORA server. # If it is not, then ORA could not find any problems. @@ -324,7 +327,7 @@ def get_problem_list(request, course_id): problem_list[i] = json.loads(problem_list[i]) except Exception: pass - if does_location_exist(course_id, problem_list[i]['location']): + if does_location_exist(course_key.make_usage_key_from_deprecated_string(problem_list[i]['location'])): valid_problem_list.append(problem_list[i]) response['problem_list'] = valid_problem_list response = json.dumps(response) @@ -372,7 +375,9 @@ def save_grade(request, course_id): Returns the same thing as get_next, except that additional error messages are possible if something goes wrong with saving the grade. """ - _check_access(request.user, course_id) + + course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) + _check_access(request.user, course_key) if request.method != 'POST': raise Http404 @@ -398,7 +403,7 @@ def save_grade(request, course_id): location = p['location'] try: - result = staff_grading_service().save_grade(course_id, + result = staff_grading_service().save_grade(course_key, grader_id, p['submission_id'], p['score'], diff --git a/lms/djangoapps/open_ended_grading/tests.py b/lms/djangoapps/open_ended_grading/tests.py index 75020ef024..79cc972b1b 100644 --- a/lms/djangoapps/open_ended_grading/tests.py +++ b/lms/djangoapps/open_ended_grading/tests.py @@ -18,6 +18,7 @@ from xblock.fields import ScopeIds from xmodule import peer_grading_module from xmodule.error_module import ErrorDescriptor from xmodule.modulestore.django import modulestore +from xmodule.modulestore.locations import SlashSeparatedCourseKey from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.open_ended_grading_classes import peer_grading_service, controller_query_service from xmodule.tests import test_util_open_ended @@ -52,7 +53,7 @@ def make_instructor(course, user_email): """ Makes a given user an instructor in a course. """ - CourseStaffRole(course.location).add_users(User.objects.get(email=user_email)) + CourseStaffRole(course.id).add_users(User.objects.get(email=user_email)) class StudentProblemListMockQuery(object): @@ -114,7 +115,7 @@ class TestStaffGradingService(ModuleStoreTestCase, LoginEnrollmentTestCase): self.activate_user(self.student) self.activate_user(self.instructor) - self.course_id = "edX/toy/2012_Fall" + self.course_id = SlashSeparatedCourseKey("edX", "toy", "2012_Fall") self.toy = modulestore().get_course(self.course_id) make_instructor(self.toy, self.instructor) @@ -131,14 +132,14 @@ class TestStaffGradingService(ModuleStoreTestCase, LoginEnrollmentTestCase): # both get and post should return 404 for view_name in ('staff_grading_get_next', 'staff_grading_save_grade'): - url = reverse(view_name, kwargs={'course_id': self.course_id}) + url = reverse(view_name, kwargs={'course_id': self.course_id.to_deprecated_string()}) check_for_get_code(self, 404, url) check_for_post_code(self, 404, url) def test_get_next(self): self.login(self.instructor, self.password) - url = reverse('staff_grading_get_next', kwargs={'course_id': self.course_id}) + url = reverse('staff_grading_get_next', kwargs={'course_id': self.course_id.to_deprecated_string()}) data = {'location': self.location} response = check_for_post_code(self, 200, url, data) @@ -159,7 +160,7 @@ class TestStaffGradingService(ModuleStoreTestCase, LoginEnrollmentTestCase): def save_grade_base(self, skip=False): self.login(self.instructor, self.password) - url = reverse('staff_grading_save_grade', kwargs={'course_id': self.course_id}) + url = reverse('staff_grading_save_grade', kwargs={'course_id': self.course_id.to_deprecated_string()}) data = {'score': '12', 'feedback': 'great!', @@ -184,7 +185,7 @@ class TestStaffGradingService(ModuleStoreTestCase, LoginEnrollmentTestCase): def test_get_problem_list(self): self.login(self.instructor, self.password) - url = reverse('staff_grading_get_problem_list', kwargs={'course_id': self.course_id}) + url = reverse('staff_grading_get_problem_list', kwargs={'course_id': self.course_id.to_deprecated_string()}) data = {} response = check_for_post_code(self, 200, url, data) @@ -207,7 +208,7 @@ class TestStaffGradingService(ModuleStoreTestCase, LoginEnrollmentTestCase): user=instructor, ) # Get the response and load its content. - response = json.loads(staff_grading_service.get_problem_list(request, self.course_id).content) + response = json.loads(staff_grading_service.get_problem_list(request, self.course_id.to_deprecated_string()).content) # A valid response will have an "error" key. self.assertTrue('error' in response) @@ -220,7 +221,7 @@ class TestStaffGradingService(ModuleStoreTestCase, LoginEnrollmentTestCase): """ self.login(self.instructor, self.password) - url = reverse('staff_grading_save_grade', kwargs={'course_id': self.course_id}) + url = reverse('staff_grading_save_grade', kwargs={'course_id': self.course_id.to_deprecated_string()}) data = { 'score': '12', @@ -267,7 +268,7 @@ class TestPeerGradingService(ModuleStoreTestCase, LoginEnrollmentTestCase): self.activate_user(self.student) self.activate_user(self.instructor) - self.course_id = "edX/toy/2012_Fall" + self.course_id = SlashSeparatedCourseKey("edX", "toy", "2012_Fall") self.toy = modulestore().get_course(self.course_id) location = "i4x://edX/toy/peergrading/init" field_data = DictFieldData({'data': "", 'location': location, 'category':'peergrading'}) @@ -444,8 +445,8 @@ class TestPanel(ModuleStoreTestCase): def setUp(self): # Toy courses should be loaded - self.course_name = 'edX/open_ended/2012_Fall' - self.course = modulestore().get_course(self.course_name) + self.course_key = SlashSeparatedCourseKey('edX', 'open_ended', '2012_Fall') + self.course = modulestore().get_course(self.course_key) self.user = factories.UserFactory() def test_open_ended_panel(self): @@ -471,7 +472,7 @@ class TestPanel(ModuleStoreTestCase): @return: """ request = Mock(user=self.user) - response = views.student_problem_list(request, self.course.id) + response = views.student_problem_list(request, self.course.id.to_deprecated_string()) self.assertRegexpMatches(response.content, "Here is a list of open ended problems for this course.") @@ -482,8 +483,8 @@ class TestPeerGradingFound(ModuleStoreTestCase): """ def setUp(self): - self.course_name = 'edX/open_ended_nopath/2012_Fall' - self.course = modulestore().get_course(self.course_name) + self.course_key = SlashSeparatedCourseKey('edX', 'open_ended_nopath', '2012_Fall') + self.course = modulestore().get_course(self.course_key) def test_peer_grading_nopath(self): """ @@ -503,8 +504,8 @@ class TestStudentProblemList(ModuleStoreTestCase): def setUp(self): # Load an open ended course with several problems. - self.course_name = 'edX/open_ended/2012_Fall' - self.course = modulestore().get_course(self.course_name) + self.course_key = SlashSeparatedCourseKey('edX', 'open_ended', '2012_Fall') + self.course = modulestore().get_course(self.course_key) self.user = factories.UserFactory() # Enroll our user in our course and make them an instructor. make_instructor(self.course, self.user.email) diff --git a/lms/djangoapps/open_ended_grading/utils.py b/lms/djangoapps/open_ended_grading/utils.py index 4833d01fc1..6c7532985a 100644 --- a/lms/djangoapps/open_ended_grading/utils.py +++ b/lms/djangoapps/open_ended_grading/utils.py @@ -1,4 +1,3 @@ -import json import logging from xmodule.modulestore import search @@ -50,20 +49,25 @@ def generate_problem_url(problem_url_parts, base_course_url): problem_url = base_course_url + "/" for i, part in enumerate(problem_url_parts): if part is not None: + # This is the course_key. We need to turn it into its deprecated + # form. + if i == 0: + part = part.to_deprecated_string() + # This is placed between the course id and the rest of the url. if i == 1: problem_url += "courseware/" problem_url += part + "/" return problem_url -def does_location_exist(course_id, location): +def does_location_exist(usage_key): """ Checks to see if a valid module exists at a given location (ie has not been deleted) course_id - string course id location - string location """ try: - search.path_to_location(modulestore(), course_id, location) + search.path_to_location(modulestore(), usage_key) return True except ItemNotFoundError: # If the problem cannot be found at the location received from the grading controller server, @@ -71,10 +75,9 @@ def does_location_exist(course_id, location): return False except NoPathToItem: # If the problem can be found, but there is no path to it, then we assume it is a draft. - # Log a warning if the problem is not a draft (location does not end in "draft"). - if not location.endswith("draft"): - log.warn(("Got an unexpected NoPathToItem error in staff grading with a non-draft location {0}. " - "Ensure that the location is valid.").format(location)) + # Log a warning in any case. + log.warn("Got an unexpected NoPathToItem error in staff grading with location %s. " + "This is ok if it is a draft; ensure that the location is valid.", usage_key) return False @@ -156,7 +159,8 @@ class StudentProblemList(object): for problem in self.problem_list: try: # Try to load the problem. - problem_url_parts = search.path_to_location(modulestore(), self.course_id, problem['location']) + usage_key = self.course_id.make_usage_key_from_deprecated_string(problem['location']) + problem_url_parts = search.path_to_location(modulestore(), usage_key) except (ItemNotFoundError, NoPathToItem): # If the problem cannot be found at the location received from the grading controller server, # it has been deleted by the course author. We should not display it. diff --git a/lms/djangoapps/open_ended_grading/views.py b/lms/djangoapps/open_ended_grading/views.py index c045d6e56f..5bc6932fae 100644 --- a/lms/djangoapps/open_ended_grading/views.py +++ b/lms/djangoapps/open_ended_grading/views.py @@ -16,7 +16,7 @@ import open_ended_notifications from xmodule.modulestore.django import modulestore from xmodule.modulestore import search -from xmodule.modulestore import Location +from xmodule.modulestore import SlashSeparatedCourseKey from xmodule.modulestore.exceptions import NoPathToItem from django.http import HttpResponse, Http404, HttpResponseRedirect @@ -28,7 +28,8 @@ from open_ended_grading.utils import (STAFF_ERROR_MESSAGE, STUDENT_ERROR_MESSAGE log = logging.getLogger(__name__) -def _reverse_with_slash(url_name, course_id): + +def _reverse_with_slash(url_name, course_key): """ Reverses the URL given the name and the course id, and then adds a trailing slash if it does not exist yet. @@ -36,6 +37,7 @@ def _reverse_with_slash(url_name, course_id): @param course_id: The id of the course object (eg course.id). @returns: The reversed url with a trailing slash. """ + course_id = course_key.to_deprecated_string() ajax_url = _reverse_without_slash(url_name, course_id) if not ajax_url.endswith('/'): ajax_url += '/' @@ -66,7 +68,7 @@ def staff_grading(request, course_id): """ Show the instructor grading interface. """ - course = get_course_with_access(request.user, course_id, 'staff') + course = get_course_with_access(request.user, 'staff', course_id) ajax_url = _reverse_with_slash('staff_grading', course_id) @@ -90,18 +92,15 @@ def find_peer_grading_module(course): found_module = False problem_url = "" - # Get the course id and split it. - peer_grading_query = course.location.replace(category='peergrading', name=None) # Get the peer grading modules currently in the course. Explicitly specify the course id to avoid issues with different runs. - items = modulestore().get_items(peer_grading_query, course_id=course.id) - #See if any of the modules are centralized modules (ie display info from multiple problems) + items = modulestore().get_items(course.id, category='peergrading') + # See if any of the modules are centralized modules (ie display info from multiple problems) items = [i for i in items if not getattr(i, "use_for_single_location", True)] # Loop through all potential peer grading modules, and find the first one that has a path to it. for item in items: - item_location = item.location # Generate a url for the first module and redirect the user to it. try: - problem_url_parts = search.path_to_location(modulestore(), course.id, item_location) + problem_url_parts = search.path_to_location(modulestore(), item.location) except NoPathToItem: # In the case of nopathtoitem, the peer grading module that was found is in an invalid state, and # can no longer be accessed. Log an informational message, but this will not impact normal behavior. @@ -121,7 +120,7 @@ def peer_grading(request, course_id): ''' #Get the current course - course = get_course_with_access(request.user, course_id, 'load') + course = get_course_with_access(request.user, 'load', course_id) found_module, problem_url = find_peer_grading_module(course) if not found_module: @@ -144,16 +143,17 @@ def student_problem_list(request, course_id): @param course_id: The id of the course to get the problem list for. @return: Renders an HTML problem list table. """ - + assert isinstance(course_id, basestring) + course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) # Load the course. Don't catch any errors here, as we want them to be loud. - course = get_course_with_access(request.user, course_id, 'load') + course = get_course_with_access(request.user, 'load', course_key) # The anonymous student id is needed for communication with ORA. student_id = unique_id_for_user(request.user) base_course_url = reverse('courses') error_text = "" - student_problem_list = StudentProblemList(course_id, student_id) + student_problem_list = StudentProblemList(course_key, student_id) # Get the problem list from ORA. success = student_problem_list.fetch_from_grading_service() # If we fetched the problem list properly, add in additional problem data. @@ -165,11 +165,11 @@ def student_problem_list(request, course_id): valid_problems = [] error_text = student_problem_list.error_text - ajax_url = _reverse_with_slash('open_ended_problems', course_id) + ajax_url = _reverse_with_slash('open_ended_problems', course_key) context = { 'course': course, - 'course_id': course_id, + 'course_id': course_key.to_deprecated_string(), 'ajax_url': ajax_url, 'success': success, 'problem_list': valid_problems, @@ -185,7 +185,8 @@ def flagged_problem_list(request, course_id): ''' Show a student problem list ''' - course = get_course_with_access(request.user, course_id, 'staff') + course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) + course = get_course_with_access(request.user, 'staff', course_key) student_id = unique_id_for_user(request.user) # call problem list service @@ -197,7 +198,7 @@ def flagged_problem_list(request, course_id): # Make a service that can query edX ORA. controller_qs = create_controller_query_service() try: - problem_list_dict = controller_qs.get_flagged_problem_list(course_id) + problem_list_dict = controller_qs.get_flagged_problem_list(course_key) success = problem_list_dict['success'] if 'error' in problem_list_dict: error_text = problem_list_dict['error'] @@ -219,7 +220,7 @@ def flagged_problem_list(request, course_id): log.error("Could not parse problem list from external grading service response.") success = False - ajax_url = _reverse_with_slash('open_ended_flagged_problems', course_id) + ajax_url = _reverse_with_slash('open_ended_flagged_problems', course_key) context = { 'course': course, 'course_id': course_id, @@ -238,7 +239,8 @@ def combined_notifications(request, course_id): """ Gets combined notifications from the grading controller and displays them """ - course = get_course_with_access(request.user, course_id, 'load') + course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) + course = get_course_with_access(request.user, 'load', course_key) user = request.user notifications = open_ended_notifications.combined_notifications(course, user) response = notifications['response'] @@ -250,7 +252,7 @@ def combined_notifications(request, course_id): if tag in response: url_name = notification_tuples[response_num][1] human_name = notification_tuples[response_num][2] - url = _reverse_without_slash(url_name, course_id) + url = _reverse_without_slash(url_name, course_key) has_img = response[tag] # check to make sure we have descriptions and alert messages @@ -282,7 +284,7 @@ def combined_notifications(request, course_id): else: notification_list.append(notification_item) - ajax_url = _reverse_with_slash('open_ended_notifications', course_id) + ajax_url = _reverse_with_slash('open_ended_notifications', course_key) combined_dict = { 'error_text': "", 'notification_list': notification_list, @@ -300,6 +302,7 @@ def take_action_on_flags(request, course_id): Takes action on student flagged submissions. Currently, only support unflag and ban actions. """ + course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) if request.method != 'POST': raise Http404 @@ -324,7 +327,7 @@ def take_action_on_flags(request, course_id): # Make a service that can query edX ORA. controller_qs = create_controller_query_service() try: - response = controller_qs.take_action_on_flags(course_id, student_id, submission_id, action_type) + response = controller_qs.take_action_on_flags(course_key, student_id, submission_id, action_type) return HttpResponse(json.dumps(response), mimetype="application/json") except GradingServiceError: log.exception( diff --git a/lms/djangoapps/psychometrics/management/commands/init_psychometrics.py b/lms/djangoapps/psychometrics/management/commands/init_psychometrics.py index f9cfbd28f5..c94c4abb82 100644 --- a/lms/djangoapps/psychometrics/management/commands/init_psychometrics.py +++ b/lms/djangoapps/psychometrics/management/commands/init_psychometrics.py @@ -7,7 +7,7 @@ import json from courseware.models import StudentModule from track.models import TrackingLog from psychometrics.models import PsychometricData -from xmodule.modulestore import Location +from xmodule.modulestore.keys import UsageKey from django.conf import settings from django.core.management.base import BaseCommand @@ -32,9 +32,8 @@ class Command(BaseCommand): smset = StudentModule.objects.using(db).exclude(max_grade=None) for sm in smset: - url = sm.module_state_key - location = Location(url) - if not location.category == "problem": + usage_key = sm.module_state_key + if not usage_key.block_type == "problem": continue try: state = json.loads(sm.state) diff --git a/lms/djangoapps/psychometrics/psychoanalyze.py b/lms/djangoapps/psychometrics/psychoanalyze.py index dac10e0b07..a315a8b89b 100644 --- a/lms/djangoapps/psychometrics/psychoanalyze.py +++ b/lms/djangoapps/psychometrics/psychoanalyze.py @@ -127,7 +127,7 @@ def problems_with_psychometric_data(course_id): Return dict of {problems (location urls): count} for which psychometric data is available. Does this for a given course_id. ''' - pmdset = PsychometricData.objects.using(db).filter(studentmodule__course_id=course_id) + pmdset = PsychometricData.objects.using(db).filter(studentmodule__course_id=course_id.to_deprecated_string()) plist = [p['studentmodule__module_state_key'] for p in pmdset.values('studentmodule__module_state_key').distinct()] problems = dict((p, pmdset.filter(studentmodule__module_state_key=p).count()) for p in plist) @@ -307,11 +307,11 @@ def make_psychometrics_data_update_handler(course_id, user, module_state_key): the PsychometricData instance for the given StudentModule instance. """ sm, status = StudentModule.objects.get_or_create( - course_id=course_id, - student=user, - module_state_key=module_state_key, - defaults={'state': '{}', 'module_type': 'problem'}, - ) + course_id=course_id.to_deprecated_string(), + student=user, + module_state_key=module_state_key, + defaults={'state': '{}', 'module_type': 'problem'}, + ) try: pmd = PsychometricData.objects.using(db).get(studentmodule=sm) diff --git a/lms/djangoapps/shoppingcart/models.py b/lms/djangoapps/shoppingcart/models.py index d2e1e082fb..e33e88a06d 100644 --- a/lms/djangoapps/shoppingcart/models.py +++ b/lms/djangoapps/shoppingcart/models.py @@ -29,6 +29,7 @@ from edxmako.shortcuts import render_to_string from student.views import course_from_id from student.models import CourseEnrollment, unenroll_done from util.query import use_read_replica_if_available +from xmodule_django.models import CourseKeyField from verify_student.models import SoftwareSecurePhotoVerification @@ -310,7 +311,7 @@ class PaidCourseRegistration(OrderItem): """ This is an inventory item for paying for a course registration """ - course_id = models.CharField(max_length=128, db_index=True) + course_id = CourseKeyField(max_length=128, db_index=True) mode = models.SlugField(default=CourseMode.DEFAULT_MODE_SLUG) @classmethod @@ -331,10 +332,9 @@ class PaidCourseRegistration(OrderItem): Returns the order item """ # First a bunch of sanity checks - try: - course = course_from_id(course_id) # actually fetch the course to make sure it exists, use this to + course = course_from_id(course_id) # actually fetch the course to make sure it exists, use this to # throw errors if it doesn't - except ItemNotFoundError: + if not course: log.error("User {} tried to add non-existent course {} to cart id {}" .format(order.user.email, course_id, order.id)) raise CourseDoesNotExistException @@ -344,7 +344,7 @@ class PaidCourseRegistration(OrderItem): .format(order.user.email, course_id, order.id)) raise ItemAlreadyInCartException - if CourseEnrollment.is_enrolled(user=order.user, course_id=course_id): + if CourseEnrollment.is_enrolled(user=order.user, course_key=course_id): log.warning("User {} trying to add course {} to cart id {}, already registered" .format(order.user.email, course_id, order.id)) raise AlreadyEnrolledInCourseException @@ -385,18 +385,11 @@ class PaidCourseRegistration(OrderItem): in CourseEnrollmentAllowed will the user be allowed to enroll. Otherwise requiring payment would in fact be quite silly since there's a clear back door. """ - try: - course_loc = CourseDescriptor.id_to_location(self.course_id) - course_exists = modulestore().has_item(self.course_id, course_loc) - except ValueError: + if not modulestore().has_course(self.course_id): raise PurchasedCallbackException( "The customer purchased Course {0}, but that course doesn't exist!".format(self.course_id)) - if not course_exists: - raise PurchasedCallbackException( - "The customer purchased Course {0}, but that course doesn't exist!".format(self.course_id)) - - CourseEnrollment.enroll(user=self.user, course_id=self.course_id, mode=self.mode) + CourseEnrollment.enroll(user=self.user, course_key=self.course_id, mode=self.mode) log.info("Enrolled {0} in paid course {1}, paid ${2}" .format(self.user.email, self.course_id, self.line_cost)) # pylint: disable=E1101 @@ -430,18 +423,19 @@ class PaidCourseRegistrationAnnotation(models.Model): And unfortunately we didn't have the concept of a "SKU" or stock item where we could keep this association, so this is to retrofit it. """ - course_id = models.CharField(unique=True, max_length=128, db_index=True) + course_id = CourseKeyField(unique=True, max_length=128, db_index=True) annotation = models.TextField(null=True) def __unicode__(self): - return u"{} : {}".format(self.course_id, self.annotation) + # pylint: disable=no-member + return u"{} : {}".format(self.course_id.to_deprecated_string(), self.annotation) class CertificateItem(OrderItem): """ This is an inventory item for purchasing certificates """ - course_id = models.CharField(max_length=128, db_index=True) + course_id = CourseKeyField(max_length=128, db_index=True) course_enrollment = models.ForeignKey(CourseEnrollment) mode = models.SlugField() @@ -568,12 +562,17 @@ class CertificateItem(OrderItem): def single_item_receipt_context(self): course = course_from_id(self.course_id) return { - "course_id" : self.course_id, + "course_id": self.course_id, "course_name": course.display_name_with_default, "course_org": course.display_org_with_default, "course_num": course.display_number_with_default, "course_start_date_text": course.start_date_text, "course_has_started": course.start > datetime.today().replace(tzinfo=pytz.utc), + "course_root_url": reverse( + 'course_root', + kwargs={'course_id': self.course_id.to_deprecated_string()} # pylint: disable=no-member + ), + "dashboard_url": reverse('dashboard'), } @property diff --git a/lms/djangoapps/shoppingcart/reports.py b/lms/djangoapps/shoppingcart/reports.py index 1eaee921f2..8be4d71e81 100644 --- a/lms/djangoapps/shoppingcart/reports.py +++ b/lms/djangoapps/shoppingcart/reports.py @@ -274,6 +274,7 @@ def course_ids_between(start_word, end_word): valid_courses = [] for course in modulestore().get_courses(): - if (start_word.lower() <= course.id.lower() <= end_word.lower()) and (get_course_by_id(course.id) is not None): + course_id = course.id.to_deprecated_string() + if start_word.lower() <= course_id.lower() <= end_word.lower(): valid_courses.append(course.id) return valid_courses diff --git a/lms/djangoapps/shoppingcart/tests/test_models.py b/lms/djangoapps/shoppingcart/tests/test_models.py index ef210f18df..e04a5b3d8d 100644 --- a/lms/djangoapps/shoppingcart/tests/test_models.py +++ b/lms/djangoapps/shoppingcart/tests/test_models.py @@ -13,6 +13,7 @@ from django.test.utils import override_settings from django.contrib.auth.models import AnonymousUser from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory +from xmodule.modulestore.locations import SlashSeparatedCourseKey from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE from shoppingcart.models import (Order, OrderItem, CertificateItem, InvalidCartItem, PaidCourseRegistration, OrderItemSubclassPK) @@ -28,8 +29,8 @@ import datetime class OrderTest(ModuleStoreTestCase): def setUp(self): self.user = UserFactory.create() - self.course_id = "org/test/Test_Course" - CourseFactory.create(org='org', number='test', display_name='Test Course') + course = CourseFactory.create(org='org', number='test', display_name='Test Course') + self.course_key = course.id for i in xrange(1, 5): CourseFactory.create(org='org', number='test', display_name='Test Course {0}'.format(i)) self.cost = 40 @@ -38,7 +39,7 @@ class OrderTest(ModuleStoreTestCase): # create a cart cart = Order.get_cart_for_user(user=self.user) # add something to it - CertificateItem.add_to_order(cart, self.course_id, self.cost, 'honor') + CertificateItem.add_to_order(cart, self.course_key, self.cost, 'honor') # should return the same cart cart2 = Order.get_cart_for_user(user=self.user) self.assertEquals(cart2.orderitem_set.count(), 1) @@ -54,8 +55,8 @@ class OrderTest(ModuleStoreTestCase): def test_cart_clear(self): cart = Order.get_cart_for_user(user=self.user) - CertificateItem.add_to_order(cart, self.course_id, self.cost, 'honor') - CertificateItem.add_to_order(cart, 'org/test/Test_Course_1', self.cost, 'honor') + CertificateItem.add_to_order(cart, self.course_key, self.cost, 'honor') + CertificateItem.add_to_order(cart, SlashSeparatedCourseKey('org', 'test', 'Test_Course_1'), self.cost, 'honor') self.assertEquals(cart.orderitem_set.count(), 2) self.assertTrue(cart.has_items()) cart.clear() @@ -64,13 +65,13 @@ class OrderTest(ModuleStoreTestCase): def test_add_item_to_cart_currency_match(self): cart = Order.get_cart_for_user(user=self.user) - CertificateItem.add_to_order(cart, self.course_id, self.cost, 'honor', currency='eur') + CertificateItem.add_to_order(cart, self.course_key, self.cost, 'honor', currency='eur') # verify that a new item has been added self.assertEquals(cart.orderitem_set.count(), 1) # verify that the cart's currency was updated self.assertEquals(cart.currency, 'eur') with self.assertRaises(InvalidCartItem): - CertificateItem.add_to_order(cart, self.course_id, self.cost, 'honor', currency='usd') + CertificateItem.add_to_order(cart, self.course_key, self.cost, 'honor', currency='usd') # assert that this item did not get added to the cart self.assertEquals(cart.orderitem_set.count(), 1) @@ -82,7 +83,7 @@ class OrderTest(ModuleStoreTestCase): ('org/test/Test_Course_3', 10), ('org/test/Test_Course_4', 20)] for course, cost in course_costs: - CertificateItem.add_to_order(cart, course, cost, 'honor') + CertificateItem.add_to_order(cart, SlashSeparatedCourseKey.from_deprecated_string(course), cost, 'honor') self.assertEquals(cart.orderitem_set.count(), len(course_costs)) self.assertEquals(cart.total_cost, sum(cost for _course, cost in course_costs)) @@ -91,12 +92,12 @@ class OrderTest(ModuleStoreTestCase): # order to do this, we end up testing the specific functionality of # CertificateItem, which is not quite good unit test form. Sorry. cart = Order.get_cart_for_user(user=self.user) - self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course_id)) - item = CertificateItem.add_to_order(cart, self.course_id, self.cost, 'honor') + self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course_key)) + item = CertificateItem.add_to_order(cart, self.course_key, self.cost, 'honor') # course enrollment object should be created but still inactive - self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course_id)) + self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course_key)) cart.purchase() - self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course_id)) + self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course_key)) # test e-mail sending self.assertEquals(len(mail.outbox), 1) @@ -109,18 +110,18 @@ class OrderTest(ModuleStoreTestCase): # once again, we're testing against the specific implementation of # CertificateItem cart = Order.get_cart_for_user(user=self.user) - CertificateItem.add_to_order(cart, self.course_id, self.cost, 'honor') + CertificateItem.add_to_order(cart, self.course_key, self.cost, 'honor') with patch('shoppingcart.models.CertificateItem.save', side_effect=DatabaseError): with self.assertRaises(DatabaseError): cart.purchase() # verify that we rolled back the entire transaction - self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course_id)) + self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course_key)) # verify that e-mail wasn't sent self.assertEquals(len(mail.outbox), 0) def test_purchase_twice(self): cart = Order.get_cart_for_user(self.user) - CertificateItem.add_to_order(cart, self.course_id, self.cost, 'honor') + CertificateItem.add_to_order(cart, self.course_key, self.cost, 'honor') # purchase the cart more than once cart.purchase() cart.purchase() @@ -129,7 +130,7 @@ class OrderTest(ModuleStoreTestCase): @patch('shoppingcart.models.log.error') def test_purchase_item_email_smtp_failure(self, error_logger): cart = Order.get_cart_for_user(user=self.user) - CertificateItem.add_to_order(cart, self.course_id, self.cost, 'honor') + CertificateItem.add_to_order(cart, self.course_key, self.cost, 'honor') with patch('shoppingcart.models.send_mail', side_effect=smtplib.SMTPException): cart.purchase() self.assertTrue(error_logger.called) @@ -137,14 +138,14 @@ class OrderTest(ModuleStoreTestCase): @patch('shoppingcart.models.log.error') def test_purchase_item_email_boto_failure(self, error_logger): cart = Order.get_cart_for_user(user=self.user) - CertificateItem.add_to_order(cart, self.course_id, self.cost, 'honor') + CertificateItem.add_to_order(cart, self.course_key, self.cost, 'honor') with patch('shoppingcart.models.send_mail', side_effect=BotoServerError("status", "reason")): cart.purchase() self.assertTrue(error_logger.called) def purchase_with_data(self, cart): """ purchase a cart with billing information """ - CertificateItem.add_to_order(cart, self.course_id, self.cost, 'honor') + CertificateItem.add_to_order(cart, self.course_key, self.cost, 'honor') cart.purchase( first='John', last='Smith', @@ -241,10 +242,10 @@ class OrderItemTest(TestCase): class PaidCourseRegistrationTest(ModuleStoreTestCase): def setUp(self): self.user = UserFactory.create() - self.course_id = "MITx/999/Robot_Super_Course" self.cost = 40 self.course = CourseFactory.create(org='MITx', number='999', display_name='Robot Super Course') - self.course_mode = CourseMode(course_id=self.course_id, + self.course_key = self.course.id + self.course_mode = CourseMode(course_id=self.course_key, mode_slug="honor", mode_display_name="honor cert", min_price=self.cost) @@ -252,7 +253,7 @@ class PaidCourseRegistrationTest(ModuleStoreTestCase): self.cart = Order.get_cart_for_user(self.user) def test_add_to_order(self): - reg1 = PaidCourseRegistration.add_to_order(self.cart, self.course_id) + reg1 = PaidCourseRegistration.add_to_order(self.cart, self.course_key) self.assertEqual(reg1.unit_cost, self.cost) self.assertEqual(reg1.line_cost, self.cost) @@ -260,8 +261,9 @@ class PaidCourseRegistrationTest(ModuleStoreTestCase): self.assertEqual(reg1.mode, "honor") self.assertEqual(reg1.user, self.user) self.assertEqual(reg1.status, "cart") - self.assertTrue(PaidCourseRegistration.contained_in_order(self.cart, self.course_id)) - self.assertFalse(PaidCourseRegistration.contained_in_order(self.cart, self.course_id + "abcd")) + self.assertTrue(PaidCourseRegistration.contained_in_order(self.cart, self.course_key)) + self.assertFalse(PaidCourseRegistration.contained_in_order(self.cart, SlashSeparatedCourseKey("MITx", "999", "Robot_Super_Course_abcd")) + self.assertEqual(self.cart.total_cost, self.cost) def test_add_with_default_mode(self): @@ -269,7 +271,7 @@ class PaidCourseRegistrationTest(ModuleStoreTestCase): Tests add_to_cart where the mode specified in the argument is NOT in the database and NOT the default "honor". In this case it just adds the user in the CourseMode.DEFAULT_MODE, 0 price """ - reg1 = PaidCourseRegistration.add_to_order(self.cart, self.course_id, mode_slug="DNE") + reg1 = PaidCourseRegistration.add_to_order(self.cart, self.course_key, mode_slug="DNE") self.assertEqual(reg1.unit_cost, 0) self.assertEqual(reg1.line_cost, 0) @@ -277,12 +279,12 @@ class PaidCourseRegistrationTest(ModuleStoreTestCase): self.assertEqual(reg1.user, self.user) self.assertEqual(reg1.status, "cart") self.assertEqual(self.cart.total_cost, 0) - self.assertTrue(PaidCourseRegistration.contained_in_order(self.cart, self.course_id)) + self.assertTrue(PaidCourseRegistration.contained_in_order(self.cart, self.course_key)) def test_purchased_callback(self): - reg1 = PaidCourseRegistration.add_to_order(self.cart, self.course_id) + reg1 = PaidCourseRegistration.add_to_order(self.cart, self.course_key) self.cart.purchase() - self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course_id)) + self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course_key)) reg1 = PaidCourseRegistration.objects.get(id=reg1.id) # reload from DB to get side-effect self.assertEqual(reg1.status, "purchased") @@ -296,7 +298,7 @@ class PaidCourseRegistrationTest(ModuleStoreTestCase): mode_display_name="honor cert", min_price=self.cost) course_mode2.save() - pr1 = PaidCourseRegistration.add_to_order(self.cart, self.course_id) + pr1 = PaidCourseRegistration.add_to_order(self.cart, self.course_key) pr2 = PaidCourseRegistration.add_to_order(self.cart, course2.id) self.cart.purchase() inst_dict, inst_set = self.cart.generate_receipt_instructions() @@ -307,18 +309,18 @@ class PaidCourseRegistrationTest(ModuleStoreTestCase): self.assertIn(pr2.pk_with_subclass, inst_dict) def test_purchased_callback_exception(self): - reg1 = PaidCourseRegistration.add_to_order(self.cart, self.course_id) - reg1.course_id = "changedforsomereason" + reg1 = PaidCourseRegistration.add_to_order(self.cart, self.course_key) + reg1.course_id = SlashSeparatedCourseKey("changed", "forsome", "reason") reg1.save() with self.assertRaises(PurchasedCallbackException): reg1.purchased_callback() - self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course_id)) + self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course_key)) - reg1.course_id = "abc/efg/hij" + reg1.course_id = SlashSeparatedCourseKey("abc", "efg", "hij") reg1.save() with self.assertRaises(PurchasedCallbackException): reg1.purchased_callback() - self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course_id)) + self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course_key)) @override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) @@ -328,15 +330,15 @@ class CertificateItemTest(ModuleStoreTestCase): """ def setUp(self): self.user = UserFactory.create() - self.course_id = "org/test/Test_Course" self.cost = 40 - CourseFactory.create(org='org', number='test', run='course', display_name='Test Course') - course_mode = CourseMode(course_id=self.course_id, + course = CourseFactory.create(org='org', number='test', display_name='Test Course') + self.course_key = course.id + course_mode = CourseMode(course_id=self.course_key, mode_slug="honor", mode_display_name="honor cert", min_price=self.cost) course_mode.save() - course_mode = CourseMode(course_id=self.course_id, + course_mode = CourseMode(course_id=self.course_key, mode_slug="verified", mode_display_name="verified cert", min_price=self.cost) @@ -351,60 +353,60 @@ class CertificateItemTest(ModuleStoreTestCase): self.mock_get_current_request.return_value = sentinel.request def test_existing_enrollment(self): - CourseEnrollment.enroll(self.user, self.course_id) + CourseEnrollment.enroll(self.user, self.course_key) cart = Order.get_cart_for_user(user=self.user) - CertificateItem.add_to_order(cart, self.course_id, self.cost, 'verified') + CertificateItem.add_to_order(cart, self.course_key, self.cost, 'verified') # verify that we are still enrolled - self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course_id)) + self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course_key)) self.mock_server_track.reset_mock() cart.purchase() - enrollment = CourseEnrollment.objects.get(user=self.user, course_id=self.course_id) + enrollment = CourseEnrollment.objects.get(user=self.user, course_id=self.course_key) self.assertEquals(enrollment.mode, u'verified') def test_single_item_template(self): cart = Order.get_cart_for_user(user=self.user) - cert_item = CertificateItem.add_to_order(cart, self.course_id, self.cost, 'verified') + cert_item = CertificateItem.add_to_order(cart, self.course_key, self.cost, 'verified') self.assertEquals(cert_item.single_item_receipt_template, 'shoppingcart/verified_cert_receipt.html') - cert_item = CertificateItem.add_to_order(cart, self.course_id, self.cost, 'honor') + cert_item = CertificateItem.add_to_order(cart, self.course_key, self.cost, 'honor') self.assertEquals(cert_item.single_item_receipt_template, 'shoppingcart/receipt.html') def test_refund_cert_callback_no_expiration(self): # When there is no expiration date on a verified mode, the user can always get a refund - CourseEnrollment.enroll(self.user, self.course_id, 'verified') + CourseEnrollment.enroll(self.user, self.course_key, 'verified') cart = Order.get_cart_for_user(user=self.user) - CertificateItem.add_to_order(cart, self.course_id, self.cost, 'verified') + CertificateItem.add_to_order(cart, self.course_key, self.cost, 'verified') cart.purchase() - CourseEnrollment.unenroll(self.user, self.course_id) - target_certs = CertificateItem.objects.filter(course_id=self.course_id, user_id=self.user, status='refunded', mode='verified') + CourseEnrollment.unenroll(self.user, self.course_key) + target_certs = CertificateItem.objects.filter(course_id=self.course_key, user_id=self.user, status='refunded', mode='verified') self.assertTrue(target_certs[0]) self.assertTrue(target_certs[0].refund_requested_time) self.assertEquals(target_certs[0].order.status, 'refunded') def test_refund_cert_callback_before_expiration(self): # If the expiration date has not yet passed on a verified mode, the user can be refunded - course_id = "refund_before_expiration/test/one" many_days = datetime.timedelta(days=60) - CourseFactory.create(org='refund_before_expiration', number='test', run='course', display_name='one') - course_mode = CourseMode(course_id=course_id, + course = CourseFactory.create(org='refund_before_expiration', number='test', display_name='one') + course_key = course.id + course_mode = CourseMode(course_id=course_key, mode_slug="verified", mode_display_name="verified cert", min_price=self.cost, expiration_datetime=(datetime.datetime.now(pytz.utc) + many_days)) course_mode.save() - CourseEnrollment.enroll(self.user, course_id, 'verified') + CourseEnrollment.enroll(self.user, course_key, 'verified') cart = Order.get_cart_for_user(user=self.user) - CertificateItem.add_to_order(cart, course_id, self.cost, 'verified') + CertificateItem.add_to_order(cart, course_key, self.cost, 'verified') cart.purchase() - CourseEnrollment.unenroll(self.user, course_id) - target_certs = CertificateItem.objects.filter(course_id=course_id, user_id=self.user, status='refunded', mode='verified') + CourseEnrollment.unenroll(self.user, course_key) + target_certs = CertificateItem.objects.filter(course_id=course_key, user_id=self.user, status='refunded', mode='verified') self.assertTrue(target_certs[0]) self.assertTrue(target_certs[0].refund_requested_time) self.assertEquals(target_certs[0].order.status, 'refunded') @@ -412,52 +414,53 @@ class CertificateItemTest(ModuleStoreTestCase): @patch('shoppingcart.models.log.error') def test_refund_cert_callback_before_expiration_email_error(self, error_logger): # If there's an error sending an email to billing, we need to log this error - course_id = "refund_before_expiration/test/one" many_days = datetime.timedelta(days=60) - CourseFactory.create(org='refund_before_expiration', number='test', run='course', display_name='one') - course_mode = CourseMode(course_id=course_id, + course = CourseFactory.create(org='refund_before_expiration', number='test', display_name='one') + course_key = course.id + + course_mode = CourseMode(course_id=course_key, mode_slug="verified", mode_display_name="verified cert", min_price=self.cost, expiration_datetime=datetime.datetime.now(pytz.utc) + many_days) course_mode.save() - CourseEnrollment.enroll(self.user, course_id, 'verified') + CourseEnrollment.enroll(self.user, course_key, 'verified') cart = Order.get_cart_for_user(user=self.user) - CertificateItem.add_to_order(cart, course_id, self.cost, 'verified') + CertificateItem.add_to_order(cart, course_key, self.cost, 'verified') cart.purchase() with patch('shoppingcart.models.send_mail', side_effect=smtplib.SMTPException): - CourseEnrollment.unenroll(self.user, course_id) + CourseEnrollment.unenroll(self.user, course_key) self.assertTrue(error_logger.called) def test_refund_cert_callback_after_expiration(self): # If the expiration date has passed, the user cannot get a refund - course_id = "refund_after_expiration/test/two" many_days = datetime.timedelta(days=60) - CourseFactory.create(org='refund_after_expiration', number='test', run='course', display_name='two') - course_mode = CourseMode(course_id=course_id, + course = CourseFactory.create(org='refund_after_expiration', number='test', display_name='two') + course_key = course.id + course_mode = CourseMode(course_id=course_key, mode_slug="verified", mode_display_name="verified cert", min_price=self.cost,) course_mode.save() - CourseEnrollment.enroll(self.user, course_id, 'verified') + CourseEnrollment.enroll(self.user, course_key, 'verified') cart = Order.get_cart_for_user(user=self.user) - CertificateItem.add_to_order(cart, course_id, self.cost, 'verified') + CertificateItem.add_to_order(cart, course_key, self.cost, 'verified') cart.purchase() course_mode.expiration_datetime = (datetime.datetime.now(pytz.utc) - many_days) course_mode.save() - CourseEnrollment.unenroll(self.user, course_id) - target_certs = CertificateItem.objects.filter(course_id=course_id, user_id=self.user, status='refunded', mode='verified') + CourseEnrollment.unenroll(self.user, course_key) + target_certs = CertificateItem.objects.filter(course_id=course_key, user_id=self.user, status='refunded', mode='verified') self.assertEqual(len(target_certs), 0) def test_refund_cert_no_cert_exists(self): # If there is no paid certificate, the refund callback should return nothing - CourseEnrollment.enroll(self.user, self.course_id, 'verified') - ret_val = CourseEnrollment.unenroll(self.user, self.course_id) + CourseEnrollment.enroll(self.user, self.course_key, 'verified') + ret_val = CourseEnrollment.unenroll(self.user, self.course_key) self.assertFalse(ret_val) diff --git a/lms/djangoapps/shoppingcart/tests/test_reports.py b/lms/djangoapps/shoppingcart/tests/test_reports.py index d0dccd0acd..22ae61fb19 100644 --- a/lms/djangoapps/shoppingcart/tests/test_reports.py +++ b/lms/djangoapps/shoppingcart/tests/test_reports.py @@ -64,17 +64,17 @@ class ReportTypeTests(ModuleStoreTestCase): # Two are verified, three are audit, one honor - self.course_id = "MITx/999/Robot_Super_Course" - settings.COURSE_LISTINGS['default'] = [self.course_id] self.cost = 40 self.course = CourseFactory.create(org='MITx', number='999', display_name=u'Robot Super Course') - course_mode = CourseMode(course_id=self.course_id, + self.course_key = self.course.id + settings.COURSE_LISTINGS['default'] = [self.course_key.to_deprecated_string()] + course_mode = CourseMode(course_id=self.course_key, mode_slug="honor", mode_display_name="honor cert", min_price=self.cost) course_mode.save() - course_mode2 = CourseMode(course_id=self.course_id, + course_mode2 = CourseMode(course_id=self.course_key, mode_slug="verified", mode_display_name="verified cert", min_price=self.cost) @@ -82,33 +82,33 @@ class ReportTypeTests(ModuleStoreTestCase): # User 1 & 2 will be verified self.cart1 = Order.get_cart_for_user(self.first_verified_user) - CertificateItem.add_to_order(self.cart1, self.course_id, self.cost, 'verified') + CertificateItem.add_to_order(self.cart1, self.course_key, self.cost, 'verified') self.cart1.purchase() self.cart2 = Order.get_cart_for_user(self.second_verified_user) - CertificateItem.add_to_order(self.cart2, self.course_id, self.cost, 'verified') + CertificateItem.add_to_order(self.cart2, self.course_key, self.cost, 'verified') self.cart2.purchase() # Users 3, 4, and 5 are audit - CourseEnrollment.enroll(self.first_audit_user, self.course_id, "audit") - CourseEnrollment.enroll(self.second_audit_user, self.course_id, "audit") - CourseEnrollment.enroll(self.third_audit_user, self.course_id, "audit") + CourseEnrollment.enroll(self.first_audit_user, self.course_key, "audit") + CourseEnrollment.enroll(self.second_audit_user, self.course_key, "audit") + CourseEnrollment.enroll(self.third_audit_user, self.course_key, "audit") # User 6 is honor - CourseEnrollment.enroll(self.honor_user, self.course_id, "honor") + CourseEnrollment.enroll(self.honor_user, self.course_key, "honor") self.now = datetime.datetime.now(pytz.UTC) # Users 7 & 8 are refunds self.cart = Order.get_cart_for_user(self.first_refund_user) - CertificateItem.add_to_order(self.cart, self.course_id, self.cost, 'verified') + CertificateItem.add_to_order(self.cart, self.course_key, self.cost, 'verified') self.cart.purchase() - CourseEnrollment.unenroll(self.first_refund_user, self.course_id) + CourseEnrollment.unenroll(self.first_refund_user, self.course_key) self.cart = Order.get_cart_for_user(self.second_refund_user) - CertificateItem.add_to_order(self.cart, self.course_id, self.cost, 'verified') - self.cart.purchase(self.second_refund_user, self.course_id) - CourseEnrollment.unenroll(self.second_refund_user, self.course_id) + CertificateItem.add_to_order(self.cart, self.course_key, self.cost, 'verified') + self.cart.purchase(self.second_refund_user, self.course_key) + CourseEnrollment.unenroll(self.second_refund_user, self.course_key) self.test_time = datetime.datetime.now(pytz.UTC) @@ -148,8 +148,8 @@ class ReportTypeTests(ModuleStoreTestCase): num_certs += 1 self.assertEqual(num_certs, 2) - self.assertTrue(CertificateItem.objects.get(user=self.first_refund_user, course_id=self.course_id)) - self.assertTrue(CertificateItem.objects.get(user=self.second_refund_user, course_id=self.course_id)) + self.assertTrue(CertificateItem.objects.get(user=self.first_refund_user, course_id=self.course_key)) + self.assertTrue(CertificateItem.objects.get(user=self.second_refund_user, course_id=self.course_key)) def test_refund_report_purchased_csv(self): """ @@ -188,33 +188,33 @@ class ItemizedPurchaseReportTest(ModuleStoreTestCase): def setUp(self): self.user = UserFactory.create() - self.course_id = "MITx/999/Robot_Super_Course" self.cost = 40 self.course = CourseFactory.create(org='MITx', number='999', display_name=u'Robot Super Course') - course_mode = CourseMode(course_id=self.course_id, + self.course_key = self.course.id + course_mode = CourseMode(course_id=self.course_key, mode_slug="honor", mode_display_name="honor cert", min_price=self.cost) course_mode.save() - course_mode2 = CourseMode(course_id=self.course_id, + course_mode2 = CourseMode(course_id=self.course_key, mode_slug="verified", mode_display_name="verified cert", min_price=self.cost) course_mode2.save() - self.annotation = PaidCourseRegistrationAnnotation(course_id=self.course_id, annotation=self.TEST_ANNOTATION) + self.annotation = PaidCourseRegistrationAnnotation(course_id=self.course_key, annotation=self.TEST_ANNOTATION) self.annotation.save() self.cart = Order.get_cart_for_user(self.user) - self.reg = PaidCourseRegistration.add_to_order(self.cart, self.course_id) - self.cert_item = CertificateItem.add_to_order(self.cart, self.course_id, self.cost, 'verified') + self.reg = PaidCourseRegistration.add_to_order(self.cart, self.course_key) + self.cert_item = CertificateItem.add_to_order(self.cart, self.course_key, self.cost, 'verified') self.cart.purchase() self.now = datetime.datetime.now(pytz.UTC) - paid_reg = PaidCourseRegistration.objects.get(course_id=self.course_id, user=self.user) + paid_reg = PaidCourseRegistration.objects.get(course_id=self.course_key, user=self.user) paid_reg.fulfilled_time = self.now paid_reg.refund_requested_time = self.now paid_reg.save() - cert = CertificateItem.objects.get(course_id=self.course_id, user=self.user) + cert = CertificateItem.objects.get(course_id=self.course_key, user=self.user) cert.fulfilled_time = self.now cert.refund_requested_time = self.now cert.save() @@ -268,4 +268,4 @@ class ItemizedPurchaseReportTest(ModuleStoreTestCase): """ Fill in gap in test coverage. __unicode__ method of PaidCourseRegistrationAnnotation """ - self.assertEqual(unicode(self.annotation), u'{} : {}'.format(self.course_id, self.TEST_ANNOTATION)) + self.assertEqual(unicode(self.annotation), u'{} : {}'.format(self.course_key.to_deprecated_string(), self.TEST_ANNOTATION)) diff --git a/lms/djangoapps/shoppingcart/tests/test_views.py b/lms/djangoapps/shoppingcart/tests/test_views.py index 9799b6ddda..99c28eabae 100644 --- a/lms/djangoapps/shoppingcart/tests/test_views.py +++ b/lms/djangoapps/shoppingcart/tests/test_views.py @@ -45,16 +45,16 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): self.user = UserFactory.create() self.user.set_password('password') self.user.save() - self.course_id = "MITx/999/Robot_Super_Course" self.cost = 40 self.course = CourseFactory.create(org='MITx', number='999', display_name='Robot Super Course') - self.course_mode = CourseMode(course_id=self.course_id, + self.course_key = self.course.id + self.course_mode = CourseMode(course_id=self.course_key, mode_slug="honor", mode_display_name="honor cert", min_price=self.cost) self.course_mode.save() - self.verified_course_id = 'org/test/Test_Course' - CourseFactory.create(org='org', number='test', run='course1', display_name='Test Course') + verified_course = CourseFactory.create(org='org', number='test', display_name='Test Course') + self.verified_course_key = verified_course.id self.cart = Order.get_cart_for_user(self.user) self.addCleanup(patcher.stop) @@ -62,22 +62,22 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): self.client.login(username=self.user.username, password="password") def test_add_course_to_cart_anon(self): - resp = self.client.post(reverse('shoppingcart.views.add_course_to_cart', args=[self.course_id])) + resp = self.client.post(reverse('shoppingcart.views.add_course_to_cart', args=[self.course_key.to_deprecated_string()])) self.assertEqual(resp.status_code, 403) def test_add_course_to_cart_already_in_cart(self): - PaidCourseRegistration.add_to_order(self.cart, self.course_id) + PaidCourseRegistration.add_to_order(self.cart, self.course_key) self.login_user() - resp = self.client.post(reverse('shoppingcart.views.add_course_to_cart', args=[self.course_id])) + resp = self.client.post(reverse('shoppingcart.views.add_course_to_cart', args=[self.course_key.to_deprecated_string()])) self.assertEqual(resp.status_code, 400) - self.assertIn(_('The course {0} is already in your cart.'.format(self.course_id)), resp.content) + self.assertIn(_('The course {0} is already in your cart.'.format(self.course_key.to_deprecated_string())), resp.content) def test_add_course_to_cart_already_registered(self): - CourseEnrollment.enroll(self.user, self.course_id) + CourseEnrollment.enroll(self.user, self.course_key) self.login_user() - resp = self.client.post(reverse('shoppingcart.views.add_course_to_cart', args=[self.course_id])) + resp = self.client.post(reverse('shoppingcart.views.add_course_to_cart', args=[self.course_key.to_deprecated_string()])) self.assertEqual(resp.status_code, 400) - self.assertIn(_('You are already registered in course {0}.'.format(self.course_id)), resp.content) + self.assertIn(_('You are already registered in course {0}.'.format(self.course_key.to_deprecated_string())), resp.content) def test_add_nonexistent_course_to_cart(self): self.login_user() @@ -87,18 +87,18 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): def test_add_course_to_cart_success(self): self.login_user() - reverse('shoppingcart.views.add_course_to_cart', args=[self.course_id]) - resp = self.client.post(reverse('shoppingcart.views.add_course_to_cart', args=[self.course_id])) + reverse('shoppingcart.views.add_course_to_cart', args=[self.course_key.to_deprecated_string()]) + resp = self.client.post(reverse('shoppingcart.views.add_course_to_cart', args=[self.course_key.to_deprecated_string()])) self.assertEqual(resp.status_code, 200) - self.assertTrue(PaidCourseRegistration.contained_in_order(self.cart, self.course_id)) + self.assertTrue(PaidCourseRegistration.contained_in_order(self.cart, self.course_key)) @patch('shoppingcart.views.render_purchase_form_html', form_mock) @patch('shoppingcart.views.render_to_response', render_mock) def test_show_cart(self): self.login_user() - reg_item = PaidCourseRegistration.add_to_order(self.cart, self.course_id) - cert_item = CertificateItem.add_to_order(self.cart, self.verified_course_id, self.cost, 'honor') + reg_item = PaidCourseRegistration.add_to_order(self.cart, self.course_key) + cert_item = CertificateItem.add_to_order(self.cart, self.verified_course_key, self.cost, 'honor') resp = self.client.get(reverse('shoppingcart.views.show_cart', args=[])) self.assertEqual(resp.status_code, 200) @@ -116,8 +116,8 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): def test_clear_cart(self): self.login_user() - PaidCourseRegistration.add_to_order(self.cart, self.course_id) - CertificateItem.add_to_order(self.cart, self.verified_course_id, self.cost, 'honor') + PaidCourseRegistration.add_to_order(self.cart, self.course_key) + CertificateItem.add_to_order(self.cart, self.verified_course_key, self.cost, 'honor') self.assertEquals(self.cart.orderitem_set.count(), 2) resp = self.client.post(reverse('shoppingcart.views.clear_cart', args=[])) self.assertEqual(resp.status_code, 200) @@ -126,8 +126,8 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): @patch('shoppingcart.views.log.exception') def test_remove_item(self, exception_log): self.login_user() - reg_item = PaidCourseRegistration.add_to_order(self.cart, self.course_id) - cert_item = CertificateItem.add_to_order(self.cart, self.verified_course_id, self.cost, 'honor') + reg_item = PaidCourseRegistration.add_to_order(self.cart, self.course_key) + cert_item = CertificateItem.add_to_order(self.cart, self.verified_course_key, self.cost, 'honor') self.assertEquals(self.cart.orderitem_set.count(), 2) resp = self.client.post(reverse('shoppingcart.views.remove_item', args=[]), {'id': reg_item.id}) @@ -172,13 +172,13 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): self.assertEqual(context['error_html'], 'ERROR_TEST!!!') def test_show_receipt_404s(self): - PaidCourseRegistration.add_to_order(self.cart, self.course_id) - CertificateItem.add_to_order(self.cart, self.verified_course_id, self.cost, 'honor') + PaidCourseRegistration.add_to_order(self.cart, self.course_key) + CertificateItem.add_to_order(self.cart, self.verified_course_key, self.cost, 'honor') self.cart.purchase() user2 = UserFactory.create() cart2 = Order.get_cart_for_user(user2) - PaidCourseRegistration.add_to_order(cart2, self.course_id) + PaidCourseRegistration.add_to_order(cart2, self.course_key) cart2.purchase() self.login_user() @@ -190,8 +190,8 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): @patch('shoppingcart.views.render_to_response', render_mock) def test_show_receipt_success(self): - reg_item = PaidCourseRegistration.add_to_order(self.cart, self.course_id) - cert_item = CertificateItem.add_to_order(self.cart, self.verified_course_id, self.cost, 'honor') + reg_item = PaidCourseRegistration.add_to_order(self.cart, self.course_key) + cert_item = CertificateItem.add_to_order(self.cart, self.verified_course_key, self.cost, 'honor') self.cart.purchase(first='FirstNameTesting123', street1='StreetTesting123') self.login_user() @@ -210,8 +210,8 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): @patch('shoppingcart.views.render_to_response', render_mock) def test_show_receipt_success_with_upgrade(self): - reg_item = PaidCourseRegistration.add_to_order(self.cart, self.course_id) - cert_item = CertificateItem.add_to_order(self.cart, self.verified_course_id, self.cost, 'honor') + reg_item = PaidCourseRegistration.add_to_order(self.cart, self.course_key) + cert_item = CertificateItem.add_to_order(self.cart, self.verified_course_key, self.cost, 'honor') self.cart.purchase(first='FirstNameTesting123', street1='StreetTesting123') self.login_user() @@ -227,7 +227,7 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): # Once they've upgraded, they're no longer *attempting* to upgrade attempting_upgrade = self.client.session.get('attempting_upgrade', False) self.assertFalse(attempting_upgrade) - + self.assertEqual(resp.status_code, 200) self.assertIn('FirstNameTesting123', resp.content) self.assertIn('80.00', resp.content) @@ -244,22 +244,22 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): self.assertIn(cert_item, context['order_items']) self.assertFalse(context['any_refunds']) - course_enrollment = CourseEnrollment.get_or_create_enrollment(self.user, self.course_id) + course_enrollment = CourseEnrollment.get_or_create_enrollment(self.user, self.course_key) course_enrollment.emit_event('edx.course.enrollment.upgrade.succeeded') self.mock_server_track.assert_any_call( None, 'edx.course.enrollment.upgrade.succeeded', { 'user_id': course_enrollment.user.id, - 'course_id': course_enrollment.course_id, + 'course_id': course_enrollment.course_id.to_deprecated_string(), 'mode': course_enrollment.mode } ) @patch('shoppingcart.views.render_to_response', render_mock) def test_show_receipt_success_refund(self): - reg_item = PaidCourseRegistration.add_to_order(self.cart, self.course_id) - cert_item = CertificateItem.add_to_order(self.cart, self.verified_course_id, self.cost, 'honor') + reg_item = PaidCourseRegistration.add_to_order(self.cart, self.course_key) + cert_item = CertificateItem.add_to_order(self.cart, self.verified_course_key, self.cost, 'honor') self.cart.purchase(first='FirstNameTesting123', street1='StreetTesting123') cert_item.status = "refunded" cert_item.save() @@ -278,7 +278,7 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): @patch('shoppingcart.views.render_to_response', render_mock) def test_show_receipt_success_custom_receipt_page(self): - cert_item = CertificateItem.add_to_order(self.cart, self.course_id, self.cost, 'honor') + cert_item = CertificateItem.add_to_order(self.cart, self.course_key, self.cost, 'honor') self.cart.purchase() self.login_user() receipt_url = reverse('shoppingcart.views.show_receipt', args=[self.cart.id]) @@ -297,21 +297,22 @@ class CSVReportViewsTest(ModuleStoreTestCase): self.user = UserFactory.create() self.user.set_password('password') self.user.save() - self.course_id = "MITx/999/Robot_Super_Course" self.cost = 40 self.course = CourseFactory.create(org='MITx', number='999', display_name='Robot Super Course') - self.course_mode = CourseMode(course_id=self.course_id, + self.course_key = self.course.id + self.course_mode = CourseMode(course_id=self.course_key, mode_slug="honor", mode_display_name="honor cert", min_price=self.cost) self.course_mode.save() - self.course_mode2 = CourseMode(course_id=self.course_id, + self.course_mode2 = CourseMode(course_id=self.course_key, mode_slug="verified", mode_display_name="verified cert", min_price=self.cost) self.course_mode2.save() - self.verified_course_id = 'org/test/Test_Course' - CourseFactory.create(org='org', number='test', run='course1', display_name='Test Course') + verified_course = CourseFactory.create(org='org', number='test', display_name='Test Course') + + self.verified_course_key = verified_course.id self.cart = Order.get_cart_for_user(self.user) self.dl_grp = Group(name=settings.PAYMENT_REPORT_GENERATOR_GROUP) self.dl_grp.save() @@ -370,7 +371,7 @@ class CSVReportViewsTest(ModuleStoreTestCase): report_type = 'itemized_purchase_report' start_date = '1970-01-01' end_date = '2100-01-01' - PaidCourseRegistration.add_to_order(self.cart, self.course_id) + PaidCourseRegistration.add_to_order(self.cart, self.course_key) self.cart.purchase() self.login_user() self.add_to_download_group(self.user) diff --git a/lms/djangoapps/shoppingcart/views.py b/lms/djangoapps/shoppingcart/views.py index fd1470588d..c5bf21bac1 100644 --- a/lms/djangoapps/shoppingcart/views.py +++ b/lms/djangoapps/shoppingcart/views.py @@ -11,6 +11,7 @@ from django.core.urlresolvers import reverse from django.views.decorators.csrf import csrf_exempt from django.contrib.auth.decorators import login_required from edxmako.shortcuts import render_to_response +from xmodule.modulestore.locations import SlashSeparatedCourseKey from shoppingcart.reports import RefundReport, ItemizedPurchaseReport, UniversityRevenueShareReport, CertificateStatusReport from student.models import CourseEnrollment from .exceptions import ItemAlreadyInCartException, AlreadyEnrolledInCourseException, CourseDoesNotExistException, ReportTypeDoesNotExistException @@ -44,13 +45,16 @@ def add_course_to_cart(request, course_id): Adds course specified by course_id to the cart. The model function add_to_order does all the heavy lifting (logging, error checking, etc) """ + + assert isinstance(course_id, basestring) if not request.user.is_authenticated(): log.info("Anon user trying to add course {} to cart".format(course_id)) return HttpResponseForbidden(_('You must be logged-in to add to a shopping cart')) cart = Order.get_cart_for_user(request.user) + course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) # All logging from here handled by the model try: - PaidCourseRegistration.add_to_order(cart, course_id) + PaidCourseRegistration.add_to_order(cart, course_key) except CourseDoesNotExistException: return HttpResponseNotFound(_('The course you requested does not exist.')) except ItemAlreadyInCartException: diff --git a/lms/djangoapps/staticbook/tests.py b/lms/djangoapps/staticbook/tests.py index 65ae5025c6..5f76cea1b4 100644 --- a/lms/djangoapps/staticbook/tests.py +++ b/lms/djangoapps/staticbook/tests.py @@ -72,7 +72,7 @@ class StaticBookTest(ModuleStoreTestCase): Automatically provides the course id. """ - kwargs['course_id'] = self.course.id + kwargs['course_id'] = self.course.id.to_deprecated_string() url = reverse(url_name, kwargs=kwargs) return url @@ -115,7 +115,7 @@ class StaticImageBookTest(StaticBookTest): self.assertEqual(response.status_code, 404) def test_bad_page_id(self): - # A bad page id will cause a 404. + # A bad page id will cause a 404. self.make_course(textbooks=[IMAGE_BOOK]) with self.assertRaises(NoReverseMatch): self.make_url('book', book_index=0, page='xyzzy') diff --git a/lms/djangoapps/staticbook/views.py b/lms/djangoapps/staticbook/views.py index 17d886be11..f1eee698c5 100644 --- a/lms/djangoapps/staticbook/views.py +++ b/lms/djangoapps/staticbook/views.py @@ -6,6 +6,7 @@ from django.contrib.auth.decorators import login_required from django.http import Http404 from edxmako.shortcuts import render_to_response +from xmodule.modulestore.locations import SlashSeparatedCourseKey from courseware.access import has_access from courseware.courses import get_course_with_access from notes.utils import notes_enabled_for_course @@ -17,8 +18,9 @@ def index(request, course_id, book_index, page=None): """ Serve static image-based textbooks. """ - course = get_course_with_access(request.user, course_id, 'load') - staff_access = has_access(request.user, course, 'staff') + course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) + course = get_course_with_access(request.user, 'load', course_key) + staff_access = has_access(request.user, 'staff', course) book_index = int(book_index) if book_index < 0 or book_index >= len(course.textbooks): @@ -50,7 +52,7 @@ def remap_static_url(original_url, course): output_url = replace_static_urls( input_url, getattr(course, 'data_dir', None), - course_id=course.location.course_id, + course_id=course.id, static_asset_path=course.static_asset_path ) # strip off the quotes again... @@ -73,8 +75,9 @@ def pdf_index(request, course_id, book_index, chapter=None, page=None): page: (optional) one-based page number to display within the PDF. Defaults to first page. """ - course = get_course_with_access(request.user, course_id, 'load') - staff_access = has_access(request.user, course, 'staff') + course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) + course = get_course_with_access(request.user, 'load', course_key) + staff_access = has_access(request.user, 'staff', course) book_index = int(book_index) if book_index < 0 or book_index >= len(course.pdf_textbooks): @@ -138,8 +141,9 @@ def html_index(request, course_id, book_index, chapter=None): Defaults to first chapter. Specifying this assumes that there are separate HTML files for each chapter in a textbook. """ - course = get_course_with_access(request.user, course_id, 'load') - staff_access = has_access(request.user, course, 'staff') + course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) + course = get_course_with_access(request.user, 'load', course_key) + staff_access = has_access(request.user, 'staff', course) notes_enabled = notes_enabled_for_course(course) book_index = int(book_index) diff --git a/lms/djangoapps/verify_student/tests/test_models.py b/lms/djangoapps/verify_student/tests/test_models.py index 4a3a3e39a9..8b7a0d9b27 100644 --- a/lms/djangoapps/verify_student/tests/test_models.py +++ b/lms/djangoapps/verify_student/tests/test_models.py @@ -2,6 +2,7 @@ from datetime import timedelta, datetime import json from xmodule.modulestore.tests.factories import CourseFactory +from xmodule.modulestore.locations import SlashSeparatedCourseKey from nose.tools import assert_is_none, assert_equals, assert_raises, assert_true, assert_false from mock import patch import pytz @@ -218,7 +219,7 @@ class TestPhotoVerification(TestCase): old_key = orig_attempt.photo_id_key window = MidcourseReverificationWindowFactory( - course_id="ponies", + course_id=SlashSeparatedCourseKey("pony", "rainbow", "dash"), start_date=datetime.now(pytz.utc) - timedelta(days=5), end_date=datetime.now(pytz.utc) + timedelta(days=5) ) @@ -422,30 +423,29 @@ class TestPhotoVerification(TestCase): class TestMidcourseReverification(TestCase): """ Tests for methods that are specific to midcourse SoftwareSecurePhotoVerification objects """ def setUp(self): - self.course_id = "MITx/999/Robot_Super_Course" self.course = CourseFactory.create(org='MITx', number='999', display_name='Robot Super Course') self.user = UserFactory.create() def test_user_is_reverified_for_all(self): # if there are no windows for a course, this should return True - self.assertTrue(SoftwareSecurePhotoVerification.user_is_reverified_for_all(self.course_id, self.user)) + self.assertTrue(SoftwareSecurePhotoVerification.user_is_reverified_for_all(self.course.id, self.user)) # first, make three windows window1 = MidcourseReverificationWindowFactory( - course_id=self.course_id, + course_id=self.course.id, start_date=datetime.now(pytz.UTC) - timedelta(days=15), end_date=datetime.now(pytz.UTC) - timedelta(days=13), ) window2 = MidcourseReverificationWindowFactory( - course_id=self.course_id, + course_id=self.course.id, start_date=datetime.now(pytz.UTC) - timedelta(days=10), end_date=datetime.now(pytz.UTC) - timedelta(days=8), ) window3 = MidcourseReverificationWindowFactory( - course_id=self.course_id, + course_id=self.course.id, start_date=datetime.now(pytz.UTC) - timedelta(days=5), end_date=datetime.now(pytz.UTC) - timedelta(days=3), ) @@ -466,7 +466,7 @@ class TestMidcourseReverification(TestCase): attempt2.save() # should return False because only 2 of 3 windows have verifications - self.assertFalse(SoftwareSecurePhotoVerification.user_is_reverified_for_all(self.course_id, self.user)) + self.assertFalse(SoftwareSecurePhotoVerification.user_is_reverified_for_all(self.course.id, self.user)) attempt3 = SoftwareSecurePhotoVerification( status="must_retry", @@ -476,19 +476,19 @@ class TestMidcourseReverification(TestCase): attempt3.save() # should return False because the last verification exists BUT is not approved - self.assertFalse(SoftwareSecurePhotoVerification.user_is_reverified_for_all(self.course_id, self.user)) + self.assertFalse(SoftwareSecurePhotoVerification.user_is_reverified_for_all(self.course.id, self.user)) attempt3.status = "approved" attempt3.save() # should now return True because all windows have approved verifications - self.assertTrue(SoftwareSecurePhotoVerification.user_is_reverified_for_all(self.course_id, self.user)) + self.assertTrue(SoftwareSecurePhotoVerification.user_is_reverified_for_all(self.course.id, self.user)) def test_original_verification(self): orig_attempt = SoftwareSecurePhotoVerification(user=self.user) orig_attempt.save() window = MidcourseReverificationWindowFactory( - course_id=self.course_id, + course_id=self.course.id, start_date=datetime.now(pytz.UTC) - timedelta(days=15), end_date=datetime.now(pytz.UTC) - timedelta(days=13), ) @@ -497,7 +497,7 @@ class TestMidcourseReverification(TestCase): def test_user_has_valid_or_pending(self): window = MidcourseReverificationWindowFactory( - course_id=self.course_id, + course_id=self.course.id, start_date=datetime.now(pytz.UTC) - timedelta(days=15), end_date=datetime.now(pytz.UTC) - timedelta(days=13), ) diff --git a/lms/djangoapps/verify_student/tests/test_views.py b/lms/djangoapps/verify_student/tests/test_views.py index 28186cfa31..88f46fab0d 100644 --- a/lms/djangoapps/verify_student/tests/test_views.py +++ b/lms/djangoapps/verify_student/tests/test_views.py @@ -24,6 +24,7 @@ from django.core.exceptions import ObjectDoesNotExist from mock import sentinel from xmodule.modulestore.tests.factories import CourseFactory +from xmodule.modulestore.locations import SlashSeparatedCourseKey from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE from student.tests.factories import UserFactory from student.models import CourseEnrollment @@ -61,9 +62,9 @@ class TestVerifyView(TestCase): def setUp(self): self.user = UserFactory.create(username="rusty", password="test") self.client.login(username="rusty", password="test") - self.course_id = 'Robot/999/Test_Course' + self.course_key = SlashSeparatedCourseKey('Robot', '999', 'Test_Course') CourseFactory.create(org='Robot', number='999', display_name='Test Course') - verified_mode = CourseMode(course_id=self.course_id, + verified_mode = CourseMode(course_id=self.course_key, mode_slug="verified", mode_display_name="Verified Certificate", min_price=50) @@ -87,8 +88,8 @@ class TestReverifyView(TestCase): def setUp(self): self.user = UserFactory.create(username="rusty", password="test") self.client.login(username="rusty", password="test") - self.course_id = "MITx/999/Robot_Super_Course" self.course = CourseFactory.create(org='MITx', number='999', display_name='Robot Super Course') + self.course_key = self.course.id @patch('verify_student.views.render_to_response', render_mock) def test_reverify_get(self): @@ -130,7 +131,7 @@ class TestMidCourseReverifyView(TestCase): def setUp(self): self.user = UserFactory.create(username="rusty", password="test") self.client.login(username="rusty", password="test") - self.course_id = 'Robot/999/Test_Course' + self.course_key = SlashSeparatedCourseKey("Robot", "999", "Test_Course") CourseFactory.create(org='Robot', number='999', display_name='Test Course') patcher = patch('student.models.server_track') @@ -145,7 +146,7 @@ class TestMidCourseReverifyView(TestCase): @patch('verify_student.views.render_to_response', render_mock) def test_midcourse_reverify_get(self): url = reverse('verify_student_midcourse_reverify', - kwargs={"course_id": self.course_id}) + kwargs={"course_id": self.course_key.to_deprecated_string()}) response = self.client.get(url) # Check that user entering the reverify flow was logged @@ -154,7 +155,7 @@ class TestMidCourseReverifyView(TestCase): 'edx.course.enrollment.reverify.started', { 'user_id': self.user.id, - 'course_id': self.course_id, + 'course_id': self.course_key.to_deprecated_string(), 'mode': "verified", } ) @@ -166,8 +167,8 @@ class TestMidCourseReverifyView(TestCase): @patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True}) def test_midcourse_reverify_post_success(self): - window = MidcourseReverificationWindowFactory(course_id=self.course_id) - url = reverse('verify_student_midcourse_reverify', kwargs={'course_id': self.course_id}) + window = MidcourseReverificationWindowFactory(course_id=self.course_key) + url = reverse('verify_student_midcourse_reverify', kwargs={'course_id': self.course_key.to_deprecated_string()}) response = self.client.post(url, {'face_image': ','}) @@ -177,7 +178,7 @@ class TestMidCourseReverifyView(TestCase): 'edx.course.enrollment.reverify.submitted', { 'user_id': self.user.id, - 'course_id': self.course_id, + 'course_id': self.course_key.to_deprecated_string(), 'mode': "verified", } ) @@ -193,11 +194,11 @@ class TestMidCourseReverifyView(TestCase): @patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True}) def test_midcourse_reverify_post_failure_expired_window(self): window = MidcourseReverificationWindowFactory( - course_id=self.course_id, + course_id=self.course_key, start_date=datetime.now(pytz.UTC) - timedelta(days=100), end_date=datetime.now(pytz.UTC) - timedelta(days=50), ) - url = reverse('verify_student_midcourse_reverify', kwargs={'course_id': self.course_id}) + url = reverse('verify_student_midcourse_reverify', kwargs={'course_id': self.course_key.to_deprecated_string()}) response = self.client.post(url, {'face_image': ','}) self.assertEquals(response.status_code, 302) with self.assertRaises(ObjectDoesNotExist): @@ -210,9 +211,9 @@ class TestMidCourseReverifyView(TestCase): # not enrolled in any courses self.assertEquals(response.status_code, 200) - enrollment = CourseEnrollment.get_or_create_enrollment(self.user, self.course_id) + enrollment = CourseEnrollment.get_or_create_enrollment(self.user, self.course_key) enrollment.update_enrollment(mode="verified", is_active=True) - MidcourseReverificationWindowFactory(course_id=self.course_id) + MidcourseReverificationWindowFactory(course_id=self.course_key) response = self.client.get(url) # enrolled in a verified course, and the window is open self.assertEquals(response.status_code, 200) diff --git a/lms/djangoapps/verify_student/views.py b/lms/djangoapps/verify_student/views.py index 865db0ef0e..4986da460c 100644 --- a/lms/djangoapps/verify_student/views.py +++ b/lms/djangoapps/verify_student/views.py @@ -34,6 +34,7 @@ from verify_student.models import ( from reverification.models import MidcourseReverificationWindow import ssencrypt from xmodule.modulestore.exceptions import ItemNotFoundError +from xmodule.modulestore.locations import SlashSeparatedCourseKey from .exceptions import WindowExpiredException log = logging.getLogger(__name__) @@ -55,12 +56,13 @@ class VerifyView(View): """ upgrade = request.GET.get('upgrade', False) + course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) # If the user has already been verified within the given time period, # redirect straight to the payment -- no need to verify again. 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_id.to_deprecated_string()}) + "?upgrade={}".format(upgrade) ) elif CourseEnrollment.enrollment_mode_for_user(request.user, course_id) == 'verified': return redirect(reverse('dashboard')) @@ -76,8 +78,8 @@ class VerifyView(View): # from the flow if not verify_mode: return redirect(reverse('dashboard')) - if course_id in request.session.get("donation_for_course", {}): - chosen_price = request.session["donation_for_course"][course_id] + if course_id.to_deprecated_string() in request.session.get("donation_for_course", {}): + chosen_price = request.session["donation_for_course"][course_id.to_deprecated_string()] else: chosen_price = verify_mode.min_price @@ -85,7 +87,8 @@ class VerifyView(View): context = { "progress_state": progress_state, "user_full_name": request.user.profile.name, - "course_id": course_id, + "course_id": course_id.to_deprecated_string(), + "course_modes_choose_url": reverse('course_modes_choose', kwargs={'course_id': course_id.to_deprecated_string()}), "course_name": course.display_name_with_default, "course_org": course.display_org_with_default, "course_num": course.display_number_with_default, @@ -114,23 +117,29 @@ class VerifiedView(View): Handle the case where we have a get request """ upgrade = request.GET.get('upgrade', False) + course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) if CourseEnrollment.enrollment_mode_for_user(request.user, course_id) == 'verified': return redirect(reverse('dashboard')) verify_mode = CourseMode.mode_for_course(course_id, "verified") - if course_id in request.session.get("donation_for_course", {}): - chosen_price = request.session["donation_for_course"][course_id] - else: - chosen_price = verify_mode.min_price.format("{:g}") + chosen_price = request.session.get( + "donation_for_course", + {} + ).get( + course_id.to_deprecated_string(), + verify_mode.min_price + ) course = course_from_id(course_id) context = { - "course_id": course_id, + "course_id": course_id.to_deprecated_string(), + "course_modes_choose_url": reverse('course_modes_choose', kwargs={'course_id': course_id.to_deprecated_string()}), "course_name": course.display_name_with_default, "course_org": course.display_org_with_default, "course_num": course.display_number_with_default, "purchase_endpoint": get_purchase_endpoint(), "currency": verify_mode.currency.upper(), "chosen_price": chosen_price, + "create_order_url": reverse("verify_student_create_order"), "upgrade": upgrade, } return render_to_response('verify_student/verified.html', context) @@ -153,6 +162,7 @@ def create_order(request): attempt.save() course_id = request.POST['course_id'] + course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) donation_for_course = request.session.get('donation_for_course', {}) current_donation = donation_for_course.get(course_id, decimal.Decimal(0)) contribution = request.POST.get("contribution", donation_for_course.get(course_id, 0)) @@ -256,7 +266,8 @@ def results_callback(request): # If this is a reverification, log an event if attempt.window: - course_id = window.course_id + course_id = attempt.window.course_id + course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) course = course_from_id(course_id) course_enrollment = CourseEnrollment.get_or_create_enrollment(attempt.user, course_id) course_enrollment.emit_event(EVENT_NAME_USER_REVERIFICATION_REVIEWED_BY_SOFTWARESECURE) @@ -269,13 +280,16 @@ def show_requirements(request, course_id): """ Show the requirements necessary for the verification flow. """ + course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) if CourseEnrollment.enrollment_mode_for_user(request.user, course_id) == 'verified': return redirect(reverse('dashboard')) upgrade = request.GET.get('upgrade', False) course = course_from_id(course_id) context = { - "course_id": course_id, + "course_id": course_id.to_deprecated_string(), + "course_modes_choose_url": reverse("course_modes_choose", kwargs={'course_id': course_id.to_deprecated_string()}), + "verify_student_url": reverse('verify_student_verify', kwargs={'course_id': course_id.to_deprecated_string()}), "course_name": course.display_name_with_default, "course_org": course.display_org_with_default, "course_num": course.display_number_with_default, @@ -354,6 +368,7 @@ class MidCourseReverifyView(View): """ display this view """ + course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) course = course_from_id(course_id) course_enrollment = CourseEnrollment.get_or_create_enrollment(request.user, course_id) course_enrollment.update_enrollment(mode="verified") @@ -361,7 +376,7 @@ class MidCourseReverifyView(View): context = { "user_full_name": request.user.profile.name, "error": False, - "course_id": course_id, + "course_id": course_id.to_deprecated_string(), "course_name": course.display_name_with_default, "course_org": course.display_org_with_default, "course_num": course.display_number_with_default, @@ -377,6 +392,7 @@ class MidCourseReverifyView(View): """ try: now = datetime.datetime.now(UTC) + course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) window = MidcourseReverificationWindow.get_window(course_id, now) if window is None: raise WindowExpiredException diff --git a/lms/lib/comment_client/utils.py b/lms/lib/comment_client/utils.py index 17fc198973..933ad62820 100644 --- a/lms/lib/comment_client/utils.py +++ b/lms/lib/comment_client/utils.py @@ -1,6 +1,5 @@ from contextlib import contextmanager from dogapi import dog_stats_api -import json import logging import requests from django.conf import settings diff --git a/lms/lib/xblock/runtime.py b/lms/lib/xblock/runtime.py index 895e8f2c74..5ecc92ca3c 100644 --- a/lms/lib/xblock/runtime.py +++ b/lms/lib/xblock/runtime.py @@ -86,8 +86,8 @@ class LmsHandlerUrls(object): view_name = 'xblock_handler_noauth' url = reverse(view_name, kwargs={ - 'course_id': self.course_id, - 'usage_id': quote_slashes(unicode(block.scope_ids.usage_id).encode('utf-8')), + 'course_id': self.course_id.to_deprecated_string(), + 'usage_id': quote_slashes(block.scope_ids.usage_id.to_deprecated_string().encode('utf-8')), 'handler': handler_name, 'suffix': suffix, }) diff --git a/lms/lib/xblock/test/test_runtime.py b/lms/lib/xblock/test/test_runtime.py index f4ec165fed..96a4f1df27 100644 --- a/lms/lib/xblock/test/test_runtime.py +++ b/lms/lib/xblock/test/test_runtime.py @@ -7,6 +7,7 @@ from ddt import ddt, data from mock import Mock from unittest import TestCase from urlparse import urlparse +from xmodule.modulestore.locations import SlashSeparatedCourseKey from lms.lib.xblock.runtime import quote_slashes, unquote_slashes, LmsModuleSystem TEST_STRINGS = [ @@ -41,14 +42,15 @@ class TestHandlerUrl(TestCase): def setUp(self): self.block = Mock() - self.course_id = "org/course/run" + self.block.scope_ids.usage_id.to_deprecated_string.return_value.encode.return_value = 'dummy' + self.course_key = SlashSeparatedCourseKey("org", "course", "run") self.runtime = LmsModuleSystem( static_url='/static', track_function=Mock(), get_module=Mock(), render_template=Mock(), replace_urls=str, - course_id=self.course_id, + course_id=self.course_key, descriptor_runtime=Mock(), ) @@ -92,7 +94,7 @@ class TestUserServiceAPI(TestCase): """Test the user service interface""" def setUp(self): - self.course_id = "org/course/run" + self.course_id = SlashSeparatedCourseKey("org", "course", "run") self.user = User(username='runtime_robot', email='runtime_robot@edx.org', password='test', first_name='Robot') self.user.save() diff --git a/lms/static/coffee/src/instructor_dashboard/student_admin.coffee b/lms/static/coffee/src/instructor_dashboard/student_admin.coffee index d930dd4b13..869a859c15 100644 --- a/lms/static/coffee/src/instructor_dashboard/student_admin.coffee +++ b/lms/static/coffee/src/instructor_dashboard/student_admin.coffee @@ -80,7 +80,7 @@ class StudentAdmin if not unique_student_identifier return @$request_response_error_grade.text gettext("Please enter a student email address or username.") if not problem_to_reset - return @$request_response_error_grade.text gettext("Please enter a problem urlname.") + return @$request_response_error_grade.text gettext("Please enter a problem location.") send_data = unique_student_identifier: unique_student_identifier problem_to_reset: problem_to_reset @@ -104,7 +104,7 @@ class StudentAdmin if not unique_student_identifier return @$request_response_error_grade.text gettext("Please enter a student email address or username.") if not problem_to_reset - return @$request_response_error_grade.text gettext("Please enter a problem urlname.") + return @$request_response_error_grade.text gettext("Please enter a problem location.") confirm_message = gettext("Delete student '<%= student_id %>'s state on problem '<%= problem_id %>'?") full_confirm_message = _.template(confirm_message, {student_id: unique_student_identifier, problem_id: problem_to_reset}) @@ -133,7 +133,7 @@ class StudentAdmin if not unique_student_identifier return @$request_response_error_grade.text gettext("Please enter a student email address or username.") if not problem_to_reset - return @$request_response_error_grade.text gettext("Please enter a problem urlname.") + return @$request_response_error_grade.text gettext("Please enter a problem location.") send_data = unique_student_identifier: unique_student_identifier problem_to_reset: problem_to_reset @@ -156,10 +156,10 @@ class StudentAdmin if not unique_student_identifier return @$request_response_error_grade.text gettext("Please enter a student email address or username.") if not problem_to_reset - return @$request_response_error_grade.text gettext("Please enter a problem urlname.") + return @$request_response_error_grade.text gettext("Please enter a problem location.") send_data = unique_student_identifier: unique_student_identifier - problem_urlname: problem_to_reset + problem_location_str: problem_to_reset error_message = gettext("Error getting task history for problem '<%= problem_id %>' and student '<%= student_id %>'. Check that the problem and student identifiers are spelled correctly.") full_error_message = _.template(error_message, {student_id: unique_student_identifier, problem_id: problem_to_reset}) @@ -175,7 +175,7 @@ class StudentAdmin @$btn_reset_attempts_all.click => problem_to_reset = @$field_problem_select_all.val() if not problem_to_reset - return @$request_response_error_all.text gettext("Please enter a problem urlname.") + return @$request_response_error_all.text gettext("Please enter a problem location.") confirm_message = gettext("Reset attempts for all students on problem '<%= problem_id %>'?") full_confirm_message = _.template(confirm_message, {problem_id: problem_to_reset}) if window.confirm full_confirm_message @@ -201,7 +201,7 @@ class StudentAdmin @$btn_rescore_problem_all.click => problem_to_reset = @$field_problem_select_all.val() if not problem_to_reset - return @$request_response_error_all.text gettext("Please enter a problem urlname.") + return @$request_response_error_all.text gettext("Please enter a problem location.") confirm_message = gettext("Rescore problem '<%= problem_id %>' for all students?") full_confirm_message = _.template(confirm_message, {problem_id: problem_to_reset}) if window.confirm full_confirm_message @@ -226,10 +226,10 @@ class StudentAdmin # list task history for problem @$btn_task_history_all.click => send_data = - problem_urlname: @$field_problem_select_all.val() + problem_location_str: @$field_problem_select_all.val() - if not send_data.problem_urlname - return @$request_response_error_all.text gettext("Please enter a problem urlname.") + if not send_data.problem_location_str + return @$request_response_error_all.text gettext("Please enter a problem location.") $.ajax dataType: 'json'