Commit 5c480753 by Brittany Cheng

Merge branch 'master' of github.com:dementrock/mitx into discussion

parents 440a6f69 f93fc7fd
...@@ -34,6 +34,7 @@ setup( ...@@ -34,6 +34,7 @@ setup(
"video = xmodule.video_module:VideoDescriptor", "video = xmodule.video_module:VideoDescriptor",
"videodev = xmodule.backcompat_module:TranslateCustomTagDescriptor", "videodev = xmodule.backcompat_module:TranslateCustomTagDescriptor",
"videosequence = xmodule.seq_module:SequenceDescriptor", "videosequence = xmodule.seq_module:SequenceDescriptor",
"discussion = xmodule.discussion_module:DiscussionDescriptor",
] ]
} }
) )
from lxml import etree
from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor
import comment_client
import dateutil
from dateutil.tz import tzlocal
from datehelper import time_ago_in_words
import json
class DiscussionModule(XModule):
def get_html(self):
context = {
'threads': comment_client.get_threads(self.discussion_id, recursive=False),
'time_ago_in_words': time_ago_in_words,
'parse': dateutil.parser.parse,
'discussion_id': self.discussion_id,
'search_bar': '',
'user_info': comment_client.get_user_info(self.user_id, raw=True),
'course_id': self.course_id,
}
return self.system.render_template('discussion/inline.html', context)
def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs):
XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs)
if isinstance(instance_state, str):
instance_state = json.loads(instance_state)
xml_data = etree.fromstring(definition['data'])
self.discussion_id = xml_data.attrib['id']
self.title = xml_data.attrib['for']
self.discussion_category = xml_data.attrib['discussion_category']
self.user_id = instance_state['user_id']
self.course_id = instance_state['course_id']
class DiscussionDescriptor(RawDescriptor):
module_class = DiscussionModule
(function () {
var output, Converter;
if (typeof exports === "object" && typeof require === "function") { // we're in a CommonJS (e.g. Node.js) module
output = exports;
Converter = require("./Markdown.Converter").Converter;
} else {
output = window.Markdown;
Converter = output.Converter;
}
output.getSanitizingConverter = function () {
var converter = new Converter();
converter.hooks.chain("postConversion", sanitizeHtml);
converter.hooks.chain("postConversion", balanceTags);
return converter;
}
function sanitizeHtml(html) {
return html.replace(/<[^>]*>?/gi, sanitizeTag);
}
// (tags that can be opened/closed) | (tags that stand alone)
var basic_tag_whitelist = /^(<\/?(b|blockquote|code|del|dd|dl|dt|em|h1|h2|h3|i|kbd|li|ol|p|pre|s|sup|sub|strong|strike|ul)>|<(br|hr)\s?\/?>)$/i;
// <a href="url..." optional title>|</a>
var a_white = /^(<a\shref="((https?|ftp):\/\/|\/)[-A-Za-z0-9+&@#\/%?=~_|!:,.;\(\)]+"(\stitle="[^"<>]+")?\s?>|<\/a>)$/i;
// <img src="url..." optional width optional height optional alt optional title
var img_white = /^(<img\ssrc="(https?:\/\/|\/)[-A-Za-z0-9+&@#\/%?=~_|!:,.;\(\)]+"(\swidth="\d{1,3}")?(\sheight="\d{1,3}")?(\salt="[^"<>]*")?(\stitle="[^"<>]*")?\s?\/?>)$/i;
function sanitizeTag(tag) {
if (tag.match(basic_tag_whitelist) || tag.match(a_white) || tag.match(img_white))
return tag;
else
return "";
}
/// <summary>
/// attempt to balance HTML tags in the html string
/// by removing any unmatched opening or closing tags
/// IMPORTANT: we *assume* HTML has *already* been
/// sanitized and is safe/sane before balancing!
///
/// adapted from CODESNIPPET: A8591DBA-D1D3-11DE-947C-BA5556D89593
/// </summary>
function balanceTags(html) {
if (html == "")
return "";
var re = /<\/?\w+[^>]*(\s|$|>)/g;
// convert everything to lower case; this makes
// our case insensitive comparisons easier
var tags = html.toLowerCase().match(re);
// no HTML tags present? nothing to do; exit now
var tagcount = (tags || []).length;
if (tagcount == 0)
return html;
var tagname, tag;
var ignoredtags = "<p><img><br><li><hr>";
var match;
var tagpaired = [];
var tagremove = [];
var needsRemoval = false;
// loop through matched tags in forward order
for (var ctag = 0; ctag < tagcount; ctag++) {
tagname = tags[ctag].replace(/<\/?(\w+).*/, "$1");
// skip any already paired tags
// and skip tags in our ignore list; assume they're self-closed
if (tagpaired[ctag] || ignoredtags.search("<" + tagname + ">") > -1)
continue;
tag = tags[ctag];
match = -1;
if (!/^<\//.test(tag)) {
// this is an opening tag
// search forwards (next tags), look for closing tags
for (var ntag = ctag + 1; ntag < tagcount; ntag++) {
if (!tagpaired[ntag] && tags[ntag] == "</" + tagname + ">") {
match = ntag;
break;
}
}
}
if (match == -1)
needsRemoval = tagremove[ctag] = true; // mark for removal
else
tagpaired[match] = true; // mark paired
}
if (!needsRemoval)
return html;
// delete all orphaned tags from the string
var ctag = 0;
html = html.replace(re, function (match) {
var res = tagremove[ctag] ? "" : match;
ctag++;
return res;
});
return html;
}
})();
...@@ -122,6 +122,13 @@ def get_module(user, request, location, student_module_cache, position=None): ...@@ -122,6 +122,13 @@ def get_module(user, request, location, student_module_cache, position=None):
''' '''
descriptor = modulestore().get_item(location) descriptor = modulestore().get_item(location)
user_id = user.id
import re
course_id = re.search(r'^/courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/', request.path)
if course_id:
course_id = course_id.group('course_id')
instance_module = student_module_cache.lookup(descriptor.category, descriptor.location.url()) instance_module = student_module_cache.lookup(descriptor.category, descriptor.location.url())
shared_state_key = getattr(descriptor, 'shared_state_key', None) shared_state_key = getattr(descriptor, 'shared_state_key', None)
...@@ -130,7 +137,12 @@ def get_module(user, request, location, student_module_cache, position=None): ...@@ -130,7 +137,12 @@ def get_module(user, request, location, student_module_cache, position=None):
else: else:
shared_module = None shared_module = None
instance_state = instance_module.state if instance_module is not None else None instance_state = instance_module.state if instance_module is not None else {}
instance_hash = json.loads(instance_state) if isinstance(instance_state, str) or isinstance(instance_state, unicode) \
else instance_state
instance_state = json.dumps(dict(instance_hash.items() + [("user_id", user.id), ("course_id", course_id)]))
shared_state = shared_module.state if shared_module is not None else None shared_state = shared_module.state if shared_module is not None else None
# TODO (vshnayder): fix hardcoded urls (use reverse) # TODO (vshnayder): fix hardcoded urls (use reverse)
......
...@@ -4,6 +4,10 @@ import logging ...@@ -4,6 +4,10 @@ import logging
import urllib import urllib
import itertools import itertools
from functools import partial
from functools import partial
from django.conf import settings from django.conf import settings
from django.core.context_processors import csrf from django.core.context_processors import csrf
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
...@@ -20,6 +24,7 @@ from module_render import toc_for_course, get_module, get_section ...@@ -20,6 +24,7 @@ from module_render import toc_for_course, get_module, get_section
from models import StudentModuleCache from models import StudentModuleCache
from student.models import UserProfile from student.models import UserProfile
from multicourse import multicourse_settings from multicourse import multicourse_settings
from django_comment_client.utils import get_discussion_title
from util.cache import cache, cache_if_anonymous from util.cache import cache, cache_if_anonymous
from student.models import UserTestGroup, CourseEnrollment from student.models import UserTestGroup, CourseEnrollment
...@@ -27,6 +32,11 @@ from courseware import grades ...@@ -27,6 +32,11 @@ from courseware import grades
from courseware.courses import check_course from courseware.courses import check_course
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
import comment_client
log = logging.getLogger("mitx.courseware") log = logging.getLogger("mitx.courseware")
template_imports = {'urllib': urllib} template_imports = {'urllib': urllib}
...@@ -259,7 +269,6 @@ def course_info(request, course_id): ...@@ -259,7 +269,6 @@ def course_info(request, course_id):
return render_to_response('info.html', {'course': course}) return render_to_response('info.html', {'course': course})
@ensure_csrf_cookie @ensure_csrf_cookie
@cache_if_anonymous @cache_if_anonymous
def course_about(request, course_id): def course_about(request, course_id):
...@@ -287,3 +296,24 @@ def university_profile(request, org_id): ...@@ -287,3 +296,24 @@ def university_profile(request, org_id):
template_file = "university_profile/{0}.html".format(org_id).lower() template_file = "university_profile/{0}.html".format(org_id).lower()
return render_to_response(template_file, context) return render_to_response(template_file, context)
def render_notifications(request, course, notifications):
context = {
'notifications': notifications,
'get_discussion_title': partial(get_discussion_title, request=request, course=course),
'course': course,
}
return render_to_string('notifications.html', context)
@login_required
def news(request, course_id):
course = check_course(course_id)
notifications = comment_client.get_notifications(request.user.id)
context = {
'course': course,
'content': render_notifications(request, course, notifications),
}
return render_to_response('news.html', context)
from django.conf.urls.defaults import url, patterns
import django_comment_client.base.views
urlpatterns = patterns('django_comment_client.base.views',
url(r'threads/(?P<thread_id>[\w\-]+)/update$', 'update_thread', name='update_thread'),
url(r'threads/(?P<thread_id>[\w\-]+)/reply$', 'create_comment', name='create_comment'),
url(r'threads/(?P<thread_id>[\w\-]+)/delete', 'delete_thread', name='delete_thread'),
url(r'threads/(?P<thread_id>[\w\-]+)/upvote$', 'vote_for_thread', {'value': 'up'}, name='upvote_thread'),
url(r'threads/(?P<thread_id>[\w\-]+)/downvote$', 'vote_for_thread', {'value': 'down'}, name='downvote_thread'),
url(r'threads/(?P<thread_id>[\w\-]+)/watch$', 'watch_thread', name='watch_thread'),
url(r'threads/(?P<thread_id>[\w\-]+)/unwatch$', 'unwatch_thread', name='unwatch_thread'),
url(r'comments/(?P<comment_id>[\w\-]+)/update$', 'update_comment', name='update_comment'),
url(r'comments/(?P<comment_id>[\w\-]+)/endorse$', 'endorse_comment', name='endorse_comment'),
url(r'comments/(?P<comment_id>[\w\-]+)/reply$', 'create_sub_comment', name='create_sub_comment'),
url(r'comments/(?P<comment_id>[\w\-]+)/delete$', 'delete_comment', name='delete_comment'),
url(r'comments/(?P<comment_id>[\w\-]+)/upvote$', 'vote_for_comment', {'value': 'up'}, name='upvote_comment'),
url(r'comments/(?P<comment_id>[\w\-]+)/downvote$', 'vote_for_comment', {'value': 'down'}, name='downvote_comment'),
url(r'(?P<commentable_id>[\w\-]+)/threads/create$', 'create_thread', name='create_thread'),
url(r'(?P<commentable_id>[\w\-]+)/watch$', 'watch_commentable', name='watch_commentable'),
url(r'(?P<commentable_id>[\w\-]+)/unwatch$', 'unwatch_commentable', name='unwatch_commentable'),
)
from django.contrib.auth.decorators import login_required
from django.views.decorators.http import require_POST, require_GET
from django.http import HttpResponse
from django.utils import simplejson
import comment_client
class JsonResponse(HttpResponse):
def __init__(self, data=None):
content = simplejson.dumps(data,
indent=2,
ensure_ascii=False)
super(JsonResponse, self).__init__(content,
mimetype='application/json; charset=utf8')
class JsonError(HttpResponse):
def __init__(self, status, error_message=""):
content = simplejson.dumps({'errors': error_message},
indent=2,
ensure_ascii=False)
super(JsonError, self).__init__(content,
status=status,
mimetype='application/json; charset=utf8')
def thread_author_only(fn):
def verified_fn(request, *args, **kwargs):
thread_id = args.get('thread_id', False) or \
kwargs.get('thread_id', False)
thread = comment_client.get_thread(thread_id)
if request.user.id == thread['user_id']:
return fn(request, *args, **kwargs)
else:
return JsonError(400, "unauthorized")
return verified_fn
def comment_author_only(fn):
def verified_fn(request, *args, **kwargs):
comment_id = args.get('comment_id', False) or \
kwargs.get('comment_id', False)
comment = comment_client.get_comment(comment_id)
if request.user.id == comment['user_id']:
return fn(request, *args, **kwargs)
else:
return JsonError(400, "unauthorized")
return verified_fn
def instructor_only(fn): #TODO add instructor verification
return fn
def extract(dic, keys):
return {k: dic[k] for k in keys}
@login_required
@require_POST
def create_thread(request, course_id, commentable_id):
attributes = extract(request.POST, ['body', 'title'])
attributes['user_id'] = request.user.id
attributes['course_id'] = course_id
response = comment_client.create_thread(commentable_id, attributes)
return JsonResponse(response)
@thread_author_only
@login_required
@require_POST
def update_thread(request, course_id, thread_id):
attributes = extract(request.POST, ['body', 'title'])
response = comment_client.update_thread(thread_id, attributes)
return JsonResponse(response)
@login_required
@require_POST
def create_comment(request, course_id, thread_id):
attributes = extract(request.POST, ['body'])
if request.POST.get('anonymous', 'false').lower() == 'false':
attributes['user_id'] = request.user.id
attributes['course_id'] = course_id
attributes['auto_subscribe'] = bool(request.POST.get('autowatch', False))
print attributes
response = comment_client.create_comment(thread_id, attributes)
return JsonResponse(response)
@thread_author_only
@login_required
@require_POST
def delete_thread(request, course_id, thread_id):
response = comment_client.delete_thread(thread_id)
return JsonResponse(response)
@thread_author_only
@login_required
@require_POST
def update_comment(request, course_id, comment_id):
attributes = extract(request.POST, ['body'])
response = comment_client.update_comment(comment_id, attributes)
return JsonResponse(response)
@instructor_only
@login_required
@require_POST
def endorse_comment(request, course_id, comment_id):
attributes = extract(request.POST, ['endorsed'])
response = comment_client.update_comment(comment_id, attributes)
return JsonResponse(response)
@login_required
@require_POST
def create_sub_comment(request, course_id, comment_id):
attributes = extract(request.POST, ['body'])
if request.POST.get('anonymous', 'false').lower() == 'false':
attributes['user_id'] = request.user.id
attributes['course_id'] = course_id
attributes['auto_subscribe'] = bool(request.POST.get('autowatch', False))
response = comment_client.create_sub_comment(comment_id, attributes)
return JsonResponse(response)
@comment_author_only
@login_required
@require_POST
def delete_comment(request, course_id, comment_id):
response = comment_client.delete_comment(comment_id)
return JsonResponse(response)
@login_required
@require_POST
def vote_for_comment(request, course_id, comment_id, value):
user_id = request.user.id
response = comment_client.vote_for_comment(comment_id, user_id, value)
return JsonResponse(response)
@login_required
@require_POST
def vote_for_thread(request, course_id, thread_id, value):
user_id = request.user.id
response = comment_client.vote_for_thread(thread_id, user_id, value)
return JsonResponse(response)
@login_required
@require_POST
def watch_thread(request, course_id, thread_id):
user_id = request.user.id
response = comment_client.subscribe_thread(user_id, thread_id)
return JsonResponse(response)
@login_required
@require_POST
def watch_commentable(request, course_id, commentable_id):
user_id = request.user.id
response = comment_client.subscribe_commentable(user_id, commentable_id)
return JsonResponse(response)
@login_required
@require_POST
def follow(request, course_id, followed_user_id):
user_id = request.user.id
response = comment_client.follow(user_id, followed_user_id)
return JsonResponse(response)
@login_required
@require_POST
def unwatch_thread(request, course_id, thread_id):
user_id = request.user.id
response = comment_client.unsubscribe_thread(user_id, thread_id)
return JsonResponse(response)
@login_required
@require_POST
def unwatch_commentable(request, course_id, commentable_id):
user_id = request.user.id
response = comment_client.unsubscribe_commentable(user_id, commentable_id)
return JsonResponse(response)
@login_required
@require_POST
def unfollow(request, course_id, followed_user_id):
user_id = request.user.id
response = comment_client.unfollow(user_id, followed_user_id)
return JsonResponse(response)
@login_required
@require_GET
def search(request, course_id):
text = request.GET.get('text', None)
commentable_id = request.GET.get('commentable_id', None)
response = comment_client.search(text, commentable_id)
return JsonResponse(response)
from django.conf.urls.defaults import url, patterns
import django_comment_client.forum.views
urlpatterns = patterns('django_comment_client.forum.views',
url(r'search$', 'search', name='search'),
url(r'threads/(?P<thread_id>\w+)$', 'single_thread', name='single_thread'),
url(r'(?P<discussion_id>\w+)$', 'forum_form_discussion', name='forum_form_discussion'),
)
from django.contrib.auth.decorators import login_required
from django.views.decorators.http import require_POST
from django.http import HttpResponse
from django.utils import simplejson
from django.core.context_processors import csrf
from mitxmako.shortcuts import render_to_response, render_to_string
from courseware.courses import check_course
import comment_client
import dateutil
from dateutil.tz import tzlocal
from datehelper import time_ago_in_words
from django_comment_client.utils import get_categorized_discussion_info
import json
def render_accordion(request, course, discussion_info, discussion_id):
context = {
'course': course,
'discussion_info': discussion_info,
'active': discussion_id,
'csrf': csrf(request)['csrf_token'],
}
return render_to_string('discussion/accordion.html', context)
def render_discussion(request, course_id, threads, discussion_id=None, search_text=''):
context = {
'threads': threads,
'discussion_id': discussion_id,
'search_bar': render_search_bar(request, course_id, discussion_id, text=search_text),
'user_info': comment_client.get_user_info(request.user.id, raw=True),
'course_id': course_id,
}
return render_to_string('discussion/inline.html', context)
def render_search_bar(request, course_id, discussion_id=None, text=''):
if not discussion_id:
return ''
context = {
'discussion_id': discussion_id,
'text': text,
'course_id': course_id,
}
return render_to_string('discussion/search_bar.html', context)
def forum_form_discussion(request, course_id, discussion_id):
course = check_course(course_id)
_, course_name, _ = course_id.split('/')
url_course_id = course_id.replace('/', '_').replace('.', '_')
discussion_info = get_categorized_discussion_info(request, course)#request.user, course, course_name, url_course_id)
search_text = request.GET.get('text', '')
if len(search_text) > 0:
threads = comment_client.search(search_text, discussion_id)
else:
threads = comment_client.get_threads(discussion_id, recursive=False)
context = {
'csrf': csrf(request)['csrf_token'],
'COURSE_TITLE': course.title,
'course': course,
'init': '',
'content': render_discussion(request, course_id, threads, discussion_id, search_text),
'accordion': render_accordion(request, course, discussion_info, discussion_id),
}
return render_to_response('discussion/index.html', context)
def render_single_thread(request, course_id, thread_id):
context = {
'thread': comment_client.get_thread(thread_id, recursive=True),
'user_info': comment_client.get_user_info(request.user.id, raw=True),
'course_id': course_id,
}
return render_to_string('discussion/single_thread.html', context)
def single_thread(request, course_id, thread_id):
course = check_course(course_id)
context = {
'csrf': csrf(request)['csrf_token'],
'init': '',
'content': render_single_thread(request, course_id, thread_id),
'accordion': '',
'user_info': comment_client.get_user_info(request.user.id, raw=True),
'course': course,
}
return render_to_response('discussion/index.html', context)
def search(request, course_id):
course = check_course(course_id)
text = request.GET.get('text', None)
threads = comment_client.search(text)
context = {
'csrf': csrf(request)['csrf_token'],
'init': '',
'content': render_discussion(request, course_id, threads, search_text=text),
'accordion': '',
'course': course,
}
return render_to_response('discussion/index.html', context)
from django.conf.urls.defaults import url, patterns, include
urlpatterns = patterns('',
url(r'forum/', include('django_comment_client.forum.urls')),
url(r'', include('django_comment_client.base.urls')),
)
from importlib import import_module
from courseware.models import StudentModuleCache
from courseware.module_render import get_module
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from django.conf import settings
import operator
import itertools
_FULLMODULES = None
_DISCUSSIONINFO = None
def get_full_modules():
global _FULLMODULES
if not _FULLMODULES:
class_path = settings.MODULESTORE['default']['ENGINE']
module_path, _, class_name = class_path.rpartition('.')
class_ = getattr(import_module(module_path), class_name)
modulestore = class_(**dict(settings.MODULESTORE['default']['OPTIONS'].items() + [('eager', True)]))
_FULLMODULES = modulestore.modules
return _FULLMODULES
def get_categorized_discussion_info(request, course):
"""
return a dict of the form {category: modules}
"""
global _DISCUSSIONINFO
if not _DISCUSSIONINFO:
initialize_discussion_info(request, course)
return _DISCUSSIONINFO['categorized']
def get_discussion_title(request, course, discussion_id):
global _DISCUSSIONINFO
if not _DISCUSSIONINFO:
initialize_discussion_info(request, course)
title = _DISCUSSIONINFO['by_id'].get(discussion_id, {}).get('title', '(no title)')
if title == '(no title)':
print "title shouldn't be none"
import pdb; pdb.set_trace()
return title
def initialize_discussion_info(request, course):
global _DISCUSSIONINFO
if _DISCUSSIONINFO:
return
course_id = course.id
_, course_name, _ = course_id.split('/')
user = request.user
url_course_id = course_id.replace('/', '_').replace('.', '_')
_is_course_discussion = lambda x: x[0].dict()['category'] == 'discussion' \
and x[0].dict()['course'] == course_name
_get_module_descriptor = operator.itemgetter(1)
def _get_module(module_descriptor):
print module_descriptor
module = get_module(user, request, module_descriptor.location, student_module_cache)[0]
return module
def _extract_info(module):
return {
'title': module.title,
'discussion_id': module.discussion_id,
'category': module.discussion_category,
}
def _pack_with_id(info):
return (info['discussion_id'], info)
discussion_module_descriptors = map(_get_module_descriptor,
filter(_is_course_discussion,
get_full_modules().items()))
student_module_cache = StudentModuleCache(user, course)
discussion_info = map(_extract_info, map(_get_module, discussion_module_descriptors))
_DISCUSSIONINFO = {}
_DISCUSSIONINFO['by_id'] = dict(map(_pack_with_id, discussion_info))
_DISCUSSIONINFO['categorized'] = dict((category, list(l)) \
for category, l in itertools.groupby(discussion_info, operator.itemgetter('category')))
_DISCUSSIONINFO['categorized']['General'] = [{
'title': 'General discussion',
'discussion_id': url_course_id,
'category': 'General',
}]
...@@ -484,6 +484,9 @@ INSTALLED_APPS = ( ...@@ -484,6 +484,9 @@ INSTALLED_APPS = (
# For testing # For testing
'django_jasmine', 'django_jasmine',
# Discussion
'django_comment_client',
# For Askbot # For Askbot
'django.contrib.sitemaps', 'django.contrib.sitemaps',
'django.contrib.admin', 'django.contrib.admin',
......
import requests
import json
SERVICE_HOST = 'http://localhost:4567'
PREFIX = SERVICE_HOST + '/api/v1'
def delete_threads(commentable_id, *args, **kwargs):
return _perform_request('delete', _url_for_commentable_threads(commentable_id), *args, **kwargs)
def get_threads(commentable_id, recursive=False, *args, **kwargs):
return _perform_request('get', _url_for_threads(commentable_id), {'recursive': recursive}, *args, **kwargs)
def create_thread(commentable_id, attributes, *args, **kwargs):
return _perform_request('post', _url_for_threads(commentable_id), attributes, *args, **kwargs)
def get_thread(thread_id, recursive=False, *args, **kwargs):
return _perform_request('get', _url_for_thread(thread_id), {'recursive': recursive}, *args, **kwargs)
def update_thread(thread_id, attributes, *args, **kwargs):
return _perform_request('put', _url_for_thread(thread_id), attributes, *args, **kwargs)
def create_comment(thread_id, attributes, *args, **kwargs):
return _perform_request('post', _url_for_thread_comments(thread_id), attributes, *args, **kwargs)
def delete_thread(thread_id, *args, **kwargs):
return _perform_request('delete', _url_for_thread(thread_id), *args, **kwargs)
def get_comment(comment_id, recursive=False, *args, **kwargs):
return _perform_request('get', _url_for_comment(comment_id), {'recursive': recursive}, *args, **kwargs)
def update_comment(comment_id, attributes, *args, **kwargs):
return _perform_request('put', _url_for_comment(comment_id), attributes, *args, **kwargs)
def create_sub_comment(comment_id, attributes, *args, **kwargs):
return _perform_request('post', _url_for_comment(comment_id), attributes, *args, **kwargs)
def delete_comment(comment_id, *args, **kwargs):
return _perform_request('delete', _url_for_comment(comment_id), *args, **kwargs)
def vote_for_comment(comment_id, user_id, value, *args, **kwargs):
return _perform_request('put', _url_for_vote_comment(comment_id), {'user_id': user_id, 'value': value}, *args, **kwargs)
def undo_vote_for_comment(comment_id, user_id, *args, **kwargs):
return _perform_request('delete', _url_for_vote_comment(comment_id), *args, **kwargs)
def vote_for_thread(thread_id, user_id, value, *args, **kwargs):
return _perform_request('put', _url_for_vote_thread(thread_id), {'user_id': user_id, 'value': value}, *args, **kwargs)
def undo_vote_for_thread(thread_id, user_id, *args, **kwargs):
return _perform_request('delete', _url_for_vote_thread(thread_id), *args, **kwargs)
def get_notifications(user_id, *args, **kwargs):
return _perform_request('get', _url_for_notifications(user_id), *args, **kwargs)
def get_user_info(user_id, complete=True, *args, **kwargs):
return _perform_request('get', _url_for_user(user_id), {'complete': complete}, *args, **kwargs)
def subscribe(user_id, subscription_detail, *args, **kwargs):
return _perform_request('post', _url_for_subscription(user_id), subscription_detail, *args, **kwargs)
def subscribe_user(user_id, followed_user_id, *args, **kwargs):
return subscribe(user_id, {'source_type': 'user', 'source_id': followed_user_id})
follow = subscribe_user
def subscribe_thread(user_id, thread_id, *args, **kwargs):
return subscribe(user_id, {'source_type': 'thread', 'source_id': thread_id})
def subscribe_commentable(user_id, commentable_id, *args, **kwargs):
return subscribe(user_id, {'source_type': 'other', 'source_id': commentable_id})
def unsubscribe(user_id, subscription_detail, *args, **kwargs):
return _perform_request('delete', _url_for_subscription(user_id), subscription_detail, *args, **kwargs)
def unsubscribe_user(user_id, followed_user_id, *args, **kwargs):
return unsubscribe(user_id, {'source_type': 'user', 'source_id': followed_user_id})
unfollow = unsubscribe_user
def unsubscribe_thread(user_id, thread_id, *args, **kwargs):
return unsubscribe(user_id, {'source_type': 'thread', 'source_id': thread_id})
def unsubscribe_commentable(user_id, commentable_id, *args, **kwargs):
return unsubscribe(user_id, {'source_type': 'other', 'source_id': commentable_id})
def search(text, commentable_id=None, *args, **kwargs):
return _perform_request('get', _url_for_search(), {'text': text, 'commentable_id': commentable_id}, *args, **kwargs)
def _perform_request(method, url, data_or_params=None, *args, **kwargs):
if method in ['post', 'put', 'patch']:
response = requests.request(method, url, data=data_or_params)
else:
response = requests.request(method, url, params=data_or_params)
if kwargs.get("raw", False):
return response.text
else:
return json.loads(response.text)
def _url_for_threads(commentable_id):
return "{prefix}/{commentable_id}/threads".format(prefix=PREFIX, commentable_id=commentable_id)
def _url_for_thread(thread_id):
return "{prefix}/threads/{thread_id}".format(prefix=PREFIX, thread_id=thread_id)
def _url_for_thread_comments(thread_id):
return "{prefix}/threads/{thread_id}/comments".format(prefix=PREFIX, thread_id=thread_id)
def _url_for_comment(comment_id):
return "{prefix}/comments/{comment_id}".format(prefix=PREFIX, comment_id=comment_id)
def _url_for_vote_comment(comment_id):
return "{prefix}/comments/{comment_id}/votes".format(prefix=PREFIX, comment_id=comment_id)
def _url_for_vote_thread(thread_id):
return "{prefix}/threads/{thread_id}/votes".format(prefix=PREFIX, thread_id=thread_id)
def _url_for_notifications(user_id):
return "{prefix}/users/{user_id}/notifications".format(prefix=PREFIX, user_id=user_id)
def _url_for_subscription(user_id):
return "{prefix}/users/{user_id}/subscriptions".format(prefix=PREFIX, user_id=user_id)
def _url_for_user(user_id):
return "{prefix}/users/{user_id}".format(prefix=PREFIX, user_id=user_id)
def _url_for_search():
return "{prefix}/search".format(prefix=PREFIX)
datehelper
==========
This Python module contains some useful date-related methods inspired by rails.
\ No newline at end of file
from time_ago_in_words import *
from datetime import datetime, timedelta
from dateutil.tz import tzlocal
import calendar
# only used for testing
def _timedelta(**kwargs):
kwargs['days'] = kwargs.get('days', 0)
if kwargs.get('years', False):
# not really a good solution since ignoring leap years
# but this is only for test anyways
kwargs['days'] += kwargs['years'] * 365
del kwargs['years']
if kwargs.get('months', False):
kwargs['days'] += kwargs['months'] * 30
del kwargs['months']
return timedelta(**kwargs)
def time_ago_in_words(from_time, include_seconds=False):
return distance_of_time_in_words(from_time, datetime.now(tzlocal()), include_seconds=False)
distance_of_time_in_words_to_now = time_ago_in_words
def _time_in_words(before_text, unit):
if before_text:
before_text += ' '
def time_in_words_generator(count):
if count <= 1:
if before_text == 'less than ':
if unit == 'hour':
count = 'an'
else:
count = 'a'
return '{0}{1} {2}'.format(before_text, count, unit)
else:
return '{0}{1} {2}s'.format(before_text, count, unit)
return time_in_words_generator
def distance_of_time_in_words(from_time, to_time=0, include_seconds=False, options = {}):
"""Return a rough description of the time interval between from_time and to_time.
This is a direct translation from rails in ActionView::Helpers::DateHelper.
Reference:
http://api.rubyonrails.org/classes/ActionView/Helpers/DateHelper.html#method-i-distance_of_time_in_words
>>> from_time = datetime.now(tzlocal())
>>> distance_of_time_in_words(from_time, from_time + _timedelta(minutes=50))
'about 1 hour'
>>> distance_of_time_in_words(from_time, from_time + _timedelta(seconds=15))
'less than a minute'
>>> distance_of_time_in_words(from_time, from_time + _timedelta(seconds=15), True)
'less than 20 seconds'
>>> distance_of_time_in_words(from_time, from_time + _timedelta(years=3))
'about 3 years'
>>> distance_of_time_in_words(from_time, from_time + _timedelta(hours=60))
'3 days'
>>> distance_of_time_in_words(from_time, from_time + _timedelta(seconds=45), True)
'less than a minute'
>>> distance_of_time_in_words(from_time, from_time + _timedelta(seconds=-45), True)
'less than a minute'
>>> distance_of_time_in_words(from_time, from_time + _timedelta(seconds=76))
'1 minute'
>>> distance_of_time_in_words(from_time, from_time + _timedelta(years=1, days=3))
'about 1 year'
>>> distance_of_time_in_words(from_time, from_time + _timedelta(years=3, months=6))
'over 3 years'
>>> distance_of_time_in_words(from_time, from_time + _timedelta(years=4, days=9, minutes=30, seconds=5))
'about 4 years'
>>> to_time = from_time + _timedelta(years=6, days=19)
>>> distance_of_time_in_words(from_time, to_time, True)
'about 6 years'
>>> distance_of_time_in_words(to_time, from_time, True)
'about 6 years'
>>> distance_of_time_in_words(from_time, from_time)
'less than a minute'
"""
if isinstance(from_time, int):
from_time = datetime.fromtimestamp(time.time()+from_time)
if isinstance(to_time, int):
to_time = datetime.fromtimestamp(time.time()+to_time)
distance_in_minutes = int(round(abs((to_time - from_time).total_seconds()) / 60))
distance_in_seconds = int(round(abs((to_time - from_time).total_seconds())))
less_than_x_minutes = _time_in_words('less than', 'minute')
less_than_x_seconds = _time_in_words('less than', 'second')
half_a_minute = 'half a minute'
x_minutes = _time_in_words('', 'minute')
about_x_hours = _time_in_words('about', 'hour')
x_days = _time_in_words('', 'day')
about_x_months = _time_in_words('about', 'month')
x_months = _time_in_words('', 'month')
about_x_years = _time_in_words('about', 'year')
over_x_years = _time_in_words('over', 'year')
almost_x_years = _time_in_words('almost', 'year')
if 0 <= distance_in_minutes <= 1:
if not include_seconds:
if distance_in_minutes == 0:
return less_than_x_minutes(1)
else:
return x_minutes(distance_in_minutes)
else:
if 0 <= distance_in_seconds <= 4:
return less_than_x_seconds(5)
elif 5 <= distance_in_seconds <= 9:
return less_than_x_seconds(10)
elif 10 <= distance_in_seconds <= 19:
return less_than_x_seconds(20)
elif 20 <= distance_in_seconds <= 39:
return half_a_minute
elif 40 <= distance_in_seconds <= 59:
return less_than_x_minutes(1)
else:
return x_minutes(1)
elif 2 <= distance_in_minutes <= 44:
return x_minutes(distance_in_minutes)
elif 45 <= distance_in_minutes <= 89:
return about_x_hours(1)
elif 90 <= distance_in_minutes <= 1439:
return about_x_hours(int(round(float(distance_in_minutes) / 60.0)))
elif 1440 <= distance_in_minutes <= 2519:
return x_days(1)
elif 2520 <= distance_in_minutes <= 43199:
return x_days(int(round(float(distance_in_minutes) / 1440.0)))
elif 43200 <= distance_in_minutes <= 86399:
return about_x_months(1)
elif 86400 <= distance_in_minutes <= 525599:
return x_months(int(round(float(distance_in_minutes) / 43200.0)))
else:
fyear = from_time.year
if from_time.month >= 3:
fyear += 1
tyear = to_time.year
if to_time.month < 3:
tyear -= 1
leap_years = 0 if fyear > tyear else len([x for x in range(fyear, tyear + 1) if calendar.isleap(x)])
minute_offset_for_leap_year = leap_years * 1440
# Discount the leap year days when calculating year distance.
# e.g. if there are 20 leap year days between 2 dates having the same day
# and month then the based on 365 days calculation
# the distance in years will come out to over 80 years when in written
# english it would read better as about 80 years.
minutes_with_offset = distance_in_minutes - minute_offset_for_leap_year
remainder = (minutes_with_offset % 525600)
distance_in_years = (minutes_with_offset / 525600)
if remainder < 131400:
return about_x_years(distance_in_years)
elif remainder < 394200:
return over_x_years(distance_in_years)
else:
return almost_x_years(distance_in_years + 1)
$ ->
converter = Markdown.getSanitizingConverter()
editor = new Markdown.Editor(converter)
#converter.hooks.chain "preConversion", removeMath
editor.run()
$comment_margin_left: 30px;
$discussion_title_size: 1.6em;
$comment_title_size: 1.2em;
$comment_body_size: 1.0em;
$comment_info_size: 0.75em;
$discussion_input_width: 60%;
@mixin discussion-font {
font-family: "Comic Sans MS", cursive, sans-serif !important;
}
@mixin discussion-clickable {
color: black;
&:hover {
text-decoration: none;
}
}
.course-content {
.discussion {
margin-left: 40px;
margin-top: 15px;
}
}
.discussion {
.discussion-search-form {
margin-bottom: 40px;
.discussion-search {
@include discussion-font;
color: #1d9dd9;
display: block;
margin-top: 10px;
font-weight: bold;
}
.discussion-search-text {
@include discussion-font;
}
}
.discussion-title {
@include discussion-font;
@include discussion-clickable;
font-size: $discussion_title_size;
font-weight: bold;
margin-bottom: 20px;
display: block;
}
.discussion-title-wrapper {
.discussion-watch-discussion, .discussion-unwatch-discussion {
display: none;
@include discussion-font;
margin-left: 5px;
font-size: $comment_info_size;
}
.discussion-title {
display: inline-block;
}
&:hover {
.discussion-watch-discussion, .discussion-unwatch-discussion {
display: inline-block;
}
}
}
.discussion-votes {
margin-right: 8px;
margin-top: 5px;
text-align: center;
height: 40px;
float: left;
.discussion-vote-count {
font-size: $comment_body_size;
@include discussion-font;
}
a.discussion-vote {
display: block;
color: black;
font-weight: bold;
font-size: 15px;
&.discussion-vote-up {
margin-bottom: 3px;
}
&.discussion-vote-down {
margin-top: 3px;
}
&.voted {
color: #1d9dd9;
}
}
}
.discussion-right-wrapper {
min-height: 40px;
float: left;
}
.new-post-form {
.new-post-title, .new-post-body {
@include discussion-font;
display: block !important;
width: $discussion_input_width !important;
}
.new-post-body {
margin-top: 10px;
}
.discussion-new-post {
@include discussion-font;
color: #1d9dd9;
display: block;
margin-top: 10px;
font-weight: bold;
}
}
.thread {
//display: none;
margin-top: 30px;
margin-bottom: 30px;
.thread-title {
@include discussion-font;
@include discussion-clickable;
font-size: $comment_title_size;
font-weight: bold;
display: block;
}
.thread-body {
@include discussion-font;
font-size: $comment_body_size;
margin-top: 7px;
margin-bottom: 2px;
}
.info {
@include discussion-font;
font-size: $comment_info_size;
font-style: italic;
margin-top: 5px;
color: gray;
.discussion-link {
margin-left: 2px;
}
.discussion-reply {
margin-left: 4px;
}
.discussion-link {
@include discussion-font;
display: inline-block;
color: #1d9dd9;
display: none;
}
}
.discussion-content:hover {
.discussion-link {
display: inline-block;
}
}
.discussion-content {
margin-top: 10px;
overflow: hidden;
.discussion-content-edit {
margin-left: $comment_margin_left;
.comment-edit {
@include discussion-font;
width: $discussion_input_width !important;
font-size: $comment_body_size;
margin-top: 10px;
display: block;
}
}
}
.comments {
//display: none;
margin-left: $comment_margin_left;
overflow: hidden;
.comment {
.comment-body {
@include discussion-font;
font-size: $comment_body_size;
margin-top: 3px;
display: block;
color: black;
}
}
.discussion-votes {
margin-right: 6px;
margin-top: 6px;
}
}
}
}
body
{
background-color: White;
font-family: sans-serif;
}
.wmd-panel
{
margin-left: 25%;
margin-right: 25%;
width: 50%;
min-width: 500px;
}
.wmd-button-bar
{
width: 100%;
background-color: Silver;
}
.wmd-input
{
height: 300px;
width: 100%;
background-color: Gainsboro;
border: 1px solid DarkGray;
}
.wmd-preview
{
background-color: #c0e0ff;
}
.wmd-button-row
{
position: relative;
margin-left: 5px;
margin-right: 5px;
margin-bottom: 5px;
margin-top: 10px;
padding: 0px;
height: 20px;
}
.wmd-spacer
{
width: 1px;
height: 20px;
margin-left: 14px;
position: absolute;
background-color: Silver;
display: inline-block;
list-style: none;
}
.wmd-button {
width: 20px;
height: 20px;
padding-left: 2px;
padding-right: 3px;
position: absolute;
display: inline-block;
list-style: none;
cursor: pointer;
}
.wmd-button > span {
background-image: url('/static/images/wmd-buttons.png');
background-repeat: no-repeat;
background-position: 0px 0px;
width: 20px;
height: 20px;
display: inline-block;
}
.wmd-spacer1
{
left: 50px;
}
.wmd-spacer2
{
left: 175px;
}
.wmd-spacer3
{
left: 300px;
}
.wmd-prompt-background
{
background-color: Black;
}
.wmd-prompt-dialog
{
border: 1px solid #999999;
background-color: #F5F5F5;
}
.wmd-prompt-dialog > div {
font-size: 0.8em;
font-family: arial, helvetica, sans-serif;
}
.wmd-prompt-dialog > form > input[type="text"] {
border: 1px solid #999999;
color: black;
}
.wmd-prompt-dialog > form > input[type="button"]{
border: 1px solid #888888;
font-family: trebuchet MS, helvetica, sans-serif;
font-size: 0.8em;
font-weight: bold;
}
@mixin news-font {
font-family: "Comic Sans MS", cursive, sans-serif !important;
}
.notifications {
@include news-font;
padding-left: 20px;
padding-top: 20px;
padding-bottom: 20px;
.notification {
@include news-font;
margin-top: 15px;
margin-botton: 15px;
a {
@include news-font;
}
}
}
...@@ -27,3 +27,6 @@ ...@@ -27,3 +27,6 @@
@import 'multicourse/password_reset'; @import 'multicourse/password_reset';
@import 'multicourse/error-pages'; @import 'multicourse/error-pages';
@import 'multicourse/help'; @import 'multicourse/help';
@import 'discussion';
@import 'news';
...@@ -15,7 +15,8 @@ def url_class(url): ...@@ -15,7 +15,8 @@ def url_class(url):
<li class="info"><a href="${reverse('info', args=[course.id])}" class="${url_class('info')}">Course Info</a></li> <li class="info"><a href="${reverse('info', args=[course.id])}" class="${url_class('info')}">Course Info</a></li>
% if user.is_authenticated(): % if user.is_authenticated():
<li class="book"><a href="${reverse('book', args=[course.id])}" class="${url_class('book')}">Textbook</a></li> <li class="book"><a href="${reverse('book', args=[course.id])}" class="${url_class('book')}">Textbook</a></li>
<li class="discussion"><a href="${reverse('questions')}">Discussion</a></li> <li class="discussion"><a href="${reverse('django_comment_client.forum.views.forum_form_discussion', args=[course.id, course.id.replace('/', '_').replace('.', '_')])}" class="${url_class('discussion')}">Discussion</a></li>
<li class="news"><a href="${reverse('news', args=[course.id])}" class="${url_class('news')}">News</a></li>
% endif % endif
<li class="wiki"><a href="${reverse('wiki_root', args=[course.id])}" class="${url_class('wiki')}">Wiki</a></li> <li class="wiki"><a href="${reverse('wiki_root', args=[course.id])}" class="${url_class('wiki')}">Wiki</a></li>
% if user.is_authenticated(): % if user.is_authenticated():
......
<%! from django.core.urlresolvers import reverse %>
<%
def url_for(commentable):
return reverse('django_comment_client.forum.views.forum_form_discussion', args=[course.id, commentable['discussion_id']])
%>
<%def name="make_category(category, commentables)">
<h3><a href="#">${category}</a></h3>
<ul>
% for commentable in commentables:
<li${' class="active"' if active == commentable['discussion_id'] else ''}>
<a href="${url_for(commentable)}">
<p>${commentable['title']}</p>
</a>
% endfor
</ul>
</%def>
% for category, commentables in discussion_info.items():
${make_category(category, commentables)}
% endfor
<%inherit file="../main.html" />
<%namespace name='static' file='../static_content.html'/>
<%block name="bodyclass">courseware discussion</%block>
<%block name="title"><title>Discussion – MITx 6.002x</title></%block>
<%block name="headextra">
<%static:css group='course'/>
</%block>
<%block name="js_extra">
<script type="text/javascript" src="${static.url('js/vendor/Markdown.Converter.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/Markdown.Sanitizer.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/Markdown.Editor.js')}"></script>
</%block>
<%include file="../course_navigation.html" args="active_page='discussion'" />
<section class="container">
<div class="course-wrapper">
<section aria-label="Course Navigation" class="course-index">
<header id="open_close_accordion">
<h2>Discussion Boards</h2>
<a href="#">close</a>
</header>
<div id="accordion" style="display:none">
<nav>
${accordion}
</nav>
</div>
</section>
<section class="course-content">
${content}
</section>
</div>
</section>
<%namespace name="renderer" file="thread.html"/>
<section class="discussion" _id="${discussion_id}">
<div class="discussion-non-content">
<div class="discussion-title-wrapper">
<a class="discussion-title" href="javascript:void(0)">Discussion</a>
</div>
${search_bar}
<form class="new-post-form" _id="${discussion_id}">
<input type="text" class="new-post-title" placeholder="Title"/>
<textarea class="new-post-body"></textarea>
<a class="discussion-new-post" href="javascript:void(0)">New Post</a>
<div class="wmd-panel">
<div id="wmd-button-bar"></div>
<textarea class="wmd-input" id="wmd-input">
</textarea>
<div id="wmd-preview" class="wmd-panel wmd-preview"></div>
</div>
</form>
</div>
% for thread in threads:
${renderer.render_thread(course_id, thread, edit_thread=False, show_comments=False)}
% endfor
</section>
<script type="text/javascript">
var $$user_info = JSON.parse('${user_info}');
var $$course_id = "${course_id}";
</script>
<%! from django.core.urlresolvers import reverse %>
<%
def url_for_search():
return reverse('django_comment_client.forum.views.search', args=[course_id])
%>
<form action="${url_for_search()}" method="get" class="discussion-search-form">
<input type="text" class="discussion-search-text" value="${text}"/>
<label for="discussion-search-within-board-${discussion_id}">search within board</label>
<input type="checkbox" id="discussion-search-within-board-${discussion_id}" class="discussion-search-within-board" checked/>
<a class="discussion-link discussion-search" href="javascript:void(0)">Search</a>
</form>
<%namespace name="renderer" file="thread.html"/>
<section class="discussion">
<a class="discussion-title" href="javascript:void(0)">Discussion</a>
${renderer.render_thread(course_id, thread, edit_thread=True, show_comments=True)}
</section>
<script type="text/javascript">
var $$user_info = JSON.parse('${user_info}');
var $$course_id = "${course_id}";
</script>
<%! from django.core.urlresolvers import reverse %>
<%! from datehelper import time_ago_in_words %>
<%! from dateutil.parser import parse %>
<%def name="render_thread(course_id, thread, edit_thread=False, show_comments=False)">
<%
if show_comments:
url_for_thread = ""
else:
thread_id = thread['id']
url_for_thread = reverse('django_comment_client.forum.views.single_thread', args=[course_id, thread_id])
%>
<div class="thread" _id="${thread['id']}">
<div class="discussion-content">
<div class="discussion-upper-wrapper clearfix">
${render_vote(thread)}
<div class="discussion-right-wrapper clearfix">
<a class="thread-title" name="${thread['id']}" href="${url_for_thread}">${thread['title']}</a>
<div class="discussion-content-view">
<div class="thread-body">${thread['body']}</div>
<div class="info">
${render_info(thread)}
% if edit_thread:
${render_reply()}
${render_edit()}
% endif
</div>
</div>
</div>
</div>
</div>
% if show_comments:
<div class="comments">
${render_comments(thread['children'])}
</div>
% endif
</div>
</%def>
<%def name="render_comments(comments)">
% for comment in comments:
<div class="comment" _id="${comment['id']}">
<div class="discussion-content">
${render_vote(comment)}
<div class="discussion-right-wrapper">
<div class="discussion-content-view">
<a class="comment-body" name="${comment['id']}">${comment['body']}</a>
<div class="info">
${render_info(comment)}
${render_reply()}
${render_edit()}
</div>
</div>
</div>
</div>
<div class="comments">
${render_comments(comment['children'])}
</div>
</div>
% endfor
</%def>
<%def name="render_info(content)">
${time_ago_in_words(parse(content['updated_at']))} ago by
% if content.get('user_id', False):
user No.${content['user_id']}
% else:
anonymous
% endif
</%def>
<%def name="render_reply()">
<a class="discussion-link discussion-reply" href="javascript:void(0)">Reply</a>
</%def>
<%def name="render_edit()">
<a class="discussion-link discussion-edit" href="javascript:void(0)">Edit</a>
</%def>
<%def name="render_watch_thread()">
<a class="discussion-link discussion-watch-thread" href="javascript:void(0)">Watch</a>
</%def>
<%def name="render_vote(content)">
<%
upvote = "&#x2C4;"
downvote = "&#x2C5;"
%>
<div class="discussion-votes" title="Current votes: ${content['votes']['point']}">
<a class="discussion-vote discussion-vote-up" href="javascript:void(0)" title="Current votes: ${content['votes']['point']}">${upvote}</a>
<a class="discussion-vote discussion-vote-down" href="javascript:void(0)" title="Current votes: ${content['votes']['point']}">${downvote}</a>
</div>
</%def>
...@@ -14,6 +14,7 @@ ...@@ -14,6 +14,7 @@
<%static:js group='main_vendor'/> <%static:js group='main_vendor'/>
<%block name="headextra"/> <%block name="headextra"/>
<!--[if lt IE 9]> <!--[if lt IE 9]>
<script src="${static.url('js/html5shiv.js')}"></script> <script src="${static.url('js/html5shiv.js')}"></script>
<![endif]--> <![endif]-->
......
<%inherit file="main.html" />
<%namespace name='static' file='static_content.html'/>
<%block name="bodyclass">courseware news</%block>
<%block name="title"><title>News – MITx 6.002x</title></%block>
<%block name="headextra">
<%static:css group='course'/>
</%block>
<%block name="js_extra">
</%block>
<%include file="course_navigation.html" args="active_page='news'" />
<section class="container">
<div class="course-wrapper">
<section class="course-content">
${content}
</section>
</div>
</section>
<%! from django.core.urlresolvers import reverse %>
<%
def url_for_thread(thread_id):
return reverse('django_comment_client.forum.views.single_thread', args=[course.id, thread_id])
%>
<%
def url_for_comment(thread_id, comment_id):
return reverse('django_comment_client.forum.views.single_thread', args=[course.id, thread_id]) + "#" + comment_id
%>
<%
def url_for_discussion(discussion_id):
return reverse('django_comment_client.forum.views.forum_form_discussion', args=[course.id, discussion_id])
%>
<%
def discussion_title(discussion_id):
return get_discussion_title(discussion_id=discussion_id)
%>
<div class="notifications">
% for notification in notifications:
${render_notification(notification)}
% endfor
</div>
<%def name="render_notification(notification)">
<div class="notification">
<% info = notification['info'] %>
% if notification['notification_type'] == 'post_reply':
User No.${notification['actor_id']} posted a
<a href="${url_for_comment(info['thread_id'], info['comment_id'])}">comment</a>
to the thread
<a href="${url_for_thread(info['thread_id'])}">${info['thread_title']}</a>
in discussion
<a href="${url_for_discussion(info['commentable_id'])}">${discussion_title(info['commentable_id'])}</a>
% elif notification['notification_type'] == 'post_topic':
User No.${notification['actor_id']} posted a new thread
<a href="${url_for_thread(info['thread_id'])}">${info['thread_title']}</a>
in discussion
<a href="${url_for_discussion(info['commentable_id'])}">${discussion_title(info['commentable_id'])}</a>
% endif
</div>
</%def>
...@@ -126,7 +126,7 @@ $(function() { ...@@ -126,7 +126,7 @@ $(function() {
%for chapter in courseware_summary: %for chapter in courseware_summary:
%if not chapter['chapter'] == "hidden": %if not chapter['chapter'] == "hidden":
<li> <li>
<h2><a href="${reverse('courseware_chapter', args=format_url_params([chapter['course'], chapter['chapter']])) }"> <h2><a href="javascript:void(0)">
${ chapter['chapter'] }</a></h2> ${ chapter['chapter'] }</a></h2>
<ol class="sections"> <ol class="sections">
...@@ -138,7 +138,7 @@ $(function() { ...@@ -138,7 +138,7 @@ $(function() {
percentageString = "{0:.0%}".format( float(earned)/total) if earned > 0 and total > 0 else "" percentageString = "{0:.0%}".format( float(earned)/total) if earned > 0 and total > 0 else ""
%> %>
<h3><a href="${reverse('courseware_section', args=format_url_params([chapter['course'], chapter['chapter'], section['section']])) }"> <h3><a href="javascript:void(0)">
${ section['section'] }</a> ${"({0:.3n}/{1:.3n}) {2}".format( float(earned), float(total), percentageString )}</h3> ${ section['section'] }</a> ${"({0:.3n}/{1:.3n}) {2}".format( float(earned), float(total), percentageString )}</h3>
${section['format']} ${section['format']}
%if 'due' in section and section['due']!="": %if 'due' in section and section['due']!="":
......
...@@ -134,6 +134,12 @@ if settings.COURSEWARE_ENABLED: ...@@ -134,6 +134,12 @@ if settings.COURSEWARE_ENABLED:
'courseware.views.profile', name="profile"), 'courseware.views.profile', name="profile"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/profile/(?P<student_id>[^/]*)/$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/profile/(?P<student_id>[^/]*)/$',
'courseware.views.profile'), 'courseware.views.profile'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/news$',
'courseware.views.news', name="news"),
# discussion
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/discussion/',
include('django_comment_client.urls')),
) )
# Multicourse wiki # Multicourse wiki
...@@ -155,6 +161,7 @@ if settings.ASKBOT_ENABLED: ...@@ -155,6 +161,7 @@ if settings.ASKBOT_ENABLED:
# url(r'^robots.txt$', include('robots.urls')), # url(r'^robots.txt$', include('robots.urls')),
) )
if settings.DEBUG: if settings.DEBUG:
## Jasmine ## Jasmine
urlpatterns=urlpatterns + (url(r'^_jasmine/', include('django_jasmine.urls')),) urlpatterns=urlpatterns + (url(r'^_jasmine/', include('django_jasmine.urls')),)
......
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