Commit cf2f5d2a by Tom Giannattasio

Merge branch 'master' into feature/tomg/fall-design

parents 163eb9d4 d084506d
......@@ -185,7 +185,7 @@ def choicegroup(element, value, status, render_template, msg=''):
if choice.text is not None:
ctext += choice.text # TODO: fix order?
choices.append((choice.get("name"), ctext))
context = {'id': eid, 'value': value, 'state': status, 'input_type': type, 'choices': choices, 'inline': True, 'name_array_suffix': ''}
context = {'id': eid, 'value': value, 'state': status, 'input_type': type, 'choices': choices, 'name_array_suffix': ''}
html = render_template("choicegroup.html", context)
return etree.XML(html)
......@@ -226,7 +226,7 @@ def radiogroup(element, value, status, render_template, msg=''):
choices = extract_choices(element)
context = {'id': eid, 'value': value, 'state': status, 'input_type': 'radio', 'choices': choices, 'inline': False, 'name_array_suffix': '[]'}
context = {'id': eid, 'value': value, 'state': status, 'input_type': 'radio', 'choices': choices, 'name_array_suffix': '[]'}
html = render_template("choicegroup.html", context)
return etree.XML(html)
......@@ -244,7 +244,7 @@ def checkboxgroup(element, value, status, render_template, msg=''):
choices = extract_choices(element)
context = {'id': eid, 'value': value, 'state': status, 'input_type': 'checkbox', 'choices': choices, 'inline': False, 'name_array_suffix': '[]'}
context = {'id': eid, 'value': value, 'state': status, 'input_type': 'checkbox', 'choices': choices, 'name_array_suffix': '[]'}
html = render_template("choicegroup.html", context)
return etree.XML(html)
......
......@@ -6,9 +6,6 @@
checked="true"
% endif
/> ${choice_description} </label>
% if not inline:
<br/>
% endif
% endfor
<span id="answer_${id}"></span>
......
......@@ -8,6 +8,8 @@ import functools
import comment_client as cc
import django_comment_client.utils as utils
import django_comment_client.settings as cc_settings
from django.core import exceptions
from django.contrib.auth.decorators import login_required
......@@ -15,13 +17,11 @@ from django.views.decorators.http import require_POST, require_GET
from django.views.decorators import csrf
from django.core.files.storage import get_storage_class
from django.utils.translation import ugettext as _
from django.conf import settings
from django.contrib.auth.models import User
from mitxmako.shortcuts import render_to_response, render_to_string
from courseware.courses import get_course_with_access
from django_comment_client.utils import JsonResponse, JsonError, extract
from django_comment_client.permissions import check_permissions_by_view
......@@ -115,6 +115,9 @@ def _create_comment(request, course_id, thread_id=None, parent_id=None):
@login_required
@permitted
def create_comment(request, course_id, thread_id):
if cc_settings.MAX_COMMENT_DEPTH is not None:
if cc_settings.MAX_COMMENT_DEPTH < 0:
return JsonError("Comment level too deep")
return _create_comment(request, course_id, thread_id=thread_id)
@require_POST
......@@ -159,6 +162,9 @@ def openclose_thread(request, course_id, thread_id):
@login_required
@permitted
def create_sub_comment(request, course_id, comment_id):
if cc_settings.MAX_COMMENT_DEPTH is not None:
if cc_settings.MAX_COMMENT_DEPTH <= cc.Comment.find(comment_id).depth:
return JsonError("Comment level too deep")
return _create_comment(request, course_id, parent_id=comment_id)
@require_POST
......@@ -282,7 +288,7 @@ def update_moderator_status(request, course_id, user_id):
'course_id': course_id,
'user': request.user,
'django_user': user,
'discussion_user': discussion_user.to_dict(),
'profiled_user': discussion_user.to_dict(),
}
return JsonResponse({
'html': render_to_string('discussion/ajax_user_profile.html', context)
......@@ -298,10 +304,13 @@ def search_similar_threads(request, course_id, commentable_id):
'text': text,
'commentable_id': commentable_id,
}
result = cc.search_similar_threads(course_id, recursive=False, query_params=query_params)
return JsonResponse(result)
threads = cc.search_similar_threads(course_id, recursive=False, query_params=query_params)
else:
return JsonResponse([])
theads = []
context = { 'threads': map(utils.extend_content, threads) }
return JsonResponse({
'html': render_to_string('discussion/_similar_posts.html', context)
})
@require_GET
def tags_autocomplete(request, course_id):
......@@ -334,8 +343,8 @@ def upload(request, course_id):#ajax upload file to a question or answer
# check file type
f = request.FILES['file-upload']
file_extension = os.path.splitext(f.name)[1].lower()
if not file_extension in settings.DISCUSSION_ALLOWED_UPLOAD_FILE_TYPES:
file_types = "', '".join(settings.DISCUSSION_ALLOWED_UPLOAD_FILE_TYPES)
if not file_extension in cc_settings.ALLOWED_UPLOAD_FILE_TYPES:
file_types = "', '".join(cc_settings.ALLOWED_UPLOAD_FILE_TYPES)
msg = _("allowed file types are '%(file_types)s'") % \
{'file_types': file_types}
raise exceptions.PermissionDenied(msg)
......@@ -354,15 +363,16 @@ def upload(request, course_id):#ajax upload file to a question or answer
# check file size
# byte
size = file_storage.size(new_file_name)
if size > settings.ASKBOT_MAX_UPLOAD_FILE_SIZE:
if size > cc_settings.MAX_UPLOAD_FILE_SIZE:
file_storage.delete(new_file_name)
msg = _("maximum upload file size is %(file_size)sK") % \
{'file_size': settings.ASKBOT_MAX_UPLOAD_FILE_SIZE}
{'file_size': cc_settings.MAX_UPLOAD_FILE_SIZE}
raise exceptions.PermissionDenied(msg)
except exceptions.PermissionDenied, e:
error = unicode(e)
except Exception, e:
print e
logging.critical(unicode(e))
error = _('Error uploading file. Please contact the site administrator. Thank you.')
......
......@@ -83,7 +83,7 @@ def render_discussion(request, course_id, threads, *args, **kwargs):
'base_url': base_url,
'query_params': strip_blank(strip_none(extract(query_params, ['page', 'sort_key', 'sort_order', 'tags', 'text']))),
'annotated_content_info': json.dumps(annotated_content_info),
'discussion_data': json.dumps({ discussion_id: threads }),
'discussion_data': json.dumps({ (discussion_id or user_id): threads })
}
context = dict(context.items() + query_params.items())
return render_to_string(template, context)
......@@ -250,7 +250,10 @@ def user_profile(request, course_id, user_id):
content = render_user_discussion(request, course_id, threads, user_id=user_id, query_params=query_params)
if request.is_ajax():
return utils.HtmlResponse(content)
return utils.JsonResponse({
'html': content,
'discussionData': threads,
})
else:
context = {
'course': course,
......
......@@ -6,13 +6,14 @@ from django.core.urlresolvers import reverse
from functools import partial
from utils import *
import django_comment_client.settings as cc_settings
import pystache_custom as pystache
import urllib
import os
def pluralize(singular_term, count):
if int(count) >= 2:
if int(count) >= 2 or int(count) == 0:
return singular_term + 's'
return singular_term
......@@ -33,26 +34,19 @@ def include_mustache_templates():
file_contents = map(read_file, filter(valid_file_name, os.listdir(mustache_dir)))
return '\n'.join(map(wrap_in_tag, map(strip_file_name, file_contents)))
def permalink(content):
if content['type'] == 'thread':
return reverse('django_comment_client.forum.views.single_thread',
args=[content['course_id'], content['commentable_id'], content['id']])
else:
return reverse('django_comment_client.forum.views.single_thread',
args=[content['course_id'], content['commentable_id'], content['thread_id']]) + '#' + content['id']
def render_content(content, additional_context={}):
content_info = {
'displayed_title': content.get('highlighted_title') or content.get('title', ''),
'displayed_body': content.get('highlighted_body') or content.get('body', ''),
'raw_tags': ','.join(content.get('tags', [])),
'permalink': permalink(content),
}
context = {
'content': merge_dict(content, content_info),
'content': extend_content(content),
content['type']: True,
}
if cc_settings.MAX_COMMENT_DEPTH is not None:
if content['type'] == 'thread':
if cc_settings.MAX_COMMENT_DEPTH < 0:
context['max_depth'] = True
elif content['type'] == 'comment':
if cc_settings.MAX_COMMENT_DEPTH <= content['depth']:
context['max_depth'] = True
context = merge_dict(context, additional_context)
partial_mustache_helpers = {k: partial(v, content) for k, v in mustache_helpers.items()}
context = merge_dict(context, partial_mustache_helpers)
......
......@@ -7,7 +7,8 @@ import inspect
def pluralize(content, text):
num, word = text.split(' ')
if int(num or '0') >= 2:
num = int(num or '0')
if num >= 2 or num == 0:
return word + 's'
else:
return word
......
from django.conf import settings
MAX_COMMENT_DEPTH = None
MAX_UPLOAD_FILE_SIZE = 1024 * 1024 #result in bytes
ALLOWED_UPLOAD_FILE_TYPES = ('.jpg', '.jpeg', '.gif', '.bmp', '.png', '.tiff')
if hasattr(settings, 'DISCUSSION_SETTINGS'):
MAX_COMMENT_DEPTH = settings.DISCUSSION_SETTINGS.get('MAX_COMMENT_DEPTH')
MAX_UPLOAD_FILE_SIZE = settings.DISCUSSION_SETTINGS.get('MAX_UPLOAD_FILE_SIZE') or MAX_UPLOAD_FILE_SIZE
ALLOWED_UPLOAD_FILE_TYPES = settings.DISCUSSION_SETTINGS.get('ALLOWED_UPLOAD_FILE_TYPES') or ALLOWED_UPLOAD_FILE_TYPES
......@@ -21,6 +21,8 @@ import pystache_custom as pystache
_FULLMODULES = None
_DISCUSSIONINFO = None
def extract(dic, keys):
return {k: dic.get(k) for k in keys}
......@@ -197,3 +199,20 @@ def url_for_tags(course_id, tags):
def render_mustache(template_name, dictionary, *args, **kwargs):
template = middleware.lookup['main'].get_template(template_name).source
return pystache.render(template, dictionary)
def permalink(content):
if content['type'] == 'thread':
return reverse('django_comment_client.forum.views.single_thread',
args=[content['course_id'], content['commentable_id'], content['id']])
else:
return reverse('django_comment_client.forum.views.single_thread',
args=[content['course_id'], content['commentable_id'], content['thread_id']]) + '#' + content['id']
def extend_content(content):
content_info = {
'displayed_title': content.get('highlighted_title') or content.get('title', ''),
'displayed_body': content.get('highlighted_body') or content.get('body', ''),
'raw_tags': ','.join(content.get('tags', [])),
'permalink': permalink(content),
}
return merge_dict(content, content_info)
......@@ -68,4 +68,4 @@ if 'COURSE_ID' in ENV_TOKENS:
ASKBOT_URL = "courses/{0}/discussions/".format(ENV_TOKENS['COURSE_ID'])
COMMENTS_SERVICE_URL = ENV_TOKENS["COMMENTS_SERVICE_URL"]
COMMENTS_SERVICE_KEY = ENV_TOKENS["COMMENTS_SERVICE_KEY"]
......@@ -38,6 +38,10 @@ ASKBOT_ENABLED = False
GENERATE_RANDOM_USER_CREDENTIALS = False
PERFSTATS = False
DISCUSSION_SETTINGS = {
'MAX_COMMENT_DEPTH': 2,
}
# Features
MITX_FEATURES = {
'SAMPLE' : False,
......
......@@ -92,6 +92,8 @@ SUBDOMAIN_BRANDING = {
'harvard': 'HarvardX',
}
COMMENTS_SERVICE_KEY = "PUT_YOUR_API_KEY_HERE"
################################ LMS Migration #################################
MITX_FEATURES['ENABLE_LMS_MIGRATION'] = True
MITX_FEATURES['ACCESS_REQUIRE_STAFF_FOR_COURSE'] = False # require that user be in the staff_* group to be able to enroll
......
......@@ -7,4 +7,7 @@ else:
PREFIX = SERVICE_HOST + '/api/v1'
API_KEY = "PUT_YOUR_API_KEY_HERE"
if hasattr(settings, "COMMENTS_SERVICE_KEY"):
API_KEY = settings.COMMENTS_SERVICE_KEY
else:
API_KEY = "PUT_YOUR_API_KEY_HERE"
class @DiscussionModuleView extends Backbone.View
events:
"click .discussion-show": "toggleDiscussion"
toggleDiscussion: (event) ->
if @showed
@$("section.discussion").hide()
$(event.target).html("Show Discussion")
@showed = false
else
if @retrieved
@$("section.discussion").show()
$(event.target).html("Hide Discussion")
@showed = true
if Backbone?
class @DiscussionModuleView extends Backbone.View
events:
"click .discussion-show": "toggleDiscussion"
toggleDiscussion: (event) ->
if @showed
@$("section.discussion").hide()
$(event.target).html("Show Discussion")
@showed = false
else
$elem = $(event.target)
discussion_id = $elem.attr("discussion_id")
url = DiscussionUtil.urlFor 'retrieve_discussion', discussion_id
Discussion.safeAjax
$elem: $elem
url: url
type: "GET"
dataType: 'json'
success: (response, textStatus) =>
@$el.append(response.html)
$discussion = @$el.find("section.discussion")
$(event.target).html("Hide Discussion")
discussion = new Discussion()
discussion.reset(response.discussionData, {silent: false})
view = new DiscussionView(el: $discussion[0], model: discussion)
DiscussionUtil.bulkUpdateContentInfo(window.$$annotated_content_info)
@retrieved = true
@showed = true
if @retrieved
@$("section.discussion").show()
$(event.target).html("Hide Discussion")
@showed = true
else
$elem = $(event.target)
discussion_id = $elem.attr("discussion_id")
url = DiscussionUtil.urlFor 'retrieve_discussion', discussion_id
DiscussionUtil.safeAjax
$elem: $elem
$loading: $elem
url: url
type: "GET"
dataType: 'json'
success: (response, textStatus) =>
@$el.append(response.html)
$discussion = @$el.find("section.discussion")
$(event.target).html("Hide Discussion")
discussion = new Discussion()
discussion.reset(response.discussionData, {silent: false})
view = new DiscussionView(el: $discussion[0], model: discussion)
DiscussionUtil.bulkUpdateContentInfo(window.$$annotated_content_info)
@retrieved = true
@showed = true
......@@ -12,4 +12,10 @@ $ ->
discussion.reset(discussionData, {silent: false})
view = new DiscussionView(el: elem, model: discussion)
DiscussionUtil.bulkUpdateContentInfo(window.$$annotated_content_info)
if window.$$annotated_content_info?
DiscussionUtil.bulkUpdateContentInfo(window.$$annotated_content_info)
$userProfile = $(".discussion-sidebar>.user-profile")
if $userProfile.length
console.log "initialize user profile"
view = new DiscussionUserProfileView(el: $userProfile[0])
if not @Discussion?
@Discussion = {}
class @DiscussionUserProfileView extends Backbone.View
toggleModeratorStatus: (event) ->
confirmValue = confirm("Are you sure?")
if not confirmValue then return
$elem = $(event.target)
if $elem.hasClass("sidebar-promote-moderator-button")
isModerator = true
else if $elem.hasClass("sidebar-revoke-moderator-button")
isModerator = false
else
console.error "unrecognized moderator status"
return
url = DiscussionUtil.urlFor('update_moderator_status', $$profiled_user_id)
DiscussionUtil.safeAjax
$elem: $elem
url: url
type: "POST"
dataType: 'json'
data:
is_moderator: isModerator
error: (response, textStatus, e) ->
console.log e
success: (response, textStatus) =>
parent = @$el.parent()
@$el.replaceWith(response.html)
view = new DiscussionUserProfileView el: parent.children(".user-profile")
Discussion = @Discussion
@Discussion = $.extend @Discussion,
initializeUserProfile: ($userProfile) ->
$local = Discussion.generateLocal $userProfile
handleUpdateModeratorStatus = (elem, isModerator) ->
confirmValue = confirm("Are you sure?")
if not confirmValue then return
url = Discussion.urlFor('update_moderator_status', $$profiled_user_id)
Discussion.safeAjax
$elem: $(elem)
url: url
type: "POST"
dataType: 'json'
data:
is_moderator: isModerator
error: (response, textStatus, e) ->
console.log e
success: (response, textStatus) ->
parent = $userProfile.parent()
$userProfile.replaceWith(response.html)
Discussion.initializeUserProfile parent.children(".user-profile")
Discussion.bindLocalEvents $local,
"click .sidebar-revoke-moderator-button": (event) ->
handleUpdateModeratorStatus(this, false)
"click .sidebar-promote-moderator-button": (event) ->
handleUpdateModeratorStatus(this, true)
initializeUserActiveDiscussion: ($discussion) ->
events:
"click .sidebar-toggle-moderator-button": "toggleModeratorStatus"
$ ->
$.fn.extend
loading: ->
$(this).after("<span class='discussion-loading'></span>")
loaded: ->
$(this).parent().children(".discussion-loading").remove()
class @DiscussionUtil
@wmdEditors: {}
......@@ -62,9 +69,16 @@ class @DiscussionUtil
$elem = params.$elem
if $elem.attr("disabled")
return
$elem.attr("disabled", "disabled")
params["beforeSend"] = ->
$elem.attr("disabled", "disabled")
if params["$loading"]
console.log "loading"
params["$loading"].loading()
$.ajax(params).always ->
$elem.removeAttr("disabled")
if params["$loading"]
console.log "loaded"
params["$loading"].loaded()
@get: ($elem, url, data, success) ->
@safeAjax
......
......@@ -35,7 +35,13 @@ $tag-text-color: #5b614f;
}
}
.discussion-loading {
background-image: url(../images/discussion/loading.gif);
width: 15px;
height: 15px;
margin-left: 2px;
display: inline-block;
}
/*** Discussions ***/
......@@ -49,8 +55,6 @@ $tag-text-color: #5b614f;
margin-top: 0;
}
/*** Sidebar ***/
.sidebar-module {
......
......@@ -21,6 +21,12 @@
@import 'course/courseware/sidebar';
@import 'course/courseware/amplifier';
// course-specific courseware (all styles in these files should be gated by a
// course-specific class). This should be replaced with a better way of
// providing course-specific styling.
@import "course/courseware/courses/_cs188.scss";
// wiki
@import "course/wiki/basic-html";
@import "course/wiki/sidebar";
......
body.cs188 {
.course-content{
.project {
ul, ol {
margin-top: 3px;
list-style: disc;
ul, ol {
margin: 0px;
}
}
}
h3, h4 {
font-weight: bold;
a {
color: inherit;
}
}
h4 {
font-size: 1em;
}
p, .code_snippet {
margin-bottom: 1.416em;
}
}
}
......@@ -22,6 +22,7 @@
## <script type="text/javascript" src="${static.url('js/vendor/CodeMirror-2.25/mode/python/python.js')}"></script>
<%static:js group='courseware'/>
<%static:js group='discussion'/>
<%include file="../discussion/_js_body_dependencies.html" />
......
<%! import django_comment_client.helpers as helpers %>
<%def name="render_content(content)">
${helpers.render_content(content)}
<%def name="render_content(content, *args, **kwargs)">
${helpers.render_content(content, *args, **kwargs)}
</%def>
<%def name="render_content_with_comments(content)">
<%def name="render_content_with_comments(content, *args, **kwargs)">
<div class="${content['type']}${helpers.show_if(' endorsed', content.get('endorsed'))}" _id="${content['id']}" _discussion_id="${content.get('commentable_id', '')}" _author_id="${helpers.show_if(content['user_id'], not content.get('anonymous'))}">
${render_content(content)}
${render_comments(content.get('children', []))}
${render_content(content, *args, **kwargs)}
${render_comments(content.get('children', []), *args, **kwargs)}
</div>
</%def>
<%def name="render_comments(comments)">
<%def name="render_comments(comments, *args, **kwargs)">
<div class="comments">
% for comment in comments:
${render_content_with_comments(comment)}
${render_content_with_comments(comment, *args, **kwargs)}
% endfor
</div>
</%def>
% if len(threads) > 0:
Similar Posts:
<a class="hide-similar-posts" href="javascript:void(0)">Hide</a>
<div class="new-post-similar-posts">
% for thread in threads:
<a class="similar-post" href="${thread['permalink']}">${thread['title']}</a>
% endfor
</div>
% endif
<%namespace name="renderer" file="_thread.html"/>
<%namespace name="renderer" file="_content_renderer.html"/>
<section class="discussion user-active-discussion">
<section class="discussion user-active-discussion" _id="${user_id}">
<div class="discussion-non-content discussion-local"></div>
<div class="discussion-non-content local"></div>
<div class="threads">
% for thread in threads:
${renderer.render_thread(course_id, thread, show_comments=True)}
${renderer.render_content_with_comments(thread, {'partial_comments': True})}
% endfor
</div>
......
<%! from django_comment_client.utils import pluralize %>
<%! from django_comment_client.helpers import pluralize %>
<%! from django_comment_client.permissions import has_permission, check_permissions_by_view %>
<%! from operator import attrgetter %>
......@@ -15,9 +15,9 @@
<div class="sidebar-comments-count"><span>${profiled_user['comments_count']}</span> ${pluralize('comment', profiled_user['comments_count'])}</div>
% if check_permissions_by_view(user, course.id, content=None, name='update_moderator_status'):
% if "Moderator" in role_names:
<a href="javascript:void(0)" class="sidebar-revoke-moderator-button">Revoke Moderator provileges</a>
<a href="javascript:void(0)" class="sidebar-toggle-moderator-button sidebar-revoke-moderator-button">Revoke Moderator provileges</a>
% else:
<a href="javascript:void(0)" class="sidebar-promote-moderator-button">Promote to Moderator</a>
<a href="javascript:void(0)" class="sidebar-toggle-moderator-button sidebar-promote-moderator-button">Promote to Moderator</a>
% endif
% endif
</div>
<%inherit file="../main.html" />
<%namespace name='static' file='../static_content.html'/>
<%block name="bodyclass">discussion</%block>
<%block name="title"><title>Discussion – MITx 6.002x</title></%block>
<%block name="title"><title>Discussion – ${course.number}</title></%block>
<%block name="headextra">
<%static:css group='course'/>
......@@ -32,7 +32,7 @@
</section>
<section class="course-content">
${content}
${content.decode('utf-8')}
</section>
</div>
</section>
......@@ -37,7 +37,7 @@
anonymous
{{/content.anonymous}}
{{^content.anonymous}}
{{content.username}}
<a href="{{##url_for_user}}{{content.user_id}}{{/url_for_user}}">{{content.username}}</a>
{{/content.anonymous}}
</div>
<div class="show-comments-wrapper">
......@@ -51,7 +51,9 @@
{{/thread}}
</div>
<ul class="discussion-actions">
<li><a class="discussion-link discussion-reply discussion-reply-{{content.type}}" href="javascript:void(0)">Reply</a></li>
{{^max_depth}}
<li><a class="discussion-link discussion-reply discussion-reply-{{content.type}}" href="javascript:void(0)">Reply</a></li>
{{/max_depth}}
{{#thread}}
<li><div class="follow-wrapper"><a class="discussion-link discussion-follow-thread" href="javascript:void(0)">Follow</a></div></li>
{{/thread}}
......
<form class="new-post-form collapsed" id="new-post-form" style="display: block; ">
<ul class="new-post-form-errors discussion-errors"></ul>
<input type="text" class="new-post-title title-input" placeholder="Title" />
<div class="new-post-similar-posts-wrapper" style="display: none">
Similar Posts:
<a class="hide-similar-posts" href="javascript:void(0)">Hide</a>
<div class="new-post-similar-posts"></div>
</div>
<div class="new-post-similar-posts-wrapper" style="display: none"></div>
<div class="new-post-body reply-body"></div>
<input class="new-post-tags" placeholder="Tags" />
<div class="post-options">
......
<%inherit file="../main.html" />
<%namespace name='static' file='../static_content.html'/>
<%block name="bodyclass">discussion</%block>
<%block name="title"><title>Discussion – MITx 6.002x</title></%block>
<%block name="title"><title>Discussion – ${course.number}</title></%block>
<%block name="headextra">
<%static:css group='course'/>
......@@ -30,7 +30,7 @@
</section>
<section class="course-content">
${content}
${content.decode('utf-8')}
</section>
</div>
</section>
<%! from django.template.defaultfilters import escapejs %>
<%namespace name="renderer" file="_thread.html"/>
<%inherit file="../main.html" />
<%namespace name='static' file='../static_content.html'/>
<%block name="bodyclass">discussion</%block>
<%block name="title"><title>Discussion – MITx 6.002x</title></%block>
<%block name="title"><title>Discussion – ${course.number}</title></%block>
<%block name="headextra">
<%static:css group='course'/>
<%include file="_js_head_dependencies.html" />
</%block>
<%block name="js_extra">
<%include file="_js_dependencies.html" />
<%include file="_js_body_dependencies.html" />
<%static:js group='discussion'/>
</%block>
<%include file="/courseware/course_navigation.html" args="active_page='discussion'" />
<%include file="../courseware/course_navigation.html" args="active_page='discussion'" />
<section class="container">
<div class="course-wrapper">
......
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