Merge pull request #14068 from edx/release
Merge release back into master
This commit is contained in:
@@ -7,6 +7,7 @@ to decide whether to check course creator role, and other such functions.
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.conf import settings
|
||||
from opaque_keys.edx.locator import LibraryLocator
|
||||
from ccx_keys.locator import CCXLocator, CCXBlockUsageLocator
|
||||
|
||||
from student.roles import GlobalStaff, CourseCreatorRole, CourseStaffRole, CourseInstructorRole, CourseRole, \
|
||||
CourseBetaTesterRole, OrgInstructorRole, OrgStaffRole, LibraryUserRole, OrgLibraryUserRole
|
||||
@@ -17,9 +18,18 @@ STUDIO_EDIT_ROLES = 8
|
||||
STUDIO_VIEW_USERS = 4
|
||||
STUDIO_EDIT_CONTENT = 2
|
||||
STUDIO_VIEW_CONTENT = 1
|
||||
STUDIO_NO_PERMISSIONS = 0
|
||||
# In addition to the above, one is always allowed to "demote" oneself to a lower role within a course, or remove oneself
|
||||
|
||||
|
||||
def is_ccx_course(course_key):
|
||||
"""
|
||||
Check whether the course locator maps to a CCX course; this is important
|
||||
because we don't allow access to CCX courses in Studio.
|
||||
"""
|
||||
return isinstance(course_key, CCXLocator) or isinstance(course_key, CCXBlockUsageLocator)
|
||||
|
||||
|
||||
def user_has_role(user, role):
|
||||
"""
|
||||
Check whether this user has access to this role (either direct or implied)
|
||||
@@ -60,6 +70,9 @@ def get_user_permissions(user, course_key, org=None):
|
||||
course_key = course_key.for_branch(None)
|
||||
else:
|
||||
assert course_key is None
|
||||
# No one has studio permissions for CCX courses
|
||||
if is_ccx_course(course_key):
|
||||
return STUDIO_NO_PERMISSIONS
|
||||
all_perms = STUDIO_EDIT_ROLES | STUDIO_VIEW_USERS | STUDIO_EDIT_CONTENT | STUDIO_VIEW_CONTENT
|
||||
# global staff, org instructors, and course instructors have all permissions:
|
||||
if GlobalStaff().has_user(user) or OrgInstructorRole(org=org).has_user(user):
|
||||
@@ -73,7 +86,7 @@ def get_user_permissions(user, course_key, org=None):
|
||||
if course_key and isinstance(course_key, LibraryLocator):
|
||||
if OrgLibraryUserRole(org=org).has_user(user) or user_has_role(user, LibraryUserRole(course_key)):
|
||||
return STUDIO_VIEW_USERS | STUDIO_VIEW_CONTENT
|
||||
return 0
|
||||
return STUDIO_NO_PERMISSIONS
|
||||
|
||||
|
||||
def has_studio_write_access(user, course_key):
|
||||
|
||||
@@ -9,8 +9,9 @@ from django.core.exceptions import PermissionDenied
|
||||
|
||||
from student.roles import CourseInstructorRole, CourseStaffRole, CourseCreatorRole
|
||||
from student.tests.factories import AdminFactory
|
||||
from student.auth import user_has_role, add_users, remove_users
|
||||
from student.auth import user_has_role, add_users, remove_users, has_studio_write_access, has_studio_read_access
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
from ccx_keys.locator import CCXLocator
|
||||
|
||||
|
||||
class CreatorGroupTest(TestCase):
|
||||
@@ -132,6 +133,45 @@ class CreatorGroupTest(TestCase):
|
||||
remove_users(self.admin, CourseCreatorRole(), self.user)
|
||||
|
||||
|
||||
class CCXCourseGroupTest(TestCase):
|
||||
"""
|
||||
Test that access to a CCX course in Studio is disallowed
|
||||
"""
|
||||
def setUp(self):
|
||||
"""
|
||||
Set up test variables
|
||||
"""
|
||||
super(CCXCourseGroupTest, self).setUp()
|
||||
self.global_admin = AdminFactory()
|
||||
self.staff = User.objects.create_user('teststaff', 'teststaff+courses@edx.org', 'foo')
|
||||
self.ccx_course_key = CCXLocator.from_string('ccx-v1:edX+DemoX+Demo_Course+ccx@1')
|
||||
add_users(self.global_admin, CourseStaffRole(self.ccx_course_key), self.staff)
|
||||
|
||||
def test_no_global_admin_write_access(self):
|
||||
"""
|
||||
Test that global admins have no write access
|
||||
"""
|
||||
self.assertFalse(has_studio_write_access(self.global_admin, self.ccx_course_key))
|
||||
|
||||
def test_no_staff_write_access(self):
|
||||
"""
|
||||
Test that course staff have no write access
|
||||
"""
|
||||
self.assertFalse(has_studio_write_access(self.staff, self.ccx_course_key))
|
||||
|
||||
def test_no_global_admin_read_access(self):
|
||||
"""
|
||||
Test that global admins have no read access
|
||||
"""
|
||||
self.assertFalse(has_studio_read_access(self.global_admin, self.ccx_course_key))
|
||||
|
||||
def test_no_staff_read_access(self):
|
||||
"""
|
||||
Test that course staff have no read access
|
||||
"""
|
||||
self.assertFalse(has_studio_read_access(self.staff, self.ccx_course_key))
|
||||
|
||||
|
||||
class CourseGroupTest(TestCase):
|
||||
"""
|
||||
Tests for instructor and staff groups for a particular course.
|
||||
|
||||
@@ -187,7 +187,7 @@ class LoncapaProblem(object):
|
||||
|
||||
# construct script processor context (eg for customresponse problems)
|
||||
if minimal_init:
|
||||
self.context = {'script_code': ""}
|
||||
self.context = {}
|
||||
else:
|
||||
self.context = self._extract_context(self.tree)
|
||||
|
||||
@@ -195,24 +195,24 @@ class LoncapaProblem(object):
|
||||
# transformations. This also creates the dict (self.responders) of Response
|
||||
# instances for each question in the problem. The dict has keys = xml subtree of
|
||||
# Response, values = Response instance
|
||||
self.problem_data = self._preprocess_problem(self.tree)
|
||||
|
||||
if not self.student_answers: # True when student_answers is an empty dict
|
||||
self.set_initial_display()
|
||||
|
||||
# dictionary of InputType objects associated with this problem
|
||||
# input_id string -> InputType object
|
||||
self.inputs = {}
|
||||
|
||||
# Run response late_transforms last (see MultipleChoiceResponse)
|
||||
# Sort the responses to be in *_1 *_2 ... order.
|
||||
responses = self.responders.values()
|
||||
responses = sorted(responses, key=lambda resp: int(resp.id[resp.id.rindex('_') + 1:]))
|
||||
for response in responses:
|
||||
if hasattr(response, 'late_transforms'):
|
||||
response.late_transforms(self)
|
||||
self.problem_data = self._preprocess_problem(self.tree, minimal_init)
|
||||
|
||||
if not minimal_init:
|
||||
if not self.student_answers: # True when student_answers is an empty dict
|
||||
self.set_initial_display()
|
||||
|
||||
# dictionary of InputType objects associated with this problem
|
||||
# input_id string -> InputType object
|
||||
self.inputs = {}
|
||||
|
||||
# Run response late_transforms last (see MultipleChoiceResponse)
|
||||
# Sort the responses to be in *_1 *_2 ... order.
|
||||
responses = self.responders.values()
|
||||
responses = sorted(responses, key=lambda resp: int(resp.id[resp.id.rindex('_') + 1:]))
|
||||
for response in responses:
|
||||
if hasattr(response, 'late_transforms'):
|
||||
response.late_transforms(self)
|
||||
|
||||
self.extracted_tree = self._extract_html(self.tree)
|
||||
|
||||
def make_xml_compatible(self, tree):
|
||||
@@ -869,7 +869,7 @@ class LoncapaProblem(object):
|
||||
|
||||
return tree
|
||||
|
||||
def _preprocess_problem(self, tree): # private
|
||||
def _preprocess_problem(self, tree, minimal_init): # private
|
||||
"""
|
||||
Assign IDs to all the responses
|
||||
Assign sub-IDs to all entries (textline, schematic, etc.)
|
||||
@@ -907,28 +907,31 @@ class LoncapaProblem(object):
|
||||
|
||||
# instantiate capa Response
|
||||
responsetype_cls = responsetypes.registry.get_class_for_tag(response.tag)
|
||||
responder = responsetype_cls(response, inputfields, self.context, self.capa_system, self.capa_module)
|
||||
responder = responsetype_cls(
|
||||
response, inputfields, self.context, self.capa_system, self.capa_module, minimal_init
|
||||
)
|
||||
# save in list in self
|
||||
self.responders[response] = responder
|
||||
|
||||
# get responder answers (do this only once, since there may be a performance cost,
|
||||
# eg with externalresponse)
|
||||
self.responder_answers = {}
|
||||
for response in self.responders.keys():
|
||||
try:
|
||||
self.responder_answers[response] = self.responders[response].get_answers()
|
||||
except:
|
||||
log.debug('responder %s failed to properly return get_answers()',
|
||||
self.responders[response]) # FIXME
|
||||
raise
|
||||
if not minimal_init:
|
||||
# get responder answers (do this only once, since there may be a performance cost,
|
||||
# eg with externalresponse)
|
||||
self.responder_answers = {}
|
||||
for response in self.responders.keys():
|
||||
try:
|
||||
self.responder_answers[response] = self.responders[response].get_answers()
|
||||
except:
|
||||
log.debug('responder %s failed to properly return get_answers()',
|
||||
self.responders[response]) # FIXME
|
||||
raise
|
||||
|
||||
# <solution>...</solution> may not be associated with any specific response; give
|
||||
# IDs for those separately
|
||||
# TODO: We should make the namespaces consistent and unique (e.g. %s_problem_%i).
|
||||
solution_id = 1
|
||||
for solution in tree.findall('.//solution'):
|
||||
solution.attrib['id'] = "%s_solution_%i" % (self.problem_id, solution_id)
|
||||
solution_id += 1
|
||||
# <solution>...</solution> may not be associated with any specific response; give
|
||||
# IDs for those separately
|
||||
# TODO: We should make the namespaces consistent and unique (e.g. %s_problem_%i).
|
||||
solution_id = 1
|
||||
for solution in tree.findall('.//solution'):
|
||||
solution.attrib['id'] = "%s_solution_%i" % (self.problem_id, solution_id)
|
||||
solution_id += 1
|
||||
|
||||
return problem_data
|
||||
|
||||
|
||||
@@ -154,7 +154,7 @@ class LoncapaResponse(object):
|
||||
# By default, we set this to False, allowing subclasses to override as appropriate.
|
||||
multi_device_support = False
|
||||
|
||||
def __init__(self, xml, inputfields, context, system, capa_module):
|
||||
def __init__(self, xml, inputfields, context, system, capa_module, minimal_init):
|
||||
"""
|
||||
Init is passed the following arguments:
|
||||
|
||||
@@ -213,28 +213,29 @@ class LoncapaResponse(object):
|
||||
maxpoints = inputfield.get('points', '1')
|
||||
self.maxpoints.update({inputfield.get('id'): int(maxpoints)})
|
||||
|
||||
# dict for default answer map (provided in input elements)
|
||||
self.default_answer_map = {}
|
||||
for entry in self.inputfields:
|
||||
answer = entry.get('correct_answer')
|
||||
if answer:
|
||||
self.default_answer_map[entry.get(
|
||||
'id')] = contextualize_text(answer, self.context)
|
||||
if not minimal_init:
|
||||
# dict for default answer map (provided in input elements)
|
||||
self.default_answer_map = {}
|
||||
for entry in self.inputfields:
|
||||
answer = entry.get('correct_answer')
|
||||
if answer:
|
||||
self.default_answer_map[entry.get(
|
||||
'id')] = contextualize_text(answer, self.context)
|
||||
|
||||
# Does this problem have partial credit?
|
||||
# If so, what kind? Get it as a list of strings.
|
||||
partial_credit = xml.xpath('.')[0].get('partial_credit', default=False)
|
||||
# Does this problem have partial credit?
|
||||
# If so, what kind? Get it as a list of strings.
|
||||
partial_credit = xml.xpath('.')[0].get('partial_credit', default=False)
|
||||
|
||||
if str(partial_credit).lower().strip() == 'false':
|
||||
self.has_partial_credit = False
|
||||
self.credit_type = []
|
||||
else:
|
||||
self.has_partial_credit = True
|
||||
self.credit_type = partial_credit.split(',')
|
||||
self.credit_type = [word.strip().lower() for word in self.credit_type]
|
||||
if str(partial_credit).lower().strip() == 'false':
|
||||
self.has_partial_credit = False
|
||||
self.credit_type = []
|
||||
else:
|
||||
self.has_partial_credit = True
|
||||
self.credit_type = partial_credit.split(',')
|
||||
self.credit_type = [word.strip().lower() for word in self.credit_type]
|
||||
|
||||
if hasattr(self, 'setup_response'):
|
||||
self.setup_response()
|
||||
if hasattr(self, 'setup_response'):
|
||||
self.setup_response()
|
||||
|
||||
def get_max_score(self):
|
||||
"""
|
||||
|
||||
@@ -5,6 +5,9 @@ For more, see http://celery.readthedocs.io/en/latest/userguide/routing.html#rout
|
||||
"""
|
||||
from abc import ABCMeta, abstractproperty
|
||||
from django.conf import settings
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AlternateEnvironmentRouter(object):
|
||||
@@ -30,6 +33,13 @@ class AlternateEnvironmentRouter(object):
|
||||
If None is returned from this method, default routing logic is used.
|
||||
"""
|
||||
alternate_env = self.alternate_env_tasks.get(task, None)
|
||||
if 'update_course_in_cache' in task:
|
||||
log.info("TNL-5408: task={task}, args={args}, alternate_env={alt_env}, queues={queues}".format(
|
||||
task=task,
|
||||
args=args,
|
||||
alt_env=alternate_env,
|
||||
queues=getattr(settings, 'CELERY_QUEUES', []).keys()
|
||||
))
|
||||
if alternate_env:
|
||||
return self.ensure_queue_env(alternate_env)
|
||||
return None
|
||||
|
||||
Reference in New Issue
Block a user