Make course ids and usage ids opaque to LMS and Studio [partial commit]
This commit adds the non-courseware lms/djangoapps and lms/lib. These keys are now objects with a limited interface, and the particular internal representation is managed by the data storage layer (the modulestore). For the LMS, there should be no outward-facing changes to the system. The keys are, for now, a change to internal representation only. For Studio, the new serialized form of the keys is used in urls, to allow for further migration in the future. Co-Author: Andy Armstrong <andya@edx.org> Co-Author: Christina Roberts <christina@edx.org> Co-Author: David Baumgold <db@edx.org> Co-Author: Diana Huang <dkh@edx.org> Co-Author: Don Mitchell <dmitchell@edx.org> Co-Author: Julia Hansbrough <julia@edx.org> Co-Author: Nimisha Asthagiri <nasthagiri@edx.org> Co-Author: Sarina Canelake <sarina@edx.org> [LMS-2370]
This commit is contained in:
@@ -70,7 +70,7 @@ def dump_grading_context(course):
|
||||
subgrader.index = 1
|
||||
graders[subgrader.type] = subgrader
|
||||
msg += hbar
|
||||
msg += "Listing grading context for course %s\n" % course.id
|
||||
msg += "Listing grading context for course %s\n" % course.id.to_deprecated_string()
|
||||
|
||||
gcontext = course.grading_context
|
||||
msg += "graded sections:\n"
|
||||
|
||||
@@ -5,6 +5,7 @@ Tests for instructor.basic
|
||||
from django.test import TestCase
|
||||
from student.models import CourseEnrollment
|
||||
from student.tests.factories import UserFactory
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
|
||||
from analytics.basic import enrolled_students_features, AVAILABLE_FEATURES, STUDENT_FEATURES, PROFILE_FEATURES
|
||||
|
||||
@@ -13,14 +14,14 @@ class TestAnalyticsBasic(TestCase):
|
||||
""" Test basic analytics functions. """
|
||||
|
||||
def setUp(self):
|
||||
self.course_id = 'some/robot/course/id'
|
||||
self.course_key = SlashSeparatedCourseKey('robot', 'course', 'id')
|
||||
self.users = tuple(UserFactory() for _ in xrange(30))
|
||||
self.ces = tuple(CourseEnrollment.enroll(user, self.course_id)
|
||||
self.ces = tuple(CourseEnrollment.enroll(user, self.course_key)
|
||||
for user in self.users)
|
||||
|
||||
def test_enrolled_students_features_username(self):
|
||||
self.assertIn('username', AVAILABLE_FEATURES)
|
||||
userreports = enrolled_students_features(self.course_id, ['username'])
|
||||
userreports = enrolled_students_features(self.course_key, ['username'])
|
||||
self.assertEqual(len(userreports), len(self.users))
|
||||
for userreport in userreports:
|
||||
self.assertEqual(userreport.keys(), ['username'])
|
||||
@@ -30,7 +31,7 @@ class TestAnalyticsBasic(TestCase):
|
||||
query_features = ('username', 'name', 'email')
|
||||
for feature in query_features:
|
||||
self.assertIn(feature, AVAILABLE_FEATURES)
|
||||
userreports = enrolled_students_features(self.course_id, query_features)
|
||||
userreports = enrolled_students_features(self.course_key, query_features)
|
||||
self.assertEqual(len(userreports), len(self.users))
|
||||
for userreport in userreports:
|
||||
self.assertEqual(set(userreport.keys()), set(query_features))
|
||||
|
||||
@@ -4,6 +4,7 @@ from django.test import TestCase
|
||||
from nose.tools import raises
|
||||
from student.models import CourseEnrollment
|
||||
from student.tests.factories import UserFactory
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
|
||||
from analytics.distributions import profile_distribution, AVAILABLE_PROFILE_FEATURES
|
||||
|
||||
@@ -12,7 +13,7 @@ class TestAnalyticsDistributions(TestCase):
|
||||
'''Test analytics distribution gathering.'''
|
||||
|
||||
def setUp(self):
|
||||
self.course_id = 'some/robot/course/id'
|
||||
self.course_id = SlashSeparatedCourseKey('robot', 'course', 'id')
|
||||
|
||||
self.users = [UserFactory(
|
||||
profile__gender=['m', 'f', 'o'][i % 3],
|
||||
@@ -53,7 +54,7 @@ class TestAnalyticsDistributionsNoData(TestCase):
|
||||
'''Test analytics distribution gathering.'''
|
||||
|
||||
def setUp(self):
|
||||
self.course_id = 'some/robot/course/id'
|
||||
self.course_id = SlashSeparatedCourseKey('robot', 'course', 'id')
|
||||
|
||||
self.users = [UserFactory(
|
||||
profile__year_of_birth=i + 1930,
|
||||
|
||||
@@ -8,9 +8,10 @@ from django.core.exceptions import ValidationError
|
||||
|
||||
from bulk_email.models import CourseEmailTemplate, COURSE_EMAIL_MESSAGE_BODY_TAG, CourseAuthorization
|
||||
|
||||
from courseware.courses import get_course_by_id
|
||||
from opaque_keys import InvalidKeyError
|
||||
from xmodule.modulestore import XML_MODULESTORE_TYPE
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -57,22 +58,26 @@ class CourseAuthorizationAdminForm(forms.ModelForm): # pylint: disable=R0924
|
||||
|
||||
def clean_course_id(self):
|
||||
"""Validate the course id"""
|
||||
course_id = self.cleaned_data["course_id"]
|
||||
cleaned_id = self.cleaned_data["course_id"]
|
||||
try:
|
||||
# Just try to get the course descriptor.
|
||||
# If we can do that, it's a real course.
|
||||
get_course_by_id(course_id, depth=1)
|
||||
except Exception as exc:
|
||||
msg = 'Error encountered ({0})'.format(str(exc).capitalize())
|
||||
msg += u' --- Entered course id was: "{0}". '.format(course_id)
|
||||
msg += 'Please recheck that you have supplied a course id in the format: ORG/COURSE/RUN'
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(cleaned_id)
|
||||
except InvalidKeyError:
|
||||
msg = u'Course id invalid.'
|
||||
msg += u' --- Entered course id was: "{0}". '.format(cleaned_id)
|
||||
msg += 'Please recheck that you have supplied a valid course id.'
|
||||
raise forms.ValidationError(msg)
|
||||
|
||||
if not modulestore().has_course(course_id):
|
||||
msg = u'COURSE NOT FOUND'
|
||||
msg += u' --- Entered course id was: "{0}". '.format(course_id.to_deprecated_string())
|
||||
msg += 'Please recheck that you have supplied a valid course id.'
|
||||
raise forms.ValidationError(msg)
|
||||
|
||||
# Now, try and discern if it is a Studio course - HTML editor doesn't work with XML courses
|
||||
is_studio_course = modulestore().get_modulestore_type(course_id) != XML_MODULESTORE_TYPE
|
||||
if not is_studio_course:
|
||||
msg = "Course Email feature is only available for courses authored in Studio. "
|
||||
msg += '"{0}" appears to be an XML backed course.'.format(course_id)
|
||||
msg += '"{0}" appears to be an XML backed course.'.format(course_id.to_deprecated_string())
|
||||
raise forms.ValidationError(msg)
|
||||
|
||||
return course_id
|
||||
return course_id.to_deprecated_string()
|
||||
|
||||
@@ -19,6 +19,8 @@ from django.db import models, transaction
|
||||
from html_to_text import html_to_text
|
||||
from mail_utils import wrap_message
|
||||
|
||||
from xmodule_django.models import CourseKeyField
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# Bulk email to_options - the send to options that users can
|
||||
@@ -63,7 +65,7 @@ class CourseEmail(Email):
|
||||
(SEND_TO_STAFF, 'Staff and instructors'),
|
||||
(SEND_TO_ALL, 'All')
|
||||
)
|
||||
course_id = models.CharField(max_length=255, db_index=True)
|
||||
course_id = CourseKeyField(max_length=255, db_index=True)
|
||||
to_option = models.CharField(max_length=64, choices=TO_OPTION_CHOICES, default=SEND_TO_MYSELF)
|
||||
|
||||
def __unicode__(self):
|
||||
@@ -127,7 +129,7 @@ class Optout(models.Model):
|
||||
# We need to first create the 'user' column with some sort of default in order to run the data migration,
|
||||
# and given the unique index, 'null' is the best default value.
|
||||
user = models.ForeignKey(User, db_index=True, null=True)
|
||||
course_id = models.CharField(max_length=255, db_index=True)
|
||||
course_id = CourseKeyField(max_length=255, db_index=True)
|
||||
|
||||
class Meta: # pylint: disable=C0111
|
||||
unique_together = ('user', 'course_id')
|
||||
@@ -220,7 +222,7 @@ class CourseAuthorization(models.Model):
|
||||
Enable the course email feature on a course-by-course basis.
|
||||
"""
|
||||
# The course that these features are attached to.
|
||||
course_id = models.CharField(max_length=255, db_index=True, unique=True)
|
||||
course_id = CourseKeyField(max_length=255, db_index=True, unique=True)
|
||||
|
||||
# Whether or not to enable instructor email
|
||||
email_enabled = models.BooleanField(default=False)
|
||||
@@ -247,4 +249,5 @@ class CourseAuthorization(models.Model):
|
||||
not_en = "Not "
|
||||
if self.email_enabled:
|
||||
not_en = ""
|
||||
return u"Course '{}': Instructor Email {}Enabled".format(self.course_id, not_en)
|
||||
# pylint: disable=no-member
|
||||
return u"Course '{}': Instructor Email {}Enabled".format(self.course_id.to_deprecated_string(), not_en)
|
||||
|
||||
@@ -107,8 +107,8 @@ def _get_recipient_queryset(user_id, to_option, course_id, course_location):
|
||||
if to_option == SEND_TO_MYSELF:
|
||||
recipient_qset = User.objects.filter(id=user_id)
|
||||
else:
|
||||
staff_qset = CourseStaffRole(course_location).users_with_role()
|
||||
instructor_qset = CourseInstructorRole(course_location).users_with_role()
|
||||
staff_qset = CourseStaffRole(course_id).users_with_role()
|
||||
instructor_qset = CourseInstructorRole(course_id).users_with_role()
|
||||
recipient_qset = staff_qset | instructor_qset
|
||||
if to_option == SEND_TO_ALL:
|
||||
# We also require students to have activated their accounts to
|
||||
@@ -129,7 +129,7 @@ def _get_course_email_context(course):
|
||||
"""
|
||||
Returns context arguments to apply to all emails, independent of recipient.
|
||||
"""
|
||||
course_id = course.id
|
||||
course_id = course.id.to_deprecated_string()
|
||||
course_title = course.display_name
|
||||
course_url = 'https://{}{}'.format(
|
||||
settings.SITE_NAME,
|
||||
@@ -160,9 +160,9 @@ def perform_delegate_email_batches(entry_id, course_id, task_input, action_name)
|
||||
# Perfunctory check, since expansion is made for convenience of other task
|
||||
# code that doesn't need the entry_id.
|
||||
if course_id != entry.course_id:
|
||||
format_msg = u"Course id conflict: explicit value {} does not match task value {}"
|
||||
log.warning("Task %s: %s", task_id, format_msg.format(course_id, entry.course_id))
|
||||
raise ValueError("Course id conflict: explicit value does not match task value")
|
||||
format_msg = u"Course id conflict: explicit value %r does not match task value %r"
|
||||
log.warning("Task %s: " + format_msg, task_id, course_id, entry.course_id)
|
||||
raise ValueError(format_msg % (course_id, entry.course_id))
|
||||
|
||||
# Fetch the CourseEmail.
|
||||
email_id = task_input['email_id']
|
||||
@@ -188,16 +188,17 @@ def perform_delegate_email_batches(entry_id, course_id, task_input, action_name)
|
||||
|
||||
# Sanity check that course for email_obj matches that of the task referencing it.
|
||||
if course_id != email_obj.course_id:
|
||||
format_msg = u"Course id conflict: explicit value {} does not match email value {}"
|
||||
log.warning("Task %s: %s", task_id, format_msg.format(course_id, entry.course_id))
|
||||
raise ValueError("Course id conflict: explicit value does not match email value")
|
||||
format_msg = u"Course id conflict: explicit value %r does not match email value %r"
|
||||
log.warning("Task %s: " + format_msg, task_id, course_id, email_obj.course_id)
|
||||
raise ValueError(format_msg % (course_id, email_obj.course_id))
|
||||
|
||||
# Fetch the course object.
|
||||
try:
|
||||
course = get_course(course_id)
|
||||
except ValueError:
|
||||
log.exception("Task %s: course not found: %s", task_id, course_id)
|
||||
raise
|
||||
course = get_course(course_id)
|
||||
|
||||
if course is None:
|
||||
msg = "Task %s: course not found: %s"
|
||||
log.error(msg, task_id, course_id)
|
||||
raise ValueError(msg % (task_id, course_id))
|
||||
|
||||
# Get arguments that will be passed to every subtask.
|
||||
to_option = email_obj.to_option
|
||||
@@ -369,15 +370,14 @@ def _get_source_address(course_id, course_title):
|
||||
"""
|
||||
course_title_no_quotes = re.sub(r'"', '', course_title)
|
||||
|
||||
# The course_id is assumed to be in the form 'org/course_num/run',
|
||||
# so pull out the course_num. Then make sure that it can be used
|
||||
# For the email address, get the course. Then make sure that it can be used
|
||||
# in an email address, by substituting a '_' anywhere a non-(ascii, period, or dash)
|
||||
# character appears.
|
||||
course_num = Location.parse_course_id(course_id)['course']
|
||||
invalid_chars = re.compile(r"[^\w.-]")
|
||||
course_num = invalid_chars.sub('_', course_num)
|
||||
|
||||
from_addr = u'"{0}" Course Staff <{1}-{2}>'.format(course_title_no_quotes, course_num, settings.BULK_EMAIL_DEFAULT_FROM_EMAIL)
|
||||
from_addr = u'"{0}" Course Staff <{1}-{2}>'.format(
|
||||
course_title_no_quotes,
|
||||
re.sub(r"[^\w.-]", '_', course_id.course),
|
||||
settings.BULK_EMAIL_DEFAULT_FROM_EMAIL
|
||||
)
|
||||
return from_addr
|
||||
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ class TestOptoutCourseEmails(ModuleStoreTestCase):
|
||||
def navigate_to_email_view(self):
|
||||
"""Navigate to the instructor dash's email view"""
|
||||
# Pull up email view on instructor dashboard
|
||||
url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id})
|
||||
url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url)
|
||||
email_link = '<a href="#" onclick="goto(\'Email\')" class="None">Email</a>'
|
||||
# If this fails, it is likely because ENABLE_INSTRUCTOR_EMAIL is set to False
|
||||
@@ -69,7 +69,7 @@ class TestOptoutCourseEmails(ModuleStoreTestCase):
|
||||
url = reverse('change_email_settings')
|
||||
# This is a checkbox, so on the post of opting out (that is, an Un-check of the box),
|
||||
# the Post that is sent will not contain 'receive_emails'
|
||||
response = self.client.post(url, {'course_id': self.course.id})
|
||||
response = self.client.post(url, {'course_id': self.course.id.to_deprecated_string()})
|
||||
self.assertEquals(json.loads(response.content), {'success': True})
|
||||
|
||||
self.client.logout()
|
||||
@@ -77,7 +77,7 @@ class TestOptoutCourseEmails(ModuleStoreTestCase):
|
||||
self.client.login(username=self.instructor.username, password="test")
|
||||
self.navigate_to_email_view()
|
||||
|
||||
url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id})
|
||||
url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
test_email = {
|
||||
'action': 'Send email',
|
||||
'to_option': 'all',
|
||||
@@ -96,7 +96,7 @@ class TestOptoutCourseEmails(ModuleStoreTestCase):
|
||||
Make sure student receives course email after opting in.
|
||||
"""
|
||||
url = reverse('change_email_settings')
|
||||
response = self.client.post(url, {'course_id': self.course.id, 'receive_emails': 'on'})
|
||||
response = self.client.post(url, {'course_id': self.course.id.to_deprecated_string(), 'receive_emails': 'on'})
|
||||
self.assertEquals(json.loads(response.content), {'success': True})
|
||||
|
||||
self.client.logout()
|
||||
@@ -106,7 +106,7 @@ class TestOptoutCourseEmails(ModuleStoreTestCase):
|
||||
self.client.login(username=self.instructor.username, password="test")
|
||||
self.navigate_to_email_view()
|
||||
|
||||
url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id})
|
||||
url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
test_email = {
|
||||
'action': 'Send email',
|
||||
'to_option': 'all',
|
||||
|
||||
@@ -51,10 +51,10 @@ class TestEmailSendFromDashboard(ModuleStoreTestCase):
|
||||
course_title = u"ẗëṡẗ title イ乇丂イ ᄊ乇丂丂ムg乇 キo尺 ムレレ тэѕт мэѕѕаБэ"
|
||||
self.course = CourseFactory.create(display_name=course_title)
|
||||
|
||||
self.instructor = InstructorFactory(course=self.course.location)
|
||||
self.instructor = InstructorFactory(course=self.course.id)
|
||||
|
||||
# Create staff
|
||||
self.staff = [StaffFactory(course=self.course.location)
|
||||
self.staff = [StaffFactory(course=self.course.id)
|
||||
for _ in xrange(STAFF_COUNT)]
|
||||
|
||||
# Create students
|
||||
@@ -68,7 +68,7 @@ class TestEmailSendFromDashboard(ModuleStoreTestCase):
|
||||
self.client.login(username=self.instructor.username, password="test")
|
||||
|
||||
# Pull up email view on instructor dashboard
|
||||
self.url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id})
|
||||
self.url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(self.url)
|
||||
email_link = '<a href="#" onclick="goto(\'Email\')" class="None">Email</a>'
|
||||
# If this fails, it is likely because ENABLE_INSTRUCTOR_EMAIL is set to False
|
||||
|
||||
@@ -17,6 +17,7 @@ from django.db import DatabaseError
|
||||
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
from student.tests.factories import UserFactory, AdminFactory, CourseEnrollmentFactory
|
||||
|
||||
from bulk_email.models import CourseEmail, SEND_TO_ALL
|
||||
@@ -51,7 +52,7 @@ class TestEmailErrors(ModuleStoreTestCase):
|
||||
|
||||
# load initial content (since we don't run migrations as part of tests):
|
||||
call_command("loaddata", "course_email_template.json")
|
||||
self.url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id})
|
||||
self.url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
|
||||
def tearDown(self):
|
||||
patch.stopall()
|
||||
@@ -171,12 +172,13 @@ class TestEmailErrors(ModuleStoreTestCase):
|
||||
"""
|
||||
Tests exception when the course in the email doesn't exist
|
||||
"""
|
||||
course_id = "I/DONT/EXIST"
|
||||
course_id = SlashSeparatedCourseKey("I", "DONT", "EXIST")
|
||||
email = CourseEmail(course_id=course_id)
|
||||
email.save()
|
||||
entry = InstructorTask.create(course_id, "task_type", "task_key", "task_input", self.instructor)
|
||||
task_input = {"email_id": email.id} # pylint: disable=E1101
|
||||
with self.assertRaisesRegexp(ValueError, "Course not found"):
|
||||
# (?i) is a regex for ignore case
|
||||
with self.assertRaisesRegexp(ValueError, r"(?i)course not found"):
|
||||
perform_delegate_email_batches(entry.id, course_id, task_input, "action_name") # pylint: disable=E1101
|
||||
|
||||
def test_nonexistent_to_option(self):
|
||||
@@ -205,7 +207,7 @@ class TestEmailErrors(ModuleStoreTestCase):
|
||||
"""
|
||||
Tests exception when the course_id in CourseEmail is not the same as one explicitly passed in.
|
||||
"""
|
||||
email = CourseEmail(course_id="bogus_course_id", to_option=SEND_TO_ALL)
|
||||
email = CourseEmail(course_id=SlashSeparatedCourseKey("bogus", "course", "id"), to_option=SEND_TO_ALL)
|
||||
email.save()
|
||||
entry = InstructorTask.create(self.course.id, "task_type", "task_key", "task_input", self.instructor)
|
||||
task_input = {"email_id": email.id} # pylint: disable=E1101
|
||||
|
||||
@@ -11,12 +11,13 @@ from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
|
||||
from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE
|
||||
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore import XML_MODULESTORE_TYPE, Location
|
||||
from xmodule.modulestore import XML_MODULESTORE_TYPE
|
||||
|
||||
from mock import patch
|
||||
|
||||
from bulk_email.models import CourseAuthorization
|
||||
from bulk_email.forms import CourseAuthorizationAdminForm
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
|
||||
@@ -38,7 +39,7 @@ class CourseAuthorizationFormTest(ModuleStoreTestCase):
|
||||
# Initially course shouldn't be authorized
|
||||
self.assertFalse(CourseAuthorization.instructor_email_enabled(self.course.id))
|
||||
# Test authorizing the course, which should totally work
|
||||
form_data = {'course_id': self.course.id, 'email_enabled': True}
|
||||
form_data = {'course_id': self.course.id.to_deprecated_string(), 'email_enabled': True}
|
||||
form = CourseAuthorizationAdminForm(data=form_data)
|
||||
# Validation should work
|
||||
self.assertTrue(form.is_valid())
|
||||
@@ -51,7 +52,7 @@ class CourseAuthorizationFormTest(ModuleStoreTestCase):
|
||||
# Initially course shouldn't be authorized
|
||||
self.assertFalse(CourseAuthorization.instructor_email_enabled(self.course.id))
|
||||
# Test authorizing the course, which should totally work
|
||||
form_data = {'course_id': self.course.id, 'email_enabled': True}
|
||||
form_data = {'course_id': self.course.id.to_deprecated_string(), 'email_enabled': True}
|
||||
form = CourseAuthorizationAdminForm(data=form_data)
|
||||
# Validation should work
|
||||
self.assertTrue(form.is_valid())
|
||||
@@ -60,7 +61,7 @@ class CourseAuthorizationFormTest(ModuleStoreTestCase):
|
||||
self.assertTrue(CourseAuthorization.instructor_email_enabled(self.course.id))
|
||||
|
||||
# Now make a new course authorization with the same course id that tries to turn email off
|
||||
form_data = {'course_id': self.course.id, 'email_enabled': False}
|
||||
form_data = {'course_id': self.course.id.to_deprecated_string(), 'email_enabled': False}
|
||||
form = CourseAuthorizationAdminForm(data=form_data)
|
||||
# Validation should not work because course_id field is unique
|
||||
self.assertFalse(form.is_valid())
|
||||
@@ -77,16 +78,31 @@ class CourseAuthorizationFormTest(ModuleStoreTestCase):
|
||||
@patch.dict(settings.FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True, 'REQUIRE_COURSE_EMAIL_AUTH': True})
|
||||
def test_form_typo(self):
|
||||
# Munge course id
|
||||
bad_id = self.course.id + '_typo'
|
||||
bad_id = SlashSeparatedCourseKey(u'Broken{}'.format(self.course.id.org), '', self.course.id.run + '_typo')
|
||||
|
||||
form_data = {'course_id': bad_id, 'email_enabled': True}
|
||||
form_data = {'course_id': bad_id.to_deprecated_string(), 'email_enabled': True}
|
||||
form = CourseAuthorizationAdminForm(data=form_data)
|
||||
# Validation shouldn't work
|
||||
self.assertFalse(form.is_valid())
|
||||
|
||||
msg = u'Error encountered (Course not found.)'
|
||||
msg += u' --- Entered course id was: "{0}". '.format(bad_id)
|
||||
msg += 'Please recheck that you have supplied a course id in the format: ORG/COURSE/RUN'
|
||||
msg = u'COURSE NOT FOUND'
|
||||
msg += u' --- Entered course id was: "{0}". '.format(bad_id.to_deprecated_string())
|
||||
msg += 'Please recheck that you have supplied a valid course id.'
|
||||
self.assertEquals(msg, form._errors['course_id'][0]) # pylint: disable=protected-access
|
||||
|
||||
with self.assertRaisesRegexp(ValueError, "The CourseAuthorization could not be created because the data didn't validate."):
|
||||
form.save()
|
||||
|
||||
@patch.dict(settings.FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True, 'REQUIRE_COURSE_EMAIL_AUTH': True})
|
||||
def test_form_invalid_key(self):
|
||||
form_data = {'course_id': "asd::**!@#$%^&*())//foobar!!", 'email_enabled': True}
|
||||
form = CourseAuthorizationAdminForm(data=form_data)
|
||||
# Validation shouldn't work
|
||||
self.assertFalse(form.is_valid())
|
||||
|
||||
msg = u'Course id invalid.'
|
||||
msg += u' --- Entered course id was: "asd::**!@#$%^&*())//foobar!!". '
|
||||
msg += 'Please recheck that you have supplied a valid course id.'
|
||||
self.assertEquals(msg, form._errors['course_id'][0]) # pylint: disable=protected-access
|
||||
|
||||
with self.assertRaisesRegexp(ValueError, "The CourseAuthorization could not be created because the data didn't validate."):
|
||||
@@ -95,16 +111,14 @@ class CourseAuthorizationFormTest(ModuleStoreTestCase):
|
||||
@patch.dict(settings.FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True, 'REQUIRE_COURSE_EMAIL_AUTH': True})
|
||||
def test_course_name_only(self):
|
||||
# Munge course id - common
|
||||
bad_id = Location.parse_course_id(self.course.id)['name']
|
||||
|
||||
form_data = {'course_id': bad_id, 'email_enabled': True}
|
||||
form_data = {'course_id': self.course.id.run, 'email_enabled': True}
|
||||
form = CourseAuthorizationAdminForm(data=form_data)
|
||||
# Validation shouldn't work
|
||||
self.assertFalse(form.is_valid())
|
||||
|
||||
error_msg = form._errors['course_id'][0]
|
||||
self.assertIn(u'--- Entered course id was: "{0}". '.format(bad_id), error_msg)
|
||||
self.assertIn(u'Please recheck that you have supplied a course id in the format: ORG/COURSE/RUN', error_msg)
|
||||
self.assertIn(u'--- Entered course id was: "{0}". '.format(self.course.id.run), error_msg)
|
||||
self.assertIn(u'Please recheck that you have supplied a valid course id.', error_msg)
|
||||
|
||||
with self.assertRaisesRegexp(ValueError, "The CourseAuthorization could not be created because the data didn't validate."):
|
||||
form.save()
|
||||
@@ -116,17 +130,17 @@ class CourseAuthorizationXMLFormTest(ModuleStoreTestCase):
|
||||
|
||||
@patch.dict(settings.FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True, 'REQUIRE_COURSE_EMAIL_AUTH': True})
|
||||
def test_xml_course_authorization(self):
|
||||
course_id = 'edX/toy/2012_Fall'
|
||||
course_id = SlashSeparatedCourseKey('edX', 'toy', '2012_Fall')
|
||||
# Assert this is an XML course
|
||||
self.assertEqual(modulestore().get_modulestore_type(course_id), XML_MODULESTORE_TYPE)
|
||||
|
||||
form_data = {'course_id': course_id, 'email_enabled': True}
|
||||
form_data = {'course_id': course_id.to_deprecated_string(), 'email_enabled': True}
|
||||
form = CourseAuthorizationAdminForm(data=form_data)
|
||||
# Validation shouldn't work
|
||||
self.assertFalse(form.is_valid())
|
||||
|
||||
msg = u"Course Email feature is only available for courses authored in Studio. "
|
||||
msg += u'"{0}" appears to be an XML backed course.'.format(course_id)
|
||||
msg += u'"{0}" appears to be an XML backed course.'.format(course_id.to_deprecated_string())
|
||||
self.assertEquals(msg, form._errors['course_id'][0]) # pylint: disable=protected-access
|
||||
|
||||
with self.assertRaisesRegexp(ValueError, "The CourseAuthorization could not be created because the data didn't validate."):
|
||||
|
||||
@@ -10,13 +10,14 @@ from student.tests.factories import UserFactory
|
||||
from mock import patch
|
||||
|
||||
from bulk_email.models import CourseEmail, SEND_TO_STAFF, CourseEmailTemplate, CourseAuthorization
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
|
||||
|
||||
class CourseEmailTest(TestCase):
|
||||
"""Test the CourseEmail model."""
|
||||
|
||||
def test_creation(self):
|
||||
course_id = 'abc/123/doremi'
|
||||
course_id = SlashSeparatedCourseKey('abc', '123', 'doremi')
|
||||
sender = UserFactory.create()
|
||||
to_option = SEND_TO_STAFF
|
||||
subject = "dummy subject"
|
||||
@@ -29,7 +30,7 @@ class CourseEmailTest(TestCase):
|
||||
self.assertEquals(email.sender, sender)
|
||||
|
||||
def test_bad_to_option(self):
|
||||
course_id = 'abc/123/doremi'
|
||||
course_id = SlashSeparatedCourseKey('abc', '123', 'doremi')
|
||||
sender = UserFactory.create()
|
||||
to_option = "fake"
|
||||
subject = "dummy subject"
|
||||
@@ -109,7 +110,7 @@ class CourseAuthorizationTest(TestCase):
|
||||
|
||||
@patch.dict(settings.FEATURES, {'REQUIRE_COURSE_EMAIL_AUTH': True})
|
||||
def test_creation_auth_on(self):
|
||||
course_id = 'abc/123/doremi'
|
||||
course_id = SlashSeparatedCourseKey('abc', '123', 'doremi')
|
||||
# Test that course is not authorized by default
|
||||
self.assertFalse(CourseAuthorization.instructor_email_enabled(course_id))
|
||||
|
||||
@@ -135,7 +136,7 @@ class CourseAuthorizationTest(TestCase):
|
||||
|
||||
@patch.dict(settings.FEATURES, {'REQUIRE_COURSE_EMAIL_AUTH': False})
|
||||
def test_creation_auth_off(self):
|
||||
course_id = 'blahx/blah101/ehhhhhhh'
|
||||
course_id = SlashSeparatedCourseKey('blahx', 'blah101', 'ehhhhhhh')
|
||||
# Test that course is authorized by default, since auth is turned off
|
||||
self.assertTrue(CourseAuthorization.instructor_email_enabled(course_id))
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ from instructor_task.subtasks import update_subtask_status, SubtaskStatus
|
||||
from instructor_task.models import InstructorTask
|
||||
from instructor_task.tests.test_base import InstructorTaskCourseTestCase
|
||||
from instructor_task.tests.factories import InstructorTaskFactory
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
|
||||
|
||||
class TestTaskFailure(Exception):
|
||||
@@ -119,7 +120,7 @@ class TestBulkEmailInstructorTask(InstructorTaskCourseTestCase):
|
||||
|
||||
def test_email_undefined_course(self):
|
||||
# Check that we fail when passing in a course that doesn't exist.
|
||||
task_entry = self._create_input_entry(course_id="bogus/course/id")
|
||||
task_entry = self._create_input_entry(course_id=SlashSeparatedCourseKey("bogus", "course", "id"))
|
||||
with self.assertRaises(ValueError):
|
||||
self._run_task_with_mock_celery(send_bulk_course_email, task_entry.id, task_entry.task_id)
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from optparse import make_option
|
||||
from opaque_keys import InvalidKeyError
|
||||
from xmodule.modulestore.keys import CourseKey
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
from certificates.models import CertificateWhitelist
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
@@ -48,6 +51,14 @@ class Command(BaseCommand):
|
||||
course_id = options['course_id']
|
||||
if not course_id:
|
||||
raise CommandError("You must specify a course-id")
|
||||
|
||||
# try to parse the serialized course key into a CourseKey
|
||||
try:
|
||||
course = CourseKey.from_string(course_id)
|
||||
except InvalidKeyError:
|
||||
log.warning("Course id %s could not be parsed as a CourseKey; falling back to SSCK.from_dep_str", course_id)
|
||||
course = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
|
||||
if options['add'] and options['del']:
|
||||
raise CommandError("Either remove or add a user, not both")
|
||||
|
||||
@@ -60,14 +71,14 @@ class Command(BaseCommand):
|
||||
|
||||
cert_whitelist, created = \
|
||||
CertificateWhitelist.objects.get_or_create(
|
||||
user=user, course_id=course_id)
|
||||
user=user, course_id=course)
|
||||
if options['add']:
|
||||
cert_whitelist.whitelist = True
|
||||
elif options['del']:
|
||||
cert_whitelist.whitelist = False
|
||||
cert_whitelist.save()
|
||||
|
||||
whitelist = CertificateWhitelist.objects.filter(course_id=course_id)
|
||||
whitelist = CertificateWhitelist.objects.filter(course_id=course)
|
||||
print "User whitelist for course {0}:\n{1}".format(course_id,
|
||||
'\n'.join(["{0} {1} {2}".format(
|
||||
u.user.username, u.user.email, u.whitelist)
|
||||
|
||||
@@ -40,8 +40,7 @@ class Command(BaseCommand):
|
||||
for course_id in [course # all courses in COURSE_LISTINGS
|
||||
for sub in settings.COURSE_LISTINGS
|
||||
for course in settings.COURSE_LISTINGS[sub]]:
|
||||
course_loc = CourseDescriptor.id_to_location(course_id)
|
||||
course = modulestore().get_instance(course_id, course_loc)
|
||||
course = modulestore().get_course(course_id)
|
||||
if course.has_ended():
|
||||
yield course_id
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ class Command(BaseCommand):
|
||||
student = User.objects.get(username=user, courseenrollment__course_id=course_id)
|
||||
|
||||
print "Fetching course data for {0}".format(course_id)
|
||||
course = modulestore().get_instance(course_id, CourseDescriptor.id_to_location(course_id), depth=2)
|
||||
course = modulestore().get_course(course_id, depth=2)
|
||||
|
||||
if not options['noop']:
|
||||
# Add the certificate request to the queue
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from certificates.models import certificate_status_for_student
|
||||
from certificates.queue import XQueueCertInterface
|
||||
from django.contrib.auth.models import User
|
||||
from optparse import make_option
|
||||
from django.conf import settings
|
||||
from opaque_keys import InvalidKeyError
|
||||
from xmodule.modulestore.keys import CourseKey
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from certificates.models import CertificateStatuses
|
||||
@@ -66,25 +69,23 @@ class Command(BaseCommand):
|
||||
STATUS_INTERVAL = 500
|
||||
|
||||
if options['course']:
|
||||
ended_courses = [options['course']]
|
||||
# try to parse out the course from the serialized form
|
||||
try:
|
||||
course = CourseKey.from_string(options['course'])
|
||||
except InvalidKeyError:
|
||||
log.warning("Course id %s could not be parsed as a CourseKey; falling back to SSCK.from_dep_str", course_id)
|
||||
course = SlashSeparatedCourseKey.from_deprecated_string(options['course'])
|
||||
ended_courses = [course]
|
||||
else:
|
||||
# Find all courses that have ended
|
||||
ended_courses = []
|
||||
for course_id in [course # all courses in COURSE_LISTINGS
|
||||
for sub in settings.COURSE_LISTINGS
|
||||
for course in settings.COURSE_LISTINGS[sub]]:
|
||||
course_loc = CourseDescriptor.id_to_location(course_id)
|
||||
course = modulestore().get_instance(course_id, course_loc)
|
||||
if course.has_ended():
|
||||
ended_courses.append(course_id)
|
||||
raise CommandError("You must specify a course")
|
||||
|
||||
for course_id in ended_courses:
|
||||
for course_key in ended_courses:
|
||||
# prefetch all chapters/sequentials by saying depth=2
|
||||
course = modulestore().get_instance(course_id, CourseDescriptor.id_to_location(course_id), depth=2)
|
||||
course = modulestore().get_course(course_key, depth=2)
|
||||
|
||||
print "Fetching enrolled students for {0}".format(course_id)
|
||||
print "Fetching enrolled students for {0}".format(course_key.to_deprecated_string())
|
||||
enrolled_students = User.objects.filter(
|
||||
courseenrollment__course_id=course_id)
|
||||
courseenrollment__course_id=course_key)
|
||||
|
||||
xq = XQueueCertInterface()
|
||||
if options['insecure']:
|
||||
@@ -108,9 +109,9 @@ class Command(BaseCommand):
|
||||
start = datetime.datetime.now(UTC)
|
||||
|
||||
if certificate_status_for_student(
|
||||
student, course_id)['status'] in valid_statuses:
|
||||
student, course_key)['status'] in valid_statuses:
|
||||
if not options['noop']:
|
||||
# Add the certificate request to the queue
|
||||
ret = xq.add_cert(student, course_id, course=course)
|
||||
ret = xq.add_cert(student, course_key, course=course)
|
||||
if ret == 'generating':
|
||||
print '{0} - {1}'.format(student, ret)
|
||||
|
||||
@@ -2,6 +2,7 @@ from django.contrib.auth.models import User
|
||||
from django.db import models
|
||||
from datetime import datetime
|
||||
from model_utils import Choices
|
||||
from xmodule_django.models import CourseKeyField, NoneToEmptyManager
|
||||
|
||||
"""
|
||||
Certificates are created for a student and an offering of a course.
|
||||
@@ -71,14 +72,17 @@ class CertificateWhitelist(models.Model):
|
||||
embargoed country restriction list
|
||||
(allow_certificate set to False in userprofile).
|
||||
"""
|
||||
|
||||
objects = NoneToEmptyManager()
|
||||
|
||||
user = models.ForeignKey(User)
|
||||
course_id = models.CharField(max_length=255, blank=True, default='')
|
||||
course_id = CourseKeyField(max_length=255, blank=True, default=None)
|
||||
whitelist = models.BooleanField(default=0)
|
||||
|
||||
|
||||
class GeneratedCertificate(models.Model):
|
||||
user = models.ForeignKey(User)
|
||||
course_id = models.CharField(max_length=255, blank=True, default='')
|
||||
course_id = CourseKeyField(max_length=255, blank=True, default=None)
|
||||
verify_uuid = models.CharField(max_length=32, blank=True, default='')
|
||||
download_uuid = models.CharField(max_length=32, blank=True, default='')
|
||||
download_url = models.CharField(max_length=128, blank=True, default='')
|
||||
|
||||
@@ -130,7 +130,7 @@ class XQueueCertInterface(object):
|
||||
|
||||
Arguments:
|
||||
student - User.object
|
||||
course_id - courseenrollment.course_id (string)
|
||||
course_id - courseenrollment.course_id (CourseKey)
|
||||
forced_grade - a string indicating a grade parameter to pass with
|
||||
the certificate request. If this is given, grading
|
||||
will be skipped.
|
||||
@@ -181,16 +181,15 @@ class XQueueCertInterface(object):
|
||||
mode_is_verified = (enrollment_mode == GeneratedCertificate.MODES.verified)
|
||||
user_is_verified = SoftwareSecurePhotoVerification.user_is_verified(student)
|
||||
user_is_reverified = SoftwareSecurePhotoVerification.user_is_reverified_for_all(course_id, student)
|
||||
course_id_dict = Location.parse_course_id(course_id)
|
||||
cert_mode = enrollment_mode
|
||||
if (mode_is_verified and user_is_verified and user_is_reverified):
|
||||
template_pdf = "certificate-template-{org}-{course}-verified.pdf".format(**course_id_dict)
|
||||
template_pdf = "certificate-template-{id.org}-{id.course}-verified.pdf".format(id=course_id)
|
||||
elif (mode_is_verified and not (user_is_verified and user_is_reverified)):
|
||||
template_pdf = "certificate-template-{org}-{course}.pdf".format(**course_id_dict)
|
||||
template_pdf = "certificate-template-{id.org}-{id.course}.pdf".format(id=course_id)
|
||||
cert_mode = GeneratedCertificate.MODES.honor
|
||||
else:
|
||||
# honor code and audit students
|
||||
template_pdf = "certificate-template-{org}-{course}.pdf".format(**course_id_dict)
|
||||
template_pdf = "certificate-template-{id.org}-{id.course}.pdf".format(id=course_id)
|
||||
if forced_grade:
|
||||
grade['grade'] = forced_grade
|
||||
|
||||
@@ -219,7 +218,7 @@ class XQueueCertInterface(object):
|
||||
contents = {
|
||||
'action': 'create',
|
||||
'username': student.username,
|
||||
'course_id': course_id,
|
||||
'course_id': course_id.to_deprecated_string(),
|
||||
'name': profile.name,
|
||||
'grade': grade['grade'],
|
||||
'template_pdf': template_pdf,
|
||||
|
||||
@@ -5,6 +5,7 @@ from django.views.decorators.csrf import csrf_exempt
|
||||
from django.http import HttpResponse
|
||||
import json
|
||||
from dogapi import dog_stats_api
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
from capa.xqueue_interface import XQUEUE_METRIC_NAME
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -27,21 +28,23 @@ def update_certificate(request):
|
||||
xqueue_header = json.loads(request.POST.get('xqueue_header'))
|
||||
|
||||
try:
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(xqueue_body['course_id'])
|
||||
|
||||
cert = GeneratedCertificate.objects.get(
|
||||
user__username=xqueue_body['username'],
|
||||
course_id=xqueue_body['course_id'],
|
||||
key=xqueue_header['lms_key'])
|
||||
user__username=xqueue_body['username'],
|
||||
course_id=course_key,
|
||||
key=xqueue_header['lms_key'])
|
||||
|
||||
except GeneratedCertificate.DoesNotExist:
|
||||
logger.critical('Unable to lookup certificate\n'
|
||||
'xqueue_body: {0}\n'
|
||||
'xqueue_header: {1}'.format(
|
||||
xqueue_body, xqueue_header))
|
||||
'xqueue_body: {0}\n'
|
||||
'xqueue_header: {1}'.format(
|
||||
xqueue_body, xqueue_header))
|
||||
|
||||
return HttpResponse(json.dumps({
|
||||
'return_code': 1,
|
||||
'content': 'unable to lookup key'}),
|
||||
mimetype='application/json')
|
||||
'return_code': 1,
|
||||
'content': 'unable to lookup key'}),
|
||||
mimetype='application/json')
|
||||
|
||||
if 'error' in xqueue_body:
|
||||
cert.status = status.error
|
||||
|
||||
@@ -7,11 +7,12 @@ from courseware import models
|
||||
from django.db.models import Count
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.inheritance import own_metadata
|
||||
from analytics.csvs import create_csv_response
|
||||
|
||||
from xmodule.modulestore import Location
|
||||
|
||||
# Used to limit the length of list displayed to the screen.
|
||||
MAX_SCREEN_LIST_LENGTH = 250
|
||||
|
||||
@@ -31,13 +32,13 @@ def get_problem_grade_distribution(course_id):
|
||||
course_id__exact=course_id,
|
||||
grade__isnull=False,
|
||||
module_type__exact="problem",
|
||||
).values('module_state_key', 'grade', 'max_grade').annotate(count_grade=Count('grade'))
|
||||
).values('module_id', 'grade', 'max_grade').annotate(count_grade=Count('grade'))
|
||||
|
||||
prob_grade_distrib = {}
|
||||
|
||||
# Loop through resultset building data for each problem
|
||||
for row in db_query:
|
||||
curr_problem = row['module_state_key']
|
||||
curr_problem = course_id.make_usage_key_from_deprecated_string(row['module_id'])
|
||||
|
||||
# Build set of grade distributions for each problem that has student responses
|
||||
if curr_problem in prob_grade_distrib:
|
||||
@@ -69,12 +70,13 @@ def get_sequential_open_distrib(course_id):
|
||||
db_query = models.StudentModule.objects.filter(
|
||||
course_id__exact=course_id,
|
||||
module_type__exact="sequential",
|
||||
).values('module_state_key').annotate(count_sequential=Count('module_state_key'))
|
||||
).values('module_id').annotate(count_sequential=Count('module_id'))
|
||||
|
||||
# Build set of "opened" data for each subsection that has "opened" data
|
||||
sequential_open_distrib = {}
|
||||
for row in db_query:
|
||||
sequential_open_distrib[row['module_state_key']] = row['count_sequential']
|
||||
row_loc = course_id.make_usage_key_from_deprecated_string(row['module_id'])
|
||||
sequential_open_distrib[row_loc] = row['count_sequential']
|
||||
|
||||
return sequential_open_distrib
|
||||
|
||||
@@ -85,7 +87,7 @@ def get_problem_set_grade_distrib(course_id, problem_set):
|
||||
|
||||
`course_id` the course ID for the course interested in
|
||||
|
||||
`problem_set` an array of strings representing problem module_id's.
|
||||
`problem_set` an array of UsageKeys representing problem module_id's.
|
||||
|
||||
Requests from the database the a count of each grade for each problem in the `problem_set`.
|
||||
|
||||
@@ -99,24 +101,25 @@ def get_problem_set_grade_distrib(course_id, problem_set):
|
||||
course_id__exact=course_id,
|
||||
grade__isnull=False,
|
||||
module_type__exact="problem",
|
||||
module_state_key__in=problem_set,
|
||||
module_id__in=problem_set,
|
||||
).values(
|
||||
'module_state_key',
|
||||
'module_id',
|
||||
'grade',
|
||||
'max_grade',
|
||||
).annotate(count_grade=Count('grade')).order_by('module_state_key', 'grade')
|
||||
).annotate(count_grade=Count('grade')).order_by('module_id', 'grade')
|
||||
|
||||
prob_grade_distrib = {}
|
||||
|
||||
# Loop through resultset building data for each problem
|
||||
for row in db_query:
|
||||
if row['module_state_key'] not in prob_grade_distrib:
|
||||
prob_grade_distrib[row['module_state_key']] = {
|
||||
row_loc = course_id.make_usage_key_from_deprecated_string(row['module_id'])
|
||||
if row_loc not in prob_grade_distrib:
|
||||
prob_grade_distrib[row_loc] = {
|
||||
'max_grade': 0,
|
||||
'grade_distrib': [],
|
||||
}
|
||||
|
||||
curr_grade_distrib = prob_grade_distrib[row['module_state_key']]
|
||||
curr_grade_distrib = prob_grade_distrib[row_loc]
|
||||
curr_grade_distrib['grade_distrib'].append((row['grade'], row['count_grade']))
|
||||
|
||||
if curr_grade_distrib['max_grade'] < row['max_grade']:
|
||||
@@ -140,7 +143,7 @@ def get_d3_problem_grade_distrib(course_id):
|
||||
d3_data = []
|
||||
|
||||
# Retrieve course object down to problems
|
||||
course = modulestore().get_instance(course_id, CourseDescriptor.id_to_location(course_id), depth=4)
|
||||
course = modulestore().get_course(course_id, depth=4)
|
||||
|
||||
# Iterate through sections, subsections, units, problems
|
||||
for section in course.get_children():
|
||||
@@ -165,10 +168,10 @@ def get_d3_problem_grade_distrib(course_id):
|
||||
label = "P{0}.{1}.{2}".format(c_subsection, c_unit, c_problem)
|
||||
|
||||
# Only problems in prob_grade_distrib have had a student submission.
|
||||
if child.location.url() in prob_grade_distrib:
|
||||
if child.location in prob_grade_distrib:
|
||||
|
||||
# Get max_grade, grade_distribution for this problem
|
||||
problem_info = prob_grade_distrib[child.location.url()]
|
||||
problem_info = prob_grade_distrib[child.location]
|
||||
|
||||
# Get problem_name for tooltip
|
||||
problem_name = own_metadata(child).get('display_name', '')
|
||||
@@ -197,7 +200,7 @@ def get_d3_problem_grade_distrib(course_id):
|
||||
'color': percent,
|
||||
'value': count_grade,
|
||||
'tooltip': tooltip,
|
||||
'module_url': child.location.url(),
|
||||
'module_url': child.location.to_deprecated_string(),
|
||||
})
|
||||
|
||||
problem = {
|
||||
@@ -227,7 +230,7 @@ def get_d3_sequential_open_distrib(course_id):
|
||||
d3_data = []
|
||||
|
||||
# Retrieve course object down to subsection
|
||||
course = modulestore().get_instance(course_id, CourseDescriptor.id_to_location(course_id), depth=2)
|
||||
course = modulestore().get_course(course_id, depth=2)
|
||||
|
||||
# Iterate through sections, subsections
|
||||
for section in course.get_children():
|
||||
@@ -242,8 +245,8 @@ def get_d3_sequential_open_distrib(course_id):
|
||||
subsection_name = own_metadata(subsection).get('display_name', '')
|
||||
|
||||
num_students = 0
|
||||
if subsection.location.url() in sequential_open_distrib:
|
||||
num_students = sequential_open_distrib[subsection.location.url()]
|
||||
if subsection.location in sequential_open_distrib:
|
||||
num_students = sequential_open_distrib[subsection.location]
|
||||
|
||||
stack_data = []
|
||||
tooltip = _("{num_students} student(s) opened Subsection {subsection_num}: {subsection_name}").format(
|
||||
@@ -256,7 +259,7 @@ def get_d3_sequential_open_distrib(course_id):
|
||||
'color': 0,
|
||||
'value': num_students,
|
||||
'tooltip': tooltip,
|
||||
'module_url': subsection.location.url(),
|
||||
'module_url': subsection.location.to_deprecated_string(),
|
||||
})
|
||||
subsection = {
|
||||
'xValue': "SS {0}".format(c_subsection),
|
||||
@@ -294,7 +297,7 @@ def get_d3_section_grade_distrib(course_id, section):
|
||||
"""
|
||||
|
||||
# Retrieve course object down to problems
|
||||
course = modulestore().get_instance(course_id, CourseDescriptor.id_to_location(course_id), depth=4)
|
||||
course = modulestore().get_course(course_id, depth=4)
|
||||
|
||||
problem_set = []
|
||||
problem_info = {}
|
||||
@@ -308,9 +311,9 @@ def get_d3_section_grade_distrib(course_id, section):
|
||||
for child in unit.get_children():
|
||||
if (child.location.category == 'problem'):
|
||||
c_problem += 1
|
||||
problem_set.append(child.location.url())
|
||||
problem_info[child.location.url()] = {
|
||||
'id': child.location.url(),
|
||||
problem_set.append(child.location)
|
||||
problem_info[child.location] = {
|
||||
'id': child.location.to_deprecated_string(),
|
||||
'x_value': "P{0}.{1}.{2}".format(c_subsection, c_unit, c_problem),
|
||||
'display_name': own_metadata(child).get('display_name', ''),
|
||||
}
|
||||
@@ -366,7 +369,7 @@ def get_section_display_name(course_id):
|
||||
The ith string in the array is the display name of the ith section in the course.
|
||||
"""
|
||||
|
||||
course = modulestore().get_instance(course_id, CourseDescriptor.id_to_location(course_id), depth=4)
|
||||
course = modulestore().get_course(course_id, depth=4)
|
||||
|
||||
section_display_name = [""] * len(course.get_children())
|
||||
i = 0
|
||||
@@ -386,7 +389,7 @@ def get_array_section_has_problem(course_id):
|
||||
The ith value in the array is true if the ith section in the course contains problems and false otherwise.
|
||||
"""
|
||||
|
||||
course = modulestore().get_instance(course_id, CourseDescriptor.id_to_location(course_id), depth=4)
|
||||
course = modulestore().get_course(course_id, depth=4)
|
||||
|
||||
b_section_has_problem = [False] * len(course.get_children())
|
||||
i = 0
|
||||
@@ -415,12 +418,12 @@ def get_students_opened_subsection(request, csv=False):
|
||||
If 'csv' is True, returns a header array, and an array of arrays in the format:
|
||||
student names, usernames for CSV download.
|
||||
"""
|
||||
module_id = request.GET.get('module_id')
|
||||
module_id = Location.from_deprecated_string(request.GET.get('module_id'))
|
||||
csv = request.GET.get('csv')
|
||||
|
||||
# Query for "opened a subsection" students
|
||||
students = models.StudentModule.objects.select_related('student').filter(
|
||||
module_state_key__exact=module_id,
|
||||
module_id__exact=module_id,
|
||||
module_type__exact='sequential',
|
||||
).values('student__username', 'student__profile__name').order_by('student__profile__name')
|
||||
|
||||
@@ -465,12 +468,12 @@ def get_students_problem_grades(request, csv=False):
|
||||
If 'csv' is True, returns a header array, and an array of arrays in the format:
|
||||
student names, usernames, grades, percents for CSV download.
|
||||
"""
|
||||
module_id = request.GET.get('module_id')
|
||||
module_id = Location.from_deprecated_string(request.GET.get('module_id'))
|
||||
csv = request.GET.get('csv')
|
||||
|
||||
# Query for "problem grades" students
|
||||
students = models.StudentModule.objects.select_related('student').filter(
|
||||
module_state_key__exact=module_id,
|
||||
module_id__exact=module_id,
|
||||
module_type__exact='problem',
|
||||
grade__isnull=False,
|
||||
).values('student__username', 'student__profile__name', 'grade', 'max_grade').order_by('student__profile__name')
|
||||
|
||||
@@ -14,7 +14,6 @@ from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
|
||||
from courseware.tests.factories import StudentModuleFactory
|
||||
from student.tests.factories import UserFactory, CourseEnrollmentFactory, AdminFactory
|
||||
from capa.tests.response_xml_factory import StringResponseXMLFactory
|
||||
from xmodule.modulestore import Location
|
||||
|
||||
from class_dashboard.dashboard_data import (get_problem_grade_distribution, get_sequential_open_distrib,
|
||||
get_problem_set_grade_distrib, get_d3_problem_grade_distrib,
|
||||
@@ -82,7 +81,7 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase):
|
||||
max_grade=1 if i < j else 0.5,
|
||||
student=user,
|
||||
course_id=self.course.id,
|
||||
module_state_key=Location(self.item.location).url(),
|
||||
module_state_key=self.item.location,
|
||||
state=json.dumps({'attempts': self.attempts}),
|
||||
)
|
||||
|
||||
@@ -90,7 +89,7 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase):
|
||||
StudentModuleFactory.create(
|
||||
course_id=self.course.id,
|
||||
module_type='sequential',
|
||||
module_state_key=Location(self.item.location).url(),
|
||||
module_state_key=self.item.location,
|
||||
)
|
||||
|
||||
def test_get_problem_grade_distribution(self):
|
||||
@@ -156,7 +155,7 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase):
|
||||
|
||||
def test_get_students_problem_grades(self):
|
||||
|
||||
attributes = '?module_id=' + self.item.location.url()
|
||||
attributes = '?module_id=' + self.item.location.to_deprecated_string()
|
||||
request = self.request_factory.get(reverse('get_students_problem_grades') + attributes)
|
||||
|
||||
response = get_students_problem_grades(request)
|
||||
@@ -174,7 +173,7 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase):
|
||||
def test_get_students_problem_grades_max(self):
|
||||
|
||||
with patch('class_dashboard.dashboard_data.MAX_SCREEN_LIST_LENGTH', 2):
|
||||
attributes = '?module_id=' + self.item.location.url()
|
||||
attributes = '?module_id=' + self.item.location.to_deprecated_string()
|
||||
request = self.request_factory.get(reverse('get_students_problem_grades') + attributes)
|
||||
|
||||
response = get_students_problem_grades(request)
|
||||
@@ -188,7 +187,7 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase):
|
||||
def test_get_students_problem_grades_csv(self):
|
||||
|
||||
tooltip = 'P1.2.1 Q1 - 3382 Students (100%: 1/1 questions)'
|
||||
attributes = '?module_id=' + self.item.location.url() + '&tooltip=' + tooltip + '&csv=true'
|
||||
attributes = '?module_id=' + self.item.location.to_deprecated_string() + '&tooltip=' + tooltip + '&csv=true'
|
||||
request = self.request_factory.get(reverse('get_students_problem_grades') + attributes)
|
||||
|
||||
response = get_students_problem_grades(request)
|
||||
@@ -208,7 +207,7 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase):
|
||||
|
||||
def test_get_students_opened_subsection(self):
|
||||
|
||||
attributes = '?module_id=' + self.item.location.url()
|
||||
attributes = '?module_id=' + self.item.location.to_deprecated_string()
|
||||
request = self.request_factory.get(reverse('get_students_opened_subsection') + attributes)
|
||||
|
||||
response = get_students_opened_subsection(request)
|
||||
@@ -221,7 +220,7 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase):
|
||||
|
||||
with patch('class_dashboard.dashboard_data.MAX_SCREEN_LIST_LENGTH', 2):
|
||||
|
||||
attributes = '?module_id=' + self.item.location.url()
|
||||
attributes = '?module_id=' + self.item.location.to_deprecated_string()
|
||||
request = self.request_factory.get(reverse('get_students_opened_subsection') + attributes)
|
||||
|
||||
response = get_students_opened_subsection(request)
|
||||
@@ -235,7 +234,7 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase):
|
||||
def test_get_students_opened_subsection_csv(self):
|
||||
|
||||
tooltip = '4162 student(s) opened Subsection 5: Relational Algebra Exercises'
|
||||
attributes = '?module_id=' + self.item.location.url() + '&tooltip=' + tooltip + '&csv=true'
|
||||
attributes = '?module_id=' + self.item.location.to_deprecated_string() + '&tooltip=' + tooltip + '&csv=true'
|
||||
request = self.request_factory.get(reverse('get_students_opened_subsection') + attributes)
|
||||
|
||||
response = get_students_opened_subsection(request)
|
||||
@@ -255,7 +254,7 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase):
|
||||
|
||||
def test_dashboard(self):
|
||||
|
||||
url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id})
|
||||
url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.post(
|
||||
url,
|
||||
{
|
||||
@@ -19,8 +19,8 @@ def has_instructor_access_for_class(user, course_id):
|
||||
Returns true if the `user` is an instructor for the course.
|
||||
"""
|
||||
|
||||
course = get_course_with_access(user, course_id, 'staff', depth=None)
|
||||
return has_access(user, course, 'staff')
|
||||
course = get_course_with_access(user, 'staff', course_id, depth=None)
|
||||
return has_access(user, 'staff', course)
|
||||
|
||||
|
||||
def all_sequential_open_distrib(request, course_id):
|
||||
|
||||
@@ -29,8 +29,8 @@ class WikiAccessMiddleware(object):
|
||||
if course_id:
|
||||
# See if we are able to view the course. If we are, redirect to it
|
||||
try:
|
||||
course = get_course_with_access(request.user, course_id, 'load')
|
||||
return redirect("/courses/{course_id}/wiki/{path}".format(course_id=course.id, path=wiki_path))
|
||||
_course = get_course_with_access(request.user, 'load', course_id)
|
||||
return redirect("/courses/{course_id}/wiki/{path}".format(course_id=course_id.to_deprecated_string(), path=wiki_path))
|
||||
except Http404:
|
||||
# Even though we came from the course, we can't see it. So don't worry about it.
|
||||
pass
|
||||
@@ -44,22 +44,23 @@ class WikiAccessMiddleware(object):
|
||||
if not view_func.__module__.startswith('wiki.'):
|
||||
return
|
||||
|
||||
course_id = course_id_from_url(request.path)
|
||||
wiki_path = request.path.split('/wiki/', 1)[1]
|
||||
|
||||
# wiki pages are login required
|
||||
if not request.user.is_authenticated():
|
||||
return redirect(reverse('accounts_login'), next=request.path)
|
||||
|
||||
course_id = course_id_from_url(request.path)
|
||||
wiki_path = request.path.partition('/wiki/')[2]
|
||||
|
||||
if course_id:
|
||||
# This is a /courses/org/name/run/wiki request
|
||||
# HACK: django-wiki monkeypatches the django reverse function to enable urls to be rewritten
|
||||
url_prefix = "/courses/{0}".format(course_id)
|
||||
reverse._transform_url = lambda url: url_prefix + url # pylint: disable=protected-access
|
||||
course_path = "/courses/{}".format(course_id.to_deprecated_string())
|
||||
# HACK: django-wiki monkeypatches the reverse function to enable
|
||||
# urls to be rewritten
|
||||
reverse._transform_url = lambda url: course_path + url # pylint: disable=protected-access
|
||||
# Authorization Check
|
||||
# Let's see if user is enrolled or the course allows for public access
|
||||
try:
|
||||
course = get_course_with_access(request.user, course_id, 'load')
|
||||
course = get_course_with_access(request.user, 'load', course_id)
|
||||
except Http404:
|
||||
# course does not exist. redirect to root wiki.
|
||||
# clearing the referrer will cause process_response not to redirect
|
||||
@@ -69,11 +70,11 @@ class WikiAccessMiddleware(object):
|
||||
|
||||
if not course.allow_public_wiki_access:
|
||||
is_enrolled = CourseEnrollment.is_enrolled(request.user, course.id)
|
||||
is_staff = has_access(request.user, course, 'staff')
|
||||
is_staff = has_access(request.user, 'staff', course)
|
||||
if not (is_enrolled or is_staff):
|
||||
# if a user is logged in, but not authorized to see a page,
|
||||
# we'll redirect them to the course about page
|
||||
return redirect('about_course', course_id)
|
||||
return redirect('about_course', course_id.to_deprecated_string())
|
||||
# set the course onto here so that the wiki template can show the course navigation
|
||||
request.course = course
|
||||
else:
|
||||
|
||||
@@ -4,7 +4,6 @@ Tests for wiki permissions
|
||||
|
||||
from django.contrib.auth.models import Group
|
||||
from student.tests.factories import UserFactory
|
||||
from xmodule.modulestore.django import loc_mapper
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
|
||||
@@ -48,12 +47,9 @@ class TestWikiAccessBase(ModuleStoreTestCase):
|
||||
def create_staff_for_course(self, course):
|
||||
"""Creates and returns users with instructor and staff access to course."""
|
||||
|
||||
course_locator = loc_mapper().translate_location(course.id, course.location)
|
||||
return [
|
||||
InstructorFactory(course=course.location), # Creates instructor_org/number/run role name
|
||||
StaffFactory(course=course.location), # Creates staff_org/number/run role name
|
||||
InstructorFactory(course=course_locator), # Creates instructor_org.number.run role name
|
||||
StaffFactory(course=course_locator), # Creates staff_org.number.run role name
|
||||
InstructorFactory(course=course.id), # Creates instructor_org/number/run role name
|
||||
StaffFactory(course=course.id), # Creates staff_org/number/run role name
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ class TestWikiAccessMiddleware(ModuleStoreTestCase):
|
||||
self.wiki = get_or_create_root()
|
||||
|
||||
self.course_math101 = CourseFactory.create(org='edx', number='math101', display_name='2014', metadata={'use_unique_wiki_id': 'false'})
|
||||
self.course_math101_instructor = InstructorFactory(course=self.course_math101.location, username='instructor', password='secret')
|
||||
self.course_math101_instructor = InstructorFactory(course=self.course_math101.id, username='instructor', password='secret')
|
||||
self.wiki_math101 = URLPath.create_article(self.wiki, 'math101', title='math101')
|
||||
|
||||
self.client = Client()
|
||||
|
||||
@@ -4,7 +4,7 @@ from django.test.utils import override_settings
|
||||
from courseware.tests.tests import LoginEnrollmentTestCase
|
||||
from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
|
||||
from mock import patch
|
||||
|
||||
@@ -15,7 +15,7 @@ class WikiRedirectTestCase(LoginEnrollmentTestCase):
|
||||
def setUp(self):
|
||||
|
||||
# Load the toy course
|
||||
self.toy = modulestore().get_course('edX/toy/2012_Fall')
|
||||
self.toy = modulestore().get_course(SlashSeparatedCourseKey('edX', 'toy', '2012_Fall'))
|
||||
|
||||
# Create two accounts
|
||||
self.student = 'view@test.com'
|
||||
@@ -43,7 +43,7 @@ class WikiRedirectTestCase(LoginEnrollmentTestCase):
|
||||
|
||||
self.enroll(self.toy)
|
||||
|
||||
referer = reverse("progress", kwargs={'course_id': self.toy.id})
|
||||
referer = reverse("progress", kwargs={'course_id': self.toy.id.to_deprecated_string()})
|
||||
destination = reverse("wiki:get", kwargs={'path': 'some/fake/wiki/page/'})
|
||||
|
||||
redirected_to = referer.replace("progress", "wiki/some/fake/wiki/page/")
|
||||
@@ -72,7 +72,7 @@ class WikiRedirectTestCase(LoginEnrollmentTestCase):
|
||||
|
||||
self.enroll(self.toy)
|
||||
|
||||
referer = reverse("progress", kwargs={'course_id': self.toy.id})
|
||||
referer = reverse("progress", kwargs={'course_id': self.toy.id.to_deprecated_string()})
|
||||
destination = reverse("wiki:get", kwargs={'path': 'some/fake/wiki/page/'})
|
||||
|
||||
resp = self.client.get(destination, HTTP_REFERER=referer)
|
||||
@@ -84,8 +84,8 @@ class WikiRedirectTestCase(LoginEnrollmentTestCase):
|
||||
The user must be enrolled in the course to see the page.
|
||||
"""
|
||||
|
||||
course_wiki_home = reverse('course_wiki', kwargs={'course_id': course.id})
|
||||
referer = reverse("progress", kwargs={'course_id': self.toy.id})
|
||||
course_wiki_home = reverse('course_wiki', kwargs={'course_id': course.id.to_deprecated_string()})
|
||||
referer = reverse("progress", kwargs={'course_id': self.toy.id.to_deprecated_string()})
|
||||
|
||||
resp = self.client.get(course_wiki_home, follow=True, HTTP_REFERER=referer)
|
||||
|
||||
@@ -117,8 +117,7 @@ class WikiRedirectTestCase(LoginEnrollmentTestCase):
|
||||
self.create_course_page(self.toy)
|
||||
|
||||
course_wiki_page = reverse('wiki:get', kwargs={'path': self.toy.wiki_slug + '/'})
|
||||
|
||||
referer = reverse("courseware", kwargs={'course_id': self.toy.id})
|
||||
referer = reverse("courseware", kwargs={'course_id': self.toy.id.to_deprecated_string()})
|
||||
|
||||
resp = self.client.get(course_wiki_page, follow=True, HTTP_REFERER=referer)
|
||||
|
||||
@@ -137,7 +136,7 @@ class WikiRedirectTestCase(LoginEnrollmentTestCase):
|
||||
|
||||
self.login(self.student, self.password)
|
||||
course_wiki_page = reverse('wiki:get', kwargs={'path': self.toy.wiki_slug + '/'})
|
||||
referer = reverse("courseware", kwargs={'course_id': self.toy.id})
|
||||
referer = reverse("courseware", kwargs={'course_id': self.toy.id.to_deprecated_string()})
|
||||
|
||||
# When not enrolled, we should get a 302
|
||||
resp = self.client.get(course_wiki_page, follow=False, HTTP_REFERER=referer)
|
||||
@@ -147,7 +146,7 @@ class WikiRedirectTestCase(LoginEnrollmentTestCase):
|
||||
resp = self.client.get(course_wiki_page, follow=True, HTTP_REFERER=referer)
|
||||
target_url, __ = resp.redirect_chain[-1]
|
||||
self.assertTrue(
|
||||
target_url.endswith(reverse('about_course', args=[self.toy.id]))
|
||||
target_url.endswith(reverse('about_course', args=[self.toy.id.to_deprecated_string()]))
|
||||
)
|
||||
|
||||
@patch.dict("django.conf.settings.FEATURES", {'ALLOW_WIKI_ROOT_ACCESS': True})
|
||||
|
||||
@@ -33,12 +33,12 @@ def user_is_article_course_staff(user, article):
|
||||
# course numbered '202_' or '202' and so we need to consider both.
|
||||
|
||||
courses = modulestore.django.modulestore().get_courses_for_wiki(wiki_slug)
|
||||
if any(courseware.access.has_access(user, course, 'staff', course.course_id) for course in courses):
|
||||
if any(courseware.access.has_access(user, 'staff', course, course.course_key) for course in courses):
|
||||
return True
|
||||
|
||||
if (wiki_slug.endswith('_') and slug_is_numerical(wiki_slug[:-1])):
|
||||
courses = modulestore.django.modulestore().get_courses_for_wiki(wiki_slug[:-1])
|
||||
if any(courseware.access.has_access(user, course, 'staff', course.course_id) for course in courses):
|
||||
if any(courseware.access.has_access(user, 'staff', course, course.course_key) for course in courses):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@@ -16,6 +16,7 @@ from wiki.models import URLPath, Article
|
||||
|
||||
from courseware.courses import get_course_by_id
|
||||
from course_wiki.utils import course_wiki_slug
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -35,7 +36,7 @@ def course_wiki_redirect(request, course_id): # pylint: disable=W0613
|
||||
as it's home page. A course's wiki must be an article on the root (for
|
||||
example, "/6.002x") to keep things simple.
|
||||
"""
|
||||
course = get_course_by_id(course_id)
|
||||
course = get_course_by_id(SlashSeparatedCourseKey.from_deprecated_string(course_id))
|
||||
course_slug = course_wiki_slug(course)
|
||||
|
||||
valid_slug = True
|
||||
|
||||
@@ -17,7 +17,7 @@ from django.utils.translation import ugettext as _
|
||||
import mongoengine
|
||||
|
||||
from dashboard.models import CourseImportLog
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.keys import CourseKey
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -222,19 +222,16 @@ def add_repo(repo, rdir_in, branch=None):
|
||||
logger.setLevel(logging.NOTSET)
|
||||
logger.removeHandler(import_log_handler)
|
||||
|
||||
course_id = 'unknown'
|
||||
course_key = None
|
||||
location = 'unknown'
|
||||
|
||||
# extract course ID from output of import-command-run and make symlink
|
||||
# this is needed in order for custom course scripts to work
|
||||
match = re.search('(?ms)===> IMPORTING course to location (\S+)',
|
||||
ret_import)
|
||||
match = re.search(r'(?ms)===> IMPORTING course (\S+)', ret_import)
|
||||
if match:
|
||||
location = Location(match.group(1))
|
||||
log.debug('location = {0}'.format(location))
|
||||
course_id = location.course_id
|
||||
|
||||
cdir = '{0}/{1}'.format(GIT_REPO_DIR, location.course)
|
||||
course_id = match.group(1)
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
cdir = '{0}/{1}'.format(GIT_REPO_DIR, course_key.course)
|
||||
log.debug('Studio course dir = {0}'.format(cdir))
|
||||
|
||||
if os.path.exists(cdir) and not os.path.islink(cdir):
|
||||
@@ -267,8 +264,8 @@ def add_repo(repo, rdir_in, branch=None):
|
||||
log.exception('Unable to connect to mongodb to save log, please '
|
||||
'check MONGODB_LOG settings')
|
||||
cil = CourseImportLog(
|
||||
course_id=course_id,
|
||||
location=unicode(location),
|
||||
course_id=course_key,
|
||||
location=location,
|
||||
repo_dir=rdir,
|
||||
created=timezone.now(),
|
||||
import_log=ret_import,
|
||||
|
||||
@@ -2,19 +2,13 @@
|
||||
Script for importing courseware from git/xml into a mongo modulestore
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import StringIO
|
||||
import subprocess
|
||||
import logging
|
||||
|
||||
from django.core import management
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
import dashboard.git_import
|
||||
from dashboard.git_import import GitImportError
|
||||
from dashboard.models import CourseImportLog
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.xml import XMLModuleStore
|
||||
|
||||
|
||||
@@ -17,10 +17,12 @@ from django.test.utils import override_settings
|
||||
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
from xmodule.modulestore.store_utilities import delete_course
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
import dashboard.git_import as git_import
|
||||
from dashboard.git_import import GitImportError
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
|
||||
TEST_MONGODB_LOG = {
|
||||
'host': 'localhost',
|
||||
@@ -45,7 +47,7 @@ class TestGitAddCourse(ModuleStoreTestCase):
|
||||
TEST_REPO = 'https://github.com/mitocw/edx4edx_lite.git'
|
||||
TEST_COURSE = 'MITx/edx4edx/edx4edx'
|
||||
TEST_BRANCH = 'testing_do_not_delete'
|
||||
TEST_BRANCH_COURSE = 'MITx/edx4edx_branch/edx4edx'
|
||||
TEST_BRANCH_COURSE = SlashSeparatedCourseKey('MITx', 'edx4edx_branch', 'edx4edx')
|
||||
GIT_REPO_DIR = getattr(settings, 'GIT_REPO_DIR')
|
||||
|
||||
def assertCommandFailureRegexp(self, regex, *args):
|
||||
@@ -162,14 +164,14 @@ class TestGitAddCourse(ModuleStoreTestCase):
|
||||
|
||||
# Delete to test branching back to master
|
||||
delete_course(def_ms, contentstore(),
|
||||
def_ms.get_course(self.TEST_BRANCH_COURSE).location,
|
||||
self.TEST_BRANCH_COURSE,
|
||||
True)
|
||||
self.assertIsNone(def_ms.get_course(self.TEST_BRANCH_COURSE))
|
||||
git_import.add_repo(self.TEST_REPO,
|
||||
repo_dir / 'edx4edx_lite',
|
||||
'master')
|
||||
self.assertIsNone(def_ms.get_course(self.TEST_BRANCH_COURSE))
|
||||
self.assertIsNotNone(def_ms.get_course(self.TEST_COURSE))
|
||||
self.assertIsNotNone(def_ms.get_course(SlashSeparatedCourseKey.from_deprecated_string(self.TEST_COURSE)))
|
||||
|
||||
def test_branch_exceptions(self):
|
||||
"""
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
"""Models for dashboard application"""
|
||||
|
||||
import mongoengine
|
||||
from xmodule.modulestore.mongoengine_fields import CourseKeyField
|
||||
|
||||
|
||||
class CourseImportLog(mongoengine.Document):
|
||||
"""Mongoengine model for git log"""
|
||||
# pylint: disable=R0924
|
||||
|
||||
course_id = mongoengine.StringField(max_length=128)
|
||||
course_id = CourseKeyField(max_length=128)
|
||||
# NOTE: this location is not a Location object but a pathname
|
||||
location = mongoengine.StringField(max_length=168)
|
||||
import_log = mongoengine.StringField(max_length=20 * 65535)
|
||||
git_log = mongoengine.StringField(max_length=65535)
|
||||
|
||||
@@ -42,6 +42,7 @@ from xmodule.modulestore import XML_MODULESTORE_TYPE
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.store_utilities import delete_course
|
||||
from xmodule.modulestore.xml import XMLModuleStore
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -78,7 +79,7 @@ class SysadminDashboardView(TemplateView):
|
||||
""" Get an iterable list of courses."""
|
||||
|
||||
courses = self.def_ms.get_courses()
|
||||
courses = dict([c.id, c] for c in courses) # no course directory
|
||||
courses = dict([c.id.to_deprecated_string(), c] for c in courses) # no course directory
|
||||
|
||||
return courses
|
||||
|
||||
@@ -258,7 +259,7 @@ class Users(SysadminDashboardView):
|
||||
self.msg += u'<ol>'
|
||||
for (cdir, course) in courses.items():
|
||||
self.msg += u'<li>{0} ({1})</li>'.format(
|
||||
escape(cdir), course.location.url())
|
||||
escape(cdir), course.location.to_deprecated_string())
|
||||
self.msg += u'</ol>'
|
||||
|
||||
def get(self, request):
|
||||
@@ -469,7 +470,7 @@ class Courses(SysadminDashboardView):
|
||||
course = self.def_ms.courses[os.path.abspath(gdir)]
|
||||
msg += _('Loaded course {0} {1}<br/>Errors:').format(
|
||||
cdir, course.display_name)
|
||||
errors = self.def_ms.get_item_errors(course.location)
|
||||
errors = self.def_ms.get_course_errors(course.id)
|
||||
if not errors:
|
||||
msg += u'None'
|
||||
else:
|
||||
@@ -489,9 +490,7 @@ class Courses(SysadminDashboardView):
|
||||
courses = self.get_courses()
|
||||
|
||||
for (cdir, course) in courses.items():
|
||||
gdir = cdir
|
||||
if '/' in cdir:
|
||||
gdir = cdir.rsplit('/', 1)[1]
|
||||
gdir = cdir.run
|
||||
data.append([course.display_name, cdir]
|
||||
+ self.git_info_for_course(gdir))
|
||||
|
||||
@@ -535,13 +534,14 @@ class Courses(SysadminDashboardView):
|
||||
|
||||
elif action == 'del_course':
|
||||
course_id = request.POST.get('course_id', '').strip()
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
course_found = False
|
||||
if course_id in courses:
|
||||
if course_key in courses:
|
||||
course_found = True
|
||||
course = courses[course_id]
|
||||
course = courses[course_key]
|
||||
else:
|
||||
try:
|
||||
course = get_course_by_id(course_id)
|
||||
course = get_course_by_id(course_key)
|
||||
course_found = True
|
||||
except Exception, err: # pylint: disable=broad-except
|
||||
self.msg += _('Error - cannot get course with ID '
|
||||
@@ -549,7 +549,7 @@ class Courses(SysadminDashboardView):
|
||||
course_id, escape(str(err))
|
||||
)
|
||||
|
||||
is_xml_course = (modulestore().get_modulestore_type(course_id) == XML_MODULESTORE_TYPE)
|
||||
is_xml_course = (modulestore().get_modulestore_type(course_key) == XML_MODULESTORE_TYPE)
|
||||
if course_found and is_xml_course:
|
||||
cdir = course.data_dir
|
||||
self.def_ms.courses.pop(cdir)
|
||||
@@ -567,14 +567,13 @@ class Courses(SysadminDashboardView):
|
||||
|
||||
elif course_found and not is_xml_course:
|
||||
# delete course that is stored with mongodb backend
|
||||
loc = course.location
|
||||
content_store = contentstore()
|
||||
commit = True
|
||||
delete_course(self.def_ms, content_store, loc, commit)
|
||||
delete_course(self.def_ms, content_store, course.id, commit)
|
||||
# don't delete user permission groups, though
|
||||
self.msg += \
|
||||
u"<font color='red'>{0} {1} = {2} ({3})</font>".format(
|
||||
_('Deleted'), loc, course.id, course.display_name)
|
||||
u"<font color='red'>{0} {1} ({2})</font>".format(
|
||||
_('Deleted'), course.id.to_deprecated_string(), course.display_name)
|
||||
datatable = self.make_datatable()
|
||||
|
||||
context = {
|
||||
@@ -606,9 +605,9 @@ class Staffing(SysadminDashboardView):
|
||||
datum = [course.display_name, course.id]
|
||||
datum += [CourseEnrollment.objects.filter(
|
||||
course_id=course.id).count()]
|
||||
datum += [CourseStaffRole(course.location).users_with_role().count()]
|
||||
datum += [CourseStaffRole(course.id).users_with_role().count()]
|
||||
datum += [','.join([x.username for x in CourseInstructorRole(
|
||||
course.location).users_with_role()])]
|
||||
course.id).users_with_role()])]
|
||||
data.append(datum)
|
||||
|
||||
datatable = dict(header=[_('Course Name'), _('course_id'),
|
||||
@@ -640,7 +639,7 @@ class Staffing(SysadminDashboardView):
|
||||
|
||||
for (cdir, course) in courses.items(): # pylint: disable=unused-variable
|
||||
for role in roles:
|
||||
for user in role(course.location).users_with_role():
|
||||
for user in role(course.id).users_with_role():
|
||||
datum = [course.id, role, user.username, user.email,
|
||||
user.profile.name]
|
||||
data.append(datum)
|
||||
@@ -667,6 +666,8 @@ class GitLogs(TemplateView):
|
||||
"""Shows logs of imports that happened as a result of a git import"""
|
||||
|
||||
course_id = kwargs.get('course_id')
|
||||
if course_id:
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
|
||||
# Set mongodb defaults even if it isn't defined in settings
|
||||
mongo_db = {
|
||||
@@ -709,16 +710,15 @@ class GitLogs(TemplateView):
|
||||
|
||||
# Allow only course team, instructors, and staff
|
||||
if not (request.user.is_staff or
|
||||
CourseInstructorRole(course.location).has_user(request.user) or
|
||||
CourseStaffRole(course.location).has_user(request.user)):
|
||||
CourseInstructorRole(course.id).has_user(request.user) or
|
||||
CourseStaffRole(course.id).has_user(request.user)):
|
||||
raise Http404
|
||||
log.debug('course_id={0}'.format(course_id))
|
||||
cilset = CourseImportLog.objects.filter(
|
||||
course_id=course_id).order_by('-created')
|
||||
cilset = CourseImportLog.objects.filter(course_id=course_id).order_by('-created')
|
||||
log.debug('cilset length={0}'.format(len(cilset)))
|
||||
mdb.disconnect()
|
||||
context = {'cilset': cilset,
|
||||
'course_id': course_id,
|
||||
'course_id': course_id.to_deprecated_string() if course_id else None,
|
||||
'error_msg': error_msg}
|
||||
|
||||
return render_to_response(self.template_name, context)
|
||||
|
||||
@@ -13,7 +13,6 @@ from django.contrib.auth.models import User
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test.client import Client
|
||||
from django.test.utils import override_settings
|
||||
from django.utils.html import escape
|
||||
from django.utils.translation import ugettext as _
|
||||
import mongoengine
|
||||
|
||||
@@ -27,6 +26,7 @@ from student.tests.factories import UserFactory
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.xml import XMLModuleStore
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
|
||||
|
||||
TEST_MONGODB_LOG = {
|
||||
@@ -47,7 +47,7 @@ class SysadminBaseTestCase(ModuleStoreTestCase):
|
||||
|
||||
TEST_REPO = 'https://github.com/mitocw/edx4edx_lite.git'
|
||||
TEST_BRANCH = 'testing_do_not_delete'
|
||||
TEST_BRANCH_COURSE = 'MITx/edx4edx_branch/edx4edx'
|
||||
TEST_BRANCH_COURSE = SlashSeparatedCourseKey('MITx', 'edx4edx_branch', 'edx4edx')
|
||||
|
||||
def setUp(self):
|
||||
"""Setup test case by adding primary user."""
|
||||
@@ -79,12 +79,16 @@ class SysadminBaseTestCase(ModuleStoreTestCase):
|
||||
course = def_ms.courses.get(course_path, None)
|
||||
except AttributeError:
|
||||
# Using mongo store
|
||||
course = def_ms.get_course('MITx/edx4edx/edx4edx')
|
||||
course = def_ms.get_course(SlashSeparatedCourseKey('MITx', 'edx4edx', 'edx4edx'))
|
||||
|
||||
# Delete git loaded course
|
||||
response = self.client.post(reverse('sysadmin_courses'),
|
||||
{'course_id': course.id,
|
||||
'action': 'del_course', })
|
||||
response = self.client.post(
|
||||
reverse('sysadmin_courses'),
|
||||
{
|
||||
'course_id': course.id.to_deprecated_string(),
|
||||
'action': 'del_course',
|
||||
}
|
||||
)
|
||||
self.addCleanup(self._rm_glob, '{0}_deleted_*'.format(course_path))
|
||||
|
||||
return response
|
||||
@@ -321,7 +325,7 @@ class TestSysadmin(SysadminBaseTestCase):
|
||||
course = def_ms.courses.get('{0}/edx4edx_lite'.format(
|
||||
os.path.abspath(settings.DATA_DIR)), None)
|
||||
self.assertIsNotNone(course)
|
||||
self.assertIn(self.TEST_BRANCH_COURSE, course.location.course_id)
|
||||
self.assertEqual(self.TEST_BRANCH_COURSE, course.id)
|
||||
self._rm_edx4edx()
|
||||
|
||||
# Try and delete a non-existent course
|
||||
@@ -363,8 +367,8 @@ class TestSysadmin(SysadminBaseTestCase):
|
||||
self._add_edx4edx()
|
||||
|
||||
def_ms = modulestore()
|
||||
course = def_ms.get_course('MITx/edx4edx/edx4edx')
|
||||
CourseStaffRole(course.location).add_users(self.user)
|
||||
course = def_ms.get_course(SlashSeparatedCourseKey('MITx', 'edx4edx', 'edx4edx'))
|
||||
CourseStaffRole(course.id).add_users(self.user)
|
||||
|
||||
response = self.client.post(reverse('sysadmin_staffing'),
|
||||
{'action': 'get_staff_csv', })
|
||||
@@ -447,11 +451,11 @@ class TestSysAdminMongoCourseImport(SysadminBaseTestCase):
|
||||
self.assertFalse(isinstance(def_ms, XMLModuleStore))
|
||||
|
||||
self._add_edx4edx()
|
||||
course = def_ms.get_course('MITx/edx4edx/edx4edx')
|
||||
course = def_ms.get_course(SlashSeparatedCourseKey('MITx', 'edx4edx', 'edx4edx'))
|
||||
self.assertIsNotNone(course)
|
||||
|
||||
self._rm_edx4edx()
|
||||
course = def_ms.get_course('MITx/edx4edx/edx4edx')
|
||||
course = def_ms.get_course(SlashSeparatedCourseKey('MITx', 'edx4edx', 'edx4edx'))
|
||||
self.assertIsNone(course)
|
||||
|
||||
def test_gitlogs(self):
|
||||
@@ -472,7 +476,7 @@ class TestSysAdminMongoCourseImport(SysadminBaseTestCase):
|
||||
reverse('gitlogs_detail', kwargs={
|
||||
'course_id': 'MITx/edx4edx/edx4edx'}))
|
||||
|
||||
self.assertIn('======> IMPORTING course to location',
|
||||
self.assertIn('======> IMPORTING course',
|
||||
response.content)
|
||||
|
||||
self._rm_edx4edx()
|
||||
@@ -505,23 +509,25 @@ class TestSysAdminMongoCourseImport(SysadminBaseTestCase):
|
||||
self.assertEqual(response.status_code, 404)
|
||||
# Or specific logs
|
||||
response = self.client.get(reverse('gitlogs_detail', kwargs={
|
||||
'course_id': 'MITx/edx4edx/edx4edx'}))
|
||||
'course_id': 'MITx/edx4edx/edx4edx'
|
||||
}))
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
# Add user as staff in course team
|
||||
def_ms = modulestore()
|
||||
course = def_ms.get_course('MITx/edx4edx/edx4edx')
|
||||
CourseStaffRole(course.location).add_users(self.user)
|
||||
course = def_ms.get_course(SlashSeparatedCourseKey('MITx', 'edx4edx', 'edx4edx'))
|
||||
CourseStaffRole(course.id).add_users(self.user)
|
||||
|
||||
self.assertTrue(CourseStaffRole(course.location).has_user(self.user))
|
||||
self.assertTrue(CourseStaffRole(course.id).has_user(self.user))
|
||||
logged_in = self.client.login(username=self.user.username,
|
||||
password='foo')
|
||||
self.assertTrue(logged_in)
|
||||
|
||||
response = self.client.get(
|
||||
reverse('gitlogs_detail', kwargs={
|
||||
'course_id': 'MITx/edx4edx/edx4edx'}))
|
||||
self.assertIn('======> IMPORTING course to location',
|
||||
'course_id': 'MITx/edx4edx/edx4edx'
|
||||
}))
|
||||
self.assertIn('======> IMPORTING course',
|
||||
response.content)
|
||||
|
||||
self._rm_edx4edx()
|
||||
|
||||
@@ -42,7 +42,7 @@ class Command(BaseCommand):
|
||||
for course_id, course_modules in xml_module_store.modules.iteritems():
|
||||
course_path = course_id.replace('/', '_')
|
||||
for location, descriptor in course_modules.iteritems():
|
||||
location_path = location.url().replace('/', '_')
|
||||
location_path = location.to_deprecated_string().replace('/', '_')
|
||||
data = {}
|
||||
for field_name, field in descriptor.fields.iteritems():
|
||||
try:
|
||||
|
||||
@@ -47,7 +47,7 @@ class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin):
|
||||
display_name='Robot Super Course')
|
||||
self.course_id = self.course.id
|
||||
# seed the forums permissions and roles
|
||||
call_command('seed_permissions_roles', self.course_id)
|
||||
call_command('seed_permissions_roles', self.course_id.to_deprecated_string())
|
||||
|
||||
# Patch the comment client user save method so it does not try
|
||||
# to create a new cc user when creating a django user
|
||||
@@ -106,7 +106,7 @@ class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin):
|
||||
"title": ["Hello"]
|
||||
}
|
||||
url = reverse('create_thread', kwargs={'commentable_id': 'i4x-MITx-999-course-Robot_Super_Course',
|
||||
'course_id': self.course_id})
|
||||
'course_id': self.course_id.to_deprecated_string()})
|
||||
response = self.client.post(url, data=thread)
|
||||
assert_true(mock_request.called)
|
||||
mock_request.assert_called_with(
|
||||
@@ -117,7 +117,8 @@ class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin):
|
||||
'anonymous_to_peers': False, 'user_id': 1,
|
||||
'title': u'Hello',
|
||||
'commentable_id': u'i4x-MITx-999-course-Robot_Super_Course',
|
||||
'anonymous': False, 'course_id': u'MITx/999/Robot_Super_Course',
|
||||
'anonymous': False,
|
||||
'course_id': u'MITx/999/Robot_Super_Course',
|
||||
},
|
||||
params={'request_id': ANY},
|
||||
headers=ANY,
|
||||
@@ -134,7 +135,7 @@ class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin):
|
||||
request = RequestFactory().post("dummy_url", {"id": test_comment_id})
|
||||
request.user = self.student
|
||||
request.view_name = "delete_comment"
|
||||
response = views.delete_comment(request, course_id=self.course.id, comment_id=test_comment_id)
|
||||
response = views.delete_comment(request, course_id=self.course.id.to_deprecated_string(), comment_id=test_comment_id)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue(mock_request.called)
|
||||
@@ -172,7 +173,7 @@ class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin):
|
||||
def test_create_thread_no_title(self, mock_request):
|
||||
self._test_request_error(
|
||||
"create_thread",
|
||||
{"commentable_id": "dummy", "course_id": self.course_id},
|
||||
{"commentable_id": "dummy", "course_id": self.course_id.to_deprecated_string()},
|
||||
{"body": "foo"},
|
||||
mock_request
|
||||
)
|
||||
@@ -180,7 +181,7 @@ class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin):
|
||||
def test_create_thread_empty_title(self, mock_request):
|
||||
self._test_request_error(
|
||||
"create_thread",
|
||||
{"commentable_id": "dummy", "course_id": self.course_id},
|
||||
{"commentable_id": "dummy", "course_id": self.course_id.to_deprecated_string()},
|
||||
{"body": "foo", "title": " "},
|
||||
mock_request
|
||||
)
|
||||
@@ -188,7 +189,7 @@ class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin):
|
||||
def test_create_thread_no_body(self, mock_request):
|
||||
self._test_request_error(
|
||||
"create_thread",
|
||||
{"commentable_id": "dummy", "course_id": self.course_id},
|
||||
{"commentable_id": "dummy", "course_id": self.course_id.to_deprecated_string()},
|
||||
{"title": "foo"},
|
||||
mock_request
|
||||
)
|
||||
@@ -196,7 +197,7 @@ class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin):
|
||||
def test_create_thread_empty_body(self, mock_request):
|
||||
self._test_request_error(
|
||||
"create_thread",
|
||||
{"commentable_id": "dummy", "course_id": self.course_id},
|
||||
{"commentable_id": "dummy", "course_id": self.course_id.to_deprecated_string()},
|
||||
{"body": " ", "title": "foo"},
|
||||
mock_request
|
||||
)
|
||||
@@ -204,7 +205,7 @@ class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin):
|
||||
def test_update_thread_no_title(self, mock_request):
|
||||
self._test_request_error(
|
||||
"update_thread",
|
||||
{"thread_id": "dummy", "course_id": self.course_id},
|
||||
{"thread_id": "dummy", "course_id": self.course_id.to_deprecated_string()},
|
||||
{"body": "foo"},
|
||||
mock_request
|
||||
)
|
||||
@@ -212,7 +213,7 @@ class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin):
|
||||
def test_update_thread_empty_title(self, mock_request):
|
||||
self._test_request_error(
|
||||
"update_thread",
|
||||
{"thread_id": "dummy", "course_id": self.course_id},
|
||||
{"thread_id": "dummy", "course_id": self.course_id.to_deprecated_string()},
|
||||
{"body": "foo", "title": " "},
|
||||
mock_request
|
||||
)
|
||||
@@ -220,7 +221,7 @@ class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin):
|
||||
def test_update_thread_no_body(self, mock_request):
|
||||
self._test_request_error(
|
||||
"update_thread",
|
||||
{"thread_id": "dummy", "course_id": self.course_id},
|
||||
{"thread_id": "dummy", "course_id": self.course_id.to_deprecated_string()},
|
||||
{"title": "foo"},
|
||||
mock_request
|
||||
)
|
||||
@@ -228,7 +229,7 @@ class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin):
|
||||
def test_update_thread_empty_body(self, mock_request):
|
||||
self._test_request_error(
|
||||
"update_thread",
|
||||
{"thread_id": "dummy", "course_id": self.course_id},
|
||||
{"thread_id": "dummy", "course_id": self.course_id.to_deprecated_string()},
|
||||
{"body": " ", "title": "foo"},
|
||||
mock_request
|
||||
)
|
||||
@@ -236,7 +237,7 @@ class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin):
|
||||
def test_create_comment_no_body(self, mock_request):
|
||||
self._test_request_error(
|
||||
"create_comment",
|
||||
{"thread_id": "dummy", "course_id": self.course_id},
|
||||
{"thread_id": "dummy", "course_id": self.course_id.to_deprecated_string()},
|
||||
{},
|
||||
mock_request
|
||||
)
|
||||
@@ -244,7 +245,7 @@ class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin):
|
||||
def test_create_comment_empty_body(self, mock_request):
|
||||
self._test_request_error(
|
||||
"create_comment",
|
||||
{"thread_id": "dummy", "course_id": self.course_id},
|
||||
{"thread_id": "dummy", "course_id": self.course_id.to_deprecated_string()},
|
||||
{"body": " "},
|
||||
mock_request
|
||||
)
|
||||
@@ -252,7 +253,7 @@ class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin):
|
||||
def test_create_sub_comment_no_body(self, mock_request):
|
||||
self._test_request_error(
|
||||
"create_sub_comment",
|
||||
{"comment_id": "dummy", "course_id": self.course_id},
|
||||
{"comment_id": "dummy", "course_id": self.course_id.to_deprecated_string()},
|
||||
{},
|
||||
mock_request
|
||||
)
|
||||
@@ -260,7 +261,7 @@ class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin):
|
||||
def test_create_sub_comment_empty_body(self, mock_request):
|
||||
self._test_request_error(
|
||||
"create_sub_comment",
|
||||
{"comment_id": "dummy", "course_id": self.course_id},
|
||||
{"comment_id": "dummy", "course_id": self.course_id.to_deprecated_string()},
|
||||
{"body": " "},
|
||||
mock_request
|
||||
)
|
||||
@@ -268,7 +269,7 @@ class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin):
|
||||
def test_update_comment_no_body(self, mock_request):
|
||||
self._test_request_error(
|
||||
"update_comment",
|
||||
{"comment_id": "dummy", "course_id": self.course_id},
|
||||
{"comment_id": "dummy", "course_id": self.course_id.to_deprecated_string()},
|
||||
{},
|
||||
mock_request
|
||||
)
|
||||
@@ -276,7 +277,7 @@ class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin):
|
||||
def test_update_comment_empty_body(self, mock_request):
|
||||
self._test_request_error(
|
||||
"update_comment",
|
||||
{"comment_id": "dummy", "course_id": self.course_id},
|
||||
{"comment_id": "dummy", "course_id": self.course_id.to_deprecated_string()},
|
||||
{"body": " "},
|
||||
mock_request
|
||||
)
|
||||
@@ -289,7 +290,7 @@ class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin):
|
||||
response = self.client.post(
|
||||
reverse(
|
||||
"update_comment",
|
||||
kwargs={"course_id": self.course_id, "comment_id": comment_id}
|
||||
kwargs={"course_id": self.course_id.to_deprecated_string(), "comment_id": comment_id}
|
||||
),
|
||||
data={"body": updated_body}
|
||||
)
|
||||
@@ -334,7 +335,7 @@ class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin):
|
||||
"read": False,
|
||||
"comments_count": 0,
|
||||
})
|
||||
url = reverse('flag_abuse_for_thread', kwargs={'thread_id': '518d4237b023791dca00000d', 'course_id': self.course_id})
|
||||
url = reverse('flag_abuse_for_thread', kwargs={'thread_id': '518d4237b023791dca00000d', 'course_id': self.course_id.to_deprecated_string()})
|
||||
response = self.client.post(url)
|
||||
assert_true(mock_request.called)
|
||||
|
||||
@@ -403,7 +404,7 @@ class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin):
|
||||
"read": False,
|
||||
"comments_count": 0
|
||||
})
|
||||
url = reverse('un_flag_abuse_for_thread', kwargs={'thread_id': '518d4237b023791dca00000d', 'course_id': self.course_id})
|
||||
url = reverse('un_flag_abuse_for_thread', kwargs={'thread_id': '518d4237b023791dca00000d', 'course_id': self.course_id.to_deprecated_string()})
|
||||
response = self.client.post(url)
|
||||
assert_true(mock_request.called)
|
||||
|
||||
@@ -466,7 +467,7 @@ class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin):
|
||||
"type": "comment",
|
||||
"endorsed": False
|
||||
})
|
||||
url = reverse('flag_abuse_for_comment', kwargs={'comment_id': '518d4237b023791dca00000d', 'course_id': self.course_id})
|
||||
url = reverse('flag_abuse_for_comment', kwargs={'comment_id': '518d4237b023791dca00000d', 'course_id': self.course_id.to_deprecated_string()})
|
||||
response = self.client.post(url)
|
||||
assert_true(mock_request.called)
|
||||
|
||||
@@ -529,7 +530,7 @@ class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSetupMixin):
|
||||
"type": "comment",
|
||||
"endorsed": False
|
||||
})
|
||||
url = reverse('un_flag_abuse_for_comment', kwargs={'comment_id': '518d4237b023791dca00000d', 'course_id': self.course_id})
|
||||
url = reverse('un_flag_abuse_for_comment', kwargs={'comment_id': '518d4237b023791dca00000d', 'course_id': self.course_id.to_deprecated_string()})
|
||||
response = self.client.post(url)
|
||||
assert_true(mock_request.called)
|
||||
|
||||
@@ -586,7 +587,7 @@ class ViewPermissionsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSet
|
||||
self._set_mock_request_data(mock_request, {})
|
||||
self.client.login(username=self.student.username, password=self.password)
|
||||
response = self.client.post(
|
||||
reverse("pin_thread", kwargs={"course_id": self.course.id, "thread_id": "dummy"})
|
||||
reverse("pin_thread", kwargs={"course_id": self.course.id.to_deprecated_string(), "thread_id": "dummy"})
|
||||
)
|
||||
self.assertEqual(response.status_code, 401)
|
||||
|
||||
@@ -594,7 +595,7 @@ class ViewPermissionsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSet
|
||||
self._set_mock_request_data(mock_request, {})
|
||||
self.client.login(username=self.moderator.username, password=self.password)
|
||||
response = self.client.post(
|
||||
reverse("pin_thread", kwargs={"course_id": self.course.id, "thread_id": "dummy"})
|
||||
reverse("pin_thread", kwargs={"course_id": self.course.id.to_deprecated_string(), "thread_id": "dummy"})
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@@ -602,7 +603,7 @@ class ViewPermissionsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSet
|
||||
self._set_mock_request_data(mock_request, {})
|
||||
self.client.login(username=self.student.username, password=self.password)
|
||||
response = self.client.post(
|
||||
reverse("un_pin_thread", kwargs={"course_id": self.course.id, "thread_id": "dummy"})
|
||||
reverse("un_pin_thread", kwargs={"course_id": self.course.id.to_deprecated_string(), "thread_id": "dummy"})
|
||||
)
|
||||
self.assertEqual(response.status_code, 401)
|
||||
|
||||
@@ -610,7 +611,7 @@ class ViewPermissionsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSet
|
||||
self._set_mock_request_data(mock_request, {})
|
||||
self.client.login(username=self.moderator.username, password=self.password)
|
||||
response = self.client.post(
|
||||
reverse("un_pin_thread", kwargs={"course_id": self.course.id, "thread_id": "dummy"})
|
||||
reverse("un_pin_thread", kwargs={"course_id": self.course.id.to_deprecated_string(), "thread_id": "dummy"})
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@@ -629,7 +630,7 @@ class CreateThreadUnicodeTestCase(ModuleStoreTestCase, UnicodeTestMixin, MockReq
|
||||
request = RequestFactory().post("dummy_url", {"body": text, "title": text})
|
||||
request.user = self.student
|
||||
request.view_name = "create_thread"
|
||||
response = views.create_thread(request, course_id=self.course.id, commentable_id="test_commentable")
|
||||
response = views.create_thread(request, course_id=self.course.id.to_deprecated_string(), commentable_id="test_commentable")
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue(mock_request.called)
|
||||
@@ -654,7 +655,7 @@ class UpdateThreadUnicodeTestCase(ModuleStoreTestCase, UnicodeTestMixin, MockReq
|
||||
request = RequestFactory().post("dummy_url", {"body": text, "title": text})
|
||||
request.user = self.student
|
||||
request.view_name = "update_thread"
|
||||
response = views.update_thread(request, course_id=self.course.id, thread_id="dummy_thread_id")
|
||||
response = views.update_thread(request, course_id=self.course.id.to_deprecated_string(), thread_id="dummy_thread_id")
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue(mock_request.called)
|
||||
@@ -678,7 +679,7 @@ class CreateCommentUnicodeTestCase(ModuleStoreTestCase, UnicodeTestMixin, MockRe
|
||||
request = RequestFactory().post("dummy_url", {"body": text})
|
||||
request.user = self.student
|
||||
request.view_name = "create_comment"
|
||||
response = views.create_comment(request, course_id=self.course.id, thread_id="dummy_thread_id")
|
||||
response = views.create_comment(request, course_id=self.course.id.to_deprecated_string(), thread_id="dummy_thread_id")
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue(mock_request.called)
|
||||
@@ -702,7 +703,7 @@ class UpdateCommentUnicodeTestCase(ModuleStoreTestCase, UnicodeTestMixin, MockRe
|
||||
request = RequestFactory().post("dummy_url", {"body": text})
|
||||
request.user = self.student
|
||||
request.view_name = "update_comment"
|
||||
response = views.update_comment(request, course_id=self.course.id, comment_id="dummy_comment_id")
|
||||
response = views.update_comment(request, course_id=self.course.id.to_deprecated_string(), comment_id="dummy_comment_id")
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue(mock_request.called)
|
||||
@@ -726,7 +727,7 @@ class CreateSubCommentUnicodeTestCase(ModuleStoreTestCase, UnicodeTestMixin, Moc
|
||||
request = RequestFactory().post("dummy_url", {"body": text})
|
||||
request.user = self.student
|
||||
request.view_name = "create_sub_comment"
|
||||
response = views.create_sub_comment(request, course_id=self.course.id, comment_id="dummy_comment_id")
|
||||
response = views.create_sub_comment(request, course_id=self.course.id.to_deprecated_string(), comment_id="dummy_comment_id")
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue(mock_request.called)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import time
|
||||
import random
|
||||
import os
|
||||
import os.path
|
||||
import logging
|
||||
import urlparse
|
||||
@@ -13,12 +12,11 @@ import django_comment_client.settings as cc_settings
|
||||
|
||||
from django.core import exceptions
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.views.decorators.http import require_POST, require_GET
|
||||
from django.views.decorators.http import require_POST
|
||||
from django.views.decorators import csrf
|
||||
from django.core.files.storage import get_storage_class
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from edxmako.shortcuts import render_to_string
|
||||
from courseware.courses import get_course_with_access, get_course_by_id
|
||||
from course_groups.cohorts import get_cohort_id, is_commentable_cohorted
|
||||
|
||||
@@ -26,6 +24,8 @@ from django_comment_client.utils import JsonResponse, JsonError, extract, add_co
|
||||
|
||||
from django_comment_client.permissions import check_permissions_by_view, cached_has_permission
|
||||
from courseware.access import has_access
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
from xmodule.modulestore.keys import CourseKey
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -41,7 +41,8 @@ def permitted(fn):
|
||||
else:
|
||||
content = None
|
||||
return content
|
||||
if check_permissions_by_view(request.user, kwargs['course_id'], fetch_content(), request.view_name):
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(kwargs['course_id'])
|
||||
if check_permissions_by_view(request.user, course_key, fetch_content(), request.view_name):
|
||||
return fn(request, *args, **kwargs)
|
||||
else:
|
||||
return JsonError("unauthorized", status=401)
|
||||
@@ -49,10 +50,6 @@ def permitted(fn):
|
||||
|
||||
|
||||
def ajax_content_response(request, course_id, content):
|
||||
context = {
|
||||
'course_id': course_id,
|
||||
'content': content,
|
||||
}
|
||||
user_info = cc.User.from_django_user(request.user).to_dict()
|
||||
annotated_content_info = utils.get_annotated_content_info(course_id, content, request.user, user_info)
|
||||
return JsonResponse({
|
||||
@@ -70,7 +67,8 @@ def create_thread(request, course_id, commentable_id):
|
||||
"""
|
||||
|
||||
log.debug("Creating new thread in %r, id %r", course_id, commentable_id)
|
||||
course = get_course_with_access(request.user, course_id, 'load')
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
course = get_course_with_access(request.user, 'load', course_id)
|
||||
post = request.POST
|
||||
|
||||
if course.allow_anonymous:
|
||||
@@ -93,7 +91,7 @@ def create_thread(request, course_id, commentable_id):
|
||||
'anonymous': anonymous,
|
||||
'anonymous_to_peers': anonymous_to_peers,
|
||||
'commentable_id': commentable_id,
|
||||
'course_id': course_id,
|
||||
'course_id': course_id.to_deprecated_string(),
|
||||
'user_id': request.user.id,
|
||||
})
|
||||
|
||||
@@ -152,23 +150,24 @@ def update_thread(request, course_id, thread_id):
|
||||
thread.update_attributes(**extract(request.POST, ['body', 'title']))
|
||||
thread.save()
|
||||
if request.is_ajax():
|
||||
return ajax_content_response(request, course_id, thread.to_dict())
|
||||
return ajax_content_response(request, SlashSeparatedCourseKey.from_deprecated_string(course_id), thread.to_dict())
|
||||
else:
|
||||
return JsonResponse(utils.safe_content(thread.to_dict()))
|
||||
|
||||
|
||||
def _create_comment(request, course_id, thread_id=None, parent_id=None):
|
||||
def _create_comment(request, course_key, thread_id=None, parent_id=None):
|
||||
"""
|
||||
given a course_id, thread_id, and parent_id, create a comment,
|
||||
called from create_comment to do the actual creation
|
||||
"""
|
||||
assert isinstance(course_key, CourseKey)
|
||||
post = request.POST
|
||||
|
||||
if 'body' not in post or not post['body'].strip():
|
||||
return JsonError(_("Body can't be empty"))
|
||||
comment = cc.Comment(**extract(post, ['body']))
|
||||
|
||||
course = get_course_with_access(request.user, course_id, 'load')
|
||||
course = get_course_with_access(request.user, 'load', course_key)
|
||||
if course.allow_anonymous:
|
||||
anonymous = post.get('anonymous', 'false').lower() == 'true'
|
||||
else:
|
||||
@@ -183,7 +182,7 @@ def _create_comment(request, course_id, thread_id=None, parent_id=None):
|
||||
'anonymous': anonymous,
|
||||
'anonymous_to_peers': anonymous_to_peers,
|
||||
'user_id': request.user.id,
|
||||
'course_id': course_id,
|
||||
'course_id': course_key,
|
||||
'thread_id': thread_id,
|
||||
'parent_id': parent_id,
|
||||
})
|
||||
@@ -192,7 +191,7 @@ def _create_comment(request, course_id, thread_id=None, parent_id=None):
|
||||
user = cc.User.from_django_user(request.user)
|
||||
user.follow(comment.thread)
|
||||
if request.is_ajax():
|
||||
return ajax_content_response(request, course_id, comment.to_dict())
|
||||
return ajax_content_response(request, course_key, comment.to_dict())
|
||||
else:
|
||||
return JsonResponse(utils.safe_content(comment.to_dict()))
|
||||
|
||||
@@ -208,7 +207,7 @@ def create_comment(request, course_id, thread_id):
|
||||
if cc_settings.MAX_COMMENT_DEPTH is not None:
|
||||
if cc_settings.MAX_COMMENT_DEPTH < 0:
|
||||
return JsonError(_("Comment level too deep"))
|
||||
return _create_comment(request, course_id, thread_id=thread_id)
|
||||
return _create_comment(request, SlashSeparatedCourseKey.from_deprecated_string(course_id), thread_id=thread_id)
|
||||
|
||||
|
||||
@require_POST
|
||||
@@ -238,7 +237,7 @@ def update_comment(request, course_id, comment_id):
|
||||
comment.update_attributes(**extract(request.POST, ['body']))
|
||||
comment.save()
|
||||
if request.is_ajax():
|
||||
return ajax_content_response(request, course_id, comment.to_dict())
|
||||
return ajax_content_response(request, SlashSeparatedCourseKey.from_deprecated_string(course_id), comment.to_dict())
|
||||
else:
|
||||
return JsonResponse(utils.safe_content(comment.to_dict()))
|
||||
|
||||
@@ -271,7 +270,7 @@ def openclose_thread(request, course_id, thread_id):
|
||||
thread = thread.to_dict()
|
||||
return JsonResponse({
|
||||
'content': utils.safe_content(thread),
|
||||
'ability': utils.get_ability(course_id, thread, request.user),
|
||||
'ability': utils.get_ability(SlashSeparatedCourseKey.from_deprecated_string(course_id), thread, request.user),
|
||||
})
|
||||
|
||||
|
||||
@@ -286,7 +285,7 @@ def create_sub_comment(request, course_id, comment_id):
|
||||
if cc_settings.MAX_COMMENT_DEPTH is not None:
|
||||
if cc_settings.MAX_COMMENT_DEPTH <= cc.Comment.find(comment_id).depth:
|
||||
return JsonError(_("Comment level too deep"))
|
||||
return _create_comment(request, course_id, parent_id=comment_id)
|
||||
return _create_comment(request, SlashSeparatedCourseKey.from_deprecated_string(course_id), parent_id=comment_id)
|
||||
|
||||
|
||||
@require_POST
|
||||
@@ -366,10 +365,11 @@ def un_flag_abuse_for_thread(request, course_id, thread_id):
|
||||
ajax only
|
||||
"""
|
||||
user = cc.User.from_django_user(request.user)
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
course = get_course_by_id(course_id)
|
||||
thread = cc.Thread.find(thread_id)
|
||||
removeAll = cached_has_permission(request.user, 'openclose_thread', course_id) or has_access(request.user, course, 'staff')
|
||||
thread.unFlagAbuse(user, thread, removeAll)
|
||||
remove_all = cached_has_permission(request.user, 'openclose_thread', course_id) or has_access(request.user, 'staff', course)
|
||||
thread.unFlagAbuse(user, thread, remove_all)
|
||||
return JsonResponse(utils.safe_content(thread.to_dict()))
|
||||
|
||||
|
||||
@@ -396,10 +396,11 @@ def un_flag_abuse_for_comment(request, course_id, comment_id):
|
||||
ajax only
|
||||
"""
|
||||
user = cc.User.from_django_user(request.user)
|
||||
course = get_course_by_id(course_id)
|
||||
removeAll = cached_has_permission(request.user, 'openclose_thread', course_id) or has_access(request.user, course, 'staff')
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
course = get_course_by_id(course_key)
|
||||
remove_all = cached_has_permission(request.user, 'openclose_thread', course_key) or has_access(request.user, 'staff', course)
|
||||
comment = cc.Comment.find(comment_id)
|
||||
comment.unFlagAbuse(user, comment, removeAll)
|
||||
comment.unFlagAbuse(user, comment, remove_all)
|
||||
return JsonResponse(utils.safe_content(comment.to_dict()))
|
||||
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ class ViewsExceptionTestCase(UrlResetMixin, ModuleStoreTestCase):
|
||||
mock_from_django_user.return_value = Mock()
|
||||
|
||||
url = reverse('django_comment_client.forum.views.user_profile',
|
||||
kwargs={'course_id': self.course.id, 'user_id': '12345'}) # There is no user 12345
|
||||
kwargs={'course_id': self.course.id.to_deprecated_string(), 'user_id': '12345'}) # There is no user 12345
|
||||
self.response = self.client.get(url)
|
||||
self.assertEqual(self.response.status_code, 404)
|
||||
|
||||
@@ -81,7 +81,7 @@ class ViewsExceptionTestCase(UrlResetMixin, ModuleStoreTestCase):
|
||||
mock_from_django_user.return_value = Mock()
|
||||
|
||||
url = reverse('django_comment_client.forum.views.followed_threads',
|
||||
kwargs={'course_id': self.course.id, 'user_id': '12345'}) # There is no user 12345
|
||||
kwargs={'course_id': self.course.id.to_deprecated_string(), 'user_id': '12345'}) # There is no user 12345
|
||||
self.response = self.client.get(url)
|
||||
self.assertEqual(self.response.status_code, 404)
|
||||
|
||||
@@ -173,7 +173,7 @@ class SingleThreadTestCase(ModuleStoreTestCase):
|
||||
request.user = self.student
|
||||
response = views.single_thread(
|
||||
request,
|
||||
self.course.id,
|
||||
self.course.id.to_deprecated_string(),
|
||||
"dummy_discussion_id",
|
||||
"test_thread_id"
|
||||
)
|
||||
@@ -208,7 +208,7 @@ class SingleThreadTestCase(ModuleStoreTestCase):
|
||||
request.user = self.student
|
||||
response = views.single_thread(
|
||||
request,
|
||||
self.course.id,
|
||||
self.course.id.to_deprecated_string(),
|
||||
"dummy_discussion_id",
|
||||
"test_thread_id"
|
||||
)
|
||||
@@ -237,7 +237,7 @@ class SingleThreadTestCase(ModuleStoreTestCase):
|
||||
request = RequestFactory().post("dummy_url")
|
||||
response = views.single_thread(
|
||||
request,
|
||||
self.course.id,
|
||||
self.course.id.to_deprecated_string(),
|
||||
"dummy_discussion_id",
|
||||
"dummy_thread_id"
|
||||
)
|
||||
@@ -252,7 +252,7 @@ class SingleThreadTestCase(ModuleStoreTestCase):
|
||||
Http404,
|
||||
views.single_thread,
|
||||
request,
|
||||
self.course.id,
|
||||
self.course.id.to_deprecated_string(),
|
||||
"test_discussion_id",
|
||||
"test_thread_id"
|
||||
)
|
||||
@@ -277,7 +277,7 @@ class UserProfileTestCase(ModuleStoreTestCase):
|
||||
request.user = self.student
|
||||
response = views.user_profile(
|
||||
request,
|
||||
self.course.id,
|
||||
self.course.id.to_deprecated_string(),
|
||||
self.profiled_user.id
|
||||
)
|
||||
mock_request.assert_any_call(
|
||||
@@ -342,7 +342,7 @@ class UserProfileTestCase(ModuleStoreTestCase):
|
||||
with self.assertRaises(Http404):
|
||||
response = views.user_profile(
|
||||
request,
|
||||
self.course.id,
|
||||
self.course.id.to_deprecated_string(),
|
||||
-999
|
||||
)
|
||||
|
||||
@@ -362,7 +362,7 @@ class UserProfileTestCase(ModuleStoreTestCase):
|
||||
request.user = self.student
|
||||
response = views.user_profile(
|
||||
request,
|
||||
self.course.id,
|
||||
self.course.id.to_deprecated_string(),
|
||||
self.profiled_user.id
|
||||
)
|
||||
self.assertEqual(response.status_code, 405)
|
||||
@@ -406,7 +406,7 @@ class CommentsServiceRequestHeadersTestCase(UrlResetMixin, ModuleStoreTestCase):
|
||||
reverse(
|
||||
"django_comment_client.forum.views.single_thread",
|
||||
kwargs={
|
||||
"course_id": self.course.id,
|
||||
"course_id": self.course.id.to_deprecated_string(),
|
||||
"discussion_id": "dummy",
|
||||
"thread_id": thread_id,
|
||||
}
|
||||
@@ -422,7 +422,7 @@ class CommentsServiceRequestHeadersTestCase(UrlResetMixin, ModuleStoreTestCase):
|
||||
self.client.get(
|
||||
reverse(
|
||||
"django_comment_client.forum.views.forum_form_discussion",
|
||||
kwargs={"course_id": self.course.id}
|
||||
kwargs={"course_id": self.course.id.to_deprecated_string()}
|
||||
),
|
||||
)
|
||||
self.assert_all_calls_have_header(mock_request, "X-Edx-Api-Key", "test_api_key")
|
||||
@@ -441,7 +441,7 @@ class InlineDiscussionUnicodeTestCase(ModuleStoreTestCase, UnicodeTestMixin):
|
||||
request = RequestFactory().get("dummy_url")
|
||||
request.user = self.student
|
||||
|
||||
response = views.inline_discussion(request, self.course.id, "dummy_discussion_id")
|
||||
response = views.inline_discussion(request, self.course.id.to_deprecated_string(), "dummy_discussion_id")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response_data = json.loads(response.content)
|
||||
self.assertEqual(response_data["discussion_data"][0]["title"], text)
|
||||
@@ -462,7 +462,7 @@ class ForumFormDiscussionUnicodeTestCase(ModuleStoreTestCase, UnicodeTestMixin):
|
||||
request.user = self.student
|
||||
request.META["HTTP_X_REQUESTED_WITH"] = "XMLHttpRequest" # so request.is_ajax() == True
|
||||
|
||||
response = views.forum_form_discussion(request, self.course.id)
|
||||
response = views.forum_form_discussion(request, self.course.id.to_deprecated_string())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response_data = json.loads(response.content)
|
||||
self.assertEqual(response_data["discussion_data"][0]["title"], text)
|
||||
@@ -484,7 +484,7 @@ class SingleThreadUnicodeTestCase(ModuleStoreTestCase, UnicodeTestMixin):
|
||||
request.user = self.student
|
||||
request.META["HTTP_X_REQUESTED_WITH"] = "XMLHttpRequest" # so request.is_ajax() == True
|
||||
|
||||
response = views.single_thread(request, self.course.id, "dummy_discussion_id", thread_id)
|
||||
response = views.single_thread(request, self.course.id.to_deprecated_string(), "dummy_discussion_id", thread_id)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response_data = json.loads(response.content)
|
||||
self.assertEqual(response_data["content"]["title"], text)
|
||||
@@ -505,7 +505,7 @@ class UserProfileUnicodeTestCase(ModuleStoreTestCase, UnicodeTestMixin):
|
||||
request.user = self.student
|
||||
request.META["HTTP_X_REQUESTED_WITH"] = "XMLHttpRequest" # so request.is_ajax() == True
|
||||
|
||||
response = views.user_profile(request, self.course.id, str(self.student.id))
|
||||
response = views.user_profile(request, self.course.id.to_deprecated_string(), str(self.student.id))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response_data = json.loads(response.content)
|
||||
self.assertEqual(response_data["discussion_data"][0]["title"], text)
|
||||
@@ -526,7 +526,7 @@ class FollowedThreadsUnicodeTestCase(ModuleStoreTestCase, UnicodeTestMixin):
|
||||
request.user = self.student
|
||||
request.META["HTTP_X_REQUESTED_WITH"] = "XMLHttpRequest" # so request.is_ajax() == True
|
||||
|
||||
response = views.followed_threads(request, self.course.id, str(self.student.id))
|
||||
response = views.followed_threads(request, self.course.id.to_deprecated_string(), str(self.student.id))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response_data = json.loads(response.content)
|
||||
self.assertEqual(response_data["discussion_data"][0]["title"], text)
|
||||
|
||||
@@ -21,6 +21,8 @@ from django_comment_client.utils import (merge_dict, extract, strip_none, add_co
|
||||
import django_comment_client.utils as utils
|
||||
import lms.lib.comment_client as cc
|
||||
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
|
||||
THREADS_PER_PAGE = 20
|
||||
INLINE_THREADS_PER_PAGE = 20
|
||||
PAGES_NEARBY_DELTA = 2
|
||||
@@ -41,7 +43,7 @@ def get_threads(request, course_id, discussion_id=None, per_page=THREADS_PER_PAG
|
||||
'sort_order': 'desc',
|
||||
'text': '',
|
||||
'commentable_id': discussion_id,
|
||||
'course_id': course_id,
|
||||
'course_id': course_id.to_deprecated_string(),
|
||||
'user_id': request.user.id,
|
||||
}
|
||||
|
||||
@@ -111,8 +113,9 @@ def inline_discussion(request, course_id, discussion_id):
|
||||
Renders JSON for DiscussionModules
|
||||
"""
|
||||
nr_transaction = newrelic.agent.current_transaction()
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
|
||||
course = get_course_with_access(request.user, course_id, 'load_forum')
|
||||
course = get_course_with_access(request.user, 'load_forum', course_id)
|
||||
|
||||
threads, query_params = get_threads(request, course_id, discussion_id, per_page=INLINE_THREADS_PER_PAGE)
|
||||
cc_user = cc.User.from_django_user(request.user)
|
||||
@@ -166,9 +169,10 @@ def forum_form_discussion(request, course_id):
|
||||
"""
|
||||
Renders the main Discussion page, potentially filtered by a search query
|
||||
"""
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
nr_transaction = newrelic.agent.current_transaction()
|
||||
|
||||
course = get_course_with_access(request.user, course_id, 'load_forum')
|
||||
course = get_course_with_access(request.user, 'load_forum', course_id)
|
||||
with newrelic.agent.FunctionTrace(nr_transaction, "get_discussion_category_map"):
|
||||
category_map = utils.get_discussion_category_map(course)
|
||||
|
||||
@@ -206,13 +210,13 @@ def forum_form_discussion(request, course_id):
|
||||
'csrf': csrf(request)['csrf_token'],
|
||||
'course': course,
|
||||
#'recent_active_threads': recent_active_threads,
|
||||
'staff_access': has_access(request.user, course, 'staff'),
|
||||
'staff_access': has_access(request.user, 'staff', course),
|
||||
'threads': saxutils.escape(json.dumps(threads), escapedict),
|
||||
'thread_pages': query_params['num_pages'],
|
||||
'user_info': saxutils.escape(json.dumps(user_info), escapedict),
|
||||
'flag_moderator': cached_has_permission(request.user, 'openclose_thread', course.id) or has_access(request.user, course, 'staff'),
|
||||
'flag_moderator': cached_has_permission(request.user, 'openclose_thread', course.id) or has_access(request.user, 'staff', course),
|
||||
'annotated_content_info': saxutils.escape(json.dumps(annotated_content_info), escapedict),
|
||||
'course_id': course.id,
|
||||
'course_id': course.id.to_deprecated_string(),
|
||||
'category_map': category_map,
|
||||
'roles': saxutils.escape(json.dumps(utils.get_role_ids(course_id)), escapedict),
|
||||
'is_moderator': cached_has_permission(request.user, "see_all_cohorts", course_id),
|
||||
@@ -228,9 +232,10 @@ def forum_form_discussion(request, course_id):
|
||||
@require_GET
|
||||
@login_required
|
||||
def single_thread(request, course_id, discussion_id, thread_id):
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
nr_transaction = newrelic.agent.current_transaction()
|
||||
|
||||
course = get_course_with_access(request.user, course_id, 'load_forum')
|
||||
course = get_course_with_access(request.user, 'load_forum', course_id)
|
||||
cc_user = cc.User.from_django_user(request.user)
|
||||
user_info = cc_user.to_dict()
|
||||
|
||||
@@ -267,7 +272,7 @@ def single_thread(request, course_id, discussion_id, thread_id):
|
||||
threads, query_params = get_threads(request, course_id)
|
||||
threads.append(thread.to_dict())
|
||||
|
||||
course = get_course_with_access(request.user, course_id, 'load_forum')
|
||||
course = get_course_with_access(request.user, 'load_forum', course_id)
|
||||
|
||||
with newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"):
|
||||
add_courseware_context(threads, course)
|
||||
@@ -298,7 +303,7 @@ def single_thread(request, course_id, discussion_id, thread_id):
|
||||
'annotated_content_info': saxutils.escape(json.dumps(annotated_content_info), escapedict),
|
||||
'course': course,
|
||||
#'recent_active_threads': recent_active_threads,
|
||||
'course_id': course.id, # TODO: Why pass both course and course.id to template?
|
||||
'course_id': course.id.to_deprecated_string(), # TODO: Why pass both course and course.id to template?
|
||||
'thread_id': thread_id,
|
||||
'threads': saxutils.escape(json.dumps(threads), escapedict),
|
||||
'category_map': category_map,
|
||||
@@ -306,7 +311,7 @@ def single_thread(request, course_id, discussion_id, thread_id):
|
||||
'thread_pages': query_params['num_pages'],
|
||||
'is_course_cohorted': is_course_cohorted(course_id),
|
||||
'is_moderator': cached_has_permission(request.user, "see_all_cohorts", course_id),
|
||||
'flag_moderator': cached_has_permission(request.user, 'openclose_thread', course.id) or has_access(request.user, course, 'staff'),
|
||||
'flag_moderator': cached_has_permission(request.user, 'openclose_thread', course.id) or has_access(request.user, 'staff', course),
|
||||
'cohorts': cohorts,
|
||||
'user_cohort': get_cohort_id(request.user, course_id),
|
||||
'cohorted_commentables': cohorted_commentables
|
||||
@@ -317,10 +322,11 @@ def single_thread(request, course_id, discussion_id, thread_id):
|
||||
@require_GET
|
||||
@login_required
|
||||
def user_profile(request, course_id, user_id):
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
nr_transaction = newrelic.agent.current_transaction()
|
||||
|
||||
#TODO: Allow sorting?
|
||||
course = get_course_with_access(request.user, course_id, 'load_forum')
|
||||
course = get_course_with_access(request.user, 'load_forum', course_id)
|
||||
try:
|
||||
profiled_user = cc.User(id=user_id, course_id=course_id)
|
||||
|
||||
@@ -365,9 +371,10 @@ def user_profile(request, course_id, user_id):
|
||||
|
||||
@login_required
|
||||
def followed_threads(request, course_id, user_id):
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
nr_transaction = newrelic.agent.current_transaction()
|
||||
|
||||
course = get_course_with_access(request.user, course_id, 'load_forum')
|
||||
course = get_course_with_access(request.user, 'load_forum', course_id)
|
||||
try:
|
||||
profiled_user = cc.User(id=user_id, course_id=course_id)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django_comment_common.utils import seed_permissions_roles
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@@ -11,6 +12,6 @@ class Command(BaseCommand):
|
||||
raise CommandError("Please provide a course id")
|
||||
if len(args) > 1:
|
||||
raise CommandError("Too many arguments")
|
||||
course_id = args[0]
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(args[0])
|
||||
|
||||
seed_permissions_roles(course_id)
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import django_comment_common.models as models
|
||||
from django.test import TestCase
|
||||
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
|
||||
|
||||
class RoleClassTestCase(TestCase):
|
||||
def setUp(self):
|
||||
# For course ID, syntax edx/classname/classdate is important
|
||||
# because xmodel.course_module.id_to_location looks for a string to split
|
||||
|
||||
self.course_id = "edX/toy/2012_Fall"
|
||||
self.course_id = SlashSeparatedCourseKey("edX", "toy", "2012_Fall")
|
||||
self.student_role = models.Role.objects.get_or_create(name="Student",
|
||||
course_id=self.course_id)[0]
|
||||
self.student_role.add_permission("delete_thread")
|
||||
@@ -15,7 +17,7 @@ class RoleClassTestCase(TestCase):
|
||||
course_id=self.course_id)[0]
|
||||
self.TA_role = models.Role.objects.get_or_create(name="Community TA",
|
||||
course_id=self.course_id)[0]
|
||||
self.course_id_2 = "edx/6.002x/2012_Fall"
|
||||
self.course_id_2 = SlashSeparatedCourseKey("edx", "6.002x", "2012_Fall")
|
||||
self.TA_role_2 = models.Role.objects.get_or_create(name="Community TA",
|
||||
course_id=self.course_id_2)[0]
|
||||
|
||||
|
||||
@@ -41,9 +41,11 @@ class DictionaryTestCase(TestCase):
|
||||
self.assertEqual(utils.merge_dict(d1, d2), expected)
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
|
||||
class AccessUtilsTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.course_id = 'edX/toy/2012_Fall'
|
||||
self.course = CourseFactory.create()
|
||||
self.course_id = self.course.id
|
||||
self.student_role = RoleFactory(name='Student', course_id=self.course_id)
|
||||
self.moderator_role = RoleFactory(name='Moderator', course_id=self.course_id)
|
||||
self.community_ta_role = RoleFactory(name='Community TA', course_id=self.course_id)
|
||||
@@ -121,8 +123,8 @@ class CoursewareContextTestCase(ModuleStoreTestCase):
|
||||
reverse(
|
||||
"jump_to",
|
||||
kwargs={
|
||||
"course_id": self.course.location.course_id,
|
||||
"location": discussion.location
|
||||
"course_id": self.course.id.to_deprecated_string(),
|
||||
"location": discussion.location.to_deprecated_string()
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
@@ -15,8 +15,9 @@ from edxmako import lookup_template
|
||||
import pystache_custom as pystache
|
||||
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore import Location
|
||||
from django.utils.timezone import UTC
|
||||
from xmodule.modulestore.locations import i4xEncoder, SlashSeparatedCourseKey
|
||||
import json
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -55,10 +56,7 @@ def has_forum_access(uname, course_id, rolename):
|
||||
|
||||
|
||||
def _get_discussion_modules(course):
|
||||
all_modules = modulestore().get_items(
|
||||
Location('i4x', course.location.org, course.location.course, 'discussion', None),
|
||||
course_id=course.id
|
||||
)
|
||||
all_modules = modulestore().get_items(course.id, category='discussion')
|
||||
|
||||
def has_required_keys(module):
|
||||
for key in ('discussion_id', 'discussion_category', 'discussion_target'):
|
||||
@@ -198,7 +196,7 @@ def get_discussion_category_map(course):
|
||||
|
||||
class JsonResponse(HttpResponse):
|
||||
def __init__(self, data=None):
|
||||
content = simplejson.dumps(data)
|
||||
content = json.dumps(data, cls=i4xEncoder)
|
||||
super(JsonResponse, self).__init__(content,
|
||||
mimetype='application/json; charset=utf-8')
|
||||
|
||||
@@ -311,12 +309,16 @@ def render_mustache(template_name, dictionary, *args, **kwargs):
|
||||
|
||||
|
||||
def permalink(content):
|
||||
if isinstance(content['course_id'], SlashSeparatedCourseKey):
|
||||
course_id = content['course_id'].to_deprecated_string()
|
||||
else:
|
||||
course_id = content['course_id']
|
||||
if content['type'] == 'thread':
|
||||
return reverse('django_comment_client.forum.views.single_thread',
|
||||
args=[content['course_id'], content['commentable_id'], content['id']])
|
||||
args=[course_id, content['commentable_id'], content['id']])
|
||||
else:
|
||||
return reverse('django_comment_client.forum.views.single_thread',
|
||||
args=[content['course_id'], content['commentable_id'], content['thread_id']]) + '#' + content['id']
|
||||
args=[course_id, content['commentable_id'], content['thread_id']]) + '#' + content['id']
|
||||
|
||||
|
||||
def extend_content(content):
|
||||
@@ -344,10 +346,10 @@ def add_courseware_context(content_list, course):
|
||||
for content in content_list:
|
||||
commentable_id = content['commentable_id']
|
||||
if commentable_id in id_map:
|
||||
location = id_map[commentable_id]["location"].url()
|
||||
location = id_map[commentable_id]["location"].to_deprecated_string()
|
||||
title = id_map[commentable_id]["title"]
|
||||
|
||||
url = reverse('jump_to', kwargs={"course_id": course.location.course_id,
|
||||
url = reverse('jump_to', kwargs={"course_id": course.id.to_deprecated_string(),
|
||||
"location": location})
|
||||
|
||||
content.update({"courseware_url": url, "courseware_title": title})
|
||||
|
||||
@@ -8,11 +8,12 @@ from django.core.urlresolvers import reverse
|
||||
|
||||
from foldit.views import foldit_ops, verify_code
|
||||
from foldit.models import PuzzleComplete, Score
|
||||
from student.models import unique_id_for_user
|
||||
from student.tests.factories import CourseEnrollmentFactory, UserFactory
|
||||
from student.models import unique_id_for_user, CourseEnrollment
|
||||
from student.tests.factories import UserFactory
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from pytz import UTC
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -23,18 +24,14 @@ class FolditTestCase(TestCase):
|
||||
self.factory = RequestFactory()
|
||||
self.url = reverse('foldit_ops')
|
||||
|
||||
self.course_id = 'course/id/1'
|
||||
self.course_id2 = 'course/id/2'
|
||||
self.course_id = SlashSeparatedCourseKey('course', 'id', '1')
|
||||
self.course_id2 = SlashSeparatedCourseKey('course', 'id', '2')
|
||||
|
||||
self.user = UserFactory.create()
|
||||
self.user2 = UserFactory.create()
|
||||
|
||||
self.course_enrollment = CourseEnrollmentFactory.create(
|
||||
user=self.user, course_id=self.course_id
|
||||
)
|
||||
self.course_enrollment2 = CourseEnrollmentFactory.create(
|
||||
user=self.user2, course_id=self.course_id2
|
||||
)
|
||||
CourseEnrollment.enroll(self.user, self.course_id)
|
||||
CourseEnrollment.enroll(self.user2, self.course_id2)
|
||||
|
||||
now = datetime.now(UTC)
|
||||
self.tomorrow = now + timedelta(days=1)
|
||||
|
||||
@@ -31,7 +31,7 @@ def list_with_level(course, level):
|
||||
There could be other levels specific to the course.
|
||||
If there is no Group for that course-level, returns an empty list
|
||||
"""
|
||||
return ROLES[level](course.location).users_with_role()
|
||||
return ROLES[level](course.id).users_with_role()
|
||||
|
||||
|
||||
def allow_access(course, user, level):
|
||||
@@ -63,7 +63,7 @@ def _change_access(course, user, level, action):
|
||||
"""
|
||||
|
||||
try:
|
||||
role = ROLES[level](course.location)
|
||||
role = ROLES[level](course.id)
|
||||
except KeyError:
|
||||
raise ValueError("unrecognized level '{}'".format(level))
|
||||
|
||||
|
||||
@@ -86,7 +86,6 @@ def enroll_email(course_id, student_email, auto_enroll=False, email_students=Fal
|
||||
returns two EmailEnrollmentState's
|
||||
representing state before and after the action.
|
||||
"""
|
||||
|
||||
previous_state = EmailEnrollmentState(course_id, student_email)
|
||||
|
||||
if previous_state.user:
|
||||
@@ -121,7 +120,6 @@ def unenroll_email(course_id, student_email, email_students=False, email_params=
|
||||
returns two EmailEnrollmentState's
|
||||
representing state before and after the action.
|
||||
"""
|
||||
|
||||
previous_state = EmailEnrollmentState(course_id, student_email)
|
||||
|
||||
if previous_state.enrollment:
|
||||
@@ -200,7 +198,7 @@ def reset_student_attempts(course_id, student, module_state_key, delete_module=F
|
||||
module_to_reset = StudentModule.objects.get(
|
||||
student_id=student.id,
|
||||
course_id=course_id,
|
||||
module_state_key=module_state_key
|
||||
module_id=module_state_key
|
||||
)
|
||||
|
||||
if delete_module:
|
||||
@@ -237,13 +235,15 @@ def get_email_params(course, auto_enroll):
|
||||
'SITE_NAME',
|
||||
settings.SITE_NAME
|
||||
)
|
||||
# TODO: Use request.build_absolute_uri rather than 'https://{}{}'.format
|
||||
# and check with the Services team that this works well with microsites
|
||||
registration_url = u'https://{}{}'.format(
|
||||
stripped_site_name,
|
||||
reverse('student.views.register_user')
|
||||
)
|
||||
course_url = u'https://{}{}'.format(
|
||||
stripped_site_name,
|
||||
reverse('course_root', kwargs={'course_id': course.id})
|
||||
reverse('course_root', kwargs={'course_id': course.id.to_deprecated_string()})
|
||||
)
|
||||
|
||||
# We can't get the url to the course's About page if the marketing site is enabled.
|
||||
@@ -251,7 +251,7 @@ def get_email_params(course, auto_enroll):
|
||||
if not settings.FEATURES.get('ENABLE_MKTG_SITE', False):
|
||||
course_about_url = u'https://{}{}'.format(
|
||||
stripped_site_name,
|
||||
reverse('about_course', kwargs={'course_id': course.id})
|
||||
reverse('about_course', kwargs={'course_id': course.id.to_deprecated_string()})
|
||||
)
|
||||
|
||||
is_shib_course = uses_shib(course)
|
||||
|
||||
@@ -29,23 +29,23 @@ def make_populated_course(step): # pylint: disable=unused-argument
|
||||
number='888',
|
||||
display_name='Bulk Email Test Course'
|
||||
)
|
||||
world.bulk_email_course_id = 'edx/888/Bulk_Email_Test_Course'
|
||||
world.bulk_email_course_id = course.id
|
||||
|
||||
try:
|
||||
# See if we've defined the instructor & staff user yet
|
||||
world.bulk_email_instructor
|
||||
except AttributeError:
|
||||
# Make & register an instructor for the course
|
||||
world.bulk_email_instructor = InstructorFactory(course=course.location)
|
||||
world.bulk_email_instructor = InstructorFactory(course=world.bulk_email_course_id)
|
||||
world.enroll_user(world.bulk_email_instructor, world.bulk_email_course_id)
|
||||
|
||||
# Make & register a staff member
|
||||
world.bulk_email_staff = StaffFactory(course=course.location)
|
||||
world.bulk_email_staff = StaffFactory(course=course.id)
|
||||
world.enroll_user(world.bulk_email_staff, world.bulk_email_course_id)
|
||||
|
||||
# Make & register a student
|
||||
world.register_by_course_id(
|
||||
'edx/888/Bulk_Email_Test_Course',
|
||||
world.register_by_course_key(
|
||||
course.id,
|
||||
username='student',
|
||||
password='test',
|
||||
is_staff=False
|
||||
|
||||
@@ -43,12 +43,12 @@ def i_am_staff_or_instructor(step, role): # pylint: disable=unused-argument
|
||||
display_name='Test Course'
|
||||
)
|
||||
|
||||
world.course_id = 'edx/999/Test_Course'
|
||||
world.course_id = course.id
|
||||
world.role = 'instructor'
|
||||
# Log in as the an instructor or staff for the course
|
||||
if role == 'instructor':
|
||||
# Make & register an instructor for the course
|
||||
world.instructor = InstructorFactory(course=course.location)
|
||||
world.instructor = InstructorFactory(course=world.course_id)
|
||||
world.enroll_user(world.instructor, world.course_id)
|
||||
|
||||
world.log_in(
|
||||
@@ -61,7 +61,7 @@ def i_am_staff_or_instructor(step, role): # pylint: disable=unused-argument
|
||||
else:
|
||||
world.role = 'staff'
|
||||
# Make & register a staff member
|
||||
world.staff = StaffFactory(course=course.location)
|
||||
world.staff = StaffFactory(course=world.course_id)
|
||||
world.enroll_user(world.staff, world.course_id)
|
||||
|
||||
world.log_in(
|
||||
|
||||
@@ -19,8 +19,9 @@ from courseware.courses import get_course_with_access
|
||||
from courseware.models import XModuleUserStateSummaryField
|
||||
import courseware.module_render as module_render
|
||||
import courseware.model_data as model_data
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@@ -28,13 +29,14 @@ def hint_manager(request, course_id):
|
||||
"""
|
||||
The URL landing function for all calls to the hint manager, both POST and GET.
|
||||
"""
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
try:
|
||||
get_course_with_access(request.user, course_id, 'staff', depth=None)
|
||||
get_course_with_access(request.user, 'staff', course_key, depth=None)
|
||||
except Http404:
|
||||
out = 'Sorry, but students are not allowed to access the hint manager!'
|
||||
return HttpResponse(out)
|
||||
if request.method == 'GET':
|
||||
out = get_hints(request, course_id, 'mod_queue')
|
||||
out = get_hints(request, course_key, 'mod_queue')
|
||||
out.update({'error': ''})
|
||||
return render_to_response('instructor/hint_manager.html', out)
|
||||
field = request.POST['field']
|
||||
@@ -52,10 +54,10 @@ def hint_manager(request, course_id):
|
||||
}
|
||||
|
||||
# Do the operation requested, and collect any error messages.
|
||||
error_text = switch_dict[request.POST['op']](request, course_id, field)
|
||||
error_text = switch_dict[request.POST['op']](request, course_key, field)
|
||||
if error_text is None:
|
||||
error_text = ''
|
||||
render_dict = get_hints(request, course_id, field)
|
||||
render_dict = get_hints(request, course_key, field)
|
||||
render_dict.update({'error': error_text})
|
||||
rendered_html = render_to_string('instructor/hint_manager_inner.html', render_dict)
|
||||
return HttpResponse(json.dumps({'success': True, 'contents': rendered_html}))
|
||||
@@ -86,13 +88,13 @@ def get_hints(request, course_id, field):
|
||||
other_field = 'mod_queue'
|
||||
field_label = 'Approved Hints'
|
||||
other_field_label = 'Hints Awaiting Moderation'
|
||||
# The course_id is of the form school/number/classname.
|
||||
# We want to use the course_id to find all matching usage_id's.
|
||||
# To do this, just take the school/number part - leave off the classname.
|
||||
course_id_dict = Location.parse_course_id(course_id)
|
||||
chopped_id = u'{org}/{course}'.format(**course_id_dict)
|
||||
chopped_id = re.escape(chopped_id)
|
||||
all_hints = XModuleUserStateSummaryField.objects.filter(field_name=field, usage_id__regex=chopped_id)
|
||||
# FIXME: we need to figure out how to do this with opaque keys
|
||||
all_hints = XModuleUserStateSummaryField.objects.filter(
|
||||
field_name=field,
|
||||
usage_id__regex=re.escape(u'{0.org}/{0.course}'.format(course_id)),
|
||||
)
|
||||
# big_out_dict[problem id] = [[answer, {pk: [hint, votes]}], sorted by answer]
|
||||
# big_out_dict maps a problem id to a list of [answer, hints] pairs, sorted in order of answer.
|
||||
big_out_dict = {}
|
||||
@@ -101,8 +103,8 @@ def get_hints(request, course_id, field):
|
||||
id_to_name = {}
|
||||
|
||||
for hints_by_problem in all_hints:
|
||||
loc = Location(hints_by_problem.usage_id)
|
||||
name = location_to_problem_name(course_id, loc)
|
||||
hints_by_problem.usage_id = hints_by_problem.usage_id.map_into_course(course_id)
|
||||
name = location_to_problem_name(course_id, hints_by_problem.usage_id)
|
||||
if name is None:
|
||||
continue
|
||||
id_to_name[hints_by_problem.usage_id] = name
|
||||
@@ -138,9 +140,9 @@ def location_to_problem_name(course_id, loc):
|
||||
problem it wraps around. Return None if the hinter no longer exists.
|
||||
"""
|
||||
try:
|
||||
descriptor = modulestore().get_items(loc, course_id=course_id)[0]
|
||||
descriptor = modulestore().get_item(loc)
|
||||
return descriptor.get_children()[0].display_name
|
||||
except IndexError:
|
||||
except ItemNotFoundError:
|
||||
# Sometimes, the problem is no longer in the course. Just
|
||||
# don't include said problem.
|
||||
return None
|
||||
@@ -164,9 +166,10 @@ def delete_hints(request, course_id, field):
|
||||
if key == 'op' or key == 'field':
|
||||
continue
|
||||
problem_id, answer, pk = request.POST.getlist(key)
|
||||
problem_key = course_id.make_usage_key_from_deprecated_string(problem_id)
|
||||
# Can be optimized - sort the delete list by problem_id, and load each problem
|
||||
# from the database only once.
|
||||
this_problem = XModuleUserStateSummaryField.objects.get(field_name=field, usage_id=problem_id)
|
||||
this_problem = XModuleUserStateSummaryField.objects.get(field_name=field, usage_id=problem_key)
|
||||
problem_dict = json.loads(this_problem.value)
|
||||
del problem_dict[answer][pk]
|
||||
this_problem.value = json.dumps(problem_dict)
|
||||
@@ -191,7 +194,8 @@ def change_votes(request, course_id, field):
|
||||
if key == 'op' or key == 'field':
|
||||
continue
|
||||
problem_id, answer, pk, new_votes = request.POST.getlist(key)
|
||||
this_problem = XModuleUserStateSummaryField.objects.get(field_name=field, usage_id=problem_id)
|
||||
problem_key = course_id.make_usage_key_from_deprecated_string(problem_id)
|
||||
this_problem = XModuleUserStateSummaryField.objects.get(field_name=field, usage_id=problem_key)
|
||||
problem_dict = json.loads(this_problem.value)
|
||||
# problem_dict[answer][pk] points to a [hint_text, #votes] pair.
|
||||
problem_dict[answer][pk][1] = int(new_votes)
|
||||
@@ -210,23 +214,27 @@ def add_hint(request, course_id, field):
|
||||
"""
|
||||
|
||||
problem_id = request.POST['problem']
|
||||
problem_key = course_id.make_usage_key_from_deprecated_string(problem_id)
|
||||
answer = request.POST['answer']
|
||||
hint_text = request.POST['hint']
|
||||
|
||||
# Validate the answer. This requires initializing the xmodules, which
|
||||
# is annoying.
|
||||
loc = Location(problem_id)
|
||||
descriptors = modulestore().get_items(loc, course_id=course_id)
|
||||
try:
|
||||
descriptor = modulestore().get_item(problem_key)
|
||||
descriptors = [descriptor]
|
||||
except ItemNotFoundError:
|
||||
descriptors = []
|
||||
field_data_cache = model_data.FieldDataCache(descriptors, course_id, request.user)
|
||||
hinter_module = module_render.get_module(request.user, request, loc, field_data_cache, course_id)
|
||||
hinter_module = module_render.get_module(request.user, request, problem_key, field_data_cache, course_id)
|
||||
if not hinter_module.validate_answer(answer):
|
||||
# Invalid answer. Don't add it to the database, or else the
|
||||
# hinter will crash when we encounter it.
|
||||
return 'Error - the answer you specified is not properly formatted: ' + str(answer)
|
||||
|
||||
this_problem = XModuleUserStateSummaryField.objects.get(field_name=field, usage_id=problem_id)
|
||||
this_problem = XModuleUserStateSummaryField.objects.get(field_name=field, usage_id=problem_key)
|
||||
|
||||
hint_pk_entry = XModuleUserStateSummaryField.objects.get(field_name='hint_pk', usage_id=problem_id)
|
||||
hint_pk_entry = XModuleUserStateSummaryField.objects.get(field_name='hint_pk', usage_id=problem_key)
|
||||
this_pk = int(hint_pk_entry.value)
|
||||
hint_pk_entry.value = this_pk + 1
|
||||
hint_pk_entry.save()
|
||||
@@ -253,16 +261,17 @@ def approve(request, course_id, field):
|
||||
if key == 'op' or key == 'field':
|
||||
continue
|
||||
problem_id, answer, pk = request.POST.getlist(key)
|
||||
problem_key = course_id.make_usage_key_from_deprecated_string(problem_id)
|
||||
# Can be optimized - sort the delete list by problem_id, and load each problem
|
||||
# from the database only once.
|
||||
problem_in_mod = XModuleUserStateSummaryField.objects.get(field_name=field, usage_id=problem_id)
|
||||
problem_in_mod = XModuleUserStateSummaryField.objects.get(field_name=field, usage_id=problem_key)
|
||||
problem_dict = json.loads(problem_in_mod.value)
|
||||
hint_to_move = problem_dict[answer][pk]
|
||||
del problem_dict[answer][pk]
|
||||
problem_in_mod.value = json.dumps(problem_dict)
|
||||
problem_in_mod.save()
|
||||
|
||||
problem_in_hints = XModuleUserStateSummaryField.objects.get(field_name='hints', usage_id=problem_id)
|
||||
problem_in_hints = XModuleUserStateSummaryField.objects.get(field_name='hints', usage_id=problem_key)
|
||||
problem_dict = json.loads(problem_in_hints.value)
|
||||
if answer not in problem_dict:
|
||||
problem_dict[answer] = {}
|
||||
|
||||
@@ -6,6 +6,8 @@ from django.core.management.base import BaseCommand
|
||||
from optparse import make_option
|
||||
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.keys import UsageKey
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
from xmodule.open_ended_grading_classes.openendedchild import OpenEndedChild
|
||||
from xmodule.open_ended_grading_classes.open_ended_module import OpenEndedModule
|
||||
|
||||
@@ -37,8 +39,8 @@ class Command(BaseCommand):
|
||||
task_number = options['task_number']
|
||||
|
||||
if len(args) == 4:
|
||||
course_id = args[0]
|
||||
location = args[1]
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(args[0])
|
||||
location = course_id.make_usage_key_from_deprecated_string(args[1])
|
||||
students_ids = [line.strip() for line in open(args[2])]
|
||||
hostname = args[3]
|
||||
else:
|
||||
@@ -51,7 +53,7 @@ class Command(BaseCommand):
|
||||
print err
|
||||
return
|
||||
|
||||
descriptor = modulestore().get_instance(course.id, location, depth=0)
|
||||
descriptor = modulestore().get_item(location, depth=0)
|
||||
if descriptor is None:
|
||||
print "Location not found in course"
|
||||
return
|
||||
@@ -76,7 +78,7 @@ def post_submission_for_student(student, course, location, task_number, dry_run=
|
||||
request.host = hostname
|
||||
|
||||
try:
|
||||
module = get_module_for_student(student, course, location, request=request)
|
||||
module = get_module_for_student(student, location, request=request)
|
||||
if module is None:
|
||||
print " WARNING: No state found."
|
||||
return False
|
||||
|
||||
@@ -9,6 +9,8 @@ from optparse import make_option
|
||||
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.keys import UsageKey
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
from xmodule.open_ended_grading_classes.openendedchild import OpenEndedChild
|
||||
|
||||
from courseware.courses import get_course
|
||||
@@ -37,8 +39,8 @@ class Command(BaseCommand):
|
||||
task_number = options['task_number']
|
||||
|
||||
if len(args) == 2:
|
||||
course_id = args[0]
|
||||
location = args[1]
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(args[0])
|
||||
usage_key = UsageKey.from_string(args[1])
|
||||
else:
|
||||
print self.help
|
||||
return
|
||||
@@ -49,16 +51,16 @@ class Command(BaseCommand):
|
||||
print err
|
||||
return
|
||||
|
||||
descriptor = modulestore().get_instance(course.id, location, depth=0)
|
||||
descriptor = modulestore().get_item(usage_key, depth=0)
|
||||
if descriptor is None:
|
||||
print "Location {0} not found in course".format(location)
|
||||
print "Location {0} not found in course".format(usage_key)
|
||||
return
|
||||
|
||||
try:
|
||||
enrolled_students = CourseEnrollment.users_enrolled_in(course_id)
|
||||
print "Total students enrolled in {0}: {1}".format(course_id, enrolled_students.count())
|
||||
|
||||
calculate_task_statistics(enrolled_students, course, location, task_number)
|
||||
calculate_task_statistics(enrolled_students, course, usage_key, task_number)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print "\nOperation Cancelled"
|
||||
@@ -79,7 +81,7 @@ def calculate_task_statistics(students, course, location, task_number, write_to_
|
||||
students_with_graded_submissions = [] # pylint: disable=invalid-name
|
||||
students_with_no_state = []
|
||||
|
||||
student_modules = StudentModule.objects.filter(module_state_key=location, student__in=students).order_by('student')
|
||||
student_modules = StudentModule.objects.filter(module_id=location, student__in=students).order_by('student')
|
||||
print "Total student modules: {0}".format(student_modules.count())
|
||||
|
||||
for index, student_module in enumerate(student_modules):
|
||||
@@ -89,7 +91,7 @@ def calculate_task_statistics(students, course, location, task_number, write_to_
|
||||
student = student_module.student
|
||||
print "{0}:{1}".format(student.id, student.username)
|
||||
|
||||
module = get_module_for_student(student, course, location)
|
||||
module = get_module_for_student(student, location)
|
||||
if module is None:
|
||||
print " WARNING: No state found"
|
||||
students_with_no_state.append(student)
|
||||
@@ -113,8 +115,6 @@ def calculate_task_statistics(students, course, location, task_number, write_to_
|
||||
elif task_state == OpenEndedChild.POST_ASSESSMENT or task_state == OpenEndedChild.DONE:
|
||||
students_with_graded_submissions.append(student)
|
||||
|
||||
location = Location(location)
|
||||
|
||||
print "----------------------------------"
|
||||
print "Time: {0}".format(time.strftime("%Y %b %d %H:%M:%S +0000", time.gmtime()))
|
||||
print "Course: {0}".format(course.id)
|
||||
@@ -132,7 +132,7 @@ def calculate_task_statistics(students, course, location, task_number, write_to_
|
||||
with open('{0}.{1}.csv'.format(filename, time_stamp), 'wb') as csv_file:
|
||||
writer = csv.writer(csv_file, delimiter=' ', quoting=csv.QUOTE_MINIMAL)
|
||||
for student in students_with_ungraded_submissions:
|
||||
writer.writerow(("ungraded", student.id, anonymous_id_for_user(student, ''), student.username))
|
||||
writer.writerow(("ungraded", student.id, anonymous_id_for_user(student, None), student.username))
|
||||
for student in students_with_graded_submissions:
|
||||
writer.writerow(("graded", student.id, anonymous_id_for_user(student, ''), student.username))
|
||||
writer.writerow(("graded", student.id, anonymous_id_for_user(student, None), student.username))
|
||||
return stats
|
||||
|
||||
@@ -24,14 +24,16 @@ from instructor.management.commands.openended_post import post_submission_for_st
|
||||
from instructor.management.commands.openended_stats import calculate_task_statistics
|
||||
from instructor.utils import get_module_for_student
|
||||
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
|
||||
class OpenEndedPostTest(ModuleStoreTestCase):
|
||||
"""Test the openended_post management command."""
|
||||
|
||||
def setUp(self):
|
||||
self.course_id = "edX/open_ended/2012_Fall"
|
||||
self.problem_location = Location(["i4x", "edX", "open_ended", "combinedopenended", "SampleQuestion"])
|
||||
self.course_id = SlashSeparatedCourseKey("edX", "open_ended", "2012_Fall")
|
||||
self.problem_location = Location("edX", "open_ended", "2012_Fall", "combinedopenended", "SampleQuestion")
|
||||
self.self_assessment_task_number = 0
|
||||
self.open_ended_task_number = 1
|
||||
|
||||
@@ -41,7 +43,7 @@ class OpenEndedPostTest(ModuleStoreTestCase):
|
||||
|
||||
StudentModuleFactory.create(
|
||||
course_id=self.course_id,
|
||||
module_state_key=self.problem_location,
|
||||
module_id=self.problem_location,
|
||||
student=self.student_on_initial,
|
||||
grade=0,
|
||||
max_grade=1,
|
||||
@@ -50,7 +52,7 @@ class OpenEndedPostTest(ModuleStoreTestCase):
|
||||
|
||||
StudentModuleFactory.create(
|
||||
course_id=self.course_id,
|
||||
module_state_key=self.problem_location,
|
||||
module_id=self.problem_location,
|
||||
student=self.student_on_accessing,
|
||||
grade=0,
|
||||
max_grade=1,
|
||||
@@ -59,7 +61,7 @@ class OpenEndedPostTest(ModuleStoreTestCase):
|
||||
|
||||
StudentModuleFactory.create(
|
||||
course_id=self.course_id,
|
||||
module_state_key=self.problem_location,
|
||||
module_id=self.problem_location,
|
||||
student=self.student_on_post_assessment,
|
||||
grade=0,
|
||||
max_grade=1,
|
||||
@@ -67,7 +69,7 @@ class OpenEndedPostTest(ModuleStoreTestCase):
|
||||
)
|
||||
|
||||
def test_post_submission_for_student_on_initial(self):
|
||||
course = get_course_with_access(self.student_on_initial, self.course_id, 'load')
|
||||
course = get_course_with_access(self.student_on_initial, 'load', self.course_id)
|
||||
|
||||
dry_run_result = post_submission_for_student(self.student_on_initial, course, self.problem_location, self.open_ended_task_number, dry_run=True)
|
||||
self.assertFalse(dry_run_result)
|
||||
@@ -76,7 +78,7 @@ class OpenEndedPostTest(ModuleStoreTestCase):
|
||||
self.assertFalse(result)
|
||||
|
||||
def test_post_submission_for_student_on_accessing(self):
|
||||
course = get_course_with_access(self.student_on_accessing, self.course_id, 'load')
|
||||
course = get_course_with_access(self.student_on_accessing, 'load', self.course_id)
|
||||
|
||||
dry_run_result = post_submission_for_student(self.student_on_accessing, course, self.problem_location, self.open_ended_task_number, dry_run=True)
|
||||
self.assertFalse(dry_run_result)
|
||||
@@ -84,11 +86,11 @@ class OpenEndedPostTest(ModuleStoreTestCase):
|
||||
with patch('capa.xqueue_interface.XQueueInterface.send_to_queue') as mock_send_to_queue:
|
||||
mock_send_to_queue.return_value = (0, "Successfully queued")
|
||||
|
||||
module = get_module_for_student(self.student_on_accessing, course, self.problem_location)
|
||||
module = get_module_for_student(self.student_on_accessing, self.problem_location)
|
||||
task = module.child_module.get_task_number(self.open_ended_task_number)
|
||||
|
||||
student_response = "Here is an answer."
|
||||
student_anonymous_id = anonymous_id_for_user(self.student_on_accessing, '')
|
||||
student_anonymous_id = anonymous_id_for_user(self.student_on_accessing, None)
|
||||
submission_time = datetime.strftime(datetime.now(UTC), xqueue_interface.dateformat)
|
||||
|
||||
result = post_submission_for_student(self.student_on_accessing, course, self.problem_location, self.open_ended_task_number, dry_run=False)
|
||||
@@ -102,7 +104,7 @@ class OpenEndedPostTest(ModuleStoreTestCase):
|
||||
self.assertGreaterEqual(body_arg_student_info['submission_time'], submission_time)
|
||||
|
||||
def test_post_submission_for_student_on_post_assessment(self):
|
||||
course = get_course_with_access(self.student_on_post_assessment, self.course_id, 'load')
|
||||
course = get_course_with_access(self.student_on_post_assessment, 'load', self.course_id)
|
||||
|
||||
dry_run_result = post_submission_for_student(self.student_on_post_assessment, course, self.problem_location, self.open_ended_task_number, dry_run=True)
|
||||
self.assertFalse(dry_run_result)
|
||||
@@ -111,7 +113,7 @@ class OpenEndedPostTest(ModuleStoreTestCase):
|
||||
self.assertFalse(result)
|
||||
|
||||
def test_post_submission_for_student_invalid_task(self):
|
||||
course = get_course_with_access(self.student_on_accessing, self.course_id, 'load')
|
||||
course = get_course_with_access(self.student_on_accessing, 'load', self.course_id)
|
||||
|
||||
result = post_submission_for_student(self.student_on_accessing, course, self.problem_location, self.self_assessment_task_number, dry_run=False)
|
||||
self.assertFalse(result)
|
||||
@@ -126,8 +128,8 @@ class OpenEndedStatsTest(ModuleStoreTestCase):
|
||||
"""Test the openended_stats management command."""
|
||||
|
||||
def setUp(self):
|
||||
self.course_id = "edX/open_ended/2012_Fall"
|
||||
self.problem_location = Location(["i4x", "edX", "open_ended", "combinedopenended", "SampleQuestion"])
|
||||
self.course_id = SlashSeparatedCourseKey("edX", "open_ended", "2012_Fall")
|
||||
self.problem_location = Location("edX", "open_ended", "2012_Fall", "combinedopenended", "SampleQuestion")
|
||||
self.task_number = 1
|
||||
self.invalid_task_number = 3
|
||||
|
||||
@@ -137,7 +139,7 @@ class OpenEndedStatsTest(ModuleStoreTestCase):
|
||||
|
||||
StudentModuleFactory.create(
|
||||
course_id=self.course_id,
|
||||
module_state_key=self.problem_location,
|
||||
module_id=self.problem_location,
|
||||
student=self.student_on_initial,
|
||||
grade=0,
|
||||
max_grade=1,
|
||||
@@ -146,7 +148,7 @@ class OpenEndedStatsTest(ModuleStoreTestCase):
|
||||
|
||||
StudentModuleFactory.create(
|
||||
course_id=self.course_id,
|
||||
module_state_key=self.problem_location,
|
||||
module_id=self.problem_location,
|
||||
student=self.student_on_accessing,
|
||||
grade=0,
|
||||
max_grade=1,
|
||||
@@ -155,7 +157,7 @@ class OpenEndedStatsTest(ModuleStoreTestCase):
|
||||
|
||||
StudentModuleFactory.create(
|
||||
course_id=self.course_id,
|
||||
module_state_key=self.problem_location,
|
||||
module_id=self.problem_location,
|
||||
student=self.student_on_post_assessment,
|
||||
grade=0,
|
||||
max_grade=1,
|
||||
@@ -165,7 +167,7 @@ class OpenEndedStatsTest(ModuleStoreTestCase):
|
||||
self.students = [self.student_on_initial, self.student_on_accessing, self.student_on_post_assessment]
|
||||
|
||||
def test_calculate_task_statistics(self):
|
||||
course = get_course_with_access(self.student_on_accessing, self.course_id, 'load')
|
||||
course = get_course_with_access(self.student_on_accessing, 'load', self.course_id)
|
||||
stats = calculate_task_statistics(self.students, course, self.problem_location, self.task_number, write_to_file=False)
|
||||
self.assertEqual(stats[OpenEndedChild.INITIAL], 1)
|
||||
self.assertEqual(stats[OpenEndedChild.ASSESSING], 1)
|
||||
|
||||
@@ -50,19 +50,19 @@ class TestInstructorAccessAllow(ModuleStoreTestCase):
|
||||
def test_allow(self):
|
||||
user = UserFactory()
|
||||
allow_access(self.course, user, 'staff')
|
||||
self.assertTrue(CourseStaffRole(self.course.location).has_user(user))
|
||||
self.assertTrue(CourseStaffRole(self.course.id).has_user(user))
|
||||
|
||||
def test_allow_twice(self):
|
||||
user = UserFactory()
|
||||
allow_access(self.course, user, 'staff')
|
||||
allow_access(self.course, user, 'staff')
|
||||
self.assertTrue(CourseStaffRole(self.course.location).has_user(user))
|
||||
self.assertTrue(CourseStaffRole(self.course.id).has_user(user))
|
||||
|
||||
def test_allow_beta(self):
|
||||
""" Test allow beta against list beta. """
|
||||
user = UserFactory()
|
||||
allow_access(self.course, user, 'beta')
|
||||
self.assertTrue(CourseBetaTesterRole(self.course.location).has_user(user))
|
||||
self.assertTrue(CourseBetaTesterRole(self.course.id).has_user(user))
|
||||
|
||||
@raises(ValueError)
|
||||
def test_allow_badlevel(self):
|
||||
@@ -91,17 +91,17 @@ class TestInstructorAccessRevoke(ModuleStoreTestCase):
|
||||
def test_revoke(self):
|
||||
user = self.staff[0]
|
||||
revoke_access(self.course, user, 'staff')
|
||||
self.assertFalse(CourseStaffRole(self.course.location).has_user(user))
|
||||
self.assertFalse(CourseStaffRole(self.course.id).has_user(user))
|
||||
|
||||
def test_revoke_twice(self):
|
||||
user = self.staff[0]
|
||||
revoke_access(self.course, user, 'staff')
|
||||
self.assertFalse(CourseStaffRole(self.course.location).has_user(user))
|
||||
self.assertFalse(CourseStaffRole(self.course.id).has_user(user))
|
||||
|
||||
def test_revoke_beta(self):
|
||||
user = self.beta_testers[0]
|
||||
revoke_access(self.course, user, 'beta')
|
||||
self.assertFalse(CourseBetaTesterRole(self.course.location).has_user(user))
|
||||
self.assertFalse(CourseBetaTesterRole(self.course.id).has_user(user))
|
||||
|
||||
@raises(ValueError)
|
||||
def test_revoke_badrolename(self):
|
||||
|
||||
@@ -12,12 +12,14 @@ from django.test import TestCase
|
||||
from nose.tools import raises
|
||||
from mock import Mock, patch
|
||||
from django.test.utils import override_settings
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django_comment_common.models import FORUM_ROLE_COMMUNITY_TA, Role
|
||||
from django_comment_common.utils import seed_permissions_roles
|
||||
from django.core import mail
|
||||
from django.utils.timezone import utc
|
||||
from django.test import RequestFactory
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE
|
||||
@@ -27,6 +29,7 @@ from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
from student.tests.factories import UserFactory
|
||||
from courseware.tests.factories import StaffFactory, InstructorFactory, BetaTesterFactory
|
||||
from student.roles import CourseBetaTesterRole
|
||||
from microsite_configuration import microsite
|
||||
|
||||
from student.models import CourseEnrollment, CourseEnrollmentAllowed
|
||||
from courseware.models import StudentModule
|
||||
@@ -35,10 +38,11 @@ from courseware.models import StudentModule
|
||||
import instructor_task.api
|
||||
from instructor.access import allow_access
|
||||
import instructor.views.api
|
||||
from instructor.views.api import _split_input_list, _msk_from_problem_urlname, common_exceptions_400
|
||||
from instructor.views.api import _split_input_list, common_exceptions_400
|
||||
from instructor_task.api_helper import AlreadyRunningError
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
|
||||
from .test_tools import get_extended_due
|
||||
from .test_tools import msk_from_problem_urlname, get_extended_due
|
||||
|
||||
|
||||
@common_exceptions_400
|
||||
@@ -108,14 +112,15 @@ class TestInstructorAPIDenyLevels(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
self.user = UserFactory.create()
|
||||
CourseEnrollment.enroll(self.user, self.course.id)
|
||||
|
||||
self.problem_urlname = 'robot-some-problem-urlname'
|
||||
self.problem_location = msk_from_problem_urlname(
|
||||
self.course.id,
|
||||
'robot-some-problem-urlname'
|
||||
)
|
||||
self.problem_urlname = str(self.problem_location)
|
||||
_module = StudentModule.objects.create(
|
||||
student=self.user,
|
||||
course_id=self.course.id,
|
||||
module_state_key=_msk_from_problem_urlname(
|
||||
self.course.id,
|
||||
self.problem_urlname
|
||||
),
|
||||
module_id=self.problem_location,
|
||||
state=json.dumps({'attempts': 10}),
|
||||
)
|
||||
|
||||
@@ -153,13 +158,11 @@ class TestInstructorAPIDenyLevels(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
status_code: expected HTTP status code response
|
||||
msg: message to display if assertion fails.
|
||||
"""
|
||||
url = reverse(endpoint, kwargs={'course_id': self.course.id})
|
||||
if endpoint in 'send_email':
|
||||
url = reverse(endpoint, kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
if endpoint in ['send_email']:
|
||||
response = self.client.post(url, args)
|
||||
else:
|
||||
response = self.client.get(url, args)
|
||||
print endpoint
|
||||
print response
|
||||
self.assertEqual(
|
||||
response.status_code,
|
||||
status_code,
|
||||
@@ -192,7 +195,7 @@ class TestInstructorAPIDenyLevels(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
"""
|
||||
Ensure that a staff member can't access instructor endpoints.
|
||||
"""
|
||||
staff_member = StaffFactory(course=self.course.location)
|
||||
staff_member = StaffFactory(course=self.course.id)
|
||||
CourseEnrollment.enroll(staff_member, self.course.id)
|
||||
self.client.login(username=staff_member.username, password='test')
|
||||
# Try to promote to forums admin - not working
|
||||
@@ -221,7 +224,7 @@ class TestInstructorAPIDenyLevels(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
"""
|
||||
Ensure that an instructor member can access all endpoints.
|
||||
"""
|
||||
inst = InstructorFactory(course=self.course.location)
|
||||
inst = InstructorFactory(course=self.course.id)
|
||||
CourseEnrollment.enroll(inst, self.course.id)
|
||||
self.client.login(username=inst.username, password='test')
|
||||
|
||||
@@ -257,8 +260,9 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
job of test_enrollment. This tests the response and action switch.
|
||||
"""
|
||||
def setUp(self):
|
||||
self.request = RequestFactory().request()
|
||||
self.course = CourseFactory.create()
|
||||
self.instructor = InstructorFactory(course=self.course.location)
|
||||
self.instructor = InstructorFactory(course=self.course.id)
|
||||
self.client.login(username=self.instructor.username, password='test')
|
||||
|
||||
self.enrolled_student = UserFactory(username='EnrolledStudent', first_name='Enrolled', last_name='Student')
|
||||
@@ -276,26 +280,41 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
self.notregistered_email = 'robot-not-an-email-yet@robot.org'
|
||||
self.assertEqual(User.objects.filter(email=self.notregistered_email).count(), 0)
|
||||
|
||||
# Email URL values
|
||||
self.site_name = microsite.get_value(
|
||||
'SITE_NAME',
|
||||
settings.SITE_NAME
|
||||
)
|
||||
self.registration_url = 'https://{}/register'.format(self.site_name)
|
||||
self.about_url = 'https://{}/courses/MITx/999/Robot_Super_Course/about'.format(self.site_name)
|
||||
self.course_url = 'https://{}/courses/MITx/999/Robot_Super_Course/'.format(self.site_name)
|
||||
|
||||
# uncomment to enable enable printing of large diffs
|
||||
# from failed assertions in the event of a test failure.
|
||||
# (comment because pylint C0103)
|
||||
# self.maxDiff = None
|
||||
|
||||
def tearDown(self):
|
||||
"""
|
||||
Undo all patches.
|
||||
"""
|
||||
patch.stopall()
|
||||
|
||||
def test_missing_params(self):
|
||||
""" Test missing all query parameters. """
|
||||
url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id})
|
||||
url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_bad_action(self):
|
||||
""" Test with an invalid action. """
|
||||
action = 'robot-not-an-action'
|
||||
url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id})
|
||||
url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {'identifiers': self.enrolled_student.email, 'action': action})
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_invalid_email(self):
|
||||
url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id})
|
||||
url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {'identifiers': 'percivaloctavius@', 'action': 'enroll', 'email_students': False})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@@ -315,7 +334,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
self.assertEqual(res_json, expected)
|
||||
|
||||
def test_invalid_username(self):
|
||||
url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id})
|
||||
url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {'identifiers': 'percivaloctavius', 'action': 'enroll', 'email_students': False})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@@ -335,7 +354,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
self.assertEqual(res_json, expected)
|
||||
|
||||
def test_enroll_with_username(self):
|
||||
url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id})
|
||||
url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {'identifiers': self.notenrolled_student.username, 'action': 'enroll', 'email_students': False})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@@ -366,7 +385,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
self.assertEqual(res_json, expected)
|
||||
|
||||
def test_enroll_without_email(self):
|
||||
url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id})
|
||||
url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {'identifiers': self.notenrolled_student.email, 'action': 'enroll', 'email_students': False})
|
||||
print "type(self.notenrolled_student.email): {}".format(type(self.notenrolled_student.email))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
@@ -405,7 +424,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
self.assertEqual(len(mail.outbox), 0)
|
||||
|
||||
def test_enroll_with_email(self):
|
||||
url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id})
|
||||
url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {'identifiers': self.notenrolled_student.email, 'action': 'enroll', 'email_students': True})
|
||||
print "type(self.notenrolled_student.email): {}".format(type(self.notenrolled_student.email))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
@@ -452,12 +471,14 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
"at edx.org by a member of the course staff. "
|
||||
"The course should now appear on your edx.org dashboard.\n\n"
|
||||
"To start accessing course materials, please visit "
|
||||
"https://edx.org/courses/MITx/999/Robot_Super_Course/\n\n----\n"
|
||||
"This email was automatically sent from edx.org to NotEnrolled Student"
|
||||
"{course_url}\n\n----\n"
|
||||
"This email was automatically sent from edx.org to NotEnrolled Student".format(
|
||||
course_url=self.course_url
|
||||
)
|
||||
)
|
||||
|
||||
def test_enroll_with_email_not_registered(self):
|
||||
url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id})
|
||||
url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {'identifiers': self.notregistered_email, 'action': 'enroll', 'email_students': True})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@@ -470,18 +491,20 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
self.assertEqual(
|
||||
mail.outbox[0].body,
|
||||
"Dear student,\n\nYou have been invited to join Robot Super Course at edx.org by a member of the course staff.\n\n"
|
||||
"To finish your registration, please visit https://edx.org/register and fill out the registration form "
|
||||
"To finish your registration, please visit {registration_url} and fill out the registration form "
|
||||
"making sure to use robot-not-an-email-yet@robot.org in the E-mail field.\n"
|
||||
"Once you have registered and activated your account, "
|
||||
"visit https://edx.org/courses/MITx/999/Robot_Super_Course/about to join the course.\n\n----\n"
|
||||
"This email was automatically sent from edx.org to robot-not-an-email-yet@robot.org"
|
||||
"visit {about_url} to join the course.\n\n----\n"
|
||||
"This email was automatically sent from edx.org to robot-not-an-email-yet@robot.org".format(
|
||||
registration_url=self.registration_url, about_url=self.about_url
|
||||
)
|
||||
)
|
||||
|
||||
@patch.dict(settings.FEATURES, {'ENABLE_MKTG_SITE': True})
|
||||
def test_enroll_email_not_registered_mktgsite(self):
|
||||
url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id})
|
||||
# Try with marketing site enabled
|
||||
with patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': True}):
|
||||
response = self.client.get(url, {'identifiers': self.notregistered_email, 'action': 'enroll', 'email_students': True})
|
||||
url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {'identifiers': self.notregistered_email, 'action': 'enroll', 'email_students': True})
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(
|
||||
@@ -494,7 +517,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
)
|
||||
|
||||
def test_enroll_with_email_not_registered_autoenroll(self):
|
||||
url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id})
|
||||
url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {'identifiers': self.notregistered_email, 'action': 'enroll', 'email_students': True, 'auto_enroll': True})
|
||||
print "type(self.notregistered_email): {}".format(type(self.notregistered_email))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
@@ -508,14 +531,16 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
self.assertEqual(
|
||||
mail.outbox[0].body,
|
||||
"Dear student,\n\nYou have been invited to join Robot Super Course at edx.org by a member of the course staff.\n\n"
|
||||
"To finish your registration, please visit https://edx.org/register and fill out the registration form "
|
||||
"To finish your registration, please visit {registration_url} and fill out the registration form "
|
||||
"making sure to use robot-not-an-email-yet@robot.org in the E-mail field.\n"
|
||||
"Once you have registered and activated your account, you will see Robot Super Course listed on your dashboard.\n\n----\n"
|
||||
"This email was automatically sent from edx.org to robot-not-an-email-yet@robot.org"
|
||||
"This email was automatically sent from edx.org to robot-not-an-email-yet@robot.org".format(
|
||||
registration_url=self.registration_url
|
||||
)
|
||||
)
|
||||
|
||||
def test_unenroll_without_email(self):
|
||||
url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id})
|
||||
url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {'identifiers': self.enrolled_student.email, 'action': 'unenroll', 'email_students': False})
|
||||
print "type(self.enrolled_student.email): {}".format(type(self.enrolled_student.email))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
@@ -554,7 +579,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
self.assertEqual(len(mail.outbox), 0)
|
||||
|
||||
def test_unenroll_with_email(self):
|
||||
url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id})
|
||||
url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {'identifiers': self.enrolled_student.email, 'action': 'unenroll', 'email_students': True})
|
||||
print "type(self.enrolled_student.email): {}".format(type(self.enrolled_student.email))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
@@ -605,7 +630,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
)
|
||||
|
||||
def test_unenroll_with_email_allowed_student(self):
|
||||
url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id})
|
||||
url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {'identifiers': self.allowed_email, 'action': 'unenroll', 'email_students': True})
|
||||
print "type(self.allowed_email): {}".format(type(self.allowed_email))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
@@ -653,7 +678,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
def test_enroll_with_email_not_registered_with_shib(self, mock_uses_shib):
|
||||
mock_uses_shib.return_value = True
|
||||
|
||||
url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id})
|
||||
url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {'identifiers': self.notregistered_email, 'action': 'enroll', 'email_students': True})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@@ -667,15 +692,19 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
self.assertEqual(
|
||||
mail.outbox[0].body,
|
||||
"Dear student,\n\nYou have been invited to join Robot Super Course at edx.org by a member of the course staff.\n\n"
|
||||
"To access the course visit https://edx.org/courses/MITx/999/Robot_Super_Course/about and register for the course.\n\n----\n"
|
||||
"This email was automatically sent from edx.org to robot-not-an-email-yet@robot.org"
|
||||
"To access the course visit {about_url} and register for the course.\n\n----\n"
|
||||
"This email was automatically sent from edx.org to robot-not-an-email-yet@robot.org".format(
|
||||
about_url=self.about_url
|
||||
)
|
||||
)
|
||||
|
||||
@patch('instructor.enrollment.uses_shib')
|
||||
@patch.dict(settings.FEATURES, {'ENABLE_MKTG_SITE': True})
|
||||
def test_enroll_email_not_registered_shib_mktgsite(self, mock_uses_shib):
|
||||
# Try with marketing site enabled and shib on
|
||||
mock_uses_shib.return_value = True
|
||||
|
||||
url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id})
|
||||
url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
# Try with marketing site enabled
|
||||
with patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': True}):
|
||||
response = self.client.get(url, {'identifiers': self.notregistered_email, 'action': 'enroll', 'email_students': True})
|
||||
@@ -692,7 +721,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
|
||||
mock_uses_shib.return_value = True
|
||||
|
||||
url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id})
|
||||
url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {'identifiers': self.notregistered_email, 'action': 'enroll', 'email_students': True, 'auto_enroll': True})
|
||||
print "type(self.notregistered_email): {}".format(type(self.notregistered_email))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
@@ -707,8 +736,10 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
self.assertEqual(
|
||||
mail.outbox[0].body,
|
||||
"Dear student,\n\nYou have been invited to join Robot Super Course at edx.org by a member of the course staff.\n\n"
|
||||
"To access the course visit https://edx.org/courses/MITx/999/Robot_Super_Course/ and login.\n\n----\n"
|
||||
"This email was automatically sent from edx.org to robot-not-an-email-yet@robot.org"
|
||||
"To access the course visit {course_url} and login.\n\n----\n"
|
||||
"This email was automatically sent from edx.org to robot-not-an-email-yet@robot.org".format(
|
||||
course_url=self.course_url
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -719,20 +750,30 @@ class TestInstructorAPIBulkBetaEnrollment(ModuleStoreTestCase, LoginEnrollmentTe
|
||||
"""
|
||||
def setUp(self):
|
||||
self.course = CourseFactory.create()
|
||||
self.instructor = InstructorFactory(course=self.course.location)
|
||||
self.instructor = InstructorFactory(course=self.course.id)
|
||||
self.client.login(username=self.instructor.username, password='test')
|
||||
|
||||
self.beta_tester = BetaTesterFactory(course=self.course.location)
|
||||
self.beta_tester = BetaTesterFactory(course=self.course.id)
|
||||
CourseEnrollment.enroll(
|
||||
self.beta_tester,
|
||||
self.course.id
|
||||
)
|
||||
self.assertTrue(CourseBetaTesterRole(self.course.id).has_user(self.beta_tester))
|
||||
|
||||
self.notenrolled_student = UserFactory(username='NotEnrolledStudent')
|
||||
|
||||
self.notregistered_email = 'robot-not-an-email-yet@robot.org'
|
||||
self.assertEqual(User.objects.filter(email=self.notregistered_email).count(), 0)
|
||||
|
||||
self.request = RequestFactory().request()
|
||||
|
||||
# Email URL values
|
||||
self.site_name = microsite.get_value(
|
||||
'SITE_NAME',
|
||||
settings.SITE_NAME
|
||||
)
|
||||
self.about_url = 'https://{}/courses/MITx/999/Robot_Super_Course/about'.format(self.site_name)
|
||||
|
||||
# uncomment to enable enable printing of large diffs
|
||||
# from failed assertions in the event of a test failure.
|
||||
# (comment because pylint C0103)
|
||||
@@ -740,14 +781,14 @@ class TestInstructorAPIBulkBetaEnrollment(ModuleStoreTestCase, LoginEnrollmentTe
|
||||
|
||||
def test_missing_params(self):
|
||||
""" Test missing all query parameters. """
|
||||
url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id})
|
||||
url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_bad_action(self):
|
||||
""" Test with an invalid action. """
|
||||
action = 'robot-not-an-action'
|
||||
url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id})
|
||||
url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {'identifiers': self.beta_tester.email, 'action': action})
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
@@ -763,7 +804,7 @@ class TestInstructorAPIBulkBetaEnrollment(ModuleStoreTestCase, LoginEnrollmentTe
|
||||
Additionally asserts no email was sent.
|
||||
"""
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue(CourseBetaTesterRole(self.course.location).has_user(self.notenrolled_student))
|
||||
self.assertTrue(CourseBetaTesterRole(self.course.id).has_user(self.notenrolled_student))
|
||||
# test the response data
|
||||
expected = {
|
||||
"action": "add",
|
||||
@@ -783,35 +824,35 @@ class TestInstructorAPIBulkBetaEnrollment(ModuleStoreTestCase, LoginEnrollmentTe
|
||||
self.assertEqual(len(mail.outbox), 0)
|
||||
|
||||
def test_add_notenrolled_email(self):
|
||||
url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id})
|
||||
url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {'identifiers': self.notenrolled_student.email, 'action': 'add', 'email_students': False})
|
||||
self.add_notenrolled(response, self.notenrolled_student.email)
|
||||
self.assertFalse(CourseEnrollment.is_enrolled(self.notenrolled_student, self.course.id))
|
||||
|
||||
def test_add_notenrolled_email_autoenroll(self):
|
||||
url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id})
|
||||
url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {'identifiers': self.notenrolled_student.email, 'action': 'add', 'email_students': False, 'auto_enroll': True})
|
||||
self.add_notenrolled(response, self.notenrolled_student.email)
|
||||
self.assertTrue(CourseEnrollment.is_enrolled(self.notenrolled_student, self.course.id))
|
||||
|
||||
def test_add_notenrolled_username(self):
|
||||
url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id})
|
||||
url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {'identifiers': self.notenrolled_student.username, 'action': 'add', 'email_students': False})
|
||||
self.add_notenrolled(response, self.notenrolled_student.username)
|
||||
self.assertFalse(CourseEnrollment.is_enrolled(self.notenrolled_student, self.course.id))
|
||||
|
||||
def test_add_notenrolled_username_autoenroll(self):
|
||||
url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id})
|
||||
url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {'identifiers': self.notenrolled_student.username, 'action': 'add', 'email_students': False, 'auto_enroll': True})
|
||||
self.add_notenrolled(response, self.notenrolled_student.username)
|
||||
self.assertTrue(CourseEnrollment.is_enrolled(self.notenrolled_student, self.course.id))
|
||||
|
||||
def test_add_notenrolled_with_email(self):
|
||||
url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id})
|
||||
url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {'identifiers': self.notenrolled_student.email, 'action': 'add', 'email_students': True})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
self.assertTrue(CourseBetaTesterRole(self.course.location).has_user(self.notenrolled_student))
|
||||
self.assertTrue(CourseBetaTesterRole(self.course.id).has_user(self.notenrolled_student))
|
||||
# test the response data
|
||||
expected = {
|
||||
"action": "add",
|
||||
@@ -837,23 +878,24 @@ class TestInstructorAPIBulkBetaEnrollment(ModuleStoreTestCase, LoginEnrollmentTe
|
||||
mail.outbox[0].body,
|
||||
u"Dear {0}\n\nYou have been invited to be a beta tester "
|
||||
"for Robot Super Course at edx.org by a member of the course staff.\n\n"
|
||||
"Visit https://edx.org/courses/MITx/999/Robot_Super_Course/about to join "
|
||||
"Visit {1} to join "
|
||||
"the course and begin the beta test.\n\n----\n"
|
||||
"This email was automatically sent from edx.org to {1}".format(
|
||||
"This email was automatically sent from edx.org to {2}".format(
|
||||
self.notenrolled_student.profile.name,
|
||||
self.about_url,
|
||||
self.notenrolled_student.email
|
||||
)
|
||||
)
|
||||
|
||||
def test_add_notenrolled_with_email_autoenroll(self):
|
||||
url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id})
|
||||
url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(
|
||||
url,
|
||||
{'identifiers': self.notenrolled_student.email, 'action': 'add', 'email_students': True, 'auto_enroll': True}
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
self.assertTrue(CourseBetaTesterRole(self.course.location).has_user(self.notenrolled_student))
|
||||
self.assertTrue(CourseBetaTesterRole(self.course.id).has_user(self.notenrolled_student))
|
||||
# test the response data
|
||||
expected = {
|
||||
"action": "add",
|
||||
@@ -887,11 +929,11 @@ class TestInstructorAPIBulkBetaEnrollment(ModuleStoreTestCase, LoginEnrollmentTe
|
||||
)
|
||||
)
|
||||
|
||||
@patch.dict(settings.FEATURES, {'ENABLE_MKTG_SITE': True})
|
||||
def test_add_notenrolled_email_mktgsite(self):
|
||||
url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id})
|
||||
# Try with marketing site enabled
|
||||
with patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': True}):
|
||||
response = self.client.get(url, {'identifiers': self.notenrolled_student.email, 'action': 'add', 'email_students': True})
|
||||
url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {'identifiers': self.notenrolled_student.email, 'action': 'add', 'email_students': True})
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(
|
||||
@@ -907,7 +949,7 @@ class TestInstructorAPIBulkBetaEnrollment(ModuleStoreTestCase, LoginEnrollmentTe
|
||||
|
||||
def test_enroll_with_email_not_registered(self):
|
||||
# User doesn't exist
|
||||
url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id})
|
||||
url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {'identifiers': self.notregistered_email, 'action': 'add', 'email_students': True})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
# test the response data
|
||||
@@ -928,11 +970,15 @@ class TestInstructorAPIBulkBetaEnrollment(ModuleStoreTestCase, LoginEnrollmentTe
|
||||
self.assertEqual(len(mail.outbox), 0)
|
||||
|
||||
def test_remove_without_email(self):
|
||||
url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id})
|
||||
url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {'identifiers': self.beta_tester.email, 'action': 'remove', 'email_students': False})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
self.assertFalse(CourseBetaTesterRole(self.course.location).has_user(self.beta_tester))
|
||||
# Works around a caching bug which supposedly can't happen in prod. The instance here is not ==
|
||||
# the instance fetched from the email above which had its cache cleared
|
||||
if hasattr(self.beta_tester, '_roles'):
|
||||
del self.beta_tester._roles
|
||||
self.assertFalse(CourseBetaTesterRole(self.course.id).has_user(self.beta_tester))
|
||||
|
||||
# test the response data
|
||||
expected = {
|
||||
@@ -952,11 +998,15 @@ class TestInstructorAPIBulkBetaEnrollment(ModuleStoreTestCase, LoginEnrollmentTe
|
||||
self.assertEqual(len(mail.outbox), 0)
|
||||
|
||||
def test_remove_with_email(self):
|
||||
url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id})
|
||||
url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {'identifiers': self.beta_tester.email, 'action': 'remove', 'email_students': True})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
self.assertFalse(CourseBetaTesterRole(self.course.location).has_user(self.beta_tester))
|
||||
# Works around a caching bug which supposedly can't happen in prod. The instance here is not ==
|
||||
# the instance fetched from the email above which had its cache cleared
|
||||
if hasattr(self.beta_tester, '_roles'):
|
||||
del self.beta_tester._roles
|
||||
self.assertFalse(CourseBetaTesterRole(self.course.id).has_user(self.beta_tester))
|
||||
|
||||
# test the response data
|
||||
expected = {
|
||||
@@ -1005,24 +1055,22 @@ class TestInstructorAPILevelsAccess(ModuleStoreTestCase, LoginEnrollmentTestCase
|
||||
"""
|
||||
def setUp(self):
|
||||
self.course = CourseFactory.create()
|
||||
self.instructor = InstructorFactory(course=self.course.location)
|
||||
self.instructor = InstructorFactory(course=self.course.id)
|
||||
self.client.login(username=self.instructor.username, password='test')
|
||||
|
||||
self.other_instructor = UserFactory()
|
||||
allow_access(self.course, self.other_instructor, 'instructor')
|
||||
self.other_staff = UserFactory()
|
||||
allow_access(self.course, self.other_staff, 'staff')
|
||||
self.other_instructor = InstructorFactory(course=self.course.id)
|
||||
self.other_staff = StaffFactory(course=self.course.id)
|
||||
self.other_user = UserFactory()
|
||||
|
||||
def test_modify_access_noparams(self):
|
||||
""" Test missing all query parameters. """
|
||||
url = reverse('modify_access', kwargs={'course_id': self.course.id})
|
||||
url = reverse('modify_access', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_modify_access_bad_action(self):
|
||||
""" Test with an invalid action parameter. """
|
||||
url = reverse('modify_access', kwargs={'course_id': self.course.id})
|
||||
url = reverse('modify_access', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {
|
||||
'unique_student_identifier': self.other_staff.email,
|
||||
'rolename': 'staff',
|
||||
@@ -1032,7 +1080,7 @@ class TestInstructorAPILevelsAccess(ModuleStoreTestCase, LoginEnrollmentTestCase
|
||||
|
||||
def test_modify_access_bad_role(self):
|
||||
""" Test with an invalid action parameter. """
|
||||
url = reverse('modify_access', kwargs={'course_id': self.course.id})
|
||||
url = reverse('modify_access', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {
|
||||
'unique_student_identifier': self.other_staff.email,
|
||||
'rolename': 'robot-not-a-roll',
|
||||
@@ -1041,16 +1089,16 @@ class TestInstructorAPILevelsAccess(ModuleStoreTestCase, LoginEnrollmentTestCase
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_modify_access_allow(self):
|
||||
url = reverse('modify_access', kwargs={'course_id': self.course.id})
|
||||
url = reverse('modify_access', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {
|
||||
'unique_student_identifier': self.other_instructor.email,
|
||||
'unique_student_identifier': self.other_user.email,
|
||||
'rolename': 'staff',
|
||||
'action': 'allow',
|
||||
})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_modify_access_allow_with_uname(self):
|
||||
url = reverse('modify_access', kwargs={'course_id': self.course.id})
|
||||
url = reverse('modify_access', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {
|
||||
'unique_student_identifier': self.other_instructor.username,
|
||||
'rolename': 'staff',
|
||||
@@ -1059,7 +1107,7 @@ class TestInstructorAPILevelsAccess(ModuleStoreTestCase, LoginEnrollmentTestCase
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_modify_access_revoke(self):
|
||||
url = reverse('modify_access', kwargs={'course_id': self.course.id})
|
||||
url = reverse('modify_access', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {
|
||||
'unique_student_identifier': self.other_staff.email,
|
||||
'rolename': 'staff',
|
||||
@@ -1068,7 +1116,7 @@ class TestInstructorAPILevelsAccess(ModuleStoreTestCase, LoginEnrollmentTestCase
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_modify_access_revoke_with_username(self):
|
||||
url = reverse('modify_access', kwargs={'course_id': self.course.id})
|
||||
url = reverse('modify_access', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {
|
||||
'unique_student_identifier': self.other_staff.username,
|
||||
'rolename': 'staff',
|
||||
@@ -1077,7 +1125,7 @@ class TestInstructorAPILevelsAccess(ModuleStoreTestCase, LoginEnrollmentTestCase
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_modify_access_with_fake_user(self):
|
||||
url = reverse('modify_access', kwargs={'course_id': self.course.id})
|
||||
url = reverse('modify_access', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {
|
||||
'unique_student_identifier': 'GandalfTheGrey',
|
||||
'rolename': 'staff',
|
||||
@@ -1094,7 +1142,7 @@ class TestInstructorAPILevelsAccess(ModuleStoreTestCase, LoginEnrollmentTestCase
|
||||
def test_modify_access_with_inactive_user(self):
|
||||
self.other_user.is_active = False
|
||||
self.other_user.save() # pylint: disable=no-member
|
||||
url = reverse('modify_access', kwargs={'course_id': self.course.id})
|
||||
url = reverse('modify_access', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {
|
||||
'unique_student_identifier': self.other_user.username,
|
||||
'rolename': 'beta',
|
||||
@@ -1110,7 +1158,7 @@ class TestInstructorAPILevelsAccess(ModuleStoreTestCase, LoginEnrollmentTestCase
|
||||
|
||||
def test_modify_access_revoke_not_allowed(self):
|
||||
""" Test revoking access that a user does not have. """
|
||||
url = reverse('modify_access', kwargs={'course_id': self.course.id})
|
||||
url = reverse('modify_access', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {
|
||||
'unique_student_identifier': self.other_staff.email,
|
||||
'rolename': 'instructor',
|
||||
@@ -1122,7 +1170,7 @@ class TestInstructorAPILevelsAccess(ModuleStoreTestCase, LoginEnrollmentTestCase
|
||||
"""
|
||||
Test that an instructor cannot remove instructor privelages from themself.
|
||||
"""
|
||||
url = reverse('modify_access', kwargs={'course_id': self.course.id})
|
||||
url = reverse('modify_access', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {
|
||||
'unique_student_identifier': self.instructor.email,
|
||||
'rolename': 'instructor',
|
||||
@@ -1141,30 +1189,28 @@ class TestInstructorAPILevelsAccess(ModuleStoreTestCase, LoginEnrollmentTestCase
|
||||
|
||||
def test_list_course_role_members_noparams(self):
|
||||
""" Test missing all query parameters. """
|
||||
url = reverse('list_course_role_members', kwargs={'course_id': self.course.id})
|
||||
url = reverse('list_course_role_members', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_list_course_role_members_bad_rolename(self):
|
||||
""" Test with an invalid rolename parameter. """
|
||||
url = reverse('list_course_role_members', kwargs={'course_id': self.course.id})
|
||||
url = reverse('list_course_role_members', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {
|
||||
'rolename': 'robot-not-a-rolename',
|
||||
})
|
||||
print response
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_list_course_role_members_staff(self):
|
||||
url = reverse('list_course_role_members', kwargs={'course_id': self.course.id})
|
||||
url = reverse('list_course_role_members', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {
|
||||
'rolename': 'staff',
|
||||
})
|
||||
print response
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# check response content
|
||||
expected = {
|
||||
'course_id': self.course.id,
|
||||
'course_id': self.course.id.to_deprecated_string(),
|
||||
'staff': [
|
||||
{
|
||||
'username': self.other_staff.username,
|
||||
@@ -1178,16 +1224,15 @@ class TestInstructorAPILevelsAccess(ModuleStoreTestCase, LoginEnrollmentTestCase
|
||||
self.assertEqual(res_json, expected)
|
||||
|
||||
def test_list_course_role_members_beta(self):
|
||||
url = reverse('list_course_role_members', kwargs={'course_id': self.course.id})
|
||||
url = reverse('list_course_role_members', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {
|
||||
'rolename': 'beta',
|
||||
})
|
||||
print response
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# check response content
|
||||
expected = {
|
||||
'course_id': self.course.id,
|
||||
'course_id': self.course.id.to_deprecated_string(),
|
||||
'beta': []
|
||||
}
|
||||
res_json = json.loads(response.content)
|
||||
@@ -1225,7 +1270,7 @@ class TestInstructorAPILevelsAccess(ModuleStoreTestCase, LoginEnrollmentTestCase
|
||||
Get unique_student_identifier, rolename and action and update forum role.
|
||||
"""
|
||||
|
||||
url = reverse('update_forum_role_membership', kwargs={'course_id': self.course.id})
|
||||
url = reverse('update_forum_role_membership', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(
|
||||
url,
|
||||
{
|
||||
@@ -1253,7 +1298,7 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa
|
||||
"""
|
||||
def setUp(self):
|
||||
self.course = CourseFactory.create()
|
||||
self.instructor = InstructorFactory(course=self.course.location)
|
||||
self.instructor = InstructorFactory(course=self.course.id)
|
||||
self.client.login(username=self.instructor.username, password='test')
|
||||
|
||||
self.students = [UserFactory() for _ in xrange(6)]
|
||||
@@ -1265,7 +1310,7 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa
|
||||
Test that some minimum of information is formatted
|
||||
correctly in the response to get_students_features.
|
||||
"""
|
||||
url = reverse('get_students_features', kwargs={'course_id': self.course.id})
|
||||
url = reverse('get_students_features', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {})
|
||||
res_json = json.loads(response.content)
|
||||
self.assertIn('students', res_json)
|
||||
@@ -1281,7 +1326,7 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa
|
||||
"""
|
||||
Test the CSV output for the anonymized user ids.
|
||||
"""
|
||||
url = reverse('get_anon_ids', kwargs={'course_id': self.course.id})
|
||||
url = reverse('get_anon_ids', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
with patch('instructor.views.api.unique_id_for_user') as mock_unique:
|
||||
mock_unique.return_value = '42'
|
||||
response = self.client.get(url, {})
|
||||
@@ -1291,7 +1336,7 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa
|
||||
self.assertTrue(body.endswith('"7","42"\n'))
|
||||
|
||||
def test_list_report_downloads(self):
|
||||
url = reverse('list_report_downloads', kwargs={'course_id': self.course.id})
|
||||
url = reverse('list_report_downloads', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
with patch('instructor_task.models.LocalFSReportStore.links_for') as mock_links_for:
|
||||
mock_links_for.return_value = [
|
||||
('mock_file_name_1', 'https://1.mock.url'),
|
||||
@@ -1317,7 +1362,7 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa
|
||||
self.assertEqual(res_json, expected_response)
|
||||
|
||||
def test_calculate_grades_csv_success(self):
|
||||
url = reverse('calculate_grades_csv', kwargs={'course_id': self.course.id})
|
||||
url = reverse('calculate_grades_csv', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
|
||||
with patch('instructor_task.api.submit_calculate_grades_csv') as mock_cal_grades:
|
||||
mock_cal_grades.return_value = True
|
||||
@@ -1326,7 +1371,7 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa
|
||||
self.assertIn(success_status, response.content)
|
||||
|
||||
def test_calculate_grades_csv_already_running(self):
|
||||
url = reverse('calculate_grades_csv', kwargs={'course_id': self.course.id})
|
||||
url = reverse('calculate_grades_csv', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
|
||||
with patch('instructor_task.api.submit_calculate_grades_csv') as mock_cal_grades:
|
||||
mock_cal_grades.side_effect = AlreadyRunningError()
|
||||
@@ -1339,7 +1384,7 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa
|
||||
Test that some minimum of information is formatted
|
||||
correctly in the response to get_students_features.
|
||||
"""
|
||||
url = reverse('get_students_features', kwargs={'course_id': self.course.id})
|
||||
url = reverse('get_students_features', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url + '/csv', {})
|
||||
self.assertEqual(response['Content-Type'], 'text/csv')
|
||||
|
||||
@@ -1348,13 +1393,13 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa
|
||||
Test that get_distribution lists available features
|
||||
when supplied no feature parameter.
|
||||
"""
|
||||
url = reverse('get_distribution', kwargs={'course_id': self.course.id})
|
||||
url = reverse('get_distribution', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
res_json = json.loads(response.content)
|
||||
self.assertEqual(type(res_json['available_features']), list)
|
||||
|
||||
url = reverse('get_distribution', kwargs={'course_id': self.course.id})
|
||||
url = reverse('get_distribution', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url + u'?feature=')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
res_json = json.loads(response.content)
|
||||
@@ -1365,7 +1410,7 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa
|
||||
Test that get_distribution fails gracefully with
|
||||
an unavailable feature.
|
||||
"""
|
||||
url = reverse('get_distribution', kwargs={'course_id': self.course.id})
|
||||
url = reverse('get_distribution', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {'feature': 'robot-not-a-real-feature'})
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
@@ -1374,11 +1419,10 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa
|
||||
Test that get_distribution fails gracefully with
|
||||
an unavailable feature.
|
||||
"""
|
||||
url = reverse('get_distribution', kwargs={'course_id': self.course.id})
|
||||
url = reverse('get_distribution', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {'feature': 'gender'})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
res_json = json.loads(response.content)
|
||||
print res_json
|
||||
self.assertEqual(res_json['feature_results']['data']['m'], 6)
|
||||
self.assertEqual(res_json['feature_results']['choices_display_names']['m'], 'Male')
|
||||
self.assertEqual(res_json['feature_results']['data']['no_data'], 0)
|
||||
@@ -1386,39 +1430,35 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa
|
||||
|
||||
def test_get_student_progress_url(self):
|
||||
""" Test that progress_url is in the successful response. """
|
||||
url = reverse('get_student_progress_url', kwargs={'course_id': self.course.id})
|
||||
url = reverse('get_student_progress_url', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
url += "?unique_student_identifier={}".format(
|
||||
quote(self.students[0].email.encode("utf-8"))
|
||||
)
|
||||
print url
|
||||
response = self.client.get(url)
|
||||
print response
|
||||
self.assertEqual(response.status_code, 200)
|
||||
res_json = json.loads(response.content)
|
||||
self.assertIn('progress_url', res_json)
|
||||
|
||||
def test_get_student_progress_url_from_uname(self):
|
||||
""" Test that progress_url is in the successful response. """
|
||||
url = reverse('get_student_progress_url', kwargs={'course_id': self.course.id})
|
||||
url = reverse('get_student_progress_url', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
url += "?unique_student_identifier={}".format(
|
||||
quote(self.students[0].username.encode("utf-8"))
|
||||
)
|
||||
print url
|
||||
response = self.client.get(url)
|
||||
print response
|
||||
self.assertEqual(response.status_code, 200)
|
||||
res_json = json.loads(response.content)
|
||||
self.assertIn('progress_url', res_json)
|
||||
|
||||
def test_get_student_progress_url_noparams(self):
|
||||
""" Test that the endpoint 404's without the required query params. """
|
||||
url = reverse('get_student_progress_url', kwargs={'course_id': self.course.id})
|
||||
url = reverse('get_student_progress_url', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_get_student_progress_url_nostudent(self):
|
||||
""" Test that the endpoint 400's when requesting an unknown email. """
|
||||
url = reverse('get_student_progress_url', kwargs={'course_id': self.course.id})
|
||||
url = reverse('get_student_progress_url', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
@@ -1434,42 +1474,42 @@ class TestInstructorAPIRegradeTask(ModuleStoreTestCase, LoginEnrollmentTestCase)
|
||||
"""
|
||||
def setUp(self):
|
||||
self.course = CourseFactory.create()
|
||||
self.instructor = InstructorFactory(course=self.course.location)
|
||||
self.instructor = InstructorFactory(course=self.course.id)
|
||||
self.client.login(username=self.instructor.username, password='test')
|
||||
|
||||
self.student = UserFactory()
|
||||
CourseEnrollment.enroll(self.student, self.course.id)
|
||||
|
||||
self.problem_urlname = 'robot-some-problem-urlname'
|
||||
self.problem_location = msk_from_problem_urlname(
|
||||
self.course.id,
|
||||
'robot-some-problem-urlname'
|
||||
)
|
||||
self.problem_urlname = str(self.problem_location)
|
||||
|
||||
self.module_to_reset = StudentModule.objects.create(
|
||||
student=self.student,
|
||||
course_id=self.course.id,
|
||||
module_state_key=_msk_from_problem_urlname(
|
||||
self.course.id,
|
||||
self.problem_urlname
|
||||
),
|
||||
module_id=self.problem_location,
|
||||
state=json.dumps({'attempts': 10}),
|
||||
)
|
||||
|
||||
def test_reset_student_attempts_deletall(self):
|
||||
""" Make sure no one can delete all students state on a problem. """
|
||||
url = reverse('reset_student_attempts', kwargs={'course_id': self.course.id})
|
||||
url = reverse('reset_student_attempts', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {
|
||||
'problem_to_reset': self.problem_urlname,
|
||||
'all_students': True,
|
||||
'delete_module': True,
|
||||
})
|
||||
print response.content
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_reset_student_attempts_single(self):
|
||||
""" Test reset single student attempts. """
|
||||
url = reverse('reset_student_attempts', kwargs={'course_id': self.course.id})
|
||||
url = reverse('reset_student_attempts', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {
|
||||
'problem_to_reset': self.problem_urlname,
|
||||
'unique_student_identifier': self.student.email,
|
||||
})
|
||||
print response.content
|
||||
self.assertEqual(response.status_code, 200)
|
||||
# make sure problem attempts have been reset.
|
||||
changed_module = StudentModule.objects.get(pk=self.module_to_reset.pk)
|
||||
@@ -1482,89 +1522,82 @@ class TestInstructorAPIRegradeTask(ModuleStoreTestCase, LoginEnrollmentTestCase)
|
||||
@patch.object(instructor_task.api, 'submit_reset_problem_attempts_for_all_students')
|
||||
def test_reset_student_attempts_all(self, act):
|
||||
""" Test reset all student attempts. """
|
||||
url = reverse('reset_student_attempts', kwargs={'course_id': self.course.id})
|
||||
url = reverse('reset_student_attempts', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {
|
||||
'problem_to_reset': self.problem_urlname,
|
||||
'all_students': True,
|
||||
})
|
||||
print response.content
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue(act.called)
|
||||
|
||||
def test_reset_student_attempts_missingmodule(self):
|
||||
""" Test reset for non-existant problem. """
|
||||
url = reverse('reset_student_attempts', kwargs={'course_id': self.course.id})
|
||||
url = reverse('reset_student_attempts', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {
|
||||
'problem_to_reset': 'robot-not-a-real-module',
|
||||
'unique_student_identifier': self.student.email,
|
||||
})
|
||||
print response.content
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_reset_student_attempts_delete(self):
|
||||
""" Test delete single student state. """
|
||||
url = reverse('reset_student_attempts', kwargs={'course_id': self.course.id})
|
||||
url = reverse('reset_student_attempts', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {
|
||||
'problem_to_reset': self.problem_urlname,
|
||||
'unique_student_identifier': self.student.email,
|
||||
'delete_module': True,
|
||||
})
|
||||
print response.content
|
||||
self.assertEqual(response.status_code, 200)
|
||||
# make sure the module has been deleted
|
||||
self.assertEqual(
|
||||
StudentModule.objects.filter(
|
||||
student=self.module_to_reset.student,
|
||||
course_id=self.module_to_reset.course_id,
|
||||
# module_state_key=self.module_to_reset.module_state_key,
|
||||
# module_id=self.module_to_reset.module_id,
|
||||
).count(),
|
||||
0
|
||||
)
|
||||
|
||||
def test_reset_student_attempts_nonsense(self):
|
||||
""" Test failure with both unique_student_identifier and all_students. """
|
||||
url = reverse('reset_student_attempts', kwargs={'course_id': self.course.id})
|
||||
url = reverse('reset_student_attempts', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {
|
||||
'problem_to_reset': self.problem_urlname,
|
||||
'unique_student_identifier': self.student.email,
|
||||
'all_students': True,
|
||||
})
|
||||
print response.content
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
@patch.object(instructor_task.api, 'submit_rescore_problem_for_student')
|
||||
def test_rescore_problem_single(self, act):
|
||||
""" Test rescoring of a single student. """
|
||||
url = reverse('rescore_problem', kwargs={'course_id': self.course.id})
|
||||
url = reverse('rescore_problem', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {
|
||||
'problem_to_reset': self.problem_urlname,
|
||||
'unique_student_identifier': self.student.email,
|
||||
})
|
||||
print response.content
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue(act.called)
|
||||
|
||||
@patch.object(instructor_task.api, 'submit_rescore_problem_for_student')
|
||||
def test_rescore_problem_single_from_uname(self, act):
|
||||
""" Test rescoring of a single student. """
|
||||
url = reverse('rescore_problem', kwargs={'course_id': self.course.id})
|
||||
url = reverse('rescore_problem', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {
|
||||
'problem_to_reset': self.problem_urlname,
|
||||
'unique_student_identifier': self.student.username,
|
||||
})
|
||||
print response.content
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue(act.called)
|
||||
|
||||
@patch.object(instructor_task.api, 'submit_rescore_problem_for_all_students')
|
||||
def test_rescore_problem_all(self, act):
|
||||
""" Test rescoring for all students. """
|
||||
url = reverse('rescore_problem', kwargs={'course_id': self.course.id})
|
||||
url = reverse('rescore_problem', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {
|
||||
'problem_to_reset': self.problem_urlname,
|
||||
'all_students': True,
|
||||
})
|
||||
print response.content
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue(act.called)
|
||||
|
||||
@@ -1578,7 +1611,7 @@ class TestInstructorSendEmail(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
"""
|
||||
def setUp(self):
|
||||
self.course = CourseFactory.create()
|
||||
self.instructor = InstructorFactory(course=self.course.location)
|
||||
self.instructor = InstructorFactory(course=self.course.id)
|
||||
self.client.login(username=self.instructor.username, password='test')
|
||||
test_subject = u'\u1234 test subject'
|
||||
test_message = u'\u6824 test message'
|
||||
@@ -1589,13 +1622,13 @@ class TestInstructorSendEmail(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
}
|
||||
|
||||
def test_send_email_as_logged_in_instructor(self):
|
||||
url = reverse('send_email', kwargs={'course_id': self.course.id})
|
||||
url = reverse('send_email', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.post(url, self.full_test_message)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_send_email_but_not_logged_in(self):
|
||||
self.client.logout()
|
||||
url = reverse('send_email', kwargs={'course_id': self.course.id})
|
||||
url = reverse('send_email', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.post(url, self.full_test_message)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
@@ -1603,7 +1636,7 @@ class TestInstructorSendEmail(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
self.client.logout()
|
||||
student = UserFactory()
|
||||
self.client.login(username=student.username, password='test')
|
||||
url = reverse('send_email', kwargs={'course_id': self.course.id})
|
||||
url = reverse('send_email', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.post(url, self.full_test_message)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
@@ -1613,7 +1646,7 @@ class TestInstructorSendEmail(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
self.assertNotEqual(response.status_code, 200)
|
||||
|
||||
def test_send_email_no_sendto(self):
|
||||
url = reverse('send_email', kwargs={'course_id': self.course.id})
|
||||
url = reverse('send_email', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.post(url, {
|
||||
'subject': 'test subject',
|
||||
'message': 'test message',
|
||||
@@ -1621,7 +1654,7 @@ class TestInstructorSendEmail(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_send_email_no_subject(self):
|
||||
url = reverse('send_email', kwargs={'course_id': self.course.id})
|
||||
url = reverse('send_email', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.post(url, {
|
||||
'send_to': 'staff',
|
||||
'message': 'test message',
|
||||
@@ -1629,7 +1662,7 @@ class TestInstructorSendEmail(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_send_email_no_message(self):
|
||||
url = reverse('send_email', kwargs={'course_id': self.course.id})
|
||||
url = reverse('send_email', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.post(url, {
|
||||
'send_to': 'staff',
|
||||
'subject': 'test subject',
|
||||
@@ -1700,20 +1733,22 @@ class TestInstructorAPITaskLists(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.course = CourseFactory.create()
|
||||
self.instructor = InstructorFactory(course=self.course.location)
|
||||
self.instructor = InstructorFactory(course=self.course.id)
|
||||
self.client.login(username=self.instructor.username, password='test')
|
||||
|
||||
self.student = UserFactory()
|
||||
CourseEnrollment.enroll(self.student, self.course.id)
|
||||
|
||||
self.problem_urlname = 'robot-some-problem-urlname'
|
||||
self.problem_location = msk_from_problem_urlname(
|
||||
self.course.id,
|
||||
'robot-some-problem-urlname'
|
||||
)
|
||||
self.problem_urlname = str(self.problem_location)
|
||||
|
||||
self.module = StudentModule.objects.create(
|
||||
student=self.student,
|
||||
course_id=self.course.id,
|
||||
module_state_key=_msk_from_problem_urlname(
|
||||
self.course.id,
|
||||
self.problem_urlname
|
||||
),
|
||||
module_id=self.problem_location,
|
||||
state=json.dumps({'attempts': 10}),
|
||||
)
|
||||
mock_factory = MockCompletionInfo()
|
||||
@@ -1730,7 +1765,7 @@ class TestInstructorAPITaskLists(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
def test_list_instructor_tasks_running(self, act):
|
||||
""" Test list of all running tasks. """
|
||||
act.return_value = self.tasks
|
||||
url = reverse('list_instructor_tasks', kwargs={'course_id': self.course.id})
|
||||
url = reverse('list_instructor_tasks', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
mock_factory = MockCompletionInfo()
|
||||
with patch('instructor.views.api.get_task_completion_info') as mock_completion_info:
|
||||
mock_completion_info.side_effect = mock_factory.mock_get_task_completion_info
|
||||
@@ -1749,7 +1784,7 @@ class TestInstructorAPITaskLists(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
def test_list_background_email_tasks(self, act):
|
||||
"""Test list of background email tasks."""
|
||||
act.return_value = self.tasks
|
||||
url = reverse('list_background_email_tasks', kwargs={'course_id': self.course.id})
|
||||
url = reverse('list_background_email_tasks', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
mock_factory = MockCompletionInfo()
|
||||
with patch('instructor.views.api.get_task_completion_info') as mock_completion_info:
|
||||
mock_completion_info.side_effect = mock_factory.mock_get_task_completion_info
|
||||
@@ -1768,12 +1803,12 @@ class TestInstructorAPITaskLists(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
def test_list_instructor_tasks_problem(self, act):
|
||||
""" Test list task history for problem. """
|
||||
act.return_value = self.tasks
|
||||
url = reverse('list_instructor_tasks', kwargs={'course_id': self.course.id})
|
||||
url = reverse('list_instructor_tasks', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
mock_factory = MockCompletionInfo()
|
||||
with patch('instructor.views.api.get_task_completion_info') as mock_completion_info:
|
||||
mock_completion_info.side_effect = mock_factory.mock_get_task_completion_info
|
||||
response = self.client.get(url, {
|
||||
'problem_urlname': self.problem_urlname,
|
||||
'problem_location_str': self.problem_urlname,
|
||||
})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@@ -1789,12 +1824,12 @@ class TestInstructorAPITaskLists(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
def test_list_instructor_tasks_problem_student(self, act):
|
||||
""" Test list task history for problem AND student. """
|
||||
act.return_value = self.tasks
|
||||
url = reverse('list_instructor_tasks', kwargs={'course_id': self.course.id})
|
||||
url = reverse('list_instructor_tasks', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
mock_factory = MockCompletionInfo()
|
||||
with patch('instructor.views.api.get_task_completion_info') as mock_completion_info:
|
||||
mock_completion_info.side_effect = mock_factory.mock_get_task_completion_info
|
||||
response = self.client.get(url, {
|
||||
'problem_urlname': self.problem_urlname,
|
||||
'problem_location_str': self.problem_urlname,
|
||||
'unique_student_identifier': self.student.email,
|
||||
})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
@@ -1831,7 +1866,7 @@ class TestInstructorAPIAnalyticsProxy(ModuleStoreTestCase, LoginEnrollmentTestCa
|
||||
|
||||
def setUp(self):
|
||||
self.course = CourseFactory.create()
|
||||
self.instructor = InstructorFactory(course=self.course.location)
|
||||
self.instructor = InstructorFactory(course=self.course.id)
|
||||
self.client.login(username=self.instructor.username, password='test')
|
||||
|
||||
@patch.object(instructor.views.api.requests, 'get')
|
||||
@@ -1839,18 +1874,17 @@ class TestInstructorAPIAnalyticsProxy(ModuleStoreTestCase, LoginEnrollmentTestCa
|
||||
""" Test legacy analytics proxy url generation. """
|
||||
act.return_value = self.FakeProxyResponse()
|
||||
|
||||
url = reverse('proxy_legacy_analytics', kwargs={'course_id': self.course.id})
|
||||
url = reverse('proxy_legacy_analytics', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {
|
||||
'aname': 'ProblemGradeDistribution'
|
||||
})
|
||||
print response.content
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# check request url
|
||||
expected_url = "{url}get?aname={aname}&course_id={course_id}&apikey={api_key}".format(
|
||||
expected_url = "{url}get?aname={aname}&course_id={course_id!s}&apikey={api_key}".format(
|
||||
url="http://robotanalyticsserver.netbot:900/",
|
||||
aname="ProblemGradeDistribution",
|
||||
course_id=self.course.id,
|
||||
course_id=self.course.id.to_deprecated_string(),
|
||||
api_key="robot_api_key",
|
||||
)
|
||||
act.assert_called_once_with(expected_url)
|
||||
@@ -1862,11 +1896,10 @@ class TestInstructorAPIAnalyticsProxy(ModuleStoreTestCase, LoginEnrollmentTestCa
|
||||
"""
|
||||
act.return_value = self.FakeProxyResponse()
|
||||
|
||||
url = reverse('proxy_legacy_analytics', kwargs={'course_id': self.course.id})
|
||||
url = reverse('proxy_legacy_analytics', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {
|
||||
'aname': 'ProblemGradeDistribution'
|
||||
})
|
||||
print response.content
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# check response
|
||||
@@ -1879,11 +1912,10 @@ class TestInstructorAPIAnalyticsProxy(ModuleStoreTestCase, LoginEnrollmentTestCa
|
||||
""" Test proxy when server reponds with failure. """
|
||||
act.return_value = self.FakeBadProxyResponse()
|
||||
|
||||
url = reverse('proxy_legacy_analytics', kwargs={'course_id': self.course.id})
|
||||
url = reverse('proxy_legacy_analytics', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {
|
||||
'aname': 'ProblemGradeDistribution'
|
||||
})
|
||||
print response.content
|
||||
self.assertEqual(response.status_code, 500)
|
||||
|
||||
@patch.object(instructor.views.api.requests, 'get')
|
||||
@@ -1891,9 +1923,8 @@ class TestInstructorAPIAnalyticsProxy(ModuleStoreTestCase, LoginEnrollmentTestCa
|
||||
""" Test proxy when missing the aname query parameter. """
|
||||
act.return_value = self.FakeProxyResponse()
|
||||
|
||||
url = reverse('proxy_legacy_analytics', kwargs={'course_id': self.course.id})
|
||||
url = reverse('proxy_legacy_analytics', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {})
|
||||
print response.content
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertFalse(act.called)
|
||||
|
||||
@@ -1917,31 +1948,15 @@ class TestInstructorAPIHelpers(TestCase):
|
||||
self.assertEqual(_split_input_list(scary_unistuff), [scary_unistuff])
|
||||
|
||||
def test_msk_from_problem_urlname(self):
|
||||
course_id = 'RobotU/Robots101/3001_Spring'
|
||||
capa_urlname = 'capa_urlname'
|
||||
capa_urlname_xml = 'capa_urlname.xml'
|
||||
xblock_urlname = 'notaproblem/someothername'
|
||||
xblock_urlname_xml = 'notaproblem/someothername.xml'
|
||||
|
||||
capa_msk = 'i4x://RobotU/Robots101/problem/capa_urlname'
|
||||
xblock_msk = 'i4x://RobotU/Robots101/notaproblem/someothername'
|
||||
|
||||
for urlname in [capa_urlname, capa_urlname_xml]:
|
||||
self.assertEqual(
|
||||
_msk_from_problem_urlname(course_id, urlname),
|
||||
capa_msk
|
||||
)
|
||||
|
||||
for urlname in [xblock_urlname, xblock_urlname_xml]:
|
||||
self.assertEqual(
|
||||
_msk_from_problem_urlname(course_id, urlname),
|
||||
xblock_msk
|
||||
)
|
||||
course_id = SlashSeparatedCourseKey('MITx', '6.002x', '2013_Spring')
|
||||
name = 'L2Node1'
|
||||
output = 'i4x://MITx/6.002x/problem/L2Node1'
|
||||
self.assertEqual(msk_from_problem_urlname(course_id, name).to_deprecated_string(), output)
|
||||
|
||||
@raises(ValueError)
|
||||
def test_msk_from_problem_urlname_error(self):
|
||||
args = ('notagoodcourse', 'L2Node1')
|
||||
_msk_from_problem_urlname(*args)
|
||||
msk_from_problem_urlname(*args)
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
|
||||
@@ -1959,60 +1974,60 @@ class TestDueDateExtensions(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
week1 = ItemFactory.create(due=due)
|
||||
week2 = ItemFactory.create(due=due)
|
||||
week3 = ItemFactory.create(due=due)
|
||||
course.children = [week1.location.url(), week2.location.url(),
|
||||
week3.location.url()]
|
||||
course.children = [week1.location.to_deprecated_string(), week2.location.to_deprecated_string(),
|
||||
week3.location.to_deprecated_string()]
|
||||
|
||||
homework = ItemFactory.create(
|
||||
parent_location=week1.location,
|
||||
due=due
|
||||
)
|
||||
week1.children = [homework.location.url()]
|
||||
week1.children = [homework.location.to_deprecated_string()]
|
||||
|
||||
user1 = UserFactory.create()
|
||||
StudentModule(
|
||||
state='{}',
|
||||
student_id=user1.id,
|
||||
course_id=course.id,
|
||||
module_state_key=week1.location.url()).save()
|
||||
module_id=week1.location).save()
|
||||
StudentModule(
|
||||
state='{}',
|
||||
student_id=user1.id,
|
||||
course_id=course.id,
|
||||
module_state_key=week2.location.url()).save()
|
||||
module_id=week2.location).save()
|
||||
StudentModule(
|
||||
state='{}',
|
||||
student_id=user1.id,
|
||||
course_id=course.id,
|
||||
module_state_key=week3.location.url()).save()
|
||||
module_id=week3.location).save()
|
||||
StudentModule(
|
||||
state='{}',
|
||||
student_id=user1.id,
|
||||
course_id=course.id,
|
||||
module_state_key=homework.location.url()).save()
|
||||
module_id=homework.location).save()
|
||||
|
||||
user2 = UserFactory.create()
|
||||
StudentModule(
|
||||
state='{}',
|
||||
student_id=user2.id,
|
||||
course_id=course.id,
|
||||
module_state_key=week1.location.url()).save()
|
||||
module_id=week1.location).save()
|
||||
StudentModule(
|
||||
state='{}',
|
||||
student_id=user2.id,
|
||||
course_id=course.id,
|
||||
module_state_key=homework.location.url()).save()
|
||||
module_id=homework.location).save()
|
||||
|
||||
user3 = UserFactory.create()
|
||||
StudentModule(
|
||||
state='{}',
|
||||
student_id=user3.id,
|
||||
course_id=course.id,
|
||||
module_state_key=week1.location.url()).save()
|
||||
module_id=week1.location).save()
|
||||
StudentModule(
|
||||
state='{}',
|
||||
student_id=user3.id,
|
||||
course_id=course.id,
|
||||
module_state_key=homework.location.url()).save()
|
||||
module_id=homework.location).save()
|
||||
|
||||
self.course = course
|
||||
self.week1 = week1
|
||||
@@ -2021,14 +2036,14 @@ class TestDueDateExtensions(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
self.user1 = user1
|
||||
self.user2 = user2
|
||||
|
||||
self.instructor = InstructorFactory(course=course.location)
|
||||
self.instructor = InstructorFactory(course=course.id)
|
||||
self.client.login(username=self.instructor.username, password='test')
|
||||
|
||||
def test_change_due_date(self):
|
||||
url = reverse('change_due_date', kwargs={'course_id': self.course.id})
|
||||
url = reverse('change_due_date', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {
|
||||
'student': self.user1.username,
|
||||
'url': self.week1.location.url(),
|
||||
'url': self.week1.location.to_deprecated_string(),
|
||||
'due_datetime': '12/30/2013 00:00'
|
||||
})
|
||||
self.assertEqual(response.status_code, 200, response.content)
|
||||
@@ -2037,10 +2052,10 @@ class TestDueDateExtensions(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
|
||||
def test_reset_date(self):
|
||||
self.test_change_due_date()
|
||||
url = reverse('reset_due_date', kwargs={'course_id': self.course.id})
|
||||
url = reverse('reset_due_date', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {
|
||||
'student': self.user1.username,
|
||||
'url': self.week1.location.url(),
|
||||
'url': self.week1.location.to_deprecated_string(),
|
||||
})
|
||||
self.assertEqual(response.status_code, 200, response.content)
|
||||
self.assertEqual(None,
|
||||
@@ -2049,8 +2064,8 @@ class TestDueDateExtensions(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
def test_show_unit_extensions(self):
|
||||
self.test_change_due_date()
|
||||
url = reverse('show_unit_extensions',
|
||||
kwargs={'course_id': self.course.id})
|
||||
response = self.client.get(url, {'url': self.week1.location.url()})
|
||||
kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {'url': self.week1.location.to_deprecated_string()})
|
||||
self.assertEqual(response.status_code, 200, response.content)
|
||||
self.assertEqual(json.loads(response.content), {
|
||||
u'data': [{u'Extended Due Date': u'2013-12-30 00:00',
|
||||
@@ -2063,7 +2078,7 @@ class TestDueDateExtensions(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
def test_show_student_extensions(self):
|
||||
self.test_change_due_date()
|
||||
url = reverse('show_student_extensions',
|
||||
kwargs={'course_id': self.course.id})
|
||||
kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.get(url, {'student': self.user1.username})
|
||||
self.assertEqual(response.status_code, 200, response.content)
|
||||
self.assertEqual(json.loads(response.content), {
|
||||
|
||||
@@ -18,6 +18,7 @@ from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE
|
||||
from mock import patch
|
||||
|
||||
from bulk_email.models import CourseAuthorization
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
|
||||
@@ -34,7 +35,7 @@ class TestNewInstructorDashboardEmailViewMongoBacked(ModuleStoreTestCase):
|
||||
self.client.login(username=instructor.username, password="test")
|
||||
|
||||
# URL for instructor dash
|
||||
self.url = reverse('instructor_dashboard_2', kwargs={'course_id': self.course.id})
|
||||
self.url = reverse('instructor_dashboard_2', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
# URL for email view
|
||||
self.email_link = '<a href="" data-section="send_email">Email</a>'
|
||||
|
||||
@@ -115,14 +116,14 @@ class TestNewInstructorDashboardEmailViewXMLBacked(ModuleStoreTestCase):
|
||||
Check for email view on the new instructor dashboard
|
||||
"""
|
||||
def setUp(self):
|
||||
self.course_name = 'edX/toy/2012_Fall'
|
||||
self.course_key = SlashSeparatedCourseKey('edX', 'toy', '2012_Fall')
|
||||
|
||||
# Create instructor account
|
||||
instructor = AdminFactory.create()
|
||||
self.client.login(username=instructor.username, password="test")
|
||||
|
||||
# URL for instructor dash
|
||||
self.url = reverse('instructor_dashboard_2', kwargs={'course_id': self.course_name})
|
||||
self.url = reverse('instructor_dashboard_2', kwargs={'course_id': self.course_key.to_deprecated_string()})
|
||||
# URL for email view
|
||||
self.email_link = '<a href="" data-section="send_email">Email</a>'
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ from courseware.models import StudentModule
|
||||
from django.conf import settings
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
from django.test.client import RequestFactory
|
||||
from student.tests.factories import UserFactory
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE
|
||||
@@ -22,15 +23,17 @@ from instructor.enrollment import (
|
||||
send_beta_role_email,
|
||||
unenroll_email
|
||||
)
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
|
||||
from submissions import api as sub_api
|
||||
from student.models import anonymous_id_for_user
|
||||
from .test_tools import msk_from_problem_urlname
|
||||
|
||||
|
||||
class TestSettableEnrollmentState(TestCase):
|
||||
""" Test the basis class for enrollment tests. """
|
||||
def setUp(self):
|
||||
self.course_id = 'robot:/a/fake/c::rse/id'
|
||||
self.course_key = SlashSeparatedCourseKey('Robot', 'fAKE', 'C-%-se-%-ID')
|
||||
|
||||
def test_mes_create(self):
|
||||
"""
|
||||
@@ -43,8 +46,8 @@ class TestSettableEnrollmentState(TestCase):
|
||||
auto_enroll=False
|
||||
)
|
||||
# enrollment objects
|
||||
eobjs = mes.create_user(self.course_id)
|
||||
ees = EmailEnrollmentState(self.course_id, eobjs.email)
|
||||
eobjs = mes.create_user(self.course_key)
|
||||
ees = EmailEnrollmentState(self.course_key, eobjs.email)
|
||||
self.assertEqual(mes, ees)
|
||||
|
||||
|
||||
@@ -60,7 +63,7 @@ class TestEnrollmentChangeBase(TestCase):
|
||||
__metaclass__ = ABCMeta
|
||||
|
||||
def setUp(self):
|
||||
self.course_id = 'robot:/a/fake/c::rse/id'
|
||||
self.course_key = SlashSeparatedCourseKey('Robot', 'fAKE', 'C-%-se-%-ID')
|
||||
|
||||
def _run_state_change_test(self, before_ideal, after_ideal, action):
|
||||
"""
|
||||
@@ -74,8 +77,8 @@ class TestEnrollmentChangeBase(TestCase):
|
||||
"""
|
||||
# initialize & check before
|
||||
print "checking initialization..."
|
||||
eobjs = before_ideal.create_user(self.course_id)
|
||||
before = EmailEnrollmentState(self.course_id, eobjs.email)
|
||||
eobjs = before_ideal.create_user(self.course_key)
|
||||
before = EmailEnrollmentState(self.course_key, eobjs.email)
|
||||
self.assertEqual(before, before_ideal)
|
||||
|
||||
# do action
|
||||
@@ -84,7 +87,7 @@ class TestEnrollmentChangeBase(TestCase):
|
||||
|
||||
# check after
|
||||
print "checking effects..."
|
||||
after = EmailEnrollmentState(self.course_id, eobjs.email)
|
||||
after = EmailEnrollmentState(self.course_key, eobjs.email)
|
||||
self.assertEqual(after, after_ideal)
|
||||
|
||||
|
||||
@@ -105,7 +108,7 @@ class TestInstructorEnrollDB(TestEnrollmentChangeBase):
|
||||
auto_enroll=False
|
||||
)
|
||||
|
||||
action = lambda email: enroll_email(self.course_id, email)
|
||||
action = lambda email: enroll_email(self.course_key, email)
|
||||
|
||||
return self._run_state_change_test(before_ideal, after_ideal, action)
|
||||
|
||||
@@ -124,7 +127,7 @@ class TestInstructorEnrollDB(TestEnrollmentChangeBase):
|
||||
auto_enroll=False,
|
||||
)
|
||||
|
||||
action = lambda email: enroll_email(self.course_id, email)
|
||||
action = lambda email: enroll_email(self.course_key, email)
|
||||
|
||||
return self._run_state_change_test(before_ideal, after_ideal, action)
|
||||
|
||||
@@ -143,7 +146,7 @@ class TestInstructorEnrollDB(TestEnrollmentChangeBase):
|
||||
auto_enroll=False,
|
||||
)
|
||||
|
||||
action = lambda email: enroll_email(self.course_id, email)
|
||||
action = lambda email: enroll_email(self.course_key, email)
|
||||
|
||||
return self._run_state_change_test(before_ideal, after_ideal, action)
|
||||
|
||||
@@ -162,7 +165,7 @@ class TestInstructorEnrollDB(TestEnrollmentChangeBase):
|
||||
auto_enroll=False,
|
||||
)
|
||||
|
||||
action = lambda email: enroll_email(self.course_id, email)
|
||||
action = lambda email: enroll_email(self.course_key, email)
|
||||
|
||||
return self._run_state_change_test(before_ideal, after_ideal, action)
|
||||
|
||||
@@ -181,7 +184,7 @@ class TestInstructorEnrollDB(TestEnrollmentChangeBase):
|
||||
auto_enroll=True,
|
||||
)
|
||||
|
||||
action = lambda email: enroll_email(self.course_id, email, auto_enroll=True)
|
||||
action = lambda email: enroll_email(self.course_key, email, auto_enroll=True)
|
||||
|
||||
return self._run_state_change_test(before_ideal, after_ideal, action)
|
||||
|
||||
@@ -200,7 +203,7 @@ class TestInstructorEnrollDB(TestEnrollmentChangeBase):
|
||||
auto_enroll=False,
|
||||
)
|
||||
|
||||
action = lambda email: enroll_email(self.course_id, email, auto_enroll=False)
|
||||
action = lambda email: enroll_email(self.course_key, email, auto_enroll=False)
|
||||
|
||||
return self._run_state_change_test(before_ideal, after_ideal, action)
|
||||
|
||||
@@ -222,7 +225,7 @@ class TestInstructorUnenrollDB(TestEnrollmentChangeBase):
|
||||
auto_enroll=False
|
||||
)
|
||||
|
||||
action = lambda email: unenroll_email(self.course_id, email)
|
||||
action = lambda email: unenroll_email(self.course_key, email)
|
||||
|
||||
return self._run_state_change_test(before_ideal, after_ideal, action)
|
||||
|
||||
@@ -241,7 +244,7 @@ class TestInstructorUnenrollDB(TestEnrollmentChangeBase):
|
||||
auto_enroll=False
|
||||
)
|
||||
|
||||
action = lambda email: unenroll_email(self.course_id, email)
|
||||
action = lambda email: unenroll_email(self.course_key, email)
|
||||
|
||||
return self._run_state_change_test(before_ideal, after_ideal, action)
|
||||
|
||||
@@ -260,7 +263,7 @@ class TestInstructorUnenrollDB(TestEnrollmentChangeBase):
|
||||
auto_enroll=False
|
||||
)
|
||||
|
||||
action = lambda email: unenroll_email(self.course_id, email)
|
||||
action = lambda email: unenroll_email(self.course_key, email)
|
||||
|
||||
return self._run_state_change_test(before_ideal, after_ideal, action)
|
||||
|
||||
@@ -279,58 +282,66 @@ class TestInstructorUnenrollDB(TestEnrollmentChangeBase):
|
||||
auto_enroll=False
|
||||
)
|
||||
|
||||
action = lambda email: unenroll_email(self.course_id, email)
|
||||
action = lambda email: unenroll_email(self.course_key, email)
|
||||
|
||||
return self._run_state_change_test(before_ideal, after_ideal, action)
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
|
||||
class TestInstructorEnrollmentStudentModule(TestCase):
|
||||
""" Test student module manipulations. """
|
||||
def setUp(self):
|
||||
self.course_id = 'robot:/a/fake/c::rse/id'
|
||||
self.course_key = SlashSeparatedCourseKey('fake', 'course', 'id')
|
||||
|
||||
def test_reset_student_attempts(self):
|
||||
user = UserFactory()
|
||||
msk = 'robot/module/state/key'
|
||||
msk = self.course_key.make_usage_key('dummy', 'module')
|
||||
original_state = json.dumps({'attempts': 32, 'otherstuff': 'alsorobots'})
|
||||
module = StudentModule.objects.create(student=user, course_id=self.course_id, module_state_key=msk, state=original_state)
|
||||
StudentModule.objects.create(student=user, course_id=self.course_key, module_id=msk, state=original_state)
|
||||
# lambda to reload the module state from the database
|
||||
module = lambda: StudentModule.objects.get(student=user, course_id=self.course_id, module_state_key=msk)
|
||||
module = lambda: StudentModule.objects.get(student=user, course_id=self.course_key, module_id=msk)
|
||||
self.assertEqual(json.loads(module().state)['attempts'], 32)
|
||||
reset_student_attempts(self.course_id, user, msk)
|
||||
reset_student_attempts(self.course_key, user, msk)
|
||||
self.assertEqual(json.loads(module().state)['attempts'], 0)
|
||||
|
||||
def test_delete_student_attempts(self):
|
||||
user = UserFactory()
|
||||
msk = 'robot/module/state/key'
|
||||
msk = self.course_key.make_usage_key('dummy', 'module')
|
||||
original_state = json.dumps({'attempts': 32, 'otherstuff': 'alsorobots'})
|
||||
StudentModule.objects.create(student=user, course_id=self.course_id, module_state_key=msk, state=original_state)
|
||||
self.assertEqual(StudentModule.objects.filter(student=user, course_id=self.course_id, module_state_key=msk).count(), 1)
|
||||
reset_student_attempts(self.course_id, user, msk, delete_module=True)
|
||||
self.assertEqual(StudentModule.objects.filter(student=user, course_id=self.course_id, module_state_key=msk).count(), 0)
|
||||
StudentModule.objects.create(student=user, course_id=self.course_key, module_id=msk, state=original_state)
|
||||
self.assertEqual(StudentModule.objects.filter(student=user, course_id=self.course_key, module_id=msk).count(), 1)
|
||||
reset_student_attempts(self.course_key, user, msk, delete_module=True)
|
||||
self.assertEqual(StudentModule.objects.filter(student=user, course_id=self.course_key, module_id=msk).count(), 0)
|
||||
|
||||
def test_delete_submission_scores(self):
|
||||
user = UserFactory()
|
||||
course_id = 'ora2/1/1'
|
||||
item_id = 'i4x://ora2/1/openassessment/b3dce2586c9c4876b73e7f390e42ef8f'
|
||||
course = CourseFactory.create()
|
||||
problem_location = msk_from_problem_urlname(
|
||||
course.id,
|
||||
'b3dce2586c9c4876b73e7f390e42ef8f',
|
||||
block_type='openassessment'
|
||||
)
|
||||
|
||||
# Create a student module for the user
|
||||
StudentModule.objects.create(
|
||||
student=user, course_id=course_id, module_state_key=item_id, state=json.dumps({})
|
||||
student=user,
|
||||
course_id=course.id,
|
||||
module_state_key=problem_location,
|
||||
state=json.dumps({})
|
||||
)
|
||||
|
||||
# Create a submission and score for the student using the submissions API
|
||||
student_item = {
|
||||
'student_id': anonymous_id_for_user(user, course_id),
|
||||
'course_id': course_id,
|
||||
'item_id': item_id,
|
||||
'student_id': anonymous_id_for_user(user, course.id),
|
||||
'course_id': course.id,
|
||||
'item_id': problem_location,
|
||||
'item_type': 'openassessment'
|
||||
}
|
||||
submission = sub_api.create_submission(student_item, 'test answer')
|
||||
sub_api.set_score(submission['uuid'], 1, 2)
|
||||
|
||||
# Delete student state using the instructor dash
|
||||
reset_student_attempts(course_id, user, item_id, delete_module=True)
|
||||
reset_student_attempts(course.id, user, problem_location, delete_module=True)
|
||||
|
||||
# Verify that the student's scores have been reset in the submissions API
|
||||
score = sub_api.get_score(student_item)
|
||||
@@ -436,7 +447,7 @@ class TestGetEmailParams(TestCase):
|
||||
site = settings.SITE_NAME
|
||||
self.course_url = u'https://{}/courses/{}/'.format(
|
||||
site,
|
||||
self.course.id
|
||||
self.course.id.to_deprecated_string()
|
||||
)
|
||||
self.course_about_url = self.course_url + 'about'
|
||||
self.registration_url = u'https://{}/register'.format(
|
||||
|
||||
@@ -26,8 +26,8 @@ class HintManagerTest(ModuleStoreTestCase):
|
||||
self.user = UserFactory.create(username='robot', email='robot@edx.org', password='test', is_staff=True)
|
||||
self.c = Client()
|
||||
self.c.login(username='robot', password='test')
|
||||
self.problem_id = 'i4x://Me/19.002/crowdsource_hinter/crowdsource_hinter_001'
|
||||
self.course_id = 'Me/19.002/test_course'
|
||||
self.course_id = self.course.id
|
||||
self.problem_id = self.course_id.make_usage_key('crowdsource_hinter', 'crowdsource_hinter_001')
|
||||
UserStateSummaryFactory.create(field_name='hints',
|
||||
usage_id=self.problem_id,
|
||||
value=json.dumps({'1.0': {'1': ['Hint 1', 2],
|
||||
@@ -60,7 +60,7 @@ class HintManagerTest(ModuleStoreTestCase):
|
||||
"""
|
||||
Makes sure that staff can access the hint management view.
|
||||
"""
|
||||
out = self.c.get('/courses/Me/19.002/test_course/hint_manager')
|
||||
out = self.c.get(self.url)
|
||||
print out
|
||||
self.assertTrue('Hints Awaiting Moderation' in out.content)
|
||||
|
||||
@@ -115,7 +115,7 @@ class HintManagerTest(ModuleStoreTestCase):
|
||||
request = RequestFactory()
|
||||
post = request.post(self.url, {'field': 'hints',
|
||||
'op': 'delete hints',
|
||||
1: [self.problem_id, '1.0', '1']})
|
||||
1: [self.problem_id.to_deprecated_string(), '1.0', '1']})
|
||||
view.delete_hints(post, self.course_id, 'hints')
|
||||
problem_hints = XModuleUserStateSummaryField.objects.get(field_name='hints', usage_id=self.problem_id).value
|
||||
self.assertTrue('1' not in json.loads(problem_hints)['1.0'])
|
||||
@@ -127,7 +127,7 @@ class HintManagerTest(ModuleStoreTestCase):
|
||||
request = RequestFactory()
|
||||
post = request.post(self.url, {'field': 'hints',
|
||||
'op': 'change votes',
|
||||
1: [self.problem_id, '1.0', '1', 5]})
|
||||
1: [self.problem_id.to_deprecated_string(), '1.0', '1', 5]})
|
||||
view.change_votes(post, self.course_id, 'hints')
|
||||
problem_hints = XModuleUserStateSummaryField.objects.get(field_name='hints', usage_id=self.problem_id).value
|
||||
# hints[answer][hint_pk (string)] = [hint text, vote count]
|
||||
@@ -146,7 +146,7 @@ class HintManagerTest(ModuleStoreTestCase):
|
||||
request = RequestFactory()
|
||||
post = request.post(self.url, {'field': 'mod_queue',
|
||||
'op': 'add hint',
|
||||
'problem': self.problem_id,
|
||||
'problem': self.problem_id.to_deprecated_string(),
|
||||
'answer': '3.14',
|
||||
'hint': 'This is a new hint.'})
|
||||
post.user = 'fake user'
|
||||
@@ -167,7 +167,7 @@ class HintManagerTest(ModuleStoreTestCase):
|
||||
request = RequestFactory()
|
||||
post = request.post(self.url, {'field': 'mod_queue',
|
||||
'op': 'add hint',
|
||||
'problem': self.problem_id,
|
||||
'problem': self.problem_id.to_deprecated_string(),
|
||||
'answer': 'fish',
|
||||
'hint': 'This is a new hint.'})
|
||||
post.user = 'fake user'
|
||||
@@ -185,7 +185,7 @@ class HintManagerTest(ModuleStoreTestCase):
|
||||
request = RequestFactory()
|
||||
post = request.post(self.url, {'field': 'mod_queue',
|
||||
'op': 'approve',
|
||||
1: [self.problem_id, '2.0', '2']})
|
||||
1: [self.problem_id.to_deprecated_string(), '2.0', '2']})
|
||||
view.approve(post, self.course_id, 'mod_queue')
|
||||
problem_hints = XModuleUserStateSummaryField.objects.get(field_name='mod_queue', usage_id=self.problem_id).value
|
||||
self.assertTrue('2.0' not in json.loads(problem_hints) or len(json.loads(problem_hints)['2.0']) == 0)
|
||||
|
||||
@@ -20,6 +20,7 @@ from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE
|
||||
from student.roles import CourseStaffRole
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.django import modulestore, clear_existing_modulestores
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
|
||||
from mock import patch
|
||||
|
||||
@@ -33,7 +34,7 @@ class TestInstructorDashboardAnonCSV(ModuleStoreTestCase, LoginEnrollmentTestCas
|
||||
# Note -- I copied this setUp from a similar test
|
||||
def setUp(self):
|
||||
clear_existing_modulestores()
|
||||
self.toy = modulestore().get_course("edX/toy/2012_Fall")
|
||||
self.toy = modulestore().get_course(SlashSeparatedCourseKey("edX", "toy", "2012_Fall"))
|
||||
|
||||
# Create two accounts
|
||||
self.student = 'view@test.com'
|
||||
@@ -44,7 +45,7 @@ class TestInstructorDashboardAnonCSV(ModuleStoreTestCase, LoginEnrollmentTestCas
|
||||
self.activate_user(self.student)
|
||||
self.activate_user(self.instructor)
|
||||
|
||||
CourseStaffRole(self.toy.location).add_users(User.objects.get(email=self.instructor))
|
||||
CourseStaffRole(self.toy.id).add_users(User.objects.get(email=self.instructor))
|
||||
|
||||
self.logout()
|
||||
self.login(self.instructor, self.password)
|
||||
@@ -52,7 +53,7 @@ class TestInstructorDashboardAnonCSV(ModuleStoreTestCase, LoginEnrollmentTestCas
|
||||
|
||||
def test_download_anon_csv(self):
|
||||
course = self.toy
|
||||
url = reverse('instructor_dashboard', kwargs={'course_id': course.id})
|
||||
url = reverse('instructor_dashboard', kwargs={'course_id': course.id.to_deprecated_string()})
|
||||
|
||||
with patch('instructor.views.legacy.unique_id_for_user') as mock_unique:
|
||||
mock_unique.return_value = 42
|
||||
|
||||
@@ -20,6 +20,7 @@ from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE
|
||||
from student.roles import CourseStaffRole
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.django import modulestore, clear_existing_modulestores
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
|
||||
@@ -30,7 +31,7 @@ class TestInstructorDashboardGradeDownloadCSV(ModuleStoreTestCase, LoginEnrollme
|
||||
|
||||
def setUp(self):
|
||||
clear_existing_modulestores()
|
||||
self.toy = modulestore().get_course("edX/toy/2012_Fall")
|
||||
self.toy = modulestore().get_course(SlashSeparatedCourseKey("edX", "toy", "2012_Fall"))
|
||||
|
||||
# Create two accounts
|
||||
self.student = 'view@test.com'
|
||||
@@ -41,7 +42,7 @@ class TestInstructorDashboardGradeDownloadCSV(ModuleStoreTestCase, LoginEnrollme
|
||||
self.activate_user(self.student)
|
||||
self.activate_user(self.instructor)
|
||||
|
||||
CourseStaffRole(self.toy.location).add_users(User.objects.get(email=self.instructor))
|
||||
CourseStaffRole(self.toy.id).add_users(User.objects.get(email=self.instructor))
|
||||
|
||||
self.logout()
|
||||
self.login(self.instructor, self.password)
|
||||
@@ -49,7 +50,7 @@ class TestInstructorDashboardGradeDownloadCSV(ModuleStoreTestCase, LoginEnrollme
|
||||
|
||||
def test_download_grades_csv(self):
|
||||
course = self.toy
|
||||
url = reverse('instructor_dashboard', kwargs={'course_id': course.id})
|
||||
url = reverse('instructor_dashboard', kwargs={'course_id': course.id.to_deprecated_string()})
|
||||
msg = "url = {0}\n".format(url)
|
||||
response = self.client.post(url, {'action': 'Download CSV of all student grades for this course'})
|
||||
msg += "instructor dashboard download csv grades: response = '{0}'\n".format(response)
|
||||
@@ -58,7 +59,7 @@ class TestInstructorDashboardGradeDownloadCSV(ModuleStoreTestCase, LoginEnrollme
|
||||
|
||||
cdisp = response['Content-Disposition']
|
||||
msg += "Content-Disposition = '%s'\n" % cdisp
|
||||
self.assertEqual(cdisp, 'attachment; filename=grades_{0}.csv'.format(course.id), msg)
|
||||
self.assertEqual(cdisp, 'attachment; filename=grades_{0}.csv'.format(course.id.to_deprecated_string()), msg)
|
||||
|
||||
body = response.content.replace('\r', '')
|
||||
msg += "body = '{0}'\n".format(body)
|
||||
|
||||
@@ -32,7 +32,7 @@ class TestInstructorDashboardEmailView(ModuleStoreTestCase):
|
||||
self.client.login(username=instructor.username, password="test")
|
||||
|
||||
# URL for instructor dash
|
||||
self.url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id})
|
||||
self.url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
# URL for email view
|
||||
self.email_link = '<a href="#" onclick="goto(\'Email\')" class="None">Email</a>'
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ class TestInstructorEnrollsStudent(ModuleStoreTestCase, LoginEnrollmentTestCase)
|
||||
course = self.course
|
||||
|
||||
# Run the Un-enroll students command
|
||||
url = reverse('instructor_dashboard', kwargs={'course_id': course.id})
|
||||
url = reverse('instructor_dashboard', kwargs={'course_id': course.id.to_deprecated_string()})
|
||||
response = self.client.post(
|
||||
url,
|
||||
{
|
||||
@@ -84,7 +84,7 @@ class TestInstructorEnrollsStudent(ModuleStoreTestCase, LoginEnrollmentTestCase)
|
||||
course = self.course
|
||||
|
||||
# Run the Enroll students command
|
||||
url = reverse('instructor_dashboard', kwargs={'course_id': course.id})
|
||||
url = reverse('instructor_dashboard', kwargs={'course_id': course.id.to_deprecated_string()})
|
||||
response = self.client.post(url, {'action': 'Enroll multiple students', 'multiple_students': 'student1_1@test.com, student1_2@test.com', 'auto_enroll': 'on'})
|
||||
|
||||
# Check the page output
|
||||
@@ -129,7 +129,7 @@ class TestInstructorEnrollsStudent(ModuleStoreTestCase, LoginEnrollmentTestCase)
|
||||
|
||||
course = self.course
|
||||
|
||||
url = reverse('instructor_dashboard', kwargs={'course_id': course.id})
|
||||
url = reverse('instructor_dashboard', kwargs={'course_id': course.id.to_deprecated_string()})
|
||||
response = self.client.post(url, {'action': 'Enroll multiple students', 'multiple_students': 'student0@test.com', 'auto_enroll': 'on'})
|
||||
self.assertContains(response, '<td>student0@test.com</td>')
|
||||
self.assertContains(response, '<td>already enrolled</td>')
|
||||
@@ -142,7 +142,7 @@ class TestInstructorEnrollsStudent(ModuleStoreTestCase, LoginEnrollmentTestCase)
|
||||
course = self.course
|
||||
|
||||
# Run the Enroll students command
|
||||
url = reverse('instructor_dashboard', kwargs={'course_id': course.id})
|
||||
url = reverse('instructor_dashboard', kwargs={'course_id': course.id.to_deprecated_string()})
|
||||
response = self.client.post(url, {'action': 'Enroll multiple students', 'multiple_students': 'student2_1@test.com, student2_2@test.com'})
|
||||
|
||||
# Check the page output
|
||||
@@ -199,7 +199,7 @@ class TestInstructorEnrollsStudent(ModuleStoreTestCase, LoginEnrollmentTestCase)
|
||||
# Create activated, but not enrolled, user
|
||||
UserFactory.create(username="student3_0", email="student3_0@test.com", first_name='Autoenrolled')
|
||||
|
||||
url = reverse('instructor_dashboard', kwargs={'course_id': course.id})
|
||||
url = reverse('instructor_dashboard', kwargs={'course_id': course.id.to_deprecated_string()})
|
||||
response = self.client.post(url, {'action': 'Enroll multiple students', 'multiple_students': 'student3_0@test.com, student3_1@test.com, student3_2@test.com', 'auto_enroll': 'on', 'email_students': 'on'})
|
||||
|
||||
# Check the page output
|
||||
@@ -254,7 +254,7 @@ class TestInstructorEnrollsStudent(ModuleStoreTestCase, LoginEnrollmentTestCase)
|
||||
cea = CourseEnrollmentAllowed(email='student4_0@test.com', course_id=course.id)
|
||||
cea.save()
|
||||
|
||||
url = reverse('instructor_dashboard', kwargs={'course_id': course.id})
|
||||
url = reverse('instructor_dashboard', kwargs={'course_id': course.id.to_deprecated_string()})
|
||||
response = self.client.post(url, {'action': 'Unenroll multiple students', 'multiple_students': 'student4_0@test.com, student2@test.com, student3@test.com', 'email_students': 'on'})
|
||||
|
||||
# Check the page output
|
||||
@@ -301,7 +301,7 @@ class TestInstructorEnrollsStudent(ModuleStoreTestCase, LoginEnrollmentTestCase)
|
||||
# Create activated, but not enrolled, user
|
||||
UserFactory.create(username="student5_0", email="student5_0@test.com", first_name="ShibTest", last_name="Enrolled")
|
||||
|
||||
url = reverse('instructor_dashboard', kwargs={'course_id': course.id})
|
||||
url = reverse('instructor_dashboard', kwargs={'course_id': course.id.to_deprecated_string()})
|
||||
response = self.client.post(url, {'action': 'Enroll multiple students', 'multiple_students': 'student5_0@test.com, student5_1@test.com', 'auto_enroll': 'on', 'email_students': 'on'})
|
||||
|
||||
# Check the page output
|
||||
|
||||
@@ -17,6 +17,7 @@ from courseware.tests.helpers import LoginEnrollmentTestCase
|
||||
from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE
|
||||
from student.roles import CourseStaffRole
|
||||
from xmodule.modulestore.django import modulestore, clear_existing_modulestores
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
|
||||
|
||||
@@ -42,7 +43,7 @@ class TestInstructorDashboardForumAdmin(ModuleStoreTestCase, LoginEnrollmentTest
|
||||
clear_existing_modulestores()
|
||||
courses = modulestore().get_courses()
|
||||
|
||||
self.course_id = "edX/toy/2012_Fall"
|
||||
self.course_id = SlashSeparatedCourseKey("edX", "toy", "2012_Fall")
|
||||
self.toy = modulestore().get_course(self.course_id)
|
||||
|
||||
# Create two accounts
|
||||
@@ -54,7 +55,7 @@ class TestInstructorDashboardForumAdmin(ModuleStoreTestCase, LoginEnrollmentTest
|
||||
self.activate_user(self.student)
|
||||
self.activate_user(self.instructor)
|
||||
|
||||
CourseStaffRole(self.toy.location).add_users(User.objects.get(email=self.instructor))
|
||||
CourseStaffRole(self.toy.id).add_users(User.objects.get(email=self.instructor))
|
||||
|
||||
self.logout()
|
||||
self.login(self.instructor, self.password)
|
||||
@@ -67,7 +68,7 @@ class TestInstructorDashboardForumAdmin(ModuleStoreTestCase, LoginEnrollmentTest
|
||||
|
||||
def test_add_forum_admin_users_for_unknown_user(self):
|
||||
course = self.toy
|
||||
url = reverse('instructor_dashboard', kwargs={'course_id': course.id})
|
||||
url = reverse('instructor_dashboard', kwargs={'course_id': course.id.to_deprecated_string()})
|
||||
username = 'unknown'
|
||||
for action in ['Add', 'Remove']:
|
||||
for rolename in FORUM_ROLES:
|
||||
@@ -76,7 +77,7 @@ class TestInstructorDashboardForumAdmin(ModuleStoreTestCase, LoginEnrollmentTest
|
||||
|
||||
def test_add_forum_admin_users_for_missing_roles(self):
|
||||
course = self.toy
|
||||
url = reverse('instructor_dashboard', kwargs={'course_id': course.id})
|
||||
url = reverse('instructor_dashboard', kwargs={'course_id': course.id.to_deprecated_string()})
|
||||
username = 'u1'
|
||||
for action in ['Add', 'Remove']:
|
||||
for rolename in FORUM_ROLES:
|
||||
@@ -86,7 +87,7 @@ class TestInstructorDashboardForumAdmin(ModuleStoreTestCase, LoginEnrollmentTest
|
||||
def test_remove_forum_admin_users_for_missing_users(self):
|
||||
course = self.toy
|
||||
self.initialize_roles(course.id)
|
||||
url = reverse('instructor_dashboard', kwargs={'course_id': course.id})
|
||||
url = reverse('instructor_dashboard', kwargs={'course_id': course.id.to_deprecated_string()})
|
||||
username = 'u1'
|
||||
action = 'Remove'
|
||||
for rolename in FORUM_ROLES:
|
||||
@@ -96,20 +97,20 @@ class TestInstructorDashboardForumAdmin(ModuleStoreTestCase, LoginEnrollmentTest
|
||||
def test_add_and_remove_forum_admin_users(self):
|
||||
course = self.toy
|
||||
self.initialize_roles(course.id)
|
||||
url = reverse('instructor_dashboard', kwargs={'course_id': course.id})
|
||||
url = reverse('instructor_dashboard', kwargs={'course_id': course.id.to_deprecated_string()})
|
||||
username = 'u2'
|
||||
for rolename in FORUM_ROLES:
|
||||
response = self.client.post(url, {'action': action_name('Add', rolename), FORUM_ADMIN_USER[rolename]: username})
|
||||
self.assertTrue(response.content.find('Added "{0}" to "{1}" forum role = "{2}"'.format(username, course.id, rolename)) >= 0)
|
||||
self.assertContains(response, 'Added "{0}" to "{1}" forum role = "{2}"'.format(username, course.id.to_deprecated_string(), rolename))
|
||||
self.assertTrue(has_forum_access(username, course.id, rolename))
|
||||
response = self.client.post(url, {'action': action_name('Remove', rolename), FORUM_ADMIN_USER[rolename]: username})
|
||||
self.assertTrue(response.content.find('Removed "{0}" from "{1}" forum role = "{2}"'.format(username, course.id, rolename)) >= 0)
|
||||
self.assertContains(response, 'Removed "{0}" from "{1}" forum role = "{2}"'.format(username, course.id.to_deprecated_string(), rolename))
|
||||
self.assertFalse(has_forum_access(username, course.id, rolename))
|
||||
|
||||
def test_add_and_read_forum_admin_users(self):
|
||||
course = self.toy
|
||||
self.initialize_roles(course.id)
|
||||
url = reverse('instructor_dashboard', kwargs={'course_id': course.id})
|
||||
url = reverse('instructor_dashboard', kwargs={'course_id': course.id.to_deprecated_string()})
|
||||
username = 'u2'
|
||||
for rolename in FORUM_ROLES:
|
||||
# perform an add, and follow with a second identical add:
|
||||
@@ -121,7 +122,7 @@ class TestInstructorDashboardForumAdmin(ModuleStoreTestCase, LoginEnrollmentTest
|
||||
def test_add_nonstaff_forum_admin_users(self):
|
||||
course = self.toy
|
||||
self.initialize_roles(course.id)
|
||||
url = reverse('instructor_dashboard', kwargs={'course_id': course.id})
|
||||
url = reverse('instructor_dashboard', kwargs={'course_id': course.id.to_deprecated_string()})
|
||||
username = 'u1'
|
||||
rolename = FORUM_ROLE_ADMINISTRATOR
|
||||
response = self.client.post(url, {'action': action_name('Add', rolename), FORUM_ADMIN_USER[rolename]: username})
|
||||
@@ -130,7 +131,7 @@ class TestInstructorDashboardForumAdmin(ModuleStoreTestCase, LoginEnrollmentTest
|
||||
def test_list_forum_admin_users(self):
|
||||
course = self.toy
|
||||
self.initialize_roles(course.id)
|
||||
url = reverse('instructor_dashboard', kwargs={'course_id': course.id})
|
||||
url = reverse('instructor_dashboard', kwargs={'course_id': course.id.to_deprecated_string()})
|
||||
username = 'u2'
|
||||
added_roles = [FORUM_ROLE_STUDENT] # u2 is already added as a student to the discussion forums
|
||||
self.assertTrue(has_forum_access(username, course.id, 'Student'))
|
||||
|
||||
@@ -11,6 +11,7 @@ from courseware.tests.tests import TEST_DATA_MIXED_MODULESTORE
|
||||
from capa.tests.response_xml_factory import StringResponseXMLFactory
|
||||
from courseware.tests.factories import StudentModuleFactory
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
|
||||
@@ -64,10 +65,13 @@ class TestGradebook(ModuleStoreTestCase):
|
||||
max_grade=1,
|
||||
student=user,
|
||||
course_id=self.course.id,
|
||||
module_state_key=Location(item.location).url()
|
||||
module_id=item.location
|
||||
)
|
||||
|
||||
self.response = self.client.get(reverse('gradebook', args=(self.course.id,)))
|
||||
self.response = self.client.get(reverse(
|
||||
'gradebook',
|
||||
args=(self.course.id.to_deprecated_string(),)
|
||||
))
|
||||
|
||||
def test_response_code(self):
|
||||
self.assertEquals(self.response.status_code, 200)
|
||||
|
||||
@@ -24,7 +24,7 @@ class TestRawGradeCSV(TestSubmittingProblems):
|
||||
self.instructor = 'view2@test.com'
|
||||
self.create_account('u2', self.instructor, self.password)
|
||||
self.activate_user(self.instructor)
|
||||
CourseStaffRole(self.course.location).add_users(User.objects.get(email=self.instructor))
|
||||
CourseStaffRole(self.course.id).add_users(User.objects.get(email=self.instructor))
|
||||
self.logout()
|
||||
self.login(self.instructor, self.password)
|
||||
self.enroll(self.course)
|
||||
@@ -45,7 +45,7 @@ class TestRawGradeCSV(TestSubmittingProblems):
|
||||
resp = self.submit_question_answer('p2', {'2_1': 'Correct'})
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id})
|
||||
url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
msg = "url = {0}\n".format(url)
|
||||
response = self.client.post(url, {'action': 'Download CSV of all RAW grades'})
|
||||
msg += "instructor dashboard download raw csv grades: response = '{0}'\n".format(response)
|
||||
|
||||
@@ -16,6 +16,7 @@ from courseware.models import StudentModule
|
||||
|
||||
from submissions import api as sub_api
|
||||
from student.models import anonymous_id_for_user
|
||||
from .test_tools import msk_from_problem_urlname
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
|
||||
@@ -35,29 +36,36 @@ class InstructorResetStudentStateTest(ModuleStoreTestCase, LoginEnrollmentTestCa
|
||||
CourseEnrollmentFactory.create(user=self.student, course_id=self.course.id)
|
||||
|
||||
def test_delete_student_state_resets_scores(self):
|
||||
item_id = 'i4x://MITx/999/openassessment/b3dce2586c9c4876b73e7f390e42ef8f'
|
||||
problem_location = msk_from_problem_urlname(
|
||||
self.course.id,
|
||||
'b3dce2586c9c4876b73e7f390e42ef8f',
|
||||
block_type='openassessment'
|
||||
)
|
||||
|
||||
# Create a student module for the user
|
||||
StudentModule.objects.create(
|
||||
student=self.student, course_id=self.course.id, module_state_key=item_id, state=json.dumps({})
|
||||
student=self.student,
|
||||
course_id=self.course.id,
|
||||
module_state_key=problem_location,
|
||||
state=json.dumps({})
|
||||
)
|
||||
|
||||
# Create a submission and score for the student using the submissions API
|
||||
student_item = {
|
||||
'student_id': anonymous_id_for_user(self.student, self.course.id),
|
||||
'course_id': self.course.id,
|
||||
'item_id': item_id,
|
||||
'item_id': problem_location,
|
||||
'item_type': 'openassessment'
|
||||
}
|
||||
submission = sub_api.create_submission(student_item, 'test answer')
|
||||
sub_api.set_score(submission['uuid'], 1, 2)
|
||||
|
||||
# Delete student state using the instructor dash
|
||||
url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id})
|
||||
url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
response = self.client.post(url, {
|
||||
'action': 'Delete student state for module',
|
||||
'unique_student_identifier': self.student.email,
|
||||
'problem_for_student': 'openassessment/b3dce2586c9c4876b73e7f390e42ef8f',
|
||||
'problem_for_student': str(problem_location),
|
||||
})
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@@ -48,7 +48,7 @@ class TestXss(ModuleStoreTestCase):
|
||||
)
|
||||
req.user = self._instructor
|
||||
req.session = {}
|
||||
resp = legacy.instructor_dashboard(req, self._course.id)
|
||||
resp = legacy.instructor_dashboard(req, self._course.id.to_deprecated_string())
|
||||
respUnicode = resp.content.decode(settings.DEFAULT_CHARSET)
|
||||
self.assertNotIn(self._evil_student.profile.name, respUnicode)
|
||||
self.assertIn(escape(self._evil_student.profile.name), respUnicode)
|
||||
|
||||
@@ -17,6 +17,7 @@ from student.tests.factories import UserFactory
|
||||
from xmodule.fields import Date
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
from xmodule.modulestore.keys import CourseKey
|
||||
|
||||
from ..views import tools
|
||||
|
||||
@@ -86,10 +87,8 @@ class TestFindUnit(ModuleStoreTestCase):
|
||||
Fixtures.
|
||||
"""
|
||||
course = CourseFactory.create()
|
||||
week1 = ItemFactory.create()
|
||||
homework = ItemFactory.create(parent_location=week1.location)
|
||||
week1.children.append(homework.location)
|
||||
course.children.append(week1.location)
|
||||
week1 = ItemFactory.create(parent=course)
|
||||
homework = ItemFactory.create(parent=week1)
|
||||
|
||||
self.course = course
|
||||
self.homework = homework
|
||||
@@ -98,7 +97,7 @@ class TestFindUnit(ModuleStoreTestCase):
|
||||
"""
|
||||
Test finding a nested unit.
|
||||
"""
|
||||
url = self.homework.location.url()
|
||||
url = self.homework.location.to_deprecated_string()
|
||||
self.assertEqual(tools.find_unit(self.course, url), self.homework)
|
||||
|
||||
def test_find_unit_notfound(self):
|
||||
@@ -121,15 +120,13 @@ class TestGetUnitsWithDueDate(ModuleStoreTestCase):
|
||||
"""
|
||||
due = datetime.datetime(2010, 5, 12, 2, 42, tzinfo=utc)
|
||||
course = CourseFactory.create()
|
||||
week1 = ItemFactory.create(due=due)
|
||||
week2 = ItemFactory.create(due=due)
|
||||
course.children = [week1.location.url(), week2.location.url()]
|
||||
week1 = ItemFactory.create(due=due, parent=course)
|
||||
week2 = ItemFactory.create(due=due, parent=course)
|
||||
|
||||
homework = ItemFactory.create(
|
||||
parent_location=week1.location,
|
||||
parent=week1,
|
||||
due=due
|
||||
)
|
||||
week1.children = [homework.location.url()]
|
||||
|
||||
self.course = course
|
||||
self.week1 = week1
|
||||
@@ -139,7 +136,7 @@ class TestGetUnitsWithDueDate(ModuleStoreTestCase):
|
||||
|
||||
def urls(seq):
|
||||
"URLs for sequence of nodes."
|
||||
return sorted(i.location.url() for i in seq)
|
||||
return sorted(i.location.to_deprecated_string() for i in seq)
|
||||
|
||||
self.assertEquals(
|
||||
urls(tools.get_units_with_due_date(self.course)),
|
||||
@@ -156,7 +153,7 @@ class TestTitleOrUrl(unittest.TestCase):
|
||||
|
||||
def test_url(self):
|
||||
unit = mock.Mock(display_name=None)
|
||||
unit.location.url.return_value = 'test:hello'
|
||||
unit.location.to_deprecated_string.return_value = 'test:hello'
|
||||
self.assertEquals(tools.title_or_url(unit), 'test:hello')
|
||||
|
||||
|
||||
@@ -171,27 +168,25 @@ class TestSetDueDateExtension(ModuleStoreTestCase):
|
||||
"""
|
||||
due = datetime.datetime(2010, 5, 12, 2, 42, tzinfo=utc)
|
||||
course = CourseFactory.create()
|
||||
week1 = ItemFactory.create(due=due)
|
||||
week2 = ItemFactory.create(due=due)
|
||||
course.children = [week1.location.url(), week2.location.url()]
|
||||
week1 = ItemFactory.create(due=due, parent=course)
|
||||
week2 = ItemFactory.create(due=due, parent=course)
|
||||
|
||||
homework = ItemFactory.create(
|
||||
parent_location=week1.location,
|
||||
parent=week1,
|
||||
due=due
|
||||
)
|
||||
week1.children = [homework.location.url()]
|
||||
|
||||
user = UserFactory.create()
|
||||
StudentModule(
|
||||
state='{}',
|
||||
student_id=user.id,
|
||||
course_id=course.id,
|
||||
module_state_key=week1.location.url()).save()
|
||||
module_state_key=week1.location).save()
|
||||
StudentModule(
|
||||
state='{}',
|
||||
student_id=user.id,
|
||||
course_id=course.id,
|
||||
module_state_key=homework.location.url()).save()
|
||||
module_state_key=homework.location).save()
|
||||
|
||||
self.course = course
|
||||
self.week1 = week1
|
||||
@@ -226,63 +221,60 @@ class TestDataDumps(ModuleStoreTestCase):
|
||||
"""
|
||||
due = datetime.datetime(2010, 5, 12, 2, 42, tzinfo=utc)
|
||||
course = CourseFactory.create()
|
||||
week1 = ItemFactory.create(due=due)
|
||||
week2 = ItemFactory.create(due=due)
|
||||
week3 = ItemFactory.create(due=due)
|
||||
course.children = [week1.location.url(), week2.location.url(),
|
||||
week3.location.url()]
|
||||
week1 = ItemFactory.create(due=due, parent=course)
|
||||
week2 = ItemFactory.create(due=due, parent=course)
|
||||
week3 = ItemFactory.create(due=due, parent=course)
|
||||
|
||||
homework = ItemFactory.create(
|
||||
parent_location=week1.location,
|
||||
parent=week1,
|
||||
due=due
|
||||
)
|
||||
week1.children = [homework.location.url()]
|
||||
|
||||
user1 = UserFactory.create()
|
||||
StudentModule(
|
||||
state='{}',
|
||||
student_id=user1.id,
|
||||
course_id=course.id,
|
||||
module_state_key=week1.location.url()).save()
|
||||
module_state_key=week1.location).save()
|
||||
StudentModule(
|
||||
state='{}',
|
||||
student_id=user1.id,
|
||||
course_id=course.id,
|
||||
module_state_key=week2.location.url()).save()
|
||||
module_state_key=week2.location).save()
|
||||
StudentModule(
|
||||
state='{}',
|
||||
student_id=user1.id,
|
||||
course_id=course.id,
|
||||
module_state_key=week3.location.url()).save()
|
||||
module_state_key=week3.location).save()
|
||||
StudentModule(
|
||||
state='{}',
|
||||
student_id=user1.id,
|
||||
course_id=course.id,
|
||||
module_state_key=homework.location.url()).save()
|
||||
module_state_key=homework.location).save()
|
||||
|
||||
user2 = UserFactory.create()
|
||||
StudentModule(
|
||||
state='{}',
|
||||
student_id=user2.id,
|
||||
course_id=course.id,
|
||||
module_state_key=week1.location.url()).save()
|
||||
module_state_key=week1.location).save()
|
||||
StudentModule(
|
||||
state='{}',
|
||||
student_id=user2.id,
|
||||
course_id=course.id,
|
||||
module_state_key=homework.location.url()).save()
|
||||
module_state_key=homework.location).save()
|
||||
|
||||
user3 = UserFactory.create()
|
||||
StudentModule(
|
||||
state='{}',
|
||||
student_id=user3.id,
|
||||
course_id=course.id,
|
||||
module_state_key=week1.location.url()).save()
|
||||
module_state_key=week1.location).save()
|
||||
StudentModule(
|
||||
state='{}',
|
||||
student_id=user3.id,
|
||||
course_id=course.id,
|
||||
module_state_key=homework.location.url()).save()
|
||||
module_state_key=homework.location).save()
|
||||
|
||||
self.course = course
|
||||
self.week1 = week1
|
||||
@@ -337,10 +329,22 @@ def get_extended_due(course, unit, student):
|
||||
student_module = StudentModule.objects.get(
|
||||
student_id=student.id,
|
||||
course_id=course.id,
|
||||
module_state_key=unit.location.url()
|
||||
module_id=unit.location
|
||||
)
|
||||
|
||||
state = json.loads(student_module.state)
|
||||
extended = state.get('extended_due', None)
|
||||
if extended:
|
||||
return DATE_FIELD.from_json(extended)
|
||||
|
||||
|
||||
def msk_from_problem_urlname(course_id, urlname, block_type='problem'):
|
||||
"""
|
||||
Convert a 'problem urlname' to a module state key (db field)
|
||||
"""
|
||||
if not isinstance(course_id, CourseKey):
|
||||
raise ValueError
|
||||
if urlname.endswith(".xml"):
|
||||
urlname = urlname[:-4]
|
||||
|
||||
return course_id.make_usage_key(block_type, urlname)
|
||||
|
||||
@@ -27,12 +27,12 @@ class DummyRequest(object):
|
||||
return False
|
||||
|
||||
|
||||
def get_module_for_student(student, course, location, request=None):
|
||||
def get_module_for_student(student, usage_key, request=None):
|
||||
"""Return the module for the (student, location) using a DummyRequest."""
|
||||
if request is None:
|
||||
request = DummyRequest()
|
||||
request.user = student
|
||||
|
||||
descriptor = modulestore().get_instance(course.id, location, depth=0)
|
||||
field_data_cache = FieldDataCache([descriptor], course.id, student)
|
||||
return get_module(student, request, location, field_data_cache, course.id)
|
||||
descriptor = modulestore().get_item(usage_key, depth=0)
|
||||
field_data_cache = FieldDataCache([descriptor], usage_key.course_key, student)
|
||||
return get_module(student, request, usage_key, field_data_cache)
|
||||
|
||||
@@ -68,6 +68,9 @@ from .tools import (
|
||||
strip_if_string,
|
||||
)
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
from xmodule.modulestore.keys import UsageKey
|
||||
from opaque_keys import InvalidKeyError
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -191,9 +194,9 @@ def require_level(level):
|
||||
def decorator(func): # pylint: disable=C0111
|
||||
def wrapped(*args, **kwargs): # pylint: disable=C0111
|
||||
request = args[0]
|
||||
course = get_course_by_id(kwargs['course_id'])
|
||||
course = get_course_by_id(SlashSeparatedCourseKey.from_deprecated_string(kwargs['course_id']))
|
||||
|
||||
if has_access(request.user, course, level):
|
||||
if has_access(request.user, level, course):
|
||||
return func(*args, **kwargs)
|
||||
else:
|
||||
return HttpResponseForbidden()
|
||||
@@ -242,6 +245,7 @@ def students_update_enrollment(request, course_id):
|
||||
]
|
||||
}
|
||||
"""
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
|
||||
action = request.GET.get('action')
|
||||
identifiers_raw = request.GET.get('identifiers')
|
||||
@@ -331,6 +335,7 @@ def bulk_beta_modify_access(request, course_id):
|
||||
anything split_input_list can handle.
|
||||
- action is one of ['add', 'remove']
|
||||
"""
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
action = request.GET.get('action')
|
||||
identifiers_raw = request.GET.get('identifiers')
|
||||
identifiers = _split_input_list(identifiers_raw)
|
||||
@@ -413,8 +418,9 @@ def modify_access(request, course_id):
|
||||
rolename is one of ['instructor', 'staff', 'beta']
|
||||
action is one of ['allow', 'revoke']
|
||||
"""
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
course = get_course_with_access(
|
||||
request.user, course_id, 'instructor', depth=None
|
||||
request.user, 'instructor', course_id, depth=None
|
||||
)
|
||||
try:
|
||||
user = get_student_from_identifier(request.GET.get('unique_student_identifier'))
|
||||
@@ -494,8 +500,9 @@ def list_course_role_members(request, course_id):
|
||||
]
|
||||
}
|
||||
"""
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
course = get_course_with_access(
|
||||
request.user, course_id, 'instructor', depth=None
|
||||
request.user, 'instructor', course_id, depth=None
|
||||
)
|
||||
|
||||
rolename = request.GET.get('rolename')
|
||||
@@ -513,7 +520,7 @@ def list_course_role_members(request, course_id):
|
||||
}
|
||||
|
||||
response_payload = {
|
||||
'course_id': course_id,
|
||||
'course_id': course_id.to_deprecated_string(),
|
||||
rolename: map(extract_user_info, list_with_level(
|
||||
course, rolename
|
||||
)),
|
||||
@@ -528,13 +535,14 @@ def get_grading_config(request, course_id):
|
||||
"""
|
||||
Respond with json which contains a html formatted grade summary.
|
||||
"""
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
course = get_course_with_access(
|
||||
request.user, course_id, 'staff', depth=None
|
||||
request.user, 'staff', course_id, depth=None
|
||||
)
|
||||
grading_config_summary = analytics.basic.dump_grading_context(course)
|
||||
|
||||
response_payload = {
|
||||
'course_id': course_id,
|
||||
'course_id': course_id.to_deprecated_string(),
|
||||
'grading_config_summary': grading_config_summary,
|
||||
}
|
||||
return JsonResponse(response_payload)
|
||||
@@ -552,6 +560,8 @@ def get_students_features(request, course_id, csv=False): # pylint: disable=W06
|
||||
|
||||
TO DO accept requests for different attribute sets.
|
||||
"""
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
|
||||
available_features = analytics.basic.AVAILABLE_FEATURES
|
||||
query_features = [
|
||||
'username', 'name', 'email', 'language', 'location', 'year_of_birth',
|
||||
@@ -578,7 +588,7 @@ def get_students_features(request, course_id, csv=False): # pylint: disable=W06
|
||||
|
||||
if not csv:
|
||||
response_payload = {
|
||||
'course_id': course_id,
|
||||
'course_id': course_id.to_deprecated_string(),
|
||||
'students': student_data,
|
||||
'students_count': len(student_data),
|
||||
'queried_features': query_features,
|
||||
@@ -601,6 +611,7 @@ def get_anon_ids(request, course_id): # pylint: disable=W0613
|
||||
# TODO: the User.objects query and CSV generation here could be
|
||||
# centralized into analytics. Currently analytics has similar functionality
|
||||
# but not quite what's needed.
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
def csv_response(filename, header, rows):
|
||||
"""Returns a CSV http response for the given header and rows (excel/utf-8)."""
|
||||
response = HttpResponse(mimetype='text/csv')
|
||||
@@ -620,7 +631,7 @@ def get_anon_ids(request, course_id): # pylint: disable=W0613
|
||||
).order_by('id')
|
||||
header = ['User ID', 'Anonymized user ID']
|
||||
rows = [[s.id, unique_id_for_user(s)] for s in students]
|
||||
return csv_response(course_id.replace('/', '-') + '-anon-ids.csv', header, rows)
|
||||
return csv_response(course_id.to_deprecated_string().replace('/', '-') + '-anon-ids.csv', header, rows)
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@@ -635,6 +646,7 @@ def get_distribution(request, course_id):
|
||||
empty response['feature_results'] object.
|
||||
A list of available will be available in the response['available_features']
|
||||
"""
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
feature = request.GET.get('feature')
|
||||
# alternate notations of None
|
||||
if feature in (None, 'null', ''):
|
||||
@@ -650,7 +662,7 @@ def get_distribution(request, course_id):
|
||||
))
|
||||
|
||||
response_payload = {
|
||||
'course_id': course_id,
|
||||
'course_id': course_id.to_deprecated_string(),
|
||||
'queried_feature': feature,
|
||||
'available_features': available_features,
|
||||
'feature_display_names': analytics.distributions.DISPLAY_NAMES,
|
||||
@@ -689,12 +701,13 @@ def get_student_progress_url(request, course_id):
|
||||
'progress_url': '/../...'
|
||||
}
|
||||
"""
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
user = get_student_from_identifier(request.GET.get('unique_student_identifier'))
|
||||
|
||||
progress_url = reverse('student_progress', kwargs={'course_id': course_id, 'student_id': user.id})
|
||||
progress_url = reverse('student_progress', kwargs={'course_id': course_id.to_deprecated_string(), 'student_id': user.id})
|
||||
|
||||
response_payload = {
|
||||
'course_id': course_id,
|
||||
'course_id': course_id.to_deprecated_string(),
|
||||
'progress_url': progress_url,
|
||||
}
|
||||
return JsonResponse(response_payload)
|
||||
@@ -725,8 +738,9 @@ def reset_student_attempts(request, course_id):
|
||||
requires instructor access
|
||||
mutually exclusive with all_students
|
||||
"""
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
course = get_course_with_access(
|
||||
request.user, course_id, 'staff', depth=None
|
||||
request.user, 'staff', course_id, depth=None
|
||||
)
|
||||
|
||||
problem_to_reset = strip_if_string(request.GET.get('problem_to_reset'))
|
||||
@@ -749,10 +763,13 @@ def reset_student_attempts(request, course_id):
|
||||
|
||||
# instructor authorization
|
||||
if all_students or delete_module:
|
||||
if not has_access(request.user, course, 'instructor'):
|
||||
if not has_access(request.user, 'instructor', course):
|
||||
return HttpResponseForbidden("Requires instructor access.")
|
||||
|
||||
module_state_key = _msk_from_problem_urlname(course_id, problem_to_reset)
|
||||
try:
|
||||
module_state_key = UsageKey.from_string(problem_to_reset)
|
||||
except InvalidKeyError:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
response_payload = {}
|
||||
response_payload['problem_to_reset'] = problem_to_reset
|
||||
@@ -768,7 +785,7 @@ def reset_student_attempts(request, course_id):
|
||||
return HttpResponse(error_msg, status=500)
|
||||
response_payload['student'] = student_identifier
|
||||
elif all_students:
|
||||
instructor_task.api.submit_reset_problem_attempts_for_all_students(request, course_id, module_state_key)
|
||||
instructor_task.api.submit_reset_problem_attempts_for_all_students(request, module_state_key)
|
||||
response_payload['task'] = 'created'
|
||||
response_payload['student'] = 'All Students'
|
||||
else:
|
||||
@@ -794,6 +811,7 @@ def rescore_problem(request, course_id):
|
||||
|
||||
all_students and unique_student_identifier cannot both be present.
|
||||
"""
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
problem_to_reset = strip_if_string(request.GET.get('problem_to_reset'))
|
||||
student_identifier = request.GET.get('unique_student_identifier', None)
|
||||
student = None
|
||||
@@ -810,17 +828,20 @@ def rescore_problem(request, course_id):
|
||||
"Cannot rescore with all_students and unique_student_identifier."
|
||||
)
|
||||
|
||||
module_state_key = _msk_from_problem_urlname(course_id, problem_to_reset)
|
||||
try:
|
||||
module_state_key = UsageKey.from_string(problem_to_reset)
|
||||
except InvalidKeyError:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
response_payload = {}
|
||||
response_payload['problem_to_reset'] = problem_to_reset
|
||||
|
||||
if student:
|
||||
response_payload['student'] = student_identifier
|
||||
instructor_task.api.submit_rescore_problem_for_student(request, course_id, module_state_key, student)
|
||||
instructor_task.api.submit_rescore_problem_for_student(request, module_state_key, student)
|
||||
response_payload['task'] = 'created'
|
||||
elif all_students:
|
||||
instructor_task.api.submit_rescore_problem_for_all_students(request, course_id, module_state_key)
|
||||
instructor_task.api.submit_rescore_problem_for_all_students(request, module_state_key)
|
||||
response_payload['task'] = 'created'
|
||||
else:
|
||||
return HttpResponseBadRequest()
|
||||
@@ -874,6 +895,7 @@ def list_background_email_tasks(request, course_id): # pylint: disable=unused-a
|
||||
"""
|
||||
List background email tasks.
|
||||
"""
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
task_type = 'bulk_course_email'
|
||||
# Specifying for the history of a single task type
|
||||
tasks = instructor_task.api.get_instructor_task_history(course_id, task_type=task_type)
|
||||
@@ -893,22 +915,26 @@ def list_instructor_tasks(request, course_id):
|
||||
|
||||
Takes optional query paremeters.
|
||||
- With no arguments, lists running tasks.
|
||||
- `problem_urlname` lists task history for problem
|
||||
- `problem_urlname` and `unique_student_identifier` lists task
|
||||
- `problem_location_str` lists task history for problem
|
||||
- `problem_location_str` and `unique_student_identifier` lists task
|
||||
history for problem AND student (intersection)
|
||||
"""
|
||||
problem_urlname = strip_if_string(request.GET.get('problem_urlname', False))
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
problem_location_str = strip_if_string(request.GET.get('problem_location_str', False))
|
||||
student = request.GET.get('unique_student_identifier', None)
|
||||
if student is not None:
|
||||
student = get_student_from_identifier(student)
|
||||
|
||||
if student and not problem_urlname:
|
||||
if student and not problem_location_str:
|
||||
return HttpResponseBadRequest(
|
||||
"unique_student_identifier must accompany problem_urlname"
|
||||
"unique_student_identifier must accompany problem_location_str"
|
||||
)
|
||||
|
||||
if problem_urlname:
|
||||
module_state_key = _msk_from_problem_urlname(course_id, problem_urlname)
|
||||
if problem_location_str:
|
||||
try:
|
||||
module_state_key = UsageKey.from_string(problem_location_str)
|
||||
except InvalidKeyError:
|
||||
return HttpResponseBadRequest()
|
||||
if student:
|
||||
# Specifying for a single student's history on this problem
|
||||
tasks = instructor_task.api.get_instructor_task_history(course_id, module_state_key, student)
|
||||
@@ -932,6 +958,7 @@ def list_report_downloads(_request, course_id):
|
||||
"""
|
||||
List grade CSV files that are available for download for this course.
|
||||
"""
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
report_store = ReportStore.from_config()
|
||||
|
||||
response_payload = {
|
||||
@@ -950,8 +977,9 @@ def calculate_grades_csv(request, course_id):
|
||||
"""
|
||||
AlreadyRunningError is raised if the course's grades are already being updated.
|
||||
"""
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
try:
|
||||
instructor_task.api.submit_calculate_grades_csv(request, course_id)
|
||||
instructor_task.api.submit_calculate_grades_csv(request, course_key)
|
||||
success_status = _("Your grade report is being generated! You can view the status of the generation task in the 'Pending Instructor Tasks' section.")
|
||||
return JsonResponse({"status": success_status})
|
||||
except AlreadyRunningError:
|
||||
@@ -976,8 +1004,9 @@ def list_forum_members(request, course_id):
|
||||
|
||||
Takes query parameter `rolename`.
|
||||
"""
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
course = get_course_by_id(course_id)
|
||||
has_instructor_access = has_access(request.user, course, 'instructor')
|
||||
has_instructor_access = has_access(request.user, 'instructor', course)
|
||||
has_forum_admin = has_forum_access(
|
||||
request.user, course_id, FORUM_ROLE_ADMINISTRATOR
|
||||
)
|
||||
@@ -1016,7 +1045,7 @@ def list_forum_members(request, course_id):
|
||||
}
|
||||
|
||||
response_payload = {
|
||||
'course_id': course_id,
|
||||
'course_id': course_id.to_deprecated_string(),
|
||||
rolename: map(extract_user_info, users),
|
||||
}
|
||||
return JsonResponse(response_payload)
|
||||
@@ -1036,6 +1065,7 @@ def send_email(request, course_id):
|
||||
- 'subject' specifies email's subject
|
||||
- 'message' specifies email's content
|
||||
"""
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
send_to = request.POST.get("send_to")
|
||||
subject = request.POST.get("subject")
|
||||
message = request.POST.get("message")
|
||||
@@ -1047,8 +1077,7 @@ def send_email(request, course_id):
|
||||
|
||||
# Submit the task, so that the correct InstructorTask object gets created (for monitoring purposes)
|
||||
instructor_task.api.submit_bulk_course_email(request, course_id, email.id) # pylint: disable=E1101
|
||||
|
||||
response_payload = {'course_id': course_id}
|
||||
response_payload = {'course_id': course_id.to_deprecated_string()}
|
||||
return JsonResponse(response_payload)
|
||||
|
||||
|
||||
@@ -1075,8 +1104,9 @@ def update_forum_role_membership(request, course_id):
|
||||
- `rolename` is one of [FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA]
|
||||
- `action` is one of ['allow', 'revoke']
|
||||
"""
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
course = get_course_by_id(course_id)
|
||||
has_instructor_access = has_access(request.user, course, 'instructor')
|
||||
has_instructor_access = has_access(request.user, 'instructor', course)
|
||||
has_forum_admin = has_forum_access(
|
||||
request.user, course_id, FORUM_ROLE_ADMINISTRATOR
|
||||
)
|
||||
@@ -1101,7 +1131,7 @@ def update_forum_role_membership(request, course_id):
|
||||
))
|
||||
|
||||
user = get_student_from_identifier(unique_student_identifier)
|
||||
target_is_instructor = has_access(user, course, 'instructor')
|
||||
target_is_instructor = has_access(user, 'instructor', course)
|
||||
# cannot revoke instructor
|
||||
if target_is_instructor and action == 'revoke' and rolename == FORUM_ROLE_ADMINISTRATOR:
|
||||
return HttpResponseBadRequest("Cannot revoke instructor forum admin privileges.")
|
||||
@@ -1112,7 +1142,7 @@ def update_forum_role_membership(request, course_id):
|
||||
return HttpResponseBadRequest("Role does not exist.")
|
||||
|
||||
response_payload = {
|
||||
'course_id': course_id,
|
||||
'course_id': course_id.to_deprecated_string(),
|
||||
'action': action,
|
||||
}
|
||||
return JsonResponse(response_payload)
|
||||
@@ -1131,6 +1161,7 @@ def proxy_legacy_analytics(request, course_id):
|
||||
|
||||
`aname` is a query parameter specifying which analytic to query.
|
||||
"""
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
analytics_name = request.GET.get('aname')
|
||||
|
||||
# abort if misconfigured
|
||||
@@ -1140,7 +1171,7 @@ def proxy_legacy_analytics(request, course_id):
|
||||
url = "{}get?aname={}&course_id={}&apikey={}".format(
|
||||
settings.ANALYTICS_SERVER_URL,
|
||||
analytics_name,
|
||||
course_id,
|
||||
course_id.to_deprecated_string(),
|
||||
settings.ANALYTICS_API_KEY,
|
||||
)
|
||||
|
||||
@@ -1175,9 +1206,9 @@ def _display_unit(unit):
|
||||
"""
|
||||
name = getattr(unit, 'display_name', None)
|
||||
if name:
|
||||
return u'{0} ({1})'.format(name, unit.location.url())
|
||||
return u'{0} ({1})'.format(name, unit.location.to_deprecated_string())
|
||||
else:
|
||||
return unit.location.url()
|
||||
return unit.location.to_deprecated_string()
|
||||
|
||||
|
||||
@handle_dashboard_error
|
||||
@@ -1189,7 +1220,7 @@ def change_due_date(request, course_id):
|
||||
"""
|
||||
Grants a due date extension to a student for a particular unit.
|
||||
"""
|
||||
course = get_course_by_id(course_id)
|
||||
course = get_course_by_id(SlashSeparatedCourseKey.from_deprecated_string(course_id))
|
||||
student = get_student_from_identifier(request.GET.get('student'))
|
||||
unit = find_unit(course, request.GET.get('url'))
|
||||
due_date = parse_datetime(request.GET.get('due_datetime'))
|
||||
@@ -1210,7 +1241,7 @@ def reset_due_date(request, course_id):
|
||||
"""
|
||||
Rescinds a due date extension for a student on a particular unit.
|
||||
"""
|
||||
course = get_course_by_id(course_id)
|
||||
course = get_course_by_id(SlashSeparatedCourseKey.from_deprecated_string(course_id))
|
||||
student = get_student_from_identifier(request.GET.get('student'))
|
||||
unit = find_unit(course, request.GET.get('url'))
|
||||
set_due_date_extension(course, unit, student, None)
|
||||
@@ -1230,7 +1261,7 @@ def show_unit_extensions(request, course_id):
|
||||
"""
|
||||
Shows all of the students which have due date extensions for the given unit.
|
||||
"""
|
||||
course = get_course_by_id(course_id)
|
||||
course = get_course_by_id(SlashSeparatedCourseKey.from_deprecated_string(course_id))
|
||||
unit = find_unit(course, request.GET.get('url'))
|
||||
return JsonResponse(dump_module_extensions(course, unit))
|
||||
|
||||
@@ -1246,7 +1277,7 @@ def show_student_extensions(request, course_id):
|
||||
particular course.
|
||||
"""
|
||||
student = get_student_from_identifier(request.GET.get('student'))
|
||||
course = get_course_by_id(course_id)
|
||||
course = get_course_by_id(SlashSeparatedCourseKey.from_deprecated_string(course_id))
|
||||
return JsonResponse(dump_student_extensions(course, student))
|
||||
|
||||
|
||||
@@ -1267,23 +1298,3 @@ def _split_input_list(str_list):
|
||||
new_list = [s for s in new_list if s != '']
|
||||
|
||||
return new_list
|
||||
|
||||
|
||||
def _msk_from_problem_urlname(course_id, urlname):
|
||||
"""
|
||||
Convert a 'problem urlname' (name that instructor's input into dashboard)
|
||||
to a module state key (db field)
|
||||
"""
|
||||
if urlname.endswith(".xml"):
|
||||
urlname = urlname[:-4]
|
||||
|
||||
# Combined open ended problems also have state that can be deleted. However,
|
||||
# prepending "problem" will only allow capa problems to be reset.
|
||||
# Get around this for xblock problems.
|
||||
if "/" not in urlname:
|
||||
urlname = "problem/" + urlname
|
||||
|
||||
parts = Location.parse_course_id(course_id)
|
||||
parts['urlname'] = urlname
|
||||
module_state_key = u"i4x://{org}/{course}/{urlname}".format(**parts)
|
||||
return module_state_key
|
||||
|
||||
@@ -11,9 +11,10 @@ from django.utils.html import escape
|
||||
from django.http import Http404
|
||||
from django.conf import settings
|
||||
|
||||
from lms.lib.xblock.runtime import quote_slashes
|
||||
from xmodule_modifiers import wrap_xblock
|
||||
from xmodule.html_module import HtmlDescriptor
|
||||
from xmodule.modulestore import XML_MODULESTORE_TYPE, Location
|
||||
from xmodule.modulestore import XML_MODULESTORE_TYPE
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xblock.field_data import DictFieldData
|
||||
from xblock.fields import ScopeIds
|
||||
@@ -26,22 +27,23 @@ from bulk_email.models import CourseAuthorization
|
||||
from class_dashboard.dashboard_data import get_section_display_name, get_array_section_has_problem
|
||||
|
||||
from .tools import get_units_with_due_date, title_or_url
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
def instructor_dashboard_2(request, course_id):
|
||||
""" Display the instructor dashboard for a course. """
|
||||
|
||||
course = get_course_by_id(course_id, depth=None)
|
||||
is_studio_course = (modulestore().get_modulestore_type(course_id) != XML_MODULESTORE_TYPE)
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
course = get_course_by_id(course_key, depth=None)
|
||||
is_studio_course = (modulestore().get_modulestore_type(course_key) != XML_MODULESTORE_TYPE)
|
||||
|
||||
access = {
|
||||
'admin': request.user.is_staff,
|
||||
'instructor': has_access(request.user, course, 'instructor'),
|
||||
'staff': has_access(request.user, course, 'staff'),
|
||||
'instructor': has_access(request.user, 'instructor', course),
|
||||
'staff': has_access(request.user, 'staff', course),
|
||||
'forum_admin': has_forum_access(
|
||||
request.user, course_id, FORUM_ROLE_ADMINISTRATOR
|
||||
request.user, course_key, FORUM_ROLE_ADMINISTRATOR
|
||||
),
|
||||
}
|
||||
|
||||
@@ -49,23 +51,24 @@ def instructor_dashboard_2(request, course_id):
|
||||
raise Http404()
|
||||
|
||||
sections = [
|
||||
_section_course_info(course_id, access),
|
||||
_section_membership(course_id, access),
|
||||
_section_student_admin(course_id, access),
|
||||
_section_data_download(course_id, access),
|
||||
_section_analytics(course_id, access),
|
||||
_section_course_info(course_key, access),
|
||||
_section_membership(course_key, access),
|
||||
_section_student_admin(course_key, access),
|
||||
_section_data_download(course_key, access),
|
||||
_section_analytics(course_key, access),
|
||||
]
|
||||
|
||||
if (settings.FEATURES.get('INDIVIDUAL_DUE_DATES') and access['instructor']):
|
||||
sections.insert(3, _section_extensions(course))
|
||||
|
||||
# Gate access to course email by feature flag & by course-specific authorization
|
||||
if settings.FEATURES['ENABLE_INSTRUCTOR_EMAIL'] and is_studio_course and CourseAuthorization.instructor_email_enabled(course_id):
|
||||
sections.append(_section_send_email(course_id, access, course))
|
||||
if settings.FEATURES['ENABLE_INSTRUCTOR_EMAIL'] and \
|
||||
is_studio_course and CourseAuthorization.instructor_email_enabled(course_key):
|
||||
sections.append(_section_send_email(course_key, access, course))
|
||||
|
||||
# Gate access to Metrics tab by featue flag and staff authorization
|
||||
if settings.FEATURES['CLASS_DASHBOARD'] and access['staff']:
|
||||
sections.append(_section_metrics(course_id, access))
|
||||
sections.append(_section_metrics(course_key, access))
|
||||
|
||||
studio_url = None
|
||||
if is_studio_course:
|
||||
@@ -79,7 +82,7 @@ def instructor_dashboard_2(request, course_id):
|
||||
|
||||
context = {
|
||||
'course': course,
|
||||
'old_dashboard_url': reverse('instructor_dashboard', kwargs={'course_id': course_id}),
|
||||
'old_dashboard_url': reverse('instructor_dashboard', kwargs={'course_id': course_key.to_deprecated_string()}),
|
||||
'studio_url': studio_url,
|
||||
'sections': sections,
|
||||
'disable_buttons': disable_buttons,
|
||||
@@ -101,25 +104,20 @@ section_display_name will be used to generate link titles in the nav bar.
|
||||
""" # pylint: disable=W0105
|
||||
|
||||
|
||||
def _section_course_info(course_id, access):
|
||||
def _section_course_info(course_key, access):
|
||||
""" Provide data for the corresponding dashboard section """
|
||||
course = get_course_by_id(course_id, depth=None)
|
||||
|
||||
course_id_dict = Location.parse_course_id(course_id)
|
||||
course = get_course_by_id(course_key, depth=None)
|
||||
|
||||
section_data = {
|
||||
'section_key': 'course_info',
|
||||
'section_display_name': _('Course Info'),
|
||||
'access': access,
|
||||
'course_id': course_id,
|
||||
'course_org': course_id_dict['org'],
|
||||
'course_num': course_id_dict['course'],
|
||||
'course_name': course_id_dict['name'],
|
||||
'course_id': course_key,
|
||||
'course_display_name': course.display_name,
|
||||
'enrollment_count': CourseEnrollment.num_enrolled_in(course_id),
|
||||
'enrollment_count': CourseEnrollment.num_enrolled_in(course_key),
|
||||
'has_started': course.has_started(),
|
||||
'has_ended': course.has_ended(),
|
||||
'list_instructor_tasks_url': reverse('list_instructor_tasks', kwargs={'course_id': course_id}),
|
||||
'list_instructor_tasks_url': reverse('list_instructor_tasks', kwargs={'course_id': course_key.to_deprecated_string()}),
|
||||
}
|
||||
|
||||
try:
|
||||
@@ -127,44 +125,44 @@ def _section_course_info(course_id, access):
|
||||
section_data['grade_cutoffs'] = reduce(advance, course.grade_cutoffs.items(), "")[:-2]
|
||||
except Exception:
|
||||
section_data['grade_cutoffs'] = "Not Available"
|
||||
# section_data['offline_grades'] = offline_grades_available(course_id)
|
||||
# section_data['offline_grades'] = offline_grades_available(course_key)
|
||||
|
||||
try:
|
||||
section_data['course_errors'] = [(escape(a), '') for (a, _unused) in modulestore().get_item_errors(course.location)]
|
||||
section_data['course_errors'] = [(escape(a), '') for (a, _unused) in modulestore().get_course_errors(course.id)]
|
||||
except Exception:
|
||||
section_data['course_errors'] = [('Error fetching errors', '')]
|
||||
|
||||
return section_data
|
||||
|
||||
|
||||
def _section_membership(course_id, access):
|
||||
def _section_membership(course_key, access):
|
||||
""" Provide data for the corresponding dashboard section """
|
||||
section_data = {
|
||||
'section_key': 'membership',
|
||||
'section_display_name': _('Membership'),
|
||||
'access': access,
|
||||
'enroll_button_url': reverse('students_update_enrollment', kwargs={'course_id': course_id}),
|
||||
'unenroll_button_url': reverse('students_update_enrollment', kwargs={'course_id': course_id}),
|
||||
'modify_beta_testers_button_url': reverse('bulk_beta_modify_access', kwargs={'course_id': course_id}),
|
||||
'list_course_role_members_url': reverse('list_course_role_members', kwargs={'course_id': course_id}),
|
||||
'modify_access_url': reverse('modify_access', kwargs={'course_id': course_id}),
|
||||
'list_forum_members_url': reverse('list_forum_members', kwargs={'course_id': course_id}),
|
||||
'update_forum_role_membership_url': reverse('update_forum_role_membership', kwargs={'course_id': course_id}),
|
||||
'enroll_button_url': reverse('students_update_enrollment', kwargs={'course_id': course_key.to_deprecated_string()}),
|
||||
'unenroll_button_url': reverse('students_update_enrollment', kwargs={'course_id': course_key.to_deprecated_string()}),
|
||||
'modify_beta_testers_button_url': reverse('bulk_beta_modify_access', kwargs={'course_id': course_key.to_deprecated_string()}),
|
||||
'list_course_role_members_url': reverse('list_course_role_members', kwargs={'course_id': course_key.to_deprecated_string()}),
|
||||
'modify_access_url': reverse('modify_access', kwargs={'course_id': course_key.to_deprecated_string()}),
|
||||
'list_forum_members_url': reverse('list_forum_members', kwargs={'course_id': course_key.to_deprecated_string()}),
|
||||
'update_forum_role_membership_url': reverse('update_forum_role_membership', kwargs={'course_id': course_key.to_deprecated_string()}),
|
||||
}
|
||||
return section_data
|
||||
|
||||
|
||||
def _section_student_admin(course_id, access):
|
||||
def _section_student_admin(course_key, access):
|
||||
""" Provide data for the corresponding dashboard section """
|
||||
section_data = {
|
||||
'section_key': 'student_admin',
|
||||
'section_display_name': _('Student Admin'),
|
||||
'access': access,
|
||||
'get_student_progress_url_url': reverse('get_student_progress_url', kwargs={'course_id': course_id}),
|
||||
'enrollment_url': reverse('students_update_enrollment', kwargs={'course_id': course_id}),
|
||||
'reset_student_attempts_url': reverse('reset_student_attempts', kwargs={'course_id': course_id}),
|
||||
'rescore_problem_url': reverse('rescore_problem', kwargs={'course_id': course_id}),
|
||||
'list_instructor_tasks_url': reverse('list_instructor_tasks', kwargs={'course_id': course_id}),
|
||||
'get_student_progress_url_url': reverse('get_student_progress_url', kwargs={'course_id': course_key.to_deprecated_string()}),
|
||||
'enrollment_url': reverse('students_update_enrollment', kwargs={'course_id': course_key.to_deprecated_string()}),
|
||||
'reset_student_attempts_url': reverse('reset_student_attempts', kwargs={'course_id': course_key.to_deprecated_string()}),
|
||||
'rescore_problem_url': reverse('rescore_problem', kwargs={'course_id': course_key.to_deprecated_string()}),
|
||||
'list_instructor_tasks_url': reverse('list_instructor_tasks', kwargs={'course_id': course_key.to_deprecated_string()}),
|
||||
}
|
||||
return section_data
|
||||
|
||||
@@ -174,74 +172,82 @@ def _section_extensions(course):
|
||||
section_data = {
|
||||
'section_key': 'extensions',
|
||||
'section_display_name': _('Extensions'),
|
||||
'units_with_due_dates': [(title_or_url(unit), unit.location.url())
|
||||
'units_with_due_dates': [(title_or_url(unit), unit.location.to_deprecated_string())
|
||||
for unit in get_units_with_due_date(course)],
|
||||
'change_due_date_url': reverse('change_due_date', kwargs={'course_id': course.id}),
|
||||
'reset_due_date_url': reverse('reset_due_date', kwargs={'course_id': course.id}),
|
||||
'show_unit_extensions_url': reverse('show_unit_extensions', kwargs={'course_id': course.id}),
|
||||
'show_student_extensions_url': reverse('show_student_extensions', kwargs={'course_id': course.id}),
|
||||
'change_due_date_url': reverse('change_due_date', kwargs={'course_id': course.id.to_deprecated_string()}),
|
||||
'reset_due_date_url': reverse('reset_due_date', kwargs={'course_id': course.id.to_deprecated_string()}),
|
||||
'show_unit_extensions_url': reverse('show_unit_extensions', kwargs={'course_id': course.id.to_deprecated_string()}),
|
||||
'show_student_extensions_url': reverse('show_student_extensions', kwargs={'course_id': course.id.to_deprecated_string()}),
|
||||
}
|
||||
return section_data
|
||||
|
||||
|
||||
def _section_data_download(course_id, access):
|
||||
def _section_data_download(course_key, access):
|
||||
""" Provide data for the corresponding dashboard section """
|
||||
section_data = {
|
||||
'section_key': 'data_download',
|
||||
'section_display_name': _('Data Download'),
|
||||
'access': access,
|
||||
'get_grading_config_url': reverse('get_grading_config', kwargs={'course_id': course_id}),
|
||||
'get_students_features_url': reverse('get_students_features', kwargs={'course_id': course_id}),
|
||||
'get_anon_ids_url': reverse('get_anon_ids', kwargs={'course_id': course_id}),
|
||||
'list_instructor_tasks_url': reverse('list_instructor_tasks', kwargs={'course_id': course_id}),
|
||||
'list_report_downloads_url': reverse('list_report_downloads', kwargs={'course_id': course_id}),
|
||||
'calculate_grades_csv_url': reverse('calculate_grades_csv', kwargs={'course_id': course_id}),
|
||||
'get_grading_config_url': reverse('get_grading_config', kwargs={'course_id': course_key.to_deprecated_string()}),
|
||||
'get_students_features_url': reverse('get_students_features', kwargs={'course_id': course_key.to_deprecated_string()}),
|
||||
'get_anon_ids_url': reverse('get_anon_ids', kwargs={'course_id': course_key.to_deprecated_string()}),
|
||||
'list_instructor_tasks_url': reverse('list_instructor_tasks', kwargs={'course_id': course_key.to_deprecated_string()}),
|
||||
'list_report_downloads_url': reverse('list_report_downloads', kwargs={'course_id': course_key.to_deprecated_string()}),
|
||||
'calculate_grades_csv_url': reverse('calculate_grades_csv', kwargs={'course_id': course_key.to_deprecated_string()}),
|
||||
}
|
||||
return section_data
|
||||
|
||||
|
||||
def _section_send_email(course_id, access, course):
|
||||
def _section_send_email(course_key, access, course):
|
||||
""" Provide data for the corresponding bulk email section """
|
||||
html_module = HtmlDescriptor(
|
||||
course.system,
|
||||
DictFieldData({'data': ''}),
|
||||
ScopeIds(None, None, None, 'i4x://dummy_org/dummy_course/html/dummy_name')
|
||||
ScopeIds(None, None, None, course_key.make_usage_key('html', 'fake'))
|
||||
)
|
||||
fragment = course.system.render(html_module, 'studio_view')
|
||||
fragment = wrap_xblock('LmsRuntime', html_module, 'studio_view', fragment, None, extra_data={"course-id": course_id})
|
||||
fragment = wrap_xblock(
|
||||
'LmsRuntime', html_module, 'studio_view', fragment, None,
|
||||
extra_data={"course-id": course_key.to_deprecated_string()},
|
||||
usage_id_serializer=lambda usage_id: quote_slashes(usage_id.to_deprecated_string())
|
||||
)
|
||||
email_editor = fragment.content
|
||||
section_data = {
|
||||
'section_key': 'send_email',
|
||||
'section_display_name': _('Email'),
|
||||
'access': access,
|
||||
'send_email': reverse('send_email', kwargs={'course_id': course_id}),
|
||||
'send_email': reverse('send_email', kwargs={'course_id': course_key.to_deprecated_string()}),
|
||||
'editor': email_editor,
|
||||
'list_instructor_tasks_url': reverse('list_instructor_tasks', kwargs={'course_id': course_id}),
|
||||
'email_background_tasks_url': reverse('list_background_email_tasks', kwargs={'course_id': course_id}),
|
||||
'list_instructor_tasks_url': reverse(
|
||||
'list_instructor_tasks', kwargs={'course_id': course_key.to_deprecated_string()}
|
||||
),
|
||||
'email_background_tasks_url': reverse(
|
||||
'list_background_email_tasks', kwargs={'course_id': course_key.to_deprecated_string()}
|
||||
),
|
||||
}
|
||||
return section_data
|
||||
|
||||
|
||||
def _section_analytics(course_id, access):
|
||||
def _section_analytics(course_key, access):
|
||||
""" Provide data for the corresponding dashboard section """
|
||||
section_data = {
|
||||
'section_key': 'analytics',
|
||||
'section_display_name': _('Analytics'),
|
||||
'access': access,
|
||||
'get_distribution_url': reverse('get_distribution', kwargs={'course_id': course_id}),
|
||||
'proxy_legacy_analytics_url': reverse('proxy_legacy_analytics', kwargs={'course_id': course_id}),
|
||||
'get_distribution_url': reverse('get_distribution', kwargs={'course_id': course_key.to_deprecated_string()}),
|
||||
'proxy_legacy_analytics_url': reverse('proxy_legacy_analytics', kwargs={'course_id': course_key.to_deprecated_string()}),
|
||||
}
|
||||
return section_data
|
||||
|
||||
|
||||
def _section_metrics(course_id, access):
|
||||
def _section_metrics(course_key, access):
|
||||
"""Provide data for the corresponding dashboard section """
|
||||
section_data = {
|
||||
'section_key': 'metrics',
|
||||
'section_display_name': ('Metrics'),
|
||||
'access': access,
|
||||
'sub_section_display_name': get_section_display_name(course_id),
|
||||
'section_has_problem': get_array_section_has_problem(course_id),
|
||||
'sub_section_display_name': get_section_display_name(course_key),
|
||||
'section_has_problem': get_array_section_has_problem(course_key),
|
||||
'get_students_opened_subsection_url': reverse('get_students_opened_subsection'),
|
||||
'get_students_problem_grades_url': reverse('get_students_problem_grades'),
|
||||
}
|
||||
|
||||
@@ -25,10 +25,14 @@ from django.utils import timezone
|
||||
|
||||
from xmodule_modifiers import wrap_xblock
|
||||
import xmodule.graders as xmgraders
|
||||
from xmodule.modulestore import XML_MODULESTORE_TYPE, Location
|
||||
from xmodule.modulestore import XML_MODULESTORE_TYPE
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from xmodule.html_module import HtmlDescriptor
|
||||
from xmodule.modulestore.keys import UsageKey
|
||||
from opaque_keys import InvalidKeyError
|
||||
from lms.lib.xblock.runtime import quote_slashes
|
||||
|
||||
# Submissions is a Django app that is currently installed
|
||||
# from the edx-ora2 repo, although it will likely move in the future.
|
||||
@@ -69,6 +73,7 @@ from xblock.fields import ScopeIds
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from microsite_configuration import microsite
|
||||
from xmodule.modulestore.locations import i4xEncoder
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -91,11 +96,12 @@ def split_by_comma_and_whitespace(a_str):
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
def instructor_dashboard(request, course_id):
|
||||
"""Display the instructor dashboard for a course."""
|
||||
course = get_course_with_access(request.user, course_id, 'staff', depth=None)
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
course = get_course_with_access(request.user, 'staff', course_key, depth=None)
|
||||
|
||||
instructor_access = has_access(request.user, course, 'instructor') # an instructor can manage staff lists
|
||||
instructor_access = has_access(request.user, 'instructor', course) # an instructor can manage staff lists
|
||||
|
||||
forum_admin_access = has_forum_access(request.user, course_id, FORUM_ROLE_ADMINISTRATOR)
|
||||
forum_admin_access = has_forum_access(request.user, course_key, FORUM_ROLE_ADMINISTRATOR)
|
||||
|
||||
msg = ''
|
||||
email_msg = ''
|
||||
@@ -115,7 +121,7 @@ def instructor_dashboard(request, course_id):
|
||||
else:
|
||||
idash_mode = request.session.get('idash_mode', 'Grades')
|
||||
|
||||
enrollment_number = CourseEnrollment.num_enrolled_in(course_id)
|
||||
enrollment_number = CourseEnrollment.num_enrolled_in(course_key)
|
||||
|
||||
# assemble some course statistics for output to instructor
|
||||
def get_course_stats_table():
|
||||
@@ -131,7 +137,10 @@ def instructor_dashboard(request, course_id):
|
||||
if getattr(field.scope, 'user', False):
|
||||
continue
|
||||
|
||||
data.append([field.name, json.dumps(field.read_json(course))])
|
||||
data.append([
|
||||
field.name,
|
||||
json.dumps(field.read_json(course), cls=i4xEncoder)
|
||||
])
|
||||
datatable['data'] = data
|
||||
return datatable
|
||||
|
||||
@@ -158,29 +167,6 @@ def instructor_dashboard(request, course_id):
|
||||
writer.writerow(encoded_row)
|
||||
return response
|
||||
|
||||
def get_module_url(urlname):
|
||||
"""
|
||||
Construct full URL for a module from its urlname.
|
||||
|
||||
Form is either urlname or modulename/urlname. If no modulename
|
||||
is provided, "problem" is assumed.
|
||||
"""
|
||||
# remove whitespace
|
||||
urlname = strip_if_string(urlname)
|
||||
|
||||
# tolerate an XML suffix in the urlname
|
||||
if urlname[-4:] == ".xml":
|
||||
urlname = urlname[:-4]
|
||||
|
||||
# implement default
|
||||
if '/' not in urlname:
|
||||
urlname = "problem/" + urlname
|
||||
|
||||
# complete the url using information about the current course:
|
||||
parts = Location.parse_course_id(course_id)
|
||||
parts['url'] = urlname
|
||||
return u"i4x://{org}/{course}/{url}".format(**parts)
|
||||
|
||||
def get_student_from_identifier(unique_student_identifier):
|
||||
"""Gets a student object using either an email address or username"""
|
||||
unique_student_identifier = strip_if_string(unique_student_identifier)
|
||||
@@ -216,52 +202,52 @@ def instructor_dashboard(request, course_id):
|
||||
track.views.server_track(request, "git-pull", {"directory": data_dir}, page="idashboard")
|
||||
|
||||
if 'Reload course' in action:
|
||||
log.debug('reloading {0} ({1})'.format(course_id, course))
|
||||
log.debug('reloading {0} ({1})'.format(course_key, course))
|
||||
try:
|
||||
data_dir = course.data_dir
|
||||
modulestore().try_load_course(data_dir)
|
||||
msg += "<br/><p>Course reloaded from {0}</p>".format(data_dir)
|
||||
track.views.server_track(request, "reload", {"directory": data_dir}, page="idashboard")
|
||||
course_errors = modulestore().get_item_errors(course.location)
|
||||
course_errors = modulestore().get_course_errors(course.id)
|
||||
msg += '<ul>'
|
||||
for cmsg, cerr in course_errors:
|
||||
msg += "<li>{0}: <pre>{1}</pre>".format(cmsg, escape(cerr))
|
||||
msg += '</ul>'
|
||||
except Exception as err:
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
msg += '<br/><p>Error: {0}</p>'.format(escape(err))
|
||||
|
||||
if action == 'Dump list of enrolled students' or action == 'List enrolled students':
|
||||
log.debug(action)
|
||||
datatable = get_student_grade_summary_data(request, course, course_id, get_grades=False, use_offline=use_offline)
|
||||
datatable['title'] = _('List of students enrolled in {course_id}').format(course_id=course_id)
|
||||
datatable = get_student_grade_summary_data(request, course, course_key, get_grades=False, use_offline=use_offline)
|
||||
datatable['title'] = _('List of students enrolled in {course_key}').format(course_key=course_key.to_deprecated_string())
|
||||
track.views.server_track(request, "list-students", {}, page="idashboard")
|
||||
|
||||
elif 'Dump Grades' in action:
|
||||
log.debug(action)
|
||||
datatable = get_student_grade_summary_data(request, course, course_id, get_grades=True, use_offline=use_offline)
|
||||
datatable['title'] = _('Summary Grades of students enrolled in {course_id}').format(course_id=course_id)
|
||||
datatable = get_student_grade_summary_data(request, course, course_key, get_grades=True, use_offline=use_offline)
|
||||
datatable['title'] = _('Summary Grades of students enrolled in {course_key}').format(course_key=course_key.to_deprecated_string())
|
||||
track.views.server_track(request, "dump-grades", {}, page="idashboard")
|
||||
|
||||
elif 'Dump all RAW grades' in action:
|
||||
log.debug(action)
|
||||
datatable = get_student_grade_summary_data(request, course, course_id, get_grades=True,
|
||||
datatable = get_student_grade_summary_data(request, course, course_key, get_grades=True,
|
||||
get_raw_scores=True, use_offline=use_offline)
|
||||
datatable['title'] = _('Raw Grades of students enrolled in {course_id}').format(course_id=course_id)
|
||||
datatable['title'] = _('Raw Grades of students enrolled in {course_key}').format(course_key=course_key)
|
||||
track.views.server_track(request, "dump-grades-raw", {}, page="idashboard")
|
||||
|
||||
elif 'Download CSV of all student grades' in action:
|
||||
track.views.server_track(request, "dump-grades-csv", {}, page="idashboard")
|
||||
return return_csv('grades_{0}.csv'.format(course_id),
|
||||
get_student_grade_summary_data(request, course, course_id, use_offline=use_offline))
|
||||
return return_csv('grades_{0}.csv'.format(course_key.to_deprecated_string()),
|
||||
get_student_grade_summary_data(request, course, course_key, use_offline=use_offline))
|
||||
|
||||
elif 'Download CSV of all RAW grades' in action:
|
||||
track.views.server_track(request, "dump-grades-csv-raw", {}, page="idashboard")
|
||||
return return_csv('grades_{0}_raw.csv'.format(course_id),
|
||||
get_student_grade_summary_data(request, course, course_id, get_raw_scores=True, use_offline=use_offline))
|
||||
return return_csv('grades_{0}_raw.csv'.format(course_key.to_deprecated_string()),
|
||||
get_student_grade_summary_data(request, course, course_key, get_raw_scores=True, use_offline=use_offline))
|
||||
|
||||
elif 'Download CSV of answer distributions' in action:
|
||||
track.views.server_track(request, "dump-answer-dist-csv", {}, page="idashboard")
|
||||
return return_csv('answer_dist_{0}.csv'.format(course_id), get_answers_distribution(request, course_id))
|
||||
return return_csv('answer_dist_{0}.csv'.format(course_key.to_deprecated_string()), get_answers_distribution(request, course_key))
|
||||
|
||||
elif 'Dump description of graded assignments configuration' in action:
|
||||
# what is "graded assignments configuration"?
|
||||
@@ -269,55 +255,72 @@ def instructor_dashboard(request, course_id):
|
||||
msg += dump_grading_context(course)
|
||||
|
||||
elif "Rescore ALL students' problem submissions" in action:
|
||||
problem_urlname = request.POST.get('problem_for_all_students', '')
|
||||
problem_url = get_module_url(problem_urlname)
|
||||
problem_location_str = strip_if_string(request.POST.get('problem_for_all_students', ''))
|
||||
try:
|
||||
instructor_task = submit_rescore_problem_for_all_students(request, course_id, problem_url)
|
||||
problem_location = UsageKey.from_string(problem_location_str)
|
||||
instructor_task = submit_rescore_problem_for_all_students(request, problem_location)
|
||||
if instructor_task is None:
|
||||
msg += '<font color="red">{text}</font>'.format(
|
||||
text=_('Failed to create a background task for rescoring "{problem_url}".').format(
|
||||
problem_url=problem_url
|
||||
problem_url=problem_location_str
|
||||
)
|
||||
)
|
||||
else:
|
||||
track.views.server_track(request, "rescore-all-submissions", {"problem": problem_url, "course": course_id}, page="idashboard")
|
||||
except ItemNotFoundError as err:
|
||||
track.views.server_track(
|
||||
request,
|
||||
"rescore-all-submissions",
|
||||
{
|
||||
"problem": problem_location_str,
|
||||
"course": course_key.to_deprecated_string()
|
||||
},
|
||||
page="idashboard"
|
||||
)
|
||||
|
||||
except (InvalidKeyError, ItemNotFoundError) as err:
|
||||
msg += '<font color="red">{text}</font>'.format(
|
||||
text=_('Failed to create a background task for rescoring "{problem_url}": problem not found.').format(
|
||||
problem_url=problem_url
|
||||
problem_url=problem_location_str
|
||||
)
|
||||
)
|
||||
except Exception as err:
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
log.error("Encountered exception from rescore: {0}".format(err))
|
||||
msg += '<font color="red">{text}</font>'.format(
|
||||
text=_('Failed to create a background task for rescoring "{url}": {message}.').format(
|
||||
url=problem_url, message=err.message
|
||||
url=problem_location_str, message=err.message
|
||||
)
|
||||
)
|
||||
|
||||
elif "Reset ALL students' attempts" in action:
|
||||
problem_urlname = request.POST.get('problem_for_all_students', '')
|
||||
problem_url = get_module_url(problem_urlname)
|
||||
problem_location_str = strip_if_string(request.POST.get('problem_for_all_students', ''))
|
||||
try:
|
||||
instructor_task = submit_reset_problem_attempts_for_all_students(request, course_id, problem_url)
|
||||
problem_location = UsageKey.from_string(problem_location_str)
|
||||
instructor_task = submit_reset_problem_attempts_for_all_students(request, problem_location)
|
||||
if instructor_task is None:
|
||||
msg += '<font color="red">{text}</font>'.format(
|
||||
text=_('Failed to create a background task for resetting "{problem_url}".').format(problem_url=problem_url)
|
||||
text=_('Failed to create a background task for resetting "{problem_url}".').format(problem_url=problem_location_str)
|
||||
)
|
||||
else:
|
||||
track.views.server_track(request, "reset-all-attempts", {"problem": problem_url, "course": course_id}, page="idashboard")
|
||||
except ItemNotFoundError as err:
|
||||
track.views.server_track(
|
||||
request,
|
||||
"reset-all-attempts",
|
||||
{
|
||||
"problem": problem_location_str,
|
||||
"course": course_key.to_deprecated_string()
|
||||
},
|
||||
page="idashboard"
|
||||
)
|
||||
except (InvalidKeyError, ItemNotFoundError) as err:
|
||||
log.error('Failure to reset: unknown problem "{0}"'.format(err))
|
||||
msg += '<font color="red">{text}</font>'.format(
|
||||
text=_('Failed to create a background task for resetting "{problem_url}": problem not found.').format(
|
||||
problem_url=problem_url
|
||||
problem_url=problem_location_str
|
||||
)
|
||||
)
|
||||
except Exception as err:
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
log.error("Encountered exception from reset: {0}".format(err))
|
||||
msg += '<font color="red">{text}</font>'.format(
|
||||
text=_('Failed to create a background task for resetting "{url}": {message}.').format(
|
||||
url=problem_url, message=err.message
|
||||
url=problem_location_str, message=err.message
|
||||
)
|
||||
)
|
||||
|
||||
@@ -328,16 +331,32 @@ def instructor_dashboard(request, course_id):
|
||||
if student is None:
|
||||
msg += message
|
||||
else:
|
||||
problem_urlname = request.POST.get('problem_for_student', '')
|
||||
problem_url = get_module_url(problem_urlname)
|
||||
message, datatable = get_background_task_table(course_id, problem_url, student)
|
||||
msg += message
|
||||
problem_location_str = strip_if_string(request.POST.get('problem_for_student', ''))
|
||||
try:
|
||||
problem_location = UsageKey.from_string(problem_location_str)
|
||||
except InvalidKeyError:
|
||||
msg += '<font color="red">{text}</font>'.format(
|
||||
text=_('Could not find problem location "{url}".').format(
|
||||
url=problem_location_str
|
||||
)
|
||||
)
|
||||
else:
|
||||
message, datatable = get_background_task_table(course_key, problem_location, student)
|
||||
msg += message
|
||||
|
||||
elif "Show Background Task History" in action:
|
||||
problem_urlname = request.POST.get('problem_for_all_students', '')
|
||||
problem_url = get_module_url(problem_urlname)
|
||||
message, datatable = get_background_task_table(course_id, problem_url)
|
||||
msg += message
|
||||
problem_location = strip_if_string(request.POST.get('problem_for_all_students', ''))
|
||||
try:
|
||||
problem_location = UsageKey.from_string(problem_location_str)
|
||||
except InvalidKeyError:
|
||||
msg += '<font color="red">{text}</font>'.format(
|
||||
text=_('Could not find problem location "{url}".').format(
|
||||
url=problem_location_str
|
||||
)
|
||||
)
|
||||
else:
|
||||
message, datatable = get_background_task_table(course_key, problem_location)
|
||||
msg += message
|
||||
|
||||
elif ("Reset student's attempts" in action or
|
||||
"Delete student state for module" in action or
|
||||
@@ -346,119 +365,135 @@ def instructor_dashboard(request, course_id):
|
||||
unique_student_identifier = request.POST.get(
|
||||
'unique_student_identifier', ''
|
||||
)
|
||||
problem_urlname = request.POST.get('problem_for_student', '')
|
||||
module_state_key = get_module_url(problem_urlname)
|
||||
# try to uniquely id student by email address or username
|
||||
message, student = get_student_from_identifier(unique_student_identifier)
|
||||
msg += message
|
||||
student_module = None
|
||||
if student is not None:
|
||||
|
||||
# Reset the student's score in the submissions API
|
||||
# Currently this is used only by open assessment (ORA 2)
|
||||
# We need to do this *before* retrieving the `StudentModule` model,
|
||||
# because it's possible for a score to exist even if no student module exists.
|
||||
if "Delete student state for module" in action:
|
||||
try:
|
||||
sub_api.reset_score(
|
||||
anonymous_id_for_user(student, course_id),
|
||||
course_id,
|
||||
module_state_key,
|
||||
)
|
||||
except sub_api.SubmissionError:
|
||||
# Trust the submissions API to log the error
|
||||
error_msg = _("An error occurred while deleting the score.")
|
||||
msg += "<font color='red'>{err}</font> ".format(err=error_msg)
|
||||
|
||||
# find the module in question
|
||||
try:
|
||||
student_module = StudentModule.objects.get(
|
||||
student_id=student.id,
|
||||
course_id=course_id,
|
||||
module_state_key=module_state_key
|
||||
problem_location_str = strip_if_string(request.POST.get('problem_for_student', ''))
|
||||
try:
|
||||
module_state_key = UsageKey.from_string(problem_location_str)
|
||||
except InvalidKeyError:
|
||||
msg += '<font color="red">{text}</font>'.format(
|
||||
text=_('Could not find problem location "{url}".').format(
|
||||
url=problem_location_str
|
||||
)
|
||||
msg += _("Found module. ")
|
||||
)
|
||||
else:
|
||||
# try to uniquely id student by email address or username
|
||||
message, student = get_student_from_identifier(unique_student_identifier)
|
||||
msg += message
|
||||
student_module = None
|
||||
if student is not None:
|
||||
# Reset the student's score in the submissions API
|
||||
# Currently this is used only by open assessment (ORA 2)
|
||||
# We need to do this *before* retrieving the `StudentModule` model,
|
||||
# because it's possible for a score to exist even if no student module exists.
|
||||
if "Delete student state for module" in action:
|
||||
try:
|
||||
sub_api.reset_score(
|
||||
anonymous_id_for_user(student, course_key),
|
||||
course_key,
|
||||
module_state_key,
|
||||
)
|
||||
except sub_api.SubmissionError:
|
||||
# Trust the submissions API to log the error
|
||||
error_msg = _("An error occurred while deleting the score.")
|
||||
msg += "<font color='red'>{err}</font> ".format(err=error_msg)
|
||||
|
||||
except StudentModule.DoesNotExist as err:
|
||||
error_msg = _("Couldn't find module with that urlname: {url}. ").format(url=problem_urlname)
|
||||
msg += "<font color='red'>{err_msg} ({err})</font>".format(err_msg=error_msg, err=err)
|
||||
log.debug(error_msg)
|
||||
|
||||
if student_module is not None:
|
||||
if "Delete student state for module" in action:
|
||||
# delete the state
|
||||
# find the module in question
|
||||
try:
|
||||
student_module.delete()
|
||||
student_module = StudentModule.objects.get(
|
||||
student_id=student.id,
|
||||
course_id=course_key,
|
||||
module_id=module_state_key
|
||||
)
|
||||
msg += _("Found module. ")
|
||||
|
||||
msg += "<font color='red'>{text}</font>".format(
|
||||
text=_("Deleted student module state for {state}!").format(state=module_state_key)
|
||||
)
|
||||
event = {
|
||||
"problem": module_state_key,
|
||||
"student": unique_student_identifier,
|
||||
"course": course_id
|
||||
}
|
||||
track.views.server_track(
|
||||
request,
|
||||
"delete-student-module-state",
|
||||
event,
|
||||
page="idashboard"
|
||||
)
|
||||
except Exception as err:
|
||||
error_msg = _("Failed to delete module state for {id}/{url}. ").format(
|
||||
id=unique_student_identifier, url=problem_urlname
|
||||
)
|
||||
except StudentModule.DoesNotExist as err:
|
||||
error_msg = _("Couldn't find module with that urlname: {url}. ").format(url=problem_location_str)
|
||||
msg += "<font color='red'>{err_msg} ({err})</font>".format(err_msg=error_msg, err=err)
|
||||
log.exception(error_msg)
|
||||
elif "Reset student's attempts" in action:
|
||||
# modify the problem's state
|
||||
try:
|
||||
# load the state json
|
||||
problem_state = json.loads(student_module.state)
|
||||
old_number_of_attempts = problem_state["attempts"]
|
||||
problem_state["attempts"] = 0
|
||||
# save
|
||||
student_module.state = json.dumps(problem_state)
|
||||
student_module.save()
|
||||
event = {
|
||||
"old_attempts": old_number_of_attempts,
|
||||
"student": unicode(student),
|
||||
"problem": student_module.module_state_key,
|
||||
"instructor": unicode(request.user),
|
||||
"course": course_id
|
||||
}
|
||||
track.views.server_track(request, "reset-student-attempts", event, page="idashboard")
|
||||
msg += "<font color='green'>{text}</font>".format(
|
||||
text=_("Module state successfully reset!")
|
||||
)
|
||||
except Exception as err:
|
||||
error_msg = _("Couldn't reset module state for {id}/{url}. ").format(
|
||||
id=unique_student_identifier, url=problem_urlname
|
||||
)
|
||||
msg += "<font color='red'>{err_msg} ({err})</font>".format(err_msg=error_msg, err=err)
|
||||
log.exception(error_msg)
|
||||
else:
|
||||
# "Rescore student's problem submission" case
|
||||
try:
|
||||
instructor_task = submit_rescore_problem_for_student(request, course_id, module_state_key, student)
|
||||
if instructor_task is None:
|
||||
log.debug(error_msg)
|
||||
|
||||
if student_module is not None:
|
||||
if "Delete student state for module" in action:
|
||||
# delete the state
|
||||
try:
|
||||
student_module.delete()
|
||||
|
||||
msg += "<font color='red'>{text}</font>".format(
|
||||
text=_("Deleted student module state for {state}!").format(state=module_state_key)
|
||||
)
|
||||
event = {
|
||||
"problem": problem_location_str,
|
||||
"student": unique_student_identifier,
|
||||
"course": course_key.to_deprecated_string()
|
||||
}
|
||||
track.views.server_track(
|
||||
request,
|
||||
"delete-student-module-state",
|
||||
event,
|
||||
page="idashboard"
|
||||
)
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
error_msg = _("Failed to delete module state for {id}/{url}. ").format(
|
||||
id=unique_student_identifier, url=problem_location_str
|
||||
)
|
||||
msg += "<font color='red'>{err_msg} ({err})</font>".format(err_msg=error_msg, err=err)
|
||||
log.exception(error_msg)
|
||||
elif "Reset student's attempts" in action:
|
||||
# modify the problem's state
|
||||
try:
|
||||
# load the state json
|
||||
problem_state = json.loads(student_module.state)
|
||||
old_number_of_attempts = problem_state["attempts"]
|
||||
problem_state["attempts"] = 0
|
||||
# save
|
||||
student_module.state = json.dumps(problem_state)
|
||||
student_module.save()
|
||||
event = {
|
||||
"old_attempts": old_number_of_attempts,
|
||||
"student": unicode(student),
|
||||
"problem": student_module.module_state_key,
|
||||
"instructor": unicode(request.user),
|
||||
"course": course_key.to_deprecated_string()
|
||||
}
|
||||
track.views.server_track(request, "reset-student-attempts", event, page="idashboard")
|
||||
msg += "<font color='green'>{text}</font>".format(
|
||||
text=_("Module state successfully reset!")
|
||||
)
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
error_msg = _("Couldn't reset module state for {id}/{url}. ").format(
|
||||
id=unique_student_identifier, url=problem_location_str
|
||||
)
|
||||
msg += "<font color='red'>{err_msg} ({err})</font>".format(err_msg=error_msg, err=err)
|
||||
log.exception(error_msg)
|
||||
else:
|
||||
# "Rescore student's problem submission" case
|
||||
try:
|
||||
instructor_task = submit_rescore_problem_for_student(request, module_state_key, student)
|
||||
if instructor_task is None:
|
||||
msg += '<font color="red">{text}</font>'.format(
|
||||
text=_('Failed to create a background task for rescoring "{key}" for student {id}.').format(
|
||||
key=module_state_key, id=unique_student_identifier
|
||||
)
|
||||
)
|
||||
else:
|
||||
track.views.server_track(
|
||||
request,
|
||||
"rescore-student-submission",
|
||||
{
|
||||
"problem": module_state_key,
|
||||
"student": unique_student_identifier,
|
||||
"course": course_key.to_deprecated_string()
|
||||
},
|
||||
page="idashboard"
|
||||
)
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
msg += '<font color="red">{text}</font>'.format(
|
||||
text=_('Failed to create a background task for rescoring "{key}" for student {id}.').format(
|
||||
key=module_state_key, id=unique_student_identifier
|
||||
text=_('Failed to create a background task for rescoring "{key}": {id}.').format(
|
||||
key=module_state_key, id=err.message
|
||||
)
|
||||
)
|
||||
else:
|
||||
track.views.server_track(request, "rescore-student-submission", {"problem": module_state_key, "student": unique_student_identifier, "course": course_id}, page="idashboard")
|
||||
except Exception as err:
|
||||
msg += '<font color="red">{text}</font>'.format(
|
||||
text=_('Failed to create a background task for rescoring "{key}": {id}.').format(
|
||||
key=module_state_key, id=err.message
|
||||
log.exception("Encountered exception from rescore: student '{0}' problem '{1}'".format(
|
||||
unique_student_identifier, module_state_key
|
||||
)
|
||||
)
|
||||
)
|
||||
log.exception("Encountered exception from rescore: student '{0}' problem '{1}'".format(
|
||||
unique_student_identifier, module_state_key
|
||||
)
|
||||
)
|
||||
|
||||
elif "Get link to student's progress page" in action:
|
||||
unique_student_identifier = request.POST.get('unique_student_identifier', '')
|
||||
@@ -466,8 +501,20 @@ def instructor_dashboard(request, course_id):
|
||||
message, student = get_student_from_identifier(unique_student_identifier)
|
||||
msg += message
|
||||
if student is not None:
|
||||
progress_url = reverse('student_progress', kwargs={'course_id': course_id, 'student_id': student.id})
|
||||
track.views.server_track(request, "get-student-progress-page", {"student": unicode(student), "instructor": unicode(request.user), "course": course_id}, page="idashboard")
|
||||
progress_url = reverse('student_progress', kwargs={
|
||||
'course_id': course_key.to_deprecated_string(),
|
||||
'student_id': student.id
|
||||
})
|
||||
track.views.server_track(
|
||||
request,
|
||||
"get-student-progress-page",
|
||||
{
|
||||
"student": unicode(student),
|
||||
"instructor": unicode(request.user),
|
||||
"course": course_key.to_deprecated_string()
|
||||
},
|
||||
page="idashboard"
|
||||
)
|
||||
msg += "<a href='{url}' target='_blank'>{text}</a>.".format(
|
||||
url=progress_url,
|
||||
text=_("Progress page for username: {username} with email address: {email}").format(
|
||||
@@ -484,7 +531,7 @@ def instructor_dashboard(request, course_id):
|
||||
|
||||
elif action == 'List assignments available for this course':
|
||||
log.debug(action)
|
||||
allgrades = get_student_grade_summary_data(request, course, course_id, get_grades=True, use_offline=use_offline)
|
||||
allgrades = get_student_grade_summary_data(request, course, course_key, get_grades=True, use_offline=use_offline)
|
||||
|
||||
assignments = [[x] for x in allgrades['assignments']]
|
||||
datatable = {'header': [_('Assignment Name')]}
|
||||
@@ -494,7 +541,7 @@ def instructor_dashboard(request, course_id):
|
||||
msg += 'assignments=<pre>%s</pre>' % assignments
|
||||
|
||||
elif action == 'List enrolled students matching remote gradebook':
|
||||
stud_data = get_student_grade_summary_data(request, course, course_id, get_grades=False, use_offline=use_offline)
|
||||
stud_data = get_student_grade_summary_data(request, course, course_key, get_grades=False, use_offline=use_offline)
|
||||
msg2, rg_stud_data = _do_remote_gradebook(request.user, course, 'get-membership')
|
||||
datatable = {'header': ['Student email', 'Match?']}
|
||||
rg_students = [x['email'] for x in rg_stud_data['retdata']]
|
||||
@@ -513,7 +560,7 @@ def instructor_dashboard(request, course_id):
|
||||
if not aname:
|
||||
msg += "<font color='red'>{text}</font>".format(text=_("Please enter an assignment name"))
|
||||
else:
|
||||
allgrades = get_student_grade_summary_data(request, course, course_id, get_grades=True, use_offline=use_offline)
|
||||
allgrades = get_student_grade_summary_data(request, course, course_key, get_grades=True, use_offline=use_offline)
|
||||
if aname not in allgrades['assignments']:
|
||||
msg += "<font color='red'>{text}</font>".format(
|
||||
text=_("Invalid assignment name '{name}'").format(name=aname)
|
||||
@@ -522,12 +569,12 @@ def instructor_dashboard(request, course_id):
|
||||
aidx = allgrades['assignments'].index(aname)
|
||||
datatable = {'header': [_('External email'), aname]}
|
||||
ddata = []
|
||||
for x in allgrades['students']: # do one by one in case there is a student who has only partial grades
|
||||
for student in allgrades['students']: # do one by one in case there is a student who has only partial grades
|
||||
try:
|
||||
ddata.append([x.email, x.grades[aidx]])
|
||||
ddata.append([student.email, student.grades[aidx]])
|
||||
except IndexError:
|
||||
log.debug('No grade for assignment {idx} ({name}) for student {email}'.format(
|
||||
idx=aidx, name=aname, email=x.email)
|
||||
idx=aidx, name=aname, email=student.email)
|
||||
)
|
||||
datatable['data'] = ddata
|
||||
|
||||
@@ -549,33 +596,33 @@ def instructor_dashboard(request, course_id):
|
||||
# Admin
|
||||
|
||||
elif 'List course staff' in action:
|
||||
role = CourseStaffRole(course.location)
|
||||
datatable = _role_members_table(role, _("List of Staff"), course_id)
|
||||
role = CourseStaffRole(course.id)
|
||||
datatable = _role_members_table(role, _("List of Staff"), course_key)
|
||||
track.views.server_track(request, "list-staff", {}, page="idashboard")
|
||||
|
||||
elif 'List course instructors' in action and GlobalStaff().has_user(request.user):
|
||||
role = CourseInstructorRole(course.location)
|
||||
datatable = _role_members_table(role, _("List of Instructors"), course_id)
|
||||
role = CourseInstructorRole(course.id)
|
||||
datatable = _role_members_table(role, _("List of Instructors"), course_key)
|
||||
track.views.server_track(request, "list-instructors", {}, page="idashboard")
|
||||
|
||||
elif action == 'Add course staff':
|
||||
uname = request.POST['staffuser']
|
||||
role = CourseStaffRole(course.location)
|
||||
role = CourseStaffRole(course.id)
|
||||
msg += add_user_to_role(request, uname, role, 'staff', 'staff')
|
||||
|
||||
elif action == 'Add instructor' and request.user.is_staff:
|
||||
uname = request.POST['instructor']
|
||||
role = CourseInstructorRole(course.location)
|
||||
role = CourseInstructorRole(course.id)
|
||||
msg += add_user_to_role(request, uname, role, 'instructor', 'instructor')
|
||||
|
||||
elif action == 'Remove course staff':
|
||||
uname = request.POST['staffuser']
|
||||
role = CourseStaffRole(course.location)
|
||||
role = CourseStaffRole(course.id)
|
||||
msg += remove_user_from_role(request, uname, role, 'staff', 'staff')
|
||||
|
||||
elif action == 'Remove instructor' and request.user.is_staff:
|
||||
uname = request.POST['instructor']
|
||||
role = CourseInstructorRole(course.location)
|
||||
role = CourseInstructorRole(course.id)
|
||||
msg += remove_user_from_role(request, uname, role, 'instructor', 'instructor')
|
||||
|
||||
#----------------------------------------
|
||||
@@ -583,20 +630,28 @@ def instructor_dashboard(request, course_id):
|
||||
|
||||
elif 'Download CSV of all student profile data' in action:
|
||||
enrolled_students = User.objects.filter(
|
||||
courseenrollment__course_id=course_id,
|
||||
courseenrollment__course_id=course_key,
|
||||
courseenrollment__is_active=1,
|
||||
).order_by('username').select_related("profile")
|
||||
profkeys = ['name', 'language', 'location', 'year_of_birth', 'gender', 'level_of_education',
|
||||
'mailing_address', 'goals']
|
||||
datatable = {'header': ['username', 'email'] + profkeys}
|
||||
|
||||
def getdat(u):
|
||||
p = u.profile
|
||||
return [u.username, u.email] + [getattr(p, x, '') for x in profkeys]
|
||||
def getdat(user):
|
||||
"""
|
||||
Return a list of profile data for the given user.
|
||||
"""
|
||||
profile = user.profile
|
||||
return [user.username, user.email] + [getattr(profile, xkey, '') for xkey in profkeys]
|
||||
|
||||
datatable['data'] = [getdat(u) for u in enrolled_students]
|
||||
datatable['title'] = _('Student profile data for course {course_id}').format(course_id = course_id)
|
||||
return return_csv('profiledata_{course_id}.csv'.format(course_id = course_id), datatable)
|
||||
datatable['title'] = _('Student profile data for course {course_id}').format(
|
||||
course_id=course_key.to_deprecated_string()
|
||||
)
|
||||
return return_csv(
|
||||
'profiledata_{course_id}.csv'.format(course_id=course_key.to_deprecated_string()),
|
||||
datatable
|
||||
)
|
||||
|
||||
elif 'Download CSV of all responses to problem' in action:
|
||||
problem_to_dump = request.POST.get('problem_to_dump', '')
|
||||
@@ -604,15 +659,14 @@ def instructor_dashboard(request, course_id):
|
||||
if problem_to_dump[-4:] == ".xml":
|
||||
problem_to_dump = problem_to_dump[:-4]
|
||||
try:
|
||||
course_id_dict = Location.parse_course_id(course_id)
|
||||
module_state_key = u"i4x://{org}/{course}/problem/{0}".format(problem_to_dump, **course_id_dict)
|
||||
module_state_key = course_key.make_usage_key(block_type='problem', name=problem_to_dump)
|
||||
smdat = StudentModule.objects.filter(
|
||||
course_id=course_id,
|
||||
course_id=course_key,
|
||||
module_state_key=module_state_key
|
||||
)
|
||||
smdat = smdat.order_by('student')
|
||||
msg += _("Found {num} records to dump.").format(num=smdat)
|
||||
except Exception as err:
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
msg += "<font color='red'>{text}</font><pre>{err}</pre>".format(
|
||||
text=_("Couldn't find module with that urlname."),
|
||||
err=escape(err)
|
||||
@@ -622,37 +676,37 @@ def instructor_dashboard(request, course_id):
|
||||
if smdat:
|
||||
datatable = {'header': ['username', 'state']}
|
||||
datatable['data'] = [[x.student.username, x.state] for x in smdat]
|
||||
datatable['title'] = _('Student state for problem {problem}').format(problem = problem_to_dump)
|
||||
return return_csv('student_state_from_{problem}.csv'.format(problem = problem_to_dump), datatable)
|
||||
datatable['title'] = _('Student state for problem {problem}').format(problem=problem_to_dump)
|
||||
return return_csv('student_state_from_{problem}.csv'.format(problem=problem_to_dump), datatable)
|
||||
|
||||
elif 'Download CSV of all student anonymized IDs' in action:
|
||||
students = User.objects.filter(
|
||||
courseenrollment__course_id=course_id,
|
||||
courseenrollment__course_id=course_key,
|
||||
).order_by('id')
|
||||
|
||||
datatable = {'header': ['User ID', 'Anonymized user ID']}
|
||||
datatable['data'] = [[s.id, unique_id_for_user(s)] for s in students]
|
||||
return return_csv(course_id.replace('/', '-') + '-anon-ids.csv', datatable)
|
||||
return return_csv(course_key.to_deprecated_string().replace('/', '-') + '-anon-ids.csv', datatable)
|
||||
|
||||
#----------------------------------------
|
||||
# Group management
|
||||
|
||||
elif 'List beta testers' in action:
|
||||
role = CourseBetaTesterRole(course.location)
|
||||
datatable = _role_members_table(role, _("List of Beta Testers"), course_id)
|
||||
role = CourseBetaTesterRole(course.id)
|
||||
datatable = _role_members_table(role, _("List of Beta Testers"), course_key)
|
||||
track.views.server_track(request, "list-beta-testers", {}, page="idashboard")
|
||||
|
||||
elif action == 'Add beta testers':
|
||||
users = request.POST['betausers']
|
||||
log.debug("users: {0!r}".format(users))
|
||||
role = CourseBetaTesterRole(course.location)
|
||||
role = CourseBetaTesterRole(course.id)
|
||||
for username_or_email in split_by_comma_and_whitespace(users):
|
||||
msg += "<p>{0}</p>".format(
|
||||
add_user_to_role(request, username_or_email, role, 'beta testers', 'beta-tester'))
|
||||
|
||||
elif action == 'Remove beta testers':
|
||||
users = request.POST['betausers']
|
||||
role = CourseBetaTesterRole(course.location)
|
||||
role = CourseBetaTesterRole(course.id)
|
||||
for username_or_email in split_by_comma_and_whitespace(users):
|
||||
msg += "<p>{0}</p>".format(
|
||||
remove_user_from_role(request, username_or_email, role, 'beta testers', 'beta-tester'))
|
||||
@@ -663,56 +717,85 @@ def instructor_dashboard(request, course_id):
|
||||
elif action == 'List course forum admins':
|
||||
rolename = FORUM_ROLE_ADMINISTRATOR
|
||||
datatable = {}
|
||||
msg += _list_course_forum_members(course_id, rolename, datatable)
|
||||
track.views.server_track(request, "list-forum-admins", {"course": course_id}, page="idashboard")
|
||||
msg += _list_course_forum_members(course_key, rolename, datatable)
|
||||
track.views.server_track(
|
||||
request, "list-forum-admins", {"course": course_key.to_deprecated_string()}, page="idashboard"
|
||||
)
|
||||
|
||||
elif action == 'Remove forum admin':
|
||||
uname = request.POST['forumadmin']
|
||||
msg += _update_forum_role_membership(uname, course, FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_REMOVE)
|
||||
track.views.server_track(request, "remove-forum-admin", {"username": uname, "course": course_id}, page="idashboard")
|
||||
track.views.server_track(
|
||||
request, "remove-forum-admin", {"username": uname, "course": course_key.to_deprecated_string()},
|
||||
page="idashboard"
|
||||
)
|
||||
|
||||
elif action == 'Add forum admin':
|
||||
uname = request.POST['forumadmin']
|
||||
msg += _update_forum_role_membership(uname, course, FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_ADD)
|
||||
track.views.server_track(request, "add-forum-admin", {"username": uname, "course": course_id}, page="idashboard")
|
||||
track.views.server_track(
|
||||
request, "add-forum-admin", {"username": uname, "course": course_key.to_deprecated_string()},
|
||||
page="idashboard"
|
||||
)
|
||||
|
||||
elif action == 'List course forum moderators':
|
||||
rolename = FORUM_ROLE_MODERATOR
|
||||
datatable = {}
|
||||
msg += _list_course_forum_members(course_id, rolename, datatable)
|
||||
track.views.server_track(request, "list-forum-mods", {"course": course_id}, page="idashboard")
|
||||
msg += _list_course_forum_members(course_key, rolename, datatable)
|
||||
track.views.server_track(
|
||||
request, "list-forum-mods", {"course": course_key.to_deprecated_string()}, page="idashboard"
|
||||
)
|
||||
|
||||
elif action == 'Remove forum moderator':
|
||||
uname = request.POST['forummoderator']
|
||||
msg += _update_forum_role_membership(uname, course, FORUM_ROLE_MODERATOR, FORUM_ROLE_REMOVE)
|
||||
track.views.server_track(request, "remove-forum-mod", {"username": uname, "course": course_id}, page="idashboard")
|
||||
track.views.server_track(
|
||||
request, "remove-forum-mod", {"username": uname, "course": course_key.to_deprecated_string()},
|
||||
page="idashboard"
|
||||
)
|
||||
|
||||
elif action == 'Add forum moderator':
|
||||
uname = request.POST['forummoderator']
|
||||
msg += _update_forum_role_membership(uname, course, FORUM_ROLE_MODERATOR, FORUM_ROLE_ADD)
|
||||
track.views.server_track(request, "add-forum-mod", {"username": uname, "course": course_id}, page="idashboard")
|
||||
track.views.server_track(
|
||||
request, "add-forum-mod", {"username": uname, "course": course_key.to_deprecated_string()},
|
||||
page="idashboard"
|
||||
)
|
||||
|
||||
elif action == 'List course forum community TAs':
|
||||
rolename = FORUM_ROLE_COMMUNITY_TA
|
||||
datatable = {}
|
||||
msg += _list_course_forum_members(course_id, rolename, datatable)
|
||||
track.views.server_track(request, "list-forum-community-TAs", {"course": course_id}, page="idashboard")
|
||||
msg += _list_course_forum_members(course_key, rolename, datatable)
|
||||
track.views.server_track(
|
||||
request, "list-forum-community-TAs", {"course": course_key.to_deprecated_string()},
|
||||
page="idashboard"
|
||||
)
|
||||
|
||||
elif action == 'Remove forum community TA':
|
||||
uname = request.POST['forummoderator']
|
||||
msg += _update_forum_role_membership(uname, course, FORUM_ROLE_COMMUNITY_TA, FORUM_ROLE_REMOVE)
|
||||
track.views.server_track(request, "remove-forum-community-TA", {"username": uname, "course": course_id}, page="idashboard")
|
||||
track.views.server_track(
|
||||
request, "remove-forum-community-TA", {
|
||||
"username": uname, "course": course_key.to_deprecated_string()
|
||||
},
|
||||
page="idashboard"
|
||||
)
|
||||
|
||||
elif action == 'Add forum community TA':
|
||||
uname = request.POST['forummoderator']
|
||||
msg += _update_forum_role_membership(uname, course, FORUM_ROLE_COMMUNITY_TA, FORUM_ROLE_ADD)
|
||||
track.views.server_track(request, "add-forum-community-TA", {"username": uname, "course": course_id}, page="idashboard")
|
||||
track.views.server_track(
|
||||
request, "add-forum-community-TA", {
|
||||
"username": uname, "course": course_key.to_deprecated_string()
|
||||
},
|
||||
page="idashboard"
|
||||
)
|
||||
|
||||
#----------------------------------------
|
||||
# enrollment
|
||||
|
||||
elif action == 'List students who may enroll but may not have yet signed up':
|
||||
ceaset = CourseEnrollmentAllowed.objects.filter(course_id=course_id)
|
||||
ceaset = CourseEnrollmentAllowed.objects.filter(course_id=course_key)
|
||||
datatable = {'header': ['StudentEmail']}
|
||||
datatable['data'] = [[x.email] for x in ceaset]
|
||||
datatable['title'] = action
|
||||
@@ -723,14 +806,14 @@ def instructor_dashboard(request, course_id):
|
||||
students = request.POST.get('multiple_students', '')
|
||||
auto_enroll = bool(request.POST.get('auto_enroll'))
|
||||
email_students = bool(request.POST.get('email_students'))
|
||||
ret = _do_enroll_students(course, course_id, students, auto_enroll=auto_enroll, email_students=email_students, is_shib_course=is_shib_course)
|
||||
ret = _do_enroll_students(course, course_key, students, auto_enroll=auto_enroll, email_students=email_students, is_shib_course=is_shib_course)
|
||||
datatable = ret['datatable']
|
||||
|
||||
elif action == 'Unenroll multiple students':
|
||||
|
||||
students = request.POST.get('multiple_students', '')
|
||||
email_students = bool(request.POST.get('email_students'))
|
||||
ret = _do_unenroll_students(course_id, students, email_students=email_students)
|
||||
ret = _do_unenroll_students(course_key, students, email_students=email_students)
|
||||
datatable = ret['datatable']
|
||||
|
||||
elif action == 'List sections available in remote gradebook':
|
||||
@@ -749,7 +832,7 @@ def instructor_dashboard(request, course_id):
|
||||
if not 'List' in action:
|
||||
students = ','.join([x['email'] for x in datatable['retdata']])
|
||||
overload = 'Overload' in action
|
||||
ret = _do_enroll_students(course, course_id, students, overload=overload)
|
||||
ret = _do_enroll_students(course, course_key, students, overload=overload)
|
||||
datatable = ret['datatable']
|
||||
|
||||
#----------------------------------------
|
||||
@@ -764,12 +847,14 @@ def instructor_dashboard(request, course_id):
|
||||
# Create the CourseEmail object. This is saved immediately, so that
|
||||
# any transaction that has been pending up to this point will also be
|
||||
# committed.
|
||||
email = CourseEmail.create(course_id, request.user, email_to_option, email_subject, html_message)
|
||||
email = CourseEmail.create(
|
||||
course_key.to_deprecated_string(), request.user, email_to_option, email_subject, html_message
|
||||
)
|
||||
|
||||
# Submit the task, so that the correct InstructorTask object gets created (for monitoring purposes)
|
||||
submit_bulk_course_email(request, course_id, email.id) # pylint: disable=E1101
|
||||
submit_bulk_course_email(request, course_key, email.id) # pylint: disable=E1101
|
||||
|
||||
except Exception as err:
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
# Catch any errors and deliver a message to the user
|
||||
error_msg = "Failed to send email! ({0})".format(err)
|
||||
msg += "<font color='red'>" + error_msg + "</font>"
|
||||
@@ -789,11 +874,11 @@ def instructor_dashboard(request, course_id):
|
||||
email_msg = '<div class="msg msg-confirm"><p class="copy">{text}</p></div>'.format(text=text)
|
||||
|
||||
elif "Show Background Email Task History" in action:
|
||||
message, datatable = get_background_task_table(course_id, task_type='bulk_course_email')
|
||||
message, datatable = get_background_task_table(course_key, task_type='bulk_course_email')
|
||||
msg += message
|
||||
|
||||
elif "Show Background Email Task History" in action:
|
||||
message, datatable = get_background_task_table(course_id, task_type='bulk_course_email')
|
||||
message, datatable = get_background_task_table(course_key, task_type='bulk_course_email')
|
||||
msg += message
|
||||
|
||||
#----------------------------------------
|
||||
@@ -806,7 +891,7 @@ def instructor_dashboard(request, course_id):
|
||||
track.views.server_track(request, "psychometrics-histogram-generation", {"problem": unicode(problem)}, page="idashboard")
|
||||
|
||||
if idash_mode == 'Psychometrics':
|
||||
problems = psychoanalyze.problems_with_psychometric_data(course_id)
|
||||
problems = psychoanalyze.problems_with_psychometric_data(course_key)
|
||||
|
||||
#----------------------------------------
|
||||
# analytics
|
||||
@@ -815,12 +900,12 @@ def instructor_dashboard(request, course_id):
|
||||
logs and swallows errors.
|
||||
"""
|
||||
url = settings.ANALYTICS_SERVER_URL + \
|
||||
u"get?aname={}&course_id={}&apikey={}".format(analytics_name,
|
||||
course_id,
|
||||
settings.ANALYTICS_API_KEY)
|
||||
u"get?aname={}&course_id={}&apikey={}".format(
|
||||
analytics_name, course_key.to_deprecated_string(), settings.ANALYTICS_API_KEY
|
||||
)
|
||||
try:
|
||||
res = requests.get(url)
|
||||
except Exception:
|
||||
except Exception: # pylint: disable=broad-except
|
||||
log.exception("Error trying to access analytics at %s", url)
|
||||
return None
|
||||
|
||||
@@ -854,8 +939,8 @@ def instructor_dashboard(request, course_id):
|
||||
|
||||
metrics_results = {}
|
||||
if settings.FEATURES.get('CLASS_DASHBOARD') and idash_mode == 'Metrics':
|
||||
metrics_results['section_display_name'] = dashboard_data.get_section_display_name(course_id)
|
||||
metrics_results['section_has_problem'] = dashboard_data.get_array_section_has_problem(course_id)
|
||||
metrics_results['section_display_name'] = dashboard_data.get_section_display_name(course_key)
|
||||
metrics_results['section_has_problem'] = dashboard_data.get_array_section_has_problem(course_key)
|
||||
|
||||
#----------------------------------------
|
||||
# offline grades?
|
||||
@@ -863,18 +948,18 @@ def instructor_dashboard(request, course_id):
|
||||
if use_offline:
|
||||
msg += "<br/><font color='orange'>{text}</font>".format(
|
||||
text=_("Grades from {course_id}").format(
|
||||
course_id=offline_grades_available(course_id)
|
||||
course_id=offline_grades_available(course_key)
|
||||
)
|
||||
)
|
||||
|
||||
# generate list of pending background tasks
|
||||
if settings.FEATURES.get('ENABLE_INSTRUCTOR_BACKGROUND_TASKS'):
|
||||
instructor_tasks = get_running_instructor_tasks(course_id)
|
||||
instructor_tasks = get_running_instructor_tasks(course_key)
|
||||
else:
|
||||
instructor_tasks = None
|
||||
|
||||
# determine if this is a studio-backed course so we can provide a link to edit this course in studio
|
||||
is_studio_course = modulestore().get_modulestore_type(course_id) != XML_MODULESTORE_TYPE
|
||||
is_studio_course = modulestore().get_modulestore_type(course_key) != XML_MODULESTORE_TYPE
|
||||
studio_url = None
|
||||
if is_studio_course:
|
||||
studio_url = get_cms_course_link(course)
|
||||
@@ -885,10 +970,14 @@ def instructor_dashboard(request, course_id):
|
||||
html_module = HtmlDescriptor(
|
||||
course.system,
|
||||
DictFieldData({'data': html_message}),
|
||||
ScopeIds(None, None, None, 'i4x://dummy_org/dummy_course/html/dummy_name')
|
||||
ScopeIds(None, None, None, course_key.make_usage_key('html', 'dummy'))
|
||||
)
|
||||
fragment = html_module.render('studio_view')
|
||||
fragment = wrap_xblock('LmsRuntime', html_module, 'studio_view', fragment, None, extra_data={"course-id": course_id})
|
||||
fragment = wrap_xblock(
|
||||
'LmsRuntime', html_module, 'studio_view', fragment, None,
|
||||
extra_data={"course-id": course_key.to_deprecated_string()},
|
||||
usage_id_serializer=lambda usage_id: quote_slashes(usage_id.to_deprecated_string())
|
||||
)
|
||||
email_editor = fragment.content
|
||||
|
||||
# Enable instructor email only if the following conditions are met:
|
||||
@@ -896,7 +985,7 @@ def instructor_dashboard(request, course_id):
|
||||
# 2. We have explicitly enabled email for the given course via django-admin
|
||||
# 3. It is NOT an XML course
|
||||
if settings.FEATURES['ENABLE_INSTRUCTOR_EMAIL'] and \
|
||||
CourseAuthorization.instructor_email_enabled(course_id) and is_studio_course:
|
||||
CourseAuthorization.instructor_email_enabled(course_key) and is_studio_course:
|
||||
show_email_tab = True
|
||||
|
||||
# display course stats only if there is no other table to display:
|
||||
@@ -931,12 +1020,12 @@ def instructor_dashboard(request, course_id):
|
||||
'email_msg': email_msg, # email
|
||||
'show_email_tab': show_email_tab, # email
|
||||
|
||||
'problems': problems, # psychometrics
|
||||
'plots': plots, # psychometrics
|
||||
'course_errors': modulestore().get_item_errors(course.location),
|
||||
'problems': problems, # psychometrics
|
||||
'plots': plots, # psychometrics
|
||||
'course_errors': modulestore().get_course_errors(course.id),
|
||||
'instructor_tasks': instructor_tasks,
|
||||
'offline_grade_log': offline_grades_available(course_id),
|
||||
'cohorts_ajax_url': reverse('cohorts', kwargs={'course_id': course_id}),
|
||||
'offline_grade_log': offline_grades_available(course_key),
|
||||
'cohorts_ajax_url': reverse('cohorts', kwargs={'course_id': course_key.to_deprecated_string()}),
|
||||
|
||||
'analytics_results': analytics_results,
|
||||
'disable_buttons': disable_buttons,
|
||||
@@ -944,7 +1033,9 @@ def instructor_dashboard(request, course_id):
|
||||
}
|
||||
|
||||
if settings.FEATURES.get('ENABLE_INSTRUCTOR_BETA_DASHBOARD'):
|
||||
context['beta_dashboard_url'] = reverse('instructor_dashboard_2', kwargs={'course_id': course_id})
|
||||
context['beta_dashboard_url'] = reverse(
|
||||
'instructor_dashboard_2', kwargs={'course_id': course_key.to_deprecated_string()}
|
||||
)
|
||||
|
||||
return render_to_response('courseware/instructor_dashboard.html', context)
|
||||
|
||||
@@ -976,20 +1067,20 @@ def _do_remote_gradebook(user, course, action, args=None, files=None):
|
||||
try:
|
||||
resp = requests.post(rgurl, data=data, verify=False, files=files)
|
||||
retdict = json.loads(resp.content)
|
||||
except Exception as err:
|
||||
msg = _("Failed to communicate with gradebook server at {url}").format(url = rgurl) + "<br/>"
|
||||
msg += _("Error: {err}").format(err = err)
|
||||
msg += "<br/>resp={resp}".format(resp = resp.content)
|
||||
msg += "<br/>data={data}".format(data = data)
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
msg = _("Failed to communicate with gradebook server at {url}").format(url=rgurl) + "<br/>"
|
||||
msg += _("Error: {err}").format(err=err)
|
||||
msg += "<br/>resp={resp}".format(resp=resp.content)
|
||||
msg += "<br/>data={data}".format(data=data)
|
||||
return msg, {}
|
||||
|
||||
msg = '<pre>{msg}</pre>'.format(msg = retdict['msg'].replace('\n', '<br/>'))
|
||||
retdata = retdict['data'] # a list of dicts
|
||||
msg = '<pre>{msg}</pre>'.format(msg=retdict['msg'].replace('\n', '<br/>'))
|
||||
retdata = retdict['data'] # a list of dicts
|
||||
|
||||
if retdata:
|
||||
datatable = {'header': retdata[0].keys()}
|
||||
datatable['data'] = [x.values() for x in retdata]
|
||||
datatable['title'] = _('Remote gradebook response for {action}').format(action = action)
|
||||
datatable['title'] = _('Remote gradebook response for {action}').format(action=action)
|
||||
datatable['retdata'] = retdata
|
||||
else:
|
||||
datatable = {}
|
||||
@@ -997,28 +1088,32 @@ def _do_remote_gradebook(user, course, action, args=None, files=None):
|
||||
return msg, datatable
|
||||
|
||||
|
||||
def _list_course_forum_members(course_id, rolename, datatable):
|
||||
def _list_course_forum_members(course_key, rolename, datatable):
|
||||
"""
|
||||
Fills in datatable with forum membership information, for a given role,
|
||||
so that it will be displayed on instructor dashboard.
|
||||
|
||||
course_ID = the ID string for a course
|
||||
course_ID = the CourseKey for a course
|
||||
rolename = one of "Administrator", "Moderator", "Community TA"
|
||||
|
||||
Returns message status string to append to displayed message, if role is unknown.
|
||||
"""
|
||||
# make sure datatable is set up properly for display first, before checking for errors
|
||||
datatable['header'] = [_('Username'), _('Full name'), _('Roles')]
|
||||
datatable['title'] = _('List of Forum {name}s in course {id}').format(name = rolename, id = course_id)
|
||||
datatable['title'] = _('List of Forum {name}s in course {id}').format(
|
||||
name=rolename, id=course_key.to_deprecated_string()
|
||||
)
|
||||
datatable['data'] = []
|
||||
try:
|
||||
role = Role.objects.get(name=rolename, course_id=course_id)
|
||||
role = Role.objects.get(name=rolename, course_id=course_key)
|
||||
except Role.DoesNotExist:
|
||||
return '<font color="red">' + _('Error: unknown rolename "{rolename}"').format(rolename=rolename) + '</font>'
|
||||
uset = role.users.all().order_by('username')
|
||||
msg = 'Role = {0}'.format(rolename)
|
||||
log.debug('role={0}'.format(rolename))
|
||||
datatable['data'] = [[x.username, x.profile.name, ', '.join([r.name for r in x.roles.filter(course_id=course_id).order_by('name')])] for x in uset]
|
||||
datatable['data'] = [[x.username, x.profile.name, ', '.join([
|
||||
r.name for r in x.roles.filter(course_id=course_key).order_by('name')
|
||||
])] for x in uset]
|
||||
return msg
|
||||
|
||||
|
||||
@@ -1053,21 +1148,21 @@ def _update_forum_role_membership(uname, course, rolename, add_or_remove):
|
||||
msg = '<font color="red">' + _('Error: user "{username}" does not have rolename "{rolename}", cannot remove').format(username=uname, rolename=rolename) + '</font>'
|
||||
else:
|
||||
user.roles.remove(role)
|
||||
msg = '<font color="green">' + _('Removed "{username}" from "{course_id}" forum role = "{rolename}"').format(username=user, course_id=course.id, rolename=rolename) + '</font>'
|
||||
msg = '<font color="green">' + _('Removed "{username}" from "{course_id}" forum role = "{rolename}"').format(username=user, course_id=course.id.to_deprecated_string(), rolename=rolename) + '</font>'
|
||||
else:
|
||||
if alreadyexists:
|
||||
msg = '<font color="red">' + _('Error: user "{username}" already has rolename "{rolename}", cannot add').format(username=uname, rolename=rolename) + '</font>'
|
||||
else:
|
||||
if (rolename == FORUM_ROLE_ADMINISTRATOR and not has_access(user, course, 'staff')):
|
||||
if (rolename == FORUM_ROLE_ADMINISTRATOR and not has_access(user, 'staff', course)):
|
||||
msg = '<font color="red">' + _('Error: user "{username}" should first be added as staff before adding as a forum administrator, cannot add').format(username=uname) + '</font>'
|
||||
else:
|
||||
user.roles.add(role)
|
||||
msg = '<font color="green">' + _('Added "{username}" to "{course_id}" forum role = "{rolename}"').format(username=user, course_id=course.id, rolename=rolename) + '</font>'
|
||||
msg = '<font color="green">' + _('Added "{username}" to "{course_id}" forum role = "{rolename}"').format(username=user, course_id=course.id.to_deprecated_string(), rolename=rolename) + '</font>'
|
||||
|
||||
return msg
|
||||
|
||||
|
||||
def _role_members_table(role, title, course_id):
|
||||
def _role_members_table(role, title, course_key):
|
||||
"""
|
||||
Return a data table of usernames and names of users in group_name.
|
||||
|
||||
@@ -1084,7 +1179,7 @@ def _role_members_table(role, title, course_id):
|
||||
uset = role.users_with_role()
|
||||
datatable = {'header': [_('Username'), _('Full name')]}
|
||||
datatable['data'] = [[x.username, x.profile.name] for x in uset]
|
||||
datatable['title'] = _('{title} in course {course_id}').format(title=title, course_id=course_id)
|
||||
datatable['title'] = _('{title} in course {course_key}').format(title=title, course_key=course_key.to_deprecated_string())
|
||||
return datatable
|
||||
|
||||
|
||||
@@ -1252,12 +1347,12 @@ class GradeTable(object):
|
||||
return self.components.keys()
|
||||
|
||||
|
||||
def get_student_grade_summary_data(request, course, course_id, get_grades=True, get_raw_scores=False, use_offline=False):
|
||||
def get_student_grade_summary_data(request, course, course_key, get_grades=True, get_raw_scores=False, use_offline=False):
|
||||
'''
|
||||
Return data arrays with student identity and grades for specified course.
|
||||
|
||||
course = CourseDescriptor
|
||||
course_id = course ID
|
||||
course_key = course ID
|
||||
|
||||
Note: both are passed in, only because instructor_dashboard already has them already.
|
||||
|
||||
@@ -1271,7 +1366,7 @@ def get_student_grade_summary_data(request, course, course_id, get_grades=True,
|
||||
|
||||
'''
|
||||
enrolled_students = User.objects.filter(
|
||||
courseenrollment__course_id=course_id,
|
||||
courseenrollment__course_id=course_key,
|
||||
courseenrollment__is_active=1,
|
||||
).prefetch_related("groups").order_by('username')
|
||||
|
||||
@@ -1330,10 +1425,11 @@ def gradebook(request, course_id):
|
||||
- only displayed to course staff
|
||||
- shows students who are enrolled.
|
||||
"""
|
||||
course = get_course_with_access(request.user, course_id, 'staff', depth=None)
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
course = get_course_with_access(request.user, 'staff', course_key, depth=None)
|
||||
|
||||
enrolled_students = User.objects.filter(
|
||||
courseenrollment__course_id=course_id,
|
||||
courseenrollment__course_id=course_key,
|
||||
courseenrollment__is_active=1
|
||||
).order_by('username').select_related("profile")
|
||||
|
||||
@@ -1351,7 +1447,7 @@ def gradebook(request, course_id):
|
||||
return render_to_response('courseware/gradebook.html', {
|
||||
'students': student_info,
|
||||
'course': course,
|
||||
'course_id': course_id,
|
||||
'course_id': course_key,
|
||||
# Checked above
|
||||
'staff_access': True,
|
||||
'ordered_grades': sorted(course.grade_cutoffs.items(), key=lambda i: i[1], reverse=True),
|
||||
@@ -1359,9 +1455,9 @@ def gradebook(request, course_id):
|
||||
|
||||
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
def grade_summary(request, course_id):
|
||||
def grade_summary(request, course_key):
|
||||
"""Display the grade summary for a course."""
|
||||
course = get_course_with_access(request.user, course_id, 'staff')
|
||||
course = get_course_with_access(request.user, 'staff', course_key)
|
||||
|
||||
# For now, just a page
|
||||
context = {'course': course,
|
||||
@@ -1372,12 +1468,12 @@ def grade_summary(request, course_id):
|
||||
#-----------------------------------------------------------------------------
|
||||
# enrollment
|
||||
|
||||
def _do_enroll_students(course, course_id, students, overload=False, auto_enroll=False, email_students=False, is_shib_course=False):
|
||||
def _do_enroll_students(course, course_key, students, overload=False, auto_enroll=False, email_students=False, is_shib_course=False):
|
||||
"""
|
||||
Do the actual work of enrolling multiple students, presented as a string
|
||||
of emails separated by commas or returns
|
||||
`course` is course object
|
||||
`course_id` id of course (a `str`)
|
||||
`course_key` id of course (a CourseKey)
|
||||
`students` string of student emails separated by commas or returns (a `str`)
|
||||
`overload` un-enrolls all existing students (a `boolean`)
|
||||
`auto_enroll` is user input preference (a `boolean`)
|
||||
@@ -1387,15 +1483,15 @@ def _do_enroll_students(course, course_id, students, overload=False, auto_enroll
|
||||
new_students, new_students_lc = get_and_clean_student_list(students)
|
||||
status = dict([x, 'unprocessed'] for x in new_students)
|
||||
|
||||
if overload: # delete all but staff
|
||||
todelete = CourseEnrollment.objects.filter(course_id=course_id)
|
||||
if overload: # delete all but staff
|
||||
todelete = CourseEnrollment.objects.filter(course_id=course_key)
|
||||
for ce in todelete:
|
||||
if not has_access(ce.user, course, 'staff') and ce.user.email.lower() not in new_students_lc:
|
||||
if not has_access(ce.user, 'staff', course) and ce.user.email.lower() not in new_students_lc:
|
||||
status[ce.user.email] = 'deleted'
|
||||
ce.deactivate()
|
||||
else:
|
||||
status[ce.user.email] = 'is staff'
|
||||
ceaset = CourseEnrollmentAllowed.objects.filter(course_id=course_id)
|
||||
ceaset = CourseEnrollmentAllowed.objects.filter(course_id=course_key)
|
||||
for cea in ceaset:
|
||||
status[cea.email] = 'removed from pending enrollment list'
|
||||
ceaset.delete()
|
||||
@@ -1405,20 +1501,22 @@ def _do_enroll_students(course, course_id, students, overload=False, auto_enroll
|
||||
'SITE_NAME',
|
||||
settings.SITE_NAME
|
||||
)
|
||||
# TODO: Use request.build_absolute_uri rather than 'https://{}{}'.format
|
||||
# and check with the Services team that this works well with microsites
|
||||
registration_url = 'https://{}{}'.format(
|
||||
stripped_site_name,
|
||||
reverse('student.views.register_user')
|
||||
)
|
||||
course_url = 'https://{}{}'.format(
|
||||
stripped_site_name,
|
||||
reverse('course_root', kwargs={'course_id': course_id})
|
||||
reverse('course_root', kwargs={'course_id': course_key.to_deprecated_string()})
|
||||
)
|
||||
# We can't get the url to the course's About page if the marketing site is enabled.
|
||||
course_about_url = None
|
||||
if not settings.FEATURES.get('ENABLE_MKTG_SITE', False):
|
||||
course_about_url = u'https://{}{}'.format(
|
||||
stripped_site_name,
|
||||
reverse('about_course', kwargs={'course_id': course.id})
|
||||
reverse('about_course', kwargs={'course_id': course_key.to_deprecated_string()})
|
||||
)
|
||||
|
||||
# Composition of email
|
||||
@@ -1438,7 +1536,7 @@ def _do_enroll_students(course, course_id, students, overload=False, auto_enroll
|
||||
except User.DoesNotExist:
|
||||
|
||||
#Student not signed up yet, put in pending enrollment allowed table
|
||||
cea = CourseEnrollmentAllowed.objects.filter(email=student, course_id=course_id)
|
||||
cea = CourseEnrollmentAllowed.objects.filter(email=student, course_id=course_key)
|
||||
|
||||
#If enrollmentallowed already exists, update auto_enroll flag to however it was set in UI
|
||||
#Will be 0 or 1 records as there is a unique key on email + course_id
|
||||
@@ -1450,32 +1548,32 @@ def _do_enroll_students(course, course_id, students, overload=False, auto_enroll
|
||||
continue
|
||||
|
||||
#EnrollmentAllowed doesn't exist so create it
|
||||
cea = CourseEnrollmentAllowed(email=student, course_id=course_id, auto_enroll=auto_enroll)
|
||||
cea = CourseEnrollmentAllowed(email=student, course_id=course_key, auto_enroll=auto_enroll)
|
||||
cea.save()
|
||||
|
||||
status[student] = 'user does not exist, enrollment allowed, pending with auto enrollment ' \
|
||||
+ ('on' if auto_enroll else 'off')
|
||||
|
||||
if email_students:
|
||||
#User is allowed to enroll but has not signed up yet
|
||||
# User is allowed to enroll but has not signed up yet
|
||||
d['email_address'] = student
|
||||
d['message'] = 'allowed_enroll'
|
||||
send_mail_ret = send_mail_to_student(student, d)
|
||||
status[student] += (', email sent' if send_mail_ret else '')
|
||||
continue
|
||||
|
||||
#Student has already registered
|
||||
if CourseEnrollment.is_enrolled(user, course_id):
|
||||
# Student has already registered
|
||||
if CourseEnrollment.is_enrolled(user, course_key):
|
||||
status[student] = 'already enrolled'
|
||||
continue
|
||||
|
||||
try:
|
||||
#Not enrolled yet
|
||||
ce = CourseEnrollment.enroll(user, course_id)
|
||||
# Not enrolled yet
|
||||
CourseEnrollment.enroll(user, course_key)
|
||||
status[student] = 'added'
|
||||
|
||||
if email_students:
|
||||
#User enrolled for first time, populate dict with user specific info
|
||||
# User enrolled for first time, populate dict with user specific info
|
||||
d['email_address'] = student
|
||||
d['full_name'] = user.profile.name
|
||||
d['message'] = 'enrolled_enroll'
|
||||
@@ -1499,11 +1597,11 @@ def _do_enroll_students(course, course_id, students, overload=False, auto_enroll
|
||||
|
||||
|
||||
#Unenrollment
|
||||
def _do_unenroll_students(course_id, students, email_students=False):
|
||||
def _do_unenroll_students(course_key, students, email_students=False):
|
||||
"""
|
||||
Do the actual work of un-enrolling multiple students, presented as a string
|
||||
of emails separated by commas or returns
|
||||
`course_id` is id of course (a `str`)
|
||||
`course_key` is id of course (a `str`)
|
||||
`students` is string of student emails separated by commas or returns (a `str`)
|
||||
`email_students` is user input preference (a `boolean`)
|
||||
"""
|
||||
@@ -1516,7 +1614,7 @@ def _do_unenroll_students(course_id, students, email_students=False):
|
||||
settings.SITE_NAME
|
||||
)
|
||||
if email_students:
|
||||
course = course_from_id(course_id)
|
||||
course = course_from_id(course_key)
|
||||
#Composition of email
|
||||
d = {'site_name': stripped_site_name,
|
||||
'course': course}
|
||||
@@ -1524,7 +1622,7 @@ def _do_unenroll_students(course_id, students, email_students=False):
|
||||
for student in old_students:
|
||||
|
||||
isok = False
|
||||
cea = CourseEnrollmentAllowed.objects.filter(course_id=course_id, email=student)
|
||||
cea = CourseEnrollmentAllowed.objects.filter(course_id=course_key, email=student)
|
||||
#Will be 0 or 1 records as there is a unique key on email + course_id
|
||||
if cea:
|
||||
cea[0].delete()
|
||||
@@ -1545,9 +1643,9 @@ def _do_unenroll_students(course_id, students, email_students=False):
|
||||
continue
|
||||
|
||||
#Will be 0 or 1 records as there is a unique key on user + course_id
|
||||
if CourseEnrollment.is_enrolled(user, course_id):
|
||||
if CourseEnrollment.is_enrolled(user, course_key):
|
||||
try:
|
||||
CourseEnrollment.unenroll(user, course_id)
|
||||
CourseEnrollment.unenroll(user, course_key)
|
||||
status[student] = "un-enrolled"
|
||||
if email_students:
|
||||
#User was enrolled
|
||||
@@ -1557,7 +1655,7 @@ def _do_unenroll_students(course_id, students, email_students=False):
|
||||
send_mail_ret = send_mail_to_student(student, d)
|
||||
status[student] += (', email sent' if send_mail_ret else '')
|
||||
|
||||
except Exception:
|
||||
except Exception: # pylint: disable=broad-except
|
||||
if not isok:
|
||||
status[student] = "Error! Failed to un-enroll"
|
||||
|
||||
@@ -1577,7 +1675,7 @@ def send_mail_to_student(student, param_dict):
|
||||
`param_dict` is a `dict` with keys [
|
||||
`site_name`: name given to edX instance (a `str`)
|
||||
`registration_url`: url for registration (a `str`)
|
||||
`course_id`: id of course (a `str`)
|
||||
`course_key`: id of course (a CourseKey)
|
||||
`auto_enroll`: user input option (a `str`)
|
||||
`course_url`: url of course (a `str`)
|
||||
`email_address`: email of student (a `str`)
|
||||
@@ -1651,7 +1749,7 @@ def get_and_clean_student_list(students):
|
||||
# answer distribution
|
||||
|
||||
|
||||
def get_answers_distribution(request, course_id):
|
||||
def get_answers_distribution(request, course_key):
|
||||
"""
|
||||
Get the distribution of answers for all graded problems in the course.
|
||||
|
||||
@@ -1659,7 +1757,7 @@ def get_answers_distribution(request, course_id):
|
||||
'header': a header row
|
||||
'data': a list of rows
|
||||
"""
|
||||
course = get_course_with_access(request.user, course_id, 'staff')
|
||||
course = get_course_with_access(request.user, 'staff', course_key)
|
||||
|
||||
dist = grades.answer_distributions(course.id)
|
||||
|
||||
@@ -1691,13 +1789,13 @@ def compute_course_stats(course):
|
||||
|
||||
def walk(module):
|
||||
children = module.get_children()
|
||||
category = module.__class__.__name__ # HtmlDescriptor, CapaDescriptor, ...
|
||||
category = module.__class__.__name__ # HtmlDescriptor, CapaDescriptor, ...
|
||||
counts[category] += 1
|
||||
for c in children:
|
||||
walk(c)
|
||||
|
||||
walk(course)
|
||||
stats = dict(counts) # number of each kind of module
|
||||
stats = dict(counts) # number of each kind of module
|
||||
return stats
|
||||
|
||||
|
||||
@@ -1721,34 +1819,34 @@ def dump_grading_context(course):
|
||||
msg += "-----------------------------------------------------------------------------\n"
|
||||
msg += "Listing grading context for course %s\n" % course.id
|
||||
|
||||
gc = course.grading_context
|
||||
gcontext = course.grading_context
|
||||
msg += "graded sections:\n"
|
||||
|
||||
msg += '%s\n' % gc['graded_sections'].keys()
|
||||
for (gs, gsvals) in gc['graded_sections'].items():
|
||||
msg += "--> Section %s:\n" % (gs)
|
||||
msg += '%s\n' % gcontext['graded_sections'].keys()
|
||||
for (gsections, gsvals) in gcontext['graded_sections'].items():
|
||||
msg += "--> Section %s:\n" % (gsections)
|
||||
for sec in gsvals:
|
||||
s = sec['section_descriptor']
|
||||
grade_format = getattr(s, 'grade_format', None)
|
||||
sdesc = sec['section_descriptor']
|
||||
grade_format = getattr(sdesc, 'grade_format', None)
|
||||
aname = ''
|
||||
if grade_format in graders:
|
||||
g = graders[grade_format]
|
||||
aname = '%s %02d' % (g.short_label, g.index)
|
||||
g.index += 1
|
||||
elif s.display_name in graders:
|
||||
g = graders[s.display_name]
|
||||
aname = '%s' % g.short_label
|
||||
gfmt = graders[grade_format]
|
||||
aname = '%s %02d' % (gfmt.short_label, gfmt.index)
|
||||
gfmt.index += 1
|
||||
elif sdesc.display_name in graders:
|
||||
gfmt = graders[sdesc.display_name]
|
||||
aname = '%s' % gfmt.short_label
|
||||
notes = ''
|
||||
if getattr(s, 'score_by_attempt', False):
|
||||
if getattr(sdesc, 'score_by_attempt', False):
|
||||
notes = ', score by attempt!'
|
||||
msg += " %s (grade_format=%s, Assignment=%s%s)\n" % (s.display_name, grade_format, aname, notes)
|
||||
msg += "all descriptors:\n"
|
||||
msg += "length=%d\n" % len(gc['all_descriptors'])
|
||||
msg += "length=%d\n" % len(gcontext['all_descriptors'])
|
||||
msg = '<pre>%s</pre>' % msg.replace('<', '<')
|
||||
return msg
|
||||
|
||||
|
||||
def get_background_task_table(course_id, problem_url=None, student=None, task_type=None):
|
||||
def get_background_task_table(course_key, problem_url=None, student=None, task_type=None):
|
||||
"""
|
||||
Construct the "datatable" structure to represent background task history.
|
||||
|
||||
@@ -1759,7 +1857,7 @@ def get_background_task_table(course_id, problem_url=None, student=None, task_ty
|
||||
Returns a tuple of (msg, datatable), where the msg is a possible error message,
|
||||
and the datatable is the datatable to be used for display.
|
||||
"""
|
||||
history_entries = get_instructor_task_history(course_id, problem_url, student, task_type)
|
||||
history_entries = get_instructor_task_history(course_key, problem_url, student, task_type)
|
||||
datatable = {}
|
||||
msg = ""
|
||||
# first check to see if there is any history at all
|
||||
@@ -1767,12 +1865,16 @@ def get_background_task_table(course_id, problem_url=None, student=None, task_ty
|
||||
# just won't find any entries.)
|
||||
if (history_entries.count()) == 0:
|
||||
if problem_url is None:
|
||||
msg += '<font color="red">Failed to find any background tasks for course "{course}".</font>'.format(course=course_id)
|
||||
msg += '<font color="red">Failed to find any background tasks for course "{course}".</font>'.format(
|
||||
course=course_key.to_deprecated_string()
|
||||
)
|
||||
elif student is not None:
|
||||
template = '<font color="red">' + _('Failed to find any background tasks for course "{course}", module "{problem}" and student "{student}".') + '</font>'
|
||||
msg += template.format(course=course_id, problem=problem_url, student=student.username)
|
||||
msg += template.format(course=course_key.to_deprecated_string(), problem=problem_url, student=student.username)
|
||||
else:
|
||||
msg += '<font color="red">' + _('Failed to find any background tasks for course "{course}" and module "{problem}".').format(course=course_id, problem=problem_url) + '</font>'
|
||||
msg += '<font color="red">' + _('Failed to find any background tasks for course "{course}" and module "{problem}".').format(
|
||||
course=course_key.to_deprecated_string(), problem=problem_url
|
||||
) + '</font>'
|
||||
else:
|
||||
datatable['header'] = ["Task Type",
|
||||
"Task Id",
|
||||
@@ -1808,13 +1910,17 @@ def get_background_task_table(course_id, problem_url=None, student=None, task_ty
|
||||
datatable['data'].append(row)
|
||||
|
||||
if problem_url is None:
|
||||
datatable['title'] = "{course_id}".format(course_id=course_id)
|
||||
datatable['title'] = "{course_id}".format(course_id=course_key.to_deprecated_string())
|
||||
elif student is not None:
|
||||
datatable['title'] = "{course_id} > {location} > {student}".format(course_id=course_id,
|
||||
location=problem_url,
|
||||
student=student.username)
|
||||
datatable['title'] = "{course_id} > {location} > {student}".format(
|
||||
course_id=course_key.to_deprecated_string(),
|
||||
location=problem_url,
|
||||
student=student.username
|
||||
)
|
||||
else:
|
||||
datatable['title'] = "{course_id} > {location}".format(course_id=course_id, location=problem_url)
|
||||
datatable['title'] = "{course_id} > {location}".format(
|
||||
course_id=course_key.to_deprecated_string(), location=problem_url
|
||||
)
|
||||
|
||||
return msg, datatable
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ def find_unit(course, url):
|
||||
"""
|
||||
Find node in course tree for url.
|
||||
"""
|
||||
if node.location.url() == url:
|
||||
if node.location.to_deprecated_string() == url:
|
||||
return node
|
||||
for child in node.get_children():
|
||||
found = find(child, url)
|
||||
@@ -132,7 +132,7 @@ def title_or_url(node):
|
||||
"""
|
||||
title = getattr(node, 'display_name', None)
|
||||
if not title:
|
||||
title = node.location.url()
|
||||
title = node.location.to_deprecated_string()
|
||||
return title
|
||||
|
||||
|
||||
@@ -148,7 +148,7 @@ def set_due_date_extension(course, unit, student, due_date):
|
||||
student_module = StudentModule.objects.get(
|
||||
student_id=student.id,
|
||||
course_id=course.id,
|
||||
module_state_key=node.location.url()
|
||||
module_id=node.location
|
||||
)
|
||||
|
||||
state = json.loads(student_module.state)
|
||||
@@ -173,7 +173,7 @@ def dump_module_extensions(course, unit):
|
||||
header = [_("Username"), _("Full Name"), _("Extended Due Date")]
|
||||
query = StudentModule.objects.filter(
|
||||
course_id=course.id,
|
||||
module_state_key=unit.location.url())
|
||||
module_id=unit.location)
|
||||
for module in query:
|
||||
state = json.loads(module.state)
|
||||
extended_due = state.get("extended_due")
|
||||
@@ -202,7 +202,7 @@ def dump_student_extensions(course, student):
|
||||
data = []
|
||||
header = [_("Unit"), _("Extended Due Date")]
|
||||
units = get_units_with_due_date(course)
|
||||
units = dict([(u.location.url(), u) for u in units])
|
||||
units = dict([(u.location, u) for u in units])
|
||||
query = StudentModule.objects.filter(
|
||||
course_id=course.id,
|
||||
student_id=student.id)
|
||||
|
||||
@@ -38,14 +38,14 @@ def get_running_instructor_tasks(course_id):
|
||||
return instructor_tasks.order_by('-id')
|
||||
|
||||
|
||||
def get_instructor_task_history(course_id, problem_url=None, student=None, task_type=None):
|
||||
def get_instructor_task_history(course_id, usage_key=None, student=None, task_type=None):
|
||||
"""
|
||||
Returns a query of InstructorTask objects of historical tasks for a given course,
|
||||
that optionally match a particular problem, a student, and/or a task type.
|
||||
"""
|
||||
instructor_tasks = InstructorTask.objects.filter(course_id=course_id)
|
||||
if problem_url is not None or student is not None:
|
||||
_, task_key = encode_problem_and_student_input(problem_url, student)
|
||||
if usage_key is not None or student is not None:
|
||||
_, task_key = encode_problem_and_student_input(usage_key, student)
|
||||
instructor_tasks = instructor_tasks.filter(task_key=task_key)
|
||||
if task_type is not None:
|
||||
instructor_tasks = instructor_tasks.filter(task_type=task_type)
|
||||
@@ -53,7 +53,8 @@ def get_instructor_task_history(course_id, problem_url=None, student=None, task_
|
||||
return instructor_tasks.order_by('-id')
|
||||
|
||||
|
||||
def submit_rescore_problem_for_student(request, course_id, problem_url, student):
|
||||
# Disabling invalid-name because this fn name is longer than 30 chars.
|
||||
def submit_rescore_problem_for_student(request, usage_key, student): # pylint: disable=invalid-name
|
||||
"""
|
||||
Request a problem to be rescored as a background task.
|
||||
|
||||
@@ -74,15 +75,15 @@ def submit_rescore_problem_for_student(request, course_id, problem_url, student)
|
||||
|
||||
"""
|
||||
# check arguments: let exceptions return up to the caller.
|
||||
check_arguments_for_rescoring(course_id, problem_url)
|
||||
check_arguments_for_rescoring(usage_key)
|
||||
|
||||
task_type = 'rescore_problem'
|
||||
task_class = rescore_problem
|
||||
task_input, task_key = encode_problem_and_student_input(problem_url, student)
|
||||
return submit_task(request, task_type, task_class, course_id, task_input, task_key)
|
||||
task_input, task_key = encode_problem_and_student_input(usage_key, student)
|
||||
return submit_task(request, task_type, task_class, usage_key.course_key, task_input, task_key)
|
||||
|
||||
|
||||
def submit_rescore_problem_for_all_students(request, course_id, problem_url):
|
||||
def submit_rescore_problem_for_all_students(request, usage_key): # pylint: disable=invalid-name
|
||||
"""
|
||||
Request a problem to be rescored as a background task.
|
||||
|
||||
@@ -103,23 +104,22 @@ def submit_rescore_problem_for_all_students(request, course_id, problem_url):
|
||||
separate transaction.
|
||||
"""
|
||||
# check arguments: let exceptions return up to the caller.
|
||||
check_arguments_for_rescoring(course_id, problem_url)
|
||||
check_arguments_for_rescoring(usage_key)
|
||||
|
||||
# check to see if task is already running, and reserve it otherwise
|
||||
task_type = 'rescore_problem'
|
||||
task_class = rescore_problem
|
||||
task_input, task_key = encode_problem_and_student_input(problem_url)
|
||||
return submit_task(request, task_type, task_class, course_id, task_input, task_key)
|
||||
task_input, task_key = encode_problem_and_student_input(usage_key)
|
||||
return submit_task(request, task_type, task_class, usage_key.course_key, task_input, task_key)
|
||||
|
||||
|
||||
def submit_reset_problem_attempts_for_all_students(request, course_id, problem_url):
|
||||
def submit_reset_problem_attempts_for_all_students(request, usage_key): # pylint: disable=invalid-name
|
||||
"""
|
||||
Request to have attempts reset for a problem as a background task.
|
||||
|
||||
The problem's attempts will be reset for all students who have accessed the
|
||||
particular problem in a course. Parameters are the `course_id` and
|
||||
the `problem_url`. The url must specify the location of the problem,
|
||||
using i4x-type notation.
|
||||
the `usage_key`, which must be a :class:`Location`.
|
||||
|
||||
ItemNotFoundException is raised if the problem doesn't exist, or AlreadyRunningError
|
||||
if the problem is already being reset.
|
||||
@@ -131,25 +131,24 @@ def submit_reset_problem_attempts_for_all_students(request, course_id, problem_u
|
||||
save here. Any future database operations will take place in a
|
||||
separate transaction.
|
||||
"""
|
||||
# check arguments: make sure that the problem_url is defined
|
||||
# check arguments: make sure that the usage_key is defined
|
||||
# (since that's currently typed in). If the corresponding module descriptor doesn't exist,
|
||||
# an exception will be raised. Let it pass up to the caller.
|
||||
modulestore().get_instance(course_id, problem_url)
|
||||
modulestore().get_item(usage_key)
|
||||
|
||||
task_type = 'reset_problem_attempts'
|
||||
task_class = reset_problem_attempts
|
||||
task_input, task_key = encode_problem_and_student_input(problem_url)
|
||||
return submit_task(request, task_type, task_class, course_id, task_input, task_key)
|
||||
task_input, task_key = encode_problem_and_student_input(usage_key)
|
||||
return submit_task(request, task_type, task_class, usage_key.course_key, task_input, task_key)
|
||||
|
||||
|
||||
def submit_delete_problem_state_for_all_students(request, course_id, problem_url):
|
||||
def submit_delete_problem_state_for_all_students(request, usage_key): # pylint: disable=invalid-name
|
||||
"""
|
||||
Request to have state deleted for a problem as a background task.
|
||||
|
||||
The problem's state will be deleted for all students who have accessed the
|
||||
particular problem in a course. Parameters are the `course_id` and
|
||||
the `problem_url`. The url must specify the location of the problem,
|
||||
using i4x-type notation.
|
||||
the `usage_key`, which must be a :class:`Location`.
|
||||
|
||||
ItemNotFoundException is raised if the problem doesn't exist, or AlreadyRunningError
|
||||
if the particular problem's state is already being deleted.
|
||||
@@ -161,23 +160,23 @@ def submit_delete_problem_state_for_all_students(request, course_id, problem_url
|
||||
save here. Any future database operations will take place in a
|
||||
separate transaction.
|
||||
"""
|
||||
# check arguments: make sure that the problem_url is defined
|
||||
# check arguments: make sure that the usage_key is defined
|
||||
# (since that's currently typed in). If the corresponding module descriptor doesn't exist,
|
||||
# an exception will be raised. Let it pass up to the caller.
|
||||
modulestore().get_instance(course_id, problem_url)
|
||||
modulestore().get_item(usage_key)
|
||||
|
||||
task_type = 'delete_problem_state'
|
||||
task_class = delete_problem_state
|
||||
task_input, task_key = encode_problem_and_student_input(problem_url)
|
||||
return submit_task(request, task_type, task_class, course_id, task_input, task_key)
|
||||
task_input, task_key = encode_problem_and_student_input(usage_key)
|
||||
return submit_task(request, task_type, task_class, usage_key.course_key, task_input, task_key)
|
||||
|
||||
|
||||
def submit_bulk_course_email(request, course_id, email_id):
|
||||
def submit_bulk_course_email(request, course_key, email_id):
|
||||
"""
|
||||
Request to have bulk email sent as a background task.
|
||||
|
||||
The specified CourseEmail object will be sent be updated for all students who have enrolled
|
||||
in a course. Parameters are the `course_id` and the `email_id`, the id of the CourseEmail object.
|
||||
in a course. Parameters are the `course_key` and the `email_id`, the id of the CourseEmail object.
|
||||
|
||||
AlreadyRunningError is raised if the same recipients are already being emailed with the same
|
||||
CourseEmail object.
|
||||
@@ -206,10 +205,10 @@ def submit_bulk_course_email(request, course_id, email_id):
|
||||
task_key_stub = "{email_id}_{to_option}".format(email_id=email_id, to_option=to_option)
|
||||
# create the key value by using MD5 hash:
|
||||
task_key = hashlib.md5(task_key_stub).hexdigest()
|
||||
return submit_task(request, task_type, task_class, course_id, task_input, task_key)
|
||||
return submit_task(request, task_type, task_class, course_key, task_input, task_key)
|
||||
|
||||
|
||||
def submit_calculate_grades_csv(request, course_id):
|
||||
def submit_calculate_grades_csv(request, course_key):
|
||||
"""
|
||||
AlreadyRunningError is raised if the course's grades are already being updated.
|
||||
"""
|
||||
@@ -218,4 +217,4 @@ def submit_calculate_grades_csv(request, course_id):
|
||||
task_input = {}
|
||||
task_key = ""
|
||||
|
||||
return submit_task(request, task_type, task_class, course_id, task_input, task_key)
|
||||
return submit_task(request, task_type, task_class, course_key, task_input, task_key)
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
"""
|
||||
Helper lib for instructor_tasks API.
|
||||
|
||||
Includes methods to check args for rescoring task, encoding student input,
|
||||
and task submission logic, including handling the Celery backend.
|
||||
"""
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
@@ -8,6 +14,7 @@ from celery.states import READY_STATES, SUCCESS, FAILURE, REVOKED
|
||||
from courseware.module_render import get_xqueue_callback_url_prefix
|
||||
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.locations import Location
|
||||
from instructor_task.models import InstructorTask, PROGRESS
|
||||
|
||||
|
||||
@@ -21,11 +28,13 @@ class AlreadyRunningError(Exception):
|
||||
|
||||
def _task_is_running(course_id, task_type, task_key):
|
||||
"""Checks if a particular task is already running"""
|
||||
runningTasks = InstructorTask.objects.filter(course_id=course_id, task_type=task_type, task_key=task_key)
|
||||
running_tasks = InstructorTask.objects.filter(
|
||||
course_id=course_id, task_type=task_type, task_key=task_key
|
||||
)
|
||||
# exclude states that are "ready" (i.e. not "running", e.g. failure, success, revoked):
|
||||
for state in READY_STATES:
|
||||
runningTasks = runningTasks.exclude(task_state=state)
|
||||
return len(runningTasks) > 0
|
||||
running_tasks = running_tasks.exclude(task_state=state)
|
||||
return len(running_tasks) > 0
|
||||
|
||||
|
||||
def _reserve_task(course_id, task_type, task_key, task_input, requester):
|
||||
@@ -229,34 +238,37 @@ def get_status_from_instructor_task(instructor_task):
|
||||
return status
|
||||
|
||||
|
||||
def check_arguments_for_rescoring(course_id, problem_url):
|
||||
def check_arguments_for_rescoring(usage_key):
|
||||
"""
|
||||
Do simple checks on the descriptor to confirm that it supports rescoring.
|
||||
|
||||
Confirms first that the problem_url is defined (since that's currently typed
|
||||
Confirms first that the usage_key is defined (since that's currently typed
|
||||
in). An ItemNotFoundException is raised if the corresponding module
|
||||
descriptor doesn't exist. NotImplementedError is raised if the
|
||||
corresponding module doesn't support rescoring calls.
|
||||
"""
|
||||
descriptor = modulestore().get_instance(course_id, problem_url)
|
||||
descriptor = modulestore().get_item(usage_key)
|
||||
if not hasattr(descriptor, 'module_class') or not hasattr(descriptor.module_class, 'rescore_problem'):
|
||||
msg = "Specified module does not support rescoring."
|
||||
raise NotImplementedError(msg)
|
||||
|
||||
|
||||
def encode_problem_and_student_input(problem_url, student=None):
|
||||
def encode_problem_and_student_input(usage_key, student=None): # pylint: disable=invalid-name
|
||||
"""
|
||||
Encode optional problem_url and optional student into task_key and task_input values.
|
||||
Encode optional usage_key and optional student into task_key and task_input values.
|
||||
|
||||
`problem_url` is full URL of the problem.
|
||||
`student` is the user object of the student
|
||||
Args:
|
||||
usage_key (Location): The usage_key identifying the problem.
|
||||
student (User): the student affected
|
||||
"""
|
||||
|
||||
assert isinstance(usage_key, Location)
|
||||
if student is not None:
|
||||
task_input = {'problem_url': problem_url, 'student': student.username}
|
||||
task_key_stub = "{student}_{problem}".format(student=student.id, problem=problem_url)
|
||||
task_input = {'problem_url': usage_key.to_deprecated_string(), 'student': student.username}
|
||||
task_key_stub = "{student}_{problem}".format(student=student.id, problem=usage_key.to_deprecated_string())
|
||||
else:
|
||||
task_input = {'problem_url': problem_url}
|
||||
task_key_stub = "_{problem}".format(problem=problem_url)
|
||||
task_input = {'problem_url': usage_key.to_deprecated_string()}
|
||||
task_key_stub = "_{problem}".format(problem=usage_key.to_deprecated_string())
|
||||
|
||||
# create the key value by using MD5 hash:
|
||||
task_key = hashlib.md5(task_key_stub).hexdigest()
|
||||
@@ -264,11 +276,11 @@ def encode_problem_and_student_input(problem_url, student=None):
|
||||
return task_input, task_key
|
||||
|
||||
|
||||
def submit_task(request, task_type, task_class, course_id, task_input, task_key):
|
||||
def submit_task(request, task_type, task_class, course_key, task_input, task_key):
|
||||
"""
|
||||
Helper method to submit a task.
|
||||
|
||||
Reserves the requested task, based on the `course_id`, `task_type`, and `task_key`,
|
||||
Reserves the requested task, based on the `course_key`, `task_type`, and `task_key`,
|
||||
checking to see if the task is already running. The `task_input` is also passed so that
|
||||
it can be stored in the resulting InstructorTask entry. Arguments are extracted from
|
||||
the `request` provided by the originating server request. Then the task is submitted to run
|
||||
@@ -285,7 +297,7 @@ def submit_task(request, task_type, task_class, course_id, task_input, task_key)
|
||||
|
||||
"""
|
||||
# check to see if task is already running, and reserve it otherwise:
|
||||
instructor_task = _reserve_task(course_id, task_type, task_key, task_input, request.user)
|
||||
instructor_task = _reserve_task(course_key, task_type, task_key, task_input, request.user)
|
||||
|
||||
# submit task:
|
||||
task_id = instructor_task.task_id
|
||||
|
||||
@@ -18,7 +18,6 @@ from uuid import uuid4
|
||||
import csv
|
||||
import json
|
||||
import hashlib
|
||||
import os
|
||||
import os.path
|
||||
import urllib
|
||||
|
||||
@@ -29,6 +28,8 @@ from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.db import models, transaction
|
||||
|
||||
from xmodule_django.models import CourseKeyField
|
||||
|
||||
|
||||
# define custom states used by InstructorTask
|
||||
QUEUING = 'QUEUING'
|
||||
@@ -58,7 +59,7 @@ class InstructorTask(models.Model):
|
||||
`updated` stores date that entry was last modified
|
||||
"""
|
||||
task_type = models.CharField(max_length=50, db_index=True)
|
||||
course_id = models.CharField(max_length=255, db_index=True)
|
||||
course_id = CourseKeyField(max_length=255, db_index=True)
|
||||
task_key = models.CharField(max_length=255, db_index=True)
|
||||
task_input = models.CharField(max_length=255)
|
||||
task_id = models.CharField(max_length=255, db_index=True) # max_length from celery_taskmeta
|
||||
@@ -251,9 +252,9 @@ class S3ReportStore(ReportStore):
|
||||
)
|
||||
|
||||
def key_for(self, course_id, filename):
|
||||
"""Return the S3 key we would use to store and retrive the data for the
|
||||
"""Return the S3 key we would use to store and retrieve the data for the
|
||||
given filename."""
|
||||
hashed_course_id = hashlib.sha1(course_id)
|
||||
hashed_course_id = hashlib.sha1(course_id.to_deprecated_string())
|
||||
|
||||
key = Key(self.bucket)
|
||||
key.key = "{}/{}/{}".format(
|
||||
@@ -360,7 +361,7 @@ class LocalFSReportStore(ReportStore):
|
||||
|
||||
def path_to(self, course_id, filename):
|
||||
"""Return the full path to a given file for a given course."""
|
||||
return os.path.join(self.root_path, urllib.quote(course_id, safe=''), filename)
|
||||
return os.path.join(self.root_path, urllib.quote(course_id.to_deprecated_string(), safe=''), filename)
|
||||
|
||||
def store(self, course_id, filename, buff):
|
||||
"""
|
||||
|
||||
@@ -375,7 +375,7 @@ def check_subtask_is_valid(entry_id, current_task_id, new_subtask_status):
|
||||
format_str = "Unexpected task_id '{}': unable to find subtasks of instructor task '{}': rejecting task {}"
|
||||
msg = format_str.format(current_task_id, entry, new_subtask_status)
|
||||
TASK_LOG.warning(msg)
|
||||
dog_stats_api.increment('instructor_task.subtask.duplicate.nosubtasks', tags=[entry.course_id])
|
||||
dog_stats_api.increment('instructor_task.subtask.duplicate.nosubtasks', tags=[_statsd_tag(entry.course_id)])
|
||||
raise DuplicateTaskException(msg)
|
||||
|
||||
# Confirm that the InstructorTask knows about this particular subtask.
|
||||
@@ -385,7 +385,7 @@ def check_subtask_is_valid(entry_id, current_task_id, new_subtask_status):
|
||||
format_str = "Unexpected task_id '{}': unable to find status for subtask of instructor task '{}': rejecting task {}"
|
||||
msg = format_str.format(current_task_id, entry, new_subtask_status)
|
||||
TASK_LOG.warning(msg)
|
||||
dog_stats_api.increment('instructor_task.subtask.duplicate.unknown', tags=[entry.course_id])
|
||||
dog_stats_api.increment('instructor_task.subtask.duplicate.unknown', tags=[_statsd_tag(entry.course_id)])
|
||||
raise DuplicateTaskException(msg)
|
||||
|
||||
# Confirm that the InstructorTask doesn't think that this subtask has already been
|
||||
@@ -396,7 +396,7 @@ def check_subtask_is_valid(entry_id, current_task_id, new_subtask_status):
|
||||
format_str = "Unexpected task_id '{}': already completed - status {} for subtask of instructor task '{}': rejecting task {}"
|
||||
msg = format_str.format(current_task_id, subtask_status, entry, new_subtask_status)
|
||||
TASK_LOG.warning(msg)
|
||||
dog_stats_api.increment('instructor_task.subtask.duplicate.completed', tags=[entry.course_id])
|
||||
dog_stats_api.increment('instructor_task.subtask.duplicate.completed', tags=[_statsd_tag(entry.course_id)])
|
||||
raise DuplicateTaskException(msg)
|
||||
|
||||
# Confirm that the InstructorTask doesn't think that this subtask is already being
|
||||
@@ -410,7 +410,7 @@ def check_subtask_is_valid(entry_id, current_task_id, new_subtask_status):
|
||||
format_str = "Unexpected task_id '{}': already retried - status {} for subtask of instructor task '{}': rejecting task {}"
|
||||
msg = format_str.format(current_task_id, subtask_status, entry, new_subtask_status)
|
||||
TASK_LOG.warning(msg)
|
||||
dog_stats_api.increment('instructor_task.subtask.duplicate.retried', tags=[entry.course_id])
|
||||
dog_stats_api.increment('instructor_task.subtask.duplicate.retried', tags=[_statsd_tag(entry.course_id)])
|
||||
raise DuplicateTaskException(msg)
|
||||
|
||||
# Now we are ready to start working on this. Try to lock it.
|
||||
@@ -420,7 +420,7 @@ def check_subtask_is_valid(entry_id, current_task_id, new_subtask_status):
|
||||
format_str = "Unexpected task_id '{}': already being executed - for subtask of instructor task '{}'"
|
||||
msg = format_str.format(current_task_id, entry)
|
||||
TASK_LOG.warning(msg)
|
||||
dog_stats_api.increment('instructor_task.subtask.duplicate.locked', tags=[entry.course_id])
|
||||
dog_stats_api.increment('instructor_task.subtask.duplicate.locked', tags=[_statsd_tag(entry.course_id)])
|
||||
raise DuplicateTaskException(msg)
|
||||
|
||||
|
||||
@@ -552,3 +552,11 @@ def _update_subtask_status(entry_id, current_task_id, new_subtask_status):
|
||||
else:
|
||||
TASK_LOG.debug("about to commit....")
|
||||
transaction.commit()
|
||||
|
||||
|
||||
def _statsd_tag(course_id):
|
||||
"""
|
||||
Calculate the tag we will use for DataDog.
|
||||
"""
|
||||
tag = unicode(course_id).encode('utf-8')
|
||||
return tag[:200]
|
||||
|
||||
@@ -244,15 +244,14 @@ def perform_module_state_update(update_fcn, filter_fcn, _entry_id, course_id, ta
|
||||
# get start time for task:
|
||||
start_time = time()
|
||||
|
||||
module_state_key = task_input.get('problem_url')
|
||||
usage_key = course_id.make_usage_key_from_deprecated_string(task_input.get('problem_url'))
|
||||
student_identifier = task_input.get('student')
|
||||
|
||||
# find the problem descriptor:
|
||||
module_descriptor = modulestore().get_instance(course_id, module_state_key)
|
||||
module_descriptor = modulestore().get_item(usage_key)
|
||||
|
||||
# find the module in question
|
||||
modules_to_update = StudentModule.objects.filter(course_id=course_id,
|
||||
module_state_key=module_state_key)
|
||||
modules_to_update = StudentModule.objects.filter(course_id=course_id, module_id=usage_key)
|
||||
|
||||
# give the option of updating an individual student. If not specified,
|
||||
# then updates all students who have responded to a problem so far
|
||||
@@ -394,13 +393,13 @@ def rescore_problem_module_state(xmodule_instance_args, module_descriptor, stude
|
||||
# unpack the StudentModule:
|
||||
course_id = student_module.course_id
|
||||
student = student_module.student
|
||||
module_state_key = student_module.module_state_key
|
||||
usage_key = student_module.module_state_key
|
||||
instance = _get_module_instance_for_task(course_id, student, module_descriptor, xmodule_instance_args, grade_bucket_type='rescore')
|
||||
|
||||
if instance is None:
|
||||
# Either permissions just changed, or someone is trying to be clever
|
||||
# and load something they shouldn't have access to.
|
||||
msg = "No module {loc} for student {student}--access denied?".format(loc=module_state_key,
|
||||
msg = "No module {loc} for student {student}--access denied?".format(loc=usage_key,
|
||||
student=student)
|
||||
TASK_LOG.debug(msg)
|
||||
raise UpdateProblemModuleStateError(msg)
|
||||
@@ -416,15 +415,15 @@ def rescore_problem_module_state(xmodule_instance_args, module_descriptor, stude
|
||||
if 'success' not in result:
|
||||
# don't consider these fatal, but false means that the individual call didn't complete:
|
||||
TASK_LOG.warning(u"error processing rescore call for course {course}, problem {loc} and student {student}: "
|
||||
u"unexpected response {msg}".format(msg=result, course=course_id, loc=module_state_key, student=student))
|
||||
u"unexpected response {msg}".format(msg=result, course=course_id, loc=usage_key, student=student))
|
||||
return UPDATE_STATUS_FAILED
|
||||
elif result['success'] not in ['correct', 'incorrect']:
|
||||
TASK_LOG.warning(u"error processing rescore call for course {course}, problem {loc} and student {student}: "
|
||||
u"{msg}".format(msg=result['success'], course=course_id, loc=module_state_key, student=student))
|
||||
u"{msg}".format(msg=result['success'], course=course_id, loc=usage_key, student=student))
|
||||
return UPDATE_STATUS_FAILED
|
||||
else:
|
||||
TASK_LOG.debug(u"successfully processed rescore call for course {course}, problem {loc} and student {student}: "
|
||||
u"{msg}".format(msg=result['success'], course=course_id, loc=module_state_key, student=student))
|
||||
u"{msg}".format(msg=result['success'], course=course_id, loc=usage_key, student=student))
|
||||
return UPDATE_STATUS_SUCCEEDED
|
||||
|
||||
|
||||
@@ -552,7 +551,7 @@ def push_grades_to_s3(_xmodule_instance_args, _entry_id, course_id, _task_input,
|
||||
|
||||
# Generate parts of the file name
|
||||
timestamp_str = start_time.strftime("%Y-%m-%d-%H%M")
|
||||
course_id_prefix = urllib.quote(course_id.replace("/", "_"))
|
||||
course_id_prefix = urllib.quote(course_id.to_deprecated_string().replace("/", "_"))
|
||||
|
||||
# Perform the actual upload
|
||||
report_store = ReportStore.from_config()
|
||||
|
||||
@@ -5,13 +5,14 @@ from factory.django import DjangoModelFactory
|
||||
from student.tests.factories import UserFactory as StudentUserFactory
|
||||
from instructor_task.models import InstructorTask
|
||||
from celery.states import PENDING
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
|
||||
|
||||
class InstructorTaskFactory(DjangoModelFactory):
|
||||
FACTORY_FOR = InstructorTask
|
||||
|
||||
task_type = 'rescore_problem'
|
||||
course_id = "MITx/999/Robot_Super_Course"
|
||||
course_id = SlashSeparatedCourseKey("MITx", "999", "Robot_Super_Course")
|
||||
task_input = json.dumps({})
|
||||
task_key = None
|
||||
task_id = None
|
||||
|
||||
@@ -22,7 +22,7 @@ from instructor_task.models import InstructorTask, PROGRESS
|
||||
from instructor_task.tests.test_base import (InstructorTaskTestCase,
|
||||
InstructorTaskCourseTestCase,
|
||||
InstructorTaskModuleTestCase,
|
||||
TEST_COURSE_ID)
|
||||
TEST_COURSE_KEY)
|
||||
|
||||
|
||||
class InstructorTaskReportTest(InstructorTaskTestCase):
|
||||
@@ -36,7 +36,7 @@ class InstructorTaskReportTest(InstructorTaskTestCase):
|
||||
self._create_failure_entry()
|
||||
self._create_success_entry()
|
||||
progress_task_ids = [self._create_progress_entry().task_id for _ in range(1, 5)]
|
||||
task_ids = [instructor_task.task_id for instructor_task in get_running_instructor_tasks(TEST_COURSE_ID)]
|
||||
task_ids = [instructor_task.task_id for instructor_task in get_running_instructor_tasks(TEST_COURSE_KEY)]
|
||||
self.assertEquals(set(task_ids), set(progress_task_ids))
|
||||
|
||||
def test_get_instructor_task_history(self):
|
||||
@@ -47,21 +47,21 @@ class InstructorTaskReportTest(InstructorTaskTestCase):
|
||||
expected_ids.append(self._create_success_entry().task_id)
|
||||
expected_ids.append(self._create_progress_entry().task_id)
|
||||
task_ids = [instructor_task.task_id for instructor_task
|
||||
in get_instructor_task_history(TEST_COURSE_ID, problem_url=self.problem_url)]
|
||||
in get_instructor_task_history(TEST_COURSE_KEY, usage_key=self.problem_url)]
|
||||
self.assertEquals(set(task_ids), set(expected_ids))
|
||||
# make the same call using explicit task_type:
|
||||
task_ids = [instructor_task.task_id for instructor_task
|
||||
in get_instructor_task_history(
|
||||
TEST_COURSE_ID,
|
||||
problem_url=self.problem_url,
|
||||
TEST_COURSE_KEY,
|
||||
usage_key=self.problem_url,
|
||||
task_type='rescore_problem'
|
||||
)]
|
||||
self.assertEquals(set(task_ids), set(expected_ids))
|
||||
# make the same call using a non-existent task_type:
|
||||
task_ids = [instructor_task.task_id for instructor_task
|
||||
in get_instructor_task_history(
|
||||
TEST_COURSE_ID,
|
||||
problem_url=self.problem_url,
|
||||
TEST_COURSE_KEY,
|
||||
usage_key=self.problem_url,
|
||||
task_type='dummy_type'
|
||||
)]
|
||||
self.assertEquals(set(task_ids), set())
|
||||
@@ -81,25 +81,25 @@ class InstructorTaskModuleSubmitTest(InstructorTaskModuleTestCase):
|
||||
course_id = self.course.id
|
||||
request = None
|
||||
with self.assertRaises(ItemNotFoundError):
|
||||
submit_rescore_problem_for_student(request, course_id, problem_url, self.student)
|
||||
submit_rescore_problem_for_student(request, problem_url, self.student)
|
||||
with self.assertRaises(ItemNotFoundError):
|
||||
submit_rescore_problem_for_all_students(request, course_id, problem_url)
|
||||
submit_rescore_problem_for_all_students(request, problem_url)
|
||||
with self.assertRaises(ItemNotFoundError):
|
||||
submit_reset_problem_attempts_for_all_students(request, course_id, problem_url)
|
||||
submit_reset_problem_attempts_for_all_students(request, problem_url)
|
||||
with self.assertRaises(ItemNotFoundError):
|
||||
submit_delete_problem_state_for_all_students(request, course_id, problem_url)
|
||||
submit_delete_problem_state_for_all_students(request, problem_url)
|
||||
|
||||
def test_submit_nonrescorable_modules(self):
|
||||
# confirm that a rescore of an existent but unscorable module returns an exception
|
||||
# (Note that it is easier to test a scoreable but non-rescorable module in test_tasks,
|
||||
# where we are creating real modules.)
|
||||
problem_url = self.problem_section.location.url()
|
||||
problem_url = self.problem_section.location
|
||||
course_id = self.course.id
|
||||
request = None
|
||||
with self.assertRaises(NotImplementedError):
|
||||
submit_rescore_problem_for_student(request, course_id, problem_url, self.student)
|
||||
submit_rescore_problem_for_student(request, problem_url, self.student)
|
||||
with self.assertRaises(NotImplementedError):
|
||||
submit_rescore_problem_for_all_students(request, course_id, problem_url)
|
||||
submit_rescore_problem_for_all_students(request, problem_url)
|
||||
|
||||
def _test_submit_with_long_url(self, task_function, student=None):
|
||||
problem_url_name = 'x' * 255
|
||||
@@ -107,9 +107,9 @@ class InstructorTaskModuleSubmitTest(InstructorTaskModuleTestCase):
|
||||
location = InstructorTaskModuleTestCase.problem_location(problem_url_name)
|
||||
with self.assertRaises(ValueError):
|
||||
if student is not None:
|
||||
task_function(self.create_task_request(self.instructor), self.course.id, location, student)
|
||||
task_function(self.create_task_request(self.instructor), location, student)
|
||||
else:
|
||||
task_function(self.create_task_request(self.instructor), self.course.id, location)
|
||||
task_function(self.create_task_request(self.instructor), location)
|
||||
|
||||
def test_submit_rescore_all_with_long_url(self):
|
||||
self._test_submit_with_long_url(submit_rescore_problem_for_all_students)
|
||||
@@ -129,11 +129,9 @@ class InstructorTaskModuleSubmitTest(InstructorTaskModuleTestCase):
|
||||
self.define_option_problem(problem_url_name)
|
||||
location = InstructorTaskModuleTestCase.problem_location(problem_url_name)
|
||||
if student is not None:
|
||||
instructor_task = task_function(self.create_task_request(self.instructor),
|
||||
self.course.id, location, student)
|
||||
instructor_task = task_function(self.create_task_request(self.instructor), location, student)
|
||||
else:
|
||||
instructor_task = task_function(self.create_task_request(self.instructor),
|
||||
self.course.id, location)
|
||||
instructor_task = task_function(self.create_task_request(self.instructor), location)
|
||||
|
||||
# test resubmitting, by updating the existing record:
|
||||
instructor_task = InstructorTask.objects.get(id=instructor_task.id)
|
||||
@@ -142,9 +140,9 @@ class InstructorTaskModuleSubmitTest(InstructorTaskModuleTestCase):
|
||||
|
||||
with self.assertRaises(AlreadyRunningError):
|
||||
if student is not None:
|
||||
task_function(self.create_task_request(self.instructor), self.course.id, location, student)
|
||||
task_function(self.create_task_request(self.instructor), location, student)
|
||||
else:
|
||||
task_function(self.create_task_request(self.instructor), self.course.id, location)
|
||||
task_function(self.create_task_request(self.instructor), location)
|
||||
|
||||
def test_submit_rescore_all(self):
|
||||
self._test_submit_task(submit_rescore_problem_for_all_students)
|
||||
|
||||
@@ -16,6 +16,7 @@ from capa.tests.response_xml_factory import OptionResponseXMLFactory
|
||||
from xmodule.modulestore.django import editable_modulestore
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.locations import Location, SlashSeparatedCourseKey
|
||||
|
||||
from student.tests.factories import CourseEnrollmentFactory, UserFactory
|
||||
from courseware.model_data import StudentModule
|
||||
@@ -28,10 +29,10 @@ from instructor_task.views import instructor_task_status
|
||||
|
||||
|
||||
TEST_COURSE_ORG = 'edx'
|
||||
TEST_COURSE_NAME = 'test course'
|
||||
TEST_COURSE_NAME = 'test_course'
|
||||
TEST_COURSE_NUMBER = '1.23x'
|
||||
TEST_COURSE_KEY = SlashSeparatedCourseKey(TEST_COURSE_ORG, TEST_COURSE_NUMBER, TEST_COURSE_NAME)
|
||||
TEST_SECTION_NAME = "Problem"
|
||||
TEST_COURSE_ID = 'edx/1.23x/test_course'
|
||||
|
||||
TEST_FAILURE_MESSAGE = 'task failed horribly'
|
||||
TEST_FAILURE_EXCEPTION = 'RandomCauseError'
|
||||
@@ -54,9 +55,7 @@ class InstructorTaskTestCase(TestCase):
|
||||
"""
|
||||
Create an internal location for a test problem.
|
||||
"""
|
||||
return "i4x://{org}/{number}/problem/{problem_url_name}".format(org='edx',
|
||||
number='1.23x',
|
||||
problem_url_name=problem_url_name)
|
||||
return TEST_COURSE_KEY.make_usage_key('problem', problem_url_name)
|
||||
|
||||
def _create_entry(self, task_state=QUEUING, task_output=None, student=None):
|
||||
"""Creates a InstructorTask entry for testing."""
|
||||
@@ -64,7 +63,7 @@ class InstructorTaskTestCase(TestCase):
|
||||
progress_json = json.dumps(task_output) if task_output is not None else None
|
||||
task_input, task_key = encode_problem_and_student_input(self.problem_url, student)
|
||||
|
||||
instructor_task = InstructorTaskFactory.create(course_id=TEST_COURSE_ID,
|
||||
instructor_task = InstructorTaskFactory.create(course_id=TEST_COURSE_KEY,
|
||||
requester=self.instructor,
|
||||
task_input=json.dumps(task_input),
|
||||
task_key=task_key,
|
||||
@@ -180,11 +179,9 @@ class InstructorTaskModuleTestCase(InstructorTaskCourseTestCase):
|
||||
Create an internal location for a test problem.
|
||||
"""
|
||||
if "i4x:" in problem_url_name:
|
||||
return problem_url_name
|
||||
return Location.from_deprecated_string(problem_url_name)
|
||||
else:
|
||||
return "i4x://{org}/{number}/problem/{problem_url_name}".format(org=TEST_COURSE_ORG,
|
||||
number=TEST_COURSE_NUMBER,
|
||||
problem_url_name=problem_url_name)
|
||||
return TEST_COURSE_KEY.make_usage_key('problem', problem_url_name)
|
||||
|
||||
def define_option_problem(self, problem_url_name):
|
||||
"""Create the problem definition so the answer is Option 1"""
|
||||
@@ -195,6 +192,7 @@ class InstructorTaskModuleTestCase(InstructorTaskCourseTestCase):
|
||||
'num_responses': 2}
|
||||
problem_xml = factory.build_xml(**factory_args)
|
||||
ItemFactory.create(parent_location=self.problem_section.location,
|
||||
parent=self.problem_section,
|
||||
category="problem",
|
||||
display_name=str(problem_url_name),
|
||||
data=problem_xml)
|
||||
@@ -208,7 +206,7 @@ class InstructorTaskModuleTestCase(InstructorTaskCourseTestCase):
|
||||
'num_responses': 2}
|
||||
problem_xml = factory.build_xml(**factory_args)
|
||||
location = InstructorTaskTestCase.problem_location(problem_url_name)
|
||||
item = self.module_store.get_instance(self.course.id, location)
|
||||
item = self.module_store.get_item(location)
|
||||
item.data = problem_xml
|
||||
self.module_store.update_item(item, '**replace_user**')
|
||||
|
||||
@@ -217,5 +215,5 @@ class InstructorTaskModuleTestCase(InstructorTaskCourseTestCase):
|
||||
return StudentModule.objects.get(course_id=self.course.id,
|
||||
student=User.objects.get(username=username),
|
||||
module_type=descriptor.location.category,
|
||||
module_state_key=descriptor.location.url(),
|
||||
module_id=descriptor.location,
|
||||
)
|
||||
|
||||
@@ -58,11 +58,12 @@ class TestIntegrationTask(InstructorTaskModuleTestCase):
|
||||
# on the right problem:
|
||||
self.login_username(username)
|
||||
# make ajax call:
|
||||
modx_url = reverse('xblock_handler',
|
||||
kwargs={'course_id': self.course.id,
|
||||
'usage_id': quote_slashes(InstructorTaskModuleTestCase.problem_location(problem_url_name)),
|
||||
'handler': 'xmodule_handler',
|
||||
'suffix': 'problem_check', })
|
||||
modx_url = reverse('xblock_handler', kwargs={
|
||||
'course_id': self.course.id.to_deprecated_string(),
|
||||
'usage_id': quote_slashes(InstructorTaskModuleTestCase.problem_location(problem_url_name).to_deprecated_string()),
|
||||
'handler': 'xmodule_handler',
|
||||
'suffix': 'problem_check',
|
||||
})
|
||||
|
||||
# we assume we have two responses, so assign them the correct identifiers.
|
||||
resp = self.client.post(modx_url, {
|
||||
@@ -79,7 +80,7 @@ class TestIntegrationTask(InstructorTaskModuleTestCase):
|
||||
self.assertEqual(instructor_task.task_type, task_type)
|
||||
task_input = json.loads(instructor_task.task_input)
|
||||
self.assertFalse('student' in task_input)
|
||||
self.assertEqual(task_input['problem_url'], InstructorTaskModuleTestCase.problem_location(problem_url_name))
|
||||
self.assertEqual(task_input['problem_url'], InstructorTaskModuleTestCase.problem_location(problem_url_name).to_deprecated_string())
|
||||
status = json.loads(instructor_task.task_output)
|
||||
self.assertEqual(status['exception'], 'ZeroDivisionError')
|
||||
self.assertEqual(status['message'], expected_message)
|
||||
@@ -112,11 +113,12 @@ class TestRescoringTask(TestIntegrationTask):
|
||||
# on the right problem:
|
||||
self.login_username(username)
|
||||
# make ajax call:
|
||||
modx_url = reverse('xblock_handler',
|
||||
kwargs={'course_id': self.course.id,
|
||||
'usage_id': quote_slashes(InstructorTaskModuleTestCase.problem_location(problem_url_name)),
|
||||
'handler': 'xmodule_handler',
|
||||
'suffix': 'problem_get', })
|
||||
modx_url = reverse('xblock_handler', kwargs={
|
||||
'course_id': self.course.id.to_deprecated_string(),
|
||||
'usage_id': quote_slashes(InstructorTaskModuleTestCase.problem_location(problem_url_name).to_deprecated_string()),
|
||||
'handler': 'xmodule_handler',
|
||||
'suffix': 'problem_get',
|
||||
})
|
||||
resp = self.client.post(modx_url, {})
|
||||
return resp
|
||||
|
||||
@@ -142,12 +144,12 @@ class TestRescoringTask(TestIntegrationTask):
|
||||
|
||||
def submit_rescore_all_student_answers(self, instructor, problem_url_name):
|
||||
"""Submits the particular problem for rescoring"""
|
||||
return submit_rescore_problem_for_all_students(self.create_task_request(instructor), self.course.id,
|
||||
return submit_rescore_problem_for_all_students(self.create_task_request(instructor),
|
||||
InstructorTaskModuleTestCase.problem_location(problem_url_name))
|
||||
|
||||
def submit_rescore_one_student_answer(self, instructor, problem_url_name, student):
|
||||
"""Submits the particular problem for rescoring for a particular student"""
|
||||
return submit_rescore_problem_for_student(self.create_task_request(instructor), self.course.id,
|
||||
return submit_rescore_problem_for_student(self.create_task_request(instructor),
|
||||
InstructorTaskModuleTestCase.problem_location(problem_url_name),
|
||||
student)
|
||||
|
||||
@@ -157,7 +159,7 @@ class TestRescoringTask(TestIntegrationTask):
|
||||
problem_url_name = 'H1P1'
|
||||
self.define_option_problem(problem_url_name)
|
||||
location = InstructorTaskModuleTestCase.problem_location(problem_url_name)
|
||||
descriptor = self.module_store.get_instance(self.course.id, location)
|
||||
descriptor = self.module_store.get_item(location)
|
||||
|
||||
# first store answers for each of the separate users:
|
||||
self.submit_student_answer('u1', problem_url_name, [OPTION_1, OPTION_1])
|
||||
@@ -227,7 +229,7 @@ class TestRescoringTask(TestIntegrationTask):
|
||||
self.assertEqual(instructor_task.task_type, 'rescore_problem')
|
||||
task_input = json.loads(instructor_task.task_input)
|
||||
self.assertFalse('student' in task_input)
|
||||
self.assertEqual(task_input['problem_url'], InstructorTaskModuleTestCase.problem_location(problem_url_name))
|
||||
self.assertEqual(task_input['problem_url'], InstructorTaskModuleTestCase.problem_location(problem_url_name).to_deprecated_string())
|
||||
status = json.loads(instructor_task.task_output)
|
||||
self.assertEqual(status['attempted'], 1)
|
||||
self.assertEqual(status['succeeded'], 0)
|
||||
@@ -288,8 +290,8 @@ class TestRescoringTask(TestIntegrationTask):
|
||||
""" % ('!=' if redefine else '=='))
|
||||
problem_xml = factory.build_xml(script=script, cfn="check_func", expect="42", num_responses=1)
|
||||
if redefine:
|
||||
descriptor = self.module_store.get_instance(
|
||||
self.course.id, InstructorTaskModuleTestCase.problem_location(problem_url_name)
|
||||
descriptor = self.module_store.get_item(
|
||||
InstructorTaskModuleTestCase.problem_location(problem_url_name)
|
||||
)
|
||||
descriptor.data = problem_xml
|
||||
self.module_store.update_item(descriptor, '**replace_user**')
|
||||
@@ -311,7 +313,7 @@ class TestRescoringTask(TestIntegrationTask):
|
||||
problem_url_name = 'H1P1'
|
||||
self.define_randomized_custom_response_problem(problem_url_name)
|
||||
location = InstructorTaskModuleTestCase.problem_location(problem_url_name)
|
||||
descriptor = self.module_store.get_instance(self.course.id, location)
|
||||
descriptor = self.module_store.get_item(location)
|
||||
# run with more than one user
|
||||
userlist = ['u1', 'u2', 'u3', 'u4']
|
||||
for username in userlist:
|
||||
@@ -375,10 +377,10 @@ class TestResetAttemptsTask(TestIntegrationTask):
|
||||
state = json.loads(module.state)
|
||||
return state['attempts']
|
||||
|
||||
def reset_problem_attempts(self, instructor, problem_url_name):
|
||||
def reset_problem_attempts(self, instructor, location):
|
||||
"""Submits the current problem for resetting"""
|
||||
return submit_reset_problem_attempts_for_all_students(self.create_task_request(instructor), self.course.id,
|
||||
InstructorTaskModuleTestCase.problem_location(problem_url_name))
|
||||
return submit_reset_problem_attempts_for_all_students(self.create_task_request(instructor),
|
||||
location)
|
||||
|
||||
def test_reset_attempts_on_problem(self):
|
||||
"""Run reset-attempts scenario on option problem"""
|
||||
@@ -386,7 +388,7 @@ class TestResetAttemptsTask(TestIntegrationTask):
|
||||
problem_url_name = 'H1P1'
|
||||
self.define_option_problem(problem_url_name)
|
||||
location = InstructorTaskModuleTestCase.problem_location(problem_url_name)
|
||||
descriptor = self.module_store.get_instance(self.course.id, location)
|
||||
descriptor = self.module_store.get_item(location)
|
||||
num_attempts = 3
|
||||
# first store answers for each of the separate users:
|
||||
for _ in range(num_attempts):
|
||||
@@ -396,7 +398,7 @@ class TestResetAttemptsTask(TestIntegrationTask):
|
||||
for username in self.userlist:
|
||||
self.assertEquals(self.get_num_attempts(username, descriptor), num_attempts)
|
||||
|
||||
self.reset_problem_attempts('instructor', problem_url_name)
|
||||
self.reset_problem_attempts('instructor', location)
|
||||
|
||||
for username in self.userlist:
|
||||
self.assertEquals(self.get_num_attempts(username, descriptor), 0)
|
||||
@@ -404,19 +406,20 @@ class TestResetAttemptsTask(TestIntegrationTask):
|
||||
def test_reset_failure(self):
|
||||
"""Simulate a failure in resetting attempts on a problem"""
|
||||
problem_url_name = 'H1P1'
|
||||
location = InstructorTaskModuleTestCase.problem_location(problem_url_name)
|
||||
self.define_option_problem(problem_url_name)
|
||||
self.submit_student_answer('u1', problem_url_name, [OPTION_1, OPTION_1])
|
||||
|
||||
expected_message = "bad things happened"
|
||||
with patch('courseware.models.StudentModule.save') as mock_save:
|
||||
mock_save.side_effect = ZeroDivisionError(expected_message)
|
||||
instructor_task = self.reset_problem_attempts('instructor', problem_url_name)
|
||||
instructor_task = self.reset_problem_attempts('instructor', location)
|
||||
self._assert_task_failure(instructor_task.id, 'reset_problem_attempts', problem_url_name, expected_message)
|
||||
|
||||
def test_reset_non_problem(self):
|
||||
"""confirm that a non-problem can still be successfully reset"""
|
||||
problem_url_name = self.problem_section.location.url()
|
||||
instructor_task = self.reset_problem_attempts('instructor', problem_url_name)
|
||||
location = self.problem_section.location
|
||||
instructor_task = self.reset_problem_attempts('instructor', location)
|
||||
instructor_task = InstructorTask.objects.get(id=instructor_task.id)
|
||||
self.assertEqual(instructor_task.task_state, SUCCESS)
|
||||
|
||||
@@ -436,10 +439,9 @@ class TestDeleteProblemTask(TestIntegrationTask):
|
||||
self.create_student(username)
|
||||
self.logout()
|
||||
|
||||
def delete_problem_state(self, instructor, problem_url_name):
|
||||
def delete_problem_state(self, instructor, location):
|
||||
"""Submits the current problem for deletion"""
|
||||
return submit_delete_problem_state_for_all_students(self.create_task_request(instructor), self.course.id,
|
||||
InstructorTaskModuleTestCase.problem_location(problem_url_name))
|
||||
return submit_delete_problem_state_for_all_students(self.create_task_request(instructor), location)
|
||||
|
||||
def test_delete_problem_state(self):
|
||||
"""Run delete-state scenario on option problem"""
|
||||
@@ -447,7 +449,7 @@ class TestDeleteProblemTask(TestIntegrationTask):
|
||||
problem_url_name = 'H1P1'
|
||||
self.define_option_problem(problem_url_name)
|
||||
location = InstructorTaskModuleTestCase.problem_location(problem_url_name)
|
||||
descriptor = self.module_store.get_instance(self.course.id, location)
|
||||
descriptor = self.module_store.get_item(location)
|
||||
# first store answers for each of the separate users:
|
||||
for username in self.userlist:
|
||||
self.submit_student_answer(username, problem_url_name, [OPTION_1, OPTION_1])
|
||||
@@ -455,7 +457,7 @@ class TestDeleteProblemTask(TestIntegrationTask):
|
||||
for username in self.userlist:
|
||||
self.assertTrue(self.get_student_module(username, descriptor) is not None)
|
||||
# run delete task:
|
||||
self.delete_problem_state('instructor', problem_url_name)
|
||||
self.delete_problem_state('instructor', location)
|
||||
# confirm that no state can be found:
|
||||
for username in self.userlist:
|
||||
with self.assertRaises(StudentModule.DoesNotExist):
|
||||
@@ -464,18 +466,19 @@ class TestDeleteProblemTask(TestIntegrationTask):
|
||||
def test_delete_failure(self):
|
||||
"""Simulate a failure in deleting state of a problem"""
|
||||
problem_url_name = 'H1P1'
|
||||
location = InstructorTaskModuleTestCase.problem_location(problem_url_name)
|
||||
self.define_option_problem(problem_url_name)
|
||||
self.submit_student_answer('u1', problem_url_name, [OPTION_1, OPTION_1])
|
||||
|
||||
expected_message = "bad things happened"
|
||||
with patch('courseware.models.StudentModule.delete') as mock_delete:
|
||||
mock_delete.side_effect = ZeroDivisionError(expected_message)
|
||||
instructor_task = self.delete_problem_state('instructor', problem_url_name)
|
||||
instructor_task = self.delete_problem_state('instructor', location)
|
||||
self._assert_task_failure(instructor_task.id, 'delete_problem_state', problem_url_name, expected_message)
|
||||
|
||||
def test_delete_non_problem(self):
|
||||
"""confirm that a non-problem can still be successfully deleted"""
|
||||
problem_url_name = self.problem_section.location.url()
|
||||
instructor_task = self.delete_problem_state('instructor', problem_url_name)
|
||||
location = self.problem_section.location
|
||||
instructor_task = self.delete_problem_state('instructor', location)
|
||||
instructor_task = InstructorTask.objects.get(id=instructor_task.id)
|
||||
self.assertEqual(instructor_task.task_state, SUCCESS)
|
||||
|
||||
@@ -13,6 +13,7 @@ from mock import Mock, MagicMock, patch
|
||||
from celery.states import SUCCESS, FAILURE
|
||||
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from xmodule.modulestore.locations import i4xEncoder
|
||||
|
||||
from courseware.models import StudentModule
|
||||
from courseware.tests.factories import StudentModuleFactory
|
||||
@@ -37,21 +38,21 @@ class TestInstructorTasks(InstructorTaskModuleTestCase):
|
||||
super(InstructorTaskModuleTestCase, self).setUp()
|
||||
self.initialize_course()
|
||||
self.instructor = self.create_instructor('instructor')
|
||||
self.problem_url = InstructorTaskModuleTestCase.problem_location(PROBLEM_URL_NAME)
|
||||
self.location = InstructorTaskModuleTestCase.problem_location(PROBLEM_URL_NAME)
|
||||
|
||||
def _create_input_entry(self, student_ident=None, use_problem_url=True, course_id=None):
|
||||
"""Creates a InstructorTask entry for testing."""
|
||||
task_id = str(uuid4())
|
||||
task_input = {}
|
||||
if use_problem_url:
|
||||
task_input['problem_url'] = self.problem_url
|
||||
task_input['problem_url'] = self.location
|
||||
if student_ident is not None:
|
||||
task_input['student'] = student_ident
|
||||
|
||||
course_id = course_id or self.course.id
|
||||
instructor_task = InstructorTaskFactory.create(course_id=course_id,
|
||||
requester=self.instructor,
|
||||
task_input=json.dumps(task_input),
|
||||
task_input=json.dumps(task_input, cls=i4xEncoder),
|
||||
task_key='dummy value',
|
||||
task_id=task_id)
|
||||
return instructor_task
|
||||
@@ -127,7 +128,7 @@ class TestInstructorTasks(InstructorTaskModuleTestCase):
|
||||
for student in students:
|
||||
CourseEnrollmentFactory.create(course_id=self.course.id, user=student)
|
||||
StudentModuleFactory.create(course_id=self.course.id,
|
||||
module_state_key=self.problem_url,
|
||||
module_id=self.location,
|
||||
student=student,
|
||||
grade=grade,
|
||||
max_grade=max_grade,
|
||||
@@ -139,7 +140,7 @@ class TestInstructorTasks(InstructorTaskModuleTestCase):
|
||||
for student in students:
|
||||
module = StudentModule.objects.get(course_id=self.course.id,
|
||||
student=student,
|
||||
module_state_key=self.problem_url)
|
||||
module_id=self.location)
|
||||
state = json.loads(module.state)
|
||||
self.assertEquals(state['attempts'], num_attempts)
|
||||
|
||||
@@ -356,7 +357,7 @@ class TestResetAttemptsInstructorTask(TestInstructorTasks):
|
||||
for student in students:
|
||||
module = StudentModule.objects.get(course_id=self.course.id,
|
||||
student=student,
|
||||
module_state_key=self.problem_url)
|
||||
module_id=self.location)
|
||||
state = json.loads(module.state)
|
||||
self.assertEquals(state['attempts'], initial_attempts)
|
||||
|
||||
@@ -382,7 +383,7 @@ class TestResetAttemptsInstructorTask(TestInstructorTasks):
|
||||
for index, student in enumerate(students):
|
||||
module = StudentModule.objects.get(course_id=self.course.id,
|
||||
student=student,
|
||||
module_state_key=self.problem_url)
|
||||
module_id=self.location)
|
||||
state = json.loads(module.state)
|
||||
if index == 3:
|
||||
self.assertEquals(state['attempts'], 0)
|
||||
@@ -429,11 +430,11 @@ class TestDeleteStateInstructorTask(TestInstructorTasks):
|
||||
for student in students:
|
||||
StudentModule.objects.get(course_id=self.course.id,
|
||||
student=student,
|
||||
module_state_key=self.problem_url)
|
||||
module_id=self.location)
|
||||
self._test_run_with_task(delete_problem_state, 'deleted', num_students)
|
||||
# confirm that no state can be found anymore:
|
||||
for student in students:
|
||||
with self.assertRaises(StudentModule.DoesNotExist):
|
||||
StudentModule.objects.get(course_id=self.course.id,
|
||||
student=student,
|
||||
module_state_key=self.problem_url)
|
||||
module_id=self.location)
|
||||
|
||||
@@ -4,6 +4,8 @@ from django.db import models, transaction
|
||||
|
||||
from student.models import User
|
||||
|
||||
from xmodule_django.models import CourseKeyField
|
||||
|
||||
log = logging.getLogger("edx.licenses")
|
||||
|
||||
|
||||
@@ -11,7 +13,7 @@ class CourseSoftware(models.Model):
|
||||
name = models.CharField(max_length=255)
|
||||
full_name = models.CharField(max_length=255)
|
||||
url = models.CharField(max_length=255)
|
||||
course_id = models.CharField(max_length=255)
|
||||
course_id = CourseKeyField(max_length=255)
|
||||
|
||||
def __unicode__(self):
|
||||
return u'{0} for {1}'.format(self.name, self.course_id)
|
||||
|
||||
@@ -6,6 +6,7 @@ from collections import namedtuple, defaultdict
|
||||
|
||||
|
||||
from edxmako.shortcuts import render_to_string
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.models import User
|
||||
@@ -59,6 +60,7 @@ def user_software_license(request):
|
||||
if not match:
|
||||
raise Http404
|
||||
course_id = match.groupdict().get('id', '')
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
|
||||
user_id = request.session.get('_auth_user_id')
|
||||
software_name = request.POST.get('software')
|
||||
@@ -66,7 +68,7 @@ def user_software_license(request):
|
||||
|
||||
try:
|
||||
software = CourseSoftware.objects.get(name=software_name,
|
||||
course_id=course_id)
|
||||
course_id=course_key)
|
||||
except CourseSoftware.DoesNotExist:
|
||||
raise Http404
|
||||
|
||||
|
||||
@@ -1,237 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Test email scripts.
|
||||
"""
|
||||
from smtplib import SMTPDataError, SMTPServerDisconnected
|
||||
import datetime
|
||||
import json
|
||||
import mock
|
||||
|
||||
from boto.ses.exceptions import SESIllegalAddressError, SESIdentityNotVerifiedError
|
||||
from certificates.models import GeneratedCertificate
|
||||
from django.contrib.auth.models import User
|
||||
from django.conf import settings
|
||||
from django.test.utils import override_settings
|
||||
from django.core import mail
|
||||
from django.utils.timezone import utc
|
||||
from django.test import TestCase
|
||||
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from student.models import UserProfile
|
||||
from xmodule.modulestore.tests.django_utils import mixed_store_config
|
||||
from linkedin.models import LinkedIn
|
||||
from linkedin.management.commands import linkedin_mailusers as mailusers
|
||||
from linkedin.management.commands.linkedin_mailusers import MAX_ATTEMPTS
|
||||
|
||||
MODULE = 'linkedin.management.commands.linkedin_mailusers.'
|
||||
|
||||
TEST_DATA_MIXED_MODULESTORE = mixed_store_config(settings.COMMON_TEST_DATA_ROOT, {})
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
|
||||
class MailusersTests(TestCase):
|
||||
"""
|
||||
Test mail users command.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
CourseFactory.create(org='TESTX', number='1', display_name='TEST1',
|
||||
start=datetime.datetime(2010, 5, 12, 2, 42, tzinfo=utc),
|
||||
end=datetime.datetime(2011, 5, 12, 2, 42, tzinfo=utc))
|
||||
CourseFactory.create(org='TESTX', number='2', display_name='TEST2',
|
||||
start=datetime.datetime(2010, 5, 12, 2, 42, tzinfo=utc),
|
||||
end=datetime.datetime(2011, 5, 12, 2, 42, tzinfo=utc))
|
||||
CourseFactory.create(org='TESTX', number='3', display_name='TEST3',
|
||||
start=datetime.datetime(2010, 5, 12, 2, 42, tzinfo=utc),
|
||||
end=datetime.datetime(2011, 5, 12, 2, 42, tzinfo=utc))
|
||||
|
||||
self.fred = fred = User(username='fred', email='fred@bedrock.gov')
|
||||
fred.save()
|
||||
UserProfile(user=fred, name='Fred Flintstone').save()
|
||||
LinkedIn(user=fred, has_linkedin_account=True).save()
|
||||
self.barney = barney = User(
|
||||
username='barney', email='barney@bedrock.gov')
|
||||
barney.save()
|
||||
|
||||
LinkedIn(user=barney, has_linkedin_account=True).save()
|
||||
UserProfile(user=barney, name='Barney Rubble').save()
|
||||
|
||||
self.adam = adam = User(
|
||||
username='adam', email='adam@adam.gov')
|
||||
adam.save()
|
||||
|
||||
LinkedIn(user=adam, has_linkedin_account=True).save()
|
||||
UserProfile(user=adam, name='Adam (חיים פּלי)').save()
|
||||
self.cert1 = cert1 = GeneratedCertificate(
|
||||
status='downloadable',
|
||||
user=fred,
|
||||
course_id='TESTX/1/TEST1',
|
||||
name='TestX/Intro101',
|
||||
download_url='http://test.foo/test')
|
||||
cert1.save()
|
||||
cert2 = GeneratedCertificate(
|
||||
status='downloadable',
|
||||
user=fred,
|
||||
course_id='TESTX/2/TEST2')
|
||||
cert2.save()
|
||||
cert3 = GeneratedCertificate(
|
||||
status='downloadable',
|
||||
user=barney,
|
||||
course_id='TESTX/3/TEST3')
|
||||
cert3.save()
|
||||
cert5 = GeneratedCertificate(
|
||||
status='downloadable',
|
||||
user=adam,
|
||||
course_id='TESTX/3/TEST3')
|
||||
cert5.save()
|
||||
|
||||
@mock.patch.dict('django.conf.settings.LINKEDIN_API',
|
||||
{'EMAIL_WHITELIST': ['barney@bedrock.gov']})
|
||||
def test_mail_users_with_whitelist(self):
|
||||
"""
|
||||
Test emailing users.
|
||||
"""
|
||||
fut = mailusers.Command().handle
|
||||
fut()
|
||||
self.assertEqual(
|
||||
json.loads(self.barney.linkedin.emailed_courses), ['TESTX/3/TEST3'])
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
self.assertEqual(
|
||||
mail.outbox[0].to, ['Barney Rubble <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):
|
||||
settings.EDX_ROOT_URL,
|
||||
escape(cdir),
|
||||
escape(cdir),
|
||||
course.location.url()
|
||||
course.location.to_deprecated_string()
|
||||
)
|
||||
html += '</ol>'
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.http import HttpResponse, Http404
|
||||
from django.core.exceptions import ValidationError
|
||||
@@ -34,11 +35,11 @@ ApiResponse = collections.namedtuple('ApiResponse', ['http_response', 'data'])
|
||||
# API requests are routed through api_request() using the resource map.
|
||||
|
||||
|
||||
def api_enabled(request, course_id):
|
||||
def api_enabled(request, course_key):
|
||||
'''
|
||||
Returns True if the api is enabled for the course, otherwise False.
|
||||
'''
|
||||
course = _get_course(request, course_id)
|
||||
course = _get_course(request, course_key)
|
||||
return notes_enabled_for_course(course)
|
||||
|
||||
|
||||
@@ -49,9 +50,11 @@ def api_request(request, course_id, **kwargs):
|
||||
Raises a 404 if the requested resource does not exist or notes are
|
||||
disabled for the course.
|
||||
'''
|
||||
assert isinstance(course_id, basestring)
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
|
||||
# Verify that the api should be accessible to this course
|
||||
if not api_enabled(request, course_id):
|
||||
if not api_enabled(request, course_key):
|
||||
log.debug('Notes are disabled for course: {0}'.format(course_id))
|
||||
raise Http404
|
||||
|
||||
@@ -78,7 +81,7 @@ def api_request(request, course_id, **kwargs):
|
||||
|
||||
log.debug('API request: {0} {1}'.format(resource_method, resource_name))
|
||||
|
||||
api_response = module[func](request, course_id, **kwargs)
|
||||
api_response = module[func](request, course_key, **kwargs)
|
||||
http_response = api_format(api_response)
|
||||
|
||||
return http_response
|
||||
@@ -104,33 +107,33 @@ def api_format(api_response):
|
||||
return http_response
|
||||
|
||||
|
||||
def _get_course(request, course_id):
|
||||
def _get_course(request, course_key):
|
||||
'''
|
||||
Helper function to load and return a user's course.
|
||||
'''
|
||||
return get_course_with_access(request.user, course_id, 'load')
|
||||
return get_course_with_access(request.user, 'load', course_key)
|
||||
|
||||
#----------------------------------------------------------------------#
|
||||
# API actions exposed via the resource map.
|
||||
|
||||
|
||||
def index(request, course_id):
|
||||
def index(request, course_key):
|
||||
'''
|
||||
Returns a list of annotation objects.
|
||||
'''
|
||||
MAX_LIMIT = API_SETTINGS.get('MAX_NOTE_LIMIT')
|
||||
|
||||
notes = Note.objects.order_by('id').filter(course_id=course_id,
|
||||
notes = Note.objects.order_by('id').filter(course_id=course_key,
|
||||
user=request.user)[:MAX_LIMIT]
|
||||
|
||||
return ApiResponse(http_response=HttpResponse(), data=[note.as_dict() for note in notes])
|
||||
|
||||
|
||||
def create(request, course_id):
|
||||
def create(request, course_key):
|
||||
'''
|
||||
Receives an annotation object to create and returns a 303 with the read location.
|
||||
'''
|
||||
note = Note(course_id=course_id, user=request.user)
|
||||
note = Note(course_id=course_key, user=request.user)
|
||||
|
||||
try:
|
||||
note.clean(request.body)
|
||||
@@ -145,7 +148,7 @@ def create(request, course_id):
|
||||
return ApiResponse(http_response=response, data=None)
|
||||
|
||||
|
||||
def read(request, course_id, note_id):
|
||||
def read(request, course_key, note_id):
|
||||
'''
|
||||
Returns a single annotation object.
|
||||
'''
|
||||
@@ -160,7 +163,7 @@ def read(request, course_id, note_id):
|
||||
return ApiResponse(http_response=HttpResponse(), data=note.as_dict())
|
||||
|
||||
|
||||
def update(request, course_id, note_id):
|
||||
def update(request, course_key, note_id):
|
||||
'''
|
||||
Updates an annotation object and returns a 303 with the read location.
|
||||
'''
|
||||
@@ -203,7 +206,7 @@ def delete(request, course_id, note_id):
|
||||
return ApiResponse(http_response=HttpResponse('', status=204), data=None)
|
||||
|
||||
|
||||
def search(request, course_id):
|
||||
def search(request, course_key):
|
||||
'''
|
||||
Returns a subset of annotation objects based on a search query.
|
||||
'''
|
||||
@@ -228,7 +231,7 @@ def search(request, course_id):
|
||||
limit = MAX_LIMIT
|
||||
|
||||
# set filters
|
||||
filters = {'course_id': course_id, 'user': request.user}
|
||||
filters = {'course_id': course_key, 'user': request.user}
|
||||
if uri != '':
|
||||
filters['uri'] = uri
|
||||
|
||||
@@ -244,7 +247,7 @@ def search(request, course_id):
|
||||
return ApiResponse(http_response=HttpResponse(), data=result)
|
||||
|
||||
|
||||
def root(request, course_id):
|
||||
def root(request, course_key):
|
||||
'''
|
||||
Returns version information about the API.
|
||||
'''
|
||||
|
||||
@@ -5,10 +5,12 @@ from django.core.exceptions import ValidationError
|
||||
from django.utils.html import strip_tags
|
||||
import json
|
||||
|
||||
from xmodule_django.models import CourseKeyField
|
||||
|
||||
|
||||
class Note(models.Model):
|
||||
user = models.ForeignKey(User, db_index=True)
|
||||
course_id = models.CharField(max_length=255, db_index=True)
|
||||
course_id = CourseKeyField(max_length=255, db_index=True)
|
||||
uri = models.CharField(max_length=255, db_index=True)
|
||||
text = models.TextField(default="")
|
||||
quote = models.TextField(default="")
|
||||
@@ -56,7 +58,8 @@ class Note(models.Model):
|
||||
"""
|
||||
Returns the absolute url for the note object.
|
||||
"""
|
||||
kwargs = {'course_id': self.course_id, 'note_id': str(self.pk)}
|
||||
# pylint: disable=no-member
|
||||
kwargs = {'course_id': self.course_id.to_deprecated_string(), 'note_id': str(self.pk)}
|
||||
return reverse('notes_api_note', kwargs=kwargs)
|
||||
|
||||
def as_dict(self):
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
Unit tests for the notes app.
|
||||
"""
|
||||
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
from django.test import TestCase
|
||||
from django.test.client import Client
|
||||
from django.core.urlresolvers import reverse
|
||||
@@ -55,10 +56,10 @@ class ApiTest(TestCase):
|
||||
self.student = User.objects.create_user('student', 'student@test.com', self.password)
|
||||
self.student2 = User.objects.create_user('student2', 'student2@test.com', self.password)
|
||||
self.instructor = User.objects.create_user('instructor', 'instructor@test.com', self.password)
|
||||
self.course_id = 'HarvardX/CB22x/The_Ancient_Greek_Hero'
|
||||
self.course_key = SlashSeparatedCourseKey('HarvardX', 'CB22x', 'The_Ancient_Greek_Hero')
|
||||
self.note = {
|
||||
'user': self.student,
|
||||
'course_id': self.course_id,
|
||||
'course_id': self.course_key,
|
||||
'uri': '/',
|
||||
'text': 'foo',
|
||||
'quote': 'bar',
|
||||
@@ -87,7 +88,7 @@ class ApiTest(TestCase):
|
||||
self.client.login(username=username, password=password)
|
||||
|
||||
def url(self, name, args={}):
|
||||
args.update({'course_id': self.course_id})
|
||||
args.update({'course_id': self.course_key.to_deprecated_string()})
|
||||
return reverse(name, kwargs=args)
|
||||
|
||||
def create_notes(self, num_notes, create=True):
|
||||
@@ -343,10 +344,10 @@ class NoteTest(TestCase):
|
||||
def setUp(self):
|
||||
self.password = 'abc'
|
||||
self.student = User.objects.create_user('student', 'student@test.com', self.password)
|
||||
self.course_id = 'HarvardX/CB22x/The_Ancient_Greek_Hero'
|
||||
self.course_key = SlashSeparatedCourseKey('HarvardX', 'CB22x', 'The_Ancient_Greek_Hero')
|
||||
self.note = {
|
||||
'user': self.student,
|
||||
'course_id': self.course_id,
|
||||
'course_id': self.course_key,
|
||||
'uri': '/',
|
||||
'text': 'foo',
|
||||
'quote': 'bar',
|
||||
@@ -361,7 +362,7 @@ class NoteTest(TestCase):
|
||||
reference_note = models.Note(**self.note)
|
||||
body = reference_note.as_dict()
|
||||
|
||||
note = models.Note(course_id=self.course_id, user=self.student)
|
||||
note = models.Note(course_id=self.course_key, user=self.student)
|
||||
try:
|
||||
note.clean(json.dumps(body))
|
||||
self.assertEqual(note.uri, body['uri'])
|
||||
@@ -376,7 +377,7 @@ class NoteTest(TestCase):
|
||||
self.fail('a valid note should not raise an exception')
|
||||
|
||||
def test_clean_invalid_note(self):
|
||||
note = models.Note(course_id=self.course_id, user=self.student)
|
||||
note = models.Note(course_id=self.course_key, user=self.student)
|
||||
for empty_type in (None, '', 0, []):
|
||||
with self.assertRaises(ValidationError):
|
||||
note.clean(None)
|
||||
@@ -389,7 +390,7 @@ class NoteTest(TestCase):
|
||||
}))
|
||||
|
||||
def test_as_dict(self):
|
||||
note = models.Note(course_id=self.course_id, user=self.student)
|
||||
note = models.Note(course_id=self.course_key, user=self.student)
|
||||
d = note.as_dict()
|
||||
self.assertNotIsInstance(d, basestring)
|
||||
self.assertEqual(d['user_id'], self.student.id)
|
||||
|
||||
@@ -10,7 +10,7 @@ from notes.utils import notes_enabled_for_course
|
||||
def notes(request, course_id):
|
||||
''' Displays the student's notes. '''
|
||||
|
||||
course = get_course_with_access(request.user, course_id, 'load')
|
||||
course = get_course_with_access(request.user, 'load', course_id)
|
||||
if not notes_enabled_for_course(course):
|
||||
raise Http404
|
||||
|
||||
|
||||
@@ -143,7 +143,7 @@ def combined_notifications(course, user):
|
||||
#Initialize controller query service using our mock system
|
||||
controller_qs = ControllerQueryService(settings.OPEN_ENDED_GRADING_INTERFACE, system)
|
||||
student_id = unique_id_for_user(user)
|
||||
user_is_staff = has_access(user, course, 'staff')
|
||||
user_is_staff = has_access(user, 'staff', course)
|
||||
course_id = course.id
|
||||
notification_type = "combined"
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ from django.http import HttpResponse, Http404
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
from xmodule.open_ended_grading_classes.grading_service_module import GradingService, GradingServiceError
|
||||
from xmodule.modulestore.django import ModuleI18nService
|
||||
|
||||
@@ -17,7 +18,6 @@ from courseware.access import has_access
|
||||
from lms.lib.xblock.runtime import LmsModuleSystem
|
||||
from edxmako.shortcuts import render_to_string
|
||||
from student.models import unique_id_for_user
|
||||
from util.json_request import expect_json
|
||||
|
||||
from open_ended_grading.utils import does_location_exist
|
||||
from dogapi import dog_stats_api
|
||||
@@ -233,8 +233,7 @@ def _check_access(user, course_id):
|
||||
"""
|
||||
Raise 404 if user doesn't have staff access to course_id
|
||||
"""
|
||||
course_location = CourseDescriptor.id_to_location(course_id)
|
||||
if not has_access(user, course_location, 'staff'):
|
||||
if not has_access(user, 'staff', course_id):
|
||||
raise Http404
|
||||
|
||||
return
|
||||
@@ -261,7 +260,9 @@ def get_next(request, course_id):
|
||||
|
||||
'error': if success is False, will have an error message with more info.
|
||||
"""
|
||||
_check_access(request.user, course_id)
|
||||
assert(isinstance(course_id, basestring))
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
_check_access(request.user, course_key)
|
||||
|
||||
required = set(['location'])
|
||||
if request.method != 'POST':
|
||||
@@ -275,7 +276,7 @@ def get_next(request, course_id):
|
||||
p = request.POST
|
||||
location = p['location']
|
||||
|
||||
return HttpResponse(json.dumps(_get_next(course_id, grader_id, location)),
|
||||
return HttpResponse(json.dumps(_get_next(course_key, grader_id, location)),
|
||||
mimetype="application/json")
|
||||
|
||||
def get_problem_list(request, course_id):
|
||||
@@ -301,9 +302,11 @@ def get_problem_list(request, course_id):
|
||||
|
||||
'error': if success is False, will have an error message with more info.
|
||||
"""
|
||||
_check_access(request.user, course_id)
|
||||
assert(isinstance(course_id, basestring))
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
_check_access(request.user, course_key)
|
||||
try:
|
||||
response = staff_grading_service().get_problem_list(course_id, unique_id_for_user(request.user))
|
||||
response = staff_grading_service().get_problem_list(course_key, unique_id_for_user(request.user))
|
||||
|
||||
# If 'problem_list' is in the response, then we got a list of problems from the ORA server.
|
||||
# If it is not, then ORA could not find any problems.
|
||||
@@ -324,7 +327,7 @@ def get_problem_list(request, course_id):
|
||||
problem_list[i] = json.loads(problem_list[i])
|
||||
except Exception:
|
||||
pass
|
||||
if does_location_exist(course_id, problem_list[i]['location']):
|
||||
if does_location_exist(course_key.make_usage_key_from_deprecated_string(problem_list[i]['location'])):
|
||||
valid_problem_list.append(problem_list[i])
|
||||
response['problem_list'] = valid_problem_list
|
||||
response = json.dumps(response)
|
||||
@@ -372,7 +375,9 @@ def save_grade(request, course_id):
|
||||
Returns the same thing as get_next, except that additional error messages
|
||||
are possible if something goes wrong with saving the grade.
|
||||
"""
|
||||
_check_access(request.user, course_id)
|
||||
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
_check_access(request.user, course_key)
|
||||
|
||||
if request.method != 'POST':
|
||||
raise Http404
|
||||
@@ -398,7 +403,7 @@ def save_grade(request, course_id):
|
||||
location = p['location']
|
||||
|
||||
try:
|
||||
result = staff_grading_service().save_grade(course_id,
|
||||
result = staff_grading_service().save_grade(course_key,
|
||||
grader_id,
|
||||
p['submission_id'],
|
||||
p['score'],
|
||||
|
||||
@@ -18,6 +18,7 @@ from xblock.fields import ScopeIds
|
||||
from xmodule import peer_grading_module
|
||||
from xmodule.error_module import ErrorDescriptor
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.locations import SlashSeparatedCourseKey
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.open_ended_grading_classes import peer_grading_service, controller_query_service
|
||||
from xmodule.tests import test_util_open_ended
|
||||
@@ -52,7 +53,7 @@ def make_instructor(course, user_email):
|
||||
"""
|
||||
Makes a given user an instructor in a course.
|
||||
"""
|
||||
CourseStaffRole(course.location).add_users(User.objects.get(email=user_email))
|
||||
CourseStaffRole(course.id).add_users(User.objects.get(email=user_email))
|
||||
|
||||
|
||||
class StudentProblemListMockQuery(object):
|
||||
@@ -114,7 +115,7 @@ class TestStaffGradingService(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
self.activate_user(self.student)
|
||||
self.activate_user(self.instructor)
|
||||
|
||||
self.course_id = "edX/toy/2012_Fall"
|
||||
self.course_id = SlashSeparatedCourseKey("edX", "toy", "2012_Fall")
|
||||
self.toy = modulestore().get_course(self.course_id)
|
||||
|
||||
make_instructor(self.toy, self.instructor)
|
||||
@@ -131,14 +132,14 @@ class TestStaffGradingService(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
|
||||
# both get and post should return 404
|
||||
for view_name in ('staff_grading_get_next', 'staff_grading_save_grade'):
|
||||
url = reverse(view_name, kwargs={'course_id': self.course_id})
|
||||
url = reverse(view_name, kwargs={'course_id': self.course_id.to_deprecated_string()})
|
||||
check_for_get_code(self, 404, url)
|
||||
check_for_post_code(self, 404, url)
|
||||
|
||||
def test_get_next(self):
|
||||
self.login(self.instructor, self.password)
|
||||
|
||||
url = reverse('staff_grading_get_next', kwargs={'course_id': self.course_id})
|
||||
url = reverse('staff_grading_get_next', kwargs={'course_id': self.course_id.to_deprecated_string()})
|
||||
data = {'location': self.location}
|
||||
|
||||
response = check_for_post_code(self, 200, url, data)
|
||||
@@ -159,7 +160,7 @@ class TestStaffGradingService(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
def save_grade_base(self, skip=False):
|
||||
self.login(self.instructor, self.password)
|
||||
|
||||
url = reverse('staff_grading_save_grade', kwargs={'course_id': self.course_id})
|
||||
url = reverse('staff_grading_save_grade', kwargs={'course_id': self.course_id.to_deprecated_string()})
|
||||
|
||||
data = {'score': '12',
|
||||
'feedback': 'great!',
|
||||
@@ -184,7 +185,7 @@ class TestStaffGradingService(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
def test_get_problem_list(self):
|
||||
self.login(self.instructor, self.password)
|
||||
|
||||
url = reverse('staff_grading_get_problem_list', kwargs={'course_id': self.course_id})
|
||||
url = reverse('staff_grading_get_problem_list', kwargs={'course_id': self.course_id.to_deprecated_string()})
|
||||
data = {}
|
||||
|
||||
response = check_for_post_code(self, 200, url, data)
|
||||
@@ -207,7 +208,7 @@ class TestStaffGradingService(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
user=instructor,
|
||||
)
|
||||
# Get the response and load its content.
|
||||
response = json.loads(staff_grading_service.get_problem_list(request, self.course_id).content)
|
||||
response = json.loads(staff_grading_service.get_problem_list(request, self.course_id.to_deprecated_string()).content)
|
||||
|
||||
# A valid response will have an "error" key.
|
||||
self.assertTrue('error' in response)
|
||||
@@ -220,7 +221,7 @@ class TestStaffGradingService(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
"""
|
||||
self.login(self.instructor, self.password)
|
||||
|
||||
url = reverse('staff_grading_save_grade', kwargs={'course_id': self.course_id})
|
||||
url = reverse('staff_grading_save_grade', kwargs={'course_id': self.course_id.to_deprecated_string()})
|
||||
|
||||
data = {
|
||||
'score': '12',
|
||||
@@ -267,7 +268,7 @@ class TestPeerGradingService(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
self.activate_user(self.student)
|
||||
self.activate_user(self.instructor)
|
||||
|
||||
self.course_id = "edX/toy/2012_Fall"
|
||||
self.course_id = SlashSeparatedCourseKey("edX", "toy", "2012_Fall")
|
||||
self.toy = modulestore().get_course(self.course_id)
|
||||
location = "i4x://edX/toy/peergrading/init"
|
||||
field_data = DictFieldData({'data': "<peergrading/>", 'location': location, 'category':'peergrading'})
|
||||
@@ -444,8 +445,8 @@ class TestPanel(ModuleStoreTestCase):
|
||||
|
||||
def setUp(self):
|
||||
# Toy courses should be loaded
|
||||
self.course_name = 'edX/open_ended/2012_Fall'
|
||||
self.course = modulestore().get_course(self.course_name)
|
||||
self.course_key = SlashSeparatedCourseKey('edX', 'open_ended', '2012_Fall')
|
||||
self.course = modulestore().get_course(self.course_key)
|
||||
self.user = factories.UserFactory()
|
||||
|
||||
def test_open_ended_panel(self):
|
||||
@@ -471,7 +472,7 @@ class TestPanel(ModuleStoreTestCase):
|
||||
@return:
|
||||
"""
|
||||
request = Mock(user=self.user)
|
||||
response = views.student_problem_list(request, self.course.id)
|
||||
response = views.student_problem_list(request, self.course.id.to_deprecated_string())
|
||||
self.assertRegexpMatches(response.content, "Here is a list of open ended problems for this course.")
|
||||
|
||||
|
||||
@@ -482,8 +483,8 @@ class TestPeerGradingFound(ModuleStoreTestCase):
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.course_name = 'edX/open_ended_nopath/2012_Fall'
|
||||
self.course = modulestore().get_course(self.course_name)
|
||||
self.course_key = SlashSeparatedCourseKey('edX', 'open_ended_nopath', '2012_Fall')
|
||||
self.course = modulestore().get_course(self.course_key)
|
||||
|
||||
def test_peer_grading_nopath(self):
|
||||
"""
|
||||
@@ -503,8 +504,8 @@ class TestStudentProblemList(ModuleStoreTestCase):
|
||||
|
||||
def setUp(self):
|
||||
# Load an open ended course with several problems.
|
||||
self.course_name = 'edX/open_ended/2012_Fall'
|
||||
self.course = modulestore().get_course(self.course_name)
|
||||
self.course_key = SlashSeparatedCourseKey('edX', 'open_ended', '2012_Fall')
|
||||
self.course = modulestore().get_course(self.course_key)
|
||||
self.user = factories.UserFactory()
|
||||
# Enroll our user in our course and make them an instructor.
|
||||
make_instructor(self.course, self.user.email)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import json
|
||||
import logging
|
||||
|
||||
from xmodule.modulestore import search
|
||||
@@ -50,20 +49,25 @@ def generate_problem_url(problem_url_parts, base_course_url):
|
||||
problem_url = base_course_url + "/"
|
||||
for i, part in enumerate(problem_url_parts):
|
||||
if part is not None:
|
||||
# This is the course_key. We need to turn it into its deprecated
|
||||
# form.
|
||||
if i == 0:
|
||||
part = part.to_deprecated_string()
|
||||
# This is placed between the course id and the rest of the url.
|
||||
if i == 1:
|
||||
problem_url += "courseware/"
|
||||
problem_url += part + "/"
|
||||
return problem_url
|
||||
|
||||
|
||||
def does_location_exist(course_id, location):
|
||||
def does_location_exist(usage_key):
|
||||
"""
|
||||
Checks to see if a valid module exists at a given location (ie has not been deleted)
|
||||
course_id - string course id
|
||||
location - string location
|
||||
"""
|
||||
try:
|
||||
search.path_to_location(modulestore(), course_id, location)
|
||||
search.path_to_location(modulestore(), usage_key)
|
||||
return True
|
||||
except ItemNotFoundError:
|
||||
# If the problem cannot be found at the location received from the grading controller server,
|
||||
@@ -71,10 +75,9 @@ def does_location_exist(course_id, location):
|
||||
return False
|
||||
except NoPathToItem:
|
||||
# If the problem can be found, but there is no path to it, then we assume it is a draft.
|
||||
# Log a warning if the problem is not a draft (location does not end in "draft").
|
||||
if not location.endswith("draft"):
|
||||
log.warn(("Got an unexpected NoPathToItem error in staff grading with a non-draft location {0}. "
|
||||
"Ensure that the location is valid.").format(location))
|
||||
# Log a warning in any case.
|
||||
log.warn("Got an unexpected NoPathToItem error in staff grading with location %s. "
|
||||
"This is ok if it is a draft; ensure that the location is valid.", usage_key)
|
||||
return False
|
||||
|
||||
|
||||
@@ -156,7 +159,8 @@ class StudentProblemList(object):
|
||||
for problem in self.problem_list:
|
||||
try:
|
||||
# Try to load the problem.
|
||||
problem_url_parts = search.path_to_location(modulestore(), self.course_id, problem['location'])
|
||||
usage_key = self.course_id.make_usage_key_from_deprecated_string(problem['location'])
|
||||
problem_url_parts = search.path_to_location(modulestore(), usage_key)
|
||||
except (ItemNotFoundError, NoPathToItem):
|
||||
# If the problem cannot be found at the location received from the grading controller server,
|
||||
# it has been deleted by the course author. We should not display it.
|
||||
|
||||
@@ -16,7 +16,7 @@ import open_ended_notifications
|
||||
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore import search
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore import SlashSeparatedCourseKey
|
||||
from xmodule.modulestore.exceptions import NoPathToItem
|
||||
|
||||
from django.http import HttpResponse, Http404, HttpResponseRedirect
|
||||
@@ -28,7 +28,8 @@ from open_ended_grading.utils import (STAFF_ERROR_MESSAGE, STUDENT_ERROR_MESSAGE
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
def _reverse_with_slash(url_name, course_id):
|
||||
|
||||
def _reverse_with_slash(url_name, course_key):
|
||||
"""
|
||||
Reverses the URL given the name and the course id, and then adds a trailing slash if
|
||||
it does not exist yet.
|
||||
@@ -36,6 +37,7 @@ def _reverse_with_slash(url_name, course_id):
|
||||
@param course_id: The id of the course object (eg course.id).
|
||||
@returns: The reversed url with a trailing slash.
|
||||
"""
|
||||
course_id = course_key.to_deprecated_string()
|
||||
ajax_url = _reverse_without_slash(url_name, course_id)
|
||||
if not ajax_url.endswith('/'):
|
||||
ajax_url += '/'
|
||||
@@ -66,7 +68,7 @@ def staff_grading(request, course_id):
|
||||
"""
|
||||
Show the instructor grading interface.
|
||||
"""
|
||||
course = get_course_with_access(request.user, course_id, 'staff')
|
||||
course = get_course_with_access(request.user, 'staff', course_id)
|
||||
|
||||
ajax_url = _reverse_with_slash('staff_grading', course_id)
|
||||
|
||||
@@ -90,18 +92,15 @@ def find_peer_grading_module(course):
|
||||
found_module = False
|
||||
problem_url = ""
|
||||
|
||||
# Get the course id and split it.
|
||||
peer_grading_query = course.location.replace(category='peergrading', name=None)
|
||||
# Get the peer grading modules currently in the course. Explicitly specify the course id to avoid issues with different runs.
|
||||
items = modulestore().get_items(peer_grading_query, course_id=course.id)
|
||||
#See if any of the modules are centralized modules (ie display info from multiple problems)
|
||||
items = modulestore().get_items(course.id, category='peergrading')
|
||||
# See if any of the modules are centralized modules (ie display info from multiple problems)
|
||||
items = [i for i in items if not getattr(i, "use_for_single_location", True)]
|
||||
# Loop through all potential peer grading modules, and find the first one that has a path to it.
|
||||
for item in items:
|
||||
item_location = item.location
|
||||
# Generate a url for the first module and redirect the user to it.
|
||||
try:
|
||||
problem_url_parts = search.path_to_location(modulestore(), course.id, item_location)
|
||||
problem_url_parts = search.path_to_location(modulestore(), item.location)
|
||||
except NoPathToItem:
|
||||
# In the case of nopathtoitem, the peer grading module that was found is in an invalid state, and
|
||||
# can no longer be accessed. Log an informational message, but this will not impact normal behavior.
|
||||
@@ -121,7 +120,7 @@ def peer_grading(request, course_id):
|
||||
'''
|
||||
|
||||
#Get the current course
|
||||
course = get_course_with_access(request.user, course_id, 'load')
|
||||
course = get_course_with_access(request.user, 'load', course_id)
|
||||
|
||||
found_module, problem_url = find_peer_grading_module(course)
|
||||
if not found_module:
|
||||
@@ -144,16 +143,17 @@ def student_problem_list(request, course_id):
|
||||
@param course_id: The id of the course to get the problem list for.
|
||||
@return: Renders an HTML problem list table.
|
||||
"""
|
||||
|
||||
assert isinstance(course_id, basestring)
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
# Load the course. Don't catch any errors here, as we want them to be loud.
|
||||
course = get_course_with_access(request.user, course_id, 'load')
|
||||
course = get_course_with_access(request.user, 'load', course_key)
|
||||
|
||||
# The anonymous student id is needed for communication with ORA.
|
||||
student_id = unique_id_for_user(request.user)
|
||||
base_course_url = reverse('courses')
|
||||
error_text = ""
|
||||
|
||||
student_problem_list = StudentProblemList(course_id, student_id)
|
||||
student_problem_list = StudentProblemList(course_key, student_id)
|
||||
# Get the problem list from ORA.
|
||||
success = student_problem_list.fetch_from_grading_service()
|
||||
# If we fetched the problem list properly, add in additional problem data.
|
||||
@@ -165,11 +165,11 @@ def student_problem_list(request, course_id):
|
||||
valid_problems = []
|
||||
error_text = student_problem_list.error_text
|
||||
|
||||
ajax_url = _reverse_with_slash('open_ended_problems', course_id)
|
||||
ajax_url = _reverse_with_slash('open_ended_problems', course_key)
|
||||
|
||||
context = {
|
||||
'course': course,
|
||||
'course_id': course_id,
|
||||
'course_id': course_key.to_deprecated_string(),
|
||||
'ajax_url': ajax_url,
|
||||
'success': success,
|
||||
'problem_list': valid_problems,
|
||||
@@ -185,7 +185,8 @@ def flagged_problem_list(request, course_id):
|
||||
'''
|
||||
Show a student problem list
|
||||
'''
|
||||
course = get_course_with_access(request.user, course_id, 'staff')
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
course = get_course_with_access(request.user, 'staff', course_key)
|
||||
student_id = unique_id_for_user(request.user)
|
||||
|
||||
# call problem list service
|
||||
@@ -197,7 +198,7 @@ def flagged_problem_list(request, course_id):
|
||||
# Make a service that can query edX ORA.
|
||||
controller_qs = create_controller_query_service()
|
||||
try:
|
||||
problem_list_dict = controller_qs.get_flagged_problem_list(course_id)
|
||||
problem_list_dict = controller_qs.get_flagged_problem_list(course_key)
|
||||
success = problem_list_dict['success']
|
||||
if 'error' in problem_list_dict:
|
||||
error_text = problem_list_dict['error']
|
||||
@@ -219,7 +220,7 @@ def flagged_problem_list(request, course_id):
|
||||
log.error("Could not parse problem list from external grading service response.")
|
||||
success = False
|
||||
|
||||
ajax_url = _reverse_with_slash('open_ended_flagged_problems', course_id)
|
||||
ajax_url = _reverse_with_slash('open_ended_flagged_problems', course_key)
|
||||
context = {
|
||||
'course': course,
|
||||
'course_id': course_id,
|
||||
@@ -238,7 +239,8 @@ def combined_notifications(request, course_id):
|
||||
"""
|
||||
Gets combined notifications from the grading controller and displays them
|
||||
"""
|
||||
course = get_course_with_access(request.user, course_id, 'load')
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
course = get_course_with_access(request.user, 'load', course_key)
|
||||
user = request.user
|
||||
notifications = open_ended_notifications.combined_notifications(course, user)
|
||||
response = notifications['response']
|
||||
@@ -250,7 +252,7 @@ def combined_notifications(request, course_id):
|
||||
if tag in response:
|
||||
url_name = notification_tuples[response_num][1]
|
||||
human_name = notification_tuples[response_num][2]
|
||||
url = _reverse_without_slash(url_name, course_id)
|
||||
url = _reverse_without_slash(url_name, course_key)
|
||||
has_img = response[tag]
|
||||
|
||||
# check to make sure we have descriptions and alert messages
|
||||
@@ -282,7 +284,7 @@ def combined_notifications(request, course_id):
|
||||
else:
|
||||
notification_list.append(notification_item)
|
||||
|
||||
ajax_url = _reverse_with_slash('open_ended_notifications', course_id)
|
||||
ajax_url = _reverse_with_slash('open_ended_notifications', course_key)
|
||||
combined_dict = {
|
||||
'error_text': "",
|
||||
'notification_list': notification_list,
|
||||
@@ -300,6 +302,7 @@ def take_action_on_flags(request, course_id):
|
||||
Takes action on student flagged submissions.
|
||||
Currently, only support unflag and ban actions.
|
||||
"""
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
if request.method != 'POST':
|
||||
raise Http404
|
||||
|
||||
@@ -324,7 +327,7 @@ def take_action_on_flags(request, course_id):
|
||||
# Make a service that can query edX ORA.
|
||||
controller_qs = create_controller_query_service()
|
||||
try:
|
||||
response = controller_qs.take_action_on_flags(course_id, student_id, submission_id, action_type)
|
||||
response = controller_qs.take_action_on_flags(course_key, student_id, submission_id, action_type)
|
||||
return HttpResponse(json.dumps(response), mimetype="application/json")
|
||||
except GradingServiceError:
|
||||
log.exception(
|
||||
|
||||
@@ -7,7 +7,7 @@ import json
|
||||
from courseware.models import StudentModule
|
||||
from track.models import TrackingLog
|
||||
from psychometrics.models import PsychometricData
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.keys import UsageKey
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand
|
||||
@@ -32,9 +32,8 @@ class Command(BaseCommand):
|
||||
smset = StudentModule.objects.using(db).exclude(max_grade=None)
|
||||
|
||||
for sm in smset:
|
||||
url = sm.module_state_key
|
||||
location = Location(url)
|
||||
if not location.category == "problem":
|
||||
usage_key = sm.module_state_key
|
||||
if not usage_key.block_type == "problem":
|
||||
continue
|
||||
try:
|
||||
state = json.loads(sm.state)
|
||||
|
||||
@@ -127,7 +127,7 @@ def problems_with_psychometric_data(course_id):
|
||||
Return dict of {problems (location urls): count} for which psychometric data is available.
|
||||
Does this for a given course_id.
|
||||
'''
|
||||
pmdset = PsychometricData.objects.using(db).filter(studentmodule__course_id=course_id)
|
||||
pmdset = PsychometricData.objects.using(db).filter(studentmodule__course_id=course_id.to_deprecated_string())
|
||||
plist = [p['studentmodule__module_state_key'] for p in pmdset.values('studentmodule__module_state_key').distinct()]
|
||||
problems = dict((p, pmdset.filter(studentmodule__module_state_key=p).count()) for p in plist)
|
||||
|
||||
@@ -307,11 +307,11 @@ def make_psychometrics_data_update_handler(course_id, user, module_state_key):
|
||||
the PsychometricData instance for the given StudentModule instance.
|
||||
"""
|
||||
sm, status = StudentModule.objects.get_or_create(
|
||||
course_id=course_id,
|
||||
student=user,
|
||||
module_state_key=module_state_key,
|
||||
defaults={'state': '{}', 'module_type': 'problem'},
|
||||
)
|
||||
course_id=course_id.to_deprecated_string(),
|
||||
student=user,
|
||||
module_state_key=module_state_key,
|
||||
defaults={'state': '{}', 'module_type': 'problem'},
|
||||
)
|
||||
|
||||
try:
|
||||
pmd = PsychometricData.objects.using(db).get(studentmodule=sm)
|
||||
|
||||
@@ -29,6 +29,7 @@ from edxmako.shortcuts import render_to_string
|
||||
from student.views import course_from_id
|
||||
from student.models import CourseEnrollment, unenroll_done
|
||||
from util.query import use_read_replica_if_available
|
||||
from xmodule_django.models import CourseKeyField
|
||||
|
||||
from verify_student.models import SoftwareSecurePhotoVerification
|
||||
|
||||
@@ -310,7 +311,7 @@ class PaidCourseRegistration(OrderItem):
|
||||
"""
|
||||
This is an inventory item for paying for a course registration
|
||||
"""
|
||||
course_id = models.CharField(max_length=128, db_index=True)
|
||||
course_id = CourseKeyField(max_length=128, db_index=True)
|
||||
mode = models.SlugField(default=CourseMode.DEFAULT_MODE_SLUG)
|
||||
|
||||
@classmethod
|
||||
@@ -331,10 +332,9 @@ class PaidCourseRegistration(OrderItem):
|
||||
Returns the order item
|
||||
"""
|
||||
# First a bunch of sanity checks
|
||||
try:
|
||||
course = course_from_id(course_id) # actually fetch the course to make sure it exists, use this to
|
||||
course = course_from_id(course_id) # actually fetch the course to make sure it exists, use this to
|
||||
# throw errors if it doesn't
|
||||
except ItemNotFoundError:
|
||||
if not course:
|
||||
log.error("User {} tried to add non-existent course {} to cart id {}"
|
||||
.format(order.user.email, course_id, order.id))
|
||||
raise CourseDoesNotExistException
|
||||
@@ -344,7 +344,7 @@ class PaidCourseRegistration(OrderItem):
|
||||
.format(order.user.email, course_id, order.id))
|
||||
raise ItemAlreadyInCartException
|
||||
|
||||
if CourseEnrollment.is_enrolled(user=order.user, course_id=course_id):
|
||||
if CourseEnrollment.is_enrolled(user=order.user, course_key=course_id):
|
||||
log.warning("User {} trying to add course {} to cart id {}, already registered"
|
||||
.format(order.user.email, course_id, order.id))
|
||||
raise AlreadyEnrolledInCourseException
|
||||
@@ -385,18 +385,11 @@ class PaidCourseRegistration(OrderItem):
|
||||
in CourseEnrollmentAllowed will the user be allowed to enroll. Otherwise requiring payment
|
||||
would in fact be quite silly since there's a clear back door.
|
||||
"""
|
||||
try:
|
||||
course_loc = CourseDescriptor.id_to_location(self.course_id)
|
||||
course_exists = modulestore().has_item(self.course_id, course_loc)
|
||||
except ValueError:
|
||||
if not modulestore().has_course(self.course_id):
|
||||
raise PurchasedCallbackException(
|
||||
"The customer purchased Course {0}, but that course doesn't exist!".format(self.course_id))
|
||||
|
||||
if not course_exists:
|
||||
raise PurchasedCallbackException(
|
||||
"The customer purchased Course {0}, but that course doesn't exist!".format(self.course_id))
|
||||
|
||||
CourseEnrollment.enroll(user=self.user, course_id=self.course_id, mode=self.mode)
|
||||
CourseEnrollment.enroll(user=self.user, course_key=self.course_id, mode=self.mode)
|
||||
|
||||
log.info("Enrolled {0} in paid course {1}, paid ${2}"
|
||||
.format(self.user.email, self.course_id, self.line_cost)) # pylint: disable=E1101
|
||||
@@ -430,18 +423,19 @@ class PaidCourseRegistrationAnnotation(models.Model):
|
||||
And unfortunately we didn't have the concept of a "SKU" or stock item where we could keep this association,
|
||||
so this is to retrofit it.
|
||||
"""
|
||||
course_id = models.CharField(unique=True, max_length=128, db_index=True)
|
||||
course_id = CourseKeyField(unique=True, max_length=128, db_index=True)
|
||||
annotation = models.TextField(null=True)
|
||||
|
||||
def __unicode__(self):
|
||||
return u"{} : {}".format(self.course_id, self.annotation)
|
||||
# pylint: disable=no-member
|
||||
return u"{} : {}".format(self.course_id.to_deprecated_string(), self.annotation)
|
||||
|
||||
|
||||
class CertificateItem(OrderItem):
|
||||
"""
|
||||
This is an inventory item for purchasing certificates
|
||||
"""
|
||||
course_id = models.CharField(max_length=128, db_index=True)
|
||||
course_id = CourseKeyField(max_length=128, db_index=True)
|
||||
course_enrollment = models.ForeignKey(CourseEnrollment)
|
||||
mode = models.SlugField()
|
||||
|
||||
@@ -568,12 +562,17 @@ class CertificateItem(OrderItem):
|
||||
def single_item_receipt_context(self):
|
||||
course = course_from_id(self.course_id)
|
||||
return {
|
||||
"course_id" : self.course_id,
|
||||
"course_id": self.course_id,
|
||||
"course_name": course.display_name_with_default,
|
||||
"course_org": course.display_org_with_default,
|
||||
"course_num": course.display_number_with_default,
|
||||
"course_start_date_text": course.start_date_text,
|
||||
"course_has_started": course.start > datetime.today().replace(tzinfo=pytz.utc),
|
||||
"course_root_url": reverse(
|
||||
'course_root',
|
||||
kwargs={'course_id': self.course_id.to_deprecated_string()} # pylint: disable=no-member
|
||||
),
|
||||
"dashboard_url": reverse('dashboard'),
|
||||
}
|
||||
|
||||
@property
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user