Everything tested and ready for Tom to fix
This commit is contained in:
148
cms/djangoapps/contentstore/course_info_model.py
Normal file
148
cms/djangoapps/contentstore/course_info_model.py
Normal file
@@ -0,0 +1,148 @@
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from lxml import etree
|
||||
import re
|
||||
from django.http import HttpResponseBadRequest
|
||||
|
||||
## TODO store as array of { date, content } and override course_info_module.definition_from_xml
|
||||
## This should be in a class which inherits from XmlDescriptor
|
||||
def get_course_updates(location):
|
||||
"""
|
||||
Retrieve the relevant course_info updates and unpack into the model which the client expects:
|
||||
[{id : location.url() + idx to make unique, date : string, content : html string}]
|
||||
"""
|
||||
try:
|
||||
course_updates = modulestore('direct').get_item(location)
|
||||
except ItemNotFoundError:
|
||||
template = Location(['i4x', 'edx', "templates", 'course_info', "Empty"])
|
||||
course_updates = modulestore('direct').clone_item(template, Location(location))
|
||||
|
||||
# current db rep: {"_id" : locationjson, "definition" : { "data" : "<ol>[<li><h2>date</h2>content</li>]</ol>"} "metadata" : ignored}
|
||||
location_base = course_updates.location.url()
|
||||
|
||||
# purely to handle free formed updates not done via editor. Actually kills them, but at least doesn't break.
|
||||
try:
|
||||
course_html_parsed = etree.fromstring(course_updates.definition['data'], etree.XMLParser(remove_blank_text=True))
|
||||
except etree.XMLSyntaxError:
|
||||
course_html_parsed = etree.fromstring("<ol></ol>")
|
||||
|
||||
# Confirm that root is <ol>, iterate over <li>, pull out <h2> subs and then rest of val
|
||||
course_upd_collection = []
|
||||
if course_html_parsed.tag == 'ol':
|
||||
# 0 is the oldest so that new ones get unique idx
|
||||
for idx, update in enumerate(course_html_parsed.iter("li")):
|
||||
if (len(update) == 0):
|
||||
continue
|
||||
elif (len(update) == 1):
|
||||
content = update.find("h2").tail
|
||||
else:
|
||||
content = etree.tostring(update[1])
|
||||
|
||||
course_upd_collection.append({"id" : location_base + "/" + str(idx),
|
||||
"date" : update.findtext("h2"),
|
||||
"content" : content})
|
||||
# return newest to oldest
|
||||
course_upd_collection.reverse()
|
||||
return course_upd_collection
|
||||
|
||||
def update_course_updates(location, update, passed_id=None):
|
||||
"""
|
||||
Either add or update the given course update. It will add it if the passed_id is absent or None. It will update it if
|
||||
it has an passed_id which has a valid value. Until updates have distinct values, the passed_id is the location url + an index
|
||||
into the html structure.
|
||||
"""
|
||||
try:
|
||||
course_updates = modulestore('direct').get_item(location)
|
||||
except ItemNotFoundError:
|
||||
return HttpResponseBadRequest
|
||||
|
||||
# purely to handle free formed updates not done via editor. Actually kills them, but at least doesn't break.
|
||||
try:
|
||||
course_html_parsed = etree.fromstring(course_updates.definition['data'], etree.XMLParser(remove_blank_text=True))
|
||||
except etree.XMLSyntaxError:
|
||||
course_html_parsed = etree.fromstring("<ol></ol>")
|
||||
|
||||
try:
|
||||
new_html_parsed = etree.fromstring(update['content'], etree.XMLParser(remove_blank_text=True))
|
||||
except etree.XMLSyntaxError:
|
||||
new_html_parsed = None
|
||||
|
||||
# Confirm that root is <ol>, iterate over <li>, pull out <h2> subs and then rest of val
|
||||
if course_html_parsed.tag == 'ol':
|
||||
# ??? Should this use the id in the json or in the url or does it matter?
|
||||
if passed_id:
|
||||
element = course_html_parsed.findall("li")[get_idx(passed_id)]
|
||||
element[0].text = update['date']
|
||||
if (len(element) == 1):
|
||||
if new_html_parsed is not None:
|
||||
element[0].tail = None
|
||||
element.append(new_html_parsed)
|
||||
else:
|
||||
element[0].tail = update['content']
|
||||
else:
|
||||
if new_html_parsed is not None:
|
||||
element[1] = new_html_parsed
|
||||
else:
|
||||
element.pop(1)
|
||||
element[0].tail = update['content']
|
||||
else:
|
||||
idx = len(course_html_parsed.findall("li"))
|
||||
passed_id = course_updates.location.url() + "/" + str(idx)
|
||||
element = etree.SubElement(course_html_parsed, "li")
|
||||
date_element = etree.SubElement(element, "h2")
|
||||
date_element.text = update['date']
|
||||
if new_html_parsed is not None:
|
||||
element[1] = new_html_parsed
|
||||
else:
|
||||
date_element.tail = update['content']
|
||||
|
||||
# update db record
|
||||
course_updates.definition['data'] = etree.tostring(course_html_parsed)
|
||||
modulestore('direct').update_item(location, course_updates.definition['data'])
|
||||
|
||||
return {"id" : passed_id,
|
||||
"date" : update['date'],
|
||||
"content" :update['content']}
|
||||
|
||||
def delete_course_update(location, update, passed_id):
|
||||
"""
|
||||
Delete the given course_info update from the db.
|
||||
Returns the resulting course_updates b/c their ids change.
|
||||
"""
|
||||
if not passed_id:
|
||||
return HttpResponseBadRequest
|
||||
|
||||
try:
|
||||
course_updates = modulestore('direct').get_item(location)
|
||||
except ItemNotFoundError:
|
||||
return HttpResponseBadRequest
|
||||
|
||||
# TODO use delete_blank_text parser throughout and cache as a static var in a class
|
||||
# purely to handle free formed updates not done via editor. Actually kills them, but at least doesn't break.
|
||||
try:
|
||||
course_html_parsed = etree.fromstring(course_updates.definition['data'], etree.XMLParser(remove_blank_text=True))
|
||||
except etree.XMLSyntaxError:
|
||||
course_html_parsed = etree.fromstring("<ol></ol>")
|
||||
|
||||
if course_html_parsed.tag == 'ol':
|
||||
# ??? Should this use the id in the json or in the url or does it matter?
|
||||
element_to_delete = course_html_parsed.xpath('/ol/li[position()=' + str(get_idx(passed_id) + 1) + "]")
|
||||
if element_to_delete:
|
||||
course_html_parsed.remove(element_to_delete[0])
|
||||
|
||||
# update db record
|
||||
course_updates.definition['data'] = etree.tostring(course_html_parsed)
|
||||
store = modulestore('direct')
|
||||
store.update_item(location, course_updates.definition['data'])
|
||||
|
||||
return get_course_updates(location)
|
||||
|
||||
def get_idx(passed_id):
|
||||
"""
|
||||
From the url w/ idx appended, get the idx.
|
||||
"""
|
||||
# TODO compile this regex into a class static and reuse for each call
|
||||
idx_matcher = re.search(r'.*/(\d)+$', passed_id)
|
||||
if idx_matcher:
|
||||
return int(idx_matcher.group(1))
|
||||
@@ -3,6 +3,6 @@
|
||||
"/static/js/vendor/jquery.min.js",
|
||||
"/static/js/vendor/json2.js",
|
||||
"/static/js/vendor/underscore-min.js",
|
||||
"/static/js/vendor/backbone-min.js"
|
||||
"/static/js/vendor/backbone.js"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<li>
|
||||
<!-- FIXME what style should we use for initially hidden? --> <!-- TODO decide whether this should use codemirror -->
|
||||
<form class="new-update-form">
|
||||
<div class="row">
|
||||
<label class="inline-label">Date:</label>
|
||||
<!-- TODO replace w/ date widget and actual date (problem is that persisted version is "Month day" not an actual date obj -->
|
||||
<input type="text" id="date-entry"
|
||||
value="<%= updateModel.get('date') %>"></input>
|
||||
</div>
|
||||
<div class="row">
|
||||
<textarea class="new-update-content text-editor"><%= updateModel.get('content') %></textarea>
|
||||
</div>
|
||||
<div class="row">
|
||||
<!-- cid rather than id b/c new ones have cid's not id's -->
|
||||
<a href="#" class="save-button" name="<%= updateModel.cid %>">Save</a>
|
||||
<a href="#" class="cancel-button" name="<%= updateModel.cid %>">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
<h2>
|
||||
<span class="calendar-icon"></span><span id="date-display"><%=
|
||||
updateModel.get('date') %></span>
|
||||
</h2>
|
||||
<div class="update-contents"><%= updateModel.get('content') %></div>
|
||||
<div class="row">
|
||||
<a href="#" class="edit-button" name="<%- updateModel.cid %>">Edit</a>
|
||||
<a href="#" class="delete-button" name="<%- updateModel.cid %>"">Delete</a>
|
||||
</div>
|
||||
</li>
|
||||
14
cms/static/coffee/src/client_templates/load_templates.html
Normal file
14
cms/static/coffee/src/client_templates/load_templates.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<!-- In order to enable better debugging of templates, put them in
|
||||
the script tag section.
|
||||
TODO add lazy load fn to load templates as needed (called
|
||||
from backbone view initialize to set this.template of the view)
|
||||
-->
|
||||
|
||||
<%block name="jsextra">
|
||||
<script type="text/javascript" charset="utf-8">
|
||||
// How do I load an html file server side so I can
|
||||
// Precompiling your templates can be a big help when debugging errors you can't reproduce. This is because precompiled templates can provide line numbers and a stack trace, something that is not possible when compiling templates on the client. The source property is available on the compiled template function for easy precompilation.
|
||||
// <script>CMS.course_info_update = <%= _.template(jstText).source %>;</script>
|
||||
|
||||
</script>
|
||||
</%block>
|
||||
@@ -1,63 +0,0 @@
|
||||
## Derived from and should inherit from a common ancestor w/ ModuleEdit
|
||||
class CMS.Views.CourseInfoEdit extends Backbone.View
|
||||
tagName: 'div'
|
||||
className: 'component'
|
||||
|
||||
events:
|
||||
"click .component-editor .cancel-button": 'clickCancelButton'
|
||||
"click .component-editor .save-button": 'clickSaveButton'
|
||||
"click .component-actions .edit-button": 'clickEditButton'
|
||||
"click .component-actions .delete-button": 'onDelete'
|
||||
|
||||
initialize: ->
|
||||
@render()
|
||||
|
||||
$component_editor: => @$el.find('.component-editor')
|
||||
|
||||
loadDisplay: ->
|
||||
XModule.loadModule(@$el.find('.xmodule_display'))
|
||||
|
||||
loadEdit: ->
|
||||
if not @module
|
||||
@module = XModule.loadModule(@$el.find('.xmodule_edit'))
|
||||
|
||||
# don't show metadata (deprecated for course_info)
|
||||
render: ->
|
||||
if @model.id
|
||||
@$el.load("/preview_component/#{@model.id}", =>
|
||||
@loadDisplay()
|
||||
@delegateEvents()
|
||||
)
|
||||
|
||||
clickSaveButton: (event) =>
|
||||
event.preventDefault()
|
||||
data = @module.save()
|
||||
@model.save(data).done( =>
|
||||
# # showToastMessage("Your changes have been saved.", null, 3)
|
||||
@module = null
|
||||
@render()
|
||||
@$el.removeClass('editing')
|
||||
).fail( ->
|
||||
showToastMessage("There was an error saving your changes. Please try again.", null, 3)
|
||||
)
|
||||
|
||||
clickCancelButton: (event) ->
|
||||
event.preventDefault()
|
||||
@$el.removeClass('editing')
|
||||
@$component_editor().slideUp(150)
|
||||
|
||||
clickEditButton: (event) ->
|
||||
event.preventDefault()
|
||||
@$el.addClass('editing')
|
||||
@$component_editor().slideDown(150)
|
||||
@loadEdit()
|
||||
|
||||
onDelete: (event) ->
|
||||
# clear contents, don't delete
|
||||
@model.definition.data = "<ol></ol>"
|
||||
# TODO change label to 'clear'
|
||||
|
||||
onNew: (event) ->
|
||||
ele = $(@model.definition.data).find("ol")
|
||||
if (ele)
|
||||
ele = $(ele).first().prepend("<li><h2>" + $.datepicker.formatDate('MM d', new Date()) + "</h2>/n</li>");
|
||||
34
cms/static/js/models/course_info.js
Normal file
34
cms/static/js/models/course_info.js
Normal file
@@ -0,0 +1,34 @@
|
||||
// single per course holds the updates and handouts
|
||||
CMS.Models.CourseInfo = Backbone.Model.extend({
|
||||
// This model class is not suited for restful operations and is considered just a server side initialized container
|
||||
url: '',
|
||||
|
||||
defaults: {
|
||||
"courseId": "", // the location url
|
||||
"updates" : null, // UpdateCollection
|
||||
"handouts": null // HandoutCollection
|
||||
},
|
||||
|
||||
idAttribute : "courseId"
|
||||
});
|
||||
|
||||
// course update -- biggest kludge here is the lack of a real id to map updates to originals
|
||||
CMS.Models.CourseUpdate = Backbone.Model.extend({
|
||||
defaults: {
|
||||
"date" : $.datepicker.formatDate('MM d', new Date()),
|
||||
"content" : ""
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
The intitializer of this collection must set id to the update's location.url and courseLocation to the course's location. Must pass the
|
||||
collection of updates as [{ date : "month day", content : "html"}]
|
||||
*/
|
||||
CMS.Models.CourseUpdateCollection = Backbone.Collection.extend({
|
||||
url : function() {return this.urlbase + "course_info/updates/";},
|
||||
|
||||
model : CMS.Models.CourseUpdate
|
||||
});
|
||||
|
||||
|
||||
|
||||
77
cms/static/js/template_loader.js
Normal file
77
cms/static/js/template_loader.js
Normal file
@@ -0,0 +1,77 @@
|
||||
// <!-- from https://github.com/Gazler/Underscore-Template-Loader/blob/master/index.html -->
|
||||
// TODO Figure out how to initialize w/ static views from server (don't call .load but instead inject in django as strings)
|
||||
// so this only loads the lazily loaded ones.
|
||||
(function() {
|
||||
if (typeof window.templateLoader == 'function') return;
|
||||
|
||||
var templateLoader = {
|
||||
templateVersion: "0.0.3",
|
||||
templates: {},
|
||||
loadRemoteTemplate: function(templateName, filename, callback) {
|
||||
if (!this.templates[templateName]) {
|
||||
var self = this;
|
||||
jQuery.ajax({url : filename,
|
||||
success : function(data) {
|
||||
self.addTemplate(templateName, data);
|
||||
self.saveLocalTemplates();
|
||||
callback(data);
|
||||
},
|
||||
error : function(xhdr, textStatus, errorThrown) {
|
||||
console.log(textStatus); },
|
||||
dataType : "html"
|
||||
})
|
||||
}
|
||||
else {
|
||||
callback(this.templates[templateName]);
|
||||
}
|
||||
},
|
||||
|
||||
addTemplate: function(templateName, data) {
|
||||
// is there a reason this doesn't go ahead and compile the template? _.template(data)
|
||||
// I suppose localstorage use would still req raw string rather than compiled version, but that sd work
|
||||
// if it maintains a separate cache of uncompiled ones
|
||||
this.templates[templateName] = data;
|
||||
},
|
||||
|
||||
localStorageAvailable: function() {
|
||||
try {
|
||||
return 'localStorage' in window && window['localStorage'] !== null;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
saveLocalTemplates: function() {
|
||||
if (this.localStorageAvailable) {
|
||||
localStorage.setItem("templates", JSON.stringify(this.templates));
|
||||
localStorage.setItem("templateVersion", this.templateVersion);
|
||||
}
|
||||
},
|
||||
|
||||
loadLocalTemplates: function() {
|
||||
if (this.localStorageAvailable) {
|
||||
var templateVersion = localStorage.getItem("templateVersion");
|
||||
if (templateVersion && templateVersion == this.templateVersion) {
|
||||
var templates = localStorage.getItem("templates");
|
||||
if (templates) {
|
||||
templates = JSON.parse(templates);
|
||||
for (var x in templates) {
|
||||
if (!this.templates[x]) {
|
||||
this.addTemplate(x, templates[x]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
localStorage.removeItem("templates");
|
||||
localStorage.removeItem("templateVersion");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
};
|
||||
templateLoader.loadLocalTemplates();
|
||||
window.templateLoader = templateLoader;
|
||||
})();
|
||||
138
cms/static/js/views/course_info_edit.js
Normal file
138
cms/static/js/views/course_info_edit.js
Normal file
@@ -0,0 +1,138 @@
|
||||
/* this view should own everything on the page which has controls effecting its operation
|
||||
generate other views for the individual editors.
|
||||
The render here adds views for each update/handout by delegating to their collections but does not
|
||||
generate any html for the surrounding page.
|
||||
*/
|
||||
CMS.Views.CourseInfoEdit = Backbone.View.extend({
|
||||
// takes CMS.Models.CourseInfo as model
|
||||
tagName: 'div',
|
||||
|
||||
render: function() {
|
||||
// instantiate the ClassInfoUpdateView and delegate the proper dom to it
|
||||
new CMS.Views.ClassInfoUpdateView({
|
||||
el: this.$('#course-update-view'),
|
||||
collection: this.model.get('updates')
|
||||
});
|
||||
// TODO instantiate the handouts view
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
// ??? Programming style question: should each of these classes be in separate files?
|
||||
CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
|
||||
// collection is CourseUpdateCollection
|
||||
events: {
|
||||
"click .new-update-button" : "onNew",
|
||||
"click .save-button" : "onSave",
|
||||
"click .cancel-button" : "onCancel",
|
||||
"click .edit-button" : "onEdit",
|
||||
"click .delete-button" : "onDelete"
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
var self = this;
|
||||
// instantiates an editor template for each update in the collection
|
||||
window.templateLoader.loadRemoteTemplate("course_info_update",
|
||||
// TODO Where should the template reside? how to use the static.url to create the path?
|
||||
"/static/coffee/src/client_templates/course_info_update.html",
|
||||
function (raw_template) {
|
||||
self.template = _.template(raw_template);
|
||||
self.render();
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
// iterate over updates and create views for each using the template
|
||||
var updateEle = this.$el.find("#course-update-list");
|
||||
// remove and then add all children
|
||||
$(updateEle).empty();
|
||||
var self = this;
|
||||
this.collection.each(function (update) {
|
||||
var newEle = self.template({ updateModel : update });
|
||||
$(updateEle).append(newEle);
|
||||
});
|
||||
this.$el.find(".new-update-form").hide();
|
||||
return this;
|
||||
},
|
||||
|
||||
onNew: function(event) {
|
||||
// create new obj, insert into collection, and render this one ele overriding the hidden attr
|
||||
var newModel = new CMS.Models.CourseUpdate();
|
||||
this.collection.add(newModel, {at : 0});
|
||||
|
||||
var newForm = this.template({ updateModel : newModel });
|
||||
var updateEle = this.$el.find("#course-update-list");
|
||||
$(updateEle).append(newForm);
|
||||
$(newForm).find(".new-update-form").show();
|
||||
},
|
||||
|
||||
onSave: function(event) {
|
||||
var targetModel = this.eventModel(event);
|
||||
targetModel.set({ date : this.dateEntry(event).val(), content : this.contentEntry(event).val() });
|
||||
// push change to display, hide the editor, submit the change
|
||||
$(this.dateDisplay(event)).val(targetModel.get('date'));
|
||||
$(this.contentDisplay(event)).val(targetModel.get('content'));
|
||||
$(this.editor(event)).hide();
|
||||
|
||||
targetModel.save();
|
||||
},
|
||||
|
||||
onCancel: function(event) {
|
||||
// change editor contents back to model values and hide the editor
|
||||
$(this.editor(event)).hide();
|
||||
var targetModel = this.eventModel(event);
|
||||
$(this.dateEntry(event)).val(targetModel.get('date'));
|
||||
$(this.contentEntry(event)).val(targetModel.get('content'));
|
||||
},
|
||||
|
||||
onEdit: function(event) {
|
||||
$(this.editor(event)).show();
|
||||
},
|
||||
|
||||
onDelete: function(event) {
|
||||
// TODO ask for confirmation
|
||||
// remove the dom element and delete the model
|
||||
var targetModel = this.eventModel(event);
|
||||
this.modelDom(event).remove();
|
||||
var cacheThis = this;
|
||||
targetModel.destroy({success : function (model, response) {
|
||||
cacheThis.collection.fetch({success : function() {cacheThis.render();}});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Dereferencing from events to screen elements
|
||||
eventModel: function(event) {
|
||||
// not sure if it should be currentTarget or delegateTarget
|
||||
return this.collection.getByCid($(event.currentTarget).attr("name"));
|
||||
},
|
||||
|
||||
modelDom: function(event) {
|
||||
return $(event.currentTarget).closest("li");
|
||||
},
|
||||
|
||||
editor: function(event) {
|
||||
var li = $(event.currentTarget).closest("li");
|
||||
if (li) return $(li).find("form").first();
|
||||
},
|
||||
|
||||
dateEntry: function(event) {
|
||||
var li = $(event.currentTarget).closest("li");
|
||||
if (li) return $(li).find("#date-entry").first();
|
||||
},
|
||||
|
||||
contentEntry: function(event) {
|
||||
return $(event.currentTarget).closest("li").find(".new-update-content").first();
|
||||
},
|
||||
|
||||
dateDisplay: function(event) {
|
||||
return $(event.currentTarget).closest("li").find("#date-display").first();
|
||||
},
|
||||
|
||||
contentDisplay: function(event) {
|
||||
return $(event.currentTarget).closest("li").find(".update-contents").first();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@@ -1,16 +1,31 @@
|
||||
<%inherit file="base.html" />
|
||||
<%namespace name='static' file='static_content.html'/>
|
||||
|
||||
<!-- TODO decode course # from context_course into title -->
|
||||
<%block name="title">Course Info</%block>
|
||||
|
||||
<%block name="jsextra">
|
||||
<script type="text/javascript" src="${static.url('js/template_loader.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/models/course_info.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/views/course_info_edit.js')}"></script>
|
||||
|
||||
<script type="text/javascript" charset="utf-8">
|
||||
$(document).ready(function(){
|
||||
editor = new CMS.Views.CourseInfoEdit({
|
||||
el: $('.course-updates'),
|
||||
model : new CMS.Models.Module({id : '${course_updates.location.url()}'})
|
||||
});
|
||||
$(".new-update-button").bind('click', editor.onNew);
|
||||
});
|
||||
$(document).ready(function(){
|
||||
|
||||
var course_updates = new CMS.Models.CourseUpdateCollection();
|
||||
course_updates.reset(${course_updates|n});
|
||||
course_updates.urlbase = '${url_base}';
|
||||
|
||||
var editor = new CMS.Views.CourseInfoEdit({
|
||||
el: $('.main-wrapper'),
|
||||
model : new CMS.Models.CourseInfo({
|
||||
courseId : '${context_course.location}',
|
||||
updates : course_updates,
|
||||
// FIXME add handouts
|
||||
handouts : null})
|
||||
});
|
||||
editor.render();
|
||||
});
|
||||
</script>
|
||||
</%block>
|
||||
|
||||
@@ -19,10 +34,10 @@ $(document).ready(function(){
|
||||
<div class="inner-wrapper">
|
||||
<h1>Course Info</h1>
|
||||
<div class="main-column">
|
||||
<div class="window">
|
||||
<div class="unit-body window" id="course-update-view">
|
||||
<h2>Updates</h2>
|
||||
<a href="#" class="new-update-button">New Update</a>
|
||||
<div class="course-updates"></div>
|
||||
<ol class="update-list" id="course-update-list"></ol>
|
||||
<!-- probably replace w/ a vertical where each element of the vertical is a separate update w/ a date and html field -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
metadata:
|
||||
display_name: Empty
|
||||
data: "<p>This is where you can add additional information about your course.</p>"
|
||||
data: "<ol></ol>"
|
||||
children: []
|
||||
Reference in New Issue
Block a user