Commit 53191d32 by David Ormsbee

Merge pull request #698 from MITx/feature/arjun/new-discussions

New Discussions
parents caadfd49 2ee41ebf
common/static/images/spinner.gif

6.78 KB | W: | H:

common/static/images/spinner.gif

6.87 KB | W: | H:

common/static/images/spinner.gif
common/static/images/spinner.gif
common/static/images/spinner.gif
common/static/images/spinner.gif
  • 2-up
  • Swipe
  • Onion skin
...@@ -9,7 +9,7 @@ If you haven't done so already: ...@@ -9,7 +9,7 @@ If you haven't done so already:
brew install mongodb brew install mongodb
Make sure that you have mongodb running. You can simply open a new terminal tab and type: Make sure that you have mongodb running. You can simply open a new terminal tab and type:
mongod mongod
## Installing elasticsearch ## Installing elasticsearch
...@@ -72,9 +72,9 @@ For convenience, add the following environment variables to the terminal (assumi ...@@ -72,9 +72,9 @@ For convenience, add the following environment variables to the terminal (assumi
export DJANGO_SETTINGS_MODULE=lms.envs.dev export DJANGO_SETTINGS_MODULE=lms.envs.dev
export PYTHONPATH=. export PYTHONPATH=.
Now initialzie roles and permissions: Now initialzie roles and permissions, providing a course id eg.:
django-admin.py seed_permissions_roles django-admin.py seed_permissions_roles "MITx/6.002x/2012_Fall"
To assign yourself as a moderator, use the following command (assuming your username is "test", and the course id is "MITx/6.002x/2012_Fall"): To assign yourself as a moderator, use the following command (assuming your username is "test", and the course id is "MITx/6.002x/2012_Fall"):
......
...@@ -22,7 +22,7 @@ from django.contrib.auth.models import User ...@@ -22,7 +22,7 @@ from django.contrib.auth.models import User
from mitxmako.shortcuts import render_to_response, render_to_string from mitxmako.shortcuts import render_to_response, render_to_string
from courseware.courses import get_course_with_access from courseware.courses import get_course_with_access
from django_comment_client.utils import JsonResponse, JsonError, extract from django_comment_client.utils import JsonResponse, JsonError, extract, get_courseware_context
from django_comment_client.permissions import check_permissions_by_view from django_comment_client.permissions import check_permissions_by_view
from django_comment_client.models import Role from django_comment_client.models import Role
...@@ -38,11 +38,10 @@ def permitted(fn): ...@@ -38,11 +38,10 @@ def permitted(fn):
else: else:
content = None content = None
return content return content
if check_permissions_by_view(request.user, kwargs['course_id'], fetch_content(), request.view_name): if check_permissions_by_view(request.user, kwargs['course_id'], fetch_content(), request.view_name):
return fn(request, *args, **kwargs) return fn(request, *args, **kwargs)
else: else:
return JsonError("unauthorized") return JsonError("unauthorized", status=401)
return wrapper return wrapper
def ajax_content_response(request, course_id, content, template_name): def ajax_content_response(request, course_id, content, template_name):
...@@ -63,22 +62,39 @@ def ajax_content_response(request, course_id, content, template_name): ...@@ -63,22 +62,39 @@ def ajax_content_response(request, course_id, content, template_name):
@login_required @login_required
@permitted @permitted
def create_thread(request, course_id, commentable_id): def create_thread(request, course_id, commentable_id):
course = get_course_with_access(request.user, course_id, 'load')
post = request.POST post = request.POST
if course.metadata.get("allow_anonymous", True):
anonymous = post.get('anonymous', 'false').lower() == 'true'
else:
anonymous = False
if course.metadata.get("allow_anonymous_to_peers", False):
anonymous_to_peers = post.get('anonymous_to_peers', 'false').lower() == 'true'
else:
anonymous_to_peers = False
thread = cc.Thread(**extract(post, ['body', 'title', 'tags'])) thread = cc.Thread(**extract(post, ['body', 'title', 'tags']))
thread.update_attributes(**{ thread.update_attributes(**{
'anonymous' : post.get('anonymous', 'false').lower() == 'true', 'anonymous' : anonymous,
'commentable_id' : commentable_id, 'anonymous_to_peers' : anonymous_to_peers,
'course_id' : course_id, 'commentable_id' : commentable_id,
'user_id' : request.user.id, 'course_id' : course_id,
'user_id' : request.user.id,
}) })
thread.save() thread.save()
if post.get('auto_subscribe', 'false').lower() == 'true': if post.get('auto_subscribe', 'false').lower() == 'true':
user = cc.User.from_django_user(request.user) user = cc.User.from_django_user(request.user)
user.follow(thread) user.follow(thread)
courseware_context = get_courseware_context(thread, course)
data = thread.to_dict()
if courseware_context:
data.update(courseware_context)
if request.is_ajax(): if request.is_ajax():
return ajax_content_response(request, course_id, thread.to_dict(), 'discussion/ajax_create_thread.html') return ajax_content_response(request, course_id, data, 'discussion/ajax_create_thread.html')
else: else:
return JsonResponse(utils.safe_content(thread.to_dict())) return JsonResponse(utils.safe_content(data))
@require_POST @require_POST
@login_required @login_required
...@@ -95,8 +111,21 @@ def update_thread(request, course_id, thread_id): ...@@ -95,8 +111,21 @@ def update_thread(request, course_id, thread_id):
def _create_comment(request, course_id, thread_id=None, parent_id=None): def _create_comment(request, course_id, thread_id=None, parent_id=None):
post = request.POST post = request.POST
comment = cc.Comment(**extract(post, ['body'])) comment = cc.Comment(**extract(post, ['body']))
course = get_course_with_access(request.user, course_id, 'load')
if course.metadata.get("allow_anonymous", True):
anonymous = post.get('anonymous', 'false').lower() == 'true'
else:
anonymous = False
if course.metadata.get("allow_anonymous_to_peers", False):
anonymous_to_peers = post.get('anonymous_to_peers', 'false').lower() == 'true'
else:
anonymous_to_peers = False
comment.update_attributes(**{ comment.update_attributes(**{
'anonymous' : post.get('anonymous', 'false').lower() == 'true', 'anonymous' : anonymous,
'anonymous_to_peers' : anonymous_to_peers,
'user_id' : request.user.id, 'user_id' : request.user.id,
'course_id' : course_id, 'course_id' : course_id,
'thread_id' : thread_id, 'thread_id' : thread_id,
...@@ -214,7 +243,7 @@ def undo_vote_for_thread(request, course_id, thread_id): ...@@ -214,7 +243,7 @@ def undo_vote_for_thread(request, course_id, thread_id):
thread = cc.Thread.find(thread_id) thread = cc.Thread.find(thread_id)
user.unvote(thread) user.unvote(thread)
return JsonResponse(utils.safe_content(thread.to_dict())) return JsonResponse(utils.safe_content(thread.to_dict()))
@require_POST @require_POST
@login_required @login_required
...@@ -288,7 +317,7 @@ def update_moderator_status(request, course_id, user_id): ...@@ -288,7 +317,7 @@ def update_moderator_status(request, course_id, user_id):
course = get_course_with_access(request.user, course_id, 'load') course = get_course_with_access(request.user, course_id, 'load')
discussion_user = cc.User(id=user_id, course_id=course_id) discussion_user = cc.User(id=user_id, course_id=course_id)
context = { context = {
'course': course, 'course': course,
'course_id': course_id, 'course_id': course_id,
'user': request.user, 'user': request.user,
'django_user': user, 'django_user': user,
...@@ -327,7 +356,7 @@ def tags_autocomplete(request, course_id): ...@@ -327,7 +356,7 @@ def tags_autocomplete(request, course_id):
@require_POST @require_POST
@login_required @login_required
@csrf.csrf_exempt @csrf.csrf_exempt
def upload(request, course_id):#ajax upload file to a question or answer def upload(request, course_id):#ajax upload file to a question or answer
"""view that handles file upload via Ajax """view that handles file upload via Ajax
""" """
...@@ -337,7 +366,7 @@ def upload(request, course_id):#ajax upload file to a question or answer ...@@ -337,7 +366,7 @@ def upload(request, course_id):#ajax upload file to a question or answer
new_file_name = '' new_file_name = ''
try: try:
# TODO authorization # TODO authorization
#may raise exceptions.PermissionDenied #may raise exceptions.PermissionDenied
#if request.user.is_anonymous(): #if request.user.is_anonymous():
# msg = _('Sorry, anonymous users cannot upload files') # msg = _('Sorry, anonymous users cannot upload files')
# raise exceptions.PermissionDenied(msg) # raise exceptions.PermissionDenied(msg)
...@@ -357,7 +386,7 @@ def upload(request, course_id):#ajax upload file to a question or answer ...@@ -357,7 +386,7 @@ def upload(request, course_id):#ajax upload file to a question or answer
new_file_name = str( new_file_name = str(
time.time() time.time()
).replace( ).replace(
'.', '.',
str(random.randint(0,100000)) str(random.randint(0,100000))
) + file_extension ) + file_extension
...@@ -386,7 +415,7 @@ def upload(request, course_id):#ajax upload file to a question or answer ...@@ -386,7 +415,7 @@ def upload(request, course_id):#ajax upload file to a question or answer
parsed_url = urlparse.urlparse(file_url) parsed_url = urlparse.urlparse(file_url)
file_url = urlparse.urlunparse( file_url = urlparse.urlunparse(
urlparse.ParseResult( urlparse.ParseResult(
parsed_url.scheme, parsed_url.scheme,
parsed_url.netloc, parsed_url.netloc,
parsed_url.path, parsed_url.path,
'', '', '' '', '', ''
......
...@@ -17,12 +17,6 @@ def pluralize(singular_term, count): ...@@ -17,12 +17,6 @@ def pluralize(singular_term, count):
return singular_term + 's' return singular_term + 's'
return singular_term return singular_term
def show_if(text, condition):
if condition:
return text
else:
return ''
# TODO there should be a better way to handle this # TODO there should be a better way to handle this
def include_mustache_templates(): def include_mustache_templates():
mustache_dir = settings.PROJECT_ROOT / 'templates' / 'discussion' / 'mustache' mustache_dir = settings.PROJECT_ROOT / 'templates' / 'discussion' / 'mustache'
...@@ -35,7 +29,7 @@ def include_mustache_templates(): ...@@ -35,7 +29,7 @@ def include_mustache_templates():
return '\n'.join(map(wrap_in_tag, map(strip_file_name, file_contents))) return '\n'.join(map(wrap_in_tag, map(strip_file_name, file_contents)))
def render_content(content, additional_context={}): def render_content(content, additional_context={}):
context = { context = {
'content': extend_content(content), 'content': extend_content(content),
content['type']: True, content['type']: True,
......
...@@ -7,8 +7,10 @@ class Command(BaseCommand): ...@@ -7,8 +7,10 @@ class Command(BaseCommand):
help = 'Seed default permisssions and roles' help = 'Seed default permisssions and roles'
def handle(self, *args, **options): def handle(self, *args, **options):
if len(args) != 1: if len(args) == 0:
raise CommandError("The number of arguments does not match. ") raise CommandError("Please provide a course id")
if len(args) > 1:
raise CommandError("Too many arguments")
course_id = args[0] course_id = args[0]
administrator_role = Role.objects.get_or_create(name="Administrator", course_id=course_id)[0] administrator_role = Role.objects.get_or_create(name="Administrator", course_id=course_id)[0]
moderator_role = Role.objects.get_or_create(name="Moderator", course_id=course_id)[0] moderator_role = Role.objects.get_or_create(name="Moderator", course_id=course_id)[0]
......
...@@ -5,12 +5,13 @@ from student.models import CourseEnrollment ...@@ -5,12 +5,13 @@ from student.models import CourseEnrollment
import logging import logging
from util.cache import cache from util.cache import cache
from django.core import cache
cache = cache.get_cache('default')
def cached_has_permission(user, permission, course_id=None): def cached_has_permission(user, permission, course_id=None):
""" """
Call has_permission if it's not cached. A change in a user's role or Call has_permission if it's not cached. A change in a user's role or
a role's permissions will only become effective after CACHE_LIFESPAN seconds. a role's permissions will only become effective after CACHE_LIFESPAN seconds.
""" """
CACHE_LIFESPAN = 60 CACHE_LIFESPAN = 60
key = "permission_%d_%s_%s" % (user.id, str(course_id), permission) key = "permission_%d_%s_%s" % (user.id, str(course_id), permission)
...@@ -53,8 +54,8 @@ def check_conditions_permissions(user, permissions, course_id, **kwargs): ...@@ -53,8 +54,8 @@ def check_conditions_permissions(user, permissions, course_id, **kwargs):
""" """
Accepts a list of permissions and proceed if any of the permission is valid. Accepts a list of permissions and proceed if any of the permission is valid.
Note that ["can_view", "can_edit"] will proceed if the user has either Note that ["can_view", "can_edit"] will proceed if the user has either
"can_view" or "can_edit" permission. To use AND operator in between, wrap them in "can_view" or "can_edit" permission. To use AND operator in between, wrap them in
a list. a list.
""" """
def test(user, per, operator="or"): def test(user, per, operator="or"):
...@@ -75,18 +76,18 @@ def check_conditions_permissions(user, permissions, course_id, **kwargs): ...@@ -75,18 +76,18 @@ def check_conditions_permissions(user, permissions, course_id, **kwargs):
VIEW_PERMISSIONS = { VIEW_PERMISSIONS = {
'update_thread' : ['edit_content', ['update_thread', 'is_open', 'is_author']], 'update_thread' : ['edit_content', ['update_thread', 'is_open', 'is_author']],
'create_comment' : [["create_comment", "is_open"]], 'create_comment' : [["create_comment", "is_open"]],
'delete_thread' : ['delete_thread'], 'delete_thread' : ['delete_thread', ['update_thread', 'is_author']],
'update_comment' : ['edit_content', ['update_comment', 'is_open', 'is_author']], 'update_comment' : ['edit_content', ['update_comment', 'is_open', 'is_author']],
'endorse_comment' : ['endorse_comment'], 'endorse_comment' : ['endorse_comment'],
'openclose_thread' : ['openclose_thread'], 'openclose_thread' : ['openclose_thread'],
'create_sub_comment': [['create_sub_comment', 'is_open']], 'create_sub_comment': [['create_sub_comment', 'is_open']],
'delete_comment' : ['delete_comment'], 'delete_comment' : ['delete_comment', ['update_comment', 'is_open', 'is_author']],
'vote_for_comment' : [['vote', 'is_open']], 'vote_for_comment' : [['vote', 'is_open']],
'undo_vote_for_comment': [['unvote', 'is_open']], 'undo_vote_for_comment': [['unvote', 'is_open']],
'vote_for_thread' : [['vote', 'is_open']], 'vote_for_thread' : [['vote', 'is_open']],
'undo_vote_for_thread': [['unvote', 'is_open']], 'undo_vote_for_thread': [['unvote', 'is_open']],
'follow_thread' : ['follow_thread'], 'follow_thread' : ['follow_thread'],
'follow_commentable': ['follow_commentable'], 'follow_commentable': ['follow_commentable'],
'follow_user' : ['follow_user'], 'follow_user' : ['follow_user'],
'unfollow_thread' : ['unfollow_thread'], 'unfollow_thread' : ['unfollow_thread'],
'unfollow_commentable': ['unfollow_commentable'], 'unfollow_commentable': ['unfollow_commentable'],
......
...@@ -70,6 +70,7 @@ SECRET_KEY = AUTH_TOKENS['SECRET_KEY'] ...@@ -70,6 +70,7 @@ SECRET_KEY = AUTH_TOKENS['SECRET_KEY']
AWS_ACCESS_KEY_ID = AUTH_TOKENS["AWS_ACCESS_KEY_ID"] AWS_ACCESS_KEY_ID = AUTH_TOKENS["AWS_ACCESS_KEY_ID"]
AWS_SECRET_ACCESS_KEY = AUTH_TOKENS["AWS_SECRET_ACCESS_KEY"] AWS_SECRET_ACCESS_KEY = AUTH_TOKENS["AWS_SECRET_ACCESS_KEY"]
AWS_STORAGE_BUCKET_NAME = 'edxuploads'
DATABASES = AUTH_TOKENS['DATABASES'] DATABASES = AUTH_TOKENS['DATABASES']
......
...@@ -448,7 +448,7 @@ main_vendor_js = [ ...@@ -448,7 +448,7 @@ main_vendor_js = [
'js/vendor/swfobject/swfobject.js', 'js/vendor/swfobject/swfobject.js',
] ]
discussion_js = sorted(glob2.glob(PROJECT_ROOT / 'static/coffee/src/discussion/*.coffee')) discussion_js = sorted(glob2.glob(PROJECT_ROOT / 'static/coffee/src/discussion/**/*.coffee'))
# Load javascript from all of the available xmodules, and # Load javascript from all of the available xmodules, and
# prep it for use in pipeline js # prep it for use in pipeline js
......
...@@ -7,15 +7,14 @@ import settings ...@@ -7,15 +7,14 @@ import settings
class Comment(models.Model): class Comment(models.Model):
accessible_fields = [ accessible_fields = [
'id', 'body', 'anonymous', 'course_id', 'id', 'body', 'anonymous', 'anonymous_to_peers', 'course_id',
'endorsed', 'parent_id', 'thread_id', 'endorsed', 'parent_id', 'thread_id', 'username', 'votes', 'user_id',
'username', 'votes', 'user_id', 'closed', 'closed', 'created_at', 'updated_at', 'depth', 'at_position_list',
'created_at', 'updated_at', 'depth', 'type', 'commentable_id',
'at_position_list', 'type', 'commentable_id',
] ]
updatable_fields = [ updatable_fields = [
'body', 'anonymous', 'course_id', 'closed', 'body', 'anonymous', 'anonymous_to_peers', 'course_id', 'closed',
'user_id', 'endorsed', 'user_id', 'endorsed',
] ]
......
...@@ -6,16 +6,14 @@ import settings ...@@ -6,16 +6,14 @@ import settings
class Thread(models.Model): class Thread(models.Model):
accessible_fields = [ accessible_fields = [
'id', 'title', 'body', 'anonymous', 'id', 'title', 'body', 'anonymous', 'anonymous_to_peers', 'course_id',
'course_id', 'closed', 'tags', 'votes', 'closed', 'tags', 'votes', 'commentable_id', 'username', 'user_id',
'commentable_id', 'username', 'user_id', 'created_at', 'updated_at', 'comments_count', 'at_position_list',
'created_at', 'updated_at', 'comments_count', 'children', 'type', 'highlighted_title', 'highlighted_body', 'endorsed'
'at_position_list', 'children', 'type',
'highlighted_title', 'highlighted_body',
] ]
updatable_fields = [ updatable_fields = [
'title', 'body', 'anonymous', 'course_id', 'title', 'body', 'anonymous', 'anonymous_to_peers', 'course_id',
'closed', 'tags', 'user_id', 'commentable_id', 'closed', 'tags', 'user_id', 'commentable_id',
] ]
...@@ -32,7 +30,7 @@ class Thread(models.Model): ...@@ -32,7 +30,7 @@ class Thread(models.Model):
'course_id': query_params['course_id'], 'course_id': query_params['course_id'],
'recursive': False} 'recursive': False}
params = merge_dict(default_params, strip_blank(strip_none(query_params))) params = merge_dict(default_params, strip_blank(strip_none(query_params)))
if query_params.get('text') or query_params.get('tags'): if query_params.get('text') or query_params.get('tags') or query_params.get('commentable_ids'):
url = cls.url(action='search') url = cls.url(action='search')
else: else:
url = cls.url(action='get_all', params=extract(params, 'commentable_id')) url = cls.url(action='get_all', params=extract(params, 'commentable_id'))
...@@ -40,7 +38,7 @@ class Thread(models.Model): ...@@ -40,7 +38,7 @@ class Thread(models.Model):
del params['commentable_id'] del params['commentable_id']
response = perform_request('get', url, params, *args, **kwargs) response = perform_request('get', url, params, *args, **kwargs)
return response.get('collection', []), response.get('page', 1), response.get('num_pages', 1) return response.get('collection', []), response.get('page', 1), response.get('num_pages', 1)
@classmethod @classmethod
def url_for_threads(cls, params={}): def url_for_threads(cls, params={}):
if params.get('commentable_id'): if params.get('commentable_id'):
......
...@@ -8,7 +8,8 @@ class User(models.Model): ...@@ -8,7 +8,8 @@ class User(models.Model):
accessible_fields = ['username', 'email', 'follower_ids', 'upvoted_ids', 'downvoted_ids', accessible_fields = ['username', 'email', 'follower_ids', 'upvoted_ids', 'downvoted_ids',
'id', 'external_id', 'subscribed_user_ids', 'children', 'course_id', 'id', 'external_id', 'subscribed_user_ids', 'children', 'course_id',
'subscribed_thread_ids', 'subscribed_commentable_ids', 'subscribed_thread_ids', 'subscribed_commentable_ids',
'threads_count', 'comments_count', 'default_sort_key' 'subscribed_course_ids', 'threads_count', 'comments_count',
'default_sort_key'
] ]
updatable_fields = ['username', 'external_id', 'email', 'default_sort_key'] updatable_fields = ['username', 'external_id', 'email', 'default_sort_key']
......
...@@ -28,9 +28,9 @@ def perform_request(method, url, data_or_params=None, *args, **kwargs): ...@@ -28,9 +28,9 @@ def perform_request(method, url, data_or_params=None, *args, **kwargs):
data_or_params['api_key'] = settings.API_KEY data_or_params['api_key'] = settings.API_KEY
try: try:
if method in ['post', 'put', 'patch']: if method in ['post', 'put', 'patch']:
response = requests.request(method, url, data=data_or_params) response = requests.request(method, url, data=data_or_params, timeout=5)
else: else:
response = requests.request(method, url, params=data_or_params) response = requests.request(method, url, params=data_or_params, timeout=5)
except Exception as err: except Exception as err:
log.exception("Trying to call {method} on {url} with params {params}".format( log.exception("Trying to call {method} on {url} with params {params}".format(
method=method, url=url, params=data_or_params)) method=method, url=url, params=data_or_params))
......
...@@ -45,6 +45,7 @@ $ -> ...@@ -45,6 +45,7 @@ $ ->
removeMath: (text) -> removeMath: (text) ->
text = text || ""
@math = [] @math = []
start = end = last = null start = end = last = null
braces = 0 braces = 0
...@@ -111,7 +112,7 @@ $ -> ...@@ -111,7 +112,7 @@ $ ->
(text) -> _this.replaceMath(text) (text) -> _this.replaceMath(text)
if Markdown? if Markdown?
Markdown.getMathCompatibleConverter = (postProcessor) -> Markdown.getMathCompatibleConverter = (postProcessor) ->
postProcessor ||= ((text) -> text) postProcessor ||= ((text) -> text)
converter = Markdown.getSanitizingConverter() converter = Markdown.getSanitizingConverter()
...@@ -123,11 +124,9 @@ $ -> ...@@ -123,11 +124,9 @@ $ ->
Markdown.makeWmdEditor = (elem, appended_id, imageUploadUrl, postProcessor) -> Markdown.makeWmdEditor = (elem, appended_id, imageUploadUrl, postProcessor) ->
$elem = $(elem) $elem = $(elem)
if not $elem.length if not $elem.length
console.log "warning: elem for makeWmdEditor doesn't exist" console.log "warning: elem for makeWmdEditor doesn't exist"
return return
if not $elem.find(".wmd-panel").length if not $elem.find(".wmd-panel").length
initialText = $elem.html() initialText = $elem.html()
$elem.empty() $elem.empty()
...@@ -162,7 +161,7 @@ $ -> ...@@ -162,7 +161,7 @@ $ ->
alert(e) alert(e)
if startUploadHandler if startUploadHandler
$('#file-upload').unbind('change').change(startUploadHandler) $('#file-upload').unbind('change').change(startUploadHandler)
imageUploadHandler = (elem, input) -> imageUploadHandler = (elem, input) ->
ajaxFileUpload(imageUploadUrl, input, imageUploadHandler) ajaxFileUpload(imageUploadUrl, input, imageUploadHandler)
......
...@@ -2,185 +2,67 @@ if Backbone? ...@@ -2,185 +2,67 @@ if Backbone?
class @Discussion extends Backbone.Collection class @Discussion extends Backbone.Collection
model: Thread model: Thread
initialize: -> initialize: (models, options={})->
DiscussionUtil.addDiscussion @id, @ @pages = options['pages'] || 1
@current_page = 1
@bind "add", (item) => @bind "add", (item) =>
item.discussion = @ item.discussion = @
@comparator = @sortByDateRecentFirst
@on "thread:remove", (thread) =>
@remove(thread)
find: (id) -> find: (id) ->
_.first @where(id: id) _.first @where(id: id)
addThread: (thread, options) -> hasMorePages: ->
options ||= {} @current_page < @pages
model = new Thread thread
@add model
model
class @DiscussionView extends Backbone.View
$: (selector) ->
@$local.find(selector)
initLocal: ->
@$local = @$el.children(".local")
@$delegateElement = @$local
initialize: ->
@initLocal()
@model.id = @$el.attr("_id")
@model.view = @
@$el.children(".threads").children(".thread").each (index, elem) =>
threadView = new ThreadView el: elem, model: @model.find $(elem).attr("_id")
if @$el.hasClass("forum-discussion")
$(".discussion-sidebar").find(".sidebar-new-post-button")
.unbind('click').click $.proxy @newPost, @
else if @$el.hasClass("inline-discussion")
@newPost()
reload: ($elem, url) ->
if not url then return
DiscussionUtil.safeAjax
$elem: $elem
$loading: $elem
loadingCallback: ->
$(this).parent().append("<span class='discussion-loading'></span>")
loadedCallback: ->
$(this).parent().children(".discussion-loading").remove()
url: url
type: "GET"
success: (response, textStatus) =>
$parent = @$el.parent()
@$el.replaceWith(response.html)
$discussion = $parent.find("section.discussion")
@model.reset(response.discussion_data, { silent: false })
view = new DiscussionView el: $discussion[0], model: @model
DiscussionUtil.bulkUpdateContentInfo(window.$$annotated_content_info)
$("html, body").animate({ scrollTop: 0 }, 0)
loadSimilarPost: (event) ->
console.log "loading similar"
$title = @$(".new-post-title")
$wrapper = @$(".new-post-similar-posts-wrapper")
$similarPosts = @$(".new-post-similar-posts")
prevText = $title.attr("prev-text")
text = $title.val()
if text == prevText
if @$(".similar-post").length
$wrapper.show()
else if $.trim(text).length
$elem = $(event.target)
url = DiscussionUtil.urlFor 'search_similar_threads', @model.id
data = { text: @$(".new-post-title").val() }
DiscussionUtil.safeAjax
$elem: $elem
url: url
data: data
dataType: 'json'
success: (response, textStatus) =>
$wrapper.html(response.html)
if $wrapper.find(".similar-post").length
$wrapper.show()
$wrapper.find(".hide-similar-posts").click =>
$wrapper.hide()
else
$wrapper.hide()
$title.attr("prev-text", text)
newPost: ->
if not @$(".wmd-panel").length
view = { discussion_id: @model.id }
@$el.children(".discussion-non-content").append Mustache.render DiscussionUtil.getTemplate("_new_post"), view
$newPostBody = @$(".new-post-body")
DiscussionUtil.makeWmdEditor @$el, $.proxy(@$, @), "new-post-body"
$input = DiscussionUtil.getWmdInput @$el, $.proxy(@$, @), "new-post-body"
$input.attr("placeholder", "post a new topic...")
if @$el.hasClass("inline-discussion")
$input.bind 'focus', (e) =>
@$(".new-post-form").removeClass('collapsed')
else if @$el.hasClass("forum-discussion")
@$(".new-post-form").removeClass('collapsed')
@$(".new-post-tags").tagsInput DiscussionUtil.tagsInputOptions()
@$(".new-post-title").blur $.proxy(@loadSimilarPost, @) addThread: (thread, options) ->
# TODO: Check for existing thread with same ID in a faster way
@$(".hide-similar-posts").click => if not @find(thread.id)
@$(".new-post-similar-posts-wrapper").hide() options ||= {}
model = new Thread thread
@$(".discussion-submit-post").click $.proxy(@submitNewPost, @) @add model
@$(".discussion-cancel-post").click $.proxy(@cancelNewPost, @) model
retrieveAnotherPage: (search_text="", commentable_ids="", sort_key="")->
@$el.children(".blank").hide() # TODO: I really feel that this belongs in DiscussionThreadListView
@$(".new-post-form").show() @current_page += 1
url = DiscussionUtil.urlFor 'threads'
submitNewPost: (event) -> data = { page: @current_page }
title = @$(".new-post-title").val() if search_text
body = DiscussionUtil.getWmdContent @$el, $.proxy(@$, @), "new-post-body" data['text'] = search_text
tags = @$(".new-post-tags").val() if sort_key
anonymous = false || @$(".discussion-post-anonymously").is(":checked") data['sort_key'] = sort_key
autowatch = false || @$(".discussion-auto-watch").is(":checked") if commentable_ids
url = DiscussionUtil.urlFor('create_thread', @model.id) data['commentable_ids'] = commentable_ids
DiscussionUtil.safeAjax DiscussionUtil.safeAjax
$elem: $(event.target) $elem: @$el
$loading: $(event.target) if event
url: url url: url
type: "POST" data: data
dataType: 'json' dataType: 'json'
data:
title: title
body: body
tags: tags
anonymous: anonymous
auto_subscribe: autowatch
error: DiscussionUtil.formErrorHandler(@$(".new-post-form-errors"))
success: (response, textStatus) => success: (response, textStatus) =>
DiscussionUtil.clearFormErrors(@$(".new-post-form-errors")) models = @models
$thread = $(response.html) new_threads = [new Thread(data) for data in response.discussion_data][0]
@$el.children(".threads").prepend($thread) new_collection = _.union(models, new_threads)
@reset new_collection
@$el.children(".blank").remove()
sortByDate: (thread) ->
@$(".new-post-similar-posts").empty() thread.get("created_at")
@$(".new-post-similar-posts-wrapper").hide()
@$(".new-post-title").val("").attr("prev-text", "") sortByDateRecentFirst: (thread) ->
DiscussionUtil.setWmdContent @$el, $.proxy(@$, @), "new-post-body", "" -(new Date(thread.get("created_at")).getTime())
@$(".new-post-tags").val("") #return String.fromCharCode.apply(String,
@$(".new-post-tags").importTags("") # _.map(thread.get("created_at").split(""),
# ((c) -> return 0xffff - c.charChodeAt()))
thread = @model.addThread response.content #)
threadView = new ThreadView el: $thread[0], model: thread
thread.updateInfo response.annotated_content_info sortByVotes: (thread1, thread2) ->
@cancelNewPost() thread1_count = parseInt(thread1.get("votes")['up_count'])
thread2_count = parseInt(thread2.get("votes")['up_count'])
thread2_count - thread1_count
cancelNewPost: (event) ->
if @$el.hasClass("inline-discussion") sortByComments: (thread1, thread2) ->
@$(".new-post-form").addClass("collapsed") thread1_count = parseInt(thread1.get("comments_count"))
else if @$el.hasClass("forum-discussion") thread2_count = parseInt(thread2.get("comments_count"))
@$(".new-post-form").hide() thread2_count - thread1_count
@$el.children(".blank").show()
search: (event) ->
event.preventDefault()
$elem = $(event.target)
url = URI($elem.attr("action")).addSearch({text: @$(".search-input").val()})
@reload($elem, url)
sort: (event) ->
$elem = $(event.target)
url = $elem.attr("sort-url")
@reload($elem, url)
page: (event) ->
$elem = $(event.target)
url = $elem.attr("page-url")
@reload($elem, url)
events:
"submit .search-wrapper>.discussion-search-form": "search"
"click .discussion-search-link": "search"
"click .discussion-sort-link": "sort"
"click .discussion-page-link": "page"
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
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.discussion_data, {silent: false})
view = new DiscussionView(el: $discussion[0], model: discussion)
DiscussionUtil.bulkUpdateContentInfo(window.$$annotated_content_info)
@retrieved = true
@showed = true
if Backbone?
class @DiscussionModuleView extends Backbone.View
events:
"click .discussion-show": "toggleDiscussion"
"click .new-post-btn": "toggleNewPost"
"click .new-post-cancel": "hideNewPost"
"click .discussion-paginator a": "navigateToPage"
paginationTemplate: -> DiscussionUtil.getTemplate("_pagination")
page_re: /\?discussion_page=(\d+)/
initialize: ->
@toggleDiscussionBtn = @$(".discussion-show")
# Set the page if it was set in the URL. This is used to allow deep linking to pages
match = @page_re.exec(window.location.href)
if match
@page = parseInt(match[1])
else
@page = 1
toggleNewPost: (event) ->
event.preventDefault()
if !@newPostForm
@toggleDiscussion()
@isWaitingOnNewPost = true;
return
if @showed
@newPostForm.slideDown(300)
else
@newPostForm.show()
@toggleDiscussionBtn.addClass('shown')
@toggleDiscussionBtn.find('.button-text').html("Hide Discussion")
@$("section.discussion").slideDown()
@showed = true
hideNewPost: (event) ->
event.preventDefault()
@newPostForm.slideUp(300)
toggleDiscussion: (event) ->
if @showed
@$("section.discussion").slideUp()
@toggleDiscussionBtn.removeClass('shown')
@toggleDiscussionBtn.find('.button-text').html("Show Discussion")
@showed = false
else
@toggleDiscussionBtn.addClass('shown')
@toggleDiscussionBtn.find('.button-text').html("Hide Discussion")
if @retrieved
@$("section.discussion").slideDown()
@showed = true
else
$elem = @toggleDiscussionBtn
@loadPage $elem
loadPage: ($elem)=>
discussionId = @$el.data("discussion-id")
url = DiscussionUtil.urlFor('retrieve_discussion', discussionId) + "?page=#{@page}"
DiscussionUtil.safeAjax
$elem: $elem
$loading: $elem
url: url
type: "GET"
dataType: 'json'
success: (response, textStatus, jqXHR) => @renderDiscussion($elem, response, textStatus, discussionId)
renderDiscussion: ($elem, response, textStatus, discussionId) =>
window.user = new DiscussionUser(response.user_info)
Content.loadContentInfos(response.annotated_content_info)
DiscussionUtil.loadRoles(response.roles)
allow_anonymous = response.allow_anonymous
allow_anonymous_to_peers = response.allow_anonymous_to_peers
# $elem.html("Hide Discussion")
@discussion = new Discussion()
@discussion.reset(response.discussion_data, {silent: false})
$discussion = $(Mustache.render $("script#_inline_discussion").html(), {'threads':response.discussion_data, 'discussionId': discussionId, 'allow_anonymous_to_peers': allow_anonymous_to_peers, 'allow_anonymous': allow_anonymous})
if @$('section.discussion').length
@$('section.discussion').replaceWith($discussion)
else
$(".discussion-module").append($discussion)
@newPostForm = $('.new-post-article')
@threadviews = @discussion.map (thread) ->
new DiscussionThreadInlineView el: @$("article#thread_#{thread.id}"), model: thread
_.each @threadviews, (dtv) -> dtv.render()
DiscussionUtil.bulkUpdateContentInfo(window.$$annotated_content_info)
@newPostView = new NewPostInlineView el: @$('.new-post-article'), collection: @discussion
@discussion.on "add", @addThread
@retrieved = true
@showed = true
@renderPagination(2, response.num_pages)
if @isWaitingOnNewPost
@newPostForm.show()
addThread: (thread, collection, options) =>
# TODO: When doing pagination, this will need to repaginate. Perhaps just reload page 1?
article = $("<article class='discussion-thread' id='thread_#{thread.id}'></article>")
@$('section.discussion > .threads').prepend(article)
threadView = new DiscussionThreadInlineView el: article, model: thread
threadView.render()
@threadviews.unshift threadView
renderPagination: (delta, numPages) =>
minPage = Math.max(@page - delta, 1)
maxPage = Math.min(@page + delta, numPages)
pageUrl = (number) ->
"?discussion_page=#{number}"
params =
page: @page
lowPages: _.range(minPage, @page).map (n) -> {number: n, url: pageUrl(n)}
highPages: _.range(@page+1, maxPage+1).map (n) -> {number: n, url: pageUrl(n)}
previous: if @page-1 >= 1 then {url: pageUrl(@page-1), number: @page-1} else false
next: if @page+1 <= numPages then {url: pageUrl(@page+1), number: @page+1} else false
leftdots: minPage > 2
rightdots: maxPage < numPages-1
first: if minPage > 1 then {url: pageUrl(1)} else false
last: if maxPage < numPages then {number: numPages, url: pageUrl(numPages)} else false
thing = Mustache.render @paginationTemplate(), params
@$('section.pagination').html(thing)
navigateToPage: (event) =>
event.preventDefault()
window.history.pushState({}, window.document.title, event.target.href)
@page = $(event.target).data('page-number')
@loadPage($(event.target))
if Backbone?
class @DiscussionRouter extends Backbone.Router
routes:
"": "allThreads"
":forum_name/threads/:thread_id" : "showThread"
initialize: (options) ->
@discussion = options['discussion']
@nav = new DiscussionThreadListView(collection: @discussion, el: $(".sidebar"))
@nav.on "thread:selected", @navigateToThread
@nav.on "thread:removed", @navigateToAllThreads
@nav.on "threads:rendered", @setActiveThread
@nav.render()
@newPostView = new NewPostView(el: $(".new-post-article"), collection: @discussion)
@nav.on "thread:created", @navigateToThread
allThreads: ->
@nav.updateSidebar()
setActiveThread: =>
if @thread
@nav.setActiveThread(@thread.get("id"))
showThread: (forum_name, thread_id) ->
@thread = @discussion.get(thread_id)
@setActiveThread()
if(@main)
@main.cleanup()
@main.undelegateEvents()
@main = new DiscussionThreadView(el: $(".discussion-column"), model: @thread)
@main.render()
@main.on "thread:responses:rendered", =>
@nav.updateSidebar()
@main.on "tag:selected", (tag) =>
search = "[#{tag}]"
@nav.setAndSearchFor(search)
navigateToThread: (thread_id) =>
thread = @discussion.get(thread_id)
@navigate("#{thread.get("commentable_id")}/threads/#{thread_id}", trigger: true)
navigateToAllThreads: =>
@navigate("", trigger: true)
$ -> if Backbone?
DiscussionApp =
window.$$contents = {} start: (elem)->
window.$$discussions = {} # TODO: Perhaps eliminate usage of global variables when possible
DiscussionUtil.loadRolesFromContainer()
$("section.discussion").each (index, elem) -> element = $(elem)
discussionData = DiscussionUtil.getDiscussionData($(elem).attr("_id")) window.$$course_id = element.data("course-id")
discussion = new Discussion() user_info = element.data("user-info")
discussion.reset(discussionData, {silent: false}) threads = element.data("threads")
view = new DiscussionView(el: elem, model: discussion) thread_pages = element.data("thread-pages")
content_info = element.data("content-info")
if window.$$annotated_content_info? window.user = new DiscussionUser(user_info)
DiscussionUtil.bulkUpdateContentInfo(window.$$annotated_content_info) Content.loadContentInfos(content_info)
discussion = new Discussion(threads, pages: thread_pages)
$userProfile = $(".discussion-sidebar>.user-profile") new DiscussionRouter({discussion: discussion})
if $userProfile.length Backbone.history.start({pushState: true, root: "/courses/#{$$course_id}/discussion/forum/"})
console.log "initialize user profile" DiscussionProfileApp =
view = new DiscussionUserProfileView(el: $userProfile[0]) start: (elem) ->
element = $(elem)
window.$$course_id = element.data("course-id")
threads = element.data("threads")
user_info = element.data("user-info")
window.user = new DiscussionUser(user_info)
new DiscussionUserProfileView(el: element, collection: threads)
$ ->
$("section.discussion").each (index, elem) ->
DiscussionApp.start(elem)
$("section.discussion-user-threads").each (index, elem) ->
DiscussionProfileApp.start(elem)
if Backbone?
class @DiscussionUser extends Backbone.Model
following: (thread) ->
_.include(@get('subscribed_thread_ids'), thread.id)
voted: (thread) ->
_.include(@get('upvoted_ids'), thread.id)
vote: (thread) ->
@get('upvoted_ids').push(thread.id)
thread.vote()
unvote: (thread) ->
@set('upvoted_ids', _.without(@get('upvoted_ids'), thread.id))
thread.unvote()
class @DiscussionUserProfileView extends Backbone.View if Backbone?
toggleModeratorStatus: (event) -> class @DiscussionUserProfileView extends Backbone.View
confirmValue = confirm("Are you sure?") toggleModeratorStatus: (event) ->
if not confirmValue then return confirmValue = confirm("Are you sure?")
$elem = $(event.target) if not confirmValue then return
if $elem.hasClass("sidebar-promote-moderator-button") $elem = $(event.target)
isModerator = true if $elem.hasClass("sidebar-promote-moderator-button")
else if $elem.hasClass("sidebar-revoke-moderator-button") isModerator = true
isModerator = false else if $elem.hasClass("sidebar-revoke-moderator-button")
else isModerator = false
console.error "unrecognized moderator status" else
return console.error "unrecognized moderator status"
url = DiscussionUtil.urlFor('update_moderator_status', $$profiled_user_id) return
DiscussionUtil.safeAjax url = DiscussionUtil.urlFor('update_moderator_status', $$profiled_user_id)
$elem: $elem DiscussionUtil.safeAjax
url: url $elem: $elem
type: "POST" url: url
dataType: 'json' type: "POST"
data: dataType: 'json'
is_moderator: isModerator data:
error: (response, textStatus, e) -> is_moderator: isModerator
console.log e error: (response, textStatus, e) ->
success: (response, textStatus) => console.log e
parent = @$el.parent() success: (response, textStatus) =>
@$el.replaceWith(response.html) parent = @$el.parent()
view = new DiscussionUserProfileView el: parent.children(".user-profile") @$el.replaceWith(response.html)
view = new DiscussionUserProfileView el: parent.children(".user-profile")
events: events:
"click .sidebar-toggle-moderator-button": "toggleModeratorStatus" "click .sidebar-toggle-moderator-button": "toggleModeratorStatus"
$ -> $ ->
if !window.$$contents
window.$$contents = {}
$.fn.extend $.fn.extend
loading: -> loading: ->
@$_loading = $("<span class='discussion-loading'></span>") @$_loading = $("<div class='loading-animation'></div>")
$(this).after(@$_loading) $(this).after(@$_loading)
loaded: -> loaded: ->
@$_loading.remove() @$_loading.remove()
...@@ -13,27 +15,26 @@ class @DiscussionUtil ...@@ -13,27 +15,26 @@ class @DiscussionUtil
@getTemplate: (id) -> @getTemplate: (id) ->
$("script##{id}").html() $("script##{id}").html()
@getDiscussionData: (id) -> @loadRoles: (roles)->
return $$discussion_data[id] @roleIds = roles
@addContent: (id, content) -> window.$$contents[id] = content @loadRolesFromContainer: ->
@loadRoles($("#discussion-container").data("roles"))
@getContent: (id) -> window.$$contents[id] @isStaff: (user_id) ->
staff = _.union(@roleIds['Staff'], @roleIds['Moderator'], @roleIds['Administrator'])
_.include(staff, parseInt(user_id))
@addDiscussion: (id, discussion) -> window.$$discussions[id] = discussion
@getDiscussion: (id) -> window.$$discussions[id]
@bulkUpdateContentInfo: (infos) -> @bulkUpdateContentInfo: (infos) ->
for id, info of infos for id, info of infos
@getContent(id).updateInfo(info) Content.getContent(id).updateInfo(info)
@generateDiscussionLink: (cls, txt, handler) -> @generateDiscussionLink: (cls, txt, handler) ->
$("<a>").addClass("discussion-link") $("<a>").addClass("discussion-link")
.attr("href", "javascript:void(0)") .attr("href", "javascript:void(0)")
.addClass(cls).html(txt) .addClass(cls).html(txt)
.click -> handler(this) .click -> handler(this)
@urlFor: (name, param, param1, param2) -> @urlFor: (name, param, param1, param2) ->
{ {
follow_discussion : "/courses/#{$$course_id}/discussion/#{param}/follow" follow_discussion : "/courses/#{$$course_id}/discussion/#{param}/follow"
...@@ -64,26 +65,32 @@ class @DiscussionUtil ...@@ -64,26 +65,32 @@ class @DiscussionUtil
openclose_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/close" openclose_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/close"
permanent_link_thread : "/courses/#{$$course_id}/discussion/forum/#{param}/threads/#{param1}" permanent_link_thread : "/courses/#{$$course_id}/discussion/forum/#{param}/threads/#{param1}"
permanent_link_comment : "/courses/#{$$course_id}/discussion/forum/#{param}/threads/#{param1}##{param2}" permanent_link_comment : "/courses/#{$$course_id}/discussion/forum/#{param}/threads/#{param1}##{param2}"
user_profile : "/courses/#{$$course_id}/discussion/forum/users/#{param}"
threads : "/courses/#{$$course_id}/discussion/forum"
}[name] }[name]
@safeAjax: (params) -> @safeAjax: (params) ->
$elem = params.$elem $elem = params.$elem
if $elem.attr("disabled") if $elem and $elem.attr("disabled")
return return
params["url"] = URI(params["url"]).addSearch ajax: 1
params["beforeSend"] = -> params["beforeSend"] = ->
$elem.attr("disabled", "disabled") if $elem
$elem.attr("disabled", "disabled")
if params["$loading"] if params["$loading"]
if params["loadingCallback"]? if params["loadingCallback"]?
params["loadingCallback"].apply(params["$loading"]) params["loadingCallback"].apply(params["$loading"])
else else
params["$loading"].loading() params["$loading"].loading()
$.ajax(params).always -> request = $.ajax(params).always ->
$elem.removeAttr("disabled") if $elem
$elem.removeAttr("disabled")
if params["$loading"] if params["$loading"]
if params["loadedCallback"]? if params["loadedCallback"]?
params["loadedCallback"].apply(params["$loading"]) params["loadedCallback"].apply(params["$loading"])
else else
params["$loading"].loaded() params["$loading"].loaded()
return request
@get: ($elem, url, data, success) -> @get: ($elem, url, data, success) ->
@safeAjax @safeAjax
...@@ -108,6 +115,9 @@ class @DiscussionUtil ...@@ -108,6 +115,9 @@ class @DiscussionUtil
[event, selector] = eventSelector.split(' ') [event, selector] = eventSelector.split(' ')
$local(selector).unbind(event)[event] handler $local(selector).unbind(event)[event] handler
@processTag: (text) ->
text.toLowerCase()
@tagsInputOptions: -> @tagsInputOptions: ->
autocomplete_url: @urlFor('tags_autocomplete') autocomplete_url: @urlFor('tags_autocomplete')
autocomplete: autocomplete:
...@@ -117,6 +127,7 @@ class @DiscussionUtil ...@@ -117,6 +127,7 @@ class @DiscussionUtil
width: '100%' width: '100%'
defaultText: "Tag your post: press enter after each tag" defaultText: "Tag your post: press enter after each tag"
removeWithBackspace: true removeWithBackspace: true
preprocessTag: @processTag
@formErrorHandler: (errorsField) -> @formErrorHandler: (errorsField) ->
(xhr, textStatus, error) -> (xhr, textStatus, error) ->
...@@ -124,11 +135,11 @@ class @DiscussionUtil ...@@ -124,11 +135,11 @@ class @DiscussionUtil
if response.errors? and response.errors.length > 0 if response.errors? and response.errors.length > 0
errorsField.empty() errorsField.empty()
for error in response.errors for error in response.errors
errorsField.append($("<li>").addClass("new-post-form-error").html(error)) errorsField.append($("<li>").addClass("new-post-form-error").html(error)).show()
@clearFormErrors: (errorsField) -> @clearFormErrors: (errorsField) ->
errorsField.empty() errorsField.empty()
@postMathJaxProcessor: (text) -> @postMathJaxProcessor: (text) ->
RE_INLINEMATH = /^\$([^\$]*)\$/g RE_INLINEMATH = /^\$([^\$]*)\$/g
RE_DISPLAYMATH = /^\$\$([^\$]*)\$\$/g RE_DISPLAYMATH = /^\$\$([^\$]*)\$\$/g
...@@ -144,21 +155,26 @@ class @DiscussionUtil ...@@ -144,21 +155,26 @@ class @DiscussionUtil
@makeWmdEditor: ($content, $local, cls_identifier) -> @makeWmdEditor: ($content, $local, cls_identifier) ->
elem = $local(".#{cls_identifier}") elem = $local(".#{cls_identifier}")
id = $content.attr("_id") placeholder = elem.data('placeholder')
id = elem.data("id")
appended_id = "-#{cls_identifier}-#{id}" appended_id = "-#{cls_identifier}-#{id}"
imageUploadUrl = @urlFor('upload') imageUploadUrl = @urlFor('upload')
_processor = (_this) -> _processor = (_this) ->
(text) -> _this.postMathJaxProcessor(text) (text) -> _this.postMathJaxProcessor(text)
editor = Markdown.makeWmdEditor elem, appended_id, imageUploadUrl, _processor(@) editor = Markdown.makeWmdEditor elem, appended_id, imageUploadUrl, _processor(@)
@wmdEditors["#{cls_identifier}-#{id}"] = editor @wmdEditors["#{cls_identifier}-#{id}"] = editor
if placeholder?
elem.find("#wmd-input#{appended_id}").attr('placeholder', placeholder)
editor editor
@getWmdEditor: ($content, $local, cls_identifier) -> @getWmdEditor: ($content, $local, cls_identifier) ->
id = $content.attr("_id") elem = $local(".#{cls_identifier}")
id = elem.data("id")
@wmdEditors["#{cls_identifier}-#{id}"] @wmdEditors["#{cls_identifier}-#{id}"]
@getWmdInput: ($content, $local, cls_identifier) -> @getWmdInput: ($content, $local, cls_identifier) ->
id = $content.attr("_id") elem = $local(".#{cls_identifier}")
id = elem.data("id")
$local("#wmd-input-#{cls_identifier}-#{id}") $local("#wmd-input-#{cls_identifier}-#{id}")
@getWmdContent: ($content, $local, cls_identifier) -> @getWmdContent: ($content, $local, cls_identifier) ->
...@@ -199,9 +215,9 @@ class @DiscussionUtil ...@@ -199,9 +215,9 @@ class @DiscussionUtil
unfollowLink() unfollowLink()
else else
followLink() followLink()
@processEachMathAndCode: (text, processor) -> @processEachMathAndCode: (text, processor) ->
codeArchive = [] codeArchive = []
RE_DISPLAYMATH = /^([^\$]*?)\$\$([^\$]*?)\$\$(.*)$/m RE_DISPLAYMATH = /^([^\$]*?)\$\$([^\$]*?)\$\$(.*)$/m
...@@ -264,5 +280,17 @@ class @DiscussionUtil ...@@ -264,5 +280,17 @@ class @DiscussionUtil
@processEachMathAndCode text, @stripHighlight @processEachMathAndCode text, @stripHighlight
@markdownWithHighlight: (text) -> @markdownWithHighlight: (text) ->
text = text.replace(/^\&gt\;/gm, ">")
converter = Markdown.getMathCompatibleConverter() converter = Markdown.getMathCompatibleConverter()
@unescapeHighlightTag @stripLatexHighlight converter.makeHtml text text = @unescapeHighlightTag @stripLatexHighlight converter.makeHtml text
return text.replace(/^>/gm,"&gt;")
@abbreviateString: (text, minLength) ->
# Abbreviates a string to at least minLength characters, stopping at word boundaries
if text.length<minLength
return text
else
while minLength < text.length && text[minLength] != ' '
minLength++
return text.substr(0, minLength) + '...'
if Backbone?
class @DiscussionContentView extends Backbone.View
attrRenderer:
endorsed: (endorsed) ->
if endorsed
@$(".action-endorse").show().addClass("is-endorsed")
else
if @model.get('ability')?.can_endorse
@$(".action-endorse").show()
else
@$(".action-endorse").hide()
@$(".action-endorse").removeClass("is-endorsed")
closed: (closed) ->
return if not @$(".action-openclose").length
return if not @$(".post-status-closed").length
if closed
@$(".post-status-closed").show()
@$(".action-openclose").html(@$(".action-openclose").html().replace("Close", "Open"))
@$(".discussion-reply-new").hide()
else
@$(".post-status-closed").hide()
@$(".action-openclose").html(@$(".action-openclose").html().replace("Open", "Close"))
@$(".discussion-reply-new").show()
voted: (voted) ->
votes_point: (votes_point) ->
comments_count: (comments_count) ->
subscribed: (subscribed) ->
if subscribed
@$(".dogear").addClass("is-followed")
else
@$(".dogear").removeClass("is-followed")
ability: (ability) ->
for action, selector of @abilityRenderer
if not ability[action]
selector.disable.apply(@)
else
selector.enable.apply(@)
abilityRenderer:
editable:
enable: -> @$(".action-edit").closest("li").show()
disable: -> @$(".action-edit").closest("li").hide()
can_delete:
enable: -> @$(".action-delete").closest("li").show()
disable: -> @$(".action-delete").closest("li").hide()
can_endorse:
enable: ->
@$(".action-endorse").show().css("cursor", "auto")
disable: ->
@$(".action-endorse").css("cursor", "default")
if not @model.get('endorsed')
@$(".action-endorse").hide()
else
@$(".action-endorse").show()
can_openclose:
enable: -> @$(".action-openclose").closest("li").show()
disable: -> @$(".action-openclose").closest("li").hide()
renderPartialAttrs: ->
for attr, value of @model.changedAttributes()
if @attrRenderer[attr]
@attrRenderer[attr].apply(@, [value])
renderAttrs: ->
for attr, value of @model.attributes
if @attrRenderer[attr]
@attrRenderer[attr].apply(@, [value])
$: (selector) ->
@$local.find(selector)
initLocal: ->
@$local = @$el.children(".local")
if not @$local.length
@$local = @$el
@$delegateElement = @$local
makeWmdEditor: (cls_identifier) =>
if not @$el.find(".wmd-panel").length
DiscussionUtil.makeWmdEditor @$el, $.proxy(@$, @), cls_identifier
getWmdEditor: (cls_identifier) =>
DiscussionUtil.getWmdEditor @$el, $.proxy(@$, @), cls_identifier
getWmdContent: (cls_identifier) =>
DiscussionUtil.getWmdContent @$el, $.proxy(@$, @), cls_identifier
setWmdContent: (cls_identifier, text) =>
DiscussionUtil.setWmdContent @$el, $.proxy(@$, @), cls_identifier, text
initialize: ->
@initLocal()
@model.bind('change', @renderPartialAttrs, @)
if Backbone?
class @DiscussionThreadEditView extends Backbone.View
events:
"click .post-update": "update"
"click .post-cancel": "cancel_edit"
$: (selector) ->
@$el.find(selector)
initialize: ->
super()
render: ->
@template = _.template($("#thread-edit-template").html())
@$el.html(@template(@model.toJSON()))
@delegateEvents()
DiscussionUtil.makeWmdEditor @$el, $.proxy(@$, @), "edit-post-body"
@$(".edit-post-tags").tagsInput DiscussionUtil.tagsInputOptions()
@
update: (event) ->
@trigger "thread:update", event
cancel_edit: (event) ->
@trigger "thread:cancel_edit", event
if Backbone?
class @DiscussionThreadProfileView extends DiscussionContentView
expanded = false
events:
"click .discussion-vote": "toggleVote"
"click .action-follow": "toggleFollowing"
"click .expand-post": "expandPost"
"click .collapse-post": "collapsePost"
initLocal: ->
@$local = @$el.children(".discussion-article").children(".local")
@$delegateElement = @$local
initialize: ->
super()
@model.on "change", @updateModelDetails
render: ->
@template = DiscussionUtil.getTemplate("_profile_thread")
if not @model.has('abbreviatedBody')
@abbreviateBody()
params = $.extend(@model.toJSON(),{expanded: @expanded, permalink: @model.urlFor('retrieve')})
if not @model.get('anonymous')
params = $.extend(params, user:{username: @model.username, user_url: @model.user_url})
@$el.html(Mustache.render(@template, params))
@initLocal()
@delegateEvents()
@renderDogear()
@renderVoted()
@renderAttrs()
@$("span.timeago").timeago()
@convertMath()
if @expanded
@renderResponses()
@
renderDogear: ->
if window.user.following(@model)
@$(".dogear").addClass("is-followed")
renderVoted: =>
if window.user.voted(@model)
@$("[data-role=discussion-vote]").addClass("is-cast")
else
@$("[data-role=discussion-vote]").removeClass("is-cast")
updateModelDetails: =>
@renderVoted()
@$("[data-role=discussion-vote] .votes-count-number").html(@model.get("votes")["up_count"])
convertMath: ->
element = @$(".post-body")
element.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight element.html()
MathJax.Hub.Queue ["Typeset", MathJax.Hub, element[0]]
renderResponses: ->
DiscussionUtil.safeAjax
url: "/courses/#{$$course_id}/discussion/forum/#{@model.get('commentable_id')}/threads/#{@model.id}"
$loading: @$el
success: (data, textStatus, xhr) =>
@$el.find(".loading").remove()
Content.loadContentInfos(data['annotated_content_info'])
comments = new Comments(data['content']['children'])
comments.each @renderResponse
@trigger "thread:responses:rendered"
renderResponse: (response) =>
response.set('thread', @model)
view = new ThreadResponseView(model: response)
view.on "comment:add", @addComment
view.render()
@$el.find(".responses").append(view.el)
addComment: =>
@model.comment()
toggleVote: (event) ->
event.preventDefault()
if window.user.voted(@model)
@unvote()
else
@vote()
toggleFollowing: (event) ->
$elem = $(event.target)
url = null
console.log "follow"
if not @model.get('subscribed')
@model.follow()
url = @model.urlFor("follow")
else
@model.unfollow()
url = @model.urlFor("unfollow")
DiscussionUtil.safeAjax
$elem: $elem
url: url
type: "POST"
vote: ->
window.user.vote(@model)
url = @model.urlFor("upvote")
DiscussionUtil.safeAjax
$elem: @$(".discussion-vote")
url: url
type: "POST"
success: (response, textStatus) =>
if textStatus == 'success'
@model.set(response)
unvote: ->
window.user.unvote(@model)
url = @model.urlFor("unvote")
DiscussionUtil.safeAjax
$elem: @$(".discussion-vote")
url: url
type: "POST"
success: (response, textStatus) =>
if textStatus == 'success'
@model.set(response)
edit: ->
abbreviateBody: ->
abbreviated = DiscussionUtil.abbreviateString @model.get('body'), 140
@model.set('abbreviatedBody', abbreviated)
expandPost: (event) ->
@expanded = true
@$el.addClass('expanded')
@$el.find('.post-body').html(@model.get('body'))
@convertMath()
@$el.find('.expand-post').css('display', 'none')
@$el.find('.collapse-post').css('display', 'block')
@$el.find('.post-extended-content').show()
if @$el.find('.loading').length
@renderResponses()
collapsePost: (event) ->
@expanded = false
@$el.removeClass('expanded')
@$el.find('.post-body').html(@model.get('abbreviatedBody'))
@convertMath()
@$el.find('.collapse-post').css('display', 'none')
@$el.find('.post-extended-content').hide()
@$el.find('.expand-post').css('display', 'block')
if Backbone?
class @DiscussionThreadShowView extends DiscussionContentView
events:
"click .discussion-vote": "toggleVote"
"click .action-follow": "toggleFollowing"
"click .action-edit": "edit"
"click .action-delete": "delete"
"click .action-openclose": "toggleClosed"
$: (selector) ->
@$el.find(selector)
initialize: ->
super()
@model.on "change", @updateModelDetails
renderTemplate: ->
@template = _.template($("#thread-show-template").html())
@template(@model.toJSON())
render: ->
@$el.html(@renderTemplate())
@delegateEvents()
@renderDogear()
@renderVoted()
@renderAttrs()
@$("span.timeago").timeago()
@convertMath()
@highlight @$(".post-body")
@highlight @$("h1,h3")
@
renderDogear: ->
if window.user.following(@model)
@$(".dogear").addClass("is-followed")
renderVoted: =>
if window.user.voted(@model)
@$("[data-role=discussion-vote]").addClass("is-cast")
else
@$("[data-role=discussion-vote]").removeClass("is-cast")
updateModelDetails: =>
@renderVoted()
@$("[data-role=discussion-vote] .votes-count-number").html(@model.get("votes")["up_count"])
convertMath: ->
element = @$(".post-body")
element.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight element.html()
MathJax.Hub.Queue ["Typeset", MathJax.Hub, element[0]]
toggleVote: (event) ->
event.preventDefault()
if window.user.voted(@model)
@unvote()
else
@vote()
toggleFollowing: (event) ->
$elem = $(event.target)
url = null
if not @model.get('subscribed')
@model.follow()
url = @model.urlFor("follow")
else
@model.unfollow()
url = @model.urlFor("unfollow")
DiscussionUtil.safeAjax
$elem: $elem
url: url
type: "POST"
vote: ->
window.user.vote(@model)
url = @model.urlFor("upvote")
DiscussionUtil.safeAjax
$elem: @$(".discussion-vote")
url: url
type: "POST"
success: (response, textStatus) =>
if textStatus == 'success'
@model.set(response, {silent: true})
unvote: ->
window.user.unvote(@model)
url = @model.urlFor("unvote")
DiscussionUtil.safeAjax
$elem: @$(".discussion-vote")
url: url
type: "POST"
success: (response, textStatus) =>
if textStatus == 'success'
@model.set(response, {silent: true})
edit: (event) ->
@trigger "thread:edit", event
delete: (event) ->
@trigger "thread:delete", event
toggleClosed: (event) ->
$elem = $(event.target)
url = @model.urlFor('close')
closed = @model.get('closed')
data = { closed: not closed }
DiscussionUtil.safeAjax
$elem: $elem
url: url
data: data
type: "POST"
success: (response, textStatus) =>
@model.set('closed', not closed)
@model.set('ability', response.ability)
toggleEndorse: (event) ->
$elem = $(event.target)
url = @model.urlFor('endorse')
endorsed = @model.get('endorsed')
data = { endorsed: not endorsed }
DiscussionUtil.safeAjax
$elem: $elem
url: url
data: data
type: "POST"
success: (response, textStatus) =>
@model.set('endorsed', not endorsed)
highlight: (el) ->
if el.html()
el.html(el.html().replace(/&lt;mark&gt;/g, "<mark>").replace(/&lt;\/mark&gt;/g, "</mark>"))
class @DiscussionThreadInlineShowView extends DiscussionThreadShowView
renderTemplate: ->
@template = DiscussionUtil.getTemplate('_inline_thread_show')
params = @model.toJSON()
if @model.get('username')?
params = $.extend(params, user:{username: @model.username, user_url: @model.user_url})
Mustache.render(@template, params)
if Backbone?
class @DiscussionThreadView extends DiscussionContentView
events:
"click .discussion-submit-post": "submitComment"
# TODO tags
# Until we decide what to do w/ tags, removing them.
#"click .thread-tag": "tagSelected"
$: (selector) ->
@$el.find(selector)
initialize: ->
super()
@createShowView()
renderTemplate: ->
@template = _.template($("#thread-template").html())
@template(@model.toJSON())
render: ->
@$el.html(@renderTemplate())
@$el.find(".loading").hide()
@delegateEvents()
@renderShowView()
@renderAttrs()
# TODO tags
# Until we decide what to do w/ tags, removing them.
#@renderTags()
@$("span.timeago").timeago()
@makeWmdEditor "reply-body"
@renderResponses()
@
cleanup: ->
if @responsesRequest?
@responsesRequest.abort()
# TODO tags
# Until we decide what to do w/ tags, removing them.
#renderTags: ->
# # tags
# for tag in @model.get("tags")
# if !tags
# tags = $('<div class="thread-tags">')
# tags.append("<a href='#' class='thread-tag'>#{tag}</a>")
# @$(".post-body").after(tags)
# TODO tags
# Until we decide what to do w tags, removing them.
#tagSelected: (e) ->
# @trigger "tag:selected", $(e.target).html()
renderResponses: ->
setTimeout(=>
@$el.find(".loading").show()
, 200)
@responsesRequest = DiscussionUtil.safeAjax
url: DiscussionUtil.urlFor('retrieve_single_thread', @model.get('commentable_id'), @model.id)
success: (data, textStatus, xhr) =>
@responsesRequest = null
@$el.find(".loading").remove()
Content.loadContentInfos(data['annotated_content_info'])
comments = new Comments(data['content']['children'])
comments.each @renderResponse
@trigger "thread:responses:rendered"
renderResponse: (response) =>
response.set('thread', @model)
view = new ThreadResponseView(model: response)
view.on "comment:add", @addComment
view.on "comment:endorse", @endorseThread
view.render()
@$el.find(".responses").append(view.el)
view.afterInsert()
addComment: =>
@model.comment()
endorseThread: (endorsed) =>
is_endorsed = @$el.find(".is-endorsed").length
@model.set 'endorsed', is_endorsed
submitComment: (event) ->
event.preventDefault()
url = @model.urlFor('reply')
body = @getWmdContent("reply-body")
return if not body.trim().length
@setWmdContent("reply-body", "")
comment = new Comment(body: body, created_at: (new Date()).toISOString(), username: window.user.get("username"), votes: { up_count: 0 }, endorsed: false, user_id: window.user.get("id"))
comment.set('thread', @model.get('thread'))
@renderResponse(comment)
@model.addComment()
DiscussionUtil.safeAjax
$elem: $(event.target)
url: url
type: "POST"
dataType: 'json'
data:
body: body
success: (data, textStatus) =>
comment.updateInfo(data.annotated_content_info)
comment.set(data.content)
edit: (event) =>
@createEditView()
@renderEditView()
update: (event) =>
newTitle = @editView.$(".edit-post-title").val()
newBody = @editView.$(".edit-post-body textarea").val()
# TODO tags
# Until we decide what to do w/ tags, removing them.
#newTags = @editView.$(".edit-post-tags").val()
url = DiscussionUtil.urlFor('update_thread', @model.id)
DiscussionUtil.safeAjax
$elem: $(event.target)
$loading: $(event.target) if event
url: url
type: "POST"
dataType: 'json'
async: false # TODO when the rest of the stuff below is made to work properly..
data:
title: newTitle
body: newBody
# TODO tags
# Until we decide what to do w/ tags, removing them.
#tags: newTags
error: DiscussionUtil.formErrorHandler(@$(".edit-post-form-errors"))
success: (response, textStatus) =>
# TODO: Move this out of the callback, this makes it feel sluggish
@editView.$(".edit-post-title").val("").attr("prev-text", "")
@editView.$(".edit-post-body textarea").val("").attr("prev-text", "")
@editView.$(".edit-post-tags").val("")
@editView.$(".edit-post-tags").importTags("")
@editView.$(".wmd-preview p").html("")
@model.set
title: newTitle
body: newBody
tags: response.content.tags
@createShowView()
@renderShowView()
# TODO tags
# Until we decide what to do w/ tags, removing them.
#@renderTags()
createEditView: () ->
if @showView?
@showView.undelegateEvents()
@showView.$el.empty()
@showView = null
@editView = new DiscussionThreadEditView(model: @model)
@editView.bind "thread:update", @update
@editView.bind "thread:cancel_edit", @cancelEdit
renderSubView: (view) ->
view.setElement(@$('.thread-content-wrapper'))
view.render()
view.delegateEvents()
renderEditView: () ->
@renderSubView(@editView)
createShowView: () ->
if @editView?
@editView.undelegateEvents()
@editView.$el.empty()
@editView = null
@showView = new DiscussionThreadShowView(model: @model)
@showView.bind "thread:delete", @delete
@showView.bind "thread:edit", @edit
renderShowView: () ->
@renderSubView(@showView)
cancelEdit: (event) =>
event.preventDefault()
@createShowView()
@renderShowView()
delete: (event) =>
url = @model.urlFor('delete')
if not @model.can('can_delete')
return
if not confirm "Are you sure to delete thread \"#{@model.get('title')}\"?"
return
@model.remove()
@showView.undelegateEvents()
@undelegateEvents()
@$el.empty()
$elem = $(event.target)
DiscussionUtil.safeAjax
$elem: $elem
url: url
type: "POST"
success: (response, textStatus) =>
if Backbone?
class @DiscussionThreadInlineView extends DiscussionThreadView
expanded = false
events:
"click .discussion-submit-post": "submitComment"
"click .expand-post": "expandPost"
"click .collapse-post": "collapsePost"
initialize: ->
super()
initLocal: ->
@$local = @$el.children(".discussion-article").children(".local")
if not @$local.length
@$local = @$el
@$delegateElement = @$local
render: ->
@template = DiscussionUtil.getTemplate("_inline_thread")
if not @model.has('abbreviatedBody')
@abbreviateBody()
params = @model.toJSON()
@$el.html(Mustache.render(@template, params))
#@createShowView()
@initLocal()
@delegateEvents()
@renderShowView()
@renderAttrs()
# TODO tags commenting out til we decide what to do with tags
#@renderTags()
@$("span.timeago").timeago()
@$el.find('.post-extended-content').hide()
if @expanded
@makeWmdEditor "reply-body"
@renderResponses()
@
createShowView: () ->
if @editView?
@editView.undelegateEvents()
@editView.$el.empty()
@editView = null
@showView = new DiscussionThreadInlineShowView(model: @model)
@showView.bind "thread:delete", @delete
@showView.bind "thread:edit", @edit
renderResponses: ->
#TODO: threadview
DiscussionUtil.safeAjax
url: "/courses/#{$$course_id}/discussion/forum/#{@model.get('commentable_id')}/threads/#{@model.id}"
$loading: @$el
success: (data, textStatus, xhr) =>
# @$el.find(".loading").remove()
Content.loadContentInfos(data['annotated_content_info'])
comments = new Comments(data['content']['children'])
comments.each @renderResponse
@trigger "thread:responses:rendered"
@$('.loading').remove()
toggleClosed: (event) ->
#TODO: showview
$elem = $(event.target)
url = @model.urlFor('close')
closed = @model.get('closed')
data = { closed: not closed }
DiscussionUtil.safeAjax
$elem: $elem
url: url
data: data
type: "POST"
success: (response, textStatus) =>
@model.set('closed', not closed)
@model.set('ability', response.ability)
toggleEndorse: (event) ->
#TODO: showview
$elem = $(event.target)
url = @model.urlFor('endorse')
endorsed = @model.get('endorsed')
data = { endorsed: not endorsed }
DiscussionUtil.safeAjax
$elem: $elem
url: url
data: data
type: "POST"
success: (response, textStatus) =>
@model.set('endorsed', not endorsed)
abbreviateBody: ->
abbreviated = DiscussionUtil.abbreviateString @model.get('body'), 140
@model.set('abbreviatedBody', abbreviated)
expandPost: (event) =>
@expanded = true
@$el.addClass('expanded')
@$el.find('.post-body').html(@model.get('body'))
@showView.convertMath()
@$el.find('.expand-post').css('display', 'none')
@$el.find('.collapse-post').css('display', 'block')
@$el.find('.post-extended-content').show()
@makeWmdEditor "reply-body"
@renderAttrs()
if @$el.find('.loading').length
@renderResponses()
collapsePost: (event) ->
@expanded = false
@$el.removeClass('expanded')
@$el.find('.post-body').html(@model.get('abbreviatedBody'))
@showView.convertMath()
@$el.find('.collapse-post').css('display', 'none')
@$el.find('.post-extended-content').hide()
@$el.find('.expand-post').css('display', 'block')
createEditView: () ->
super()
@editView.bind "thread:update", @expandPost
@editView.bind "thread:update", @abbreviateBody
@editView.bind "thread:cancel_edit", @expandPost
if Backbone?
class @DiscussionUserProfileView extends Backbone.View
# events:
# "":""
initialize: (options) ->
@renderThreads @$el, @collection
renderThreads: ($elem, threads) =>
#Content.loadContentInfos(response.annotated_content_info)
console.log threads
@discussion = new Discussion()
@discussion.reset(threads, {silent: false})
$discussion = $(Mustache.render $("script#_user_profile").html(), {'threads':threads})
console.log $discussion
$elem.append($discussion)
@threadviews = @discussion.map (thread) ->
new DiscussionThreadProfileView el: @$("article#thread_#{thread.id}"), model: thread
console.log @threadviews
_.each @threadviews, (dtv) -> dtv.render()
addThread: (thread, collection, options) =>
# TODO: When doing pagination, this will need to repaginate. Perhaps just reload page 1?
article = $("<article class='discussion-thread' id='thread_#{thread.id}'></article>")
@$('section.discussion > .threads').prepend(article)
threadView = new DiscussionThreadInlineView el: article, model: thread
threadView.render()
@threadviews.unshift threadView
if Backbone?
class @NewPostInlineView extends Backbone.View
initialize: () ->
@topicId = @$(".topic").first().data("discussion-id")
@maxNameWidth = 100
DiscussionUtil.makeWmdEditor @$el, $.proxy(@$, @), "new-post-body"
# TODO tags: commenting out til we know what to do with them
#@$(".new-post-tags").tagsInput DiscussionUtil.tagsInputOptions()
events:
"submit .new-post-form": "createPost"
# Because we want the behavior that when the body is clicked the menu is
# closed, we need to ignore clicks in the search field and stop propagation.
# Without this, clicking the search field would also close the menu.
ignoreClick: (event) ->
event.stopPropagation()
createPost: (event) ->
event.preventDefault()
title = @$(".new-post-title").val()
body = @$(".new-post-body").find(".wmd-input").val()
# TODO tags: commenting out til we know what to do with them
#tags = @$(".new-post-tags").val()
anonymous = false || @$("input.discussion-anonymous").is(":checked")
anonymous_to_peers = false || @$("input.discussion-anonymous-to-peers").is(":checked")
follow = false || @$("input.discussion-follow").is(":checked")
url = DiscussionUtil.urlFor('create_thread', @topicId)
DiscussionUtil.safeAjax
$elem: $(event.target)
$loading: $(event.target) if event
url: url
type: "POST"
dataType: 'json'
async: false # TODO when the rest of the stuff below is made to work properly..
data:
title: title
body: body
# TODO tags: commenting out til we know what to do with them
#tags: tags
anonymous: anonymous
anonymous_to_peers: anonymous_to_peers
auto_subscribe: follow
error: DiscussionUtil.formErrorHandler(@$(".new-post-form-errors"))
success: (response, textStatus) =>
# TODO: Move this out of the callback, this makes it feel sluggish
thread = new Thread response['content']
DiscussionUtil.clearFormErrors(@$(".new-post-form-errors"))
@$el.hide()
@$(".new-post-title").val("").attr("prev-text", "")
@$(".new-post-body textarea").val("").attr("prev-text", "")
# TODO tags, commenting out til we know what to do with them
#@$(".new-post-tags").val("")
#@$(".new-post-tags").importTags("")
@collection.add thread
if Backbone?
class @NewPostView extends Backbone.View
initialize: () ->
@dropdownButton = @$(".topic_dropdown_button")
@topicMenu = @$(".topic_menu_wrapper")
@menuOpen = @dropdownButton.hasClass('dropped')
@topicId = @$(".topic").first().data("discussion_id")
@topicText = @getFullTopicName(@$(".topic").first())
@maxNameWidth = 100
@setSelectedTopic()
DiscussionUtil.makeWmdEditor @$el, $.proxy(@$, @), "new-post-body"
@$(".new-post-tags").tagsInput DiscussionUtil.tagsInputOptions()
events:
"submit .new-post-form": "createPost"
"click .topic_dropdown_button": "toggleTopicDropdown"
"click .topic_menu_wrapper": "setTopic"
"click .topic_menu_search": "ignoreClick"
# Because we want the behavior that when the body is clicked the menu is
# closed, we need to ignore clicks in the search field and stop propagation.
# Without this, clicking the search field would also close the menu.
ignoreClick: (event) ->
event.stopPropagation()
toggleTopicDropdown: (event) ->
event.stopPropagation()
if @menuOpen
@hideTopicDropdown()
else
@showTopicDropdown()
showTopicDropdown: () ->
@menuOpen = true
@dropdownButton.addClass('dropped')
@topicMenu.show()
$(".form-topic-drop-search-input").focus()
$("body").bind "keydown", @setActiveItem
$("body").bind "click", @hideTopicDropdown
# Set here because 1) the window might get resized and things could
# change and 2) can't set in initialize because the button is hidden
@maxNameWidth = @dropdownButton.width() * 0.9
# Need a fat arrow because hideTopicDropdown is passed as a callback to bind
hideTopicDropdown: () =>
@menuOpen = false
@dropdownButton.removeClass('dropped')
@topicMenu.hide()
$("body").unbind "keydown", @setActiveItem
$("body").unbind "click", @hideTopicDropdown
setTopic: (event) ->
$target = $(event.target)
if $target.data('discussion_id')
@topicText = $target.html()
@topicText = @getFullTopicName($target)
@topicId = $target.data('discussion_id')
@setSelectedTopic()
setSelectedTopic: ->
@dropdownButton.html(@fitName(@topicText) + ' <span class="drop-arrow">▾</span>')
getFullTopicName: (topicElement) ->
name = topicElement.html()
topicElement.parents('ul').not('.topic_menu').each ->
name = $(this).siblings('a').html() + ' / ' + name
return name
getNameWidth: (name) ->
test = $("<div>")
test.css
"font-size": @dropdownButton.css('font-size')
opacity: 0
position: 'absolute'
left: -1000
top: -1000
$("body").append(test)
test.html(name)
width = test.width()
test.remove()
return width
fitName: (name) ->
width = @getNameWidth(name)
if width < @maxNameWidth
return name
path = (x.replace /^\s+|\s+$/g, "" for x in name.split("/"))
while path.length > 1
path.shift()
partialName = "... / " + path.join(" / ")
if @getNameWidth(partialName) < @maxNameWidth
return partialName
rawName = path[0]
name = "... / " + rawName
while @getNameWidth(name) > @maxNameWidth
rawName = rawName[0...rawName.length-1]
name = "... / " + rawName + " ..."
return name
createPost: (event) ->
event.preventDefault()
title = @$(".new-post-title").val()
body = @$(".new-post-body").find(".wmd-input").val()
tags = @$(".new-post-tags").val()
anonymous = false || @$("input.discussion-anonymous").is(":checked")
anonymous_to_peers = false || @$("input.discussion-anonymous-to-peers").is(":checked")
follow = false || @$("input.discussion-follow").is(":checked")
$formTopicDropBtn.bind('click', showFormTopicDrop)
$formTopicDropMenu.bind('click', setFormTopic)
url = DiscussionUtil.urlFor('create_thread', @topicId)
DiscussionUtil.safeAjax
$elem: $(event.target)
$loading: $(event.target) if event
url: url
type: "POST"
dataType: 'json'
async: false # TODO when the rest of the stuff below is made to work properly..
data:
title: title
body: body
tags: tags
anonymous: anonymous
anonymous_to_peers: anonymous_to_peers
auto_subscribe: follow
error: DiscussionUtil.formErrorHandler(@$(".new-post-form-errors"))
success: (response, textStatus) =>
# TODO: Move this out of the callback, this makes it feel sluggish
thread = new Thread response['content']
DiscussionUtil.clearFormErrors(@$(".new-post-form-errors"))
@$el.hide()
@$(".new-post-title").val("").attr("prev-text", "")
@$(".new-post-body textarea").val("").attr("prev-text", "")
@$(".new-post-tags").val("")
@$(".new-post-tags").importTags("")
@$(".wmd-preview p").html("")
@collection.add thread
setActiveItem: (event) ->
if event.which == 13
$(".topic_menu_wrapper .focused").click()
return
if event.which != 40 && event.which != 38
return
event.preventDefault()
items = $.makeArray($(".topic_menu_wrapper a").not(".hidden"))
index = items.indexOf($('.topic_menu_wrapper .focused')[0])
if event.which == 40
index = Math.min(index + 1, items.length - 1)
if event.which == 38
index = Math.max(index - 1, 0)
$(".topic_menu_wrapper .focused").removeClass("focused")
$(items[index]).addClass("focused")
itemTop = $(items[index]).parent().offset().top
scrollTop = $(".topic_menu").scrollTop()
itemFromTop = $(".topic_menu").offset().top - itemTop
scrollTarget = Math.min(scrollTop - itemFromTop, scrollTop)
scrollTarget = Math.max(scrollTop - itemFromTop - $(".topic_menu").height() + $(items[index]).height() + 20, scrollTarget)
$(".topic_menu").scrollTop(scrollTarget)
if Backbone?
class @ResponseCommentShowView extends DiscussionContentView
tagName: "li"
render: ->
@template = _.template($("#response-comment-show-template").html())
params = @model.toJSON()
@$el.html(@template(params))
@initLocal()
@delegateEvents()
@renderAttrs()
@markAsStaff()
@$el.find(".timeago").timeago()
@convertMath()
@addReplyLink()
@
addReplyLink: () ->
if @model.hasOwnProperty('parent')
name = @model.parent.get('username') ? "anonymous"
html = "<a href='#comment_#{@model.parent.id}'>@#{name}</a>: "
p = @$('.response-body p:first')
p.prepend(html)
convertMath: ->
body = @$el.find(".response-body")
body.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight body.html()
MathJax.Hub.Queue ["Typeset", MathJax.Hub, body[0]]
markAsStaff: ->
if DiscussionUtil.isStaff(@model.get("user_id"))
@$el.find("a.profile-link").after('<span class="staff-label">staff</span>')
if Backbone?
class @ResponseCommentView extends DiscussionContentView
tagName: "li"
$: (selector) ->
@$el.find(selector)
initialize: ->
super()
@createShowView()
render: ->
@renderShowView()
@
createShowView: () ->
if @editView?
@editView.undelegateEvents()
@editView.$el.empty()
@editView = null
@showView = new ResponseCommentShowView(model: @model)
renderSubView: (view) ->
view.setElement(@$el)
view.render()
view.delegateEvents()
renderShowView: () ->
@renderSubView(@showView)
if Backbone?
class @ThreadResponseEditView extends Backbone.View
events:
"click .post-update": "update"
"click .post-cancel": "cancel_edit"
$: (selector) ->
@$el.find(selector)
initialize: ->
super()
render: ->
@template = _.template($("#thread-response-edit-template").html())
@$el.html(@template(@model.toJSON()))
@delegateEvents()
DiscussionUtil.makeWmdEditor @$el, $.proxy(@$, @), "edit-post-body"
@
update: (event) ->
@trigger "response:update", event
cancel_edit: (event) ->
@trigger "response:cancel_edit", event
if Backbone?
class @ThreadResponseShowView extends DiscussionContentView
events:
"click .vote-btn": "toggleVote"
"click .action-endorse": "toggleEndorse"
"click .action-delete": "delete"
"click .action-edit": "edit"
$: (selector) ->
@$el.find(selector)
initialize: ->
super()
@model.on "change", @updateModelDetails
renderTemplate: ->
@template = _.template($("#thread-response-show-template").html())
@template(@model.toJSON())
render: ->
@$el.html(@renderTemplate())
@delegateEvents()
if window.user.voted(@model)
@$(".vote-btn").addClass("is-cast")
@renderAttrs()
@$el.find(".posted-details").timeago()
@convertMath()
@markAsStaff()
@
convertMath: ->
element = @$(".response-body")
element.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight element.html()
MathJax.Hub.Queue ["Typeset", MathJax.Hub, element[0]]
markAsStaff: ->
if DiscussionUtil.isStaff(@model.get("user_id"))
@$el.addClass("staff")
@$el.prepend('<div class="staff-banner">staff</div>')
toggleVote: (event) ->
event.preventDefault()
@$(".vote-btn").toggleClass("is-cast")
if @$(".vote-btn").hasClass("is-cast")
@vote()
else
@unvote()
vote: ->
url = @model.urlFor("upvote")
@$(".votes-count-number").html(parseInt(@$(".votes-count-number").html()) + 1)
DiscussionUtil.safeAjax
$elem: @$(".discussion-vote")
url: url
type: "POST"
success: (response, textStatus) =>
if textStatus == 'success'
@model.set(response)
unvote: ->
url = @model.urlFor("unvote")
@$(".votes-count-number").html(parseInt(@$(".votes-count-number").html()) - 1)
DiscussionUtil.safeAjax
$elem: @$(".discussion-vote")
url: url
type: "POST"
success: (response, textStatus) =>
if textStatus == 'success'
@model.set(response)
edit: (event) ->
@trigger "response:edit", event
delete: (event) ->
@trigger "response:delete", event
toggleEndorse: (event) ->
event.preventDefault()
if not @model.can('can_endorse')
return
$elem = $(event.target)
url = @model.urlFor('endorse')
endorsed = @model.get('endorsed')
data = { endorsed: not endorsed }
@model.set('endorsed', not endorsed)
@trigger "comment:endorse", not endorsed
DiscussionUtil.safeAjax
$elem: $elem
url: url
data: data
type: "POST"
if Backbone?
class @ThreadResponseView extends DiscussionContentView
tagName: "li"
events:
"click .discussion-submit-comment": "submitComment"
"focus .wmd-input": "showEditorChrome"
$: (selector) ->
@$el.find(selector)
initialize: ->
@createShowView()
renderTemplate: ->
@template = _.template($("#thread-response-template").html())
templateData = @model.toJSON()
templateData.wmdId = @model.id ? (new Date()).getTime()
@template(templateData)
render: ->
@$el.html(@renderTemplate())
@delegateEvents()
@renderShowView()
@renderAttrs()
@renderComments()
@
afterInsert: ->
@makeWmdEditor "comment-body"
@hideEditorChrome()
hideEditorChrome: ->
@$('.wmd-button-row').hide()
@$('.wmd-preview').hide()
@$('.wmd-input').css({
height: '35px',
padding: '5px'
})
@$('.comment-post-control').hide()
showEditorChrome: ->
@$('.wmd-button-row').show()
@$('.wmd-preview').show()
@$('.comment-post-control').show()
@$('.wmd-input').css({
height: '125px',
padding: '10px'
})
renderComments: ->
comments = new Comments()
comments.comparator = (comment) ->
comment.get('created_at')
collectComments = (comment) ->
comments.add(comment)
children = new Comments(comment.get('children'))
children.each (child) ->
child.parent = comment
collectComments(child)
@model.get('comments').each collectComments
comments.each (comment) => @renderComment(comment, false, null)
renderComment: (comment) =>
comment.set('thread', @model.get('thread'))
view = new ResponseCommentView(model: comment)
view.render()
@$el.find(".comments .new-comment").before(view.el)
view
submitComment: (event) ->
event.preventDefault()
url = @model.urlFor('reply')
body = @getWmdContent("comment-body")
return if not body.trim().length
@setWmdContent("comment-body", "")
comment = new Comment(body: body, created_at: (new Date()).toISOString(), username: window.user.get("username"), user_id: window.user.get("id"), id:"unsaved")
view = @renderComment(comment)
@hideEditorChrome()
@trigger "comment:add", comment
DiscussionUtil.safeAjax
$elem: $(event.target)
url: url
type: "POST"
dataType: 'json'
data:
body: body
success: (response, textStatus) ->
comment.set(response.content)
view.render() # This is just to update the id for the most part, but might be useful in general
delete: (event) =>
event.preventDefault()
if not @model.can('can_delete')
return
if not confirm "Are you sure to delete this response? "
return
url = @model.urlFor('delete')
@model.remove()
@$el.remove()
$elem = $(event.target)
DiscussionUtil.safeAjax
$elem: $elem
url: url
type: "POST"
success: (response, textStatus) =>
createEditView: () ->
if @showView?
@showView.undelegateEvents()
@showView.$el.empty()
@showView = null
@editView = new ThreadResponseEditView(model: @model)
@editView.bind "response:update", @update
@editView.bind "response:cancel_edit", @cancelEdit
renderSubView: (view) ->
view.setElement(@$('.discussion-response'))
view.render()
view.delegateEvents()
renderEditView: () ->
@renderSubView(@editView)
hideCommentForm: () ->
@$('.comment-form').closest('li').hide()
showCommentForm: () ->
@$('.comment-form').closest('li').show()
createShowView: () ->
if @editView?
@editView.undelegateEvents()
@editView.$el.empty()
@editView = null
@showView = new ThreadResponseShowView(model: @model)
@showView.bind "response:delete", @delete
@showView.bind "response:edit", @edit
renderShowView: () ->
@renderSubView(@showView)
cancelEdit: (event) =>
event.preventDefault()
@createShowView()
@renderShowView()
@showCommentForm()
edit: (event) =>
@createEditView()
@renderEditView()
@hideCommentForm()
update: (event) =>
newBody = @editView.$(".edit-post-body textarea").val()
url = DiscussionUtil.urlFor('update_comment', @model.id)
DiscussionUtil.safeAjax
$elem: $(event.target)
$loading: $(event.target) if event
url: url
type: "POST"
dataType: 'json'
async: false # TODO when the rest of the stuff below is made to work properly..
data:
body: newBody
error: DiscussionUtil.formErrorHandler(@$(".edit-post-form-errors"))
success: (response, textStatus) =>
# TODO: Move this out of the callback, this makes it feel sluggish
@editView.$(".edit-post-body textarea").val("").attr("prev-text", "")
@editView.$(".wmd-preview p").html("")
@model.set
body: newBody
@createShowView()
@renderShowView()
@showCommentForm()
var $body;
var $browse;
var $search;
var $searchField;
var $topicDrop;
var $currentBoard;
var $tooltip;
var $newPost;
var $thread;
var $sidebar;
var $sidebarWidthStyles;
var $formTopicDropBtn;
var $formTopicDropMenu;
var $postListWrapper;
var $dropFilter;
var $topicFilter;
var $discussionBody;
var sidebarWidth;
var sidebarHeight;
var sidebarHeaderHeight;
var sidebarXOffset;
var scrollTop;
var discussionsBodyTop;
var discussionsBodyBottom;
var tooltipTimer;
var tooltipCoords;
var SIDEBAR_PADDING = 10;
var SIDEBAR_HEADER_HEIGHT = 87;
$(document).ready(function() {
$body = $('body');
//$browse = $('.browse-search .browse');
//$search = $('.browse-search .search');
$searchField = $('.post-search-field');
//$topicDrop = $('.browse-topic-drop-menu-wrapper');
$currentBoard = $('.current-board');
$tooltip = $('<div class="tooltip"></div>');
$newPost = $('.new-post-article');
$sidebar = $('.sidebar');
$discussionBody = $('.discussion-body');
$postListWrapper = $('.post-list-wrapper');
$formTopicDropBtn = $('.new-post-article .form-topic-drop-btn');
$formTopicDropMenu = $('.new-post-article .form-topic-drop-menu-wrapper');
// $dropFilter = $('.browse-topic-drop-search-input');
// $topicFilter = $('.topic-drop-search-input');
$sidebarWidthStyles = $('<style></style>');
$body.append($sidebarWidthStyles);
sidebarWidth = $('.sidebar').width();
sidebarXOffset = $sidebar.offset().top;
//$browse.bind('click', showTopicDrop);
//$search.bind('click', showSearch);
// $topicDrop.bind('click', setTopic);
$formTopicDropBtn.bind('click', showFormTopicDrop);
$formTopicDropMenu.bind('click', setFormTopic);
$('.new-post-btn').bind('click', newPost);
$('.new-post-cancel').bind('click', closeNewPost);
$body.delegate('[data-tooltip]', {
'mouseover': showTooltip,
'mousemove': moveTooltip,
'mouseout': hideTooltip,
'click': hideTooltip
});
$body.delegate('.browse-topic-drop-search-input, .form-topic-drop-search-input', 'keyup', filterDrop);
});
function filterDrop(e) {
/*
* multiple queries
*/
// var $drop = $(e.target).parents('.form-topic-drop-menu-wrapper, .browse-topic-drop-menu-wrapper');
// var queries = $(this).val().split(' ');
// var $items = $drop.find('a');
// if(queries.length == 0) {
// $items.show();
// return;
// }
// $items.hide();
// $items.each(function(i) {
// var thisText = $(this).children().not('.unread').text();
// $(this).parents('ul').siblings('a').not('.unread').each(function(i) {
// thisText = thisText + ' ' + $(this).text();
// });
// var test = true;
// var terms = thisText.split(' ');
// for(var i = 0; i < queries.length; i++) {
// if(thisText.toLowerCase().search(queries[i].toLowerCase()) == -1) {
// test = false;
// }
// }
// if(test) {
// $(this).show();
// // show children
// $(this).parent().find('a').show();
// // show parents
// $(this).parents('ul').siblings('a').show();
// }
// });
/*
* single query
*/
var $drop = $(e.target).parents('.topic_menu_wrapper, .browse-topic-drop-menu-wrapper');
var query = $(this).val();
var $items = $drop.find('a');
if(query.length == 0) {
$items.removeClass('hidden');
return;
}
$items.addClass('hidden');
$items.each(function(i) {
var thisText = $(this).not('.unread').text();
$(this).parents('ul').siblings('a').not('.unread').each(function(i) {
thisText = thisText + ' ' + $(this).text();
});
var test = true;
var terms = thisText.split(' ');
if(thisText.toLowerCase().search(query.toLowerCase()) == -1) {
test = false;
}
if(test) {
$(this).removeClass('hidden');
// show children
$(this).parent().find('a').removeClass('hidden');
// show parents
$(this).parents('ul').siblings('a').removeClass('hidden');
}
});
}
function showTooltip(e) {
var tooltipText = $(this).attr('data-tooltip');
$tooltip.html(tooltipText);
$body.append($tooltip);
$(this).children().css('pointer-events', 'none');
tooltipCoords = {
x: e.pageX - ($tooltip.outerWidth() / 2),
y: e.pageY - ($tooltip.outerHeight() + 15)
};
$tooltip.css({
'left': tooltipCoords.x,
'top': tooltipCoords.y
});
tooltipTimer = setTimeout(function() {
$tooltip.show().css('opacity', 1);
tooltipTimer = setTimeout(function() {
hideTooltip();
}, 3000);
}, 500);
}
function moveTooltip(e) {
tooltipCoords = {
x: e.pageX - ($tooltip.outerWidth() / 2),
y: e.pageY - ($tooltip.outerHeight() + 15)
};
$tooltip.css({
'left': tooltipCoords.x,
'top': tooltipCoords.y
});
}
function hideTooltip(e) {
$tooltip.hide().css('opacity', 0);
clearTimeout(tooltipTimer);
}
function showBrowse(e) {
$browse.addClass('is-open');
$search.removeClass('is-open');
$searchField.val('');
}
function showSearch(e) {
$search.addClass('is-open');
$browse.removeClass('is-open');
setTimeout(function() {
$searchField.focus();
}, 200);
}
function showTopicDrop(e) {
e.preventDefault();
$browse.addClass('is-dropped');
if(!$topicDrop[0]) {
$topicDrop = $('.browse-topic-drop-menu-wrapper');
}
$topicDrop.show();
$browse.unbind('click', showTopicDrop);
$body.bind('keyup', setActiveDropItem);
$browse.bind('click', hideTopicDrop);
setTimeout(function() {
$body.bind('click', hideTopicDrop);
}, 0);
}
function hideTopicDrop(e) {
if(e.target == $('.browse-topic-drop-search-input')[0]) {
return;
}
$browse.removeClass('is-dropped');
$topicDrop.hide();
$body.unbind('click', hideTopicDrop);
$browse.bind('click', showTopicDrop);
}
function setTopic(e) {
if(e.target == $('.browse-topic-drop-search-input')[0]) {
return;
}
var $item = $(e.target).closest('a');
var boardName = $item.find('.board-name').html();
$item.parents('ul').not('.browse-topic-drop-menu').each(function(i) {
boardName = $(this).siblings('a').find('.board-name').html() + ' / ' + boardName;
});
if(!$currentBoard[0]) {
$currentBoard = $('.current-board');
}
$currentBoard.html(boardName);
var fontSize = 16;
$currentBoard.css('font-size', '16px');
while($currentBoard.width() > (sidebarWidth * .8) - 40) {
fontSize--;
if(fontSize < 11) {
break;
}
$currentBoard.css('font-size', fontSize + 'px');
}
showBrowse();
}
function newPost(e) {
$newPost.slideDown(300);
$('.new-post-title').focus();
}
function closeNewPost(e) {
$newPost.slideUp(300);
}
function showFormTopicDrop(e) {
$formTopicDropBtn.addClass('is-dropped');
$formTopicDropMenu.show();
$formTopicDropBtn.unbind('click', showFormTopicDrop);
$formTopicDropBtn.bind('click', hideFormTopicDrop);
setTimeout(function() {
$body.bind('click', hideFormTopicDrop);
}, 0);
}
function hideFormTopicDrop(e) {
if(e.target == $('.topic-drop-search-input')[0]) {
return;
}
$formTopicDropBtn.removeClass('is-dropped');
$formTopicDropMenu.hide();
$body.unbind('click', hideFormTopicDrop);
$formTopicDropBtn.unbind('click', hideFormTopicDrop);
$formTopicDropBtn.bind('click', showFormTopicDrop);
}
function setFormTopic(e) {
if(e.target == $('.topic-drop-search-input')[0]) {
return;
}
$formTopicDropBtn.removeClass('is-dropped');
hideFormTopicDrop(e);
var $item = $(e.target);
var boardName = $item.html();
$item.parents('ul').not('.form-topic-drop-menu').each(function(i) {
boardName = $(this).siblings('a').html() + ' / ' + boardName;
});
$formTopicDropBtn.html(boardName + ' <span class="drop-arrow">▾</span>');
}
function updateSidebar(e) {
// determine page scroll attributes
scrollTop = $(window).scrollTop();
discussionsBodyTop = $discussionBody.offset().top;
discussionsBodyBottom = discussionsBodyTop + $discussionBody.height();
var windowHeight = $(window).height();
// toggle fixed positioning
if(scrollTop > discussionsBodyTop - SIDEBAR_PADDING) {
$sidebar.addClass('fixed');
$sidebar.css('top', SIDEBAR_PADDING + 'px');
} else {
$sidebar.removeClass('fixed');
$sidebar.css('top', '0');
}
// set sidebar width
var sidebarWidth = .32 * $discussionBody.width() - 10;
$sidebar.css('width', sidebarWidth + 'px');
// show the entire sidebar at all times
var sidebarHeight = windowHeight - (scrollTop < discussionsBodyTop - SIDEBAR_PADDING ? discussionsBodyTop - scrollTop : SIDEBAR_PADDING) - SIDEBAR_PADDING - (scrollTop + windowHeight > discussionsBodyBottom + SIDEBAR_PADDING ? scrollTop + windowHeight - discussionsBodyBottom - SIDEBAR_PADDING : 0);
$sidebar.css('height', sidebarHeight > 400 ? sidebarHeight : 400 + 'px');
// update the list height
if(!$postListWrapper[0]) {
$postListWrapper = $('.post-list-wrapper');
}
$postListWrapper.css('height', (sidebarHeight - SIDEBAR_HEADER_HEIGHT - 4) + 'px');
// update title wrappers
var titleWidth = sidebarWidth - 115;
$sidebarWidthStyles.html('.discussion-body .post-list a .title { width: ' + titleWidth + 'px !important; }');
}
/* /*
jQuery Tags Input Plugin 1.3.3 jQuery Tags Input Plugin 1.3.3
Copyright (c) 2011 XOXCO, Inc Copyright (c) 2011 XOXCO, Inc
Documentation for this plugin lives here: Documentation for this plugin lives here:
http://xoxco.com/clickable/jquery-tags-input http://xoxco.com/clickable/jquery-tags-input
Licensed under the MIT license: Licensed under the MIT license:
http://www.opensource.org/licenses/mit-license.php http://www.opensource.org/licenses/mit-license.php
...@@ -24,9 +24,9 @@ ...@@ -24,9 +24,9 @@
val = '', val = '',
input = $(this), input = $(this),
testSubject = $('#'+$(this).data('tester_id')); testSubject = $('#'+$(this).data('tester_id'));
if (val === (val = input.val())) {return;} if (val === (val = input.val())) {return;}
// Enter new content into testSubject // Enter new content into testSubject
var escaped = val.replace(/&/g, '&amp;').replace(/\s/g,' ').replace(/</g, '&lt;').replace(/>/g, '&gt;'); var escaped = val.replace(/&/g, '&amp;').replace(/\s/g,' ').replace(/</g, '&lt;').replace(/>/g, '&gt;');
testSubject.html(escaped); testSubject.html(escaped);
...@@ -36,7 +36,7 @@ ...@@ -36,7 +36,7 @@
currentWidth = input.width(), currentWidth = input.width(),
isValidWidthChange = (newWidth < currentWidth && newWidth >= minWidth) isValidWidthChange = (newWidth < currentWidth && newWidth >= minWidth)
|| (newWidth > minWidth && newWidth < maxWidth); || (newWidth > minWidth && newWidth < maxWidth);
// Animate width // Animate width
if (isValidWidthChange) { if (isValidWidthChange) {
input.width(newWidth); input.width(newWidth);
...@@ -72,19 +72,24 @@ ...@@ -72,19 +72,24 @@
input.data('tester_id', testerId); input.data('tester_id', testerId);
input.css('width', minWidth); input.css('width', minWidth);
}; };
$.fn.addTag = function(value,options) { $.fn.addTag = function(value,options) {
options = jQuery.extend({focus:false,callback:true},options); options = jQuery.extend({focus:false,callback:true},options);
this.each(function() { this.each(function() {
var id = $(this).attr('id'); var id = $(this).attr('id');
var tagslist = $(this).val().split(delimiter[id]); var tagslist = $(this).val().split(delimiter[id]);
if (tagslist[0] == '') { if (tagslist[0] == '') {
tagslist = new Array(); tagslist = new Array();
} }
value = jQuery.trim(value); value = jQuery.trim(value);
if (options.callback && tags_callbacks[id] && tags_callbacks[id]['preprocessTag']) {
var f = tags_callbacks[id]['preprocessTag'];
value = f.call(this, value);
}
if (options.unique) { if (options.unique) {
var skipTag = $(this).tagExist(value); var skipTag = $(this).tagExist(value);
if(skipTag == true) { if(skipTag == true) {
...@@ -92,15 +97,15 @@ ...@@ -92,15 +97,15 @@
$('#'+id+'_tag').addClass('not_valid'); $('#'+id+'_tag').addClass('not_valid');
} }
} else { } else {
var skipTag = false; var skipTag = false;
} }
if (value !='' && skipTag != true) { if (value !='' && skipTag != true) {
$('<span>').addClass('tag').append( $('<span>').addClass('tag').append(
$('<span>').text(value).append('&nbsp;&nbsp;'), $('<span>').text(value).append('&nbsp;&nbsp;'),
$('<a>', { $('<a>', {
href : '#', href : '#',
title : 'Removing tag', title : 'Remove tag',
text : 'x' text : 'x'
}).click(function () { }).click(function () {
return $('#' + id).removeTag(escape(value)); return $('#' + id).removeTag(escape(value));
...@@ -108,16 +113,16 @@ ...@@ -108,16 +113,16 @@
).insertBefore('#' + id + '_addTag'); ).insertBefore('#' + id + '_addTag');
tagslist.push(value); tagslist.push(value);
$('#'+id+'_tag').val(''); $('#'+id+'_tag').val('');
if (options.focus) { if (options.focus) {
$('#'+id+'_tag').focus(); $('#'+id+'_tag').focus();
} else { } else {
$('#'+id+'_tag').blur(); $('#'+id+'_tag').blur();
} }
$.fn.tagsInput.updateTagsField(this,tagslist); $.fn.tagsInput.updateTagsField(this,tagslist);
if (options.callback && tags_callbacks[id] && tags_callbacks[id]['onAddTag']) { if (options.callback && tags_callbacks[id] && tags_callbacks[id]['onAddTag']) {
var f = tags_callbacks[id]['onAddTag']; var f = tags_callbacks[id]['onAddTag'];
f.call(this, value); f.call(this, value);
...@@ -127,29 +132,29 @@ ...@@ -127,29 +132,29 @@
var i = tagslist.length; var i = tagslist.length;
var f = tags_callbacks[id]['onChange']; var f = tags_callbacks[id]['onChange'];
f.call(this, $(this), tagslist[i-1]); f.call(this, $(this), tagslist[i-1]);
} }
} }
}); });
return false; return false;
}; };
$.fn.removeTag = function(value) { $.fn.removeTag = function(value) {
value = unescape(value); value = unescape(value);
this.each(function() { this.each(function() {
var id = $(this).attr('id'); var id = $(this).attr('id');
var old = $(this).val().split(delimiter[id]); var old = $(this).val().split(delimiter[id]);
$('#'+id+'_tagsinput .tag').remove(); $('#'+id+'_tagsinput .tag').remove();
str = ''; str = '';
for (i=0; i< old.length; i++) { for (i=0; i< old.length; i++) {
if (old[i]!=value) { if (old[i]!=value) {
str = str + delimiter[id] +old[i]; str = str + delimiter[id] +old[i];
} }
} }
$.fn.tagsInput.importTags(this,str); $.fn.tagsInput.importTags(this,str);
if (tags_callbacks[id] && tags_callbacks[id]['onRemoveTag']) { if (tags_callbacks[id] && tags_callbacks[id]['onRemoveTag']) {
...@@ -157,24 +162,24 @@ ...@@ -157,24 +162,24 @@
f.call(this, value); f.call(this, value);
} }
}); });
return false; return false;
}; };
$.fn.tagExist = function(val) { $.fn.tagExist = function(val) {
var id = $(this).attr('id'); var id = $(this).attr('id');
var tagslist = $(this).val().split(delimiter[id]); var tagslist = $(this).val().split(delimiter[id]);
return (jQuery.inArray(val, tagslist) >= 0); //true when tag exists, false when not return (jQuery.inArray(val, tagslist) >= 0); //true when tag exists, false when not
}; };
// clear all existing tags and import new ones from a string // clear all existing tags and import new ones from a string
$.fn.importTags = function(str) { $.fn.importTags = function(str) {
id = $(this).attr('id'); id = $(this).attr('id');
$('#'+id+'_tagsinput .tag').remove(); $('#'+id+'_tagsinput .tag').remove();
$.fn.tagsInput.importTags(this,str); $.fn.tagsInput.importTags(this,str);
} }
$.fn.tagsInput = function(options) { $.fn.tagsInput = function(options) {
var settings = jQuery.extend({ var settings = jQuery.extend({
interactive:true, interactive:true,
defaultText:'add a tag', defaultText:'add a tag',
...@@ -192,15 +197,15 @@ ...@@ -192,15 +197,15 @@
inputPadding: 6*2 inputPadding: 6*2
},options); },options);
this.each(function() { this.each(function() {
if (settings.hide) { if (settings.hide) {
$(this).hide(); $(this).hide();
} }
var id = $(this).attr('id'); var id = $(this).attr('id');
if (!id || delimiter[$(this).attr('id')]) { if (!id || delimiter[$(this).attr('id')]) {
id = $(this).attr('id', 'tags' + new Date().getTime()).attr('id'); id = $(this).attr('id', 'tags' + new Date().getTime()).attr('id');
} }
var data = jQuery.extend({ var data = jQuery.extend({
pid:id, pid:id,
real_input: '#'+id, real_input: '#'+id,
...@@ -208,57 +213,58 @@ ...@@ -208,57 +213,58 @@
input_wrapper: '#'+id+'_addTag', input_wrapper: '#'+id+'_addTag',
fake_input: '#'+id+'_tag' fake_input: '#'+id+'_tag'
},settings); },settings);
delimiter[id] = data.delimiter; delimiter[id] = data.delimiter;
if (settings.onAddTag || settings.onRemoveTag || settings.onChange) { if (settings.onAddTag || settings.onRemoveTag || settings.onChange || settings.preprocessTag) {
tags_callbacks[id] = new Array(); tags_callbacks[id] = new Array();
tags_callbacks[id]['onAddTag'] = settings.onAddTag; tags_callbacks[id]['onAddTag'] = settings.onAddTag;
tags_callbacks[id]['onRemoveTag'] = settings.onRemoveTag; tags_callbacks[id]['onRemoveTag'] = settings.onRemoveTag;
tags_callbacks[id]['onChange'] = settings.onChange; tags_callbacks[id]['onChange'] = settings.onChange;
tags_callbacks[id]['preprocessTag'] = settings.preprocessTag;
} }
var markup = '<div id="'+id+'_tagsinput" class="tagsinput"><div id="'+id+'_addTag">'; var markup = '<div id="'+id+'_tagsinput" class="tagsinput"><div id="'+id+'_addTag">';
if (settings.interactive) { if (settings.interactive) {
markup = markup + '<input id="'+id+'_tag" value="" data-default="'+settings.defaultText+'" />'; markup = markup + '<input id="'+id+'_tag" value="" data-default="'+settings.defaultText+'" />';
} }
markup = markup + '</div><div class="tags_clear"></div></div>'; markup = markup + '</div><div class="tags_clear"></div></div>';
$(markup).insertAfter(this); $(markup).insertAfter(this);
$(data.holder).css('width',settings.width); $(data.holder).css('width',settings.width);
$(data.holder).css('min-height',settings.height); $(data.holder).css('min-height',settings.height);
$(data.holder).css('height','100%'); $(data.holder).css('height','100%');
if ($(data.real_input).val()!='') { if ($(data.real_input).val()!='') {
$.fn.tagsInput.importTags($(data.real_input),$(data.real_input).val()); $.fn.tagsInput.importTags($(data.real_input),$(data.real_input).val());
} }
if (settings.interactive) { if (settings.interactive) {
$(data.fake_input).val($(data.fake_input).attr('data-default')); $(data.fake_input).val($(data.fake_input).attr('data-default'));
$(data.fake_input).css('color',settings.placeholderColor); $(data.fake_input).css('color',settings.placeholderColor);
$(data.fake_input).resetAutosize(settings); $(data.fake_input).resetAutosize(settings);
$(data.fake_input).doAutosize(settings); $(data.fake_input).doAutosize(settings);
$(data.holder).bind('click',data,function(event) { $(data.holder).bind('click',data,function(event) {
$(event.data.fake_input).focus(); $(event.data.fake_input).focus();
}); });
$(data.fake_input).bind('focus',data,function(event) { $(data.fake_input).bind('focus',data,function(event) {
if ($(event.data.fake_input).val()==$(event.data.fake_input).attr('data-default')) { if ($(event.data.fake_input).val()==$(event.data.fake_input).attr('data-default')) {
$(event.data.fake_input).val(''); $(event.data.fake_input).val('');
} }
$(event.data.fake_input).css('color','#000000'); $(event.data.fake_input).css('color','#000000');
}); });
if (settings.autocomplete_url != undefined) { if (settings.autocomplete_url != undefined) {
autocomplete_options = {source: settings.autocomplete_url}; autocomplete_options = {source: settings.autocomplete_url};
for (attrname in settings.autocomplete) { for (attrname in settings.autocomplete) {
autocomplete_options[attrname] = settings.autocomplete[attrname]; autocomplete_options[attrname] = settings.autocomplete[attrname];
} }
if (jQuery.Autocompleter !== undefined) { if (jQuery.Autocompleter !== undefined) {
onSelectCallback = settings.autocomplete.onItemSelect; onSelectCallback = settings.autocomplete.onItemSelect;
settings.autocomplete.onItemSelect = function() { settings.autocomplete.onItemSelect = function() {
...@@ -278,18 +284,18 @@ ...@@ -278,18 +284,18 @@
$(data.fake_input).autocomplete(autocomplete_options); $(data.fake_input).autocomplete(autocomplete_options);
$(data.fake_input).bind('autocompleteselect',data,function(event,ui) { $(data.fake_input).bind('autocompleteselect',data,function(event,ui) {
$(event.data.real_input).addTag(ui.item.value,{focus:true,unique:(settings.unique)}); $(event.data.real_input).addTag(ui.item.value,{focus:true,unique:(settings.unique)});
return false; return false;
}); });
} }
} else { } else {
// if a user tabs out of the field, create a new tag // if a user tabs out of the field, create a new tag
// this is only available if autocomplete is not used. // this is only available if autocomplete is not used.
$(data.fake_input).bind('blur',data,function(event) { $(data.fake_input).bind('blur',data,function(event) {
var d = $(this).attr('data-default'); var d = $(this).attr('data-default');
if ($(event.data.fake_input).val()!='' && $(event.data.fake_input).val()!=d) { if ($(event.data.fake_input).val()!='' && $(event.data.fake_input).val()!=d) {
if( (event.data.minChars <= $(event.data.fake_input).val().length) && (!event.data.maxChars || (event.data.maxChars >= $(event.data.fake_input).val().length)) ) if( (event.data.minChars <= $(event.data.fake_input).val().length) && (!event.data.maxChars || (event.data.maxChars >= $(event.data.fake_input).val().length)) )
$(event.data.real_input).addTag($(event.data.fake_input).val(),{focus:true,unique:(settings.unique)}); $(event.data.real_input).addTag($(event.data.fake_input).val(),{focus:true,unique:(settings.unique)});
} else { } else {
...@@ -298,7 +304,7 @@ ...@@ -298,7 +304,7 @@
} }
return false; return false;
}); });
} }
// if user types a comma, create a new tag // if user types a comma, create a new tag
$(data.fake_input).bind('keypress',data,function(event) { $(data.fake_input).bind('keypress',data,function(event) {
...@@ -326,7 +332,7 @@ ...@@ -326,7 +332,7 @@
} }
}); });
$(data.fake_input).blur(); $(data.fake_input).blur();
//Removes the not_valid class when user changes the value of the fake input //Removes the not_valid class when user changes the value of the fake input
if(data.unique) { if(data.unique) {
$(data.fake_input).keydown(function(event){ $(data.fake_input).keydown(function(event){
...@@ -337,21 +343,21 @@ ...@@ -337,21 +343,21 @@
} }
} // if settings.interactive } // if settings.interactive
}); });
return this; return this;
}; };
$.fn.tagsInput.updateTagsField = function(obj,tagslist) { $.fn.tagsInput.updateTagsField = function(obj,tagslist) {
var id = $(obj).attr('id'); var id = $(obj).attr('id');
$(obj).val(tagslist.join(delimiter[id])); $(obj).val(tagslist.join(delimiter[id]));
}; };
$.fn.tagsInput.importTags = function(obj,val) { $.fn.tagsInput.importTags = function(obj,val) {
$(obj).val(''); $(obj).val('');
var id = $(obj).attr('id'); var id = $(obj).attr('id');
var tags = val.split(delimiter[id]); var tags = val.split(delimiter[id]);
for (i=0; i<tags.length; i++) { for (i=0; i<tags.length; i++) {
$(obj).addTag(tags[i],{focus:false,callback:false}); $(obj).addTag(tags[i],{focus:false,callback:false});
} }
if(tags_callbacks[id] && tags_callbacks[id]['onChange']) if(tags_callbacks[id] && tags_callbacks[id]['onChange'])
......
@mixin blue-button {
display: block;
height: 33px;
margin: 12px;
padding: 0 15px;
border-radius: 3px;
border: 1px solid #4697c1;
background: -webkit-linear-gradient(top, #6dccf1, #38a8e5);
font-size: 13px;
font-weight: 700;
line-height: 30px;
color: #fff;
text-shadow: 0 1px 0 rgba(0, 0, 0, .4);
box-shadow: 0 1px 0 rgba(255, 255, 255, 0.4) inset, 0 1px 1px rgba(0, 0, 0, .15);
&:hover {
border-color: #297095;
background: -webkit-linear-gradient(top, #4fbbe4, #2090d0);
}
}
.discussion-body {
.vote-btn {
float: right;
display: block;
height: 27px;
padding: 0 8px;
border-radius: 5px;
border: 1px solid #b2b2b2;
background: -webkit-linear-gradient(top, #fff 35%, #ebebeb);
box-shadow: 0 1px 1px rgba(0, 0, 0, .15);
font-size: 12px;
font-weight: 700;
line-height: 25px;
color: #333;
.plus-icon {
float: left;
margin-right: 6px;
font-size: 18px;
color: #17b429;
}
&.is-cast {
border-color: #379a42;
background: -webkit-linear-gradient(top, #50cc5e, #3db84b);
color: #fff;
text-shadow: 0 1px 0 rgba(0, 0, 0, .3);
box-shadow: 0 1px 0 rgba(255, 255, 255, .4) inset, 0 1px 2px rgba(0, 0, 0, .2);
.plus-icon {
color: #336a39;
text-shadow: 0 1px 0 rgba(255, 255, 255, .4);
}
}
}
.new-post-btn {
@include blue-button;
float: right;
}
.new-post-icon {
display: block;
float: left;
width: 16px;
height: 17px;
margin: 7px 7px 0 0;
background: url(../images/new-post-icon.png) no-repeat;
}
.post-search {
float: right;
}
.post-search-field {
width: 280px;
height: 30px;
padding: 0 15px 0 30px;
margin-top: 14px;
border: 1px solid #acacac;
border-radius: 30px;
box-shadow: 0 1px 3px rgba(0, 0, 0, .1) inset, 0 1px 0 rgba(255, 255, 255, .5);
background: url(../images/search-icon.png) no-repeat 8px center #fff;
font-family: 'Open Sans', sans-serif;
font-weight: 400;
font-size: 13px;
line-height: 30px;
color: #333;
outline: 0;
-webkit-transition: border-color .1s;
&:focus {
border-color: #4697c1;
}
}
h1, ul, li, a, ol {
margin: 0;
padding: 0;
border: 0;
outline: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
ul, li {
list-style-type: none;
}
a {
text-decoration: none;
color: #009fe2;
}
display: table;
table-layout: fixed;
width: 100%;
height: 500px;
background: #fff;
border-radius: 3px;
border: 1px solid #aaa;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
.sidebar {
display: table-cell;
vertical-align: top;
width: 27.7%;
background: #f6f6f6;
border-radius: 3px 0 0 3px;
border-right: 1px solid #bcbcbc;
.post-list {
background-color: #ddd;
li:last-child a {
border-bottom: 1px solid #ddd;
}
a {
position: relative;
display: block;
height: 36px;
padding: 0 10px;
margin-bottom: 1px;
background: #fff;
font-size: 13px;
font-weight: 700;
line-height: 34px;
color: #333;
&.read .title {
font-weight: 400;
color: #737373;
}
&.followed:after {
content: '';
position: absolute;
top: 0;
right: 0;
width: 12px;
height: 12px;
background: url(../images/following-flag.png) no-repeat;
}
&.active {
background: -webkit-linear-gradient(top, #96e0fd, #61c7fc);
border-color: #4697c1;
box-shadow: 0 1px 0 #4697c1, 0 -1px 0 #4697c1;
.title {
color: #333;
}
.votes-count,
.comments-count {
background: -webkit-linear-gradient(top, #3994c7, #4da7d3);
color: #fff;
&:after {
color: #4da7d3;
}
}
&.followed:after {
background-position: 0 -12px;
}
}
}
.title {
display: block;
float: left;
width: 70%;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.votes-count,
.comments-count {
display: block;
float: right;
width: 32px;
height: 16px;
margin-top: 9px;
border-radius: 2px;
background: -webkit-linear-gradient(top, #d4d4d4, #dfdfdf);
font-size: 9px;
font-weight: 700;
line-height: 16px;
text-align: center;
color: #767676;
}
.comments-count {
position: relative;
margin-left: 4px;
&:after {
content: '◥';
display: block;
position: absolute;
top: 11px;
right: 3px;
font-size: 6px;
color: #dfdfdf;
}
&.new {
background: -webkit-linear-gradient(top, #84d7fe, #99e0fe);
color: #333;
&:after {
color: #99e0fe;
}
}
}
}
}
.board-drop-btn {
display: block;
height: 60px;
border-bottom: 1px solid #a3a3a3;
border-radius: 3px 0 0 0;
background: -webkit-linear-gradient(top, #ebebeb, #d9d9d9);
font-size: 16px;
font-weight: 700;
line-height: 58px;
text-align: center;
color: #333;
text-shadow: 0 1px 0 rgba(255, 255, 255, .8);
}
.sort-bar {
height: 27px;
border-bottom: 1px solid #a3a3a3;
background: -webkit-linear-gradient(top, #cdcdcd, #b6b6b6);
box-shadow: 0 1px 0 rgba(255, 255, 255, .4) inset;
a {
display: block;
float: right;
height: 27px;
margin-right: 10px;
font-size: 11px;
font-weight: bold;
line-height: 23px;
color: #333;
text-shadow: 0 1px 0 rgba(255, 255, 255, .5);
.sort-label {
font-size: 9px;
text-transform: uppercase;
}
}
}
}
.global-discussion-actions {
height: 60px;
background: -webkit-linear-gradient(top, #ebebeb, #d9d9d9);
border-radius: 0 3px 0 0;
border-bottom: 1px solid #bcbcbc;
}
.discussion-article {
position: relative;
display: table-cell;
vertical-align: top;
width: 72.3%;
padding: 40px;
h1 {
font-size: 28px;
font-weight: 700;
}
.posted-details {
font-size: 12px;
font-style: italic;
color: #888;
}
p + p {
margin-top: 20px;
}
.dogear {
display: block;
position: absolute;
top: 0;
right: -1px;
width: 52px;
height: 51px;
background: url(../images/follow-dog-ear.png) 0 -51px no-repeat;
&.is-followed {
background-position: 0 0;
}
}
}
.discussion-post header,
.responses li header {
margin-bottom: 20px;
}
.responses {
margin-top: 40px;
> li {
margin: 0 -10px;
padding: 30px;
border-radius: 3px;
border: 1px solid #b2b2b2;
box-shadow: 0 1px 3px rgba(0, 0, 0, .15);
}
.posted-by {
font-weight: 700;
}
}
.endorse-btn {
display: block;
float: right;
width: 27px;
height: 27px;
margin-right: 10px;
border-radius: 27px;
border: 1px solid #a0a0a0;
background: -webkit-linear-gradient(top, #fff 35%, #ebebeb);
box-shadow: 0 1px 1px rgba(0, 0, 0, .1);
.check-icon {
display: block;
width: 13px;
height: 12px;
margin: 8px auto;
background: url(../images/endorse-icon.png) no-repeat;
}
&.is-endorsed {
border: 1px solid #4697c1;
background: -webkit-linear-gradient(top, #6dccf1, #38a8e5);
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, .4) inset;
.check-icon {
background-position: 0 -12px;
}
}
}
.comments {
margin-top: 20px;
border-top: 1px solid #ddd;
li {
background: #f6f6f6;
border-bottom: 1px solid #ddd;
}
p {
font-size: 13px;
padding: 10px 20px;
.posted-details {
font-size: 11px;
white-space: nowrap;
}
}
}
.comment-form {
padding: 8px 20px;
}
.comment-form-input {
width: 100%;
height: 31px;
padding: 0 10px;
box-sizing: border-box;
border: 1px solid #b2b2b2;
border-radius: 3px;
box-shadow: 0 1px 3px rgba(0, 0, 0, .1) inset;
-webkit-transition: border-color .1s;
outline: 0;
&:focus {
border-color: #4697c1;
}
}
.moderator-actions {
margin-top: 20px;
@include clearfix;
li {
float: left;
margin-right: 8px;
}
a {
display: block;
height: 26px;
padding: 0 12px;
border-radius: 3px;
border: 1px solid #b2b2b2;
background: -webkit-linear-gradient(top, #fff 35%, #ebebeb);
font-size: 13px;
line-height: 24px;
color: #737373;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
.delete-icon {
display: block;
float: left;
width: 10px;
height: 10px;
margin: 8px 4px 0 0;
background: url(../images/moderator-delete-icon.png) no-repeat;
}
.edit-icon {
display: block;
float: left;
width: 10px;
height: 10px;
margin: 7px 4px 0 0;
background: url(../images/moderator-edit-icon.png) no-repeat;
}
}
}
...@@ -45,6 +45,7 @@ span { ...@@ -45,6 +45,7 @@ span {
font: normal 1em/1.6em $sans-serif; font: normal 1em/1.6em $sans-serif;
color: $base-font-color; color: $base-font-color;
} }
/* Fix for CodeMirror: prevent top-level span from affecting deeply-embedded span in CodeMirror */ /* Fix for CodeMirror: prevent top-level span from affecting deeply-embedded span in CodeMirror */
.CodeMirror span { .CodeMirror span {
font: inherit; font: inherit;
...@@ -136,3 +137,23 @@ span.edx { ...@@ -136,3 +137,23 @@ span.edx {
margin-top: 20px; margin-top: 20px;
} }
} }
.loading-animation {
position: absolute;
left: 50%;
width: 20px;
height: 20px;
margin-left: -10px;
background: url(../images/spinner.gif) no-repeat;
}
mark {
padding: 0 3px;
border-radius: 2px;
background-color: #f7e9a8;
color: #333;
}
...@@ -12,6 +12,7 @@ ...@@ -12,6 +12,7 @@
// Course base / layout styles // Course base / layout styles
@import 'course/layout/courseware_header'; @import 'course/layout/courseware_header';
@import 'course/layout/footer'; @import 'course/layout/footer';
@import 'course/base/mixins';
@import 'course/base/base'; @import 'course/base/base';
@import 'course/base/extends'; @import 'course/base/extends';
@import 'module/module-styles.scss'; @import 'module/module-styles.scss';
......
...@@ -39,11 +39,6 @@ a { ...@@ -39,11 +39,6 @@ a {
} }
} }
form { form {
label { label {
display: block; display: block;
...@@ -102,3 +97,90 @@ img { ...@@ -102,3 +97,90 @@ img {
background: #444; background: #444;
color: #fff; color: #fff;
} }
.tooltip {
position: absolute;
top: 0;
left: 0;
z-index: 99999;
padding: 0 10px;
border-radius: 3px;
background: rgba(0, 0, 0, .85);
font-size: 11px;
font-weight: 400;
line-height: 26px;
color: #fff;
pointer-events: none;
opacity: 0;
@include transition(opacity .1s);
&:after {
content: '▾';
display: block;
position: absolute;
bottom: -14px;
left: 50%;
margin-left: -7px;
font-size: 20px;
color: rgba(0, 0, 0, .85);
}
}
.test-class {
border: 1px solid #f00;
}
.toast-notification {
position: fixed;
top: 20px;
right: 20px;
z-index: 99999;
max-width: 350px;
padding: 15px 20px 17px;
border-radius: 3px;
border: 1px solid #333;
background: -webkit-linear-gradient(top, rgba(255, 255, 255, .1), rgba(255, 255, 255, 0)) rgba(30, 30, 30, .92);
box-shadow: 0 1px 3px rgba(0, 0, 0, .3), 0 1px 0 rgba(255, 255, 255, .1) inset;
font-size: 13px;
color: #fff;
opacity: 0;
-webkit-transition: all .2s;
p, span {
color: #fff;
}
strong {
display: block;
margin-bottom: 10px;
font-size: 16px;
font-weight: 700;
text-align: center;
}
.close-btn {
position: absolute;
top: 0;
right: 0;
width: 27px;
height: 27px;
font-size: 22px;
font-weight: 700;
line-height: 25px;
color: #aaa;
text-align: center;
.close-icon {
font-size: 16px;
font-weight: 700;
}
}
.action-btn {
@include dark-grey-button;
margin-top: 10px;
text-align: center;
}
}
@mixin blue-button {
display: block;
height: 35px;
padding: 0 15px;
border-radius: 3px;
border: 1px solid #2d81ad;
@include linear-gradient(top, #6dccf1, #38a8e5);
font-size: 13px;
font-weight: 700;
line-height: 32px;
color: #fff;
text-shadow: 0 1px 0 rgba(0, 0, 0, .3);
box-shadow: 0 1px 0 rgba(255, 255, 255, 0.4) inset, 0 1px 1px rgba(0, 0, 0, .15);
&:hover {
border-color: #297095;
@include linear-gradient(top, #4fbbe4, #2090d0);
}
}
@mixin white-button {
display: block;
height: 35px;
padding: 0 15px;
border-radius: 3px;
border: 1px solid #444;
@include linear-gradient(top, #eee, #ccc);
font-size: 13px;
font-weight: 700;
line-height: 32px;
color: #333;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6);
box-shadow: 0 1px 0 rgba(255, 255, 255, 0.4) inset, 0 1px 1px rgba(0, 0, 0, .15);
&:hover {
@include linear-gradient(top, #fff, #ddd);
}
}
@mixin dark-grey-button {
display: block;
height: 35px;
padding: 0 15px;
border-radius: 3px;
border: 1px solid #222;
background: -webkit-linear-gradient(top, #777, #555);
font-size: 13px;
font-weight: 700;
line-height: 32px;
color: #fff;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.6);
box-shadow: 0 1px 0 rgba(255, 255, 255, 0.4) inset, 0 1px 1px rgba(0, 0, 0, .15);
&:hover {
background: -webkit-linear-gradient(top, #888, #666);
}
}
\ No newline at end of file
...@@ -54,6 +54,7 @@ def url_class(url): ...@@ -54,6 +54,7 @@ def url_class(url):
% if staff_access: % if staff_access:
<li class="instructor"><a href="${reverse('instructor_dashboard', args=[course.id])}" class="${url_class('instructor')}">Instructor</a></li> <li class="instructor"><a href="${reverse('instructor_dashboard', args=[course.id])}" class="${url_class('instructor')}">Instructor</a></li>
% endif % endif
<%block name="extratabs" />
</ol> </ol>
</div> </div>
</nav> </nav>
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
</%def> </%def>
<%def name="render_content_with_comments(content, *args, **kwargs)"> <%def name="render_content_with_comments(content, *args, **kwargs)">
<div class="${content['type'] | h}${helpers.show_if(' endorsed', content.get('endorsed')) | h}" _id="${content['id'] | h}" _discussion_id="${content.get('commentable_id', '') | h}" _author_id="${helpers.show_if(content['user_id'], not content.get('anonymous')) | h}"> <div class="${content['type'] | h}${' endorsed' if content.get('endorsed') else ''| h}" _id="${content['id'] | h}" _discussion_id="${content.get('commentable_id', '') | h}" _author_id="${content['user_id'] if (not content.get('anonymous')) else '' | h}">
${render_content(content, *args, **kwargs)} ${render_content(content, *args, **kwargs)}
${render_comments(content.get('children', []), *args, **kwargs)} ${render_comments(content.get('children', []), *args, **kwargs)}
</div> </div>
......
<%inherit file="../courseware/course_navigation.html" />
<%block name="extratabs">
<li class="right"><a href="#" class="new-post-btn"><span class="new-post-icon"></span>New Post</a></li>
</%block>
\ No newline at end of file
<div class="discussion-module"> <%include file="_underscore_templates.html" />
<a class="discussion-show control-button" href="javascript:void(0)" discussion_id="${discussion_id | h}">Show Discussion</a>
<div class="discussion-module" data-discussion-id="${discussion_id | h}">
<a class="discussion-show control-button" href="javascript:void(0)" data-discussion-id="${discussion_id | h}"><span class="show-hide-discussion-icon"></span><span class="button-text">Show Discussion</span></a>
<a href="#" class="new-post-btn"><span class="new-post-icon"></span>New Post</a>
</div> </div>
<%! import json %>
<%def name="render_dropdown(map)">
% for child in map["children"]:
% if child in map["entries"]:
${render_entry(map["entries"], child)}
%else:
${render_category(map["subcategories"], child)}
%endif
%endfor
</%def>
<%def name="render_entry(entries, entry)">
<li><a href="#"><span class="board-name" data-discussion_id='${json.dumps(entries[entry])}'>${entry}</span></a></li>
</%def>
<%def name="render_category(categories, category)">
<li>
<a href="#"><span class="board-name">${category}</span></a>
<ul>
${render_dropdown(categories[category])}
</ul>
</li>
</%def>
<div class="browse-topic-drop-menu-wrapper">
<div class="browse-topic-drop-search">
<input type="text" class="browse-topic-drop-search-input" placeholder="filter topics">
</div>
<ul class="browse-topic-drop-menu">
<li>
<a href="#">
<span class="board-name" data-discussion_id='#all'>All</span>
</a>
</li>
${render_dropdown(category_map)}
</ul>
</div>
<article class="new-post-article">
<div class="inner-wrapper">
<div class="new-post-form-errors">
</div>
<form class="new-post-form">
<div class="left-column">
<div class="options">
<input type="checkbox" name="follow" class="discussion-follow" class="discussion-follow" id="new-post-follow" checked><label for="new-post-follow">follow this post</label>
<br>
% if course.metadata.get("allow_anonymous", True):
<input type="checkbox" name="anonymous" class="discussion-anonymous" id="new-post-anonymous"><label for="new-post-anonymous">post anonymously</label>
%elif course.metadata.get("allow_anonymous_to_peers", False):
<input type="checkbox" name="anonymous_to_peers" class="discussion-anonymous-to-peers" id="new-post-anonymous-to-peers"><label for="new-post-anonymous-to-peers">post anonymously to classmates</label>
%endif
</div>
</div>
<div class="right-column">
<div class="form-row">
<input type="text" class="new-post-title" name="title" placeholder="Title">
</div>
<div class="form-row">
<div class="new-post-body" name="body" placeholder="Enter your question or comment&hellip;"></div>
<!---<div class="new-post-preview"><span class="new-post-preview-label">Preview</span></div>-->
</div>
## TODO commenting out tags til we figure out what to do with them
##<div class="form-row">
## <input type="text" class="new-post-tags" name="tags" placeholder="Tags">
##</div>
<input type="submit" class="submit" value="Add post">
<a href="#" class="new-post-cancel">Cancel</a>
</div>
</form>
</div>
</article>
<%def name="render_form_filter_dropdown(map)">
% for child in map["children"]:
% if child in map["entries"]:
${render_entry(map["entries"], child)}
%else:
${render_category(map["subcategories"], child)}
%endif
%endfor
</%def>
<%def name="render_entry(entries, entry)">
<li><a href="#" class="topic" data-discussion_id="${entries[entry]['id']}">${entry}</a></li>
</%def>
<%def name="render_category(categories, category)">
<li>
<a href="#">${category}</a>
<ul>
${render_form_filter_dropdown(categories[category])}
</ul>
</li>
</%def>
<article class="new-post-article">
<div class="inner-wrapper">
<form class="new-post-form">
<div class="left-column">
<label>Create new post about:</label>
<div class="form-topic-drop">
<a href="#" class="topic_dropdown_button">All<span class="drop-arrow"></span></a>
<div class="topic_menu_wrapper">
<div class="topic_menu_search">
<input type="text" class="form-topic-drop-search-input" placeholder="filter topics">
</div>
<ul class="topic_menu">
${render_form_filter_dropdown(category_map)}
</ul>
</div>
</div>
<div class="options">
<input type="checkbox" name="follow" class="discussion-follow" class="discussion-follow" id="new-post-follow" checked><label for="new-post-follow">follow this post</label>
<br>
% if course.metadata.get("allow_anonymous", True):
<input type="checkbox" name="anonymous" class="discussion-anonymous" id="new-post-anonymous"><label for="new-post-anonymous">post anonymously</label>
%elif course.metadata.get("allow_anonymous_to_peers", False):
<input type="checkbox" name="anonymous_to_peers" class="discussion-anonymous-to-peers" id="new-post-anonymous-to-peers"><label for="new-post-anonymous-to-peers">post anonymously to classmates</label>
%endif
</div>
</div>
<div class="right-column">
<ul class="new-post-form-errors"></ul>
<div class="form-row">
<input type="text" class="new-post-title" name="title" placeholder="Title">
</div>
<div class="form-row">
<div class="new-post-body" name="body" placeholder="Enter your question or comment…"></div>
<!---<div class="new-post-preview"><span class="new-post-preview-label">Preview</span></div>-->
</div>
## TODO tags commenting out til we figure out what to do w/ tags
##<div class="form-row">
## <input type="text" class="new-post-tags" name="tags" placeholder="Tags">
##</div>
<input type="submit" class="submit" value="Add post">
<a href="#" class="new-post-cancel">Cancel</a>
</div>
</form>
</div>
</article>
<%namespace name="renderer" file="_content_renderer.html"/> <%namespace name="renderer" file="_content_renderer.html"/>
<%! from django_comment_client.mustache_helpers import url_for_user %>
<section class="discussion" _id="${discussion_id | h}"> <article class="discussion-article" data-id="${discussion_id| h}">
<a class="discussion-title" href="javascript:void(0)">Discussion</a> <a href="#" class="dogear"></a>
<div class="threads"> <div class="discussion-post">
${renderer.render_content_with_comments(thread)} <header>
<a href="#" class="vote-btn discussion-vote discussion-vote-up"><span class="plus-icon">+</span> <span class='votes-count-number'>${thread['votes']['up_count']}</span></a>
<h1>${thread['title']}</h1>
<p class="posted-details">
<span class="timeago" title="${thread['created_at'] | h}">sometime</span> by
<a href="${url_for_user(thread, thread['user_id'])}">${thread['username']}</a>
</p>
</header>
<div class="post-body">
${thread['body']}
</div>
</div> </div>
</section> <ol class="responses">
% for reply in thread.get("children", []):
<li>
<div class="response-body">${reply['body']}</div>
<ol class="comments">
% for comment in reply.get("children", []):
<li><div class="comment-body">${comment['body']}</div></li>
% endfor
</ol>
</li>
% endfor
</ol>
</article>
<%include file="_js_data.html" /> <%include file="_js_data.html" />
\ No newline at end of file
<script type="text/template" id="thread-list-template">
<div class="browse-search">
<div class="browse is-open">
<a href="#" class="browse-topic-drop-icon"></a>
<a href="#" class="browse-topic-drop-btn"><span class="current-board">All</span> <span class="drop-arrow">▾</span></a>
</div>
<%include file="_filter_dropdown.html" />
<div class="search">
<form class="post-search">
<input type="text" placeholder="Search all discussions" class="post-search-field">
</form>
</div>
</div>
<div class="sort-bar">
<span class="sort-label">Sort by:</span>
<ul>
<li><a href="#" class="active" data-sort="date">date</a></li>
<li><a href="#" data-sort="votes">votes</a></li>
<li><a href="#" data-sort="comments">comments</a></li>
</ul>
</div>
<div class="post-list-wrapper">
<ul class="post-list">
</ul>
</div>
</script>
<script type="text/template" id="thread-template">
<article class="discussion-article" data-id="${'<%- id %>'}">
<div class="thread-content-wrapper"></div>
<ol class="responses">
<li class="loading"><div class="loading-animation"></div></li>
</ol>
<div class="post-status-closed bottom-post-status" style="display: none">
This thread is closed.
</div>
<form class="discussion-reply-new" data-id="${'<%- id %>'}">
<h4>Post a response:</h4>
<ul class="discussion-errors"></ul>
<div class="reply-body" data-id="${'<%- id %>'}"></div>
<div class="reply-post-control">
<a class="discussion-submit-post control-button" href="#">Submit</a>
</div>
</form>
</article>
</script>
<script type="text/template" id="thread-show-template">
<div class="discussion-post">
<div><a href="javascript:void(0)" class="dogear action-follow" data-tooltip="follow"></a></div>
<header>
<a href="#" class="vote-btn discussion-vote discussion-vote-up" data-role="discussion-vote" data-tooltip="vote"><span class="plus-icon">+</span> <span class='votes-count-number'>${'<%- votes["up_count"] %>'}</span></a>
<h1>${'<%- title %>'}</h1>
<p class="posted-details">
${"<% if (obj.username) { %>"}
<a href="${'<%- user_url %>'}" class="username">${'<%- username %>'}</a>
${"<% } else {print('anonymous');} %>"}
<span class="timeago" title="${'<%- created_at %>'}">${'<%- created_at %>'}</span>
<span class="post-status-closed top-post-status" style="display: none">
&bull; This thread is closed.
</span>
</p>
</header>
<div class="post-body">${'<%- body %>'}</div>
${'<% if (obj.courseware_url) { %>'}
<div class="post-context">
(this post is about <a href="${'<%- courseware_url%>'}">${'<%- courseware_title %>'}</a>)
</div>
${'<% } %>'}
<ul class="moderator-actions">
<li style="display: none"><a class="action-edit" href="javascript:void(0)"><span class="edit-icon"></span> Edit</a></li>
<li style="display: none"><a class="action-delete" href="javascript:void(0)"><span class="delete-icon"></span> Delete</a></li>
<li style="display: none"><a class="action-openclose" href="javascript:void(0)"><span class="edit-icon"></span> Close</a></li>
</ul>
</div>
</script>
<script type="text/template" id="thread-edit-template">
<div class="discussion-post edit-post-form">
<h1>Editing post</h1>
<ul class="edit-post-form-errors"></ul>
<div class="form-row">
<input type="text" class="edit-post-title" name="title" value="${"<%-title %>"}" placeholder="Title">
</div>
<div class="form-row">
<div class="edit-post-body" name="body">${"<%- body %>"}</div>
</div>
## TODO tags
## Until we decide what to do with tags, commenting them out.
##<div class="form-row">
## <input type="text" class="edit-post-tags" name="tags" placeholder="Tags" value="${"<%- tags %>"}">
##</div>
<input type="submit" class="post-update" value="Update post">
<a href="#" class="post-cancel">Cancel</a>
</div>
</script>
<script type="text/template" id="thread-response-template">
<div class="discussion-response"></div>
<ol class="comments">
<li class="new-comment response-local">
<form class="comment-form" data-id="${'<%- wmdId %>'}">
<ul class="discussion-errors"></ul>
<div class="comment-body" data-id="${'<%- wmdId %>'}"
data-placeholder="Add a comment..."></div>
<div class="comment-post-control">
<a class="discussion-submit-comment control-button" href="#">Submit</a>
</div>
</form>
</li>
</ol>
</script>
<script type="text/template" id="thread-response-show-template">
<header class="response-local">
<a href="javascript:void(0)" class="vote-btn" data-tooltip="vote"><span class="plus-icon"></span><span class="votes-count-number">${"<%- votes['up_count'] %>"}</span></a>
<a href="javascript:void(0)" class="endorse-btn${'<% if (endorsed) { %> is-endorsed<% } %>'} action-endorse" style="cursor: default; display: none;" data-tooltip="endorse"><span class="check-icon" style="pointer-events: none; "></span></a>
${"<% if (obj.username) { %>"}
<a href="${'<%- user_url %>'}" class="posted-by">${'<%- username %>'}</a>
${"<% } else {print('<span class=\"anonymous\"><em>anonymous</em></span>');} %>"}
<p class="posted-details" title="${'<%- created_at %>'}">${'<%- created_at %>'}</p>
</header>
<div class="response-local"><div class="response-body">${"<%- body %>"}</div></div>
<ul class="moderator-actions response-local">
<li style="display: none"><a class="action-edit" href="javascript:void(0)"><span class="edit-icon"></span> Edit</a></li>
<li style="display: none"><a class="action-delete" href="javascript:void(0)"><span class="delete-icon"></span> Delete</a></li>
<li style="display: none"><a class="action-openclose" href="javascript:void(0)"><span class="edit-icon"></span> Close</a></li>
</ul>
</script>
<script type="text/template" id="thread-response-edit-template">
<div class="edit-post-form">
<h1>Editing response</h1>
<ul class="edit-post-form-errors"></ul>
<div class="form-row">
<div class="edit-post-body" name="body">${"<%- body %>"}</div>
</div>
<input type="submit" class="post-update" value="Update response">
<a href="#" class="post-cancel">Cancel</a>
</div>
</script>
<script type="text/template" id="response-comment-show-template">
<div id="comment_${'<%- id %>'}">
<div class="response-body">${'<%- body %>'}</div>
<p class="posted-details">&ndash;posted <span class="timeago" title="${'<%- created_at %>'}">${'<%- created_at %>'}</span> by
${"<% if (obj.username) { %>"}
<a href="${'<%- user_url %>'}" class="profile-link">${'<%- username %>'}</a>
${"<% } else {print('anonymous');} %>"}
</p>
</div>
</script>
<script type="text/template" id="thread-list-item-template">
<a href="${'<%- id %>'}" data-id="${'<%- id %>'}"><span class="title">${"<%- title %>"}</span> <span class="comments-count">${"<%- comments_count %>"}</span><span class="votes-count">+${"<%- votes['up_count'] %>"}</span></a>
</script>
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
<div class="user-profile"> <div class="user-profile">
<% <%
role_names = sorted(map(attrgetter('name'), django_user.roles.all())) role_names = sorted(set(map(attrgetter('name'), django_user.roles.all())))
%> %>
<div class="sidebar-username">${django_user.username | h}</div> <div class="sidebar-username">${django_user.username | h}</div>
<div class="sidebar-user-roles"> <div class="sidebar-user-roles">
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
<div class="sidebar-comments-count"><span>${profiled_user['comments_count'] | h}</span> ${pluralize('comment', profiled_user['comments_count']) | h}</div> <div class="sidebar-comments-count"><span>${profiled_user['comments_count'] | h}</span> ${pluralize('comment', profiled_user['comments_count']) | h}</div>
% if check_permissions_by_view(user, course.id, content=None, name='update_moderator_status'): % if check_permissions_by_view(user, course.id, content=None, name='update_moderator_status'):
% if "Moderator" in role_names: % if "Moderator" in role_names:
<a href="javascript:void(0)" class="sidebar-toggle-moderator-button sidebar-revoke-moderator-button">Revoke Moderator provileges</a> <a href="javascript:void(0)" class="sidebar-toggle-moderator-button sidebar-revoke-moderator-button">Revoke Moderator rights</a>
% else: % else:
<a href="javascript:void(0)" class="sidebar-toggle-moderator-button 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
......
<%! import django_comment_client.helpers as helpers %>
<%! from django.template.defaultfilters import escapejs %>
<%! from django.core.urlresolvers import reverse %>
<%inherit file="../main.html" /> <%inherit file="../main.html" />
<%namespace name='static' file='../static_content.html'/> <%namespace name='static' file='../static_content.html'/>
<%block name="bodyclass">discussion</%block> <%block name="bodyclass">discussion</%block>
...@@ -13,26 +17,23 @@ ...@@ -13,26 +17,23 @@
<%static:js group='discussion'/> <%static:js group='discussion'/>
</%block> </%block>
<%include file="../courseware/course_navigation.html" args="active_page='discussion'" /> <%include file="_discussion_course_navigation.html" args="active_page='discussion'" />
<section class="container"> <%include file="_new_post.html" />
<div class="course-wrapper">
<section aria-label="Course Navigation" class="course-index">
<nav>
<article class="sidebar-module discussion-sidebar">
<a href="#" class="sidebar-new-post-button">New Post</a>
</article>
<%include file="_recent_active_posts.html" /> <script type="text/javascript" src="${static.url('js/discussions-temp.js')}"></script>
<%include file="_trending_tags.html" /> <section class="discussion container" id="discussion-container" data-roles="${roles}" data-course-id="${course_id}" data-user-info="${user_info}" data-threads="${threads}" data-thread-pages="${thread_pages}">
<div class="discussion-body">
</nav> <div class="sidebar"></div>
</section> <div class="discussion-column">
<div class="discussion-article blank-slate">
<h1>${course.title} Discussion</h1>
<section class="course-content"> </div>
${content.decode('utf-8')} </div>
</section> </div>
</div>
</section> </section>
<%include file="_underscore_templates.html" />
<%include file="_thread_list_template.html" />
<section class="discussion" data-discussion-id="{{discussionId}}">
<article class="new-post-article">
<span class="topic" data-discussion-id="{{discussionId}}" />
<div class="inner-wrapper">
<div class="new-post-form-errors">
</div>
<form class="new-post-form">
<div class="form-row">
<input type="text" class="new-post-title" name="title" placeholder="Title">
</div>
<div class="form-row">
<div class="new-post-body" name="body" placeholder="Enter your question or comment&hellip;"></div>
<!---<div class="new-post-preview"><span class="new-post-preview-label">Preview</span></div>-->
</div>
{{! TODO tags: Getting rid of tags for now. }}
{{!<div class="form-row">}}
{{! <input type="text" class="new-post-tags" name="tags" placeholder="Tags">}}
{{!</div>}}
<input type="submit" class="submit" value="Add post">
<a href="#" class="new-post-cancel">Cancel</a>
<div class="options">
<input type="checkbox" name="follow" class="discussion-follow" class="discussion-follow" id="new-post-follow" checked><label for="new-post-follow">follow this post</label>
<br>
{{#allow_anonymous}}
<input type="checkbox" name="anonymous" class="discussion-anonymous" id="new-post-anonymous"><label for="new-post-anonymous">post anonymously</label>
{{/allow_anonymous}}
{{#allow_anonymous_to_peers}}
<input type="checkbox" name="anonymous" class="discussion-anonymous-to-peers" id="new-post-anonymous-to-peers"><label for="new-post-anonymous-to-peers">post anonymously to classmates</label>
{{/allow_anonymous_to_peers}}
</div>
</form>
</div>
</article>
<section class="threads">
{{#threads}}
<article class="discussion-thread" id="thread_{{id}}">
</article>
{{/threads}}
</section>
<section class="pagination">
</section>
</section>
<article class="discussion-article" data-id="{{id}}">
<div class="thread-content-wrapper"></div>
<ol class="responses post-extended-content">
<li class="loading"><div class="loading-animation"></div></li>
</ol>
<form class="local discussion-reply-new post-extended-content" data-id="{{id}}">
<h4>Post a response:</h4>
<ul class="discussion-errors"></ul>
<div class="reply-body" data-id="{{id}}"></div>
<div class="reply-post-control">
<a class="discussion-submit-post control-button" href="#">Submit</a>
</div>
</form>
<div class="local post-tools">
<a href="javascript:void(0)" class="expand-post">View discussion</a>
<a href="javascript:void(0)" class="collapse-post">Hide discussion</a>
</div>
</article>
\ No newline at end of file
<div class="discussion-post local">
<div><a href="javascript:void(0)" class="dogear action-follow" data-tooltip="follow"></a></div>
<header>
<a href="#" class="vote-btn discussion-vote discussion-vote-up" data-role="discussion-vote" data-tooltip="vote"><span class="plus-icon">+</span> <span class='votes-count-number'>{{votes.up_count}}</span></a>
<h3>{{title}}</h3>
<p class="posted-details">
{{#user}}
<a href="{{user_url}}" class="username">{{username}}</a>
{{/user}}
{{^user}}
anonymous
{{/user}}
<span class="timeago" title="{{created_at}}">{{created_at}}</span>
<span class="post-status-closed top-post-status" style="display: none">
&bull; This thread is closed.
</span>
</p>
</header>
<div class="post-body">{{abbreviatedBody}}</div>
<ul class="moderator-actions post-extended-content">
<li style="display: none"><a class="action-edit" href="javascript:void(0)"><span class="edit-icon"></span> Edit</a></li>
<li style="display: none"><a class="action-delete" href="javascript:void(0)"><span class="delete-icon"></span> Delete</a></li>
<li style="display: none"><a class="action-openclose" href="javascript:void(0)"><span class="edit-icon"></span> Close</a></li>
</ul>
</div>
...@@ -3,7 +3,8 @@ ...@@ -3,7 +3,8 @@
<input type="text" class="new-post-title title-input" placeholder="Title" /> <input type="text" class="new-post-title title-input" placeholder="Title" />
<div class="new-post-similar-posts-wrapper" style="display: none"></div> <div class="new-post-similar-posts-wrapper" style="display: none"></div>
<div class="new-post-body reply-body"></div> <div class="new-post-body reply-body"></div>
<input class="new-post-tags" placeholder="Tags" /> {{! TODO tags: Getting rid of tags for now. }}
{{! <input class="new-post-tags" placeholder="Tags" /> }}
<div class="post-options"> <div class="post-options">
<input type="checkbox" class="discussion-post-anonymously" id="discussion-post-anonymously-${discussion_id}"> <input type="checkbox" class="discussion-post-anonymously" id="discussion-post-anonymously-${discussion_id}">
<label for="discussion-post-anonymously-${discussion_id}">post anonymously</label> <label for="discussion-post-anonymously-${discussion_id}">post anonymously</label>
......
<nav class="discussion-{{discussiontype}}-paginator discussion-paginator local">
<ol>
{{#previous}}
<li><a class="discussion-pagination" href="{{url}}" data-page-number="{{number}}">&lt; Previous</a></li>
{{/previous}}
{{#first}}
<li><a class="discussion-pagination" href="{{url}}" data-page-number="1">1</a></li>
{{/first}}
{{#leftdots}}
<li>&hellip;</li>
{{/leftdots}}
{{#lowPages}}
<li><a class="discussion-pagination" href="{{url}}" data-page-number="{{number}}">{{number}}</a></li>
{{/lowPages}}
<li class="current-page">{{page}}</li>
{{#highPages}}
<li><a class="discussion-pagination" href="{{url}}" data-page-number="{{number}}">{{number}}</a></li>
{{/highPages}}
{{#rightdots}}
<li>&hellip;</li>
{{/rightdots}}
{{#last}}
<li><a class="discussion-pagination" href="{{url}}" data-page-number="{{number}}">{{number}}</a></li>
{{/last}}
{{#next}}
<li><a class="discussion-pagination" href="{{url}}" data-page-number="{{number}}">Next &gt;</a></li>
{{/next}}
</ol>
</nav>
<article class="discussion-article" data-id="{{id}}">
<div class="local"><a href="javascript:void(0)" class="dogear action-follow"></a></div>
<div class="discussion-post local">
<header>
<a href="#" class="vote-btn discussion-vote discussion-vote-up" data-role="discussion-vote"><span class="plus-icon">+</span> <span class='votes-count-number'>{{votes.up_count}}</span></a>
<h3>{{title}}</h3>
<p class="posted-details">
{{#user}}
<a href="{{user_url}}" class="username">{{username}}</a>
{{/user}}
{{^user}}
anonymous
{{/user}}
<span class="timeago" title="{{created_at}}">{{created_at}}</span>
<span class="post-status-closed top-post-status" style="display: none">
&bull; This thread is closed.
</span>
</p>
</header>
<div class="post-body">{{abbreviatedBody}}</div>
</div>
<ol class="responses post-extended-content">
<li class="loading"></li>
</ol>
<div class="local post-tools">
<a href="{{permalink}}">View discussion</a>
<!-- <a href="javascript:void(0)" class="collapse-post">Hide discussion</a> -->
</div>
</article>
<section class="discussion-user-threads" >
<section class="discussion">
{{#threads}}
<article class="discussion-thread" id="thread_{{id}}">
</article>
{{/threads}}
</div>
<section class="pagination">
</section>
</section>
<%! import django_comment_client.helpers as helpers %>
<%! from django.template.defaultfilters import escapejs %>
<%! from django.core.urlresolvers import reverse %>
<%! from courseware.access import has_access %>
<%inherit file="../main.html" /> <%inherit file="../main.html" />
<%namespace name='static' file='../static_content.html'/> <%namespace name='static' file='../static_content.html'/>
<%block name="bodyclass">discussion</%block> <%block name="bodyclass">discussion</%block>
...@@ -11,26 +16,20 @@ ...@@ -11,26 +16,20 @@
<%block name="js_extra"> <%block name="js_extra">
<%include file="_js_body_dependencies.html" /> <%include file="_js_body_dependencies.html" />
<%static:js group='discussion'/> <%static:js group='discussion'/>
<script type="text/javascript" src="${static.url('js/discussions-temp.js')}"></script>
</%block> </%block>
<%include file="../courseware/course_navigation.html" args="active_page='discussion'" /> <%include file="_discussion_course_navigation.html" args="active_page='discussion'"/>
<section class="container"> <%include file="_new_post.html" />
<div class="course-wrapper">
<section aria-label="Course Navigation" class="course-index"> <section class="discussion container" id="discussion-container" data-roles="${roles}" data-course-id="${course_id}" data-user-info="${user_info}" data-threads="${threads}" data-content-info="${annotated_content_info}" data-thread-pages="${thread_pages}">
<nav> <div class="discussion-body">
<div class="sidebar"></div>
<article class="sidebar-module discussion-sidebar"> <div class="discussion-column"></div>
</article>
<%include file="_recent_active_posts.html" />
<%include file="_trending_tags.html" />
</nav>
</section>
<section class="course-content">
${content.decode('utf-8')}
</section>
</div> </div>
</section> </section>
<%include file="_underscore_templates.html" />
<%include file="_thread_list_template.html" />
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
<div class="course-wrapper"> <div class="course-wrapper">
<section aria-label="User Profile" class="user-profile"> <section aria-label="User Profile" class="user-profile">
<nav> <nav>
<article class="sidebar-module discussion-sidebar"> <article class="sidebar-module discussion-sidebar">
<%include file="_user_profile.html" /> <%include file="_user_profile.html" />
</article> </article>
...@@ -29,8 +29,8 @@ ...@@ -29,8 +29,8 @@
</nav> </nav>
</section> </section>
<section class="course-content"> <section class="course-content container discussion-user-threads" data-user-id="${django_user.id | escapejs}" data-course-id="${course.id | escapejs}" data-threads="${threads}" data-user-info="${user_info}">
${content.decode('utf-8')} <h2>Active Threads</h2>
</section> </section>
</div> </div>
</section> </section>
...@@ -39,3 +39,4 @@ ...@@ -39,3 +39,4 @@
var $$profiled_user_id = "${django_user.id | escapejs}"; var $$profiled_user_id = "${django_user.id | escapejs}";
var $$course_id = "${course.id | escapejs}"; var $$course_id = "${course.id | escapejs}";
</script> </script>
<%include file="_underscore_templates.html" />
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