Commit 003ad76c by Don Mitchell

Merge pull request #2209 from edx/db/lms_date_agnostic

Detached categories don't access control on date
parents 7f742978 d999d0af
...@@ -40,9 +40,6 @@ __all__ = ['orphan_handler', 'xblock_handler'] ...@@ -40,9 +40,6 @@ __all__ = ['orphan_handler', 'xblock_handler']
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
# cdodge: these are categories which should not be parented, they are detached from the hierarchy
DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info']
CREATE_IF_NOT_FOUND = ['course_info'] CREATE_IF_NOT_FOUND = ['course_info']
...@@ -297,10 +294,11 @@ def _create_item(request): ...@@ -297,10 +294,11 @@ def _create_item(request):
dest_location, dest_location,
definition_data=data, definition_data=data,
metadata=metadata, metadata=metadata,
system=parent.system, system=parent.runtime,
) )
if category not in DETACHED_CATEGORIES: # TODO replace w/ nicer accessor
if not 'detached' in parent.runtime.load_block_type(category)._class_tags:
get_modulestore(parent.location).update_children(parent_location, parent.children + [dest_location.url()]) get_modulestore(parent.location).update_children(parent_location, parent.children + [dest_location.url()])
course_location = loc_mapper().translate_locator_to_location(parent_locator, get_course=True) course_location = loc_mapper().translate_locator_to_location(parent_locator, get_course=True)
...@@ -329,7 +327,7 @@ def _duplicate_item(parent_location, duplicate_source_location, display_name=Non ...@@ -329,7 +327,7 @@ def _duplicate_item(parent_location, duplicate_source_location, display_name=Non
dest_location, dest_location,
definition_data=source_item.data if hasattr(source_item, 'data') else None, definition_data=source_item.data if hasattr(source_item, 'data') else None,
metadata=duplicate_metadata, metadata=duplicate_metadata,
system=source_item.system if hasattr(source_item, 'system') else None, system=source_item.runtime,
) )
# Children are not automatically copied over (and not all xblocks have a 'children' attribute). # Children are not automatically copied over (and not all xblocks have a 'children' attribute).
...@@ -340,7 +338,7 @@ def _duplicate_item(parent_location, duplicate_source_location, display_name=Non ...@@ -340,7 +338,7 @@ def _duplicate_item(parent_location, duplicate_source_location, display_name=Non
copied_children.append(_duplicate_item(dest_location, Location(child)).url()) copied_children.append(_duplicate_item(dest_location, Location(child)).url())
get_modulestore(dest_location).update_children(dest_location, copied_children) get_modulestore(dest_location).update_children(dest_location, copied_children)
if category not in DETACHED_CATEGORIES: if not 'detached' in source_item.runtime.load_block_type(category)._class_tags:
parent = get_modulestore(parent_location).get_item(parent_location) parent = get_modulestore(parent_location).get_item(parent_location)
# If source was already a child of the parent, add duplicate immediately afterward. # If source was already a child of the parent, add duplicate immediately afterward.
# Otherwise, add child to end. # Otherwise, add child to end.
...@@ -404,12 +402,12 @@ def orphan_handler(request, tag=None, package_id=None, branch=None, version_guid ...@@ -404,12 +402,12 @@ def orphan_handler(request, tag=None, package_id=None, branch=None, version_guid
old_location = loc_mapper().translate_locator_to_location(location) old_location = loc_mapper().translate_locator_to_location(location)
if request.method == 'GET': if request.method == 'GET':
if has_course_access(request.user, old_location): if has_course_access(request.user, old_location):
return JsonResponse(modulestore().get_orphans(old_location, DETACHED_CATEGORIES, 'draft')) return JsonResponse(modulestore().get_orphans(old_location, 'draft'))
else: else:
raise PermissionDenied() raise PermissionDenied()
if request.method == 'DELETE': if request.method == 'DELETE':
if request.user.is_staff: if request.user.is_staff:
items = modulestore().get_orphans(old_location, DETACHED_CATEGORIES, 'draft') items = modulestore().get_orphans(old_location, 'draft')
for item in items: for item in items:
modulestore('draft').delete_item(item, True) modulestore('draft').delete_item(item, True)
return JsonResponse({'deleted': items}) return JsonResponse({'deleted': items})
......
...@@ -3,14 +3,11 @@ from fs.errors import ResourceNotFoundError ...@@ -3,14 +3,11 @@ from fs.errors import ResourceNotFoundError
import logging import logging
import os import os
import sys import sys
from datetime import datetime
from lxml import etree from lxml import etree
from path import path from path import path
from pytz import UTC
from pkg_resources import resource_string from pkg_resources import resource_string
from xblock.fields import Scope, String, Boolean from xblock.fields import Scope, String, Boolean
from xmodule.fields import Date
from xmodule.editing_module import EditingDescriptor from xmodule.editing_module import EditingDescriptor
from xmodule.html_checker import check_html from xmodule.html_checker import check_html
from xmodule.stringify import stringify_children from xmodule.stringify import stringify_children
...@@ -18,6 +15,7 @@ from xmodule.x_module import XModule ...@@ -18,6 +15,7 @@ from xmodule.x_module import XModule
from xmodule.xml_module import XmlDescriptor, name_to_pathname from xmodule.xml_module import XmlDescriptor, name_to_pathname
import textwrap import textwrap
from xmodule.contentstore.content import StaticContent from xmodule.contentstore.content import StaticContent
from xblock.core import XBlock
log = logging.getLogger("edx.courseware") log = logging.getLogger("edx.courseware")
...@@ -230,14 +228,9 @@ class AboutFields(object): ...@@ -230,14 +228,9 @@ class AboutFields(object):
default="", default="",
scope=Scope.content scope=Scope.content
) )
# this exists purely to override the default start date
start = Date(
help="placeholder to make sure that About is always active",
default=datetime.fromtimestamp(0, UTC),
scope=Scope.settings,
)
@XBlock.tag("detached")
class AboutModule(AboutFields, HtmlModule): class AboutModule(AboutFields, HtmlModule):
""" """
Overriding defaults but otherwise treated as HtmlModule. Overriding defaults but otherwise treated as HtmlModule.
...@@ -245,6 +238,7 @@ class AboutModule(AboutFields, HtmlModule): ...@@ -245,6 +238,7 @@ class AboutModule(AboutFields, HtmlModule):
pass pass
@XBlock.tag("detached")
class AboutDescriptor(AboutFields, HtmlDescriptor): class AboutDescriptor(AboutFields, HtmlDescriptor):
""" """
These pieces of course content are treated as HtmlModules but we need to overload where the templates are located These pieces of course content are treated as HtmlModules but we need to overload where the templates are located
...@@ -271,14 +265,9 @@ class StaticTabFields(object): ...@@ -271,14 +265,9 @@ class StaticTabFields(object):
scope=Scope.content, scope=Scope.content,
help="HTML for the additional pages" help="HTML for the additional pages"
) )
# this exists purely to override the default start date
start = Date(
help="placeholder to make sure that Static Tabs are always active",
default=datetime.fromtimestamp(0, UTC),
scope=Scope.settings,
)
@XBlock.tag("detached")
class StaticTabModule(StaticTabFields, HtmlModule): class StaticTabModule(StaticTabFields, HtmlModule):
""" """
Supports the field overrides Supports the field overrides
...@@ -286,6 +275,7 @@ class StaticTabModule(StaticTabFields, HtmlModule): ...@@ -286,6 +275,7 @@ class StaticTabModule(StaticTabFields, HtmlModule):
pass pass
@XBlock.tag("detached")
class StaticTabDescriptor(StaticTabFields, HtmlDescriptor): class StaticTabDescriptor(StaticTabFields, HtmlDescriptor):
""" """
These pieces of course content are treated as HtmlModules but we need to overload where the templates are located These pieces of course content are treated as HtmlModules but we need to overload where the templates are located
...@@ -304,14 +294,9 @@ class CourseInfoFields(object): ...@@ -304,14 +294,9 @@ class CourseInfoFields(object):
default="<ol></ol>", default="<ol></ol>",
scope=Scope.content scope=Scope.content
) )
# this exists purely to override the default start date
start = Date(
help="placeholder to make sure that Course Info is always active",
default=datetime.fromtimestamp(0, UTC),
scope=Scope.settings,
)
@XBlock.tag("detached")
class CourseInfoModule(CourseInfoFields, HtmlModule): class CourseInfoModule(CourseInfoFields, HtmlModule):
""" """
Just to support xblock field overrides Just to support xblock field overrides
...@@ -319,6 +304,7 @@ class CourseInfoModule(CourseInfoFields, HtmlModule): ...@@ -319,6 +304,7 @@ class CourseInfoModule(CourseInfoFields, HtmlModule):
pass pass
@XBlock.tag("detached")
class CourseInfoDescriptor(CourseInfoFields, HtmlDescriptor): class CourseInfoDescriptor(CourseInfoFields, HtmlDescriptor):
""" """
These pieces of course content are treated as HtmlModules but we need to overload where the templates are located These pieces of course content are treated as HtmlModules but we need to overload where the templates are located
......
...@@ -34,6 +34,7 @@ from xmodule.modulestore import ModuleStoreWriteBase, Location, MONGO_MODULESTOR ...@@ -34,6 +34,7 @@ from xmodule.modulestore import ModuleStoreWriteBase, Location, MONGO_MODULESTOR
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore.inheritance import own_metadata, InheritanceMixin, inherit_metadata, InheritanceKeyValueStore from xmodule.modulestore.inheritance import own_metadata, InheritanceMixin, inherit_metadata, InheritanceKeyValueStore
from xmodule.modulestore.xml import LocationReader from xmodule.modulestore.xml import LocationReader
from xblock.core import XBlock
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -869,10 +870,11 @@ class MongoModuleStore(ModuleStoreWriteBase): ...@@ -869,10 +870,11 @@ class MongoModuleStore(ModuleStoreWriteBase):
""" """
return MONGO_MODULESTORE_TYPE return MONGO_MODULESTORE_TYPE
def get_orphans(self, course_location, detached_categories, _branch): def get_orphans(self, course_location, _branch):
""" """
Return an array all of the locations for orphans in the course. Return an array all of the locations for orphans in the course.
""" """
detached_categories = [name for name, __ in XBlock.load_tagged_classes("detached")]
all_items = self.collection.find({ all_items = self.collection.find({
'_id.org': course_location.org, '_id.org': course_location.org,
'_id.course': course_location.course, '_id.course': course_location.course,
......
...@@ -439,12 +439,13 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -439,12 +439,13 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
) )
for parent_id in items] for parent_id in items]
def get_orphans(self, package_id, detached_categories, branch): def get_orphans(self, package_id, branch):
""" """
Return a dict of all of the orphans in the course. Return a dict of all of the orphans in the course.
:param package_id: :param package_id:
""" """
detached_categories = [name for name, __ in XBlock.load_tagged_classes("detached")]
course = self._lookup_course(CourseLocator(package_id=package_id, branch=branch)) course = self._lookup_course(CourseLocator(package_id=package_id, branch=branch))
items = {LocMapperStore.decode_key_from_mongo(block_id) for block_id in course['structure']['blocks'].keys()} items = {LocMapperStore.decode_key_from_mongo(block_id) for block_id in course['structure']['blocks'].keys()}
items.remove(course['structure']['root']) items.remove(course['structure']['root'])
......
...@@ -135,7 +135,7 @@ class TestOrphan(unittest.TestCase): ...@@ -135,7 +135,7 @@ class TestOrphan(unittest.TestCase):
""" """
Test that old mongo finds the orphans Test that old mongo finds the orphans
""" """
orphans = self.old_mongo.get_orphans(self.course_location, ['static_tab', 'about', 'course_info'], None) orphans = self.old_mongo.get_orphans(self.course_location, None)
self.assertEqual(len(orphans), 3, "Wrong # {}".format(orphans)) self.assertEqual(len(orphans), 3, "Wrong # {}".format(orphans))
location = self.course_location.replace(category='chapter', name='OrphanChapter') location = self.course_location.replace(category='chapter', name='OrphanChapter')
self.assertIn(location.url(), orphans) self.assertIn(location.url(), orphans)
...@@ -148,7 +148,7 @@ class TestOrphan(unittest.TestCase): ...@@ -148,7 +148,7 @@ class TestOrphan(unittest.TestCase):
""" """
Test that old mongo finds the orphans Test that old mongo finds the orphans
""" """
orphans = self.split_mongo.get_orphans(self.split_package_id, ['static_tab', 'about', 'course_info'], 'draft') orphans = self.split_mongo.get_orphans(self.split_package_id, 'draft')
self.assertEqual(len(orphans), 3, "Wrong # {}".format(orphans)) self.assertEqual(len(orphans), 3, "Wrong # {}".format(orphans))
location = BlockUsageLocator(package_id=self.split_package_id, branch='draft', block_id='OrphanChapter') location = BlockUsageLocator(package_id=self.split_package_id, branch='draft', block_id='OrphanChapter')
self.assertIn(location, orphans) self.assertIn(location, orphans)
......
...@@ -24,7 +24,6 @@ from student.roles import ( ...@@ -24,7 +24,6 @@ from student.roles import (
GlobalStaff, CourseStaffRole, CourseInstructorRole, GlobalStaff, CourseStaffRole, CourseInstructorRole,
OrgStaffRole, OrgInstructorRole, CourseBetaTesterRole OrgStaffRole, OrgInstructorRole, CourseBetaTesterRole
) )
DEBUG_ACCESS = False DEBUG_ACCESS = False
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -246,7 +245,7 @@ def _has_access_descriptor(user, descriptor, action, course_context=None): ...@@ -246,7 +245,7 @@ def _has_access_descriptor(user, descriptor, action, course_context=None):
return True return True
# Check start date # Check start date
if descriptor.start is not None: if 'detached' not in descriptor._class_tags and descriptor.start is not None:
now = datetime.now(UTC()) now = datetime.now(UTC())
effective_start = _adjust_start_date_for_beta_testers( effective_start = _adjust_start_date_for_beta_testers(
user, user,
......
"""
Test the about xblock
"""
from django.test.utils import override_settings
from django.core.urlresolvers import reverse
from .helpers import LoginEnrollmentTestCase
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
class AboutTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase):
def setUp(self):
self.course = CourseFactory.create()
self.about = ItemFactory.create(
category="about", parent_location=self.course.location,
data="OOGIE BLOOGIE", display_name="overview"
)
def test_logged_in(self):
self.setup_user()
url = reverse('about_course', args=[self.course.id])
resp = self.client.get(url)
self.assertEqual(resp.status_code, 200)
self.assertIn("OOGIE BLOOGIE", resp.content)
def test_anonymous_user(self):
url = reverse('about_course', args=[self.course.id])
resp = self.client.get(url)
self.assertEqual(resp.status_code, 200)
self.assertIn("OOGIE BLOOGIE", resp.content)
"""
Test the course_info xblock
"""
from django.test.utils import override_settings
from django.core.urlresolvers import reverse
from .helpers import LoginEnrollmentTestCase
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
class CourseInfoTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase):
def setUp(self):
self.course = CourseFactory.create()
self.page = ItemFactory.create(
category="course_info", parent_location=self.course.location,
data="OOGIE BLOOGIE", display_name="updates"
)
def test_logged_in(self):
self.setup_user()
url = reverse('info', args=[self.course.id])
resp = self.client.get(url)
self.assertEqual(resp.status_code, 200)
self.assertIn("OOGIE BLOOGIE", resp.content)
def test_anonymous_user(self):
url = reverse('info', args=[self.course.id])
resp = self.client.get(url)
self.assertEqual(resp.status_code, 200)
self.assertNotIn("OOGIE BLOOGIE", resp.content)
...@@ -8,8 +8,9 @@ from django.test.utils import override_settings ...@@ -8,8 +8,9 @@ from django.test.utils import override_settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE
from .helpers import LoginEnrollmentTestCase
FAKE_REQUEST = None FAKE_REQUEST = None
...@@ -146,6 +147,29 @@ class StaticTabTestCase(TestCase): ...@@ -146,6 +147,29 @@ class StaticTabTestCase(TestCase):
) )
self.assertEqual(tab_list[0].is_active, False) self.assertEqual(tab_list[0].is_active, False)
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
class StaticTabDateTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase):
def setUp(self):
self.course = CourseFactory.create()
self.page = ItemFactory.create(
category="static_tab", parent_location=self.course.location,
data="OOGIE BLOOGIE", display_name="new_tab"
)
def test_logged_in(self):
self.setup_user()
url = reverse('static_tab', args=[self.course.id, 'new_tab'])
resp = self.client.get(url)
self.assertEqual(resp.status_code, 200)
self.assertIn("OOGIE BLOOGIE", resp.content)
def test_anonymous_user(self):
url = reverse('static_tab', args=[self.course.id, 'new_tab'])
resp = self.client.get(url)
self.assertEqual(resp.status_code, 200)
self.assertIn("OOGIE BLOOGIE", resp.content)
class TextbooksTestCase(TestCase): class TextbooksTestCase(TestCase):
def setUp(self): def setUp(self):
......
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