Commit cd746bf8 by Calen Pennington

Make course ids and usage ids opaque to LMS and Studio [partial commit]

This commit adds the non-courseware lms/djangoapps and lms/lib.

These keys are now objects with a limited interface, and the particular
internal representation is managed by the data storage layer (the
modulestore).

For the LMS, there should be no outward-facing changes to the system.
The keys are, for now, a change to internal representation only. For
Studio, the new serialized form of the keys is used in urls, to allow
for further migration in the future.

Co-Author: Andy Armstrong <andya@edx.org>
Co-Author: Christina Roberts <christina@edx.org>
Co-Author: David Baumgold <db@edx.org>
Co-Author: Diana Huang <dkh@edx.org>
Co-Author: Don Mitchell <dmitchell@edx.org>
Co-Author: Julia Hansbrough <julia@edx.org>
Co-Author: Nimisha Asthagiri <nasthagiri@edx.org>
Co-Author: Sarina Canelake <sarina@edx.org>

[LMS-2370]
parent 7852906c
...@@ -70,7 +70,7 @@ def dump_grading_context(course): ...@@ -70,7 +70,7 @@ def dump_grading_context(course):
subgrader.index = 1 subgrader.index = 1
graders[subgrader.type] = subgrader graders[subgrader.type] = subgrader
msg += hbar 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 gcontext = course.grading_context
msg += "graded sections:\n" msg += "graded sections:\n"
......
...@@ -5,6 +5,7 @@ Tests for instructor.basic ...@@ -5,6 +5,7 @@ Tests for instructor.basic
from django.test import TestCase from django.test import TestCase
from student.models import CourseEnrollment from student.models import CourseEnrollment
from student.tests.factories import UserFactory 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 from analytics.basic import enrolled_students_features, AVAILABLE_FEATURES, STUDENT_FEATURES, PROFILE_FEATURES
...@@ -13,14 +14,14 @@ class TestAnalyticsBasic(TestCase): ...@@ -13,14 +14,14 @@ class TestAnalyticsBasic(TestCase):
""" Test basic analytics functions. """ """ Test basic analytics functions. """
def setUp(self): 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.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) for user in self.users)
def test_enrolled_students_features_username(self): def test_enrolled_students_features_username(self):
self.assertIn('username', AVAILABLE_FEATURES) 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)) self.assertEqual(len(userreports), len(self.users))
for userreport in userreports: for userreport in userreports:
self.assertEqual(userreport.keys(), ['username']) self.assertEqual(userreport.keys(), ['username'])
...@@ -30,7 +31,7 @@ class TestAnalyticsBasic(TestCase): ...@@ -30,7 +31,7 @@ class TestAnalyticsBasic(TestCase):
query_features = ('username', 'name', 'email') query_features = ('username', 'name', 'email')
for feature in query_features: for feature in query_features:
self.assertIn(feature, AVAILABLE_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)) self.assertEqual(len(userreports), len(self.users))
for userreport in userreports: for userreport in userreports:
self.assertEqual(set(userreport.keys()), set(query_features)) self.assertEqual(set(userreport.keys()), set(query_features))
......
...@@ -4,6 +4,7 @@ from django.test import TestCase ...@@ -4,6 +4,7 @@ from django.test import TestCase
from nose.tools import raises from nose.tools import raises
from student.models import CourseEnrollment from student.models import CourseEnrollment
from student.tests.factories import UserFactory from student.tests.factories import UserFactory
from xmodule.modulestore.locations import SlashSeparatedCourseKey
from analytics.distributions import profile_distribution, AVAILABLE_PROFILE_FEATURES from analytics.distributions import profile_distribution, AVAILABLE_PROFILE_FEATURES
...@@ -12,7 +13,7 @@ class TestAnalyticsDistributions(TestCase): ...@@ -12,7 +13,7 @@ class TestAnalyticsDistributions(TestCase):
'''Test analytics distribution gathering.''' '''Test analytics distribution gathering.'''
def setUp(self): def setUp(self):
self.course_id = 'some/robot/course/id' self.course_id = SlashSeparatedCourseKey('robot', 'course', 'id')
self.users = [UserFactory( self.users = [UserFactory(
profile__gender=['m', 'f', 'o'][i % 3], profile__gender=['m', 'f', 'o'][i % 3],
...@@ -53,7 +54,7 @@ class TestAnalyticsDistributionsNoData(TestCase): ...@@ -53,7 +54,7 @@ class TestAnalyticsDistributionsNoData(TestCase):
'''Test analytics distribution gathering.''' '''Test analytics distribution gathering.'''
def setUp(self): def setUp(self):
self.course_id = 'some/robot/course/id' self.course_id = SlashSeparatedCourseKey('robot', 'course', 'id')
self.users = [UserFactory( self.users = [UserFactory(
profile__year_of_birth=i + 1930, profile__year_of_birth=i + 1930,
......
...@@ -8,9 +8,10 @@ from django.core.exceptions import ValidationError ...@@ -8,9 +8,10 @@ from django.core.exceptions import ValidationError
from bulk_email.models import CourseEmailTemplate, COURSE_EMAIL_MESSAGE_BODY_TAG, CourseAuthorization 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 import XML_MODULESTORE_TYPE
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.locations import SlashSeparatedCourseKey
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -57,22 +58,26 @@ class CourseAuthorizationAdminForm(forms.ModelForm): # pylint: disable=R0924 ...@@ -57,22 +58,26 @@ class CourseAuthorizationAdminForm(forms.ModelForm): # pylint: disable=R0924
def clean_course_id(self): def clean_course_id(self):
"""Validate the course id""" """Validate the course id"""
course_id = self.cleaned_data["course_id"] cleaned_id = self.cleaned_data["course_id"]
try: try:
# Just try to get the course descriptor. course_id = SlashSeparatedCourseKey.from_deprecated_string(cleaned_id)
# If we can do that, it's a real course. except InvalidKeyError:
get_course_by_id(course_id, depth=1) msg = u'Course id invalid.'
except Exception as exc: msg += u' --- Entered course id was: "{0}". '.format(cleaned_id)
msg = 'Error encountered ({0})'.format(str(exc).capitalize()) msg += 'Please recheck that you have supplied a valid course id.'
msg += u' --- Entered course id was: "{0}". '.format(course_id) raise forms.ValidationError(msg)
msg += 'Please recheck that you have supplied a course id in the format: ORG/COURSE/RUN'
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) raise forms.ValidationError(msg)
# Now, try and discern if it is a Studio course - HTML editor doesn't work with XML courses # 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 is_studio_course = modulestore().get_modulestore_type(course_id) != XML_MODULESTORE_TYPE
if not is_studio_course: if not is_studio_course:
msg = "Course Email feature is only available for courses authored in Studio. " 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) raise forms.ValidationError(msg)
return course_id return course_id.to_deprecated_string()
...@@ -19,6 +19,8 @@ from django.db import models, transaction ...@@ -19,6 +19,8 @@ from django.db import models, transaction
from html_to_text import html_to_text from html_to_text import html_to_text
from mail_utils import wrap_message from mail_utils import wrap_message
from xmodule_django.models import CourseKeyField
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
# Bulk email to_options - the send to options that users can # Bulk email to_options - the send to options that users can
...@@ -63,7 +65,7 @@ class CourseEmail(Email): ...@@ -63,7 +65,7 @@ class CourseEmail(Email):
(SEND_TO_STAFF, 'Staff and instructors'), (SEND_TO_STAFF, 'Staff and instructors'),
(SEND_TO_ALL, 'All') (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) to_option = models.CharField(max_length=64, choices=TO_OPTION_CHOICES, default=SEND_TO_MYSELF)
def __unicode__(self): def __unicode__(self):
...@@ -127,7 +129,7 @@ class Optout(models.Model): ...@@ -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, # 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. # and given the unique index, 'null' is the best default value.
user = models.ForeignKey(User, db_index=True, null=True) 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 class Meta: # pylint: disable=C0111
unique_together = ('user', 'course_id') unique_together = ('user', 'course_id')
...@@ -220,7 +222,7 @@ class CourseAuthorization(models.Model): ...@@ -220,7 +222,7 @@ class CourseAuthorization(models.Model):
Enable the course email feature on a course-by-course basis. Enable the course email feature on a course-by-course basis.
""" """
# The course that these features are attached to. # 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 # Whether or not to enable instructor email
email_enabled = models.BooleanField(default=False) email_enabled = models.BooleanField(default=False)
...@@ -247,4 +249,5 @@ class CourseAuthorization(models.Model): ...@@ -247,4 +249,5 @@ class CourseAuthorization(models.Model):
not_en = "Not " not_en = "Not "
if self.email_enabled: if self.email_enabled:
not_en = "" 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)
...@@ -107,8 +107,8 @@ def _get_recipient_queryset(user_id, to_option, course_id, course_location): ...@@ -107,8 +107,8 @@ def _get_recipient_queryset(user_id, to_option, course_id, course_location):
if to_option == SEND_TO_MYSELF: if to_option == SEND_TO_MYSELF:
recipient_qset = User.objects.filter(id=user_id) recipient_qset = User.objects.filter(id=user_id)
else: else:
staff_qset = CourseStaffRole(course_location).users_with_role() staff_qset = CourseStaffRole(course_id).users_with_role()
instructor_qset = CourseInstructorRole(course_location).users_with_role() instructor_qset = CourseInstructorRole(course_id).users_with_role()
recipient_qset = staff_qset | instructor_qset recipient_qset = staff_qset | instructor_qset
if to_option == SEND_TO_ALL: if to_option == SEND_TO_ALL:
# We also require students to have activated their accounts to # We also require students to have activated their accounts to
...@@ -129,7 +129,7 @@ def _get_course_email_context(course): ...@@ -129,7 +129,7 @@ def _get_course_email_context(course):
""" """
Returns context arguments to apply to all emails, independent of recipient. 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_title = course.display_name
course_url = 'https://{}{}'.format( course_url = 'https://{}{}'.format(
settings.SITE_NAME, settings.SITE_NAME,
...@@ -160,9 +160,9 @@ def perform_delegate_email_batches(entry_id, course_id, task_input, action_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 # Perfunctory check, since expansion is made for convenience of other task
# code that doesn't need the entry_id. # code that doesn't need the entry_id.
if course_id != entry.course_id: if course_id != entry.course_id:
format_msg = u"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: %s", task_id, format_msg.format(course_id, entry.course_id)) log.warning("Task %s: " + format_msg, task_id, course_id, entry.course_id)
raise ValueError("Course id conflict: explicit value does not match task value") raise ValueError(format_msg % (course_id, entry.course_id))
# Fetch the CourseEmail. # Fetch the CourseEmail.
email_id = task_input['email_id'] email_id = task_input['email_id']
...@@ -188,16 +188,17 @@ def perform_delegate_email_batches(entry_id, course_id, task_input, action_name) ...@@ -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. # Sanity check that course for email_obj matches that of the task referencing it.
if course_id != email_obj.course_id: if course_id != email_obj.course_id:
format_msg = u"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: %s", task_id, format_msg.format(course_id, entry.course_id)) log.warning("Task %s: " + format_msg, task_id, course_id, email_obj.course_id)
raise ValueError("Course id conflict: explicit value does not match email value") raise ValueError(format_msg % (course_id, email_obj.course_id))
# Fetch the course object. # Fetch the course object.
try: course = get_course(course_id)
course = get_course(course_id)
except ValueError: if course is None:
log.exception("Task %s: course not found: %s", task_id, course_id) msg = "Task %s: course not found: %s"
raise log.error(msg, task_id, course_id)
raise ValueError(msg % (task_id, course_id))
# Get arguments that will be passed to every subtask. # Get arguments that will be passed to every subtask.
to_option = email_obj.to_option to_option = email_obj.to_option
...@@ -369,15 +370,14 @@ def _get_source_address(course_id, course_title): ...@@ -369,15 +370,14 @@ def _get_source_address(course_id, course_title):
""" """
course_title_no_quotes = re.sub(r'"', '', 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', # For the email address, get the course. Then make sure that it can be used
# so pull out the course_num. Then make sure that it can be used
# in an email address, by substituting a '_' anywhere a non-(ascii, period, or dash) # in an email address, by substituting a '_' anywhere a non-(ascii, period, or dash)
# character appears. # character appears.
course_num = Location.parse_course_id(course_id)['course'] from_addr = u'"{0}" Course Staff <{1}-{2}>'.format(
invalid_chars = re.compile(r"[^\w.-]") course_title_no_quotes,
course_num = invalid_chars.sub('_', course_num) re.sub(r"[^\w.-]", '_', course_id.course),
settings.BULK_EMAIL_DEFAULT_FROM_EMAIL
from_addr = u'"{0}" Course Staff <{1}-{2}>'.format(course_title_no_quotes, course_num, settings.BULK_EMAIL_DEFAULT_FROM_EMAIL) )
return from_addr return from_addr
......
...@@ -47,7 +47,7 @@ class TestOptoutCourseEmails(ModuleStoreTestCase): ...@@ -47,7 +47,7 @@ class TestOptoutCourseEmails(ModuleStoreTestCase):
def navigate_to_email_view(self): def navigate_to_email_view(self):
"""Navigate to the instructor dash's email view""" """Navigate to the instructor dash's email view"""
# Pull up email view on instructor dashboard # 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) response = self.client.get(url)
email_link = '<a href="#" onclick="goto(\'Email\')" class="None">Email</a>' email_link = '<a href="#" onclick="goto(\'Email\')" class="None">Email</a>'
# If this fails, it is likely because ENABLE_INSTRUCTOR_EMAIL is set to False # If this fails, it is likely because ENABLE_INSTRUCTOR_EMAIL is set to False
...@@ -69,7 +69,7 @@ class TestOptoutCourseEmails(ModuleStoreTestCase): ...@@ -69,7 +69,7 @@ class TestOptoutCourseEmails(ModuleStoreTestCase):
url = reverse('change_email_settings') url = reverse('change_email_settings')
# This is a checkbox, so on the post of opting out (that is, an Un-check of the box), # 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' # 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.assertEquals(json.loads(response.content), {'success': True})
self.client.logout() self.client.logout()
...@@ -77,7 +77,7 @@ class TestOptoutCourseEmails(ModuleStoreTestCase): ...@@ -77,7 +77,7 @@ class TestOptoutCourseEmails(ModuleStoreTestCase):
self.client.login(username=self.instructor.username, password="test") self.client.login(username=self.instructor.username, password="test")
self.navigate_to_email_view() 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 = { test_email = {
'action': 'Send email', 'action': 'Send email',
'to_option': 'all', 'to_option': 'all',
...@@ -96,7 +96,7 @@ class TestOptoutCourseEmails(ModuleStoreTestCase): ...@@ -96,7 +96,7 @@ class TestOptoutCourseEmails(ModuleStoreTestCase):
Make sure student receives course email after opting in. Make sure student receives course email after opting in.
""" """
url = reverse('change_email_settings') 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.assertEquals(json.loads(response.content), {'success': True})
self.client.logout() self.client.logout()
...@@ -106,7 +106,7 @@ class TestOptoutCourseEmails(ModuleStoreTestCase): ...@@ -106,7 +106,7 @@ class TestOptoutCourseEmails(ModuleStoreTestCase):
self.client.login(username=self.instructor.username, password="test") self.client.login(username=self.instructor.username, password="test")
self.navigate_to_email_view() 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 = { test_email = {
'action': 'Send email', 'action': 'Send email',
'to_option': 'all', 'to_option': 'all',
......
...@@ -51,10 +51,10 @@ class TestEmailSendFromDashboard(ModuleStoreTestCase): ...@@ -51,10 +51,10 @@ class TestEmailSendFromDashboard(ModuleStoreTestCase):
course_title = u"ẗëṡẗ title イ乇丂イ ᄊ乇丂丂ムg乇 キo尺 ムレレ тэѕт мэѕѕаБэ" course_title = u"ẗëṡẗ title イ乇丂イ ᄊ乇丂丂ムg乇 キo尺 ムレレ тэѕт мэѕѕаБэ"
self.course = CourseFactory.create(display_name=course_title) self.course = CourseFactory.create(display_name=course_title)
self.instructor = InstructorFactory(course=self.course.location) self.instructor = InstructorFactory(course=self.course.id)
# Create staff # Create staff
self.staff = [StaffFactory(course=self.course.location) self.staff = [StaffFactory(course=self.course.id)
for _ in xrange(STAFF_COUNT)] for _ in xrange(STAFF_COUNT)]
# Create students # Create students
...@@ -68,7 +68,7 @@ class TestEmailSendFromDashboard(ModuleStoreTestCase): ...@@ -68,7 +68,7 @@ class TestEmailSendFromDashboard(ModuleStoreTestCase):
self.client.login(username=self.instructor.username, password="test") self.client.login(username=self.instructor.username, password="test")
# Pull up email view on instructor dashboard # 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) response = self.client.get(self.url)
email_link = '<a href="#" onclick="goto(\'Email\')" class="None">Email</a>' email_link = '<a href="#" onclick="goto(\'Email\')" class="None">Email</a>'
# If this fails, it is likely because ENABLE_INSTRUCTOR_EMAIL is set to False # If this fails, it is likely because ENABLE_INSTRUCTOR_EMAIL is set to False
......
...@@ -17,6 +17,7 @@ from django.db import DatabaseError ...@@ -17,6 +17,7 @@ from django.db import DatabaseError
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.locations import SlashSeparatedCourseKey
from student.tests.factories import UserFactory, AdminFactory, CourseEnrollmentFactory from student.tests.factories import UserFactory, AdminFactory, CourseEnrollmentFactory
from bulk_email.models import CourseEmail, SEND_TO_ALL from bulk_email.models import CourseEmail, SEND_TO_ALL
...@@ -51,7 +52,7 @@ class TestEmailErrors(ModuleStoreTestCase): ...@@ -51,7 +52,7 @@ class TestEmailErrors(ModuleStoreTestCase):
# load initial content (since we don't run migrations as part of tests): # load initial content (since we don't run migrations as part of tests):
call_command("loaddata", "course_email_template.json") 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): def tearDown(self):
patch.stopall() patch.stopall()
...@@ -171,12 +172,13 @@ class TestEmailErrors(ModuleStoreTestCase): ...@@ -171,12 +172,13 @@ class TestEmailErrors(ModuleStoreTestCase):
""" """
Tests exception when the course in the email doesn't exist 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 = CourseEmail(course_id=course_id)
email.save() email.save()
entry = InstructorTask.create(course_id, "task_type", "task_key", "task_input", self.instructor) entry = InstructorTask.create(course_id, "task_type", "task_key", "task_input", self.instructor)
task_input = {"email_id": email.id} # pylint: disable=E1101 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 perform_delegate_email_batches(entry.id, course_id, task_input, "action_name") # pylint: disable=E1101
def test_nonexistent_to_option(self): def test_nonexistent_to_option(self):
...@@ -205,7 +207,7 @@ class TestEmailErrors(ModuleStoreTestCase): ...@@ -205,7 +207,7 @@ class TestEmailErrors(ModuleStoreTestCase):
""" """
Tests exception when the course_id in CourseEmail is not the same as one explicitly passed in. 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() email.save()
entry = InstructorTask.create(self.course.id, "task_type", "task_key", "task_input", self.instructor) entry = InstructorTask.create(self.course.id, "task_type", "task_key", "task_input", self.instructor)
task_input = {"email_id": email.id} # pylint: disable=E1101 task_input = {"email_id": email.id} # pylint: disable=E1101
......
...@@ -11,12 +11,13 @@ from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE ...@@ -11,12 +11,13 @@ from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE
from xmodule.modulestore.django import 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 mock import patch
from bulk_email.models import CourseAuthorization from bulk_email.models import CourseAuthorization
from bulk_email.forms import CourseAuthorizationAdminForm from bulk_email.forms import CourseAuthorizationAdminForm
from xmodule.modulestore.locations import SlashSeparatedCourseKey
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) @override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
...@@ -38,7 +39,7 @@ class CourseAuthorizationFormTest(ModuleStoreTestCase): ...@@ -38,7 +39,7 @@ class CourseAuthorizationFormTest(ModuleStoreTestCase):
# Initially course shouldn't be authorized # Initially course shouldn't be authorized
self.assertFalse(CourseAuthorization.instructor_email_enabled(self.course.id)) self.assertFalse(CourseAuthorization.instructor_email_enabled(self.course.id))
# Test authorizing the course, which should totally work # 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) form = CourseAuthorizationAdminForm(data=form_data)
# Validation should work # Validation should work
self.assertTrue(form.is_valid()) self.assertTrue(form.is_valid())
...@@ -51,7 +52,7 @@ class CourseAuthorizationFormTest(ModuleStoreTestCase): ...@@ -51,7 +52,7 @@ class CourseAuthorizationFormTest(ModuleStoreTestCase):
# Initially course shouldn't be authorized # Initially course shouldn't be authorized
self.assertFalse(CourseAuthorization.instructor_email_enabled(self.course.id)) self.assertFalse(CourseAuthorization.instructor_email_enabled(self.course.id))
# Test authorizing the course, which should totally work # 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) form = CourseAuthorizationAdminForm(data=form_data)
# Validation should work # Validation should work
self.assertTrue(form.is_valid()) self.assertTrue(form.is_valid())
...@@ -60,7 +61,7 @@ class CourseAuthorizationFormTest(ModuleStoreTestCase): ...@@ -60,7 +61,7 @@ class CourseAuthorizationFormTest(ModuleStoreTestCase):
self.assertTrue(CourseAuthorization.instructor_email_enabled(self.course.id)) 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 # 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) form = CourseAuthorizationAdminForm(data=form_data)
# Validation should not work because course_id field is unique # Validation should not work because course_id field is unique
self.assertFalse(form.is_valid()) self.assertFalse(form.is_valid())
...@@ -77,16 +78,31 @@ class CourseAuthorizationFormTest(ModuleStoreTestCase): ...@@ -77,16 +78,31 @@ class CourseAuthorizationFormTest(ModuleStoreTestCase):
@patch.dict(settings.FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True, 'REQUIRE_COURSE_EMAIL_AUTH': True}) @patch.dict(settings.FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True, 'REQUIRE_COURSE_EMAIL_AUTH': True})
def test_form_typo(self): def test_form_typo(self):
# Munge course id # 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) form = CourseAuthorizationAdminForm(data=form_data)
# Validation shouldn't work # Validation shouldn't work
self.assertFalse(form.is_valid()) self.assertFalse(form.is_valid())
msg = u'Error encountered (Course not found.)' msg = u'COURSE NOT FOUND'
msg += u' --- Entered course id was: "{0}". '.format(bad_id) msg += u' --- Entered course id was: "{0}". '.format(bad_id.to_deprecated_string())
msg += 'Please recheck that you have supplied a course id in the format: ORG/COURSE/RUN' 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 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."): with self.assertRaisesRegexp(ValueError, "The CourseAuthorization could not be created because the data didn't validate."):
...@@ -95,16 +111,14 @@ class CourseAuthorizationFormTest(ModuleStoreTestCase): ...@@ -95,16 +111,14 @@ class CourseAuthorizationFormTest(ModuleStoreTestCase):
@patch.dict(settings.FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True, 'REQUIRE_COURSE_EMAIL_AUTH': True}) @patch.dict(settings.FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True, 'REQUIRE_COURSE_EMAIL_AUTH': True})
def test_course_name_only(self): def test_course_name_only(self):
# Munge course id - common # Munge course id - common
bad_id = Location.parse_course_id(self.course.id)['name'] form_data = {'course_id': self.course.id.run, 'email_enabled': True}
form_data = {'course_id': bad_id, 'email_enabled': True}
form = CourseAuthorizationAdminForm(data=form_data) form = CourseAuthorizationAdminForm(data=form_data)
# Validation shouldn't work # Validation shouldn't work
self.assertFalse(form.is_valid()) self.assertFalse(form.is_valid())
error_msg = form._errors['course_id'][0] error_msg = form._errors['course_id'][0]
self.assertIn(u'--- Entered course id was: "{0}". '.format(bad_id), 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 course id in the format: ORG/COURSE/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."): with self.assertRaisesRegexp(ValueError, "The CourseAuthorization could not be created because the data didn't validate."):
form.save() form.save()
...@@ -116,17 +130,17 @@ class CourseAuthorizationXMLFormTest(ModuleStoreTestCase): ...@@ -116,17 +130,17 @@ class CourseAuthorizationXMLFormTest(ModuleStoreTestCase):
@patch.dict(settings.FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True, 'REQUIRE_COURSE_EMAIL_AUTH': True}) @patch.dict(settings.FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True, 'REQUIRE_COURSE_EMAIL_AUTH': True})
def test_xml_course_authorization(self): 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 # Assert this is an XML course
self.assertEqual(modulestore().get_modulestore_type(course_id), XML_MODULESTORE_TYPE) 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) form = CourseAuthorizationAdminForm(data=form_data)
# Validation shouldn't work # Validation shouldn't work
self.assertFalse(form.is_valid()) self.assertFalse(form.is_valid())
msg = u"Course Email feature is only available for courses authored in Studio. " 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 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."): with self.assertRaisesRegexp(ValueError, "The CourseAuthorization could not be created because the data didn't validate."):
......
...@@ -10,13 +10,14 @@ from student.tests.factories import UserFactory ...@@ -10,13 +10,14 @@ from student.tests.factories import UserFactory
from mock import patch from mock import patch
from bulk_email.models import CourseEmail, SEND_TO_STAFF, CourseEmailTemplate, CourseAuthorization from bulk_email.models import CourseEmail, SEND_TO_STAFF, CourseEmailTemplate, CourseAuthorization
from xmodule.modulestore.locations import SlashSeparatedCourseKey
class CourseEmailTest(TestCase): class CourseEmailTest(TestCase):
"""Test the CourseEmail model.""" """Test the CourseEmail model."""
def test_creation(self): def test_creation(self):
course_id = 'abc/123/doremi' course_id = SlashSeparatedCourseKey('abc', '123', 'doremi')
sender = UserFactory.create() sender = UserFactory.create()
to_option = SEND_TO_STAFF to_option = SEND_TO_STAFF
subject = "dummy subject" subject = "dummy subject"
...@@ -29,7 +30,7 @@ class CourseEmailTest(TestCase): ...@@ -29,7 +30,7 @@ class CourseEmailTest(TestCase):
self.assertEquals(email.sender, sender) self.assertEquals(email.sender, sender)
def test_bad_to_option(self): def test_bad_to_option(self):
course_id = 'abc/123/doremi' course_id = SlashSeparatedCourseKey('abc', '123', 'doremi')
sender = UserFactory.create() sender = UserFactory.create()
to_option = "fake" to_option = "fake"
subject = "dummy subject" subject = "dummy subject"
...@@ -109,7 +110,7 @@ class CourseAuthorizationTest(TestCase): ...@@ -109,7 +110,7 @@ class CourseAuthorizationTest(TestCase):
@patch.dict(settings.FEATURES, {'REQUIRE_COURSE_EMAIL_AUTH': True}) @patch.dict(settings.FEATURES, {'REQUIRE_COURSE_EMAIL_AUTH': True})
def test_creation_auth_on(self): 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 # Test that course is not authorized by default
self.assertFalse(CourseAuthorization.instructor_email_enabled(course_id)) self.assertFalse(CourseAuthorization.instructor_email_enabled(course_id))
...@@ -135,7 +136,7 @@ class CourseAuthorizationTest(TestCase): ...@@ -135,7 +136,7 @@ class CourseAuthorizationTest(TestCase):
@patch.dict(settings.FEATURES, {'REQUIRE_COURSE_EMAIL_AUTH': False}) @patch.dict(settings.FEATURES, {'REQUIRE_COURSE_EMAIL_AUTH': False})
def test_creation_auth_off(self): 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 # Test that course is authorized by default, since auth is turned off
self.assertTrue(CourseAuthorization.instructor_email_enabled(course_id)) self.assertTrue(CourseAuthorization.instructor_email_enabled(course_id))
......
...@@ -35,6 +35,7 @@ from instructor_task.subtasks import update_subtask_status, SubtaskStatus ...@@ -35,6 +35,7 @@ from instructor_task.subtasks import update_subtask_status, SubtaskStatus
from instructor_task.models import InstructorTask from instructor_task.models import InstructorTask
from instructor_task.tests.test_base import InstructorTaskCourseTestCase from instructor_task.tests.test_base import InstructorTaskCourseTestCase
from instructor_task.tests.factories import InstructorTaskFactory from instructor_task.tests.factories import InstructorTaskFactory
from xmodule.modulestore.locations import SlashSeparatedCourseKey
class TestTaskFailure(Exception): class TestTaskFailure(Exception):
...@@ -119,7 +120,7 @@ class TestBulkEmailInstructorTask(InstructorTaskCourseTestCase): ...@@ -119,7 +120,7 @@ class TestBulkEmailInstructorTask(InstructorTaskCourseTestCase):
def test_email_undefined_course(self): def test_email_undefined_course(self):
# Check that we fail when passing in a course that doesn't exist. # 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): with self.assertRaises(ValueError):
self._run_task_with_mock_celery(send_bulk_course_email, task_entry.id, task_entry.task_id) self._run_task_with_mock_celery(send_bulk_course_email, task_entry.id, task_entry.task_id)
......
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from optparse import make_option 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 certificates.models import CertificateWhitelist
from django.contrib.auth.models import User from django.contrib.auth.models import User
...@@ -48,6 +51,14 @@ class Command(BaseCommand): ...@@ -48,6 +51,14 @@ class Command(BaseCommand):
course_id = options['course_id'] course_id = options['course_id']
if not course_id: if not course_id:
raise CommandError("You must specify a 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']: if options['add'] and options['del']:
raise CommandError("Either remove or add a user, not both") raise CommandError("Either remove or add a user, not both")
...@@ -60,14 +71,14 @@ class Command(BaseCommand): ...@@ -60,14 +71,14 @@ class Command(BaseCommand):
cert_whitelist, created = \ cert_whitelist, created = \
CertificateWhitelist.objects.get_or_create( CertificateWhitelist.objects.get_or_create(
user=user, course_id=course_id) user=user, course_id=course)
if options['add']: if options['add']:
cert_whitelist.whitelist = True cert_whitelist.whitelist = True
elif options['del']: elif options['del']:
cert_whitelist.whitelist = False cert_whitelist.whitelist = False
cert_whitelist.save() 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, print "User whitelist for course {0}:\n{1}".format(course_id,
'\n'.join(["{0} {1} {2}".format( '\n'.join(["{0} {1} {2}".format(
u.user.username, u.user.email, u.whitelist) u.user.username, u.user.email, u.whitelist)
......
...@@ -40,8 +40,7 @@ class Command(BaseCommand): ...@@ -40,8 +40,7 @@ class Command(BaseCommand):
for course_id in [course # all courses in COURSE_LISTINGS for course_id in [course # all courses in COURSE_LISTINGS
for sub in settings.COURSE_LISTINGS for sub in settings.COURSE_LISTINGS
for course in settings.COURSE_LISTINGS[sub]]: for course in settings.COURSE_LISTINGS[sub]]:
course_loc = CourseDescriptor.id_to_location(course_id) course = modulestore().get_course(course_id)
course = modulestore().get_instance(course_id, course_loc)
if course.has_ended(): if course.has_ended():
yield course_id yield course_id
......
...@@ -62,7 +62,7 @@ class Command(BaseCommand): ...@@ -62,7 +62,7 @@ class Command(BaseCommand):
student = User.objects.get(username=user, courseenrollment__course_id=course_id) student = User.objects.get(username=user, courseenrollment__course_id=course_id)
print "Fetching course data for {0}".format(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']: if not options['noop']:
# Add the certificate request to the queue # Add the certificate request to the queue
......
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.models import certificate_status_for_student
from certificates.queue import XQueueCertInterface from certificates.queue import XQueueCertInterface
from django.contrib.auth.models import User from django.contrib.auth.models import User
from optparse import make_option from optparse import make_option
from django.conf import settings 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.course_module import CourseDescriptor
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from certificates.models import CertificateStatuses from certificates.models import CertificateStatuses
...@@ -66,25 +69,23 @@ class Command(BaseCommand): ...@@ -66,25 +69,23 @@ class Command(BaseCommand):
STATUS_INTERVAL = 500 STATUS_INTERVAL = 500
if options['course']: 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: else:
# Find all courses that have ended raise CommandError("You must specify a course")
ended_courses = []
for course_id in [course # all courses in COURSE_LISTINGS for course_key in ended_courses:
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)
for course_id in ended_courses:
# prefetch all chapters/sequentials by saying depth=2 # 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( enrolled_students = User.objects.filter(
courseenrollment__course_id=course_id) courseenrollment__course_id=course_key)
xq = XQueueCertInterface() xq = XQueueCertInterface()
if options['insecure']: if options['insecure']:
...@@ -108,9 +109,9 @@ class Command(BaseCommand): ...@@ -108,9 +109,9 @@ class Command(BaseCommand):
start = datetime.datetime.now(UTC) start = datetime.datetime.now(UTC)
if certificate_status_for_student( if certificate_status_for_student(
student, course_id)['status'] in valid_statuses: student, course_key)['status'] in valid_statuses:
if not options['noop']: if not options['noop']:
# Add the certificate request to the queue # 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': if ret == 'generating':
print '{0} - {1}'.format(student, ret) print '{0} - {1}'.format(student, ret)
...@@ -2,6 +2,7 @@ from django.contrib.auth.models import User ...@@ -2,6 +2,7 @@ from django.contrib.auth.models import User
from django.db import models from django.db import models
from datetime import datetime from datetime import datetime
from model_utils import Choices from model_utils import Choices
from xmodule_django.models import CourseKeyField, NoneToEmptyManager
""" """
Certificates are created for a student and an offering of a course. Certificates are created for a student and an offering of a course.
...@@ -71,14 +72,17 @@ class CertificateWhitelist(models.Model): ...@@ -71,14 +72,17 @@ class CertificateWhitelist(models.Model):
embargoed country restriction list embargoed country restriction list
(allow_certificate set to False in userprofile). (allow_certificate set to False in userprofile).
""" """
objects = NoneToEmptyManager()
user = models.ForeignKey(User) 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) whitelist = models.BooleanField(default=0)
class GeneratedCertificate(models.Model): class GeneratedCertificate(models.Model):
user = models.ForeignKey(User) 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='') verify_uuid = models.CharField(max_length=32, blank=True, default='')
download_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='') download_url = models.CharField(max_length=128, blank=True, default='')
......
...@@ -130,7 +130,7 @@ class XQueueCertInterface(object): ...@@ -130,7 +130,7 @@ class XQueueCertInterface(object):
Arguments: Arguments:
student - User.object 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 forced_grade - a string indicating a grade parameter to pass with
the certificate request. If this is given, grading the certificate request. If this is given, grading
will be skipped. will be skipped.
...@@ -181,16 +181,15 @@ class XQueueCertInterface(object): ...@@ -181,16 +181,15 @@ class XQueueCertInterface(object):
mode_is_verified = (enrollment_mode == GeneratedCertificate.MODES.verified) mode_is_verified = (enrollment_mode == GeneratedCertificate.MODES.verified)
user_is_verified = SoftwareSecurePhotoVerification.user_is_verified(student) user_is_verified = SoftwareSecurePhotoVerification.user_is_verified(student)
user_is_reverified = SoftwareSecurePhotoVerification.user_is_reverified_for_all(course_id, 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 cert_mode = enrollment_mode
if (mode_is_verified and user_is_verified and user_is_reverified): 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)): 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 cert_mode = GeneratedCertificate.MODES.honor
else: else:
# honor code and audit students # 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: if forced_grade:
grade['grade'] = forced_grade grade['grade'] = forced_grade
...@@ -219,7 +218,7 @@ class XQueueCertInterface(object): ...@@ -219,7 +218,7 @@ class XQueueCertInterface(object):
contents = { contents = {
'action': 'create', 'action': 'create',
'username': student.username, 'username': student.username,
'course_id': course_id, 'course_id': course_id.to_deprecated_string(),
'name': profile.name, 'name': profile.name,
'grade': grade['grade'], 'grade': grade['grade'],
'template_pdf': template_pdf, 'template_pdf': template_pdf,
......
...@@ -5,6 +5,7 @@ from django.views.decorators.csrf import csrf_exempt ...@@ -5,6 +5,7 @@ from django.views.decorators.csrf import csrf_exempt
from django.http import HttpResponse from django.http import HttpResponse
import json import json
from dogapi import dog_stats_api from dogapi import dog_stats_api
from xmodule.modulestore.locations import SlashSeparatedCourseKey
from capa.xqueue_interface import XQUEUE_METRIC_NAME from capa.xqueue_interface import XQUEUE_METRIC_NAME
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -27,21 +28,23 @@ def update_certificate(request): ...@@ -27,21 +28,23 @@ def update_certificate(request):
xqueue_header = json.loads(request.POST.get('xqueue_header')) xqueue_header = json.loads(request.POST.get('xqueue_header'))
try: try:
course_key = SlashSeparatedCourseKey.from_deprecated_string(xqueue_body['course_id'])
cert = GeneratedCertificate.objects.get( cert = GeneratedCertificate.objects.get(
user__username=xqueue_body['username'], user__username=xqueue_body['username'],
course_id=xqueue_body['course_id'], course_id=course_key,
key=xqueue_header['lms_key']) key=xqueue_header['lms_key'])
except GeneratedCertificate.DoesNotExist: except GeneratedCertificate.DoesNotExist:
logger.critical('Unable to lookup certificate\n' logger.critical('Unable to lookup certificate\n'
'xqueue_body: {0}\n' 'xqueue_body: {0}\n'
'xqueue_header: {1}'.format( 'xqueue_header: {1}'.format(
xqueue_body, xqueue_header)) xqueue_body, xqueue_header))
return HttpResponse(json.dumps({ return HttpResponse(json.dumps({
'return_code': 1, 'return_code': 1,
'content': 'unable to lookup key'}), 'content': 'unable to lookup key'}),
mimetype='application/json') mimetype='application/json')
if 'error' in xqueue_body: if 'error' in xqueue_body:
cert.status = status.error cert.status = status.error
......
...@@ -14,7 +14,6 @@ from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE ...@@ -14,7 +14,6 @@ from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
from courseware.tests.factories import StudentModuleFactory from courseware.tests.factories import StudentModuleFactory
from student.tests.factories import UserFactory, CourseEnrollmentFactory, AdminFactory from student.tests.factories import UserFactory, CourseEnrollmentFactory, AdminFactory
from capa.tests.response_xml_factory import StringResponseXMLFactory 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, from class_dashboard.dashboard_data import (get_problem_grade_distribution, get_sequential_open_distrib,
get_problem_set_grade_distrib, get_d3_problem_grade_distrib, get_problem_set_grade_distrib, get_d3_problem_grade_distrib,
...@@ -82,7 +81,7 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase): ...@@ -82,7 +81,7 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase):
max_grade=1 if i < j else 0.5, max_grade=1 if i < j else 0.5,
student=user, student=user,
course_id=self.course.id, 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}), state=json.dumps({'attempts': self.attempts}),
) )
...@@ -90,7 +89,7 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase): ...@@ -90,7 +89,7 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase):
StudentModuleFactory.create( StudentModuleFactory.create(
course_id=self.course.id, course_id=self.course.id,
module_type='sequential', module_type='sequential',
module_state_key=Location(self.item.location).url(), module_state_key=self.item.location,
) )
def test_get_problem_grade_distribution(self): def test_get_problem_grade_distribution(self):
...@@ -156,7 +155,7 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase): ...@@ -156,7 +155,7 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase):
def test_get_students_problem_grades(self): 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) request = self.request_factory.get(reverse('get_students_problem_grades') + attributes)
response = get_students_problem_grades(request) response = get_students_problem_grades(request)
...@@ -174,7 +173,7 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase): ...@@ -174,7 +173,7 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase):
def test_get_students_problem_grades_max(self): def test_get_students_problem_grades_max(self):
with patch('class_dashboard.dashboard_data.MAX_SCREEN_LIST_LENGTH', 2): 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) request = self.request_factory.get(reverse('get_students_problem_grades') + attributes)
response = get_students_problem_grades(request) response = get_students_problem_grades(request)
...@@ -188,7 +187,7 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase): ...@@ -188,7 +187,7 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase):
def test_get_students_problem_grades_csv(self): def test_get_students_problem_grades_csv(self):
tooltip = 'P1.2.1 Q1 - 3382 Students (100%: 1/1 questions)' 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) request = self.request_factory.get(reverse('get_students_problem_grades') + attributes)
response = get_students_problem_grades(request) response = get_students_problem_grades(request)
...@@ -208,7 +207,7 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase): ...@@ -208,7 +207,7 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase):
def test_get_students_opened_subsection(self): 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) request = self.request_factory.get(reverse('get_students_opened_subsection') + attributes)
response = get_students_opened_subsection(request) response = get_students_opened_subsection(request)
...@@ -221,7 +220,7 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase): ...@@ -221,7 +220,7 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase):
with patch('class_dashboard.dashboard_data.MAX_SCREEN_LIST_LENGTH', 2): 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) request = self.request_factory.get(reverse('get_students_opened_subsection') + attributes)
response = get_students_opened_subsection(request) response = get_students_opened_subsection(request)
...@@ -235,7 +234,7 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase): ...@@ -235,7 +234,7 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase):
def test_get_students_opened_subsection_csv(self): def test_get_students_opened_subsection_csv(self):
tooltip = '4162 student(s) opened Subsection 5: Relational Algebra Exercises' 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) request = self.request_factory.get(reverse('get_students_opened_subsection') + attributes)
response = get_students_opened_subsection(request) response = get_students_opened_subsection(request)
...@@ -255,7 +254,7 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase): ...@@ -255,7 +254,7 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase):
def test_dashboard(self): 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( response = self.client.post(
url, url,
{ {
......
...@@ -19,8 +19,8 @@ def has_instructor_access_for_class(user, course_id): ...@@ -19,8 +19,8 @@ def has_instructor_access_for_class(user, course_id):
Returns true if the `user` is an instructor for the course. Returns true if the `user` is an instructor for the course.
""" """
course = get_course_with_access(user, course_id, 'staff', depth=None) course = get_course_with_access(user, 'staff', course_id, depth=None)
return has_access(user, course, 'staff') return has_access(user, 'staff', course)
def all_sequential_open_distrib(request, course_id): def all_sequential_open_distrib(request, course_id):
......
...@@ -29,8 +29,8 @@ class WikiAccessMiddleware(object): ...@@ -29,8 +29,8 @@ class WikiAccessMiddleware(object):
if course_id: if course_id:
# See if we are able to view the course. If we are, redirect to it # See if we are able to view the course. If we are, redirect to it
try: try:
course = get_course_with_access(request.user, course_id, 'load') _course = get_course_with_access(request.user, 'load', course_id)
return redirect("/courses/{course_id}/wiki/{path}".format(course_id=course.id, path=wiki_path)) return redirect("/courses/{course_id}/wiki/{path}".format(course_id=course_id.to_deprecated_string(), path=wiki_path))
except Http404: except Http404:
# Even though we came from the course, we can't see it. So don't worry about it. # Even though we came from the course, we can't see it. So don't worry about it.
pass pass
...@@ -44,22 +44,23 @@ class WikiAccessMiddleware(object): ...@@ -44,22 +44,23 @@ class WikiAccessMiddleware(object):
if not view_func.__module__.startswith('wiki.'): if not view_func.__module__.startswith('wiki.'):
return return
course_id = course_id_from_url(request.path)
wiki_path = request.path.split('/wiki/', 1)[1]
# wiki pages are login required # wiki pages are login required
if not request.user.is_authenticated(): if not request.user.is_authenticated():
return redirect(reverse('accounts_login'), next=request.path) 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: if course_id:
# This is a /courses/org/name/run/wiki request # This is a /courses/org/name/run/wiki request
# HACK: django-wiki monkeypatches the django reverse function to enable urls to be rewritten course_path = "/courses/{}".format(course_id.to_deprecated_string())
url_prefix = "/courses/{0}".format(course_id) # HACK: django-wiki monkeypatches the reverse function to enable
reverse._transform_url = lambda url: url_prefix + url # pylint: disable=protected-access # urls to be rewritten
reverse._transform_url = lambda url: course_path + url # pylint: disable=protected-access
# Authorization Check # Authorization Check
# Let's see if user is enrolled or the course allows for public access # Let's see if user is enrolled or the course allows for public access
try: try:
course = get_course_with_access(request.user, course_id, 'load') course = get_course_with_access(request.user, 'load', course_id)
except Http404: except Http404:
# course does not exist. redirect to root wiki. # course does not exist. redirect to root wiki.
# clearing the referrer will cause process_response not to redirect # clearing the referrer will cause process_response not to redirect
...@@ -69,11 +70,11 @@ class WikiAccessMiddleware(object): ...@@ -69,11 +70,11 @@ class WikiAccessMiddleware(object):
if not course.allow_public_wiki_access: if not course.allow_public_wiki_access:
is_enrolled = CourseEnrollment.is_enrolled(request.user, course.id) 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 not (is_enrolled or is_staff):
# if a user is logged in, but not authorized to see a page, # if a user is logged in, but not authorized to see a page,
# we'll redirect them to the course about 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 # set the course onto here so that the wiki template can show the course navigation
request.course = course request.course = course
else: else:
......
...@@ -4,7 +4,6 @@ Tests for wiki permissions ...@@ -4,7 +4,6 @@ Tests for wiki permissions
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from student.tests.factories import UserFactory from student.tests.factories import UserFactory
from xmodule.modulestore.django import loc_mapper
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
...@@ -48,12 +47,9 @@ class TestWikiAccessBase(ModuleStoreTestCase): ...@@ -48,12 +47,9 @@ class TestWikiAccessBase(ModuleStoreTestCase):
def create_staff_for_course(self, course): def create_staff_for_course(self, course):
"""Creates and returns users with instructor and staff access to course.""" """Creates and returns users with instructor and staff access to course."""
course_locator = loc_mapper().translate_location(course.id, course.location)
return [ return [
InstructorFactory(course=course.location), # Creates instructor_org/number/run role name InstructorFactory(course=course.id), # Creates instructor_org/number/run role name
StaffFactory(course=course.location), # Creates staff_org/number/run role name StaffFactory(course=course.id), # 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
] ]
......
...@@ -23,7 +23,7 @@ class TestWikiAccessMiddleware(ModuleStoreTestCase): ...@@ -23,7 +23,7 @@ class TestWikiAccessMiddleware(ModuleStoreTestCase):
self.wiki = get_or_create_root() 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 = 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.wiki_math101 = URLPath.create_article(self.wiki, 'math101', title='math101')
self.client = Client() self.client = Client()
......
...@@ -4,7 +4,7 @@ from django.test.utils import override_settings ...@@ -4,7 +4,7 @@ from django.test.utils import override_settings
from courseware.tests.tests import LoginEnrollmentTestCase from courseware.tests.tests import LoginEnrollmentTestCase
from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.locations import SlashSeparatedCourseKey
from mock import patch from mock import patch
...@@ -15,7 +15,7 @@ class WikiRedirectTestCase(LoginEnrollmentTestCase): ...@@ -15,7 +15,7 @@ class WikiRedirectTestCase(LoginEnrollmentTestCase):
def setUp(self): def setUp(self):
# Load the toy course # 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 # Create two accounts
self.student = 'view@test.com' self.student = 'view@test.com'
...@@ -43,7 +43,7 @@ class WikiRedirectTestCase(LoginEnrollmentTestCase): ...@@ -43,7 +43,7 @@ class WikiRedirectTestCase(LoginEnrollmentTestCase):
self.enroll(self.toy) 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/'}) destination = reverse("wiki:get", kwargs={'path': 'some/fake/wiki/page/'})
redirected_to = referer.replace("progress", "wiki/some/fake/wiki/page/") redirected_to = referer.replace("progress", "wiki/some/fake/wiki/page/")
...@@ -72,7 +72,7 @@ class WikiRedirectTestCase(LoginEnrollmentTestCase): ...@@ -72,7 +72,7 @@ class WikiRedirectTestCase(LoginEnrollmentTestCase):
self.enroll(self.toy) 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/'}) destination = reverse("wiki:get", kwargs={'path': 'some/fake/wiki/page/'})
resp = self.client.get(destination, HTTP_REFERER=referer) resp = self.client.get(destination, HTTP_REFERER=referer)
...@@ -84,8 +84,8 @@ class WikiRedirectTestCase(LoginEnrollmentTestCase): ...@@ -84,8 +84,8 @@ class WikiRedirectTestCase(LoginEnrollmentTestCase):
The user must be enrolled in the course to see the page. The user must be enrolled in the course to see the page.
""" """
course_wiki_home = reverse('course_wiki', kwargs={'course_id': course.id}) course_wiki_home = reverse('course_wiki', kwargs={'course_id': course.id.to_deprecated_string()})
referer = reverse("progress", kwargs={'course_id': self.toy.id}) referer = reverse("progress", kwargs={'course_id': self.toy.id.to_deprecated_string()})
resp = self.client.get(course_wiki_home, follow=True, HTTP_REFERER=referer) resp = self.client.get(course_wiki_home, follow=True, HTTP_REFERER=referer)
...@@ -117,8 +117,7 @@ class WikiRedirectTestCase(LoginEnrollmentTestCase): ...@@ -117,8 +117,7 @@ class WikiRedirectTestCase(LoginEnrollmentTestCase):
self.create_course_page(self.toy) self.create_course_page(self.toy)
course_wiki_page = reverse('wiki:get', kwargs={'path': self.toy.wiki_slug + '/'}) course_wiki_page = reverse('wiki:get', kwargs={'path': self.toy.wiki_slug + '/'})
referer = reverse("courseware", kwargs={'course_id': self.toy.id.to_deprecated_string()})
referer = reverse("courseware", kwargs={'course_id': self.toy.id})
resp = self.client.get(course_wiki_page, follow=True, HTTP_REFERER=referer) resp = self.client.get(course_wiki_page, follow=True, HTTP_REFERER=referer)
...@@ -137,7 +136,7 @@ class WikiRedirectTestCase(LoginEnrollmentTestCase): ...@@ -137,7 +136,7 @@ class WikiRedirectTestCase(LoginEnrollmentTestCase):
self.login(self.student, self.password) self.login(self.student, self.password)
course_wiki_page = reverse('wiki:get', kwargs={'path': self.toy.wiki_slug + '/'}) 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 # When not enrolled, we should get a 302
resp = self.client.get(course_wiki_page, follow=False, HTTP_REFERER=referer) resp = self.client.get(course_wiki_page, follow=False, HTTP_REFERER=referer)
...@@ -147,7 +146,7 @@ class WikiRedirectTestCase(LoginEnrollmentTestCase): ...@@ -147,7 +146,7 @@ class WikiRedirectTestCase(LoginEnrollmentTestCase):
resp = self.client.get(course_wiki_page, follow=True, HTTP_REFERER=referer) resp = self.client.get(course_wiki_page, follow=True, HTTP_REFERER=referer)
target_url, __ = resp.redirect_chain[-1] target_url, __ = resp.redirect_chain[-1]
self.assertTrue( 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}) @patch.dict("django.conf.settings.FEATURES", {'ALLOW_WIKI_ROOT_ACCESS': True})
......
...@@ -33,12 +33,12 @@ def user_is_article_course_staff(user, article): ...@@ -33,12 +33,12 @@ def user_is_article_course_staff(user, article):
# course numbered '202_' or '202' and so we need to consider both. # course numbered '202_' or '202' and so we need to consider both.
courses = modulestore.django.modulestore().get_courses_for_wiki(wiki_slug) 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 return True
if (wiki_slug.endswith('_') and slug_is_numerical(wiki_slug[:-1])): if (wiki_slug.endswith('_') and slug_is_numerical(wiki_slug[:-1])):
courses = modulestore.django.modulestore().get_courses_for_wiki(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 True
return False return False
......
...@@ -16,6 +16,7 @@ from wiki.models import URLPath, Article ...@@ -16,6 +16,7 @@ from wiki.models import URLPath, Article
from courseware.courses import get_course_by_id from courseware.courses import get_course_by_id
from course_wiki.utils import course_wiki_slug from course_wiki.utils import course_wiki_slug
from xmodule.modulestore.locations import SlashSeparatedCourseKey
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -35,7 +36,7 @@ def course_wiki_redirect(request, course_id): # pylint: disable=W0613 ...@@ -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 as it's home page. A course's wiki must be an article on the root (for
example, "/6.002x") to keep things simple. 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) course_slug = course_wiki_slug(course)
valid_slug = True valid_slug = True
......
...@@ -17,7 +17,7 @@ from django.utils.translation import ugettext as _ ...@@ -17,7 +17,7 @@ from django.utils.translation import ugettext as _
import mongoengine import mongoengine
from dashboard.models import CourseImportLog from dashboard.models import CourseImportLog
from xmodule.modulestore import Location from xmodule.modulestore.keys import CourseKey
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -222,19 +222,16 @@ def add_repo(repo, rdir_in, branch=None): ...@@ -222,19 +222,16 @@ def add_repo(repo, rdir_in, branch=None):
logger.setLevel(logging.NOTSET) logger.setLevel(logging.NOTSET)
logger.removeHandler(import_log_handler) logger.removeHandler(import_log_handler)
course_id = 'unknown' course_key = None
location = 'unknown' location = 'unknown'
# extract course ID from output of import-command-run and make symlink # extract course ID from output of import-command-run and make symlink
# this is needed in order for custom course scripts to work # this is needed in order for custom course scripts to work
match = re.search('(?ms)===> IMPORTING course to location (\S+)', match = re.search(r'(?ms)===> IMPORTING course (\S+)', ret_import)
ret_import)
if match: if match:
location = Location(match.group(1)) course_id = match.group(1)
log.debug('location = {0}'.format(location)) course_key = CourseKey.from_string(course_id)
course_id = location.course_id cdir = '{0}/{1}'.format(GIT_REPO_DIR, course_key.course)
cdir = '{0}/{1}'.format(GIT_REPO_DIR, location.course)
log.debug('Studio course dir = {0}'.format(cdir)) log.debug('Studio course dir = {0}'.format(cdir))
if os.path.exists(cdir) and not os.path.islink(cdir): if os.path.exists(cdir) and not os.path.islink(cdir):
...@@ -267,8 +264,8 @@ def add_repo(repo, rdir_in, branch=None): ...@@ -267,8 +264,8 @@ def add_repo(repo, rdir_in, branch=None):
log.exception('Unable to connect to mongodb to save log, please ' log.exception('Unable to connect to mongodb to save log, please '
'check MONGODB_LOG settings') 'check MONGODB_LOG settings')
cil = CourseImportLog( cil = CourseImportLog(
course_id=course_id, course_id=course_key,
location=unicode(location), location=location,
repo_dir=rdir, repo_dir=rdir,
created=timezone.now(), created=timezone.now(),
import_log=ret_import, import_log=ret_import,
......
...@@ -2,19 +2,13 @@ ...@@ -2,19 +2,13 @@
Script for importing courseware from git/xml into a mongo modulestore Script for importing courseware from git/xml into a mongo modulestore
""" """
import os
import re
import StringIO
import subprocess
import logging import logging
from django.core import management
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
import dashboard.git_import import dashboard.git_import
from dashboard.git_import import GitImportError from dashboard.git_import import GitImportError
from dashboard.models import CourseImportLog
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.xml import XMLModuleStore from xmodule.modulestore.xml import XMLModuleStore
......
...@@ -17,10 +17,12 @@ from django.test.utils import override_settings ...@@ -17,10 +17,12 @@ from django.test.utils import override_settings
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
from xmodule.contentstore.django import contentstore from xmodule.contentstore.django import contentstore
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.locations import SlashSeparatedCourseKey
from xmodule.modulestore.store_utilities import delete_course from xmodule.modulestore.store_utilities import delete_course
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
import dashboard.git_import as git_import import dashboard.git_import as git_import
from dashboard.git_import import GitImportError from dashboard.git_import import GitImportError
from xmodule.modulestore.locations import SlashSeparatedCourseKey
TEST_MONGODB_LOG = { TEST_MONGODB_LOG = {
'host': 'localhost', 'host': 'localhost',
...@@ -45,7 +47,7 @@ class TestGitAddCourse(ModuleStoreTestCase): ...@@ -45,7 +47,7 @@ class TestGitAddCourse(ModuleStoreTestCase):
TEST_REPO = 'https://github.com/mitocw/edx4edx_lite.git' TEST_REPO = 'https://github.com/mitocw/edx4edx_lite.git'
TEST_COURSE = 'MITx/edx4edx/edx4edx' TEST_COURSE = 'MITx/edx4edx/edx4edx'
TEST_BRANCH = 'testing_do_not_delete' 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') GIT_REPO_DIR = getattr(settings, 'GIT_REPO_DIR')
def assertCommandFailureRegexp(self, regex, *args): def assertCommandFailureRegexp(self, regex, *args):
...@@ -162,14 +164,14 @@ class TestGitAddCourse(ModuleStoreTestCase): ...@@ -162,14 +164,14 @@ class TestGitAddCourse(ModuleStoreTestCase):
# Delete to test branching back to master # Delete to test branching back to master
delete_course(def_ms, contentstore(), delete_course(def_ms, contentstore(),
def_ms.get_course(self.TEST_BRANCH_COURSE).location, self.TEST_BRANCH_COURSE,
True) True)
self.assertIsNone(def_ms.get_course(self.TEST_BRANCH_COURSE)) self.assertIsNone(def_ms.get_course(self.TEST_BRANCH_COURSE))
git_import.add_repo(self.TEST_REPO, git_import.add_repo(self.TEST_REPO,
repo_dir / 'edx4edx_lite', repo_dir / 'edx4edx_lite',
'master') 'master')
self.assertIsNone(def_ms.get_course(self.TEST_BRANCH_COURSE)) 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): def test_branch_exceptions(self):
""" """
......
"""Models for dashboard application""" """Models for dashboard application"""
import mongoengine import mongoengine
from xmodule.modulestore.mongoengine_fields import CourseKeyField
class CourseImportLog(mongoengine.Document): class CourseImportLog(mongoengine.Document):
"""Mongoengine model for git log""" """Mongoengine model for git log"""
# pylint: disable=R0924 # 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) location = mongoengine.StringField(max_length=168)
import_log = mongoengine.StringField(max_length=20 * 65535) import_log = mongoengine.StringField(max_length=20 * 65535)
git_log = mongoengine.StringField(max_length=65535) git_log = mongoengine.StringField(max_length=65535)
......
...@@ -42,6 +42,7 @@ from xmodule.modulestore import XML_MODULESTORE_TYPE ...@@ -42,6 +42,7 @@ from xmodule.modulestore import XML_MODULESTORE_TYPE
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.store_utilities import delete_course from xmodule.modulestore.store_utilities import delete_course
from xmodule.modulestore.xml import XMLModuleStore from xmodule.modulestore.xml import XMLModuleStore
from xmodule.modulestore.locations import SlashSeparatedCourseKey
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -78,7 +79,7 @@ class SysadminDashboardView(TemplateView): ...@@ -78,7 +79,7 @@ class SysadminDashboardView(TemplateView):
""" Get an iterable list of courses.""" """ Get an iterable list of courses."""
courses = self.def_ms.get_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 return courses
...@@ -258,7 +259,7 @@ class Users(SysadminDashboardView): ...@@ -258,7 +259,7 @@ class Users(SysadminDashboardView):
self.msg += u'<ol>' self.msg += u'<ol>'
for (cdir, course) in courses.items(): for (cdir, course) in courses.items():
self.msg += u'<li>{0} ({1})</li>'.format( self.msg += u'<li>{0} ({1})</li>'.format(
escape(cdir), course.location.url()) escape(cdir), course.location.to_deprecated_string())
self.msg += u'</ol>' self.msg += u'</ol>'
def get(self, request): def get(self, request):
...@@ -469,7 +470,7 @@ class Courses(SysadminDashboardView): ...@@ -469,7 +470,7 @@ class Courses(SysadminDashboardView):
course = self.def_ms.courses[os.path.abspath(gdir)] course = self.def_ms.courses[os.path.abspath(gdir)]
msg += _('Loaded course {0} {1}<br/>Errors:').format( msg += _('Loaded course {0} {1}<br/>Errors:').format(
cdir, course.display_name) 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: if not errors:
msg += u'None' msg += u'None'
else: else:
...@@ -489,9 +490,7 @@ class Courses(SysadminDashboardView): ...@@ -489,9 +490,7 @@ class Courses(SysadminDashboardView):
courses = self.get_courses() courses = self.get_courses()
for (cdir, course) in courses.items(): for (cdir, course) in courses.items():
gdir = cdir gdir = cdir.run
if '/' in cdir:
gdir = cdir.rsplit('/', 1)[1]
data.append([course.display_name, cdir] data.append([course.display_name, cdir]
+ self.git_info_for_course(gdir)) + self.git_info_for_course(gdir))
...@@ -535,13 +534,14 @@ class Courses(SysadminDashboardView): ...@@ -535,13 +534,14 @@ class Courses(SysadminDashboardView):
elif action == 'del_course': elif action == 'del_course':
course_id = request.POST.get('course_id', '').strip() course_id = request.POST.get('course_id', '').strip()
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
course_found = False course_found = False
if course_id in courses: if course_key in courses:
course_found = True course_found = True
course = courses[course_id] course = courses[course_key]
else: else:
try: try:
course = get_course_by_id(course_id) course = get_course_by_id(course_key)
course_found = True course_found = True
except Exception, err: # pylint: disable=broad-except except Exception, err: # pylint: disable=broad-except
self.msg += _('Error - cannot get course with ID ' self.msg += _('Error - cannot get course with ID '
...@@ -549,7 +549,7 @@ class Courses(SysadminDashboardView): ...@@ -549,7 +549,7 @@ class Courses(SysadminDashboardView):
course_id, escape(str(err)) 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: if course_found and is_xml_course:
cdir = course.data_dir cdir = course.data_dir
self.def_ms.courses.pop(cdir) self.def_ms.courses.pop(cdir)
...@@ -567,14 +567,13 @@ class Courses(SysadminDashboardView): ...@@ -567,14 +567,13 @@ class Courses(SysadminDashboardView):
elif course_found and not is_xml_course: elif course_found and not is_xml_course:
# delete course that is stored with mongodb backend # delete course that is stored with mongodb backend
loc = course.location
content_store = contentstore() content_store = contentstore()
commit = True 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 # don't delete user permission groups, though
self.msg += \ self.msg += \
u"<font color='red'>{0} {1} = {2} ({3})</font>".format( u"<font color='red'>{0} {1} ({2})</font>".format(
_('Deleted'), loc, course.id, course.display_name) _('Deleted'), course.id.to_deprecated_string(), course.display_name)
datatable = self.make_datatable() datatable = self.make_datatable()
context = { context = {
...@@ -606,9 +605,9 @@ class Staffing(SysadminDashboardView): ...@@ -606,9 +605,9 @@ class Staffing(SysadminDashboardView):
datum = [course.display_name, course.id] datum = [course.display_name, course.id]
datum += [CourseEnrollment.objects.filter( datum += [CourseEnrollment.objects.filter(
course_id=course.id).count()] 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( datum += [','.join([x.username for x in CourseInstructorRole(
course.location).users_with_role()])] course.id).users_with_role()])]
data.append(datum) data.append(datum)
datatable = dict(header=[_('Course Name'), _('course_id'), datatable = dict(header=[_('Course Name'), _('course_id'),
...@@ -640,7 +639,7 @@ class Staffing(SysadminDashboardView): ...@@ -640,7 +639,7 @@ class Staffing(SysadminDashboardView):
for (cdir, course) in courses.items(): # pylint: disable=unused-variable for (cdir, course) in courses.items(): # pylint: disable=unused-variable
for role in roles: 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, datum = [course.id, role, user.username, user.email,
user.profile.name] user.profile.name]
data.append(datum) data.append(datum)
...@@ -667,6 +666,8 @@ class GitLogs(TemplateView): ...@@ -667,6 +666,8 @@ class GitLogs(TemplateView):
"""Shows logs of imports that happened as a result of a git import""" """Shows logs of imports that happened as a result of a git import"""
course_id = kwargs.get('course_id') 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 # Set mongodb defaults even if it isn't defined in settings
mongo_db = { mongo_db = {
...@@ -709,16 +710,15 @@ class GitLogs(TemplateView): ...@@ -709,16 +710,15 @@ class GitLogs(TemplateView):
# Allow only course team, instructors, and staff # Allow only course team, instructors, and staff
if not (request.user.is_staff or if not (request.user.is_staff or
CourseInstructorRole(course.location).has_user(request.user) or CourseInstructorRole(course.id).has_user(request.user) or
CourseStaffRole(course.location).has_user(request.user)): CourseStaffRole(course.id).has_user(request.user)):
raise Http404 raise Http404
log.debug('course_id={0}'.format(course_id)) log.debug('course_id={0}'.format(course_id))
cilset = CourseImportLog.objects.filter( cilset = CourseImportLog.objects.filter(course_id=course_id).order_by('-created')
course_id=course_id).order_by('-created')
log.debug('cilset length={0}'.format(len(cilset))) log.debug('cilset length={0}'.format(len(cilset)))
mdb.disconnect() mdb.disconnect()
context = {'cilset': cilset, context = {'cilset': cilset,
'course_id': course_id, 'course_id': course_id.to_deprecated_string() if course_id else None,
'error_msg': error_msg} 'error_msg': error_msg}
return render_to_response(self.template_name, context) return render_to_response(self.template_name, context)
...@@ -13,7 +13,6 @@ from django.contrib.auth.models import User ...@@ -13,7 +13,6 @@ from django.contrib.auth.models import User
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.test.client import Client from django.test.client import Client
from django.test.utils import override_settings from django.test.utils import override_settings
from django.utils.html import escape
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
import mongoengine import mongoengine
...@@ -27,6 +26,7 @@ from student.tests.factories import UserFactory ...@@ -27,6 +26,7 @@ from student.tests.factories import UserFactory
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.xml import XMLModuleStore from xmodule.modulestore.xml import XMLModuleStore
from xmodule.modulestore.locations import SlashSeparatedCourseKey
TEST_MONGODB_LOG = { TEST_MONGODB_LOG = {
...@@ -47,7 +47,7 @@ class SysadminBaseTestCase(ModuleStoreTestCase): ...@@ -47,7 +47,7 @@ class SysadminBaseTestCase(ModuleStoreTestCase):
TEST_REPO = 'https://github.com/mitocw/edx4edx_lite.git' TEST_REPO = 'https://github.com/mitocw/edx4edx_lite.git'
TEST_BRANCH = 'testing_do_not_delete' TEST_BRANCH = 'testing_do_not_delete'
TEST_BRANCH_COURSE = 'MITx/edx4edx_branch/edx4edx' TEST_BRANCH_COURSE = SlashSeparatedCourseKey('MITx', 'edx4edx_branch', 'edx4edx')
def setUp(self): def setUp(self):
"""Setup test case by adding primary user.""" """Setup test case by adding primary user."""
...@@ -79,12 +79,16 @@ class SysadminBaseTestCase(ModuleStoreTestCase): ...@@ -79,12 +79,16 @@ class SysadminBaseTestCase(ModuleStoreTestCase):
course = def_ms.courses.get(course_path, None) course = def_ms.courses.get(course_path, None)
except AttributeError: except AttributeError:
# Using mongo store # Using mongo store
course = def_ms.get_course('MITx/edx4edx/edx4edx') course = def_ms.get_course(SlashSeparatedCourseKey('MITx', 'edx4edx', 'edx4edx'))
# Delete git loaded course # Delete git loaded course
response = self.client.post(reverse('sysadmin_courses'), response = self.client.post(
{'course_id': course.id, reverse('sysadmin_courses'),
'action': 'del_course', }) {
'course_id': course.id.to_deprecated_string(),
'action': 'del_course',
}
)
self.addCleanup(self._rm_glob, '{0}_deleted_*'.format(course_path)) self.addCleanup(self._rm_glob, '{0}_deleted_*'.format(course_path))
return response return response
...@@ -321,7 +325,7 @@ class TestSysadmin(SysadminBaseTestCase): ...@@ -321,7 +325,7 @@ class TestSysadmin(SysadminBaseTestCase):
course = def_ms.courses.get('{0}/edx4edx_lite'.format( course = def_ms.courses.get('{0}/edx4edx_lite'.format(
os.path.abspath(settings.DATA_DIR)), None) os.path.abspath(settings.DATA_DIR)), None)
self.assertIsNotNone(course) self.assertIsNotNone(course)
self.assertIn(self.TEST_BRANCH_COURSE, course.location.course_id) self.assertEqual(self.TEST_BRANCH_COURSE, course.id)
self._rm_edx4edx() self._rm_edx4edx()
# Try and delete a non-existent course # Try and delete a non-existent course
...@@ -363,8 +367,8 @@ class TestSysadmin(SysadminBaseTestCase): ...@@ -363,8 +367,8 @@ class TestSysadmin(SysadminBaseTestCase):
self._add_edx4edx() self._add_edx4edx()
def_ms = modulestore() def_ms = modulestore()
course = def_ms.get_course('MITx/edx4edx/edx4edx') course = def_ms.get_course(SlashSeparatedCourseKey('MITx', 'edx4edx', 'edx4edx'))
CourseStaffRole(course.location).add_users(self.user) CourseStaffRole(course.id).add_users(self.user)
response = self.client.post(reverse('sysadmin_staffing'), response = self.client.post(reverse('sysadmin_staffing'),
{'action': 'get_staff_csv', }) {'action': 'get_staff_csv', })
...@@ -447,11 +451,11 @@ class TestSysAdminMongoCourseImport(SysadminBaseTestCase): ...@@ -447,11 +451,11 @@ class TestSysAdminMongoCourseImport(SysadminBaseTestCase):
self.assertFalse(isinstance(def_ms, XMLModuleStore)) self.assertFalse(isinstance(def_ms, XMLModuleStore))
self._add_edx4edx() self._add_edx4edx()
course = def_ms.get_course('MITx/edx4edx/edx4edx') course = def_ms.get_course(SlashSeparatedCourseKey('MITx', 'edx4edx', 'edx4edx'))
self.assertIsNotNone(course) self.assertIsNotNone(course)
self._rm_edx4edx() self._rm_edx4edx()
course = def_ms.get_course('MITx/edx4edx/edx4edx') course = def_ms.get_course(SlashSeparatedCourseKey('MITx', 'edx4edx', 'edx4edx'))
self.assertIsNone(course) self.assertIsNone(course)
def test_gitlogs(self): def test_gitlogs(self):
...@@ -472,7 +476,7 @@ class TestSysAdminMongoCourseImport(SysadminBaseTestCase): ...@@ -472,7 +476,7 @@ class TestSysAdminMongoCourseImport(SysadminBaseTestCase):
reverse('gitlogs_detail', kwargs={ reverse('gitlogs_detail', kwargs={
'course_id': 'MITx/edx4edx/edx4edx'})) 'course_id': 'MITx/edx4edx/edx4edx'}))
self.assertIn('======&gt; IMPORTING course to location', self.assertIn('======&gt; IMPORTING course',
response.content) response.content)
self._rm_edx4edx() self._rm_edx4edx()
...@@ -505,23 +509,25 @@ class TestSysAdminMongoCourseImport(SysadminBaseTestCase): ...@@ -505,23 +509,25 @@ class TestSysAdminMongoCourseImport(SysadminBaseTestCase):
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
# Or specific logs # Or specific logs
response = self.client.get(reverse('gitlogs_detail', kwargs={ response = self.client.get(reverse('gitlogs_detail', kwargs={
'course_id': 'MITx/edx4edx/edx4edx'})) 'course_id': 'MITx/edx4edx/edx4edx'
}))
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
# Add user as staff in course team # Add user as staff in course team
def_ms = modulestore() def_ms = modulestore()
course = def_ms.get_course('MITx/edx4edx/edx4edx') course = def_ms.get_course(SlashSeparatedCourseKey('MITx', 'edx4edx', 'edx4edx'))
CourseStaffRole(course.location).add_users(self.user) 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, logged_in = self.client.login(username=self.user.username,
password='foo') password='foo')
self.assertTrue(logged_in) self.assertTrue(logged_in)
response = self.client.get( response = self.client.get(
reverse('gitlogs_detail', kwargs={ reverse('gitlogs_detail', kwargs={
'course_id': 'MITx/edx4edx/edx4edx'})) 'course_id': 'MITx/edx4edx/edx4edx'
self.assertIn('======&gt; IMPORTING course to location', }))
self.assertIn('======&gt; IMPORTING course',
response.content) response.content)
self._rm_edx4edx() self._rm_edx4edx()
...@@ -42,7 +42,7 @@ class Command(BaseCommand): ...@@ -42,7 +42,7 @@ class Command(BaseCommand):
for course_id, course_modules in xml_module_store.modules.iteritems(): for course_id, course_modules in xml_module_store.modules.iteritems():
course_path = course_id.replace('/', '_') course_path = course_id.replace('/', '_')
for location, descriptor in course_modules.iteritems(): for location, descriptor in course_modules.iteritems():
location_path = location.url().replace('/', '_') location_path = location.to_deprecated_string().replace('/', '_')
data = {} data = {}
for field_name, field in descriptor.fields.iteritems(): for field_name, field in descriptor.fields.iteritems():
try: try:
......
import time import time
import random import random
import os
import os.path import os.path
import logging import logging
import urlparse import urlparse
...@@ -13,12 +12,11 @@ import django_comment_client.settings as cc_settings ...@@ -13,12 +12,11 @@ import django_comment_client.settings as cc_settings
from django.core import exceptions from django.core import exceptions
from django.contrib.auth.decorators import login_required 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.views.decorators import csrf
from django.core.files.storage import get_storage_class from django.core.files.storage import get_storage_class
from django.utils.translation import ugettext as _ 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 courseware.courses import get_course_with_access, get_course_by_id
from course_groups.cohorts import get_cohort_id, is_commentable_cohorted 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 ...@@ -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 django_comment_client.permissions import check_permissions_by_view, cached_has_permission
from courseware.access import has_access from courseware.access import has_access
from xmodule.modulestore.locations import SlashSeparatedCourseKey
from xmodule.modulestore.keys import CourseKey
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -41,7 +41,8 @@ def permitted(fn): ...@@ -41,7 +41,8 @@ def permitted(fn):
else: else:
content = None content = None
return content 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) return fn(request, *args, **kwargs)
else: else:
return JsonError("unauthorized", status=401) return JsonError("unauthorized", status=401)
...@@ -49,10 +50,6 @@ def permitted(fn): ...@@ -49,10 +50,6 @@ def permitted(fn):
def ajax_content_response(request, course_id, content): 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() 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) annotated_content_info = utils.get_annotated_content_info(course_id, content, request.user, user_info)
return JsonResponse({ return JsonResponse({
...@@ -70,7 +67,8 @@ def create_thread(request, course_id, commentable_id): ...@@ -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) 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 post = request.POST
if course.allow_anonymous: if course.allow_anonymous:
...@@ -93,7 +91,7 @@ def create_thread(request, course_id, commentable_id): ...@@ -93,7 +91,7 @@ def create_thread(request, course_id, commentable_id):
'anonymous': anonymous, 'anonymous': anonymous,
'anonymous_to_peers': anonymous_to_peers, 'anonymous_to_peers': anonymous_to_peers,
'commentable_id': commentable_id, 'commentable_id': commentable_id,
'course_id': course_id, 'course_id': course_id.to_deprecated_string(),
'user_id': request.user.id, 'user_id': request.user.id,
}) })
...@@ -152,23 +150,24 @@ def update_thread(request, course_id, thread_id): ...@@ -152,23 +150,24 @@ def update_thread(request, course_id, thread_id):
thread.update_attributes(**extract(request.POST, ['body', 'title'])) thread.update_attributes(**extract(request.POST, ['body', 'title']))
thread.save() thread.save()
if request.is_ajax(): 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: else:
return JsonResponse(utils.safe_content(thread.to_dict())) 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, given a course_id, thread_id, and parent_id, create a comment,
called from create_comment to do the actual creation called from create_comment to do the actual creation
""" """
assert isinstance(course_key, CourseKey)
post = request.POST post = request.POST
if 'body' not in post or not post['body'].strip(): if 'body' not in post or not post['body'].strip():
return JsonError(_("Body can't be empty")) return JsonError(_("Body can't be empty"))
comment = cc.Comment(**extract(post, ['body'])) 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: if course.allow_anonymous:
anonymous = post.get('anonymous', 'false').lower() == 'true' anonymous = post.get('anonymous', 'false').lower() == 'true'
else: else:
...@@ -183,7 +182,7 @@ def _create_comment(request, course_id, thread_id=None, parent_id=None): ...@@ -183,7 +182,7 @@ def _create_comment(request, course_id, thread_id=None, parent_id=None):
'anonymous': anonymous, 'anonymous': anonymous,
'anonymous_to_peers': anonymous_to_peers, 'anonymous_to_peers': anonymous_to_peers,
'user_id': request.user.id, 'user_id': request.user.id,
'course_id': course_id, 'course_id': course_key,
'thread_id': thread_id, 'thread_id': thread_id,
'parent_id': parent_id, 'parent_id': parent_id,
}) })
...@@ -192,7 +191,7 @@ def _create_comment(request, course_id, thread_id=None, parent_id=None): ...@@ -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 = cc.User.from_django_user(request.user)
user.follow(comment.thread) user.follow(comment.thread)
if request.is_ajax(): 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: else:
return JsonResponse(utils.safe_content(comment.to_dict())) return JsonResponse(utils.safe_content(comment.to_dict()))
...@@ -208,7 +207,7 @@ def create_comment(request, course_id, thread_id): ...@@ -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 is not None:
if cc_settings.MAX_COMMENT_DEPTH < 0: if cc_settings.MAX_COMMENT_DEPTH < 0:
return JsonError(_("Comment level too deep")) 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 @require_POST
...@@ -238,7 +237,7 @@ def update_comment(request, course_id, comment_id): ...@@ -238,7 +237,7 @@ def update_comment(request, course_id, comment_id):
comment.update_attributes(**extract(request.POST, ['body'])) comment.update_attributes(**extract(request.POST, ['body']))
comment.save() comment.save()
if request.is_ajax(): 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: else:
return JsonResponse(utils.safe_content(comment.to_dict())) return JsonResponse(utils.safe_content(comment.to_dict()))
...@@ -271,7 +270,7 @@ def openclose_thread(request, course_id, thread_id): ...@@ -271,7 +270,7 @@ def openclose_thread(request, course_id, thread_id):
thread = thread.to_dict() thread = thread.to_dict()
return JsonResponse({ return JsonResponse({
'content': utils.safe_content(thread), '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): ...@@ -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 is not None:
if cc_settings.MAX_COMMENT_DEPTH <= cc.Comment.find(comment_id).depth: if cc_settings.MAX_COMMENT_DEPTH <= cc.Comment.find(comment_id).depth:
return JsonError(_("Comment level too deep")) 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 @require_POST
...@@ -366,10 +365,11 @@ def un_flag_abuse_for_thread(request, course_id, thread_id): ...@@ -366,10 +365,11 @@ def un_flag_abuse_for_thread(request, course_id, thread_id):
ajax only ajax only
""" """
user = cc.User.from_django_user(request.user) user = cc.User.from_django_user(request.user)
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
course = get_course_by_id(course_id) course = get_course_by_id(course_id)
thread = cc.Thread.find(thread_id) thread = cc.Thread.find(thread_id)
removeAll = cached_has_permission(request.user, 'openclose_thread', course_id) or has_access(request.user, course, 'staff') remove_all = cached_has_permission(request.user, 'openclose_thread', course_id) or has_access(request.user, 'staff', course)
thread.unFlagAbuse(user, thread, removeAll) thread.unFlagAbuse(user, thread, remove_all)
return JsonResponse(utils.safe_content(thread.to_dict())) return JsonResponse(utils.safe_content(thread.to_dict()))
...@@ -396,10 +396,11 @@ def un_flag_abuse_for_comment(request, course_id, comment_id): ...@@ -396,10 +396,11 @@ def un_flag_abuse_for_comment(request, course_id, comment_id):
ajax only ajax only
""" """
user = cc.User.from_django_user(request.user) user = cc.User.from_django_user(request.user)
course = get_course_by_id(course_id) course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
removeAll = cached_has_permission(request.user, 'openclose_thread', course_id) or has_access(request.user, course, 'staff') 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 = cc.Comment.find(comment_id)
comment.unFlagAbuse(user, comment, removeAll) comment.unFlagAbuse(user, comment, remove_all)
return JsonResponse(utils.safe_content(comment.to_dict())) return JsonResponse(utils.safe_content(comment.to_dict()))
......
...@@ -64,7 +64,7 @@ class ViewsExceptionTestCase(UrlResetMixin, ModuleStoreTestCase): ...@@ -64,7 +64,7 @@ class ViewsExceptionTestCase(UrlResetMixin, ModuleStoreTestCase):
mock_from_django_user.return_value = Mock() mock_from_django_user.return_value = Mock()
url = reverse('django_comment_client.forum.views.user_profile', 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.response = self.client.get(url)
self.assertEqual(self.response.status_code, 404) self.assertEqual(self.response.status_code, 404)
...@@ -81,7 +81,7 @@ class ViewsExceptionTestCase(UrlResetMixin, ModuleStoreTestCase): ...@@ -81,7 +81,7 @@ class ViewsExceptionTestCase(UrlResetMixin, ModuleStoreTestCase):
mock_from_django_user.return_value = Mock() mock_from_django_user.return_value = Mock()
url = reverse('django_comment_client.forum.views.followed_threads', 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.response = self.client.get(url)
self.assertEqual(self.response.status_code, 404) self.assertEqual(self.response.status_code, 404)
...@@ -173,7 +173,7 @@ class SingleThreadTestCase(ModuleStoreTestCase): ...@@ -173,7 +173,7 @@ class SingleThreadTestCase(ModuleStoreTestCase):
request.user = self.student request.user = self.student
response = views.single_thread( response = views.single_thread(
request, request,
self.course.id, self.course.id.to_deprecated_string(),
"dummy_discussion_id", "dummy_discussion_id",
"test_thread_id" "test_thread_id"
) )
...@@ -208,7 +208,7 @@ class SingleThreadTestCase(ModuleStoreTestCase): ...@@ -208,7 +208,7 @@ class SingleThreadTestCase(ModuleStoreTestCase):
request.user = self.student request.user = self.student
response = views.single_thread( response = views.single_thread(
request, request,
self.course.id, self.course.id.to_deprecated_string(),
"dummy_discussion_id", "dummy_discussion_id",
"test_thread_id" "test_thread_id"
) )
...@@ -237,7 +237,7 @@ class SingleThreadTestCase(ModuleStoreTestCase): ...@@ -237,7 +237,7 @@ class SingleThreadTestCase(ModuleStoreTestCase):
request = RequestFactory().post("dummy_url") request = RequestFactory().post("dummy_url")
response = views.single_thread( response = views.single_thread(
request, request,
self.course.id, self.course.id.to_deprecated_string(),
"dummy_discussion_id", "dummy_discussion_id",
"dummy_thread_id" "dummy_thread_id"
) )
...@@ -252,7 +252,7 @@ class SingleThreadTestCase(ModuleStoreTestCase): ...@@ -252,7 +252,7 @@ class SingleThreadTestCase(ModuleStoreTestCase):
Http404, Http404,
views.single_thread, views.single_thread,
request, request,
self.course.id, self.course.id.to_deprecated_string(),
"test_discussion_id", "test_discussion_id",
"test_thread_id" "test_thread_id"
) )
...@@ -277,7 +277,7 @@ class UserProfileTestCase(ModuleStoreTestCase): ...@@ -277,7 +277,7 @@ class UserProfileTestCase(ModuleStoreTestCase):
request.user = self.student request.user = self.student
response = views.user_profile( response = views.user_profile(
request, request,
self.course.id, self.course.id.to_deprecated_string(),
self.profiled_user.id self.profiled_user.id
) )
mock_request.assert_any_call( mock_request.assert_any_call(
...@@ -342,7 +342,7 @@ class UserProfileTestCase(ModuleStoreTestCase): ...@@ -342,7 +342,7 @@ class UserProfileTestCase(ModuleStoreTestCase):
with self.assertRaises(Http404): with self.assertRaises(Http404):
response = views.user_profile( response = views.user_profile(
request, request,
self.course.id, self.course.id.to_deprecated_string(),
-999 -999
) )
...@@ -362,7 +362,7 @@ class UserProfileTestCase(ModuleStoreTestCase): ...@@ -362,7 +362,7 @@ class UserProfileTestCase(ModuleStoreTestCase):
request.user = self.student request.user = self.student
response = views.user_profile( response = views.user_profile(
request, request,
self.course.id, self.course.id.to_deprecated_string(),
self.profiled_user.id self.profiled_user.id
) )
self.assertEqual(response.status_code, 405) self.assertEqual(response.status_code, 405)
...@@ -406,7 +406,7 @@ class CommentsServiceRequestHeadersTestCase(UrlResetMixin, ModuleStoreTestCase): ...@@ -406,7 +406,7 @@ class CommentsServiceRequestHeadersTestCase(UrlResetMixin, ModuleStoreTestCase):
reverse( reverse(
"django_comment_client.forum.views.single_thread", "django_comment_client.forum.views.single_thread",
kwargs={ kwargs={
"course_id": self.course.id, "course_id": self.course.id.to_deprecated_string(),
"discussion_id": "dummy", "discussion_id": "dummy",
"thread_id": thread_id, "thread_id": thread_id,
} }
...@@ -422,7 +422,7 @@ class CommentsServiceRequestHeadersTestCase(UrlResetMixin, ModuleStoreTestCase): ...@@ -422,7 +422,7 @@ class CommentsServiceRequestHeadersTestCase(UrlResetMixin, ModuleStoreTestCase):
self.client.get( self.client.get(
reverse( reverse(
"django_comment_client.forum.views.forum_form_discussion", "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") self.assert_all_calls_have_header(mock_request, "X-Edx-Api-Key", "test_api_key")
...@@ -441,7 +441,7 @@ class InlineDiscussionUnicodeTestCase(ModuleStoreTestCase, UnicodeTestMixin): ...@@ -441,7 +441,7 @@ class InlineDiscussionUnicodeTestCase(ModuleStoreTestCase, UnicodeTestMixin):
request = RequestFactory().get("dummy_url") request = RequestFactory().get("dummy_url")
request.user = self.student 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) self.assertEqual(response.status_code, 200)
response_data = json.loads(response.content) response_data = json.loads(response.content)
self.assertEqual(response_data["discussion_data"][0]["title"], text) self.assertEqual(response_data["discussion_data"][0]["title"], text)
...@@ -462,7 +462,7 @@ class ForumFormDiscussionUnicodeTestCase(ModuleStoreTestCase, UnicodeTestMixin): ...@@ -462,7 +462,7 @@ class ForumFormDiscussionUnicodeTestCase(ModuleStoreTestCase, UnicodeTestMixin):
request.user = self.student request.user = self.student
request.META["HTTP_X_REQUESTED_WITH"] = "XMLHttpRequest" # so request.is_ajax() == True 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) self.assertEqual(response.status_code, 200)
response_data = json.loads(response.content) response_data = json.loads(response.content)
self.assertEqual(response_data["discussion_data"][0]["title"], text) self.assertEqual(response_data["discussion_data"][0]["title"], text)
...@@ -484,7 +484,7 @@ class SingleThreadUnicodeTestCase(ModuleStoreTestCase, UnicodeTestMixin): ...@@ -484,7 +484,7 @@ class SingleThreadUnicodeTestCase(ModuleStoreTestCase, UnicodeTestMixin):
request.user = self.student request.user = self.student
request.META["HTTP_X_REQUESTED_WITH"] = "XMLHttpRequest" # so request.is_ajax() == True 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) self.assertEqual(response.status_code, 200)
response_data = json.loads(response.content) response_data = json.loads(response.content)
self.assertEqual(response_data["content"]["title"], text) self.assertEqual(response_data["content"]["title"], text)
...@@ -505,7 +505,7 @@ class UserProfileUnicodeTestCase(ModuleStoreTestCase, UnicodeTestMixin): ...@@ -505,7 +505,7 @@ class UserProfileUnicodeTestCase(ModuleStoreTestCase, UnicodeTestMixin):
request.user = self.student request.user = self.student
request.META["HTTP_X_REQUESTED_WITH"] = "XMLHttpRequest" # so request.is_ajax() == True 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) self.assertEqual(response.status_code, 200)
response_data = json.loads(response.content) response_data = json.loads(response.content)
self.assertEqual(response_data["discussion_data"][0]["title"], text) self.assertEqual(response_data["discussion_data"][0]["title"], text)
...@@ -526,7 +526,7 @@ class FollowedThreadsUnicodeTestCase(ModuleStoreTestCase, UnicodeTestMixin): ...@@ -526,7 +526,7 @@ class FollowedThreadsUnicodeTestCase(ModuleStoreTestCase, UnicodeTestMixin):
request.user = self.student request.user = self.student
request.META["HTTP_X_REQUESTED_WITH"] = "XMLHttpRequest" # so request.is_ajax() == True 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) self.assertEqual(response.status_code, 200)
response_data = json.loads(response.content) response_data = json.loads(response.content)
self.assertEqual(response_data["discussion_data"][0]["title"], text) self.assertEqual(response_data["discussion_data"][0]["title"], text)
......
...@@ -21,6 +21,8 @@ from django_comment_client.utils import (merge_dict, extract, strip_none, add_co ...@@ -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 django_comment_client.utils as utils
import lms.lib.comment_client as cc import lms.lib.comment_client as cc
from xmodule.modulestore.locations import SlashSeparatedCourseKey
THREADS_PER_PAGE = 20 THREADS_PER_PAGE = 20
INLINE_THREADS_PER_PAGE = 20 INLINE_THREADS_PER_PAGE = 20
PAGES_NEARBY_DELTA = 2 PAGES_NEARBY_DELTA = 2
...@@ -41,7 +43,7 @@ def get_threads(request, course_id, discussion_id=None, per_page=THREADS_PER_PAG ...@@ -41,7 +43,7 @@ def get_threads(request, course_id, discussion_id=None, per_page=THREADS_PER_PAG
'sort_order': 'desc', 'sort_order': 'desc',
'text': '', 'text': '',
'commentable_id': discussion_id, 'commentable_id': discussion_id,
'course_id': course_id, 'course_id': course_id.to_deprecated_string(),
'user_id': request.user.id, 'user_id': request.user.id,
} }
...@@ -111,8 +113,9 @@ def inline_discussion(request, course_id, discussion_id): ...@@ -111,8 +113,9 @@ def inline_discussion(request, course_id, discussion_id):
Renders JSON for DiscussionModules Renders JSON for DiscussionModules
""" """
nr_transaction = newrelic.agent.current_transaction() 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) 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) cc_user = cc.User.from_django_user(request.user)
...@@ -166,9 +169,10 @@ def forum_form_discussion(request, course_id): ...@@ -166,9 +169,10 @@ def forum_form_discussion(request, course_id):
""" """
Renders the main Discussion page, potentially filtered by a search query 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() 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"): with newrelic.agent.FunctionTrace(nr_transaction, "get_discussion_category_map"):
category_map = utils.get_discussion_category_map(course) category_map = utils.get_discussion_category_map(course)
...@@ -206,13 +210,13 @@ def forum_form_discussion(request, course_id): ...@@ -206,13 +210,13 @@ def forum_form_discussion(request, course_id):
'csrf': csrf(request)['csrf_token'], 'csrf': csrf(request)['csrf_token'],
'course': course, 'course': course,
#'recent_active_threads': recent_active_threads, #'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), 'threads': saxutils.escape(json.dumps(threads), escapedict),
'thread_pages': query_params['num_pages'], 'thread_pages': query_params['num_pages'],
'user_info': saxutils.escape(json.dumps(user_info), escapedict), '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), '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, 'category_map': category_map,
'roles': saxutils.escape(json.dumps(utils.get_role_ids(course_id)), escapedict), 'roles': saxutils.escape(json.dumps(utils.get_role_ids(course_id)), escapedict),
'is_moderator': cached_has_permission(request.user, "see_all_cohorts", course_id), 'is_moderator': cached_has_permission(request.user, "see_all_cohorts", course_id),
...@@ -228,9 +232,10 @@ def forum_form_discussion(request, course_id): ...@@ -228,9 +232,10 @@ def forum_form_discussion(request, course_id):
@require_GET @require_GET
@login_required @login_required
def single_thread(request, course_id, discussion_id, thread_id): def single_thread(request, course_id, discussion_id, thread_id):
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
nr_transaction = newrelic.agent.current_transaction() 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) cc_user = cc.User.from_django_user(request.user)
user_info = cc_user.to_dict() user_info = cc_user.to_dict()
...@@ -267,7 +272,7 @@ def single_thread(request, course_id, discussion_id, thread_id): ...@@ -267,7 +272,7 @@ def single_thread(request, course_id, discussion_id, thread_id):
threads, query_params = get_threads(request, course_id) threads, query_params = get_threads(request, course_id)
threads.append(thread.to_dict()) 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"): with newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"):
add_courseware_context(threads, course) add_courseware_context(threads, course)
...@@ -298,7 +303,7 @@ def single_thread(request, course_id, discussion_id, thread_id): ...@@ -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), 'annotated_content_info': saxutils.escape(json.dumps(annotated_content_info), escapedict),
'course': course, 'course': course,
#'recent_active_threads': recent_active_threads, #'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, 'thread_id': thread_id,
'threads': saxutils.escape(json.dumps(threads), escapedict), 'threads': saxutils.escape(json.dumps(threads), escapedict),
'category_map': category_map, 'category_map': category_map,
...@@ -306,7 +311,7 @@ def single_thread(request, course_id, discussion_id, thread_id): ...@@ -306,7 +311,7 @@ def single_thread(request, course_id, discussion_id, thread_id):
'thread_pages': query_params['num_pages'], 'thread_pages': query_params['num_pages'],
'is_course_cohorted': is_course_cohorted(course_id), 'is_course_cohorted': is_course_cohorted(course_id),
'is_moderator': cached_has_permission(request.user, "see_all_cohorts", 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, 'cohorts': cohorts,
'user_cohort': get_cohort_id(request.user, course_id), 'user_cohort': get_cohort_id(request.user, course_id),
'cohorted_commentables': cohorted_commentables 'cohorted_commentables': cohorted_commentables
...@@ -317,10 +322,11 @@ def single_thread(request, course_id, discussion_id, thread_id): ...@@ -317,10 +322,11 @@ def single_thread(request, course_id, discussion_id, thread_id):
@require_GET @require_GET
@login_required @login_required
def user_profile(request, course_id, user_id): def user_profile(request, course_id, user_id):
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
nr_transaction = newrelic.agent.current_transaction() nr_transaction = newrelic.agent.current_transaction()
#TODO: Allow sorting? #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: try:
profiled_user = cc.User(id=user_id, course_id=course_id) profiled_user = cc.User(id=user_id, course_id=course_id)
...@@ -365,9 +371,10 @@ def user_profile(request, course_id, user_id): ...@@ -365,9 +371,10 @@ def user_profile(request, course_id, user_id):
@login_required @login_required
def followed_threads(request, course_id, user_id): def followed_threads(request, course_id, user_id):
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
nr_transaction = newrelic.agent.current_transaction() 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: try:
profiled_user = cc.User(id=user_id, course_id=course_id) profiled_user = cc.User(id=user_id, course_id=course_id)
......
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from django_comment_common.utils import seed_permissions_roles from django_comment_common.utils import seed_permissions_roles
from xmodule.modulestore.locations import SlashSeparatedCourseKey
class Command(BaseCommand): class Command(BaseCommand):
...@@ -11,6 +12,6 @@ class Command(BaseCommand): ...@@ -11,6 +12,6 @@ class Command(BaseCommand):
raise CommandError("Please provide a course id") raise CommandError("Please provide a course id")
if len(args) > 1: if len(args) > 1:
raise CommandError("Too many arguments") raise CommandError("Too many arguments")
course_id = args[0] course_id = SlashSeparatedCourseKey.from_deprecated_string(args[0])
seed_permissions_roles(course_id) seed_permissions_roles(course_id)
import django_comment_common.models as models import django_comment_common.models as models
from django.test import TestCase from django.test import TestCase
from xmodule.modulestore.locations import SlashSeparatedCourseKey
class RoleClassTestCase(TestCase): class RoleClassTestCase(TestCase):
def setUp(self): def setUp(self):
# For course ID, syntax edx/classname/classdate is important # For course ID, syntax edx/classname/classdate is important
# because xmodel.course_module.id_to_location looks for a string to split # 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", self.student_role = models.Role.objects.get_or_create(name="Student",
course_id=self.course_id)[0] course_id=self.course_id)[0]
self.student_role.add_permission("delete_thread") self.student_role.add_permission("delete_thread")
...@@ -15,7 +17,7 @@ class RoleClassTestCase(TestCase): ...@@ -15,7 +17,7 @@ class RoleClassTestCase(TestCase):
course_id=self.course_id)[0] course_id=self.course_id)[0]
self.TA_role = models.Role.objects.get_or_create(name="Community TA", self.TA_role = models.Role.objects.get_or_create(name="Community TA",
course_id=self.course_id)[0] 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", self.TA_role_2 = models.Role.objects.get_or_create(name="Community TA",
course_id=self.course_id_2)[0] course_id=self.course_id_2)[0]
......
...@@ -41,9 +41,11 @@ class DictionaryTestCase(TestCase): ...@@ -41,9 +41,11 @@ class DictionaryTestCase(TestCase):
self.assertEqual(utils.merge_dict(d1, d2), expected) self.assertEqual(utils.merge_dict(d1, d2), expected)
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
class AccessUtilsTestCase(TestCase): class AccessUtilsTestCase(TestCase):
def setUp(self): 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.student_role = RoleFactory(name='Student', course_id=self.course_id)
self.moderator_role = RoleFactory(name='Moderator', 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) self.community_ta_role = RoleFactory(name='Community TA', course_id=self.course_id)
...@@ -121,8 +123,8 @@ class CoursewareContextTestCase(ModuleStoreTestCase): ...@@ -121,8 +123,8 @@ class CoursewareContextTestCase(ModuleStoreTestCase):
reverse( reverse(
"jump_to", "jump_to",
kwargs={ kwargs={
"course_id": self.course.location.course_id, "course_id": self.course.id.to_deprecated_string(),
"location": discussion.location "location": discussion.location.to_deprecated_string()
} }
) )
) )
......
...@@ -15,8 +15,9 @@ from edxmako import lookup_template ...@@ -15,8 +15,9 @@ from edxmako import lookup_template
import pystache_custom as pystache import pystache_custom as pystache
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore import Location
from django.utils.timezone import UTC from django.utils.timezone import UTC
from xmodule.modulestore.locations import i4xEncoder, SlashSeparatedCourseKey
import json
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -55,10 +56,7 @@ def has_forum_access(uname, course_id, rolename): ...@@ -55,10 +56,7 @@ def has_forum_access(uname, course_id, rolename):
def _get_discussion_modules(course): def _get_discussion_modules(course):
all_modules = modulestore().get_items( all_modules = modulestore().get_items(course.id, category='discussion')
Location('i4x', course.location.org, course.location.course, 'discussion', None),
course_id=course.id
)
def has_required_keys(module): def has_required_keys(module):
for key in ('discussion_id', 'discussion_category', 'discussion_target'): for key in ('discussion_id', 'discussion_category', 'discussion_target'):
...@@ -198,7 +196,7 @@ def get_discussion_category_map(course): ...@@ -198,7 +196,7 @@ def get_discussion_category_map(course):
class JsonResponse(HttpResponse): class JsonResponse(HttpResponse):
def __init__(self, data=None): def __init__(self, data=None):
content = simplejson.dumps(data) content = json.dumps(data, cls=i4xEncoder)
super(JsonResponse, self).__init__(content, super(JsonResponse, self).__init__(content,
mimetype='application/json; charset=utf-8') mimetype='application/json; charset=utf-8')
...@@ -311,12 +309,16 @@ def render_mustache(template_name, dictionary, *args, **kwargs): ...@@ -311,12 +309,16 @@ def render_mustache(template_name, dictionary, *args, **kwargs):
def permalink(content): 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': if content['type'] == 'thread':
return reverse('django_comment_client.forum.views.single_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: else:
return reverse('django_comment_client.forum.views.single_thread', 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): def extend_content(content):
...@@ -344,10 +346,10 @@ def add_courseware_context(content_list, course): ...@@ -344,10 +346,10 @@ def add_courseware_context(content_list, course):
for content in content_list: for content in content_list:
commentable_id = content['commentable_id'] commentable_id = content['commentable_id']
if commentable_id in id_map: 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"] 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}) "location": location})
content.update({"courseware_url": url, "courseware_title": title}) content.update({"courseware_url": url, "courseware_title": title})
......
...@@ -8,11 +8,12 @@ from django.core.urlresolvers import reverse ...@@ -8,11 +8,12 @@ from django.core.urlresolvers import reverse
from foldit.views import foldit_ops, verify_code from foldit.views import foldit_ops, verify_code
from foldit.models import PuzzleComplete, Score from foldit.models import PuzzleComplete, Score
from student.models import unique_id_for_user from student.models import unique_id_for_user, CourseEnrollment
from student.tests.factories import CourseEnrollmentFactory, UserFactory from student.tests.factories import UserFactory
from datetime import datetime, timedelta from datetime import datetime, timedelta
from pytz import UTC from pytz import UTC
from xmodule.modulestore.locations import SlashSeparatedCourseKey
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -23,18 +24,14 @@ class FolditTestCase(TestCase): ...@@ -23,18 +24,14 @@ class FolditTestCase(TestCase):
self.factory = RequestFactory() self.factory = RequestFactory()
self.url = reverse('foldit_ops') self.url = reverse('foldit_ops')
self.course_id = 'course/id/1' self.course_id = SlashSeparatedCourseKey('course', 'id', '1')
self.course_id2 = 'course/id/2' self.course_id2 = SlashSeparatedCourseKey('course', 'id', '2')
self.user = UserFactory.create() self.user = UserFactory.create()
self.user2 = UserFactory.create() self.user2 = UserFactory.create()
self.course_enrollment = CourseEnrollmentFactory.create( CourseEnrollment.enroll(self.user, self.course_id)
user=self.user, course_id=self.course_id CourseEnrollment.enroll(self.user2, self.course_id2)
)
self.course_enrollment2 = CourseEnrollmentFactory.create(
user=self.user2, course_id=self.course_id2
)
now = datetime.now(UTC) now = datetime.now(UTC)
self.tomorrow = now + timedelta(days=1) self.tomorrow = now + timedelta(days=1)
......
...@@ -31,7 +31,7 @@ def list_with_level(course, level): ...@@ -31,7 +31,7 @@ def list_with_level(course, level):
There could be other levels specific to the course. There could be other levels specific to the course.
If there is no Group for that course-level, returns an empty list 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): def allow_access(course, user, level):
...@@ -63,7 +63,7 @@ def _change_access(course, user, level, action): ...@@ -63,7 +63,7 @@ def _change_access(course, user, level, action):
""" """
try: try:
role = ROLES[level](course.location) role = ROLES[level](course.id)
except KeyError: except KeyError:
raise ValueError("unrecognized level '{}'".format(level)) raise ValueError("unrecognized level '{}'".format(level))
......
...@@ -86,7 +86,6 @@ def enroll_email(course_id, student_email, auto_enroll=False, email_students=Fal ...@@ -86,7 +86,6 @@ def enroll_email(course_id, student_email, auto_enroll=False, email_students=Fal
returns two EmailEnrollmentState's returns two EmailEnrollmentState's
representing state before and after the action. representing state before and after the action.
""" """
previous_state = EmailEnrollmentState(course_id, student_email) previous_state = EmailEnrollmentState(course_id, student_email)
if previous_state.user: if previous_state.user:
...@@ -121,7 +120,6 @@ def unenroll_email(course_id, student_email, email_students=False, email_params= ...@@ -121,7 +120,6 @@ def unenroll_email(course_id, student_email, email_students=False, email_params=
returns two EmailEnrollmentState's returns two EmailEnrollmentState's
representing state before and after the action. representing state before and after the action.
""" """
previous_state = EmailEnrollmentState(course_id, student_email) previous_state = EmailEnrollmentState(course_id, student_email)
if previous_state.enrollment: if previous_state.enrollment:
...@@ -200,7 +198,7 @@ def reset_student_attempts(course_id, student, module_state_key, delete_module=F ...@@ -200,7 +198,7 @@ def reset_student_attempts(course_id, student, module_state_key, delete_module=F
module_to_reset = StudentModule.objects.get( module_to_reset = StudentModule.objects.get(
student_id=student.id, student_id=student.id,
course_id=course_id, course_id=course_id,
module_state_key=module_state_key module_id=module_state_key
) )
if delete_module: if delete_module:
...@@ -237,13 +235,15 @@ def get_email_params(course, auto_enroll): ...@@ -237,13 +235,15 @@ def get_email_params(course, auto_enroll):
'SITE_NAME', 'SITE_NAME',
settings.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( registration_url = u'https://{}{}'.format(
stripped_site_name, stripped_site_name,
reverse('student.views.register_user') reverse('student.views.register_user')
) )
course_url = u'https://{}{}'.format( course_url = u'https://{}{}'.format(
stripped_site_name, 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. # 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): ...@@ -251,7 +251,7 @@ def get_email_params(course, auto_enroll):
if not settings.FEATURES.get('ENABLE_MKTG_SITE', False): if not settings.FEATURES.get('ENABLE_MKTG_SITE', False):
course_about_url = u'https://{}{}'.format( course_about_url = u'https://{}{}'.format(
stripped_site_name, 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) is_shib_course = uses_shib(course)
......
...@@ -29,23 +29,23 @@ def make_populated_course(step): # pylint: disable=unused-argument ...@@ -29,23 +29,23 @@ def make_populated_course(step): # pylint: disable=unused-argument
number='888', number='888',
display_name='Bulk Email Test Course' 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: try:
# See if we've defined the instructor & staff user yet # See if we've defined the instructor & staff user yet
world.bulk_email_instructor world.bulk_email_instructor
except AttributeError: except AttributeError:
# Make & register an instructor for the course # 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) world.enroll_user(world.bulk_email_instructor, world.bulk_email_course_id)
# Make & register a staff member # 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) world.enroll_user(world.bulk_email_staff, world.bulk_email_course_id)
# Make & register a student # Make & register a student
world.register_by_course_id( world.register_by_course_key(
'edx/888/Bulk_Email_Test_Course', course.id,
username='student', username='student',
password='test', password='test',
is_staff=False is_staff=False
......
...@@ -43,12 +43,12 @@ def i_am_staff_or_instructor(step, role): # pylint: disable=unused-argument ...@@ -43,12 +43,12 @@ def i_am_staff_or_instructor(step, role): # pylint: disable=unused-argument
display_name='Test Course' display_name='Test Course'
) )
world.course_id = 'edx/999/Test_Course' world.course_id = course.id
world.role = 'instructor' world.role = 'instructor'
# Log in as the an instructor or staff for the course # Log in as the an instructor or staff for the course
if role == 'instructor': if role == 'instructor':
# Make & register an instructor for the course # 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.enroll_user(world.instructor, world.course_id)
world.log_in( world.log_in(
...@@ -61,7 +61,7 @@ def i_am_staff_or_instructor(step, role): # pylint: disable=unused-argument ...@@ -61,7 +61,7 @@ def i_am_staff_or_instructor(step, role): # pylint: disable=unused-argument
else: else:
world.role = 'staff' world.role = 'staff'
# Make & register a staff member # 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.enroll_user(world.staff, world.course_id)
world.log_in( world.log_in(
......
...@@ -19,8 +19,9 @@ from courseware.courses import get_course_with_access ...@@ -19,8 +19,9 @@ from courseware.courses import get_course_with_access
from courseware.models import XModuleUserStateSummaryField from courseware.models import XModuleUserStateSummaryField
import courseware.module_render as module_render import courseware.module_render as module_render
import courseware.model_data as model_data import courseware.model_data as model_data
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.locations import SlashSeparatedCourseKey
from xmodule.modulestore.exceptions import ItemNotFoundError
@ensure_csrf_cookie @ensure_csrf_cookie
...@@ -28,13 +29,14 @@ def hint_manager(request, course_id): ...@@ -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. The URL landing function for all calls to the hint manager, both POST and GET.
""" """
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
try: 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: except Http404:
out = 'Sorry, but students are not allowed to access the hint manager!' out = 'Sorry, but students are not allowed to access the hint manager!'
return HttpResponse(out) return HttpResponse(out)
if request.method == 'GET': if request.method == 'GET':
out = get_hints(request, course_id, 'mod_queue') out = get_hints(request, course_key, 'mod_queue')
out.update({'error': ''}) out.update({'error': ''})
return render_to_response('instructor/hint_manager.html', out) return render_to_response('instructor/hint_manager.html', out)
field = request.POST['field'] field = request.POST['field']
...@@ -52,10 +54,10 @@ def hint_manager(request, course_id): ...@@ -52,10 +54,10 @@ def hint_manager(request, course_id):
} }
# Do the operation requested, and collect any error messages. # 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: if error_text is None:
error_text = '' error_text = ''
render_dict = get_hints(request, course_id, field) render_dict = get_hints(request, course_key, field)
render_dict.update({'error': error_text}) render_dict.update({'error': error_text})
rendered_html = render_to_string('instructor/hint_manager_inner.html', render_dict) rendered_html = render_to_string('instructor/hint_manager_inner.html', render_dict)
return HttpResponse(json.dumps({'success': True, 'contents': rendered_html})) return HttpResponse(json.dumps({'success': True, 'contents': rendered_html}))
...@@ -86,13 +88,13 @@ def get_hints(request, course_id, field): ...@@ -86,13 +88,13 @@ def get_hints(request, course_id, field):
other_field = 'mod_queue' other_field = 'mod_queue'
field_label = 'Approved Hints' field_label = 'Approved Hints'
other_field_label = 'Hints Awaiting Moderation' 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. # 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. # To do this, just take the school/number part - leave off the classname.
course_id_dict = Location.parse_course_id(course_id) # FIXME: we need to figure out how to do this with opaque keys
chopped_id = u'{org}/{course}'.format(**course_id_dict) all_hints = XModuleUserStateSummaryField.objects.filter(
chopped_id = re.escape(chopped_id) field_name=field,
all_hints = XModuleUserStateSummaryField.objects.filter(field_name=field, usage_id__regex=chopped_id) 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[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 maps a problem id to a list of [answer, hints] pairs, sorted in order of answer.
big_out_dict = {} big_out_dict = {}
...@@ -101,8 +103,8 @@ def get_hints(request, course_id, field): ...@@ -101,8 +103,8 @@ def get_hints(request, course_id, field):
id_to_name = {} id_to_name = {}
for hints_by_problem in all_hints: for hints_by_problem in all_hints:
loc = Location(hints_by_problem.usage_id) hints_by_problem.usage_id = hints_by_problem.usage_id.map_into_course(course_id)
name = location_to_problem_name(course_id, loc) name = location_to_problem_name(course_id, hints_by_problem.usage_id)
if name is None: if name is None:
continue continue
id_to_name[hints_by_problem.usage_id] = name id_to_name[hints_by_problem.usage_id] = name
...@@ -138,9 +140,9 @@ def location_to_problem_name(course_id, loc): ...@@ -138,9 +140,9 @@ def location_to_problem_name(course_id, loc):
problem it wraps around. Return None if the hinter no longer exists. problem it wraps around. Return None if the hinter no longer exists.
""" """
try: try:
descriptor = modulestore().get_items(loc, course_id=course_id)[0] descriptor = modulestore().get_item(loc)
return descriptor.get_children()[0].display_name return descriptor.get_children()[0].display_name
except IndexError: except ItemNotFoundError:
# Sometimes, the problem is no longer in the course. Just # Sometimes, the problem is no longer in the course. Just
# don't include said problem. # don't include said problem.
return None return None
...@@ -164,9 +166,10 @@ def delete_hints(request, course_id, field): ...@@ -164,9 +166,10 @@ def delete_hints(request, course_id, field):
if key == 'op' or key == 'field': if key == 'op' or key == 'field':
continue continue
problem_id, answer, pk = request.POST.getlist(key) 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 # Can be optimized - sort the delete list by problem_id, and load each problem
# from the database only once. # 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) problem_dict = json.loads(this_problem.value)
del problem_dict[answer][pk] del problem_dict[answer][pk]
this_problem.value = json.dumps(problem_dict) this_problem.value = json.dumps(problem_dict)
...@@ -191,7 +194,8 @@ def change_votes(request, course_id, field): ...@@ -191,7 +194,8 @@ def change_votes(request, course_id, field):
if key == 'op' or key == 'field': if key == 'op' or key == 'field':
continue continue
problem_id, answer, pk, new_votes = request.POST.getlist(key) 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 = json.loads(this_problem.value)
# problem_dict[answer][pk] points to a [hint_text, #votes] pair. # problem_dict[answer][pk] points to a [hint_text, #votes] pair.
problem_dict[answer][pk][1] = int(new_votes) problem_dict[answer][pk][1] = int(new_votes)
...@@ -210,23 +214,27 @@ def add_hint(request, course_id, field): ...@@ -210,23 +214,27 @@ def add_hint(request, course_id, field):
""" """
problem_id = request.POST['problem'] problem_id = request.POST['problem']
problem_key = course_id.make_usage_key_from_deprecated_string(problem_id)
answer = request.POST['answer'] answer = request.POST['answer']
hint_text = request.POST['hint'] hint_text = request.POST['hint']
# Validate the answer. This requires initializing the xmodules, which # Validate the answer. This requires initializing the xmodules, which
# is annoying. # is annoying.
loc = Location(problem_id) try:
descriptors = modulestore().get_items(loc, course_id=course_id) descriptor = modulestore().get_item(problem_key)
descriptors = [descriptor]
except ItemNotFoundError:
descriptors = []
field_data_cache = model_data.FieldDataCache(descriptors, course_id, request.user) 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): if not hinter_module.validate_answer(answer):
# Invalid answer. Don't add it to the database, or else the # Invalid answer. Don't add it to the database, or else the
# hinter will crash when we encounter it. # hinter will crash when we encounter it.
return 'Error - the answer you specified is not properly formatted: ' + str(answer) 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) this_pk = int(hint_pk_entry.value)
hint_pk_entry.value = this_pk + 1 hint_pk_entry.value = this_pk + 1
hint_pk_entry.save() hint_pk_entry.save()
...@@ -253,16 +261,17 @@ def approve(request, course_id, field): ...@@ -253,16 +261,17 @@ def approve(request, course_id, field):
if key == 'op' or key == 'field': if key == 'op' or key == 'field':
continue continue
problem_id, answer, pk = request.POST.getlist(key) 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 # Can be optimized - sort the delete list by problem_id, and load each problem
# from the database only once. # 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) problem_dict = json.loads(problem_in_mod.value)
hint_to_move = problem_dict[answer][pk] hint_to_move = problem_dict[answer][pk]
del problem_dict[answer][pk] del problem_dict[answer][pk]
problem_in_mod.value = json.dumps(problem_dict) problem_in_mod.value = json.dumps(problem_dict)
problem_in_mod.save() 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) problem_dict = json.loads(problem_in_hints.value)
if answer not in problem_dict: if answer not in problem_dict:
problem_dict[answer] = {} problem_dict[answer] = {}
......
...@@ -6,6 +6,8 @@ from django.core.management.base import BaseCommand ...@@ -6,6 +6,8 @@ from django.core.management.base import BaseCommand
from optparse import make_option from optparse import make_option
from xmodule.modulestore.django import modulestore 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.openendedchild import OpenEndedChild
from xmodule.open_ended_grading_classes.open_ended_module import OpenEndedModule from xmodule.open_ended_grading_classes.open_ended_module import OpenEndedModule
...@@ -37,8 +39,8 @@ class Command(BaseCommand): ...@@ -37,8 +39,8 @@ class Command(BaseCommand):
task_number = options['task_number'] task_number = options['task_number']
if len(args) == 4: if len(args) == 4:
course_id = args[0] course_id = SlashSeparatedCourseKey.from_deprecated_string(args[0])
location = args[1] location = course_id.make_usage_key_from_deprecated_string(args[1])
students_ids = [line.strip() for line in open(args[2])] students_ids = [line.strip() for line in open(args[2])]
hostname = args[3] hostname = args[3]
else: else:
...@@ -51,7 +53,7 @@ class Command(BaseCommand): ...@@ -51,7 +53,7 @@ class Command(BaseCommand):
print err print err
return return
descriptor = modulestore().get_instance(course.id, location, depth=0) descriptor = modulestore().get_item(location, depth=0)
if descriptor is None: if descriptor is None:
print "Location not found in course" print "Location not found in course"
return return
...@@ -76,7 +78,7 @@ def post_submission_for_student(student, course, location, task_number, dry_run= ...@@ -76,7 +78,7 @@ def post_submission_for_student(student, course, location, task_number, dry_run=
request.host = hostname request.host = hostname
try: try:
module = get_module_for_student(student, course, location, request=request) module = get_module_for_student(student, location, request=request)
if module is None: if module is None:
print " WARNING: No state found." print " WARNING: No state found."
return False return False
......
...@@ -9,6 +9,8 @@ from optparse import make_option ...@@ -9,6 +9,8 @@ from optparse import make_option
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore 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.openendedchild import OpenEndedChild
from courseware.courses import get_course from courseware.courses import get_course
...@@ -37,8 +39,8 @@ class Command(BaseCommand): ...@@ -37,8 +39,8 @@ class Command(BaseCommand):
task_number = options['task_number'] task_number = options['task_number']
if len(args) == 2: if len(args) == 2:
course_id = args[0] course_id = SlashSeparatedCourseKey.from_deprecated_string(args[0])
location = args[1] usage_key = UsageKey.from_string(args[1])
else: else:
print self.help print self.help
return return
...@@ -49,16 +51,16 @@ class Command(BaseCommand): ...@@ -49,16 +51,16 @@ class Command(BaseCommand):
print err print err
return return
descriptor = modulestore().get_instance(course.id, location, depth=0) descriptor = modulestore().get_item(usage_key, depth=0)
if descriptor is None: if descriptor is None:
print "Location {0} not found in course".format(location) print "Location {0} not found in course".format(usage_key)
return return
try: try:
enrolled_students = CourseEnrollment.users_enrolled_in(course_id) enrolled_students = CourseEnrollment.users_enrolled_in(course_id)
print "Total students enrolled in {0}: {1}".format(course_id, enrolled_students.count()) 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: except KeyboardInterrupt:
print "\nOperation Cancelled" print "\nOperation Cancelled"
...@@ -79,7 +81,7 @@ def calculate_task_statistics(students, course, location, task_number, write_to_ ...@@ -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_graded_submissions = [] # pylint: disable=invalid-name
students_with_no_state = [] 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()) print "Total student modules: {0}".format(student_modules.count())
for index, student_module in enumerate(student_modules): for index, student_module in enumerate(student_modules):
...@@ -89,7 +91,7 @@ def calculate_task_statistics(students, course, location, task_number, write_to_ ...@@ -89,7 +91,7 @@ def calculate_task_statistics(students, course, location, task_number, write_to_
student = student_module.student student = student_module.student
print "{0}:{1}".format(student.id, student.username) 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: if module is None:
print " WARNING: No state found" print " WARNING: No state found"
students_with_no_state.append(student) students_with_no_state.append(student)
...@@ -113,8 +115,6 @@ def calculate_task_statistics(students, course, location, task_number, write_to_ ...@@ -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: elif task_state == OpenEndedChild.POST_ASSESSMENT or task_state == OpenEndedChild.DONE:
students_with_graded_submissions.append(student) students_with_graded_submissions.append(student)
location = Location(location)
print "----------------------------------" print "----------------------------------"
print "Time: {0}".format(time.strftime("%Y %b %d %H:%M:%S +0000", time.gmtime())) print "Time: {0}".format(time.strftime("%Y %b %d %H:%M:%S +0000", time.gmtime()))
print "Course: {0}".format(course.id) print "Course: {0}".format(course.id)
...@@ -132,7 +132,7 @@ def calculate_task_statistics(students, course, location, task_number, write_to_ ...@@ -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: with open('{0}.{1}.csv'.format(filename, time_stamp), 'wb') as csv_file:
writer = csv.writer(csv_file, delimiter=' ', quoting=csv.QUOTE_MINIMAL) writer = csv.writer(csv_file, delimiter=' ', quoting=csv.QUOTE_MINIMAL)
for student in students_with_ungraded_submissions: 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: 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 return stats
...@@ -24,14 +24,16 @@ from instructor.management.commands.openended_post import post_submission_for_st ...@@ -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.management.commands.openended_stats import calculate_task_statistics
from instructor.utils import get_module_for_student from instructor.utils import get_module_for_student
from xmodule.modulestore.locations import SlashSeparatedCourseKey
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) @override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
class OpenEndedPostTest(ModuleStoreTestCase): class OpenEndedPostTest(ModuleStoreTestCase):
"""Test the openended_post management command.""" """Test the openended_post management command."""
def setUp(self): def setUp(self):
self.course_id = "edX/open_ended/2012_Fall" self.course_id = SlashSeparatedCourseKey("edX", "open_ended", "2012_Fall")
self.problem_location = Location(["i4x", "edX", "open_ended", "combinedopenended", "SampleQuestion"]) self.problem_location = Location("edX", "open_ended", "2012_Fall", "combinedopenended", "SampleQuestion")
self.self_assessment_task_number = 0 self.self_assessment_task_number = 0
self.open_ended_task_number = 1 self.open_ended_task_number = 1
...@@ -41,7 +43,7 @@ class OpenEndedPostTest(ModuleStoreTestCase): ...@@ -41,7 +43,7 @@ class OpenEndedPostTest(ModuleStoreTestCase):
StudentModuleFactory.create( StudentModuleFactory.create(
course_id=self.course_id, course_id=self.course_id,
module_state_key=self.problem_location, module_id=self.problem_location,
student=self.student_on_initial, student=self.student_on_initial,
grade=0, grade=0,
max_grade=1, max_grade=1,
...@@ -50,7 +52,7 @@ class OpenEndedPostTest(ModuleStoreTestCase): ...@@ -50,7 +52,7 @@ class OpenEndedPostTest(ModuleStoreTestCase):
StudentModuleFactory.create( StudentModuleFactory.create(
course_id=self.course_id, course_id=self.course_id,
module_state_key=self.problem_location, module_id=self.problem_location,
student=self.student_on_accessing, student=self.student_on_accessing,
grade=0, grade=0,
max_grade=1, max_grade=1,
...@@ -59,7 +61,7 @@ class OpenEndedPostTest(ModuleStoreTestCase): ...@@ -59,7 +61,7 @@ class OpenEndedPostTest(ModuleStoreTestCase):
StudentModuleFactory.create( StudentModuleFactory.create(
course_id=self.course_id, course_id=self.course_id,
module_state_key=self.problem_location, module_id=self.problem_location,
student=self.student_on_post_assessment, student=self.student_on_post_assessment,
grade=0, grade=0,
max_grade=1, max_grade=1,
...@@ -67,7 +69,7 @@ class OpenEndedPostTest(ModuleStoreTestCase): ...@@ -67,7 +69,7 @@ class OpenEndedPostTest(ModuleStoreTestCase):
) )
def test_post_submission_for_student_on_initial(self): 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) 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) self.assertFalse(dry_run_result)
...@@ -76,7 +78,7 @@ class OpenEndedPostTest(ModuleStoreTestCase): ...@@ -76,7 +78,7 @@ class OpenEndedPostTest(ModuleStoreTestCase):
self.assertFalse(result) self.assertFalse(result)
def test_post_submission_for_student_on_accessing(self): 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) 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) self.assertFalse(dry_run_result)
...@@ -84,11 +86,11 @@ class OpenEndedPostTest(ModuleStoreTestCase): ...@@ -84,11 +86,11 @@ class OpenEndedPostTest(ModuleStoreTestCase):
with patch('capa.xqueue_interface.XQueueInterface.send_to_queue') as mock_send_to_queue: with patch('capa.xqueue_interface.XQueueInterface.send_to_queue') as mock_send_to_queue:
mock_send_to_queue.return_value = (0, "Successfully queued") 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) task = module.child_module.get_task_number(self.open_ended_task_number)
student_response = "Here is an answer." 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) 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) 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): ...@@ -102,7 +104,7 @@ class OpenEndedPostTest(ModuleStoreTestCase):
self.assertGreaterEqual(body_arg_student_info['submission_time'], submission_time) self.assertGreaterEqual(body_arg_student_info['submission_time'], submission_time)
def test_post_submission_for_student_on_post_assessment(self): 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) 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) self.assertFalse(dry_run_result)
...@@ -111,7 +113,7 @@ class OpenEndedPostTest(ModuleStoreTestCase): ...@@ -111,7 +113,7 @@ class OpenEndedPostTest(ModuleStoreTestCase):
self.assertFalse(result) self.assertFalse(result)
def test_post_submission_for_student_invalid_task(self): 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) result = post_submission_for_student(self.student_on_accessing, course, self.problem_location, self.self_assessment_task_number, dry_run=False)
self.assertFalse(result) self.assertFalse(result)
...@@ -126,8 +128,8 @@ class OpenEndedStatsTest(ModuleStoreTestCase): ...@@ -126,8 +128,8 @@ class OpenEndedStatsTest(ModuleStoreTestCase):
"""Test the openended_stats management command.""" """Test the openended_stats management command."""
def setUp(self): def setUp(self):
self.course_id = "edX/open_ended/2012_Fall" self.course_id = SlashSeparatedCourseKey("edX", "open_ended", "2012_Fall")
self.problem_location = Location(["i4x", "edX", "open_ended", "combinedopenended", "SampleQuestion"]) self.problem_location = Location("edX", "open_ended", "2012_Fall", "combinedopenended", "SampleQuestion")
self.task_number = 1 self.task_number = 1
self.invalid_task_number = 3 self.invalid_task_number = 3
...@@ -137,7 +139,7 @@ class OpenEndedStatsTest(ModuleStoreTestCase): ...@@ -137,7 +139,7 @@ class OpenEndedStatsTest(ModuleStoreTestCase):
StudentModuleFactory.create( StudentModuleFactory.create(
course_id=self.course_id, course_id=self.course_id,
module_state_key=self.problem_location, module_id=self.problem_location,
student=self.student_on_initial, student=self.student_on_initial,
grade=0, grade=0,
max_grade=1, max_grade=1,
...@@ -146,7 +148,7 @@ class OpenEndedStatsTest(ModuleStoreTestCase): ...@@ -146,7 +148,7 @@ class OpenEndedStatsTest(ModuleStoreTestCase):
StudentModuleFactory.create( StudentModuleFactory.create(
course_id=self.course_id, course_id=self.course_id,
module_state_key=self.problem_location, module_id=self.problem_location,
student=self.student_on_accessing, student=self.student_on_accessing,
grade=0, grade=0,
max_grade=1, max_grade=1,
...@@ -155,7 +157,7 @@ class OpenEndedStatsTest(ModuleStoreTestCase): ...@@ -155,7 +157,7 @@ class OpenEndedStatsTest(ModuleStoreTestCase):
StudentModuleFactory.create( StudentModuleFactory.create(
course_id=self.course_id, course_id=self.course_id,
module_state_key=self.problem_location, module_id=self.problem_location,
student=self.student_on_post_assessment, student=self.student_on_post_assessment,
grade=0, grade=0,
max_grade=1, max_grade=1,
...@@ -165,7 +167,7 @@ class OpenEndedStatsTest(ModuleStoreTestCase): ...@@ -165,7 +167,7 @@ class OpenEndedStatsTest(ModuleStoreTestCase):
self.students = [self.student_on_initial, self.student_on_accessing, self.student_on_post_assessment] self.students = [self.student_on_initial, self.student_on_accessing, self.student_on_post_assessment]
def test_calculate_task_statistics(self): 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) 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.INITIAL], 1)
self.assertEqual(stats[OpenEndedChild.ASSESSING], 1) self.assertEqual(stats[OpenEndedChild.ASSESSING], 1)
......
...@@ -50,19 +50,19 @@ class TestInstructorAccessAllow(ModuleStoreTestCase): ...@@ -50,19 +50,19 @@ class TestInstructorAccessAllow(ModuleStoreTestCase):
def test_allow(self): def test_allow(self):
user = UserFactory() 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_twice(self): def test_allow_twice(self):
user = UserFactory() user = UserFactory()
allow_access(self.course, user, 'staff') allow_access(self.course, user, 'staff')
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): def test_allow_beta(self):
""" Test allow beta against list beta. """ """ Test allow beta against list beta. """
user = UserFactory() user = UserFactory()
allow_access(self.course, user, 'beta') 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) @raises(ValueError)
def test_allow_badlevel(self): def test_allow_badlevel(self):
...@@ -91,17 +91,17 @@ class TestInstructorAccessRevoke(ModuleStoreTestCase): ...@@ -91,17 +91,17 @@ class TestInstructorAccessRevoke(ModuleStoreTestCase):
def test_revoke(self): def test_revoke(self):
user = self.staff[0] user = self.staff[0]
revoke_access(self.course, user, 'staff') 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): def test_revoke_twice(self):
user = self.staff[0] user = self.staff[0]
revoke_access(self.course, user, 'staff') 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): def test_revoke_beta(self):
user = self.beta_testers[0] user = self.beta_testers[0]
revoke_access(self.course, user, 'beta') 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) @raises(ValueError)
def test_revoke_badrolename(self): def test_revoke_badrolename(self):
......
...@@ -18,6 +18,7 @@ from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE ...@@ -18,6 +18,7 @@ from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE
from mock import patch from mock import patch
from bulk_email.models import CourseAuthorization from bulk_email.models import CourseAuthorization
from xmodule.modulestore.locations import SlashSeparatedCourseKey
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) @override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
...@@ -34,7 +35,7 @@ class TestNewInstructorDashboardEmailViewMongoBacked(ModuleStoreTestCase): ...@@ -34,7 +35,7 @@ class TestNewInstructorDashboardEmailViewMongoBacked(ModuleStoreTestCase):
self.client.login(username=instructor.username, password="test") self.client.login(username=instructor.username, password="test")
# URL for instructor dash # 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 # URL for email view
self.email_link = '<a href="" data-section="send_email">Email</a>' self.email_link = '<a href="" data-section="send_email">Email</a>'
...@@ -115,14 +116,14 @@ class TestNewInstructorDashboardEmailViewXMLBacked(ModuleStoreTestCase): ...@@ -115,14 +116,14 @@ class TestNewInstructorDashboardEmailViewXMLBacked(ModuleStoreTestCase):
Check for email view on the new instructor dashboard Check for email view on the new instructor dashboard
""" """
def setUp(self): def setUp(self):
self.course_name = 'edX/toy/2012_Fall' self.course_key = SlashSeparatedCourseKey('edX', 'toy', '2012_Fall')
# Create instructor account # Create instructor account
instructor = AdminFactory.create() instructor = AdminFactory.create()
self.client.login(username=instructor.username, password="test") self.client.login(username=instructor.username, password="test")
# URL for instructor dash # 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 # URL for email view
self.email_link = '<a href="" data-section="send_email">Email</a>' self.email_link = '<a href="" data-section="send_email">Email</a>'
......
...@@ -26,8 +26,8 @@ class HintManagerTest(ModuleStoreTestCase): ...@@ -26,8 +26,8 @@ class HintManagerTest(ModuleStoreTestCase):
self.user = UserFactory.create(username='robot', email='robot@edx.org', password='test', is_staff=True) self.user = UserFactory.create(username='robot', email='robot@edx.org', password='test', is_staff=True)
self.c = Client() self.c = Client()
self.c.login(username='robot', password='test') self.c.login(username='robot', password='test')
self.problem_id = 'i4x://Me/19.002/crowdsource_hinter/crowdsource_hinter_001' self.course_id = self.course.id
self.course_id = 'Me/19.002/test_course' self.problem_id = self.course_id.make_usage_key('crowdsource_hinter', 'crowdsource_hinter_001')
UserStateSummaryFactory.create(field_name='hints', UserStateSummaryFactory.create(field_name='hints',
usage_id=self.problem_id, usage_id=self.problem_id,
value=json.dumps({'1.0': {'1': ['Hint 1', 2], value=json.dumps({'1.0': {'1': ['Hint 1', 2],
...@@ -60,7 +60,7 @@ class HintManagerTest(ModuleStoreTestCase): ...@@ -60,7 +60,7 @@ class HintManagerTest(ModuleStoreTestCase):
""" """
Makes sure that staff can access the hint management view. 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 print out
self.assertTrue('Hints Awaiting Moderation' in out.content) self.assertTrue('Hints Awaiting Moderation' in out.content)
...@@ -115,7 +115,7 @@ class HintManagerTest(ModuleStoreTestCase): ...@@ -115,7 +115,7 @@ class HintManagerTest(ModuleStoreTestCase):
request = RequestFactory() request = RequestFactory()
post = request.post(self.url, {'field': 'hints', post = request.post(self.url, {'field': 'hints',
'op': 'delete 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') view.delete_hints(post, self.course_id, 'hints')
problem_hints = XModuleUserStateSummaryField.objects.get(field_name='hints', usage_id=self.problem_id).value 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']) self.assertTrue('1' not in json.loads(problem_hints)['1.0'])
...@@ -127,7 +127,7 @@ class HintManagerTest(ModuleStoreTestCase): ...@@ -127,7 +127,7 @@ class HintManagerTest(ModuleStoreTestCase):
request = RequestFactory() request = RequestFactory()
post = request.post(self.url, {'field': 'hints', post = request.post(self.url, {'field': 'hints',
'op': 'change votes', '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') view.change_votes(post, self.course_id, 'hints')
problem_hints = XModuleUserStateSummaryField.objects.get(field_name='hints', usage_id=self.problem_id).value problem_hints = XModuleUserStateSummaryField.objects.get(field_name='hints', usage_id=self.problem_id).value
# hints[answer][hint_pk (string)] = [hint text, vote count] # hints[answer][hint_pk (string)] = [hint text, vote count]
...@@ -146,7 +146,7 @@ class HintManagerTest(ModuleStoreTestCase): ...@@ -146,7 +146,7 @@ class HintManagerTest(ModuleStoreTestCase):
request = RequestFactory() request = RequestFactory()
post = request.post(self.url, {'field': 'mod_queue', post = request.post(self.url, {'field': 'mod_queue',
'op': 'add hint', 'op': 'add hint',
'problem': self.problem_id, 'problem': self.problem_id.to_deprecated_string(),
'answer': '3.14', 'answer': '3.14',
'hint': 'This is a new hint.'}) 'hint': 'This is a new hint.'})
post.user = 'fake user' post.user = 'fake user'
...@@ -167,7 +167,7 @@ class HintManagerTest(ModuleStoreTestCase): ...@@ -167,7 +167,7 @@ class HintManagerTest(ModuleStoreTestCase):
request = RequestFactory() request = RequestFactory()
post = request.post(self.url, {'field': 'mod_queue', post = request.post(self.url, {'field': 'mod_queue',
'op': 'add hint', 'op': 'add hint',
'problem': self.problem_id, 'problem': self.problem_id.to_deprecated_string(),
'answer': 'fish', 'answer': 'fish',
'hint': 'This is a new hint.'}) 'hint': 'This is a new hint.'})
post.user = 'fake user' post.user = 'fake user'
...@@ -185,7 +185,7 @@ class HintManagerTest(ModuleStoreTestCase): ...@@ -185,7 +185,7 @@ class HintManagerTest(ModuleStoreTestCase):
request = RequestFactory() request = RequestFactory()
post = request.post(self.url, {'field': 'mod_queue', post = request.post(self.url, {'field': 'mod_queue',
'op': 'approve', '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') view.approve(post, self.course_id, 'mod_queue')
problem_hints = XModuleUserStateSummaryField.objects.get(field_name='mod_queue', usage_id=self.problem_id).value 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) self.assertTrue('2.0' not in json.loads(problem_hints) or len(json.loads(problem_hints)['2.0']) == 0)
......
...@@ -20,6 +20,7 @@ from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE ...@@ -20,6 +20,7 @@ from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE
from student.roles import CourseStaffRole from student.roles import CourseStaffRole
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.django import modulestore, clear_existing_modulestores from xmodule.modulestore.django import modulestore, clear_existing_modulestores
from xmodule.modulestore.locations import SlashSeparatedCourseKey
from mock import patch from mock import patch
...@@ -33,7 +34,7 @@ class TestInstructorDashboardAnonCSV(ModuleStoreTestCase, LoginEnrollmentTestCas ...@@ -33,7 +34,7 @@ class TestInstructorDashboardAnonCSV(ModuleStoreTestCase, LoginEnrollmentTestCas
# Note -- I copied this setUp from a similar test # Note -- I copied this setUp from a similar test
def setUp(self): def setUp(self):
clear_existing_modulestores() 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 # Create two accounts
self.student = 'view@test.com' self.student = 'view@test.com'
...@@ -44,7 +45,7 @@ class TestInstructorDashboardAnonCSV(ModuleStoreTestCase, LoginEnrollmentTestCas ...@@ -44,7 +45,7 @@ class TestInstructorDashboardAnonCSV(ModuleStoreTestCase, LoginEnrollmentTestCas
self.activate_user(self.student) self.activate_user(self.student)
self.activate_user(self.instructor) 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.logout()
self.login(self.instructor, self.password) self.login(self.instructor, self.password)
...@@ -52,7 +53,7 @@ class TestInstructorDashboardAnonCSV(ModuleStoreTestCase, LoginEnrollmentTestCas ...@@ -52,7 +53,7 @@ class TestInstructorDashboardAnonCSV(ModuleStoreTestCase, LoginEnrollmentTestCas
def test_download_anon_csv(self): def test_download_anon_csv(self):
course = self.toy 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: with patch('instructor.views.legacy.unique_id_for_user') as mock_unique:
mock_unique.return_value = 42 mock_unique.return_value = 42
......
...@@ -20,6 +20,7 @@ from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE ...@@ -20,6 +20,7 @@ from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE
from student.roles import CourseStaffRole from student.roles import CourseStaffRole
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.django import modulestore, clear_existing_modulestores from xmodule.modulestore.django import modulestore, clear_existing_modulestores
from xmodule.modulestore.locations import SlashSeparatedCourseKey
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) @override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
...@@ -30,7 +31,7 @@ class TestInstructorDashboardGradeDownloadCSV(ModuleStoreTestCase, LoginEnrollme ...@@ -30,7 +31,7 @@ class TestInstructorDashboardGradeDownloadCSV(ModuleStoreTestCase, LoginEnrollme
def setUp(self): def setUp(self):
clear_existing_modulestores() 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 # Create two accounts
self.student = 'view@test.com' self.student = 'view@test.com'
...@@ -41,7 +42,7 @@ class TestInstructorDashboardGradeDownloadCSV(ModuleStoreTestCase, LoginEnrollme ...@@ -41,7 +42,7 @@ class TestInstructorDashboardGradeDownloadCSV(ModuleStoreTestCase, LoginEnrollme
self.activate_user(self.student) self.activate_user(self.student)
self.activate_user(self.instructor) 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.logout()
self.login(self.instructor, self.password) self.login(self.instructor, self.password)
...@@ -49,7 +50,7 @@ class TestInstructorDashboardGradeDownloadCSV(ModuleStoreTestCase, LoginEnrollme ...@@ -49,7 +50,7 @@ class TestInstructorDashboardGradeDownloadCSV(ModuleStoreTestCase, LoginEnrollme
def test_download_grades_csv(self): def test_download_grades_csv(self):
course = self.toy 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) msg = "url = {0}\n".format(url)
response = self.client.post(url, {'action': 'Download CSV of all student grades for this course'}) 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) msg += "instructor dashboard download csv grades: response = '{0}'\n".format(response)
...@@ -58,7 +59,7 @@ class TestInstructorDashboardGradeDownloadCSV(ModuleStoreTestCase, LoginEnrollme ...@@ -58,7 +59,7 @@ class TestInstructorDashboardGradeDownloadCSV(ModuleStoreTestCase, LoginEnrollme
cdisp = response['Content-Disposition'] cdisp = response['Content-Disposition']
msg += "Content-Disposition = '%s'\n" % cdisp 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', '') body = response.content.replace('\r', '')
msg += "body = '{0}'\n".format(body) msg += "body = '{0}'\n".format(body)
......
...@@ -32,7 +32,7 @@ class TestInstructorDashboardEmailView(ModuleStoreTestCase): ...@@ -32,7 +32,7 @@ class TestInstructorDashboardEmailView(ModuleStoreTestCase):
self.client.login(username=instructor.username, password="test") self.client.login(username=instructor.username, password="test")
# URL for instructor dash # 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 # URL for email view
self.email_link = '<a href="#" onclick="goto(\'Email\')" class="None">Email</a>' self.email_link = '<a href="#" onclick="goto(\'Email\')" class="None">Email</a>'
......
...@@ -52,7 +52,7 @@ class TestInstructorEnrollsStudent(ModuleStoreTestCase, LoginEnrollmentTestCase) ...@@ -52,7 +52,7 @@ class TestInstructorEnrollsStudent(ModuleStoreTestCase, LoginEnrollmentTestCase)
course = self.course course = self.course
# Run the Un-enroll students command # 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( response = self.client.post(
url, url,
{ {
...@@ -84,7 +84,7 @@ class TestInstructorEnrollsStudent(ModuleStoreTestCase, LoginEnrollmentTestCase) ...@@ -84,7 +84,7 @@ class TestInstructorEnrollsStudent(ModuleStoreTestCase, LoginEnrollmentTestCase)
course = self.course course = self.course
# Run the Enroll students command # 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'}) 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 # Check the page output
...@@ -129,7 +129,7 @@ class TestInstructorEnrollsStudent(ModuleStoreTestCase, LoginEnrollmentTestCase) ...@@ -129,7 +129,7 @@ class TestInstructorEnrollsStudent(ModuleStoreTestCase, LoginEnrollmentTestCase)
course = self.course 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'}) response = self.client.post(url, {'action': 'Enroll multiple students', 'multiple_students': 'student0@test.com', 'auto_enroll': 'on'})
self.assertContains(response, '<td>student0@test.com</td>') self.assertContains(response, '<td>student0@test.com</td>')
self.assertContains(response, '<td>already enrolled</td>') self.assertContains(response, '<td>already enrolled</td>')
...@@ -142,7 +142,7 @@ class TestInstructorEnrollsStudent(ModuleStoreTestCase, LoginEnrollmentTestCase) ...@@ -142,7 +142,7 @@ class TestInstructorEnrollsStudent(ModuleStoreTestCase, LoginEnrollmentTestCase)
course = self.course course = self.course
# Run the Enroll students command # 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'}) response = self.client.post(url, {'action': 'Enroll multiple students', 'multiple_students': 'student2_1@test.com, student2_2@test.com'})
# Check the page output # Check the page output
...@@ -199,7 +199,7 @@ class TestInstructorEnrollsStudent(ModuleStoreTestCase, LoginEnrollmentTestCase) ...@@ -199,7 +199,7 @@ class TestInstructorEnrollsStudent(ModuleStoreTestCase, LoginEnrollmentTestCase)
# Create activated, but not enrolled, user # Create activated, but not enrolled, user
UserFactory.create(username="student3_0", email="student3_0@test.com", first_name='Autoenrolled') 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'}) 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 # Check the page output
...@@ -254,7 +254,7 @@ class TestInstructorEnrollsStudent(ModuleStoreTestCase, LoginEnrollmentTestCase) ...@@ -254,7 +254,7 @@ class TestInstructorEnrollsStudent(ModuleStoreTestCase, LoginEnrollmentTestCase)
cea = CourseEnrollmentAllowed(email='student4_0@test.com', course_id=course.id) cea = CourseEnrollmentAllowed(email='student4_0@test.com', course_id=course.id)
cea.save() 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'}) 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 # Check the page output
...@@ -301,7 +301,7 @@ class TestInstructorEnrollsStudent(ModuleStoreTestCase, LoginEnrollmentTestCase) ...@@ -301,7 +301,7 @@ class TestInstructorEnrollsStudent(ModuleStoreTestCase, LoginEnrollmentTestCase)
# Create activated, but not enrolled, user # Create activated, but not enrolled, user
UserFactory.create(username="student5_0", email="student5_0@test.com", first_name="ShibTest", last_name="Enrolled") 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'}) 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 # Check the page output
......
...@@ -17,6 +17,7 @@ from courseware.tests.helpers import LoginEnrollmentTestCase ...@@ -17,6 +17,7 @@ from courseware.tests.helpers import LoginEnrollmentTestCase
from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE
from student.roles import CourseStaffRole from student.roles import CourseStaffRole
from xmodule.modulestore.django import modulestore, clear_existing_modulestores from xmodule.modulestore.django import modulestore, clear_existing_modulestores
from xmodule.modulestore.locations import SlashSeparatedCourseKey
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
...@@ -42,7 +43,7 @@ class TestInstructorDashboardForumAdmin(ModuleStoreTestCase, LoginEnrollmentTest ...@@ -42,7 +43,7 @@ class TestInstructorDashboardForumAdmin(ModuleStoreTestCase, LoginEnrollmentTest
clear_existing_modulestores() clear_existing_modulestores()
courses = modulestore().get_courses() 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) self.toy = modulestore().get_course(self.course_id)
# Create two accounts # Create two accounts
...@@ -54,7 +55,7 @@ class TestInstructorDashboardForumAdmin(ModuleStoreTestCase, LoginEnrollmentTest ...@@ -54,7 +55,7 @@ class TestInstructorDashboardForumAdmin(ModuleStoreTestCase, LoginEnrollmentTest
self.activate_user(self.student) self.activate_user(self.student)
self.activate_user(self.instructor) 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.logout()
self.login(self.instructor, self.password) self.login(self.instructor, self.password)
...@@ -67,7 +68,7 @@ class TestInstructorDashboardForumAdmin(ModuleStoreTestCase, LoginEnrollmentTest ...@@ -67,7 +68,7 @@ class TestInstructorDashboardForumAdmin(ModuleStoreTestCase, LoginEnrollmentTest
def test_add_forum_admin_users_for_unknown_user(self): def test_add_forum_admin_users_for_unknown_user(self):
course = self.toy 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' username = 'unknown'
for action in ['Add', 'Remove']: for action in ['Add', 'Remove']:
for rolename in FORUM_ROLES: for rolename in FORUM_ROLES:
...@@ -76,7 +77,7 @@ class TestInstructorDashboardForumAdmin(ModuleStoreTestCase, LoginEnrollmentTest ...@@ -76,7 +77,7 @@ class TestInstructorDashboardForumAdmin(ModuleStoreTestCase, LoginEnrollmentTest
def test_add_forum_admin_users_for_missing_roles(self): def test_add_forum_admin_users_for_missing_roles(self):
course = self.toy 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' username = 'u1'
for action in ['Add', 'Remove']: for action in ['Add', 'Remove']:
for rolename in FORUM_ROLES: for rolename in FORUM_ROLES:
...@@ -86,7 +87,7 @@ class TestInstructorDashboardForumAdmin(ModuleStoreTestCase, LoginEnrollmentTest ...@@ -86,7 +87,7 @@ class TestInstructorDashboardForumAdmin(ModuleStoreTestCase, LoginEnrollmentTest
def test_remove_forum_admin_users_for_missing_users(self): def test_remove_forum_admin_users_for_missing_users(self):
course = self.toy course = self.toy
self.initialize_roles(course.id) 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' username = 'u1'
action = 'Remove' action = 'Remove'
for rolename in FORUM_ROLES: for rolename in FORUM_ROLES:
...@@ -96,20 +97,20 @@ class TestInstructorDashboardForumAdmin(ModuleStoreTestCase, LoginEnrollmentTest ...@@ -96,20 +97,20 @@ class TestInstructorDashboardForumAdmin(ModuleStoreTestCase, LoginEnrollmentTest
def test_add_and_remove_forum_admin_users(self): def test_add_and_remove_forum_admin_users(self):
course = self.toy course = self.toy
self.initialize_roles(course.id) 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' username = 'u2'
for rolename in FORUM_ROLES: for rolename in FORUM_ROLES:
response = self.client.post(url, {'action': action_name('Add', rolename), FORUM_ADMIN_USER[rolename]: username}) 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)) self.assertTrue(has_forum_access(username, course.id, rolename))
response = self.client.post(url, {'action': action_name('Remove', rolename), FORUM_ADMIN_USER[rolename]: username}) 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)) self.assertFalse(has_forum_access(username, course.id, rolename))
def test_add_and_read_forum_admin_users(self): def test_add_and_read_forum_admin_users(self):
course = self.toy course = self.toy
self.initialize_roles(course.id) 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' username = 'u2'
for rolename in FORUM_ROLES: for rolename in FORUM_ROLES:
# perform an add, and follow with a second identical add: # perform an add, and follow with a second identical add:
...@@ -121,7 +122,7 @@ class TestInstructorDashboardForumAdmin(ModuleStoreTestCase, LoginEnrollmentTest ...@@ -121,7 +122,7 @@ class TestInstructorDashboardForumAdmin(ModuleStoreTestCase, LoginEnrollmentTest
def test_add_nonstaff_forum_admin_users(self): def test_add_nonstaff_forum_admin_users(self):
course = self.toy course = self.toy
self.initialize_roles(course.id) 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' username = 'u1'
rolename = FORUM_ROLE_ADMINISTRATOR rolename = FORUM_ROLE_ADMINISTRATOR
response = self.client.post(url, {'action': action_name('Add', rolename), FORUM_ADMIN_USER[rolename]: username}) response = self.client.post(url, {'action': action_name('Add', rolename), FORUM_ADMIN_USER[rolename]: username})
...@@ -130,7 +131,7 @@ class TestInstructorDashboardForumAdmin(ModuleStoreTestCase, LoginEnrollmentTest ...@@ -130,7 +131,7 @@ class TestInstructorDashboardForumAdmin(ModuleStoreTestCase, LoginEnrollmentTest
def test_list_forum_admin_users(self): def test_list_forum_admin_users(self):
course = self.toy course = self.toy
self.initialize_roles(course.id) 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' username = 'u2'
added_roles = [FORUM_ROLE_STUDENT] # u2 is already added as a student to the discussion forums 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')) self.assertTrue(has_forum_access(username, course.id, 'Student'))
......
...@@ -11,6 +11,7 @@ from courseware.tests.tests import TEST_DATA_MIXED_MODULESTORE ...@@ -11,6 +11,7 @@ from courseware.tests.tests import TEST_DATA_MIXED_MODULESTORE
from capa.tests.response_xml_factory import StringResponseXMLFactory from capa.tests.response_xml_factory import StringResponseXMLFactory
from courseware.tests.factories import StudentModuleFactory from courseware.tests.factories import StudentModuleFactory
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.locations import SlashSeparatedCourseKey
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
...@@ -64,10 +65,13 @@ class TestGradebook(ModuleStoreTestCase): ...@@ -64,10 +65,13 @@ class TestGradebook(ModuleStoreTestCase):
max_grade=1, max_grade=1,
student=user, student=user,
course_id=self.course.id, 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): def test_response_code(self):
self.assertEquals(self.response.status_code, 200) self.assertEquals(self.response.status_code, 200)
......
...@@ -24,7 +24,7 @@ class TestRawGradeCSV(TestSubmittingProblems): ...@@ -24,7 +24,7 @@ class TestRawGradeCSV(TestSubmittingProblems):
self.instructor = 'view2@test.com' self.instructor = 'view2@test.com'
self.create_account('u2', self.instructor, self.password) self.create_account('u2', self.instructor, self.password)
self.activate_user(self.instructor) 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.logout()
self.login(self.instructor, self.password) self.login(self.instructor, self.password)
self.enroll(self.course) self.enroll(self.course)
...@@ -45,7 +45,7 @@ class TestRawGradeCSV(TestSubmittingProblems): ...@@ -45,7 +45,7 @@ class TestRawGradeCSV(TestSubmittingProblems):
resp = self.submit_question_answer('p2', {'2_1': 'Correct'}) resp = self.submit_question_answer('p2', {'2_1': 'Correct'})
self.assertEqual(resp.status_code, 200) 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) msg = "url = {0}\n".format(url)
response = self.client.post(url, {'action': 'Download CSV of all RAW grades'}) response = self.client.post(url, {'action': 'Download CSV of all RAW grades'})
msg += "instructor dashboard download raw csv grades: response = '{0}'\n".format(response) msg += "instructor dashboard download raw csv grades: response = '{0}'\n".format(response)
......
...@@ -16,6 +16,7 @@ from courseware.models import StudentModule ...@@ -16,6 +16,7 @@ from courseware.models import StudentModule
from submissions import api as sub_api from submissions import api as sub_api
from student.models import anonymous_id_for_user from student.models import anonymous_id_for_user
from .test_tools import msk_from_problem_urlname
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) @override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
...@@ -35,29 +36,36 @@ class InstructorResetStudentStateTest(ModuleStoreTestCase, LoginEnrollmentTestCa ...@@ -35,29 +36,36 @@ class InstructorResetStudentStateTest(ModuleStoreTestCase, LoginEnrollmentTestCa
CourseEnrollmentFactory.create(user=self.student, course_id=self.course.id) CourseEnrollmentFactory.create(user=self.student, course_id=self.course.id)
def test_delete_student_state_resets_scores(self): 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 # Create a student module for the user
StudentModule.objects.create( 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 # Create a submission and score for the student using the submissions API
student_item = { student_item = {
'student_id': anonymous_id_for_user(self.student, self.course.id), 'student_id': anonymous_id_for_user(self.student, self.course.id),
'course_id': self.course.id, 'course_id': self.course.id,
'item_id': item_id, 'item_id': problem_location,
'item_type': 'openassessment' 'item_type': 'openassessment'
} }
submission = sub_api.create_submission(student_item, 'test answer') submission = sub_api.create_submission(student_item, 'test answer')
sub_api.set_score(submission['uuid'], 1, 2) sub_api.set_score(submission['uuid'], 1, 2)
# Delete student state using the instructor dash # 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, { response = self.client.post(url, {
'action': 'Delete student state for module', 'action': 'Delete student state for module',
'unique_student_identifier': self.student.email, 'unique_student_identifier': self.student.email,
'problem_for_student': 'openassessment/b3dce2586c9c4876b73e7f390e42ef8f', 'problem_for_student': str(problem_location),
}) })
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
......
...@@ -48,7 +48,7 @@ class TestXss(ModuleStoreTestCase): ...@@ -48,7 +48,7 @@ class TestXss(ModuleStoreTestCase):
) )
req.user = self._instructor req.user = self._instructor
req.session = {} 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) respUnicode = resp.content.decode(settings.DEFAULT_CHARSET)
self.assertNotIn(self._evil_student.profile.name, respUnicode) self.assertNotIn(self._evil_student.profile.name, respUnicode)
self.assertIn(escape(self._evil_student.profile.name), respUnicode) self.assertIn(escape(self._evil_student.profile.name), respUnicode)
......
...@@ -17,6 +17,7 @@ from student.tests.factories import UserFactory ...@@ -17,6 +17,7 @@ from student.tests.factories import UserFactory
from xmodule.fields import Date from xmodule.fields import Date
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.modulestore.keys import CourseKey
from ..views import tools from ..views import tools
...@@ -86,10 +87,8 @@ class TestFindUnit(ModuleStoreTestCase): ...@@ -86,10 +87,8 @@ class TestFindUnit(ModuleStoreTestCase):
Fixtures. Fixtures.
""" """
course = CourseFactory.create() course = CourseFactory.create()
week1 = ItemFactory.create() week1 = ItemFactory.create(parent=course)
homework = ItemFactory.create(parent_location=week1.location) homework = ItemFactory.create(parent=week1)
week1.children.append(homework.location)
course.children.append(week1.location)
self.course = course self.course = course
self.homework = homework self.homework = homework
...@@ -98,7 +97,7 @@ class TestFindUnit(ModuleStoreTestCase): ...@@ -98,7 +97,7 @@ class TestFindUnit(ModuleStoreTestCase):
""" """
Test finding a nested unit. 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) self.assertEqual(tools.find_unit(self.course, url), self.homework)
def test_find_unit_notfound(self): def test_find_unit_notfound(self):
...@@ -121,15 +120,13 @@ class TestGetUnitsWithDueDate(ModuleStoreTestCase): ...@@ -121,15 +120,13 @@ class TestGetUnitsWithDueDate(ModuleStoreTestCase):
""" """
due = datetime.datetime(2010, 5, 12, 2, 42, tzinfo=utc) due = datetime.datetime(2010, 5, 12, 2, 42, tzinfo=utc)
course = CourseFactory.create() course = CourseFactory.create()
week1 = ItemFactory.create(due=due) week1 = ItemFactory.create(due=due, parent=course)
week2 = ItemFactory.create(due=due) week2 = ItemFactory.create(due=due, parent=course)
course.children = [week1.location.url(), week2.location.url()]
homework = ItemFactory.create( homework = ItemFactory.create(
parent_location=week1.location, parent=week1,
due=due due=due
) )
week1.children = [homework.location.url()]
self.course = course self.course = course
self.week1 = week1 self.week1 = week1
...@@ -139,7 +136,7 @@ class TestGetUnitsWithDueDate(ModuleStoreTestCase): ...@@ -139,7 +136,7 @@ class TestGetUnitsWithDueDate(ModuleStoreTestCase):
def urls(seq): def urls(seq):
"URLs for sequence of nodes." "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( self.assertEquals(
urls(tools.get_units_with_due_date(self.course)), urls(tools.get_units_with_due_date(self.course)),
...@@ -156,7 +153,7 @@ class TestTitleOrUrl(unittest.TestCase): ...@@ -156,7 +153,7 @@ class TestTitleOrUrl(unittest.TestCase):
def test_url(self): def test_url(self):
unit = mock.Mock(display_name=None) 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') self.assertEquals(tools.title_or_url(unit), 'test:hello')
...@@ -171,27 +168,25 @@ class TestSetDueDateExtension(ModuleStoreTestCase): ...@@ -171,27 +168,25 @@ class TestSetDueDateExtension(ModuleStoreTestCase):
""" """
due = datetime.datetime(2010, 5, 12, 2, 42, tzinfo=utc) due = datetime.datetime(2010, 5, 12, 2, 42, tzinfo=utc)
course = CourseFactory.create() course = CourseFactory.create()
week1 = ItemFactory.create(due=due) week1 = ItemFactory.create(due=due, parent=course)
week2 = ItemFactory.create(due=due) week2 = ItemFactory.create(due=due, parent=course)
course.children = [week1.location.url(), week2.location.url()]
homework = ItemFactory.create( homework = ItemFactory.create(
parent_location=week1.location, parent=week1,
due=due due=due
) )
week1.children = [homework.location.url()]
user = UserFactory.create() user = UserFactory.create()
StudentModule( StudentModule(
state='{}', state='{}',
student_id=user.id, student_id=user.id,
course_id=course.id, course_id=course.id,
module_state_key=week1.location.url()).save() module_state_key=week1.location).save()
StudentModule( StudentModule(
state='{}', state='{}',
student_id=user.id, student_id=user.id,
course_id=course.id, course_id=course.id,
module_state_key=homework.location.url()).save() module_state_key=homework.location).save()
self.course = course self.course = course
self.week1 = week1 self.week1 = week1
...@@ -226,63 +221,60 @@ class TestDataDumps(ModuleStoreTestCase): ...@@ -226,63 +221,60 @@ class TestDataDumps(ModuleStoreTestCase):
""" """
due = datetime.datetime(2010, 5, 12, 2, 42, tzinfo=utc) due = datetime.datetime(2010, 5, 12, 2, 42, tzinfo=utc)
course = CourseFactory.create() course = CourseFactory.create()
week1 = ItemFactory.create(due=due) week1 = ItemFactory.create(due=due, parent=course)
week2 = ItemFactory.create(due=due) week2 = ItemFactory.create(due=due, parent=course)
week3 = ItemFactory.create(due=due) week3 = ItemFactory.create(due=due, parent=course)
course.children = [week1.location.url(), week2.location.url(),
week3.location.url()]
homework = ItemFactory.create( homework = ItemFactory.create(
parent_location=week1.location, parent=week1,
due=due due=due
) )
week1.children = [homework.location.url()]
user1 = UserFactory.create() user1 = UserFactory.create()
StudentModule( StudentModule(
state='{}', state='{}',
student_id=user1.id, student_id=user1.id,
course_id=course.id, course_id=course.id,
module_state_key=week1.location.url()).save() module_state_key=week1.location).save()
StudentModule( StudentModule(
state='{}', state='{}',
student_id=user1.id, student_id=user1.id,
course_id=course.id, course_id=course.id,
module_state_key=week2.location.url()).save() module_state_key=week2.location).save()
StudentModule( StudentModule(
state='{}', state='{}',
student_id=user1.id, student_id=user1.id,
course_id=course.id, course_id=course.id,
module_state_key=week3.location.url()).save() module_state_key=week3.location).save()
StudentModule( StudentModule(
state='{}', state='{}',
student_id=user1.id, student_id=user1.id,
course_id=course.id, course_id=course.id,
module_state_key=homework.location.url()).save() module_state_key=homework.location).save()
user2 = UserFactory.create() user2 = UserFactory.create()
StudentModule( StudentModule(
state='{}', state='{}',
student_id=user2.id, student_id=user2.id,
course_id=course.id, course_id=course.id,
module_state_key=week1.location.url()).save() module_state_key=week1.location).save()
StudentModule( StudentModule(
state='{}', state='{}',
student_id=user2.id, student_id=user2.id,
course_id=course.id, course_id=course.id,
module_state_key=homework.location.url()).save() module_state_key=homework.location).save()
user3 = UserFactory.create() user3 = UserFactory.create()
StudentModule( StudentModule(
state='{}', state='{}',
student_id=user3.id, student_id=user3.id,
course_id=course.id, course_id=course.id,
module_state_key=week1.location.url()).save() module_state_key=week1.location).save()
StudentModule( StudentModule(
state='{}', state='{}',
student_id=user3.id, student_id=user3.id,
course_id=course.id, course_id=course.id,
module_state_key=homework.location.url()).save() module_state_key=homework.location).save()
self.course = course self.course = course
self.week1 = week1 self.week1 = week1
...@@ -337,10 +329,22 @@ def get_extended_due(course, unit, student): ...@@ -337,10 +329,22 @@ def get_extended_due(course, unit, student):
student_module = StudentModule.objects.get( student_module = StudentModule.objects.get(
student_id=student.id, student_id=student.id,
course_id=course.id, course_id=course.id,
module_state_key=unit.location.url() module_id=unit.location
) )
state = json.loads(student_module.state) state = json.loads(student_module.state)
extended = state.get('extended_due', None) extended = state.get('extended_due', None)
if extended: if extended:
return DATE_FIELD.from_json(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)
...@@ -27,12 +27,12 @@ class DummyRequest(object): ...@@ -27,12 +27,12 @@ class DummyRequest(object):
return False 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.""" """Return the module for the (student, location) using a DummyRequest."""
if request is None: if request is None:
request = DummyRequest() request = DummyRequest()
request.user = student request.user = student
descriptor = modulestore().get_instance(course.id, location, depth=0) descriptor = modulestore().get_item(usage_key, depth=0)
field_data_cache = FieldDataCache([descriptor], course.id, student) field_data_cache = FieldDataCache([descriptor], usage_key.course_key, student)
return get_module(student, request, location, field_data_cache, course.id) return get_module(student, request, usage_key, field_data_cache)
...@@ -88,7 +88,7 @@ def find_unit(course, url): ...@@ -88,7 +88,7 @@ def find_unit(course, url):
""" """
Find node in course tree for url. Find node in course tree for url.
""" """
if node.location.url() == url: if node.location.to_deprecated_string() == url:
return node return node
for child in node.get_children(): for child in node.get_children():
found = find(child, url) found = find(child, url)
...@@ -132,7 +132,7 @@ def title_or_url(node): ...@@ -132,7 +132,7 @@ def title_or_url(node):
""" """
title = getattr(node, 'display_name', None) title = getattr(node, 'display_name', None)
if not title: if not title:
title = node.location.url() title = node.location.to_deprecated_string()
return title return title
...@@ -148,7 +148,7 @@ def set_due_date_extension(course, unit, student, due_date): ...@@ -148,7 +148,7 @@ def set_due_date_extension(course, unit, student, due_date):
student_module = StudentModule.objects.get( student_module = StudentModule.objects.get(
student_id=student.id, student_id=student.id,
course_id=course.id, course_id=course.id,
module_state_key=node.location.url() module_id=node.location
) )
state = json.loads(student_module.state) state = json.loads(student_module.state)
...@@ -173,7 +173,7 @@ def dump_module_extensions(course, unit): ...@@ -173,7 +173,7 @@ def dump_module_extensions(course, unit):
header = [_("Username"), _("Full Name"), _("Extended Due Date")] header = [_("Username"), _("Full Name"), _("Extended Due Date")]
query = StudentModule.objects.filter( query = StudentModule.objects.filter(
course_id=course.id, course_id=course.id,
module_state_key=unit.location.url()) module_id=unit.location)
for module in query: for module in query:
state = json.loads(module.state) state = json.loads(module.state)
extended_due = state.get("extended_due") extended_due = state.get("extended_due")
...@@ -202,7 +202,7 @@ def dump_student_extensions(course, student): ...@@ -202,7 +202,7 @@ def dump_student_extensions(course, student):
data = [] data = []
header = [_("Unit"), _("Extended Due Date")] header = [_("Unit"), _("Extended Due Date")]
units = get_units_with_due_date(course) 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( query = StudentModule.objects.filter(
course_id=course.id, course_id=course.id,
student_id=student.id) student_id=student.id)
......
...@@ -38,14 +38,14 @@ def get_running_instructor_tasks(course_id): ...@@ -38,14 +38,14 @@ def get_running_instructor_tasks(course_id):
return instructor_tasks.order_by('-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, 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. that optionally match a particular problem, a student, and/or a task type.
""" """
instructor_tasks = InstructorTask.objects.filter(course_id=course_id) instructor_tasks = InstructorTask.objects.filter(course_id=course_id)
if problem_url is not None or student is not None: if usage_key is not None or student is not None:
_, task_key = encode_problem_and_student_input(problem_url, student) _, task_key = encode_problem_and_student_input(usage_key, student)
instructor_tasks = instructor_tasks.filter(task_key=task_key) instructor_tasks = instructor_tasks.filter(task_key=task_key)
if task_type is not None: if task_type is not None:
instructor_tasks = instructor_tasks.filter(task_type=task_type) 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_ ...@@ -53,7 +53,8 @@ def get_instructor_task_history(course_id, problem_url=None, student=None, task_
return instructor_tasks.order_by('-id') 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. 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) ...@@ -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: 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_type = 'rescore_problem'
task_class = rescore_problem task_class = rescore_problem
task_input, task_key = encode_problem_and_student_input(problem_url, student) task_input, task_key = encode_problem_and_student_input(usage_key, student)
return submit_task(request, task_type, task_class, course_id, task_input, task_key) 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. 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): ...@@ -103,23 +104,22 @@ def submit_rescore_problem_for_all_students(request, course_id, problem_url):
separate transaction. separate transaction.
""" """
# check arguments: let exceptions return up to the caller. # 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 # check to see if task is already running, and reserve it otherwise
task_type = 'rescore_problem' task_type = 'rescore_problem'
task_class = rescore_problem task_class = rescore_problem
task_input, task_key = encode_problem_and_student_input(problem_url) task_input, task_key = encode_problem_and_student_input(usage_key)
return submit_task(request, task_type, task_class, course_id, task_input, task_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. 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 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 particular problem in a course. Parameters are the `course_id` and
the `problem_url`. The url must specify the location of the problem, the `usage_key`, which must be a :class:`Location`.
using i4x-type notation.
ItemNotFoundException is raised if the problem doesn't exist, or AlreadyRunningError ItemNotFoundException is raised if the problem doesn't exist, or AlreadyRunningError
if the problem is already being reset. if the problem is already being reset.
...@@ -131,25 +131,24 @@ def submit_reset_problem_attempts_for_all_students(request, course_id, problem_u ...@@ -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 save here. Any future database operations will take place in a
separate transaction. 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, # (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. # 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_type = 'reset_problem_attempts'
task_class = reset_problem_attempts task_class = reset_problem_attempts
task_input, task_key = encode_problem_and_student_input(problem_url) task_input, task_key = encode_problem_and_student_input(usage_key)
return submit_task(request, task_type, task_class, course_id, task_input, task_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. 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 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 particular problem in a course. Parameters are the `course_id` and
the `problem_url`. The url must specify the location of the problem, the `usage_key`, which must be a :class:`Location`.
using i4x-type notation.
ItemNotFoundException is raised if the problem doesn't exist, or AlreadyRunningError ItemNotFoundException is raised if the problem doesn't exist, or AlreadyRunningError
if the particular problem's state is already being deleted. 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 ...@@ -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 save here. Any future database operations will take place in a
separate transaction. 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, # (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. # 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_type = 'delete_problem_state'
task_class = delete_problem_state task_class = delete_problem_state
task_input, task_key = encode_problem_and_student_input(problem_url) task_input, task_key = encode_problem_and_student_input(usage_key)
return submit_task(request, task_type, task_class, course_id, task_input, task_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. 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 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 AlreadyRunningError is raised if the same recipients are already being emailed with the same
CourseEmail object. CourseEmail object.
...@@ -206,10 +205,10 @@ def submit_bulk_course_email(request, course_id, email_id): ...@@ -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) task_key_stub = "{email_id}_{to_option}".format(email_id=email_id, to_option=to_option)
# create the key value by using MD5 hash: # create the key value by using MD5 hash:
task_key = hashlib.md5(task_key_stub).hexdigest() 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. AlreadyRunningError is raised if the course's grades are already being updated.
""" """
...@@ -218,4 +217,4 @@ def submit_calculate_grades_csv(request, course_id): ...@@ -218,4 +217,4 @@ def submit_calculate_grades_csv(request, course_id):
task_input = {} task_input = {}
task_key = "" 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)
"""
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 hashlib
import json import json
import logging import logging
...@@ -8,6 +14,7 @@ from celery.states import READY_STATES, SUCCESS, FAILURE, REVOKED ...@@ -8,6 +14,7 @@ from celery.states import READY_STATES, SUCCESS, FAILURE, REVOKED
from courseware.module_render import get_xqueue_callback_url_prefix from courseware.module_render import get_xqueue_callback_url_prefix
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.locations import Location
from instructor_task.models import InstructorTask, PROGRESS from instructor_task.models import InstructorTask, PROGRESS
...@@ -21,11 +28,13 @@ class AlreadyRunningError(Exception): ...@@ -21,11 +28,13 @@ class AlreadyRunningError(Exception):
def _task_is_running(course_id, task_type, task_key): def _task_is_running(course_id, task_type, task_key):
"""Checks if a particular task is already running""" """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): # exclude states that are "ready" (i.e. not "running", e.g. failure, success, revoked):
for state in READY_STATES: for state in READY_STATES:
runningTasks = runningTasks.exclude(task_state=state) running_tasks = running_tasks.exclude(task_state=state)
return len(runningTasks) > 0 return len(running_tasks) > 0
def _reserve_task(course_id, task_type, task_key, task_input, requester): def _reserve_task(course_id, task_type, task_key, task_input, requester):
...@@ -229,34 +238,37 @@ def get_status_from_instructor_task(instructor_task): ...@@ -229,34 +238,37 @@ def get_status_from_instructor_task(instructor_task):
return status 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. 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 in). An ItemNotFoundException is raised if the corresponding module
descriptor doesn't exist. NotImplementedError is raised if the descriptor doesn't exist. NotImplementedError is raised if the
corresponding module doesn't support rescoring calls. 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'): if not hasattr(descriptor, 'module_class') or not hasattr(descriptor.module_class, 'rescore_problem'):
msg = "Specified module does not support rescoring." msg = "Specified module does not support rescoring."
raise NotImplementedError(msg) 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. Args:
`student` is the user object of the student usage_key (Location): The usage_key identifying the problem.
student (User): the student affected
""" """
assert isinstance(usage_key, Location)
if student is not None: if student is not None:
task_input = {'problem_url': problem_url, 'student': student.username} task_input = {'problem_url': usage_key.to_deprecated_string(), 'student': student.username}
task_key_stub = "{student}_{problem}".format(student=student.id, problem=problem_url) task_key_stub = "{student}_{problem}".format(student=student.id, problem=usage_key.to_deprecated_string())
else: else:
task_input = {'problem_url': problem_url} task_input = {'problem_url': usage_key.to_deprecated_string()}
task_key_stub = "_{problem}".format(problem=problem_url) task_key_stub = "_{problem}".format(problem=usage_key.to_deprecated_string())
# create the key value by using MD5 hash: # create the key value by using MD5 hash:
task_key = hashlib.md5(task_key_stub).hexdigest() task_key = hashlib.md5(task_key_stub).hexdigest()
...@@ -264,11 +276,11 @@ def encode_problem_and_student_input(problem_url, student=None): ...@@ -264,11 +276,11 @@ def encode_problem_and_student_input(problem_url, student=None):
return task_input, task_key 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. 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 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 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 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) ...@@ -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: # 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: # submit task:
task_id = instructor_task.task_id task_id = instructor_task.task_id
......
...@@ -18,7 +18,6 @@ from uuid import uuid4 ...@@ -18,7 +18,6 @@ from uuid import uuid4
import csv import csv
import json import json
import hashlib import hashlib
import os
import os.path import os.path
import urllib import urllib
...@@ -29,6 +28,8 @@ from django.conf import settings ...@@ -29,6 +28,8 @@ from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db import models, transaction from django.db import models, transaction
from xmodule_django.models import CourseKeyField
# define custom states used by InstructorTask # define custom states used by InstructorTask
QUEUING = 'QUEUING' QUEUING = 'QUEUING'
...@@ -58,7 +59,7 @@ class InstructorTask(models.Model): ...@@ -58,7 +59,7 @@ class InstructorTask(models.Model):
`updated` stores date that entry was last modified `updated` stores date that entry was last modified
""" """
task_type = models.CharField(max_length=50, db_index=True) 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_key = models.CharField(max_length=255, db_index=True)
task_input = models.CharField(max_length=255) task_input = models.CharField(max_length=255)
task_id = models.CharField(max_length=255, db_index=True) # max_length from celery_taskmeta task_id = models.CharField(max_length=255, db_index=True) # max_length from celery_taskmeta
...@@ -251,9 +252,9 @@ class S3ReportStore(ReportStore): ...@@ -251,9 +252,9 @@ class S3ReportStore(ReportStore):
) )
def key_for(self, course_id, filename): 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.""" 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(self.bucket)
key.key = "{}/{}/{}".format( key.key = "{}/{}/{}".format(
...@@ -360,7 +361,7 @@ class LocalFSReportStore(ReportStore): ...@@ -360,7 +361,7 @@ class LocalFSReportStore(ReportStore):
def path_to(self, course_id, filename): def path_to(self, course_id, filename):
"""Return the full path to a given file for a given course.""" """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): def store(self, course_id, filename, buff):
""" """
......
...@@ -375,7 +375,7 @@ def check_subtask_is_valid(entry_id, current_task_id, new_subtask_status): ...@@ -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 {}" 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) msg = format_str.format(current_task_id, entry, new_subtask_status)
TASK_LOG.warning(msg) 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) raise DuplicateTaskException(msg)
# Confirm that the InstructorTask knows about this particular subtask. # 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): ...@@ -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 {}" 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) msg = format_str.format(current_task_id, entry, new_subtask_status)
TASK_LOG.warning(msg) 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) raise DuplicateTaskException(msg)
# Confirm that the InstructorTask doesn't think that this subtask has already been # 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): ...@@ -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 {}" 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) msg = format_str.format(current_task_id, subtask_status, entry, new_subtask_status)
TASK_LOG.warning(msg) 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) raise DuplicateTaskException(msg)
# Confirm that the InstructorTask doesn't think that this subtask is already being # 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): ...@@ -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 {}" 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) msg = format_str.format(current_task_id, subtask_status, entry, new_subtask_status)
TASK_LOG.warning(msg) 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) raise DuplicateTaskException(msg)
# Now we are ready to start working on this. Try to lock it. # 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): ...@@ -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 '{}'" format_str = "Unexpected task_id '{}': already being executed - for subtask of instructor task '{}'"
msg = format_str.format(current_task_id, entry) msg = format_str.format(current_task_id, entry)
TASK_LOG.warning(msg) 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) raise DuplicateTaskException(msg)
...@@ -552,3 +552,11 @@ def _update_subtask_status(entry_id, current_task_id, new_subtask_status): ...@@ -552,3 +552,11 @@ def _update_subtask_status(entry_id, current_task_id, new_subtask_status):
else: else:
TASK_LOG.debug("about to commit....") TASK_LOG.debug("about to commit....")
transaction.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]
...@@ -244,15 +244,14 @@ def perform_module_state_update(update_fcn, filter_fcn, _entry_id, course_id, ta ...@@ -244,15 +244,14 @@ def perform_module_state_update(update_fcn, filter_fcn, _entry_id, course_id, ta
# get start time for task: # get start time for task:
start_time = time() 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') student_identifier = task_input.get('student')
# find the problem descriptor: # 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 # find the module in question
modules_to_update = StudentModule.objects.filter(course_id=course_id, modules_to_update = StudentModule.objects.filter(course_id=course_id, module_id=usage_key)
module_state_key=module_state_key)
# give the option of updating an individual student. If not specified, # give the option of updating an individual student. If not specified,
# then updates all students who have responded to a problem so far # 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 ...@@ -394,13 +393,13 @@ def rescore_problem_module_state(xmodule_instance_args, module_descriptor, stude
# unpack the StudentModule: # unpack the StudentModule:
course_id = student_module.course_id course_id = student_module.course_id
student = student_module.student 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') instance = _get_module_instance_for_task(course_id, student, module_descriptor, xmodule_instance_args, grade_bucket_type='rescore')
if instance is None: if instance is None:
# Either permissions just changed, or someone is trying to be clever # Either permissions just changed, or someone is trying to be clever
# and load something they shouldn't have access to. # 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) student=student)
TASK_LOG.debug(msg) TASK_LOG.debug(msg)
raise UpdateProblemModuleStateError(msg) raise UpdateProblemModuleStateError(msg)
...@@ -416,15 +415,15 @@ def rescore_problem_module_state(xmodule_instance_args, module_descriptor, stude ...@@ -416,15 +415,15 @@ def rescore_problem_module_state(xmodule_instance_args, module_descriptor, stude
if 'success' not in result: if 'success' not in result:
# don't consider these fatal, but false means that the individual call didn't complete: # 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}: " 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 return UPDATE_STATUS_FAILED
elif result['success'] not in ['correct', 'incorrect']: elif result['success'] not in ['correct', 'incorrect']:
TASK_LOG.warning(u"error processing rescore call for course {course}, problem {loc} and student {student}: " 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 return UPDATE_STATUS_FAILED
else: else:
TASK_LOG.debug(u"successfully processed rescore call for course {course}, problem {loc} and student {student}: " 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 return UPDATE_STATUS_SUCCEEDED
...@@ -552,7 +551,7 @@ def push_grades_to_s3(_xmodule_instance_args, _entry_id, course_id, _task_input, ...@@ -552,7 +551,7 @@ def push_grades_to_s3(_xmodule_instance_args, _entry_id, course_id, _task_input,
# Generate parts of the file name # Generate parts of the file name
timestamp_str = start_time.strftime("%Y-%m-%d-%H%M") 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 # Perform the actual upload
report_store = ReportStore.from_config() report_store = ReportStore.from_config()
......
...@@ -5,13 +5,14 @@ from factory.django import DjangoModelFactory ...@@ -5,13 +5,14 @@ from factory.django import DjangoModelFactory
from student.tests.factories import UserFactory as StudentUserFactory from student.tests.factories import UserFactory as StudentUserFactory
from instructor_task.models import InstructorTask from instructor_task.models import InstructorTask
from celery.states import PENDING from celery.states import PENDING
from xmodule.modulestore.locations import SlashSeparatedCourseKey
class InstructorTaskFactory(DjangoModelFactory): class InstructorTaskFactory(DjangoModelFactory):
FACTORY_FOR = InstructorTask FACTORY_FOR = InstructorTask
task_type = 'rescore_problem' task_type = 'rescore_problem'
course_id = "MITx/999/Robot_Super_Course" course_id = SlashSeparatedCourseKey("MITx", "999", "Robot_Super_Course")
task_input = json.dumps({}) task_input = json.dumps({})
task_key = None task_key = None
task_id = None task_id = None
......
...@@ -22,7 +22,7 @@ from instructor_task.models import InstructorTask, PROGRESS ...@@ -22,7 +22,7 @@ from instructor_task.models import InstructorTask, PROGRESS
from instructor_task.tests.test_base import (InstructorTaskTestCase, from instructor_task.tests.test_base import (InstructorTaskTestCase,
InstructorTaskCourseTestCase, InstructorTaskCourseTestCase,
InstructorTaskModuleTestCase, InstructorTaskModuleTestCase,
TEST_COURSE_ID) TEST_COURSE_KEY)
class InstructorTaskReportTest(InstructorTaskTestCase): class InstructorTaskReportTest(InstructorTaskTestCase):
...@@ -36,7 +36,7 @@ class InstructorTaskReportTest(InstructorTaskTestCase): ...@@ -36,7 +36,7 @@ class InstructorTaskReportTest(InstructorTaskTestCase):
self._create_failure_entry() self._create_failure_entry()
self._create_success_entry() self._create_success_entry()
progress_task_ids = [self._create_progress_entry().task_id for _ in range(1, 5)] 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)) self.assertEquals(set(task_ids), set(progress_task_ids))
def test_get_instructor_task_history(self): def test_get_instructor_task_history(self):
...@@ -47,21 +47,21 @@ class InstructorTaskReportTest(InstructorTaskTestCase): ...@@ -47,21 +47,21 @@ class InstructorTaskReportTest(InstructorTaskTestCase):
expected_ids.append(self._create_success_entry().task_id) expected_ids.append(self._create_success_entry().task_id)
expected_ids.append(self._create_progress_entry().task_id) expected_ids.append(self._create_progress_entry().task_id)
task_ids = [instructor_task.task_id for instructor_task 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)) self.assertEquals(set(task_ids), set(expected_ids))
# make the same call using explicit task_type: # make the same call using explicit task_type:
task_ids = [instructor_task.task_id for instructor_task task_ids = [instructor_task.task_id for instructor_task
in get_instructor_task_history( in get_instructor_task_history(
TEST_COURSE_ID, TEST_COURSE_KEY,
problem_url=self.problem_url, usage_key=self.problem_url,
task_type='rescore_problem' task_type='rescore_problem'
)] )]
self.assertEquals(set(task_ids), set(expected_ids)) self.assertEquals(set(task_ids), set(expected_ids))
# make the same call using a non-existent task_type: # make the same call using a non-existent task_type:
task_ids = [instructor_task.task_id for instructor_task task_ids = [instructor_task.task_id for instructor_task
in get_instructor_task_history( in get_instructor_task_history(
TEST_COURSE_ID, TEST_COURSE_KEY,
problem_url=self.problem_url, usage_key=self.problem_url,
task_type='dummy_type' task_type='dummy_type'
)] )]
self.assertEquals(set(task_ids), set()) self.assertEquals(set(task_ids), set())
...@@ -81,25 +81,25 @@ class InstructorTaskModuleSubmitTest(InstructorTaskModuleTestCase): ...@@ -81,25 +81,25 @@ class InstructorTaskModuleSubmitTest(InstructorTaskModuleTestCase):
course_id = self.course.id course_id = self.course.id
request = None request = None
with self.assertRaises(ItemNotFoundError): 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): 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): 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): 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): def test_submit_nonrescorable_modules(self):
# confirm that a rescore of an existent but unscorable module returns an exception # 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, # (Note that it is easier to test a scoreable but non-rescorable module in test_tasks,
# where we are creating real modules.) # where we are creating real modules.)
problem_url = self.problem_section.location.url() problem_url = self.problem_section.location
course_id = self.course.id course_id = self.course.id
request = None request = None
with self.assertRaises(NotImplementedError): 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): 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): def _test_submit_with_long_url(self, task_function, student=None):
problem_url_name = 'x' * 255 problem_url_name = 'x' * 255
...@@ -107,9 +107,9 @@ class InstructorTaskModuleSubmitTest(InstructorTaskModuleTestCase): ...@@ -107,9 +107,9 @@ class InstructorTaskModuleSubmitTest(InstructorTaskModuleTestCase):
location = InstructorTaskModuleTestCase.problem_location(problem_url_name) location = InstructorTaskModuleTestCase.problem_location(problem_url_name)
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
if student is not None: 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: 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): def test_submit_rescore_all_with_long_url(self):
self._test_submit_with_long_url(submit_rescore_problem_for_all_students) self._test_submit_with_long_url(submit_rescore_problem_for_all_students)
...@@ -129,11 +129,9 @@ class InstructorTaskModuleSubmitTest(InstructorTaskModuleTestCase): ...@@ -129,11 +129,9 @@ class InstructorTaskModuleSubmitTest(InstructorTaskModuleTestCase):
self.define_option_problem(problem_url_name) self.define_option_problem(problem_url_name)
location = InstructorTaskModuleTestCase.problem_location(problem_url_name) location = InstructorTaskModuleTestCase.problem_location(problem_url_name)
if student is not None: if student is not None:
instructor_task = task_function(self.create_task_request(self.instructor), instructor_task = task_function(self.create_task_request(self.instructor), location, student)
self.course.id, location, student)
else: else:
instructor_task = task_function(self.create_task_request(self.instructor), instructor_task = task_function(self.create_task_request(self.instructor), location)
self.course.id, location)
# test resubmitting, by updating the existing record: # test resubmitting, by updating the existing record:
instructor_task = InstructorTask.objects.get(id=instructor_task.id) instructor_task = InstructorTask.objects.get(id=instructor_task.id)
...@@ -142,9 +140,9 @@ class InstructorTaskModuleSubmitTest(InstructorTaskModuleTestCase): ...@@ -142,9 +140,9 @@ class InstructorTaskModuleSubmitTest(InstructorTaskModuleTestCase):
with self.assertRaises(AlreadyRunningError): with self.assertRaises(AlreadyRunningError):
if student is not None: 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: 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): def test_submit_rescore_all(self):
self._test_submit_task(submit_rescore_problem_for_all_students) self._test_submit_task(submit_rescore_problem_for_all_students)
......
...@@ -16,6 +16,7 @@ from capa.tests.response_xml_factory import OptionResponseXMLFactory ...@@ -16,6 +16,7 @@ from capa.tests.response_xml_factory import OptionResponseXMLFactory
from xmodule.modulestore.django import editable_modulestore from xmodule.modulestore.django import editable_modulestore
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.locations import Location, SlashSeparatedCourseKey
from student.tests.factories import CourseEnrollmentFactory, UserFactory from student.tests.factories import CourseEnrollmentFactory, UserFactory
from courseware.model_data import StudentModule from courseware.model_data import StudentModule
...@@ -28,10 +29,10 @@ from instructor_task.views import instructor_task_status ...@@ -28,10 +29,10 @@ from instructor_task.views import instructor_task_status
TEST_COURSE_ORG = 'edx' TEST_COURSE_ORG = 'edx'
TEST_COURSE_NAME = 'test course' TEST_COURSE_NAME = 'test_course'
TEST_COURSE_NUMBER = '1.23x' TEST_COURSE_NUMBER = '1.23x'
TEST_COURSE_KEY = SlashSeparatedCourseKey(TEST_COURSE_ORG, TEST_COURSE_NUMBER, TEST_COURSE_NAME)
TEST_SECTION_NAME = "Problem" TEST_SECTION_NAME = "Problem"
TEST_COURSE_ID = 'edx/1.23x/test_course'
TEST_FAILURE_MESSAGE = 'task failed horribly' TEST_FAILURE_MESSAGE = 'task failed horribly'
TEST_FAILURE_EXCEPTION = 'RandomCauseError' TEST_FAILURE_EXCEPTION = 'RandomCauseError'
...@@ -54,9 +55,7 @@ class InstructorTaskTestCase(TestCase): ...@@ -54,9 +55,7 @@ class InstructorTaskTestCase(TestCase):
""" """
Create an internal location for a test problem. Create an internal location for a test problem.
""" """
return "i4x://{org}/{number}/problem/{problem_url_name}".format(org='edx', return TEST_COURSE_KEY.make_usage_key('problem', problem_url_name)
number='1.23x',
problem_url_name=problem_url_name)
def _create_entry(self, task_state=QUEUING, task_output=None, student=None): def _create_entry(self, task_state=QUEUING, task_output=None, student=None):
"""Creates a InstructorTask entry for testing.""" """Creates a InstructorTask entry for testing."""
...@@ -64,7 +63,7 @@ class InstructorTaskTestCase(TestCase): ...@@ -64,7 +63,7 @@ class InstructorTaskTestCase(TestCase):
progress_json = json.dumps(task_output) if task_output is not None else None 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) 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, requester=self.instructor,
task_input=json.dumps(task_input), task_input=json.dumps(task_input),
task_key=task_key, task_key=task_key,
...@@ -180,11 +179,9 @@ class InstructorTaskModuleTestCase(InstructorTaskCourseTestCase): ...@@ -180,11 +179,9 @@ class InstructorTaskModuleTestCase(InstructorTaskCourseTestCase):
Create an internal location for a test problem. Create an internal location for a test problem.
""" """
if "i4x:" in problem_url_name: if "i4x:" in problem_url_name:
return problem_url_name return Location.from_deprecated_string(problem_url_name)
else: else:
return "i4x://{org}/{number}/problem/{problem_url_name}".format(org=TEST_COURSE_ORG, return TEST_COURSE_KEY.make_usage_key('problem', problem_url_name)
number=TEST_COURSE_NUMBER,
problem_url_name=problem_url_name)
def define_option_problem(self, problem_url_name): def define_option_problem(self, problem_url_name):
"""Create the problem definition so the answer is Option 1""" """Create the problem definition so the answer is Option 1"""
...@@ -195,6 +192,7 @@ class InstructorTaskModuleTestCase(InstructorTaskCourseTestCase): ...@@ -195,6 +192,7 @@ class InstructorTaskModuleTestCase(InstructorTaskCourseTestCase):
'num_responses': 2} 'num_responses': 2}
problem_xml = factory.build_xml(**factory_args) problem_xml = factory.build_xml(**factory_args)
ItemFactory.create(parent_location=self.problem_section.location, ItemFactory.create(parent_location=self.problem_section.location,
parent=self.problem_section,
category="problem", category="problem",
display_name=str(problem_url_name), display_name=str(problem_url_name),
data=problem_xml) data=problem_xml)
...@@ -208,7 +206,7 @@ class InstructorTaskModuleTestCase(InstructorTaskCourseTestCase): ...@@ -208,7 +206,7 @@ class InstructorTaskModuleTestCase(InstructorTaskCourseTestCase):
'num_responses': 2} 'num_responses': 2}
problem_xml = factory.build_xml(**factory_args) problem_xml = factory.build_xml(**factory_args)
location = InstructorTaskTestCase.problem_location(problem_url_name) 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 item.data = problem_xml
self.module_store.update_item(item, '**replace_user**') self.module_store.update_item(item, '**replace_user**')
...@@ -217,5 +215,5 @@ class InstructorTaskModuleTestCase(InstructorTaskCourseTestCase): ...@@ -217,5 +215,5 @@ class InstructorTaskModuleTestCase(InstructorTaskCourseTestCase):
return StudentModule.objects.get(course_id=self.course.id, return StudentModule.objects.get(course_id=self.course.id,
student=User.objects.get(username=username), student=User.objects.get(username=username),
module_type=descriptor.location.category, module_type=descriptor.location.category,
module_state_key=descriptor.location.url(), module_id=descriptor.location,
) )
...@@ -13,6 +13,7 @@ from mock import Mock, MagicMock, patch ...@@ -13,6 +13,7 @@ from mock import Mock, MagicMock, patch
from celery.states import SUCCESS, FAILURE from celery.states import SUCCESS, FAILURE
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore.locations import i4xEncoder
from courseware.models import StudentModule from courseware.models import StudentModule
from courseware.tests.factories import StudentModuleFactory from courseware.tests.factories import StudentModuleFactory
...@@ -37,21 +38,21 @@ class TestInstructorTasks(InstructorTaskModuleTestCase): ...@@ -37,21 +38,21 @@ class TestInstructorTasks(InstructorTaskModuleTestCase):
super(InstructorTaskModuleTestCase, self).setUp() super(InstructorTaskModuleTestCase, self).setUp()
self.initialize_course() self.initialize_course()
self.instructor = self.create_instructor('instructor') 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): def _create_input_entry(self, student_ident=None, use_problem_url=True, course_id=None):
"""Creates a InstructorTask entry for testing.""" """Creates a InstructorTask entry for testing."""
task_id = str(uuid4()) task_id = str(uuid4())
task_input = {} task_input = {}
if use_problem_url: if use_problem_url:
task_input['problem_url'] = self.problem_url task_input['problem_url'] = self.location
if student_ident is not None: if student_ident is not None:
task_input['student'] = student_ident task_input['student'] = student_ident
course_id = course_id or self.course.id course_id = course_id or self.course.id
instructor_task = InstructorTaskFactory.create(course_id=course_id, instructor_task = InstructorTaskFactory.create(course_id=course_id,
requester=self.instructor, requester=self.instructor,
task_input=json.dumps(task_input), task_input=json.dumps(task_input, cls=i4xEncoder),
task_key='dummy value', task_key='dummy value',
task_id=task_id) task_id=task_id)
return instructor_task return instructor_task
...@@ -127,7 +128,7 @@ class TestInstructorTasks(InstructorTaskModuleTestCase): ...@@ -127,7 +128,7 @@ class TestInstructorTasks(InstructorTaskModuleTestCase):
for student in students: for student in students:
CourseEnrollmentFactory.create(course_id=self.course.id, user=student) CourseEnrollmentFactory.create(course_id=self.course.id, user=student)
StudentModuleFactory.create(course_id=self.course.id, StudentModuleFactory.create(course_id=self.course.id,
module_state_key=self.problem_url, module_id=self.location,
student=student, student=student,
grade=grade, grade=grade,
max_grade=max_grade, max_grade=max_grade,
...@@ -139,7 +140,7 @@ class TestInstructorTasks(InstructorTaskModuleTestCase): ...@@ -139,7 +140,7 @@ class TestInstructorTasks(InstructorTaskModuleTestCase):
for student in students: for student in students:
module = StudentModule.objects.get(course_id=self.course.id, module = StudentModule.objects.get(course_id=self.course.id,
student=student, student=student,
module_state_key=self.problem_url) module_id=self.location)
state = json.loads(module.state) state = json.loads(module.state)
self.assertEquals(state['attempts'], num_attempts) self.assertEquals(state['attempts'], num_attempts)
...@@ -356,7 +357,7 @@ class TestResetAttemptsInstructorTask(TestInstructorTasks): ...@@ -356,7 +357,7 @@ class TestResetAttemptsInstructorTask(TestInstructorTasks):
for student in students: for student in students:
module = StudentModule.objects.get(course_id=self.course.id, module = StudentModule.objects.get(course_id=self.course.id,
student=student, student=student,
module_state_key=self.problem_url) module_id=self.location)
state = json.loads(module.state) state = json.loads(module.state)
self.assertEquals(state['attempts'], initial_attempts) self.assertEquals(state['attempts'], initial_attempts)
...@@ -382,7 +383,7 @@ class TestResetAttemptsInstructorTask(TestInstructorTasks): ...@@ -382,7 +383,7 @@ class TestResetAttemptsInstructorTask(TestInstructorTasks):
for index, student in enumerate(students): for index, student in enumerate(students):
module = StudentModule.objects.get(course_id=self.course.id, module = StudentModule.objects.get(course_id=self.course.id,
student=student, student=student,
module_state_key=self.problem_url) module_id=self.location)
state = json.loads(module.state) state = json.loads(module.state)
if index == 3: if index == 3:
self.assertEquals(state['attempts'], 0) self.assertEquals(state['attempts'], 0)
...@@ -429,11 +430,11 @@ class TestDeleteStateInstructorTask(TestInstructorTasks): ...@@ -429,11 +430,11 @@ class TestDeleteStateInstructorTask(TestInstructorTasks):
for student in students: for student in students:
StudentModule.objects.get(course_id=self.course.id, StudentModule.objects.get(course_id=self.course.id,
student=student, student=student,
module_state_key=self.problem_url) module_id=self.location)
self._test_run_with_task(delete_problem_state, 'deleted', num_students) self._test_run_with_task(delete_problem_state, 'deleted', num_students)
# confirm that no state can be found anymore: # confirm that no state can be found anymore:
for student in students: for student in students:
with self.assertRaises(StudentModule.DoesNotExist): with self.assertRaises(StudentModule.DoesNotExist):
StudentModule.objects.get(course_id=self.course.id, StudentModule.objects.get(course_id=self.course.id,
student=student, student=student,
module_state_key=self.problem_url) module_id=self.location)
...@@ -4,6 +4,8 @@ from django.db import models, transaction ...@@ -4,6 +4,8 @@ from django.db import models, transaction
from student.models import User from student.models import User
from xmodule_django.models import CourseKeyField
log = logging.getLogger("edx.licenses") log = logging.getLogger("edx.licenses")
...@@ -11,7 +13,7 @@ class CourseSoftware(models.Model): ...@@ -11,7 +13,7 @@ class CourseSoftware(models.Model):
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
full_name = models.CharField(max_length=255) full_name = models.CharField(max_length=255)
url = 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): def __unicode__(self):
return u'{0} for {1}'.format(self.name, self.course_id) return u'{0} for {1}'.format(self.name, self.course_id)
......
...@@ -6,6 +6,7 @@ from collections import namedtuple, defaultdict ...@@ -6,6 +6,7 @@ from collections import namedtuple, defaultdict
from edxmako.shortcuts import render_to_string 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.decorators import login_required
from django.contrib.auth.models import User from django.contrib.auth.models import User
...@@ -59,6 +60,7 @@ def user_software_license(request): ...@@ -59,6 +60,7 @@ def user_software_license(request):
if not match: if not match:
raise Http404 raise Http404
course_id = match.groupdict().get('id', '') course_id = match.groupdict().get('id', '')
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
user_id = request.session.get('_auth_user_id') user_id = request.session.get('_auth_user_id')
software_name = request.POST.get('software') software_name = request.POST.get('software')
...@@ -66,7 +68,7 @@ def user_software_license(request): ...@@ -66,7 +68,7 @@ def user_software_license(request):
try: try:
software = CourseSoftware.objects.get(name=software_name, software = CourseSoftware.objects.get(name=software_name,
course_id=course_id) course_id=course_key)
except CourseSoftware.DoesNotExist: except CourseSoftware.DoesNotExist:
raise Http404 raise Http404
......
# -*- 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 <barney@bedrock.gov>'])
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 <fred@bedrock.gov>'])
self.assertEqual(
mail.outbox[0].subject, 'Fred Flintstone, Add your Achievements to your LinkedIn Profile')
self.assertEqual(
mail.outbox[1].to, ['Barney Rubble <barney@bedrock.gov>'])
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()
...@@ -121,7 +121,7 @@ def manage_modulestores(request, reload_dir=None, commit_id=None): ...@@ -121,7 +121,7 @@ def manage_modulestores(request, reload_dir=None, commit_id=None):
settings.EDX_ROOT_URL, settings.EDX_ROOT_URL,
escape(cdir), escape(cdir),
escape(cdir), escape(cdir),
course.location.url() course.location.to_deprecated_string()
) )
html += '</ol>' html += '</ol>'
......
from xmodule.modulestore.locations import SlashSeparatedCourseKey
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.http import HttpResponse, Http404 from django.http import HttpResponse, Http404
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
...@@ -34,11 +35,11 @@ ApiResponse = collections.namedtuple('ApiResponse', ['http_response', 'data']) ...@@ -34,11 +35,11 @@ ApiResponse = collections.namedtuple('ApiResponse', ['http_response', 'data'])
# API requests are routed through api_request() using the resource map. # 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. 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) return notes_enabled_for_course(course)
...@@ -49,9 +50,11 @@ def api_request(request, course_id, **kwargs): ...@@ -49,9 +50,11 @@ def api_request(request, course_id, **kwargs):
Raises a 404 if the requested resource does not exist or notes are Raises a 404 if the requested resource does not exist or notes are
disabled for the course. 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 # 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)) log.debug('Notes are disabled for course: {0}'.format(course_id))
raise Http404 raise Http404
...@@ -78,7 +81,7 @@ def api_request(request, course_id, **kwargs): ...@@ -78,7 +81,7 @@ def api_request(request, course_id, **kwargs):
log.debug('API request: {0} {1}'.format(resource_method, resource_name)) 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) http_response = api_format(api_response)
return http_response return http_response
...@@ -104,33 +107,33 @@ def api_format(api_response): ...@@ -104,33 +107,33 @@ def api_format(api_response):
return http_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. 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. # API actions exposed via the resource map.
def index(request, course_id): def index(request, course_key):
''' '''
Returns a list of annotation objects. Returns a list of annotation objects.
''' '''
MAX_LIMIT = API_SETTINGS.get('MAX_NOTE_LIMIT') 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] user=request.user)[:MAX_LIMIT]
return ApiResponse(http_response=HttpResponse(), data=[note.as_dict() for note in notes]) 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. 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: try:
note.clean(request.body) note.clean(request.body)
...@@ -145,7 +148,7 @@ def create(request, course_id): ...@@ -145,7 +148,7 @@ def create(request, course_id):
return ApiResponse(http_response=response, data=None) 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. Returns a single annotation object.
''' '''
...@@ -160,7 +163,7 @@ def read(request, course_id, note_id): ...@@ -160,7 +163,7 @@ def read(request, course_id, note_id):
return ApiResponse(http_response=HttpResponse(), data=note.as_dict()) 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. Updates an annotation object and returns a 303 with the read location.
''' '''
...@@ -203,7 +206,7 @@ def delete(request, course_id, note_id): ...@@ -203,7 +206,7 @@ def delete(request, course_id, note_id):
return ApiResponse(http_response=HttpResponse('', status=204), data=None) 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. Returns a subset of annotation objects based on a search query.
''' '''
...@@ -228,7 +231,7 @@ def search(request, course_id): ...@@ -228,7 +231,7 @@ def search(request, course_id):
limit = MAX_LIMIT limit = MAX_LIMIT
# set filters # set filters
filters = {'course_id': course_id, 'user': request.user} filters = {'course_id': course_key, 'user': request.user}
if uri != '': if uri != '':
filters['uri'] = uri filters['uri'] = uri
...@@ -244,7 +247,7 @@ def search(request, course_id): ...@@ -244,7 +247,7 @@ def search(request, course_id):
return ApiResponse(http_response=HttpResponse(), data=result) return ApiResponse(http_response=HttpResponse(), data=result)
def root(request, course_id): def root(request, course_key):
''' '''
Returns version information about the API. Returns version information about the API.
''' '''
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment