diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py
index ef6ab71b88..faec60f3e8 100644
--- a/cms/djangoapps/contentstore/tests/test_contentstore.py
+++ b/cms/djangoapps/contentstore/tests/test_contentstore.py
@@ -14,6 +14,7 @@ from json import loads
import traceback
from django.contrib.auth.models import User
+from django.dispatch import Signal
from contentstore.utils import get_modulestore
from .utils import ModuleStoreTestCase, parse_json
@@ -792,6 +793,45 @@ class ContentStoreTest(ModuleStoreTestCase):
# make sure we found the item (e.g. it didn't error while loading)
self.assertTrue(did_load_item)
+ def test_forum_id_generation(self):
+ import_from_xml(modulestore(), 'common/test/data/', ['full'])
+ module_store = modulestore('direct')
+ new_component_location = Location('i4x', 'edX', 'full', 'discussion', 'new_component')
+ source_template_location = Location('i4x', 'edx', 'templates', 'discussion', 'Discussion_Tag')
+
+ # crate a new module and add it as a child to a vertical
+ module_store.clone_item(source_template_location, new_component_location)
+
+ new_discussion_item = module_store.get_item(new_component_location)
+
+ self.assertNotEquals(new_discussion_item.discussion_id, '$$GUID$$')
+
+ def test_update_modulestore_signal_did_fire(self):
+
+ import_from_xml(modulestore(), 'common/test/data/', ['full'])
+ module_store = modulestore('direct')
+
+ try:
+ module_store.modulestore_update_signal = Signal(providing_args=['modulestore', 'course_id', 'location'])
+
+ self.got_signal = False
+
+ def _signal_hander(modulestore=None, course_id=None, location=None, **kwargs):
+ self.got_signal = True
+
+ module_store.modulestore_update_signal.connect(_signal_hander)
+
+ new_component_location = Location('i4x', 'edX', 'full', 'html', 'new_component')
+ source_template_location = Location('i4x', 'edx', 'templates', 'html', 'Blank_HTML_Page')
+
+ # crate a new module
+ module_store.clone_item(source_template_location, new_component_location)
+
+ finally:
+ module_store.modulestore_update_signal = None
+
+ self.assertTrue(self.got_signal)
+
def test_metadata_inheritance(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py
index 83a2bde72d..ec439b3312 100644
--- a/cms/djangoapps/contentstore/utils.py
+++ b/cms/djangoapps/contentstore/utils.py
@@ -11,6 +11,7 @@ DIRECT_ONLY_CATEGORIES = ['course', 'chapter', 'sequential', 'about', 'static_ta
#In order to instantiate an open ended tab automatically, need to have this data
OPEN_ENDED_PANEL = {"name" : "Open Ended Panel", "type" : "open_ended"}
+
def get_modulestore(location):
"""
Returns the correct modulestore to use for modifying the specified location
diff --git a/cms/one_time_startup.py b/cms/one_time_startup.py
index 6e88fed439..5cc74a8fd7 100644
--- a/cms/one_time_startup.py
+++ b/cms/one_time_startup.py
@@ -1,9 +1,10 @@
from dogapi import dog_http_api, dog_stats_api
from django.conf import settings
from xmodule.modulestore.django import modulestore
+from django.dispatch import Signal
from request_cache.middleware import RequestCache
-from django.core.cache import get_cache, InvalidCacheBackendError
+from django.core.cache import get_cache
cache = get_cache('mongo_metadata_inheritance')
for store_name in settings.MODULESTORE:
@@ -11,6 +12,8 @@ for store_name in settings.MODULESTORE:
store.metadata_inheritance_cache_subsystem = cache
store.request_cache = RequestCache.get_request_cache()
+ modulestore_update_signal = Signal(providing_args=['modulestore', 'course_id', 'location'])
+ store.modulestore_update_signal = modulestore_update_signal
if hasattr(settings, 'DATADOG_API'):
dog_http_api.api_key = settings.DATADOG_API
dog_stats_api.start(api_key=settings.DATADOG_API, statsd=True)
diff --git a/cms/templates/widgets/metadata-only-edit.html b/cms/templates/widgets/metadata-only-edit.html
new file mode 100644
index 0000000000..a784f3798c
--- /dev/null
+++ b/cms/templates/widgets/metadata-only-edit.html
@@ -0,0 +1 @@
+<%include file="metadata-edit.html" />
diff --git a/common/lib/xmodule/xmodule/discussion_module.py b/common/lib/xmodule/xmodule/discussion_module.py
index 7725a88e77..a0a5207e16 100644
--- a/common/lib/xmodule/xmodule/discussion_module.py
+++ b/common/lib/xmodule/xmodule/discussion_module.py
@@ -3,6 +3,7 @@ from pkg_resources import resource_string, resource_listdir
from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor
+from xmodule.editing_module import MetadataOnlyEditingDescriptor
from xblock.core import String, Scope
@@ -28,7 +29,7 @@ class DiscussionModule(DiscussionFields, XModule):
return self.system.render_template('discussion/_discussion_module.html', context)
-class DiscussionDescriptor(DiscussionFields, RawDescriptor):
+class DiscussionDescriptor(DiscussionFields, MetadataOnlyEditingDescriptor, RawDescriptor):
module_class = DiscussionModule
template_dir_name = "discussion"
diff --git a/common/lib/xmodule/xmodule/editing_module.py b/common/lib/xmodule/xmodule/editing_module.py
index b93727a96b..df4ebc5646 100644
--- a/common/lib/xmodule/xmodule/editing_module.py
+++ b/common/lib/xmodule/xmodule/editing_module.py
@@ -41,6 +41,18 @@ class XMLEditingDescriptor(EditingDescriptor):
js_module_name = "XMLEditingDescriptor"
+class MetadataOnlyEditingDescriptor(EditingDescriptor):
+ """
+ Module which only provides an editing interface for the metadata, it does
+ not expose a UI for editing the module data
+ """
+
+ js = {'coffee': [resource_string(__name__, 'js/src/raw/edit/metadata-only.coffee')]}
+ js_module_name = "MetadataOnlyEditingDescriptor"
+
+ mako_template = "widgets/metadata-only-edit.html"
+
+
class JSONEditingDescriptor(EditingDescriptor):
"""
Module that provides a raw editing view of its data as XML. It does not perform
diff --git a/common/lib/xmodule/xmodule/js/src/raw/edit/metadata-only.coffee b/common/lib/xmodule/xmodule/js/src/raw/edit/metadata-only.coffee
new file mode 100644
index 0000000000..8c9afe86aa
--- /dev/null
+++ b/common/lib/xmodule/xmodule/js/src/raw/edit/metadata-only.coffee
@@ -0,0 +1,5 @@
+class @MetadataOnlyEditingDescriptor extends XModule.Descriptor
+ constructor: (@element) ->
+
+ save: ->
+ data: null
diff --git a/common/lib/xmodule/xmodule/modulestore/__init__.py b/common/lib/xmodule/xmodule/modulestore/__init__.py
index 2593b04472..ae04e3aac4 100644
--- a/common/lib/xmodule/xmodule/modulestore/__init__.py
+++ b/common/lib/xmodule/xmodule/modulestore/__init__.py
@@ -252,7 +252,6 @@ class Location(_LocationBase):
def __repr__(self):
return "Location%s" % repr(tuple(self))
-
@property
def course_id(self):
"""Return the ID of the Course that this item belongs to by looking
@@ -414,7 +413,6 @@ class ModuleStore(object):
return courses
-
class ModuleStoreBase(ModuleStore):
'''
Implement interface functionality that can be shared.
@@ -425,6 +423,7 @@ class ModuleStoreBase(ModuleStore):
'''
self._location_errors = {} # location -> ErrorLog
self.metadata_inheritance_cache = None
+ self.modulestore_update_signal = None # can be set by runtime to route notifications of datastore changes
def _get_errorlog(self, location):
"""
diff --git a/common/lib/xmodule/xmodule/modulestore/mongo.py b/common/lib/xmodule/xmodule/modulestore/mongo.py
index 0ed93c9768..653a7ca22a 100644
--- a/common/lib/xmodule/xmodule/modulestore/mongo.py
+++ b/common/lib/xmodule/xmodule/modulestore/mongo.py
@@ -9,6 +9,7 @@ from itertools import repeat
from path import path
from datetime import datetime
from operator import attrgetter
+from uuid import uuid4
from importlib import import_module
from xmodule.errortracker import null_error_tracker, exc_info_to_str
@@ -30,6 +31,10 @@ log = logging.getLogger(__name__)
# there is only one revision for each item. Once we start versioning inside the CMS,
# that assumption will have to change
+def get_course_id_no_run(location):
+ '''
+ '''
+ return "/".join([location.org, location.course])
class MongoKeyValueStore(KeyValueStore):
"""
@@ -333,7 +338,7 @@ class MongoModuleStore(ModuleStoreBase):
'''
key = metadata_cache_key(location)
tree = {}
-
+
if not force_refresh:
# see if we are first in the request cache (if present)
if self.request_cache is not None and key in self.request_cache.data.get('metadata_inheritance', {}):
@@ -348,7 +353,7 @@ class MongoModuleStore(ModuleStoreBase):
if not tree:
# if not in subsystem, or we are on force refresh, then we have to compute
tree = self.compute_metadata_inheritance_tree(location)
-
+
# now write out computed tree to caching subsystem (e.g. memcached), if available
if self.metadata_inheritance_cache_subsystem is not None:
self.metadata_inheritance_cache_subsystem.set(key, tree)
@@ -541,8 +546,15 @@ class MongoModuleStore(ModuleStoreBase):
Clone a new item that is a copy of the item at the location `source`
and writes it to `location`
"""
+ item = None
try:
source_item = self.collection.find_one(location_to_query(source))
+
+ # allow for some programmatically generated substitutions in metadata, e.g. Discussion_id's should be auto-generated
+ for key in source_item['metadata'].keys():
+ if source_item['metadata'][key] == '$$GUID$$':
+ source_item['metadata'][key] = uuid4().hex
+
source_item['_id'] = Location(location).dict()
self.collection.insert(
source_item,
@@ -566,12 +578,19 @@ class MongoModuleStore(ModuleStoreBase):
course.tabs = existing_tabs
self.update_metadata(course.location, course._model_data._kvs._metadata)
- return item
except pymongo.errors.DuplicateKeyError:
raise DuplicateItemError(location)
# recompute (and update) the metadata inheritance tree which is cached
self.refresh_cached_metadata_inheritance_tree(Location(location))
+ self.fire_updated_modulestore_signal(get_course_id_no_run(Location(location)), Location(location))
+
+ return item
+
+ def fire_updated_modulestore_signal(self, course_id, location):
+ if self.modulestore_update_signal is not None:
+ self.modulestore_update_signal.send(self, modulestore=self, course_id=course_id,
+ location=location)
def get_course_for_item(self, location, depth=0):
'''
@@ -643,6 +662,8 @@ class MongoModuleStore(ModuleStoreBase):
self._update_single_item(location, {'definition.children': children})
# recompute (and update) the metadata inheritance tree which is cached
self.refresh_cached_metadata_inheritance_tree(Location(location))
+ # fire signal that we've written to DB
+ self.fire_updated_modulestore_signal(get_course_id_no_run(Location(location)), Location(location))
def update_metadata(self, location, metadata):
"""
@@ -669,6 +690,7 @@ class MongoModuleStore(ModuleStoreBase):
self._update_single_item(location, {'metadata': metadata})
# recompute (and update) the metadata inheritance tree which is cached
self.refresh_cached_metadata_inheritance_tree(loc)
+ self.fire_updated_modulestore_signal(get_course_id_no_run(Location(location)), Location(location))
def delete_item(self, location):
"""
@@ -692,6 +714,7 @@ class MongoModuleStore(ModuleStoreBase):
safe=self.collection.safe)
# recompute (and update) the metadata inheritance tree which is cached
self.refresh_cached_metadata_inheritance_tree(Location(location))
+ self.fire_updated_modulestore_signal(get_course_id_no_run(Location(location)), Location(location))
def get_parent_locations(self, location, course_id):
'''Find all locations that are the parents of this location in this
diff --git a/common/lib/xmodule/xmodule/raw_module.py b/common/lib/xmodule/xmodule/raw_module.py
index 2c6e157018..554be73926 100644
--- a/common/lib/xmodule/xmodule/raw_module.py
+++ b/common/lib/xmodule/xmodule/raw_module.py
@@ -29,6 +29,6 @@ class RawDescriptor(XmlDescriptor, XMLEditingDescriptor):
line, offset = err.position
msg = ("Unable to create xml for problem {loc}. "
"Context: '{context}'".format(
- context=lines[line - 1][offset - 40:offset + 40],
- loc=self.location))
+ context=lines[line - 1][offset - 40:offset + 40],
+ loc=self.location))
raise Exception, msg, sys.exc_info()[2]
diff --git a/common/lib/xmodule/xmodule/templates/discussion/default.yaml b/common/lib/xmodule/xmodule/templates/discussion/default.yaml
index d34e6378e6..049e34b3e7 100644
--- a/common/lib/xmodule/xmodule/templates/discussion/default.yaml
+++ b/common/lib/xmodule/xmodule/templates/discussion/default.yaml
@@ -2,8 +2,8 @@
metadata:
display_name: Discussion Tag
for: Topic-Level Student-Visible Label
- id: 6002x_group_discussion_by_this
+ id: $$GUID$$
discussion_category: Week 1
data: |
-
+
children: []
diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py
index bd331ec7d2..e6d367ac7a 100644
--- a/common/lib/xmodule/xmodule/x_module.py
+++ b/common/lib/xmodule/xmodule/x_module.py
@@ -340,7 +340,9 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
# cdodge: this is a list of metadata names which are 'system' metadata
# and should not be edited by an end-user
- system_metadata_fields = ['data_dir', 'published_date', 'published_by', 'is_draft', 'xml_attributes']
+
+ system_metadata_fields = ['data_dir', 'published_date', 'published_by', 'is_draft',
+ 'discussion_id', 'xml_attributes']
# A list of descriptor attributes that must be equal for the descriptors to
# be equal
diff --git a/lms/djangoapps/django_comment_client/management/commands/seed_permissions_roles.py b/lms/djangoapps/django_comment_client/management/commands/seed_permissions_roles.py
index 6a31e73af3..9d6eefd11d 100644
--- a/lms/djangoapps/django_comment_client/management/commands/seed_permissions_roles.py
+++ b/lms/djangoapps/django_comment_client/management/commands/seed_permissions_roles.py
@@ -1,5 +1,5 @@
from django.core.management.base import BaseCommand, CommandError
-from django_comment_client.models import Permission, Role
+from django_comment_client.models import Role
class Command(BaseCommand):
@@ -12,18 +12,19 @@ class Command(BaseCommand):
if len(args) > 1:
raise CommandError("Too many arguments")
course_id = args[0]
+
administrator_role = Role.objects.get_or_create(name="Administrator", course_id=course_id)[0]
moderator_role = Role.objects.get_or_create(name="Moderator", course_id=course_id)[0]
community_ta_role = Role.objects.get_or_create(name="Community TA", course_id=course_id)[0]
student_role = Role.objects.get_or_create(name="Student", course_id=course_id)[0]
for per in ["vote", "update_thread", "follow_thread", "unfollow_thread",
- "update_comment", "create_sub_comment", "unvote", "create_thread",
- "follow_commentable", "unfollow_commentable", "create_comment", ]:
+ "update_comment", "create_sub_comment", "unvote", "create_thread",
+ "follow_commentable", "unfollow_commentable", "create_comment", ]:
student_role.add_permission(per)
for per in ["edit_content", "delete_thread", "openclose_thread",
- "endorse_comment", "delete_comment", "see_all_cohorts"]:
+ "endorse_comment", "delete_comment", "see_all_cohorts"]:
moderator_role.add_permission(per)
for per in ["manage_moderator"]:
diff --git a/lms/djangoapps/django_comment_client/utils.py b/lms/djangoapps/django_comment_client/utils.py
index 42233b84da..9bfb9a9d0d 100644
--- a/lms/djangoapps/django_comment_client/utils.py
+++ b/lms/djangoapps/django_comment_client/utils.py
@@ -146,28 +146,16 @@ def sort_map_entries(category_map):
def initialize_discussion_info(course):
-
global _DISCUSSIONINFO
- # only cache in-memory discussion information for 10 minutes
- # this is because we need a short-term hack fix for
- # mongo-backed courseware whereby new discussion modules can be added
- # without LMS service restart
-
- if _DISCUSSIONINFO[course.id]:
- timestamp = _DISCUSSIONINFO[course.id].get('timestamp', datetime.now())
- age = datetime.now() - timestamp
- # expire every 5 minutes
- if age.seconds < 300:
- return
-
course_id = course.id
discussion_id_map = {}
unexpanded_category_map = defaultdict(list)
# get all discussion models within this course_id
- all_modules = modulestore().get_items(['i4x', course.location.org, course.location.course, 'discussion', None], course_id=course_id)
+ all_modules = modulestore().get_items(['i4x', course.location.org, course.location.course,
+ 'discussion', None], course_id=course_id)
for module in all_modules:
skip_module = False