Commit f47a5e1b by Calen Pennington

Merge pull request #5498 from edx/jeskew/split_fix_auto_auth

Optimize LMS views for Split courses.
parents 13a7365b eb33f0e0
......@@ -205,10 +205,11 @@ class CourseGradingModel(object):
@staticmethod
def jsonize_grader(i, grader):
grader['id'] = i
if grader['weight']:
grader['weight'] *= 100
if not 'short_label' in grader:
grader['short_label'] = ""
return grader
return {
"id": i,
"type": grader["type"],
"min_count": grader.get('min_count', 0),
"drop_count": grader.get('drop_count', 0),
"short_label": grader.get('short_label', ""),
"weight": grader.get('weight', 0) * 100,
}
......@@ -7,14 +7,25 @@ from django_comment_common.utils import seed_permissions_roles
from student.models import CourseEnrollment, UserProfile
from util.testing import UrlResetMixin
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from opaque_keys.edx.locator import CourseLocator
from mock import patch
import ddt
@ddt.ddt
class AutoAuthEnabledTestCase(UrlResetMixin, TestCase):
"""
Tests for the Auto auth view that we have for load testing.
"""
COURSE_ID_MONGO = 'edX/Test101/2014_Spring'
COURSE_ID_SPLIT = 'course-v1:edX+Test101+2014_Spring'
COURSE_IDS_DDT = (
(COURSE_ID_MONGO, SlashSeparatedCourseKey.from_deprecated_string(COURSE_ID_MONGO)),
(COURSE_ID_SPLIT, SlashSeparatedCourseKey.from_deprecated_string(COURSE_ID_SPLIT)),
(COURSE_ID_MONGO, CourseLocator.from_string(COURSE_ID_MONGO)),
(COURSE_ID_SPLIT, CourseLocator.from_string(COURSE_ID_SPLIT)),
)
@patch.dict("django.conf.settings.FEATURES", {"AUTOMATIC_AUTH_FOR_TESTING": True})
def setUp(self):
# Patching the settings.FEATURES['AUTOMATIC_AUTH_FOR_TESTING']
......@@ -24,8 +35,6 @@ class AutoAuthEnabledTestCase(UrlResetMixin, TestCase):
super(AutoAuthEnabledTestCase, self).setUp()
self.url = '/auto_auth'
self.client = Client()
self.course_id = 'edX/Test101/2014_Spring'
self.course_key = SlashSeparatedCourseKey.from_deprecated_string(self.course_id)
def test_create_user(self):
"""
......@@ -83,42 +92,48 @@ class AutoAuthEnabledTestCase(UrlResetMixin, TestCase):
user = User.objects.get(username='test')
self.assertFalse(user.is_staff)
def test_course_enrollment(self):
@ddt.data(*COURSE_IDS_DDT)
@ddt.unpack
def test_course_enrollment(self, course_id, course_key):
# Create a user and enroll in a course
self._auto_auth(username='test', course_id=self.course_id)
self._auto_auth(username='test', course_id=course_id)
# Check that a course enrollment was created for the user
self.assertEqual(CourseEnrollment.objects.count(), 1)
enrollment = CourseEnrollment.objects.get(course_id=self.course_key)
enrollment = CourseEnrollment.objects.get(course_id=course_key)
self.assertEqual(enrollment.user.username, "test")
def test_double_enrollment(self):
@ddt.data(*COURSE_IDS_DDT)
@ddt.unpack
def test_double_enrollment(self, course_id, course_key):
# Create a user and enroll in a course
self._auto_auth(username='test', course_id=self.course_id)
self._auto_auth(username='test', course_id=course_id)
# Make the same call again, re-enrolling the student in the same course
self._auto_auth(username='test', course_id=self.course_id)
self._auto_auth(username='test', course_id=course_id)
# Check that only one course enrollment was created for the user
self.assertEqual(CourseEnrollment.objects.count(), 1)
enrollment = CourseEnrollment.objects.get(course_id=self.course_key)
enrollment = CourseEnrollment.objects.get(course_id=course_key)
self.assertEqual(enrollment.user.username, "test")
def test_set_roles(self):
seed_permissions_roles(self.course_key)
course_roles = dict((r.name, r) for r in Role.objects.filter(course_id=self.course_key))
@ddt.data(*COURSE_IDS_DDT)
@ddt.unpack
def test_set_roles(self, course_id, course_key):
seed_permissions_roles(course_key)
course_roles = dict((r.name, r) for r in Role.objects.filter(course_id=course_key))
self.assertEqual(len(course_roles), 4) # sanity check
# Student role is assigned by default on course enrollment.
self._auto_auth(username='a_student', course_id=self.course_id)
self._auto_auth(username='a_student', course_id=course_id)
user = User.objects.get(username='a_student')
user_roles = user.roles.all()
self.assertEqual(len(user_roles), 1)
self.assertEqual(user_roles[0], course_roles[FORUM_ROLE_STUDENT])
self._auto_auth(username='a_moderator', course_id=self.course_id, roles='Moderator')
self._auto_auth(username='a_moderator', course_id=course_id, roles='Moderator')
user = User.objects.get(username='a_moderator')
user_roles = user.roles.all()
self.assertEqual(
......@@ -127,7 +142,7 @@ class AutoAuthEnabledTestCase(UrlResetMixin, TestCase):
course_roles[FORUM_ROLE_MODERATOR]]))
# check multiple roles work.
self._auto_auth(username='an_admin', course_id=self.course_id,
self._auto_auth(username='an_admin', course_id=course_id,
roles='{},{}'.format(FORUM_ROLE_MODERATOR, FORUM_ROLE_ADMINISTRATOR))
user = User.objects.get(username='an_admin')
user_roles = user.roles.all()
......
......@@ -57,6 +57,7 @@ from dark_lang.models import DarkLangConfig
from xmodule.modulestore.django import modulestore
from opaque_keys import InvalidKeyError
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from opaque_keys.edx.locator import CourseLocator
from xmodule.modulestore import ModuleStoreEnum
from collections import namedtuple
......@@ -245,23 +246,25 @@ def get_course_enrollment_pairs(user, course_org_filter, org_filter_out_set):
a student's dashboard.
"""
for enrollment in CourseEnrollment.enrollments_for_user(user):
course = modulestore().get_course(enrollment.course_id)
if course and not isinstance(course, ErrorDescriptor):
# if we are in a Microsite, then filter out anything that is not
# attributed (by ORG) to that Microsite
if course_org_filter and course_org_filter != course.location.org:
continue
# Conversely, if we are not in a Microsite, then let's filter out any enrollments
# with courses attributed (by ORG) to Microsites
elif course.location.org in org_filter_out_set:
continue
yield (course, enrollment)
else:
log.error("User {0} enrolled in {2} course {1}".format(
user.username, enrollment.course_id, "broken" if course else "non-existent"
))
store = modulestore()
with store.bulk_operations(enrollment.course_id):
course = store.get_course(enrollment.course_id)
if course and not isinstance(course, ErrorDescriptor):
# if we are in a Microsite, then filter out anything that is not
# attributed (by ORG) to that Microsite
if course_org_filter and course_org_filter != course.location.org:
continue
# Conversely, if we are not in a Microsite, then let's filter out any enrollments
# with courses attributed (by ORG) to Microsites
elif course.location.org in org_filter_out_set:
continue
yield (course, enrollment)
else:
log.error("User {0} enrolled in {2} course {1}".format(
user.username, enrollment.course_id, "broken" if course else "non-existent"
))
def _cert_info(user, course, cert_status):
......@@ -1677,7 +1680,7 @@ def auto_auth(request):
course_id = request.GET.get('course_id', None)
course_key = None
if course_id:
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
course_key = CourseLocator.from_string(course_id)
role_names = [v.strip() for v in request.GET.get('roles', '').split(',') if v.strip()]
# Get or create the user object
......
......@@ -646,14 +646,14 @@ class LoncapaProblem(object):
code = unescape(script.text, XMLESC)
all_code += code
# An asset named python_lib.zip can be imported by Python code.
extra_files = []
zip_lib = self.capa_system.get_python_lib_zip()
if zip_lib is not None:
extra_files.append(("python_lib.zip", zip_lib))
python_path.append("python_lib.zip")
if all_code:
# An asset named python_lib.zip can be imported by Python code.
zip_lib = self.capa_system.get_python_lib_zip()
if zip_lib is not None:
extra_files.append(("python_lib.zip", zip_lib))
python_path.append("python_lib.zip")
try:
safe_exec(
all_code,
......
......@@ -240,7 +240,7 @@ class CombinedOpenEndedFields(object):
help=_("The number of times the student can try to answer this problem."),
default=1,
scope=Scope.settings,
values={"min": 1 }
values={"min": 1}
)
accept_file_upload = Boolean(
display_name=_("Allow File Uploads"),
......
......@@ -76,6 +76,11 @@ class CachingDescriptorSystem(MakoDescriptorSystem, EditInfoRuntimeMixin):
@contract(usage_key="BlockUsageLocator | BlockKey", course_entry_override="CourseEnvelope | None")
def _load_item(self, usage_key, course_entry_override=None, **kwargs):
"""
Instantiate the xblock fetching it either from the cache or from the structure
:param course_entry_override: the course_info with the course_key to use (defaults to cached)
"""
# usage_key is either a UsageKey or just the block_key. if a usage_key,
if isinstance(usage_key, BlockUsageLocator):
......@@ -90,21 +95,25 @@ class CachingDescriptorSystem(MakoDescriptorSystem, EditInfoRuntimeMixin):
raise ItemNotFoundError
else:
block_key = BlockKey.from_usage_key(usage_key)
version_guid = self.course_entry.course_key.version_guid
else:
block_key = usage_key
course_info = course_entry_override or self.course_entry
course_key = course_info.course_key
version_guid = course_key.version_guid
if course_entry_override:
structure_id = course_entry_override.structure.get('_id')
else:
structure_id = self.course_entry.structure.get('_id')
# look in cache
cached_module = self.modulestore.get_cached_block(course_key, version_guid, block_key)
if cached_module:
return cached_module
json_data = self.get_module_data(block_key, course_key)
class_ = self.load_block_type(json_data.get('block_type'))
return self.xblock_from_json(class_, course_key, block_key, json_data, course_entry_override, **kwargs)
block = self.xblock_from_json(class_, course_key, block_key, json_data, course_entry_override, **kwargs)
self.modulestore.cache_block(course_key, version_guid, block_key, block)
return block
@contract(block_key=BlockKey, course_key=CourseLocator)
def get_module_data(self, block_key, course_key):
......
......@@ -117,6 +117,8 @@ class SplitBulkWriteRecord(BulkOpsRecord):
self.index = None
self.structures = {}
self.structures_in_db = set()
# dict(version_guid, dict(BlockKey, module))
self.modules = defaultdict(dict)
self.definitions = {}
self.definitions_in_db = set()
......@@ -309,6 +311,38 @@ class SplitBulkWriteMixin(BulkOperationsMixin):
else:
self.db_connection.insert_structure(structure)
def get_cached_block(self, course_key, version_guid, block_id):
"""
If there's an active bulk_operation, see if it's cached this module and just return it
Don't do any extra work to get the ones which are not cached. Make the caller do the work & cache them.
"""
bulk_write_record = self._get_bulk_ops_record(course_key)
if bulk_write_record.active:
return bulk_write_record.modules[version_guid].get(block_id, None)
else:
return None
def cache_block(self, course_key, version_guid, block_key, block):
"""
The counterpart to :method `get_cached_block` which caches a block.
Returns nothing.
"""
bulk_write_record = self._get_bulk_ops_record(course_key)
if bulk_write_record.active:
bulk_write_record.modules[version_guid][block_key] = block
def decache_block(self, course_key, version_guid, block_key):
"""
Write operations which don't write from blocks must remove the target blocks from the cache.
Returns nothing.
"""
bulk_write_record = self._get_bulk_ops_record(course_key)
if bulk_write_record.active:
try:
del bulk_write_record.modules[version_guid][block_key]
except KeyError:
pass
def get_definition(self, course_key, definition_guid):
"""
Retrieve a single definition by id, respecting the active bulk operation
......@@ -637,8 +671,9 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
@contract(course_entry=CourseEnvelope, block_keys="list(BlockKey)", depth="int | None")
def _load_items(self, course_entry, block_keys, depth=0, lazy=True, **kwargs):
'''
Load & cache the given blocks from the course. Prefetch down to the
given depth. Load the definitions into each block if lazy is False;
Load & cache the given blocks from the course. May return the blocks in any order.
Load the definitions into each block if lazy is False;
otherwise, use the lazy definition placeholder.
'''
runtime = self._get_cache(course_entry.structure['_id'])
......@@ -646,6 +681,7 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
runtime = self.create_runtime(course_entry, lazy)
self._add_cache(course_entry.structure['_id'], runtime)
self.cache_items(runtime, block_keys, course_entry.course_key, depth, lazy)
return [runtime.load_item(block_key, course_entry, **kwargs) for block_key in block_keys]
def _get_cache(self, course_version_guid):
......@@ -1364,6 +1400,7 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
# if the parent hadn't been previously changed in this bulk transaction, indicate that it's
# part of the bulk transaction
self.version_block(parent, user_id, new_structure['_id'])
self.decache_block(parent_usage_key.course_key, new_structure['_id'], block_id)
# db update
self.update_structure(parent_usage_key.course_key, new_structure)
......@@ -1957,6 +1994,7 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
parent_block['edit_info']['edited_by'] = user_id
parent_block['edit_info']['previous_version'] = parent_block['edit_info']['update_version']
parent_block['edit_info']['update_version'] = new_id
self.decache_block(usage_locator.course_key, new_id, parent_block_key)
self._remove_subtree(BlockKey.from_usage_key(usage_locator), new_blocks)
......
......@@ -1229,7 +1229,6 @@ class CombinedOpenEndedV1Descriptor():
return {'task_xml': parse_task('task'), 'prompt': parse('prompt'), 'rubric': parse('rubric')}
def definition_to_xml(self, resource_fs):
'''Return an xml element representing this definition.'''
elt = etree.Element('combinedopenended')
......
......@@ -66,7 +66,8 @@ def get_course_by_id(course_key, depth=0):
depth: The number of levels of children for the modulestore to cache. None means infinite depth
"""
course = modulestore().get_course(course_key, depth=depth)
with modulestore().bulk_operations(course_key):
course = modulestore().get_course(course_key, depth=depth)
if course:
return course
else:
......
......@@ -285,6 +285,11 @@ def index(request, course_id, chapter=None, section=None,
return redirect(reverse('dashboard'))
request.user = user # keep just one instance of User
with modulestore().bulk_operations(course_key):
return _index_bulk_op(request, user, course_key, chapter, section, position)
def _index_bulk_op(request, user, course_key, chapter, section, position):
course = get_course_with_access(user, 'load', course_key, depth=2)
staff_access = has_access(user, 'staff', course)
registered = registered_for_course(course, user)
......@@ -554,41 +559,42 @@ def course_info(request, course_id):
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
course = get_course_with_access(request.user, 'load', course_key)
staff_access = has_access(request.user, 'staff', course)
masq = setup_masquerade(request, staff_access) # allow staff to toggle masquerade on info page
reverifications = fetch_reverify_banner_info(request, course_key)
studio_url = get_studio_url(course, 'course_info')
with modulestore().bulk_operations(course_key):
course = get_course_with_access(request.user, 'load', course_key)
staff_access = has_access(request.user, 'staff', course)
masq = setup_masquerade(request, staff_access) # allow staff to toggle masquerade on info page
reverifications = fetch_reverify_banner_info(request, course_key)
studio_url = get_studio_url(course, 'course_info')
# link to where the student should go to enroll in the course:
# about page if there is not marketing site, SITE_NAME if there is
url_to_enroll = reverse(course_about, args=[course_id])
if settings.FEATURES.get('ENABLE_MKTG_SITE'):
url_to_enroll = marketing_link('COURSES')
# link to where the student should go to enroll in the course:
# about page if there is not marketing site, SITE_NAME if there is
url_to_enroll = reverse(course_about, args=[course_id])
if settings.FEATURES.get('ENABLE_MKTG_SITE'):
url_to_enroll = marketing_link('COURSES')
show_enroll_banner = request.user.is_authenticated() and not CourseEnrollment.is_enrolled(request.user, course.id)
show_enroll_banner = request.user.is_authenticated() and not CourseEnrollment.is_enrolled(request.user, course.id)
context = {
'request': request,
'course_id': course_key.to_deprecated_string(),
'cache': None,
'course': course,
'staff_access': staff_access,
'masquerade': masq,
'studio_url': studio_url,
'reverifications': reverifications,
'show_enroll_banner': show_enroll_banner,
'url_to_enroll': url_to_enroll,
}
context = {
'request': request,
'course_id': course_key.to_deprecated_string(),
'cache': None,
'course': course,
'staff_access': staff_access,
'masquerade': masq,
'studio_url': studio_url,
'reverifications': reverifications,
'show_enroll_banner': show_enroll_banner,
'url_to_enroll': url_to_enroll,
}
now = datetime.now(UTC())
effective_start = _adjust_start_date_for_beta_testers(request.user, course, course_key)
if staff_access and now < effective_start:
# Disable student view button if user is staff and
# course is not yet visible to students.
context['disable_student_access'] = True
now = datetime.now(UTC())
effective_start = _adjust_start_date_for_beta_testers(request.user, course, course_key)
if staff_access and now < effective_start:
# Disable student view button if user is staff and
# course is not yet visible to students.
context['disable_student_access'] = True
return render_to_response('courseware/info.html', context)
return render_to_response('courseware/info.html', context)
@ensure_csrf_cookie
......@@ -805,8 +811,9 @@ def progress(request, course_id, student_id=None):
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
with grades.manual_transaction():
return _progress(request, course_key, student_id)
with modulestore().bulk_operations(course_key):
with grades.manual_transaction():
return _progress(request, course_key, student_id)
def _progress(request, course_key, student_id):
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment