Commit a0b3fb0e by Chris Dodge

Merge branch 'feature/cale/cms-master' of github.com:MITx/mitx into…

Merge branch 'feature/cale/cms-master' of github.com:MITx/mitx into feature/cdodge/subsection-edit-page
parents e24dcaea 5e769955
...@@ -442,14 +442,13 @@ def clone_item(request): ...@@ -442,14 +442,13 @@ def clone_item(request):
return HttpResponse(json.dumps({'id': dest_location.url()})) return HttpResponse(json.dumps({'id': dest_location.url()}))
'''
cdodge: this method allows for POST uploading of files into the course asset library, which will
be supported by GridFS in MongoDB.
'''
#@login_required #@login_required
#@ensure_csrf_cookie #@ensure_csrf_cookie
def upload_asset(request, org, course, coursename): def upload_asset(request, org, course, coursename):
'''
cdodge: this method allows for POST uploading of files into the course asset library, which will
be supported by GridFS in MongoDB.
'''
if request.method != 'POST': if request.method != 'POST':
# (cdodge) @todo: Is there a way to do a - say - 'raise Http400'? # (cdodge) @todo: Is there a way to do a - say - 'raise Http400'?
return HttpResponseBadRequest() return HttpResponseBadRequest()
...@@ -476,7 +475,7 @@ def upload_asset(request, org, course, coursename): ...@@ -476,7 +475,7 @@ def upload_asset(request, org, course, coursename):
mime_type = request.FILES['file'].content_type mime_type = request.FILES['file'].content_type
filedata = request.FILES['file'].read() filedata = request.FILES['file'].read()
file_location = StaticContent.compute_location_filename(org, course, name) file_location = StaticContent.compute_location(org, course, name)
content = StaticContent(file_location, name, mime_type, filedata) content = StaticContent(file_location, name, mime_type, filedata)
...@@ -489,7 +488,7 @@ def upload_asset(request, org, course, coursename): ...@@ -489,7 +488,7 @@ def upload_asset(request, org, course, coursename):
# browser-side caching support. We *could* re-fetch the saved content so that we have the # browser-side caching support. We *could* re-fetch the saved content so that we have the
# timestamp populated, but we might as well wait for the first real request to come in # timestamp populated, but we might as well wait for the first real request to come in
# to re-populate the cache. # to re-populate the cache.
del_cached_content(file_location) del_cached_content(content.location)
# if we're uploading an image, then let's generate a thumbnail so that we can # if we're uploading an image, then let's generate a thumbnail so that we can
# serve it up when needed without having to rescale on the fly # serve it up when needed without having to rescale on the fly
...@@ -514,10 +513,10 @@ def upload_asset(request, org, course, coursename): ...@@ -514,10 +513,10 @@ def upload_asset(request, org, course, coursename):
thumbnail_file.seek(0) thumbnail_file.seek(0)
# use a naming convention to associate originals with the thumbnail # use a naming convention to associate originals with the thumbnail
# <name_without_extention>.thumbnail.jpg thumbnail_name = content.generate_thumbnail_name()
thumbnail_name = os.path.splitext(name)[0] + '.thumbnail.jpg'
# then just store this thumbnail as any other piece of content # then just store this thumbnail as any other piece of content
thumbnail_file_location = StaticContent.compute_location_filename(org, course, thumbnail_file_location = StaticContent.compute_location(org, course,
thumbnail_name) thumbnail_name)
thumbnail_content = StaticContent(thumbnail_file_location, thumbnail_name, thumbnail_content = StaticContent(thumbnail_file_location, thumbnail_name,
'image/jpeg', thumbnail_file) 'image/jpeg', thumbnail_file)
...@@ -525,11 +524,12 @@ def upload_asset(request, org, course, coursename): ...@@ -525,11 +524,12 @@ def upload_asset(request, org, course, coursename):
# remove any cached content at this location, as thumbnails are treated just like any # remove any cached content at this location, as thumbnails are treated just like any
# other bit of static content # other bit of static content
del_cached_content(thumbnail_file_location) del_cached_content(thumbnail_content.location)
except: except:
# catch, log, and continue as thumbnails are not a hard requirement # catch, log, and continue as thumbnails are not a hard requirement
logging.error('Failed to generate thumbnail for {0}. Continuing...'.format(name)) logging.error('Failed to generate thumbnail for {0}. Continuing...'.format(name))
return HttpResponse('Upload completed') return HttpResponse('Upload completed')
''' '''
......
...@@ -157,7 +157,8 @@ ...@@ -157,7 +157,8 @@
} }
.draft-item, .draft-item,
.hidden-item { .hidden-item,
.private-item {
color: #a4aab7; color: #a4aab7;
} }
......
...@@ -24,7 +24,7 @@ ...@@ -24,7 +24,7 @@
.class-name { .class-name {
display: block; display: block;
font-size: 22px; font-size: 19px;
font-weight: 300; font-weight: 300;
} }
......
...@@ -111,6 +111,7 @@ ...@@ -111,6 +111,7 @@
.draft-tag, .draft-tag,
.hidden-tag, .hidden-tag,
.private-tag,
.has-new-draft-tag { .has-new-draft-tag {
margin-left: 3px; margin-left: 3px;
font-size: 9px; font-size: 9px;
......
...@@ -226,9 +226,20 @@ ...@@ -226,9 +226,20 @@
padding: 20px; padding: 20px;
border-radius: 0 0 3px 3px; border-radius: 0 0 3px 3px;
background: -webkit-linear-gradient(top, rgba(0, 0, 0, 0), rgba(0, 0, 0, .1)) $blue; background: -webkit-linear-gradient(top, rgba(0, 0, 0, 0), rgba(0, 0, 0, .1)) $blue;
color: #fff;
.module-editor { .metadata_edit {
background: white; margin-bottom: 20px;
font-size: 13px;
}
.CodeMirror {
border: 1px solid #3c3c3c;
}
h3 {
font-size: 18px;
font-weight: 700;
} }
h5 { h5 {
...@@ -238,7 +249,8 @@ ...@@ -238,7 +249,8 @@
} }
.save-button { .save-button {
margin-right: 8px; margin-top: 10px;
margin: 15px 8px 0 0;
} }
} }
} }
...@@ -266,19 +278,8 @@ ...@@ -266,19 +278,8 @@
} }
} }
.visibility-options .option { input[type="radio"] {
margin-right: 10px;
padding: 3px 13px 6px;
border-radius: 3px;
background: #edf1f5;
&.checked {
background: #d1dae3;
}
input[type="radio"] {
margin-right: 7px; margin-right: 7px;
}
} }
.status { .status {
......
...@@ -49,12 +49,11 @@ ...@@ -49,12 +49,11 @@
% for unit in subsection.get_children(): % for unit in subsection.get_children():
<li class="leaf"> <li class="leaf">
<div class="section-item"> <div class="section-item">
<a href="${reverse('edit_unit', args=[unit.location])}" class="hidden-item"> <a href="${reverse('edit_unit', args=[unit.location])}" class="private-item">
<span class="${unit.category}-icon"></span>${unit.display_name} <span class="hidden-tag">– hidden</span> <span class="${unit.category}-icon"></span>${unit.display_name} <span class="private-tag">– private</span>
</a> </a>
<div class="item-actions"> <div class="item-actions">
<a href="${reverse('delete_unit', args=[unit.location])}" class="edit-button wip"><span class="delete-icon"></span></a> <a href="${reverse('delete_unit', args=[unit.location])}" class="edit-button wip"><span class="delete-icon"></span></a>
<a href="#" class="visibility-toggle hidden wip"><span class="toggle-icon"></span></a>
<a href="#" class="drag-handle wip"></a> <a href="#" class="drag-handle wip"></a>
</div> </div>
</div> </div>
......
...@@ -12,7 +12,7 @@ from django.core.cache import cache ...@@ -12,7 +12,7 @@ from django.core.cache import cache
from django.db import DEFAULT_DB_ALIAS from django.db import DEFAULT_DB_ALIAS
from . import app_settings from . import app_settings
from xmodule.contentstore.content import StaticContent
def get_instance(model, instance_or_pk, timeout=None, using=None): def get_instance(model, instance_or_pk, timeout=None, using=None):
""" """
...@@ -108,14 +108,11 @@ def instance_key(model, instance_or_pk): ...@@ -108,14 +108,11 @@ def instance_key(model, instance_or_pk):
getattr(instance_or_pk, 'pk', instance_or_pk), getattr(instance_or_pk, 'pk', instance_or_pk),
) )
def content_key(filename):
return 'content:%s' % (filename)
def set_cached_content(content): def set_cached_content(content):
cache.set(content_key(content.filename), content) cache.set(content.get_id(), content)
def get_cached_content(filename): def get_cached_content(location):
return cache.get(content_key(filename)) return cache.get(StaticContent.get_id_from_location(location))
def del_cached_content(filename): def del_cached_content(location):
cache.delete(content_key(filename)) cache.delete(StaticContent.get_id_from_location(location))
...@@ -12,14 +12,14 @@ from xmodule.exceptions import NotFoundError ...@@ -12,14 +12,14 @@ from xmodule.exceptions import NotFoundError
class StaticContentServer(object): class StaticContentServer(object):
def process_request(self, request): def process_request(self, request):
# look to see if the request is prefixed with 'c4x' tag # look to see if the request is prefixed with 'c4x' tag
if request.path.startswith('/' + XASSET_LOCATION_TAG): if request.path.startswith('/' + XASSET_LOCATION_TAG +'/'):
loc = StaticContent.get_location_from_path(request.path)
# first look in our cache so we don't have to round-trip to the DB # first look in our cache so we don't have to round-trip to the DB
content = get_cached_content(request.path) content = get_cached_content(loc)
if content is None: if content is None:
# nope, not in cache, let's fetch from DB # nope, not in cache, let's fetch from DB
try: try:
content = contentstore().find(request.path) content = contentstore().find(loc)
except NotFoundError: except NotFoundError:
raise Http404 raise Http404
......
...@@ -339,7 +339,10 @@ class CapaModule(XModule): ...@@ -339,7 +339,10 @@ class CapaModule(XModule):
# NOTE: rewrite_content_links is defined in XModule # NOTE: rewrite_content_links is defined in XModule
# This is a bit unfortunate and I'm sure we'll try to considate this into # This is a bit unfortunate and I'm sure we'll try to considate this into
# a one step process. # a one step process.
html = rewrite_links(html, self.rewrite_content_links) try:
html = rewrite_links(html, self.rewrite_content_links)
except:
logging.error('error rewriting links in {0}'.format(html))
# now do the substitutions which are filesystem based, e.g. '/static/' prefixes # now do the substitutions which are filesystem based, e.g. '/static/' prefixes
return self.system.replace_urls(html, self.metadata['data_dir']) return self.system.replace_urls(html, self.metadata['data_dir'])
......
XASSET_LOCATION_TAG = 'c4x' XASSET_LOCATION_TAG = 'c4x'
XASSET_SRCREF_PREFIX = 'xasset:' XASSET_SRCREF_PREFIX = 'xasset:'
XASSET_THUMBNAIL_TAIL_NAME = '.thumbnail.jpg'
import os
import logging
from xmodule.modulestore import Location
class StaticContent(object): class StaticContent(object):
def __init__(self, filename, name, content_type, data, last_modified_at=None): def __init__(self, loc, name, content_type, data, last_modified_at=None):
self.filename = filename self.location = loc
self.name = name self.name = name #a display string which can be edited, and thus not part of the location which needs to be fixed
self.content_type = content_type self.content_type = content_type
self.data = data self.data = data
self.last_modified_at = last_modified_at self.last_modified_at = last_modified_at
@property
def is_thumbnail(self):
return self.name.endswith(XASSET_THUMBNAIL_TAIL_NAME)
def generate_thumbnail_name(self):
return ('{0}'+XASSET_THUMBNAIL_TAIL_NAME).format(os.path.splitext(self.name)[0])
@staticmethod
def compute_location(org, course, name, revision=None):
return Location([XASSET_LOCATION_TAG, org, course, 'asset', name, revision])
def get_id(self):
return StaticContent.get_id_from_location(self.location)
def get_url_path(self):
return StaticContent.get_url_path_from_location(self.location)
@staticmethod @staticmethod
def compute_location_filename(org, course, name): def get_url_path_from_location(location):
return '/{0}/{1}/{2}/asset/{3}'.format(XASSET_LOCATION_TAG, org, course, name) return "/{tag}/{org}/{course}/{category}/{name}".format(**location.dict())
@staticmethod
def get_id_from_location(location):
return { 'tag':location.tag, 'org' : location.org, 'course' : location.course,
'category' : location.category, 'name' : location.name,
'revision' : location.revision}
@staticmethod
def get_location_from_path(path):
# remove leading / character if it is there one
if path.startswith('/'):
path = path[1:]
return Location(path.split('/'))
@staticmethod
def get_id_from_path(path):
return get_id_from_location(get_location_from_path(path))
'''
Abstraction for all ContentStore providers (e.g. MongoDB)
'''
class ContentStore(object): class ContentStore(object):
'''
Abstraction for all ContentStore providers (e.g. MongoDB)
'''
def save(self, content): def save(self, content):
raise NotImplementedError raise NotImplementedError
def find(self, filename): def find(self, filename):
raise NotImplementedError raise NotImplementedError
def get_all_content_for_course(self, location):
raise NotImplementedError
from bson.son import SON
from pymongo import Connection from pymongo import Connection
import gridfs import gridfs
from gridfs.errors import NoFile from gridfs.errors import NoFile
from xmodule.modulestore.mongo import location_to_query, Location
from xmodule.contentstore.content import XASSET_LOCATION_TAG
import sys import sys
import logging import logging
...@@ -14,19 +18,53 @@ class MongoContentStore(ContentStore): ...@@ -14,19 +18,53 @@ class MongoContentStore(ContentStore):
logging.debug( 'Using MongoDB for static content serving at host={0} db={1}'.format(host,db)) logging.debug( 'Using MongoDB for static content serving at host={0} db={1}'.format(host,db))
_db = Connection(host=host, port=port)[db] _db = Connection(host=host, port=port)[db]
self.fs = gridfs.GridFS(_db) self.fs = gridfs.GridFS(_db)
self.fs_files = _db["fs.files"] # the underlying collection GridFS uses
def save(self, content): def save(self, content):
with self.fs.new_file(filename=content.filename, content_type=content.content_type, displayname=content.name) as fp: id = content.get_id()
# Seems like with the GridFS we can't update existing ID's we have to do a delete/add pair
if self.fs.exists({"_id" : id}):
self.fs.delete(id)
with self.fs.new_file(_id = id, content_type=content.content_type, displayname=content.name) as fp:
fp.write(content.data) fp.write(content.data)
return content return content
def find(self, filename): def find(self, location):
id = StaticContent.get_id_from_location(location)
try: try:
with self.fs.get_last_version(filename) as fp: with self.fs.get(id) as fp:
return StaticContent(fp.filename, fp.displayname, fp.content_type, fp.read(), fp.uploadDate) return StaticContent(location, fp.displayname, fp.content_type, fp.read(), fp.uploadDate)
except NoFile: except NoFile:
raise NotFoundError() raise NotFoundError()
def get_all_content_info_for_course(self, location):
'''
Returns a list of all static assets for a course. The return format is a list of dictionary elements. Example:
[
{u'displayname': u'profile.jpg', u'chunkSize': 262144, u'length': 85374,
u'uploadDate': datetime.datetime(2012, 10, 3, 5, 41, 54, 183000), u'contentType': u'image/jpeg',
u'_id': {u'category': u'asset', u'name': u'profile.jpg', u'course': u'6.002x', u'tag': u'c4x',
u'org': u'MITx', u'revision': None}, u'md5': u'36dc53519d4b735eb6beba51cd686a0e'},
{u'displayname': u'profile.thumbnail.jpg', u'chunkSize': 262144, u'length': 4073,
u'uploadDate': datetime.datetime(2012, 10, 3, 5, 41, 54, 196000), u'contentType': u'image/jpeg',
u'_id': {u'category': u'asset', u'name': u'profile.thumbnail.jpg', u'course': u'6.002x', u'tag': u'c4x',
u'org': u'MITx', u'revision': None}, u'md5': u'ff1532598830e3feac91c2449eaa60d6'},
....
]
'''
course_filter = Location(XASSET_LOCATION_TAG, category="asset",course=location.course,org=location.org)
# 'borrow' the function 'location_to_query' from the Mongo modulestore implementation
items = self.fs_files.find(location_to_query(course_filter))
return list(items)
.CodeMirror {
background: #fff;
font-size: 13px;
color: #3c3c3c;
}
\ No newline at end of file
...@@ -30,6 +30,8 @@ class XMLEditingDescriptor(EditingDescriptor): ...@@ -30,6 +30,8 @@ class XMLEditingDescriptor(EditingDescriptor):
any validation of its definition any validation of its definition
""" """
css = {'scss': [resource_string(__name__, 'css/codemirror/codemirror.scss')]}
js = {'coffee': [resource_string(__name__, 'js/src/raw/edit/xml.coffee')]} js = {'coffee': [resource_string(__name__, 'js/src/raw/edit/xml.coffee')]}
js_module_name = "XMLEditingDescriptor" js_module_name = "XMLEditingDescriptor"
...@@ -40,5 +42,7 @@ class JSONEditingDescriptor(EditingDescriptor): ...@@ -40,5 +42,7 @@ class JSONEditingDescriptor(EditingDescriptor):
any validation of its definition any validation of its definition
""" """
css = {'scss': [resource_string(__name__, 'css/codemirror/codemirror.scss')]}
js = {'coffee': [resource_string(__name__, 'js/src/raw/edit/json.coffee')]} js = {'coffee': [resource_string(__name__, 'js/src/raw/edit/json.coffee')]}
js_module_name = "JSONEditingDescriptor" js_module_name = "JSONEditingDescriptor"
...@@ -26,7 +26,13 @@ class HtmlModule(XModule): ...@@ -26,7 +26,13 @@ class HtmlModule(XModule):
def get_html(self): def get_html(self):
# cdodge: perform link substitutions for any references to course static content (e.g. images) # cdodge: perform link substitutions for any references to course static content (e.g. images)
return rewrite_links(self.html, self.rewrite_content_links) _html = self.html
try:
_html = rewrite_links(_html, self.rewrite_content_links)
except:
logging.error('error rewriting links on the following HTML content: {0}'.format(_html))
return _html
def __init__(self, system, location, definition, descriptor, def __init__(self, system, location, definition, descriptor,
instance_state=None, shared_state=None, **kwargs): instance_state=None, shared_state=None, **kwargs):
......
...@@ -328,7 +328,8 @@ class XModule(HTMLSnippet): ...@@ -328,7 +328,8 @@ class XModule(HTMLSnippet):
name = link[len(XASSET_SRCREF_PREFIX):] name = link[len(XASSET_SRCREF_PREFIX):]
loc = Location(self.location) loc = Location(self.location)
# resolve the reference to our internal 'filepath' which # resolve the reference to our internal 'filepath' which
link = StaticContent.compute_location_filename(loc.org, loc.course, name) content_loc = StaticContent.compute_location(loc.org, loc.course, name)
link = StaticContent.get_url_path_from_location(content_loc)
return link return link
......
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