Commit 205ded99 by Toby Lawrence Committed by GitHub

Merge pull request #14068 from edx/release

Merge release back into master
parents 989d01b5 68714c58
...@@ -7,6 +7,7 @@ to decide whether to check course creator role, and other such functions. ...@@ -7,6 +7,7 @@ to decide whether to check course creator role, and other such functions.
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.conf import settings from django.conf import settings
from opaque_keys.edx.locator import LibraryLocator from opaque_keys.edx.locator import LibraryLocator
from ccx_keys.locator import CCXLocator, CCXBlockUsageLocator
from student.roles import GlobalStaff, CourseCreatorRole, CourseStaffRole, CourseInstructorRole, CourseRole, \ from student.roles import GlobalStaff, CourseCreatorRole, CourseStaffRole, CourseInstructorRole, CourseRole, \
CourseBetaTesterRole, OrgInstructorRole, OrgStaffRole, LibraryUserRole, OrgLibraryUserRole CourseBetaTesterRole, OrgInstructorRole, OrgStaffRole, LibraryUserRole, OrgLibraryUserRole
...@@ -17,9 +18,18 @@ STUDIO_EDIT_ROLES = 8 ...@@ -17,9 +18,18 @@ STUDIO_EDIT_ROLES = 8
STUDIO_VIEW_USERS = 4 STUDIO_VIEW_USERS = 4
STUDIO_EDIT_CONTENT = 2 STUDIO_EDIT_CONTENT = 2
STUDIO_VIEW_CONTENT = 1 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 # 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): def user_has_role(user, role):
""" """
Check whether this user has access to this role (either direct or implied) 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): ...@@ -60,6 +70,9 @@ def get_user_permissions(user, course_key, org=None):
course_key = course_key.for_branch(None) course_key = course_key.for_branch(None)
else: else:
assert course_key is None 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 all_perms = STUDIO_EDIT_ROLES | STUDIO_VIEW_USERS | STUDIO_EDIT_CONTENT | STUDIO_VIEW_CONTENT
# global staff, org instructors, and course instructors have all permissions: # global staff, org instructors, and course instructors have all permissions:
if GlobalStaff().has_user(user) or OrgInstructorRole(org=org).has_user(user): 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): ...@@ -73,7 +86,7 @@ def get_user_permissions(user, course_key, org=None):
if course_key and isinstance(course_key, LibraryLocator): if course_key and isinstance(course_key, LibraryLocator):
if OrgLibraryUserRole(org=org).has_user(user) or user_has_role(user, LibraryUserRole(course_key)): if OrgLibraryUserRole(org=org).has_user(user) or user_has_role(user, LibraryUserRole(course_key)):
return STUDIO_VIEW_USERS | STUDIO_VIEW_CONTENT return STUDIO_VIEW_USERS | STUDIO_VIEW_CONTENT
return 0 return STUDIO_NO_PERMISSIONS
def has_studio_write_access(user, course_key): def has_studio_write_access(user, course_key):
......
...@@ -9,8 +9,9 @@ from django.core.exceptions import PermissionDenied ...@@ -9,8 +9,9 @@ from django.core.exceptions import PermissionDenied
from student.roles import CourseInstructorRole, CourseStaffRole, CourseCreatorRole from student.roles import CourseInstructorRole, CourseStaffRole, CourseCreatorRole
from student.tests.factories import AdminFactory 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 opaque_keys.edx.locations import SlashSeparatedCourseKey
from ccx_keys.locator import CCXLocator
class CreatorGroupTest(TestCase): class CreatorGroupTest(TestCase):
...@@ -132,6 +133,45 @@ class CreatorGroupTest(TestCase): ...@@ -132,6 +133,45 @@ class CreatorGroupTest(TestCase):
remove_users(self.admin, CourseCreatorRole(), self.user) 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): class CourseGroupTest(TestCase):
""" """
Tests for instructor and staff groups for a particular course. Tests for instructor and staff groups for a particular course.
......
...@@ -187,7 +187,7 @@ class LoncapaProblem(object): ...@@ -187,7 +187,7 @@ class LoncapaProblem(object):
# construct script processor context (eg for customresponse problems) # construct script processor context (eg for customresponse problems)
if minimal_init: if minimal_init:
self.context = {'script_code': ""} self.context = {}
else: else:
self.context = self._extract_context(self.tree) self.context = self._extract_context(self.tree)
...@@ -195,24 +195,24 @@ class LoncapaProblem(object): ...@@ -195,24 +195,24 @@ class LoncapaProblem(object):
# transformations. This also creates the dict (self.responders) of Response # transformations. This also creates the dict (self.responders) of Response
# instances for each question in the problem. The dict has keys = xml subtree of # instances for each question in the problem. The dict has keys = xml subtree of
# Response, values = Response instance # Response, values = Response instance
self.problem_data = self._preprocess_problem(self.tree) self.problem_data = self._preprocess_problem(self.tree, minimal_init)
if not self.student_answers: # True when student_answers is an empty dict if not minimal_init:
self.set_initial_display() 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 # dictionary of InputType objects associated with this problem
# input_id string -> InputType object # input_id string -> InputType object
self.inputs = {} self.inputs = {}
# Run response late_transforms last (see MultipleChoiceResponse) # Run response late_transforms last (see MultipleChoiceResponse)
# Sort the responses to be in *_1 *_2 ... order. # Sort the responses to be in *_1 *_2 ... order.
responses = self.responders.values() responses = self.responders.values()
responses = sorted(responses, key=lambda resp: int(resp.id[resp.id.rindex('_') + 1:])) responses = sorted(responses, key=lambda resp: int(resp.id[resp.id.rindex('_') + 1:]))
for response in responses: for response in responses:
if hasattr(response, 'late_transforms'): if hasattr(response, 'late_transforms'):
response.late_transforms(self) response.late_transforms(self)
if not minimal_init:
self.extracted_tree = self._extract_html(self.tree) self.extracted_tree = self._extract_html(self.tree)
def make_xml_compatible(self, tree): def make_xml_compatible(self, tree):
...@@ -869,7 +869,7 @@ class LoncapaProblem(object): ...@@ -869,7 +869,7 @@ class LoncapaProblem(object):
return tree return tree
def _preprocess_problem(self, tree): # private def _preprocess_problem(self, tree, minimal_init): # private
""" """
Assign IDs to all the responses Assign IDs to all the responses
Assign sub-IDs to all entries (textline, schematic, etc.) Assign sub-IDs to all entries (textline, schematic, etc.)
...@@ -907,28 +907,31 @@ class LoncapaProblem(object): ...@@ -907,28 +907,31 @@ class LoncapaProblem(object):
# instantiate capa Response # instantiate capa Response
responsetype_cls = responsetypes.registry.get_class_for_tag(response.tag) 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 # save in list in self
self.responders[response] = responder self.responders[response] = responder
# get responder answers (do this only once, since there may be a performance cost, if not minimal_init:
# eg with externalresponse) # get responder answers (do this only once, since there may be a performance cost,
self.responder_answers = {} # eg with externalresponse)
for response in self.responders.keys(): self.responder_answers = {}
try: for response in self.responders.keys():
self.responder_answers[response] = self.responders[response].get_answers() try:
except: self.responder_answers[response] = self.responders[response].get_answers()
log.debug('responder %s failed to properly return get_answers()', except:
self.responders[response]) # FIXME log.debug('responder %s failed to properly return get_answers()',
raise self.responders[response]) # FIXME
raise
# <solution>...</solution> may not be associated with any specific response; give
# IDs for those separately # <solution>...</solution> may not be associated with any specific response; give
# TODO: We should make the namespaces consistent and unique (e.g. %s_problem_%i). # IDs for those separately
solution_id = 1 # TODO: We should make the namespaces consistent and unique (e.g. %s_problem_%i).
for solution in tree.findall('.//solution'): solution_id = 1
solution.attrib['id'] = "%s_solution_%i" % (self.problem_id, solution_id) for solution in tree.findall('.//solution'):
solution_id += 1 solution.attrib['id'] = "%s_solution_%i" % (self.problem_id, solution_id)
solution_id += 1
return problem_data return problem_data
......
...@@ -154,7 +154,7 @@ class LoncapaResponse(object): ...@@ -154,7 +154,7 @@ class LoncapaResponse(object):
# By default, we set this to False, allowing subclasses to override as appropriate. # By default, we set this to False, allowing subclasses to override as appropriate.
multi_device_support = False 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: Init is passed the following arguments:
...@@ -213,28 +213,29 @@ class LoncapaResponse(object): ...@@ -213,28 +213,29 @@ class LoncapaResponse(object):
maxpoints = inputfield.get('points', '1') maxpoints = inputfield.get('points', '1')
self.maxpoints.update({inputfield.get('id'): int(maxpoints)}) self.maxpoints.update({inputfield.get('id'): int(maxpoints)})
# dict for default answer map (provided in input elements) if not minimal_init:
self.default_answer_map = {} # dict for default answer map (provided in input elements)
for entry in self.inputfields: self.default_answer_map = {}
answer = entry.get('correct_answer') for entry in self.inputfields:
if answer: answer = entry.get('correct_answer')
self.default_answer_map[entry.get( if answer:
'id')] = contextualize_text(answer, self.context) 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. # Does this problem have partial credit?
partial_credit = xml.xpath('.')[0].get('partial_credit', default=False) # 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 if str(partial_credit).lower().strip() == 'false':
self.credit_type = [] self.has_partial_credit = False
else: self.credit_type = []
self.has_partial_credit = True else:
self.credit_type = partial_credit.split(',') self.has_partial_credit = True
self.credit_type = [word.strip().lower() for word in self.credit_type] self.credit_type = partial_credit.split(',')
self.credit_type = [word.strip().lower() for word in self.credit_type]
if hasattr(self, 'setup_response'): if hasattr(self, 'setup_response'):
self.setup_response() self.setup_response()
def get_max_score(self): def get_max_score(self):
""" """
......
...@@ -5,6 +5,9 @@ For more, see http://celery.readthedocs.io/en/latest/userguide/routing.html#rout ...@@ -5,6 +5,9 @@ For more, see http://celery.readthedocs.io/en/latest/userguide/routing.html#rout
""" """
from abc import ABCMeta, abstractproperty from abc import ABCMeta, abstractproperty
from django.conf import settings from django.conf import settings
import logging
log = logging.getLogger(__name__)
class AlternateEnvironmentRouter(object): class AlternateEnvironmentRouter(object):
...@@ -30,6 +33,13 @@ class AlternateEnvironmentRouter(object): ...@@ -30,6 +33,13 @@ class AlternateEnvironmentRouter(object):
If None is returned from this method, default routing logic is used. If None is returned from this method, default routing logic is used.
""" """
alternate_env = self.alternate_env_tasks.get(task, None) 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: if alternate_env:
return self.ensure_queue_env(alternate_env) return self.ensure_queue_env(alternate_env)
return None return None
......
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