Commit b7986b76 by kimth

Merge branch 'master' into kimth/remember-caption

parents 675aef20 04c83d52
......@@ -382,10 +382,10 @@ class LoncapaProblem(object):
original_path = sys.path
for script in scripts:
sys.path = original_path + self._extract_system_path(script)
stype = script.get('type')
if stype:
if 'javascript' in stype:
continue # skip javascript
......
......@@ -326,8 +326,16 @@ def textline_dynamath(element, value, status, render_template, msg=''):
count = int(eid.split('_')[-2]) - 1 # HACK
size = element.get('size')
hidden = element.get('hidden', '') # if specified, then textline is hidden and id is stored in div of name given by hidden
# Preprocessor to insert between raw input and Mathjax
preprocessor = {'class_name': element.get('preprocessorClassName',''),
'script_src': element.get('preprocessorSrc','')}
if '' in preprocessor.values():
preprocessor = None
context = {'id': eid, 'value': value, 'state': status, 'count': count, 'size': size,
'msg': msg, 'hidden': hidden,
'preprocessor': preprocessor,
}
html = render_template("textinput_dynamath.html", context)
return etree.XML(html)
......
###
### version of textline.html which does dynammic math
### version of textline.html which does dynamic math
###
<section class="text-input-dynamath">
<section class="text-input-dynamath capa_inputtype" id="inputtype_${id}">
% if preprocessor is not None:
<div class="text-input-dynamath_data" data-preprocessor="${preprocessor['class_name']}"/>
<div class="script_placeholder" data-src="${preprocessor['script_src']}"/>
% endif
% if state == 'unsubmitted':
<div class="unanswered" id="status_${id}">
% elif state == 'correct':
......@@ -20,7 +26,6 @@
style="display:none;"
% endif
/>
<p class="status">
% if state == 'unsubmitted':
unanswered
......
......@@ -11,7 +11,9 @@ class @Problem
$(selector, @el)
bind: =>
MathJax.Hub.Queue ["Typeset", MathJax.Hub]
@el.find('.problem > div').each (index, element) =>
MathJax.Hub.Queue ["Typeset", MathJax.Hub, element]
window.update_schematics()
problem_prefix = @element_id.replace(/problem_/,'')
......@@ -23,7 +25,11 @@ class @Problem
@$('section.action input.reset').click @reset
@$('section.action input.show').click @show
@$('section.action input.save').click @save
@$('input.math').keyup(@refreshMath).each(@refreshMath)
# Dynamath
@$('input.math').keyup(@refreshMath)
@$('input.math').each (index, element) =>
MathJax.Hub.Queue [@refreshMath, null, element]
updateProgress: (response) =>
if response.progress_changed
......@@ -262,7 +268,9 @@ class @Problem
showMethod = @inputtypeShowAnswerMethods[cls]
showMethod(inputtype, display, answers) if showMethod?
MathJax.Hub.Queue ["Typeset", MathJax.Hub]
@el.find('.problem > div').each (index, element) =>
MathJax.Hub.Queue ["Typeset", MathJax.Hub, element]
@$('.show').val 'Hide Answer'
@el.addClass 'showed'
@updateProgress response
......@@ -296,11 +304,20 @@ class @Problem
refreshMath: (event, element) =>
element = event.target unless element
target = "display_#{element.id.replace(/^input_/, '')}"
elid = element.id.replace(/^input_/,'')
target = "display_" + elid
# MathJax preprocessor is loaded by 'setupInputTypes'
preprocessor_tag = "inputtype_" + elid
mathjax_preprocessor = @inputtypeDisplays[preprocessor_tag]
if jax = MathJax.Hub.getAllJax(target)[0]
MathJax.Hub.Queue ['Text', jax, $(element).val()],
[@updateMathML, jax, element]
eqn = $(element).val()
if mathjax_preprocessor
eqn = mathjax_preprocessor(eqn)
MathJax.Hub.Queue(['Text', jax, eqn], [@updateMathML, jax, element])
return # Explicit return for CoffeeScript
updateMathML: (jax, element) =>
try
......@@ -317,6 +334,22 @@ class @Problem
@answers = @inputs.serialize()
inputtypeSetupMethods:
'text-input-dynamath': (element) =>
###
Return: function (eqn) -> eqn that preprocesses the user formula input before
it is fed into MathJax. Return 'false' if no preprocessor specified
###
data = $(element).find('.text-input-dynamath_data')
preprocessorClassName = data.data('preprocessor')
preprocessorClass = window[preprocessorClassName]
if not preprocessorClass?
return false
else
preprocessor = new preprocessorClass()
return preprocessor.fn
javascriptinput: (element) =>
data = $(element).find(".javascriptinput_data")
......
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
......@@ -72,9 +72,9 @@ For convenience, add the following environment variables to the terminal (assumi
export DJANGO_SETTINGS_MODULE=lms.envs.dev
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"):
......
......@@ -195,7 +195,11 @@ def get_course_syllabus_section(course, section_key):
if section_key in ['syllabus', 'guest_syllabus']:
try:
with course.system.resources_fs.open(path("syllabus") / section_key + ".html") as htmlFile:
fs = course.system.resources_fs
# first look for a run-specific version
dirs = [path("syllabus") / course.url_name, path("syllabus")]
filepath = find_file(fs, dirs, section_key + ".html")
with fs.open(filepath) as htmlFile:
return replace_urls(htmlFile.read().decode('utf-8'),
course.metadata['data_dir'])
except ResourceNotFoundError:
......
......@@ -22,7 +22,7 @@ from django.contrib.auth.models import User
from mitxmako.shortcuts import render_to_response, render_to_string
from courseware.courses import get_course_with_access
from django_comment_client.utils import JsonResponse, JsonError, extract
from django_comment_client.utils import JsonResponse, JsonError, extract, get_courseware_context
from django_comment_client.permissions import check_permissions_by_view
from django_comment_client.models import Role
......@@ -38,11 +38,10 @@ def permitted(fn):
else:
content = None
return content
if check_permissions_by_view(request.user, kwargs['course_id'], fetch_content(), request.view_name):
return fn(request, *args, **kwargs)
else:
return JsonError("unauthorized")
return JsonError("unauthorized", status=401)
return wrapper
def ajax_content_response(request, course_id, content, template_name):
......@@ -63,10 +62,23 @@ def ajax_content_response(request, course_id, content, template_name):
@login_required
@permitted
def create_thread(request, course_id, commentable_id):
course = get_course_with_access(request.user, course_id, 'load')
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.update_attributes(**{
'anonymous' : post.get('anonymous', 'false').lower() == 'true',
'anonymous' : anonymous,
'anonymous_to_peers' : anonymous_to_peers,
'commentable_id' : commentable_id,
'course_id' : course_id,
'user_id' : request.user.id,
......@@ -75,10 +87,14 @@ def create_thread(request, course_id, commentable_id):
if post.get('auto_subscribe', 'false').lower() == 'true':
user = cc.User.from_django_user(request.user)
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():
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:
return JsonResponse(utils.safe_content(thread.to_dict()))
return JsonResponse(utils.safe_content(data))
@require_POST
@login_required
......@@ -95,8 +111,21 @@ def update_thread(request, course_id, thread_id):
def _create_comment(request, course_id, thread_id=None, parent_id=None):
post = request.POST
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(**{
'anonymous' : post.get('anonymous', 'false').lower() == 'true',
'anonymous' : anonymous,
'anonymous_to_peers' : anonymous_to_peers,
'user_id' : request.user.id,
'course_id' : course_id,
'thread_id' : thread_id,
......
import json
import logging
from django.contrib.auth.decorators import login_required
from django.views.decorators.http import require_POST
from django.http import HttpResponse, Http404
......@@ -15,14 +18,15 @@ from operator import methodcaller
from django_comment_client.permissions import check_permissions_by_view
from django_comment_client.utils import merge_dict, extract, strip_none, strip_blank, get_courseware_context
import json
import django_comment_client.utils as utils
import comment_client as cc
import xml.sax.saxutils as saxutils
THREADS_PER_PAGE = 8
THREADS_PER_PAGE = 20
INLINE_THREADS_PER_PAGE = 20
PAGES_NEARBY_DELTA = 2
escapedict = {'"': '&quot;'}
log = logging.getLogger("edx.discussions")
def _general_discussion_id(course_id):
return course_id.replace('/', '_').replace('.', '_')
......@@ -31,9 +35,8 @@ def _should_perform_search(request):
return bool(request.GET.get('text', False) or \
request.GET.get('tags', False))
def render_accordion(request, course, discussion_id):
# TODO: Delete if obsolete
discussion_info = utils.get_categorized_discussion_info(request, course)
context = {
......@@ -45,69 +48,7 @@ def render_accordion(request, course, discussion_id):
return render_to_string('discussion/_accordion.html', context)
def render_discussion(request, course_id, threads, *args, **kwargs):
discussion_id = kwargs.get('discussion_id')
user_id = kwargs.get('user_id')
discussion_type = kwargs.get('discussion_type', 'inline')
query_params = kwargs.get('query_params', {})
template = {
'inline': 'discussion/_inline.html',
'forum': 'discussion/_forum.html',
'user': 'discussion/_user_active_threads.html',
}[discussion_type]
base_url = {
'inline': (lambda: reverse('django_comment_client.forum.views.inline_discussion', args=[course_id, discussion_id])),
'forum': (lambda: reverse('django_comment_client.forum.views.forum_form_discussion', args=[course_id])),
'user': (lambda: reverse('django_comment_client.forum.views.user_profile', args=[course_id, user_id])),
}[discussion_type]()
user_info = cc.User.from_django_user(request.user).to_dict()
def infogetter(thread):
return utils.get_annotated_content_infos(course_id, thread, request.user, user_info)
annotated_content_info = reduce(merge_dict, map(infogetter, threads), {})
if discussion_type != 'inline':
course = get_course_with_access(request.user, course_id, 'load')
for thread in threads:
courseware_context = get_courseware_context(thread, course)
if courseware_context:
thread['courseware_location'] = courseware_context['courseware_location']
thread['courseware_title'] = courseware_context['courseware_title']
context = {
'threads': threads,
'discussion_id': discussion_id,
'user_id': user_id,
'course_id': course_id,
'request': request,
'performed_search': _should_perform_search(request),
'pages_nearby_delta': PAGES_NEARBY_DELTA,
'discussion_type': discussion_type,
'base_url': base_url,
'query_params': strip_blank(strip_none(extract(query_params, ['page', 'sort_key', 'sort_order', 'tags', 'text']))),
'annotated_content_info': json.dumps(annotated_content_info),
'discussion_data': json.dumps({ (discussion_id or user_id): map(utils.safe_content, threads) })
}
context = dict(context.items() + query_params.items())
return render_to_string(template, context)
def render_inline_discussion(*args, **kwargs):
return render_discussion(discussion_type='inline', *args, **kwargs)
def render_forum_discussion(*args, **kwargs):
return render_discussion(discussion_type='forum', *args, **kwargs)
def render_user_discussion(*args, **kwargs):
return render_discussion(discussion_type='user', *args, **kwargs)
def get_threads(request, course_id, discussion_id=None):
def get_threads(request, course_id, discussion_id=None, per_page=THREADS_PER_PAGE):
"""
This may raise cc.utils.CommentClientError or
cc.utils.CommentClientUnknownError if something goes wrong.
......@@ -115,7 +56,7 @@ def get_threads(request, course_id, discussion_id=None):
default_query_params = {
'page': 1,
'per_page': THREADS_PER_PAGE,
'per_page': per_page,
'sort_key': 'date',
'sort_order': 'desc',
'text': '',
......@@ -137,7 +78,7 @@ def get_threads(request, course_id, discussion_id=None):
user.save()
query_params = merge_dict(default_query_params,
strip_none(extract(request.GET, ['page', 'sort_key', 'sort_order', 'text', 'tags'])))
strip_none(extract(request.GET, ['page', 'sort_key', 'sort_order', 'text', 'tags', 'commentable_ids'])))
threads, page, num_pages = cc.Thread.search(query_params)
......@@ -146,132 +87,182 @@ def get_threads(request, course_id, discussion_id=None):
return threads, query_params
# discussion per page is fixed for now
def inline_discussion(request, course_id, discussion_id):
"""
Renders JSON for DiscussionModules
"""
course = get_course_with_access(request.user, course_id, 'load')
try:
threads, query_params = get_threads(request, course_id, discussion_id)
threads, query_params = get_threads(request, course_id, discussion_id, per_page=INLINE_THREADS_PER_PAGE)
user_info = cc.User.from_django_user(request.user).to_dict()
except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError) as err:
# TODO (vshnayder): since none of this code seems to be aware of the fact that
# sometimes things go wrong, I suspect that the js client is also not
# checking for errors on request. Check and fix as needed.
raise Http404
html = render_inline_discussion(request, course_id, threads, discussion_id=discussion_id, \
query_params=query_params)
def infogetter(thread):
return utils.get_annotated_content_infos(course_id, thread, request.user, user_info)
annotated_content_info = reduce(merge_dict, map(infogetter, threads), {})
allow_anonymous = course.metadata.get("allow_anonymous", True)
allow_anonymous_to_peers = course.metadata.get("allow_anonymous_to_peers", False)
return utils.JsonResponse({
'html': html,
'discussion_data': map(utils.safe_content, threads),
'user_info': user_info,
'annotated_content_info': annotated_content_info,
'page': query_params['page'],
'num_pages': query_params['num_pages'],
'roles': utils.get_role_ids(course_id),
'allow_anonymous_to_peers': allow_anonymous_to_peers,
'allow_anonymous': allow_anonymous,
})
def render_search_bar(request, course_id, discussion_id=None, text=''):
if not discussion_id:
return ''
context = {
'discussion_id': discussion_id,
'text': text,
'course_id': course_id,
}
return render_to_string('discussion/_search_bar.html', context)
@login_required
def forum_form_discussion(request, course_id):
"""
Renders the main Discussion page, potentially filtered by a search query
"""
course = get_course_with_access(request.user, course_id, 'load')
category_map = utils.get_discussion_category_map(course)
try:
threads, query_params = get_threads(request, course_id)
content = render_forum_discussion(request, course_id, threads, discussion_id=_general_discussion_id(course_id), query_params=query_params)
unsafethreads, query_params = get_threads(request, course_id) # This might process a search query
threads = [utils.safe_content(thread) for thread in unsafethreads]
except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError) as err:
raise Http404
user_info = cc.User.from_django_user(request.user).to_dict()
def infogetter(thread):
return utils.get_annotated_content_infos(course_id, thread, request.user, user_info)
annotated_content_info = reduce(merge_dict, map(infogetter, threads), {})
for thread in threads:
courseware_context = get_courseware_context(thread, course)
if courseware_context:
thread.update(courseware_context)
if request.is_ajax():
return utils.JsonResponse({
'html': content,
'discussion_data': map(utils.safe_content, threads),
'discussion_data': threads, # TODO: Standardize on 'discussion_data' vs 'threads'
'annotated_content_info': annotated_content_info,
'num_pages': query_params['num_pages'],
'page': query_params['page'],
})
else:
recent_active_threads = cc.search_recent_active_threads(
course_id,
recursive=False,
query_params={'follower_id': request.user.id},
)
trending_tags = cc.search_trending_tags(
course_id,
)
#recent_active_threads = cc.search_recent_active_threads(
# course_id,
# recursive=False,
# query_params={'follower_id': request.user.id},
#)
#trending_tags = cc.search_trending_tags(
# course_id,
#)
context = {
'csrf': csrf(request)['csrf_token'],
'course': course,
'content': content,
'recent_active_threads': recent_active_threads,
'trending_tags': trending_tags,
#'recent_active_threads': recent_active_threads,
#'trending_tags': trending_tags,
'staff_access' : has_access(request.user, course, 'staff'),
'threads': saxutils.escape(json.dumps(threads),escapedict),
'thread_pages': query_params['num_pages'],
'user_info': saxutils.escape(json.dumps(user_info),escapedict),
'annotated_content_info': saxutils.escape(json.dumps(annotated_content_info),escapedict),
'course_id': course.id,
'category_map': category_map,
'roles': saxutils.escape(json.dumps(utils.get_role_ids(course_id)), escapedict),
}
# print "start rendering.."
return render_to_response('discussion/index.html', context)
except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError) as err:
raise Http404
def render_single_thread(request, discussion_id, course_id, thread_id):
thread = cc.Thread.find(thread_id).retrieve(recursive=True).to_dict()
user_info = cc.User.from_django_user(request.user).to_dict()
annotated_content_info = utils.get_annotated_content_infos(course_id, thread=thread, user=request.user, user_info=user_info)
context = {
'discussion_id': discussion_id,
'thread': thread,
'annotated_content_info': json.dumps(annotated_content_info),
'course_id': course_id,
'request': request,
'discussion_data': json.dumps({ discussion_id: [utils.safe_content(thread)] }),
}
return render_to_string('discussion/_single_thread.html', context)
@login_required
def single_thread(request, course_id, discussion_id, thread_id):
try:
if request.is_ajax():
course = get_course_with_access(request.user, course_id, 'load')
user_info = cc.User.from_django_user(request.user).to_dict()
try:
thread = cc.Thread.find(thread_id).retrieve(recursive=True)
except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError) as err:
raise Http404
courseware_context = get_courseware_context(thread, course)
annotated_content_info = utils.get_annotated_content_infos(course_id, thread, request.user, user_info=user_info)
context = {'thread': thread.to_dict(), 'course_id': course_id}
html = render_to_string('discussion/_ajax_single_thread.html', context)
# TODO: Remove completely or switch back to server side rendering
# html = render_to_string('discussion/_ajax_single_thread.html', context)
content = utils.safe_content(thread.to_dict())
if courseware_context:
content.update(courseware_context)
return utils.JsonResponse({
'html': html,
'content': utils.safe_content(thread.to_dict()),
#'html': html,
'content': content,
'annotated_content_info': annotated_content_info,
})
else:
course = get_course_with_access(request.user, course_id, 'load')
category_map = utils.get_discussion_category_map(course)
try:
threads, query_params = get_threads(request, course_id)
thread = cc.Thread.find(thread_id).retrieve(recursive=True)
threads.append(thread.to_dict())
except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError) as err:
raise Http404
course = get_course_with_access(request.user, course_id, 'load')
for thread in threads:
courseware_context = get_courseware_context(thread, course)
if courseware_context:
thread.update(courseware_context)
threads = [utils.safe_content(thread) for thread in threads]
#recent_active_threads = cc.search_recent_active_threads(
# course_id,
# recursive=False,
# query_params={'follower_id': request.user.id},
#)
#trending_tags = cc.search_trending_tags(
# course_id,
#)
user_info = cc.User.from_django_user(request.user).to_dict()
recent_active_threads = cc.search_recent_active_threads(
course_id,
recursive=False,
query_params={'follower_id': request.user.id},
)
def infogetter(thread):
return utils.get_annotated_content_infos(course_id, thread, request.user, user_info)
trending_tags = cc.search_trending_tags(
course_id,
)
annotated_content_info = reduce(merge_dict, map(infogetter, threads), {})
context = {
'discussion_id': discussion_id,
'csrf': csrf(request)['csrf_token'],
'init': '',
'content': render_single_thread(request, discussion_id, course_id, thread_id),
'init': '', #TODO: What is this?
'user_info': saxutils.escape(json.dumps(user_info),escapedict),
'annotated_content_info': saxutils.escape(json.dumps(annotated_content_info), escapedict),
'course': course,
'recent_active_threads': recent_active_threads,
'trending_tags': trending_tags,
'course_id': course.id,
#'recent_active_threads': recent_active_threads,
#'trending_tags': trending_tags,
'course_id': course.id, #TODO: Why pass both course and course.id to template?
'thread_id': thread_id,
'threads': saxutils.escape(json.dumps(threads), escapedict),
'category_map': category_map,
'roles': saxutils.escape(json.dumps(utils.get_role_ids(course_id)), escapedict),
'thread_pages': query_params['num_pages'],
}
return render_to_response('discussion/single_thread.html', context)
except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError) as err:
raise Http404
@login_required
def user_profile(request, course_id, user_id):
course = get_course_with_access(request.user, course_id, 'load')
......@@ -284,24 +275,30 @@ def user_profile(request, course_id, user_id):
}
threads, page, num_pages = profiled_user.active_threads(query_params)
query_params['page'] = page
query_params['num_pages'] = num_pages
content = render_user_discussion(request, course_id, threads, user_id=user_id, query_params=query_params)
if request.is_ajax():
return utils.JsonResponse({
'html': content,
'discussion_data': map(utils.safe_content, threads),
})
else:
user_info = cc.User.from_django_user(request.user).to_dict()
def infogetter(thread):
return utils.get_annotated_content_infos(course_id, thread, request.user, user_info)
annotated_content_info = reduce(merge_dict, map(infogetter, threads), {})
context = {
'course': course,
'user': request.user,
'django_user': User.objects.get(id=user_id),
'profiled_user': profiled_user.to_dict(),
'content': content,
'threads': saxutils.escape(json.dumps(threads), escapedict),
'user_info': saxutils.escape(json.dumps(user_info),escapedict),
'annotated_content_info': saxutils.escape(json.dumps(annotated_content_info),escapedict),
# 'content': content,
}
return render_to_response('discussion/user_profile.html', context)
......
......@@ -17,12 +17,6 @@ def pluralize(singular_term, count):
return singular_term + 's'
return singular_term
def show_if(text, condition):
if condition:
return text
else:
return ''
# TODO there should be a better way to handle this
def include_mustache_templates():
mustache_dir = settings.PROJECT_ROOT / 'templates' / 'discussion' / 'mustache'
......
......@@ -7,8 +7,10 @@ class Command(BaseCommand):
help = 'Seed default permisssions and roles'
def handle(self, *args, **options):
if len(args) != 1:
raise CommandError("The number of arguments does not match. ")
if len(args) == 0:
raise CommandError("Please provide a course id")
if len(args) > 1:
raise CommandError("Too many arguments")
course_id = args[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]
......
......@@ -5,7 +5,8 @@ from student.models import CourseEnrollment
import logging
from util.cache import cache
from django.core import cache
cache = cache.get_cache('default')
def cached_has_permission(user, permission, course_id=None):
"""
......@@ -75,12 +76,12 @@ def check_conditions_permissions(user, permissions, course_id, **kwargs):
VIEW_PERMISSIONS = {
'update_thread' : ['edit_content', ['update_thread', 'is_open', 'is_author']],
'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']],
'endorse_comment' : ['endorse_comment'],
'openclose_thread' : ['openclose_thread'],
'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']],
'undo_vote_for_comment': [['unvote', 'is_open']],
'vote_for_thread' : [['vote', 'is_open']],
......
import time
from collections import defaultdict
from importlib import import_module
from courseware.models import StudentModuleCache
from courseware.module_render import get_module
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.search import path_to_location
from django.http import HttpResponse
from django.utils import simplejson
from django.db import connection
......@@ -23,8 +26,7 @@ import pystache_custom as pystache
# TODO these should be cached via django's caching rather than in-memory globals
_FULLMODULES = None
_DISCUSSIONINFO = None
_DISCUSSIONINFO = defaultdict(dict)
def extract(dic, keys):
return {k: dic.get(k) for k in keys}
......@@ -40,6 +42,14 @@ def strip_blank(dic):
def merge_dict(dic1, dic2):
return dict(dic1.items() + dic2.items())
def get_role_ids(course_id):
roles = Role.objects.filter(course_id=course_id)
staff = list(User.objects.filter(is_staff=True).values_list('id', flat=True))
roles_with_ids = {'Staff': staff}
for role in roles:
roles_with_ids[role.name] = list(role.users.values_list('id', flat=True))
return roles_with_ids
def get_full_modules():
global _FULLMODULES
if not _FULLMODULES:
......@@ -51,23 +61,60 @@ def get_discussion_id_map(course):
return a dict of the form {category: modules}
"""
global _DISCUSSIONINFO
if not _DISCUSSIONINFO:
if not _DISCUSSIONINFO[course.id]:
initialize_discussion_info(course)
return _DISCUSSIONINFO['id_map']
return _DISCUSSIONINFO[course.id]['id_map']
def get_discussion_title(request, course, discussion_id):
def get_discussion_title(course, discussion_id):
global _DISCUSSIONINFO
if not _DISCUSSIONINFO:
if not _DISCUSSIONINFO[course.id]:
initialize_discussion_info(course)
title = _DISCUSSIONINFO['id_map'].get(discussion_id, {}).get('title', '(no title)')
title = _DISCUSSIONINFO[course.id]['id_map'].get(discussion_id, {}).get('title', '(no title)')
return title
def get_discussion_category_map(course):
global _DISCUSSIONINFO
if not _DISCUSSIONINFO:
if not _DISCUSSIONINFO[course.id]:
initialize_discussion_info(course)
return _DISCUSSIONINFO['category_map']
return filter_unstarted_categories(_DISCUSSIONINFO[course.id]['category_map'])
def filter_unstarted_categories(category_map):
now = time.gmtime()
result_map = {}
unfiltered_queue = [category_map]
filtered_queue = [result_map]
while len(unfiltered_queue) > 0:
unfiltered_map = unfiltered_queue.pop()
filtered_map = filtered_queue.pop()
filtered_map["children"] = []
filtered_map["entries"] = {}
filtered_map["subcategories"] = {}
for child in unfiltered_map["children"]:
if child in unfiltered_map["entries"]:
if unfiltered_map["entries"][child]["start_date"] <= now:
filtered_map["children"].append(child)
filtered_map["entries"][child] = {}
for key in unfiltered_map["entries"][child]:
if key != "start_date":
filtered_map["entries"][child][key] = unfiltered_map["entries"][child][key]
else:
print "filtering %s" % child, unfiltered_map["entries"][child]["start_date"]
else:
if unfiltered_map["subcategories"][child]["start_date"] < now:
filtered_map["children"].append(child)
filtered_map["subcategories"][child] = {}
unfiltered_queue.append(unfiltered_map["subcategories"][child])
filtered_queue.append(filtered_map["subcategories"][child])
return result_map
def sort_map_entries(category_map):
things = []
......@@ -78,11 +125,10 @@ def sort_map_entries(category_map):
sort_map_entries(category_map["subcategories"][title])
category_map["children"] = [x[0] for x in sorted(things, key=lambda x: x[1]["sort_key"])]
def initialize_discussion_info(course):
global _DISCUSSIONINFO
if _DISCUSSIONINFO:
if _DISCUSSIONINFO[course.id]:
return
course_id = course.id
......@@ -91,45 +137,68 @@ def initialize_discussion_info(course):
all_modules = get_full_modules()[course_id]
discussion_id_map = {}
unexpanded_category_map = defaultdict(list)
for location, module in all_modules.items():
if location.category == 'discussion':
id = module.metadata['id']
category = module.metadata['discussion_category']
title = module.metadata['for']
sort_key = module.metadata.get('sort_key', title)
discussion_id_map[id] = {"location": location, "title": title}
category = " / ".join([x.strip() for x in category.split("/")])
last_category = category.split("/")[-1]
discussion_id_map[id] = {"location": location, "title": last_category + " / " + title}
unexpanded_category_map[category].append({"title": title, "id": id,
"sort_key": sort_key})
"sort_key": sort_key, "start_date": module.start})
category_map = {"entries": defaultdict(dict), "subcategories": defaultdict(dict)}
for category_path, entries in unexpanded_category_map.items():
node = category_map["subcategories"]
path = [x.strip() for x in category_path.split("/")]
# Find the earliest start date for the entries in this category
category_start_date = None
for entry in entries:
if category_start_date is None or entry["start_date"] < category_start_date:
category_start_date = entry["start_date"]
for level in path[:-1]:
if level not in node:
node[level] = {"subcategories": defaultdict(dict),
"entries": defaultdict(dict),
"sort_key": level}
"sort_key": level,
"start_date": category_start_date}
else:
if node[level]["start_date"] > category_start_date:
node[level]["start_date"] = category_start_date
node = node[level]["subcategories"]
level = path[-1]
if level not in node:
node[level] = {"subcategories": defaultdict(dict),
"entries": defaultdict(dict),
"sort_key": level}
"sort_key": level,
"start_date": category_start_date}
else:
if node[level]["start_date"] > category_start_date:
node[level]["start_date"] = category_start_date
for entry in entries:
node[level]["entries"][entry["title"]] = {"id": entry["id"],
"sort_key": entry["sort_key"]}
"sort_key": entry["sort_key"],
"start_date": entry["start_date"]}
default_topics = {'General': course.location.html_id()}
discussion_topics = course.metadata.get('discussion_topics', default_topics)
for topic, entry in discussion_topics.items():
category_map['entries'][topic] = {"id": entry["id"],
"sort_key": entry.get("sort_key", topic),
"start_date": time.gmtime()}
sort_map_entries(category_map)
_DISCUSSIONINFO = {}
_DISCUSSIONINFO['id_map'] = discussion_id_map
_DISCUSSIONINFO['category_map'] = category_map
_DISCUSSIONINFO[course.id]['id_map'] = discussion_id_map
_DISCUSSIONINFO[course.id]['category_map'] = category_map
class JsonResponse(HttpResponse):
def __init__(self, data=None):
......@@ -138,14 +207,14 @@ class JsonResponse(HttpResponse):
mimetype='application/json; charset=utf8')
class JsonError(HttpResponse):
def __init__(self, error_messages=[]):
def __init__(self, error_messages=[], status=400):
if isinstance(error_messages, str):
error_messages = [error_messages]
content = simplejson.dumps({'errors': error_messages},
indent=2,
ensure_ascii=False)
super(JsonError, self).__init__(content,
mimetype='application/json; charset=utf8', status=400)
mimetype='application/json; charset=utf8', status=status)
class HtmlResponse(HttpResponse):
def __init__(self, html=''):
......@@ -228,8 +297,14 @@ def permalink(content):
args=[content['course_id'], content['commentable_id'], content['thread_id']]) + '#' + content['id']
def extend_content(content):
roles = {}
if content.get('user_id'):
try:
user = User.objects.get(pk=content['user_id'])
roles = dict(('name', role.name.lower()) for role in user.roles.filter(course_id=content['course_id']))
except user.DoesNotExist:
logging.error('User ID {0} in comment content {1} but not in our DB.'.format(content.get('user_id'), content.get('id')))
content_info = {
'displayed_title': content.get('highlighted_title') or content.get('title', ''),
'displayed_body': content.get('highlighted_body') or content.get('body', ''),
......@@ -247,20 +322,28 @@ def get_courseware_context(content, course):
if id in id_map:
location = id_map[id]["location"].url()
title = id_map[id]["title"]
content_info = { "courseware_location": location, "courseware_title": title}
(course_id, chapter, section, position) = path_to_location(modulestore(), course.id, location)
url = reverse('courseware_position', kwargs={"course_id":course_id,
"chapter":chapter,
"section":section,
"position":position})
content_info = {"courseware_url": url, "courseware_title": title}
return content_info
def safe_content(content):
fields = [
'id', 'title', 'body', 'course_id', 'anonymous', 'endorsed',
'parent_id', 'thread_id', 'votes', 'closed',
'created_at', 'updated_at', 'depth', 'type',
'commentable_id', 'comments_count', 'at_position_list',
'children', 'highlighted_title', 'highlighted_body',
'id', 'title', 'body', 'course_id', 'anonymous', 'anonymous_to_peers',
'endorsed', 'parent_id', 'thread_id', 'votes', 'closed', 'created_at',
'updated_at', 'depth', 'type', 'commentable_id', 'comments_count',
'at_position_list', 'children', 'highlighted_title', 'highlighted_body',
'courseware_title', 'courseware_url', 'tags'
]
if content.get('anonymous') is False:
if (content.get('anonymous') is False) and (content.get('anonymous_to_peers') is False):
fields += ['username', 'user_id']
if 'children' in content:
safe_children = [safe_content(child) for child in content['children']]
content['children'] = safe_children
return strip_none(extract(content, fields))
......@@ -70,6 +70,7 @@ SECRET_KEY = AUTH_TOKENS['SECRET_KEY']
AWS_ACCESS_KEY_ID = AUTH_TOKENS["AWS_ACCESS_KEY_ID"]
AWS_SECRET_ACCESS_KEY = AUTH_TOKENS["AWS_SECRET_ACCESS_KEY"]
AWS_STORAGE_BUCKET_NAME = 'edxuploads'
DATABASES = AUTH_TOKENS['DATABASES']
......
......@@ -448,7 +448,7 @@ main_vendor_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
# prep it for use in pipeline js
......
......@@ -7,15 +7,14 @@ import settings
class Comment(models.Model):
accessible_fields = [
'id', 'body', 'anonymous', 'course_id',
'endorsed', 'parent_id', 'thread_id',
'username', 'votes', 'user_id', 'closed',
'created_at', 'updated_at', 'depth',
'at_position_list', 'type', 'commentable_id',
'id', 'body', 'anonymous', 'anonymous_to_peers', 'course_id',
'endorsed', 'parent_id', 'thread_id', 'username', 'votes', 'user_id',
'closed', 'created_at', 'updated_at', 'depth', 'at_position_list',
'type', 'commentable_id',
]
updatable_fields = [
'body', 'anonymous', 'course_id', 'closed',
'body', 'anonymous', 'anonymous_to_peers', 'course_id', 'closed',
'user_id', 'endorsed',
]
......
......@@ -6,16 +6,14 @@ import settings
class Thread(models.Model):
accessible_fields = [
'id', 'title', 'body', 'anonymous',
'course_id', 'closed', 'tags', 'votes',
'commentable_id', 'username', 'user_id',
'created_at', 'updated_at', 'comments_count',
'at_position_list', 'children', 'type',
'highlighted_title', 'highlighted_body',
'id', 'title', 'body', 'anonymous', 'anonymous_to_peers', 'course_id',
'closed', 'tags', 'votes', 'commentable_id', 'username', 'user_id',
'created_at', 'updated_at', 'comments_count', 'at_position_list',
'children', 'type', 'highlighted_title', 'highlighted_body', 'endorsed'
]
updatable_fields = [
'title', 'body', 'anonymous', 'course_id',
'title', 'body', 'anonymous', 'anonymous_to_peers', 'course_id',
'closed', 'tags', 'user_id', 'commentable_id',
]
......@@ -32,7 +30,7 @@ class Thread(models.Model):
'course_id': query_params['course_id'],
'recursive': False}
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')
else:
url = cls.url(action='get_all', params=extract(params, 'commentable_id'))
......
......@@ -8,7 +8,8 @@ class User(models.Model):
accessible_fields = ['username', 'email', 'follower_ids', 'upvoted_ids', 'downvoted_ids',
'id', 'external_id', 'subscribed_user_ids', 'children', 'course_id',
'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']
......
......@@ -28,9 +28,9 @@ def perform_request(method, url, data_or_params=None, *args, **kwargs):
data_or_params['api_key'] = settings.API_KEY
try:
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:
response = requests.request(method, url, params=data_or_params)
response = requests.request(method, url, params=data_or_params, timeout=5)
except Exception as err:
log.exception("Trying to call {method} on {url} with params {params}".format(
method=method, url=url, params=data_or_params))
......
......@@ -45,6 +45,7 @@ $ ->
removeMath: (text) ->
text = text || ""
@math = []
start = end = last = null
braces = 0
......@@ -123,11 +124,9 @@ $ ->
Markdown.makeWmdEditor = (elem, appended_id, imageUploadUrl, postProcessor) ->
$elem = $(elem)
if not $elem.length
console.log "warning: elem for makeWmdEditor doesn't exist"
return
if not $elem.find(".wmd-panel").length
initialText = $elem.html()
$elem.empty()
......
if Backbone?
class @Content extends Backbone.Model
@contents: {}
@contentInfos: {}
template: -> DiscussionUtil.getTemplate('_content')
actions:
......@@ -16,9 +19,10 @@ if Backbone?
@urlMappers[name].apply(@)
can: (action) ->
DiscussionUtil.getContentInfo @id, action
(@get('ability') || {})[action]
updateInfo: (info) ->
if info
@set('ability', info.ability)
@set('voted', info.voted)
@set('subscribed', info.subscribed)
......@@ -32,12 +36,14 @@ if Backbone?
@get('children').push comment
model = new Comment $.extend {}, comment, { thread: @get('thread') }
@get('comments').add model
@trigger "comment:add"
model
removeComment: (comment) ->
thread = @get('thread')
comments_count = parseInt(thread.get('comments_count'))
thread.set('comments_count', comments_count - 1 - comment.getCommentsCount())
@trigger "comment:remove"
resetComments: (children) ->
@set 'children', []
......@@ -46,363 +52,32 @@ if Backbone?
@addComment comment, { silent: true }
initialize: ->
DiscussionUtil.addContent @id, @
Content.addContent @id, @
if Content.getInfo(@id)
@updateInfo(Content.getInfo(@id))
@set 'user_url', DiscussionUtil.urlFor('user_profile', @get('user_id'))
@resetComments(@get('children'))
remove: ->
class @ContentView extends Backbone.View
$: (selector) ->
@$local.find(selector)
partial:
endorsed: (endorsed) ->
if endorsed
@$el.addClass("endorsed")
else
@$el.removeClass("endorsed")
closed: (closed) -> # we should just re-render the whole thread, or update according to new abilities
if closed
@$el.addClass("closed")
@$(".admin-openclose").text "Re-open Thread"
if @get('type') == 'comment'
@get('thread').removeComment(@)
@get('thread').trigger "comment:remove", @
else
@$el.removeClass("closed")
@$(".admin-openclose").text "Close Thread"
@trigger "thread:remove", @
voted: (voted) ->
@$(".discussion-vote-up").removeClass("voted") if voted != "up"
@$(".discussion-vote-down").removeClass("voted") if voted != "down"
@$(".discussion-vote-#{voted}").addClass("voted") if voted in ["up", "down"]
@addContent: (id, content) -> @contents[id] = content
votes_point: (votes_point) ->
@$(".discussion-votes-point").html(votes_point)
comments_count: (comments_count) ->
@$(".comments-count").html(comments_count)
subscribed: (subscribed) ->
if subscribed
@$(".discussion-follow-thread").addClass("discussion-unfollow-thread").html("Unfollow")
else
@$(".discussion-follow-thread").removeClass("discussion-unfollow-thread").html("Follow")
ability: (ability) ->
for action, elemSelector of @model.actions
if not ability[action]
@$(elemSelector).parent().hide()
else
@$(elemSelector).parent().show()
@getContent: (id) -> @contents[id]
$discussionContent: ->
@_discussionContent ||= @$el.children(".discussion-content")
@getInfo: (id) ->
@contentInfos[id]
$showComments: ->
@_showComments ||= @$(".discussion-show-comments")
updateShowComments: ->
if @showed
@$showComments().html @$showComments().html().replace "Show", "Hide"
else
@$showComments().html @$showComments().html().replace "Hide", "Show"
retrieved: ->
@$showComments().hasClass("retrieved")
hideSingleThread: (event) ->
@$el.children(".comments").hide()
@showed = false
@updateShowComments()
showSingleThread: (event) ->
if @retrieved()
@$el.children(".comments").show()
@showed = true
@updateShowComments()
else
$elem = $.merge @$(".thread-title"), @$showComments()
url = @model.urlFor('retrieve')
DiscussionUtil.safeAjax
$elem: $elem
$loading: @$(".discussion-show-comments")
type: "GET"
url: url
success: (response, textStatus) =>
@showed = true
@updateShowComments()
@$showComments().addClass("retrieved")
@$el.children(".comments").replaceWith response.html
@model.resetComments response.content.children
@initCommentViews()
DiscussionUtil.bulkUpdateContentInfo response.annotated_content_info
toggleSingleThread: (event) ->
if @showed
@hideSingleThread(event)
else
@showSingleThread(event)
initCommentViews: ->
@$el.children(".comments").children(".comment").each (index, elem) =>
model = @model.get('comments').find $(elem).attr("_id")
if not model.view
commentView = new CommentView el: elem, model: model
reply: ->
if @model.get('type') == 'thread'
@showSingleThread()
$replyView = @$(".discussion-reply-new")
if $replyView.length
$replyView.show()
else
view = {}
view.id = @model.id
view.showWatchCheckbox = not @model.get('thread').get('subscribed')
html = Mustache.render DiscussionUtil.getTemplate('_reply'), view
@$discussionContent().append html
DiscussionUtil.makeWmdEditor @$el, $.proxy(@$, @), "reply-body"
@$(".discussion-submit-post").click $.proxy(@submitReply, @)
@$(".discussion-cancel-post").click $.proxy(@cancelReply, @)
@$(".discussion-reply").hide()
@$(".discussion-edit").hide()
submitReply: (event) ->
url = @model.urlFor('reply')
body = DiscussionUtil.getWmdContent @$el, $.proxy(@$, @), "reply-body"
anonymous = false || @$(".discussion-post-anonymously").is(":checked")
autowatch = false || @$(".discussion-auto-watch").is(":checked")
DiscussionUtil.safeAjax
$elem: $(event.target)
$loading: $(event.target) if event
url: url
type: "POST"
dataType: 'json'
data:
body: body
anonymous: anonymous
auto_subscribe: autowatch
error: DiscussionUtil.formErrorHandler @$(".discussion-errors")
success: (response, textStatus) =>
DiscussionUtil.clearFormErrors @$(".discussion-errors")
$comment = $(response.html)
@$el.children(".comments").prepend $comment
DiscussionUtil.setWmdContent @$el, $.proxy(@$, @), "reply-body", ""
comment = @model.addComment response.content
commentView = new CommentView el: $comment[0], model: comment
comment.updateInfo response.annotated_content_info
if autowatch
@model.get('thread').set('subscribed', true)
@cancelReply()
cancelReply: ->
$replyView = @$(".discussion-reply-new")
if $replyView.length
$replyView.hide()
@$(".discussion-reply").show()
@$(".discussion-edit").show()
unvote: (event) ->
url = @model.urlFor('unvote')
$elem = @$(".discussion-vote")
DiscussionUtil.safeAjax
$elem: $elem
url: url
type: "POST"
success: (response, textStatus) =>
@model.set('voted', '')
@model.set('votes_point', response.votes.point)
vote: (event, value) ->
url = @model.urlFor("#{value}vote")
$elem = @$(".discussion-vote")
DiscussionUtil.safeAjax
$elem: $elem
url: url
type: "POST"
success: (response, textStatus) =>
@model.set('voted', value)
@model.set('votes_point', response.votes.point)
toggleVote: (event) ->
$elem = $(event.target)
value = $elem.attr("value")
if @model.get("voted") == value
@unvote(event)
else
@vote(event, value)
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)
toggleFollow: (event) ->
$elem = $(event.target)
subscribed = @model.get('subscribed')
if subscribed
url = @model.urlFor('unfollow')
else
url = @model.urlFor('follow')
DiscussionUtil.safeAjax
$elem: $elem
url: url
type: "POST"
success: (response, textStatus) =>
@model.set('subscribed', not subscribed)
toggleClosed: (event) ->
$elem = $(event.target)
url = @model.urlFor('close')
closed = @model.get('closed')
data = { closed: not closed }
DiscussionUtil.safeAjax
$elem: $elem
url: url
type: "POST"
data: data
success: (response, textStatus) =>
@model.set('closed', not closed)
@model.set('ability', response.ability)
edit: (event) ->
@$(".discussion-content-wrapper").hide()
$editView = @$(".discussion-content-edit")
if $editView.length
$editView.show()
else
view = {}
view.id = @model.id
if @model.get('type') == 'thread'
view.title = @model.get('title')
view.body = @model.get('body')
view.tags = @model.get('tags')
else
view.body = @model.get('body')
@$discussionContent().append Mustache.render DiscussionUtil.getTemplate("_edit_#{@model.get('type')}"), view
DiscussionUtil.makeWmdEditor @$el, $.proxy(@$, @), "#{@model.get('type')}-body-edit"
@$(".thread-tags-edit").tagsInput DiscussionUtil.tagsInputOptions()
@$(".discussion-submit-update").unbind("click").click $.proxy(@submitEdit, @)
@$(".discussion-cancel-update").unbind("click").click $.proxy(@cancelEdit, @)
submitEdit: (event) ->
url = @model.urlFor('update')
data = {}
if @model.get('type') == 'thread'
data.title = @$(".thread-title-edit").val()
data.body = DiscussionUtil.getWmdContent @$el, $.proxy(@$, @), "thread-body-edit"
data.tags = @$(".thread-tags-edit").val()
else
data.body = DiscussionUtil.getWmdContent @$el, $.proxy(@$, @), "comment-body-edit"
DiscussionUtil.safeAjax
$elem: $(event.target)
$loading: $(event.target) if event
url: url
type: "POST"
dataType: 'json'
data: data
error: DiscussionUtil.formErrorHandler @$(".discussion-update-errors")
success: (response, textStatus) =>
DiscussionUtil.clearFormErrors @$(".discussion-update-errors")
@$discussionContent().replaceWith(response.html)
if @model.get('type') == 'thread'
@model = new Thread response.content
else
@model = new Comment $.extend {}, response.content, { thread: @model.get('thread') }
@reconstruct()
@model.updateInfo response.annotated_content_info, { forceUpdate: true }
cancelEdit: (event) ->
@$(".discussion-content-edit").hide()
@$(".discussion-content-wrapper").show()
delete: (event) ->
url = @model.urlFor('delete')
if @model.get('type') == 'thread'
c = confirm "Are you sure to delete thread \"#{@model.get('title')}\"?"
else
c = confirm "Are you sure to delete this comment? "
if not c
return
$elem = $(event.target)
DiscussionUtil.safeAjax
$elem: $elem
url: url
type: "POST"
success: (response, textStatus) =>
@$el.remove()
if @model.get('type') == 'comment'
@model.get('thread').removeComment(@model)
events:
"click .discussion-follow-thread": "toggleFollow"
"click .thread-title": "toggleSingleThread"
"click .discussion-show-comments": "toggleSingleThread"
"click .discussion-reply-thread": "reply"
"click .discussion-reply-comment": "reply"
"click .discussion-cancel-reply": "cancelReply"
"click .discussion-vote-up": "toggleVote"
"click .discussion-vote-down": "toggleVote"
"click .admin-endorse": "toggleEndorse"
"click .admin-openclose": "toggleClosed"
"click .admin-edit": "edit"
"click .admin-delete": "delete"
initLocal: ->
@$local = @$el.children(".local")
@$delegateElement = @$local
initTitle: ->
$contentTitle = @$(".thread-title")
if $contentTitle.length
$contentTitle.html DiscussionUtil.unescapeHighlightTag DiscussionUtil.stripLatexHighlight $contentTitle.html()
initBody: ->
$contentBody = @$(".content-body")
$contentBody.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight $contentBody.html()
MathJax.Hub.Queue ["Typeset", MathJax.Hub, $contentBody.attr("id")]
initTimeago: ->
@$("span.timeago").each (index, element) ->
elem = $(element)
elem.html("posted on #{$.timeago.parse(elem.html()).toLocaleString()}")
@$("span.timeago").timeago()
renderPartial: ->
for attr, value of @model.changedAttributes()
if @partial[attr]
@partial[attr].apply(@, [value])
initBindings: ->
@model.view = @
@model.bind('change', @renderPartial, @)
initialize: ->
@initBindings()
@initLocal()
@initTimeago()
@initTitle()
@initBody()
@initCommentViews()
reconstruct: ->
@initBindings()
@initLocal()
@initTimeago()
@initTitle()
@initBody()
@delegateEvents()
@loadContentInfos: (infos) ->
for id, info of infos
if @getContent(id)
@getContent(id).updateInfo(info)
$.extend @contentInfos, infos
class @Thread extends @Content
urlMappers:
......@@ -421,7 +96,38 @@ if Backbone?
@set('thread', @)
super()
class @ThreadView extends @ContentView
comment: ->
@set("comments_count", parseInt(@get("comments_count")) + 1)
follow: ->
@set('subscribed', true)
unfollow: ->
@set('subscribed', false)
vote: ->
@get("votes")["up_count"] = parseInt(@get("votes")["up_count"]) + 1
@trigger "change", @
unvote: ->
@get("votes")["up_count"] = parseInt(@get("votes")["up_count"]) - 1
@trigger "change", @
display_body: ->
if @has("highlighted_body")
String(@get("highlighted_body")).replace(/<highlight>/g, '<mark>').replace(/<\/highlight>/g, '</mark>')
else
@get("body")
display_title: ->
if @has("highlighted_title")
String(@get("highlighted_title")).replace(/<highlight>/g, '<mark>').replace(/<\/highlight>/g, '</mark>')
else
@get("title")
toJSON: ->
json_attributes = _.clone(@attributes)
_.extend(json_attributes, { title: @display_title(), body: @display_body() })
class @Comment extends @Content
urlMappers:
......@@ -439,8 +145,6 @@ if Backbone?
count += comment.getCommentsCount() + 1
count
class @CommentView extends @ContentView
class @Comments extends Backbone.Collection
model: Comment
......
......@@ -2,185 +2,67 @@ if Backbone?
class @Discussion extends Backbone.Collection
model: Thread
initialize: ->
DiscussionUtil.addDiscussion @id, @
initialize: (models, options={})->
@pages = options['pages'] || 1
@current_page = 1
@bind "add", (item) =>
item.discussion = @
@comparator = @sortByDateRecentFirst
@on "thread:remove", (thread) =>
@remove(thread)
find: (id) ->
_.first @where(id: id)
hasMorePages: ->
@current_page < @pages
addThread: (thread, options) ->
# TODO: Check for existing thread with same ID in a faster way
if not @find(thread.id)
options ||= {}
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() }
retrieveAnotherPage: (search_text="", commentable_ids="", sort_key="")->
# TODO: I really feel that this belongs in DiscussionThreadListView
@current_page += 1
url = DiscussionUtil.urlFor 'threads'
data = { page: @current_page }
if search_text
data['text'] = search_text
if sort_key
data['sort_key'] = sort_key
if commentable_ids
data['commentable_ids'] = commentable_ids
DiscussionUtil.safeAjax
$elem: $elem
$elem: @$el
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, @)
@$(".hide-similar-posts").click =>
@$(".new-post-similar-posts-wrapper").hide()
@$(".discussion-submit-post").click $.proxy(@submitNewPost, @)
@$(".discussion-cancel-post").click $.proxy(@cancelNewPost, @)
@$el.children(".blank").hide()
@$(".new-post-form").show()
submitNewPost: (event) ->
title = @$(".new-post-title").val()
body = DiscussionUtil.getWmdContent @$el, $.proxy(@$, @), "new-post-body"
tags = @$(".new-post-tags").val()
anonymous = false || @$(".discussion-post-anonymously").is(":checked")
autowatch = false || @$(".discussion-auto-watch").is(":checked")
url = DiscussionUtil.urlFor('create_thread', @model.id)
DiscussionUtil.safeAjax
$elem: $(event.target)
$loading: $(event.target) if event
url: url
type: "POST"
dataType: 'json'
data:
title: title
body: body
tags: tags
anonymous: anonymous
auto_subscribe: autowatch
error: DiscussionUtil.formErrorHandler(@$(".new-post-form-errors"))
success: (response, textStatus) =>
DiscussionUtil.clearFormErrors(@$(".new-post-form-errors"))
$thread = $(response.html)
@$el.children(".threads").prepend($thread)
@$el.children(".blank").remove()
@$(".new-post-similar-posts").empty()
@$(".new-post-similar-posts-wrapper").hide()
@$(".new-post-title").val("").attr("prev-text", "")
DiscussionUtil.setWmdContent @$el, $.proxy(@$, @), "new-post-body", ""
@$(".new-post-tags").val("")
@$(".new-post-tags").importTags("")
thread = @model.addThread response.content
threadView = new ThreadView el: $thread[0], model: thread
thread.updateInfo response.annotated_content_info
@cancelNewPost()
cancelNewPost: (event) ->
if @$el.hasClass("inline-discussion")
@$(".new-post-form").addClass("collapsed")
else if @$el.hasClass("forum-discussion")
@$(".new-post-form").hide()
@$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"
models = @models
new_threads = [new Thread(data) for data in response.discussion_data][0]
new_collection = _.union(models, new_threads)
@reset new_collection
sortByDate: (thread) ->
thread.get("created_at")
sortByDateRecentFirst: (thread) ->
-(new Date(thread.get("created_at")).getTime())
#return String.fromCharCode.apply(String,
# _.map(thread.get("created_at").split(""),
# ((c) -> return 0xffff - c.charChodeAt()))
#)
sortByVotes: (thread1, thread2) ->
thread1_count = parseInt(thread1.get("votes")['up_count'])
thread2_count = parseInt(thread2.get("votes")['up_count'])
thread2_count - thread1_count
sortByComments: (thread1, thread2) ->
thread1_count = parseInt(thread1.get("comments_count"))
thread2_count = parseInt(thread2.get("comments_count"))
thread2_count - thread1_count
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)
$ ->
window.$$contents = {}
window.$$discussions = {}
if Backbone?
DiscussionApp =
start: (elem)->
# TODO: Perhaps eliminate usage of global variables when possible
DiscussionUtil.loadRolesFromContainer()
element = $(elem)
window.$$course_id = element.data("course-id")
user_info = element.data("user-info")
threads = element.data("threads")
thread_pages = element.data("thread-pages")
content_info = element.data("content-info")
window.user = new DiscussionUser(user_info)
Content.loadContentInfos(content_info)
discussion = new Discussion(threads, pages: thread_pages)
new DiscussionRouter({discussion: discussion})
Backbone.history.start({pushState: true, root: "/courses/#{$$course_id}/discussion/forum/"})
DiscussionProfileApp =
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) ->
discussionData = DiscussionUtil.getDiscussionData($(elem).attr("_id"))
discussion = new Discussion()
discussion.reset(discussionData, {silent: false})
view = new DiscussionView(el: elem, model: discussion)
if window.$$annotated_content_info?
DiscussionUtil.bulkUpdateContentInfo(window.$$annotated_content_info)
$userProfile = $(".discussion-sidebar>.user-profile")
if $userProfile.length
console.log "initialize user profile"
view = new DiscussionUserProfileView(el: $userProfile[0])
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?
class @DiscussionUserProfileView extends Backbone.View
toggleModeratorStatus: (event) ->
confirmValue = confirm("Are you sure?")
if not confirmValue then return
......
$ ->
if !window.$$contents
window.$$contents = {}
$.fn.extend
loading: ->
@$_loading = $("<span class='discussion-loading'></span>")
@$_loading = $("<div class='loading-animation'></div>")
$(this).after(@$_loading)
loaded: ->
@$_loading.remove()
......@@ -13,20 +15,19 @@ class @DiscussionUtil
@getTemplate: (id) ->
$("script##{id}").html()
@getDiscussionData: (id) ->
return $$discussion_data[id]
@loadRoles: (roles)->
@roleIds = roles
@addContent: (id, content) -> window.$$contents[id] = content
@loadRolesFromContainer: ->
@loadRoles($("#discussion-container").data("roles"))
@getContent: (id) -> window.$$contents[id]
@addDiscussion: (id, discussion) -> window.$$discussions[id] = discussion
@getDiscussion: (id) -> window.$$discussions[id]
@isStaff: (user_id) ->
staff = _.union(@roleIds['Staff'], @roleIds['Moderator'], @roleIds['Administrator'])
_.include(staff, parseInt(user_id))
@bulkUpdateContentInfo: (infos) ->
for id, info of infos
@getContent(id).updateInfo(info)
Content.getContent(id).updateInfo(info)
@generateDiscussionLink: (cls, txt, handler) ->
$("<a>").addClass("discussion-link")
......@@ -64,26 +65,32 @@ class @DiscussionUtil
openclose_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/close"
permanent_link_thread : "/courses/#{$$course_id}/discussion/forum/#{param}/threads/#{param1}"
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]
@safeAjax: (params) ->
$elem = params.$elem
if $elem.attr("disabled")
if $elem and $elem.attr("disabled")
return
params["url"] = URI(params["url"]).addSearch ajax: 1
params["beforeSend"] = ->
if $elem
$elem.attr("disabled", "disabled")
if params["$loading"]
if params["loadingCallback"]?
params["loadingCallback"].apply(params["$loading"])
else
params["$loading"].loading()
$.ajax(params).always ->
request = $.ajax(params).always ->
if $elem
$elem.removeAttr("disabled")
if params["$loading"]
if params["loadedCallback"]?
params["loadedCallback"].apply(params["$loading"])
else
params["$loading"].loaded()
return request
@get: ($elem, url, data, success) ->
@safeAjax
......@@ -108,6 +115,9 @@ class @DiscussionUtil
[event, selector] = eventSelector.split(' ')
$local(selector).unbind(event)[event] handler
@processTag: (text) ->
text.toLowerCase()
@tagsInputOptions: ->
autocomplete_url: @urlFor('tags_autocomplete')
autocomplete:
......@@ -117,6 +127,7 @@ class @DiscussionUtil
width: '100%'
defaultText: "Tag your post: press enter after each tag"
removeWithBackspace: true
preprocessTag: @processTag
@formErrorHandler: (errorsField) ->
(xhr, textStatus, error) ->
......@@ -124,7 +135,7 @@ class @DiscussionUtil
if response.errors? and response.errors.length > 0
errorsField.empty()
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) ->
errorsField.empty()
......@@ -144,21 +155,26 @@ class @DiscussionUtil
@makeWmdEditor: ($content, $local, cls_identifier) ->
elem = $local(".#{cls_identifier}")
id = $content.attr("_id")
placeholder = elem.data('placeholder')
id = elem.data("id")
appended_id = "-#{cls_identifier}-#{id}"
imageUploadUrl = @urlFor('upload')
_processor = (_this) ->
(text) -> _this.postMathJaxProcessor(text)
editor = Markdown.makeWmdEditor elem, appended_id, imageUploadUrl, _processor(@)
@wmdEditors["#{cls_identifier}-#{id}"] = editor
if placeholder?
elem.find("#wmd-input#{appended_id}").attr('placeholder', placeholder)
editor
@getWmdEditor: ($content, $local, cls_identifier) ->
id = $content.attr("_id")
elem = $local(".#{cls_identifier}")
id = elem.data("id")
@wmdEditors["#{cls_identifier}-#{id}"]
@getWmdInput: ($content, $local, cls_identifier) ->
id = $content.attr("_id")
elem = $local(".#{cls_identifier}")
id = elem.data("id")
$local("#wmd-input-#{cls_identifier}-#{id}")
@getWmdContent: ($content, $local, cls_identifier) ->
......@@ -264,5 +280,17 @@ class @DiscussionUtil
@processEachMathAndCode text, @stripHighlight
@markdownWithHighlight: (text) ->
text = text.replace(/^\&gt\;/gm, ">")
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 @DiscussionThreadListView extends Backbone.View
events:
"click .search": "showSearch"
"click .browse": "toggleTopicDrop"
"keydown .post-search-field": "performSearch"
"click .sort-bar a": "sortThreads"
"click .browse-topic-drop-menu": "filterTopic"
"click .browse-topic-drop-search-input": "ignoreClick"
"click .post-list .list-item a": "threadSelected"
"click .post-list .more-pages a": "loadMorePages"
initialize: ->
@displayedCollection = new Discussion(@collection.models, pages: @collection.pages)
@collection.on "change", @reloadDisplayedCollection
@sortBy = "date"
@discussionIds=""
@collection.on "reset", (discussion) =>
board = $(".current-board").html()
@displayedCollection.current_page = discussion.current_page
@displayedCollection.pages = discussion.pages
@displayedCollection.reset discussion.models
# TODO: filter correctly
# target = _.filter($("a.topic:contains('#{board}')"), (el) -> el.innerText == "General" || el.innerHTML == "General")
# if target.length > 0
# @filterTopic($.Event("filter", {'target': target[0]}))
@collection.on "add", @addAndSelectThread
@sidebar_padding = 10
@sidebar_header_height = 87
@boardName
@template = _.template($("#thread-list-template").html())
@current_search = ""
reloadDisplayedCollection: (thread) =>
thread_id = thread.get('id')
content = @renderThread(thread)
current_el = @$("a[data-id=#{thread_id}]")
active = current_el.hasClass("active")
current_el.replaceWith(content)
if active
@setActiveThread(thread_id)
#TODO fix this entire chain of events
addAndSelectThread: (thread) =>
commentable_id = thread.get("commentable_id")
commentable = @$(".board-name[data-discussion_id]").filter(-> $(this).data("discussion_id").id == commentable_id)
@setTopicHack(commentable)
@retrieveDiscussion commentable_id, =>
@trigger "thread:created", thread.get('id')
updateSidebar: =>
scrollTop = $(window).scrollTop();
windowHeight = $(window).height();
discussionBody = $(".discussion-article")
discussionsBodyTop = if discussionBody[0] then discussionBody.offset().top
discussionsBodyBottom = discussionsBodyTop + discussionBody.outerHeight()
sidebar = $(".sidebar")
if scrollTop > discussionsBodyTop - @sidebar_padding
sidebar.addClass('fixed');
sidebar.css('top', @sidebar_padding);
else
sidebar.removeClass('fixed');
sidebar.css('top', '0');
sidebarWidth = .31 * $(".discussion-body").width();
sidebar.css('width', sidebarWidth + 'px');
sidebarHeight = windowHeight - Math.max(discussionsBodyTop - scrollTop, @sidebar_padding)
topOffset = scrollTop + windowHeight
discussionBottomOffset = discussionsBodyBottom + @sidebar_padding
amount = Math.max(topOffset - discussionBottomOffset, 0)
sidebarHeight = sidebarHeight - @sidebar_padding - amount
sidebarHeight = Math.min(sidebarHeight + 1, discussionBody.outerHeight())
sidebar.css 'height', sidebarHeight
postListWrapper = @$('.post-list-wrapper')
postListWrapper.css('height', (sidebarHeight - @sidebar_header_height - 4) + 'px')
# 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()
render: ->
@timer = 0
@$el.html(@template())
$(window).bind "scroll", @updateSidebar
$(window).bind "resize", @updateSidebar
@displayedCollection.on "reset", @renderThreads
@displayedCollection.on "thread:remove", @renderThreads
@renderThreads()
@
renderThreads: =>
@$(".post-list").html("")
rendered = $("<div></div>")
for thread in @displayedCollection.models
content = @renderThread(thread)
rendered.append content
content.wrap("<li class='list-item' data-id='\"#{thread.get('id')}\"' />")
@$(".post-list").html(rendered.html())
@renderMorePages()
@updateSidebar()
@trigger "threads:rendered"
renderMorePages: ->
if @displayedCollection.hasMorePages()
@$(".post-list").append("<li class='more-pages'><a href='#'>Load more</a></li>")
loadMorePages: (event) ->
event.preventDefault()
@$(".more-pages").html('<div class="loading-animation"></div>')
@$(".more-pages").addClass("loading")
@collection.retrieveAnotherPage(@current_search, @discussionIds, @sortBy)
renderThread: (thread) =>
content = $(_.template($("#thread-list-item-template").html())(thread.toJSON()))
if thread.get('subscribed')
content.addClass("followed")
if thread.get('endorsed')
content.addClass("resolved")
@highlight(content)
highlight: (el) ->
el.html(el.html().replace(/&lt;mark&gt;/g, "<mark>").replace(/&lt;\/mark&gt;/g, "</mark>"))
renderThreadListItem: (thread) =>
view = new ThreadListItemView(model: thread)
view.on "thread:selected", @threadSelected
view.on "thread:removed", @threadRemoved
view.render()
@$(".post-list").append(view.el)
threadSelected: (e) =>
thread_id = $(e.target).closest("a").data("id")
@setActiveThread(thread_id)
@trigger("thread:selected", thread_id)
false
threadRemoved: (thread_id) =>
@trigger("thread:removed", thread_id)
setActiveThread: (thread_id) ->
@$(".post-list a[data-id!='#{thread_id}']").removeClass("active")
@$(".post-list a[data-id='#{thread_id}']").addClass("active")
showSearch: ->
@$(".browse").removeClass('is-dropped')
@hideTopicDrop()
@$(".search").addClass('is-open')
@$(".browse").removeClass('is-open')
setTimeout (-> @$(".post-search-field").focus()), 200
toggleTopicDrop: (event) =>
event.preventDefault()
event.stopPropagation()
if @current_search != ""
@clearSearch()
@$(".search").removeClass('is-open')
@$(".browse").addClass('is-open')
@$(".browse").toggleClass('is-dropped')
if @$(".browse").hasClass('is-dropped')
@$(".browse-topic-drop-menu-wrapper").show()
$(".browse-topic-drop-search-input").focus()
$("body").bind "click", @toggleTopicDrop
$("body").bind "keydown", @setActiveItem
else
@hideTopicDrop()
hideTopicDrop: ->
@$(".browse-topic-drop-menu-wrapper").hide()
$("body").unbind "click", @toggleTopicDrop
$("body").unbind "keydown", @setActiveItem
# TODO get rid of this asap
setTopicHack: (boardNameContainer) ->
item = $(boardNameContainer).closest('a')
boardName = item.find(".board-name").html()
_.each item.parents('ul').not('.browse-topic-drop-menu'), (parent) ->
boardName = $(parent).siblings('a').find('.board-name').html() + ' / ' + boardName
@$(".current-board").html(@fitName(boardName))
setTopic: (event) ->
item = $(event.target).closest('a')
boardName = item.find(".board-name").html()
_.each item.parents('ul').not('.browse-topic-drop-menu'), (parent) ->
boardName = $(parent).siblings('a').find('.board-name').html() + ' / ' + boardName
@$(".current-board").html(@fitName(boardName))
setSelectedTopic: (name) ->
@$(".current-board").html(@fitName(name))
getNameWidth: (name) ->
test = $("<div>")
test.css
"font-size": @$(".current-board").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) ->
@maxNameWidth = (@$el.width() * .8) - 50
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
filterTopic: (event) ->
if @current_search != ""
@setTopic(event)
@clearSearch @filterTopic, event
else
@setTopic(event) # just sets the title for the dropdown
item = $(event.target).closest('li')
if item.find("span.board-name").data("discussion_id") == "#all"
@discussionIds = ""
@clearSearch()
else
discussionIds = _.map item.find(".board-name[data-discussion_id]"), (board) -> $(board).data("discussion_id").id
@retrieveDiscussions(discussionIds)
retrieveDiscussion: (discussion_id, callback=null) ->
url = DiscussionUtil.urlFor("retrieve_discussion", discussion_id)
DiscussionUtil.safeAjax
url: url
type: "GET"
success: (response, textStatus) =>
@collection.current_page = response.page
@collection.pages = response.num_pages
@collection.reset(response.discussion_data)
Content.loadContentInfos(response.content_info)
@displayedCollection.reset(@collection.models)
if callback?
callback()
retrieveDiscussions: (discussion_ids) ->
@discussionIds = discussion_ids.join(',')
url = DiscussionUtil.urlFor("search")
DiscussionUtil.safeAjax
data: { 'commentable_ids': @discussionIds }
url: url
type: "GET"
success: (response, textStatus) =>
@collection.current_page = response.page
@collection.pages = response.num_pages
@collection.reset(response.discussion_data)
Content.loadContentInfos(response.content_info)
@displayedCollection.reset(@collection.models)
sortThreads: (event) ->
@$(".sort-bar a").removeClass("active")
$(event.target).addClass("active")
@sortBy = $(event.target).data("sort")
if @sortBy == "date"
@displayedCollection.comparator = @displayedCollection.sortByDateRecentFirst
else if @sortBy == "votes"
@displayedCollection.comparator = @displayedCollection.sortByVotes
else if @sortBy == "comments"
@displayedCollection.comparator = @displayedCollection.sortByComments
@displayedCollection.sort()
performSearch: (event) ->
if event.which == 13
event.preventDefault()
text = @$(".post-search-field").val()
@searchFor(text)
setAndSearchFor: (text) ->
@showSearch()
@$(".post-search-field").val(text)
@searchFor(text)
searchFor: (text, callback, value) ->
@current_search = text
url = DiscussionUtil.urlFor("search")
DiscussionUtil.safeAjax
$elem: @$(".post-search-field")
data: { text: text }
url: url
type: "GET"
$loading: $
loadingCallback: =>
@$(".post-list").html('<li class="loading"><div class="loading-animation"></div></li>')
loadedCallback: =>
if callback
callback.apply @, [value]
success: (response, textStatus) =>
if textStatus == 'success'
# TODO: Augment existing collection?
@collection.reset(response.discussion_data)
Content.loadContentInfos(response.content_info)
@collection.current_page = response.page
@collection.pages = response.num_pages
# TODO: Perhaps reload user info so that votes can be updated.
# In the future we might not load all of a user's votes at once
# so this would probably be necessary anyway
@displayedCollection.reset(@collection.models)
clearSearch: (callback, value) ->
@$(".post-search-field").val("")
@searchFor("", callback, value)
setActiveItem: (event) ->
if event.which == 13
$(".browse-topic-drop-menu-wrapper .focused").click()
return
if event.which != 40 && event.which != 38
return
event.preventDefault()
items = $.makeArray($(".browse-topic-drop-menu-wrapper a").not(".hidden"))
index = items.indexOf($('.browse-topic-drop-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)
$(".browse-topic-drop-menu-wrapper .focused").removeClass("focused")
$(items[index]).addClass("focused")
itemTop = $(items[index]).parent().offset().top
scrollTop = $(".browse-topic-drop-menu").scrollTop()
itemFromTop = $(".browse-topic-drop-menu").offset().top - itemTop
scrollTarget = Math.min(scrollTop - itemFromTop, scrollTop)
scrollTarget = Math.max(scrollTop - itemFromTop - $(".browse-topic-drop-menu").height() + $(items[index]).height(), scrollTarget)
$(".browse-topic-drop-menu").scrollTop(scrollTarget)
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; }');
}
......@@ -85,6 +85,11 @@
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) {
var skipTag = $(this).tagExist(value);
if(skipTag == true) {
......@@ -100,7 +105,7 @@
$('<span>').text(value).append('&nbsp;&nbsp;'),
$('<a>', {
href : '#',
title : 'Removing tag',
title : 'Remove tag',
text : 'x'
}).click(function () {
return $('#' + id).removeTag(escape(value));
......@@ -211,11 +216,12 @@
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]['onAddTag'] = settings.onAddTag;
tags_callbacks[id]['onRemoveTag'] = settings.onRemoveTag;
tags_callbacks[id]['onChange'] = settings.onChange;
tags_callbacks[id]['preprocessTag'] = settings.preprocessTag;
}
var markup = '<div id="'+id+'_tagsinput" class="tagsinput"><div id="'+id+'_addTag">';
......
@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;
}
}
}
/*** Variables ***/
@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 #aaa;
@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);
}
}
@mixin discussion-wmd-input {
width: 100%;
height: 240px;
margin-top: 0;
padding: 10px;
@include box-sizing(border-box);
border: 1px solid #aaa;
border-radius: 3px 3px 0 0;
background: #fff;
font-family: 'Monaco', monospace;
font-size: 13px;
line-height: 1.6;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15) inset;
}
@mixin discussion-wmd-preview {
width: 100%;
min-height: 40px;
padding: 25px 20px 10px 20px;
@include box-sizing(border-box);
border: 1px solid #aaa;
border-top: none;
border-radius: 0 0 3px 3px;
background: #eee;
color: #333;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15) inset;
}
@-webkit-keyframes fadeIn {
0% { opacity: 0; }
100% { opacity: 1; }
}
body.discussion {
.new-post-form-errors {
display: none;
background: $error-red;
padding: 0;
border: 1px solid #333;
list-style: none;
color: #fff;
line-height: 1.6;
border-radius: 3px;
@include box-shadow(0 1px 2px rgba(0, 0, 0, 0.3) inset, 0 1px 0 rgba(255, 255, 255, .2));
li {
padding: 10px 20px 12px 45px;
border-bottom: 1px solid #dc4949;
background: url(../images/white-error-icon.png) no-repeat 15px 14px;
&:last-child {
border-bottom: none;
}
}
}
.course-tabs .right {
float: right;
.new-post-btn {
@include blue-button;
font-size: 13px;
margin-right: 4px;
}
.new-post-icon {
display: block;
float: left;
width: 16px;
height: 17px;
margin: 8px 7px 0 0;
background: url(../images/new-post-icon.png) no-repeat;
}
}
.new-post-article {
display: none;
margin-top: 20px;
.inner-wrapper {
max-width: 1180px;
min-width: 760px;
margin: auto;
}
.left-column {
float: left;
width: 32%;
padding: 40px;
@include box-sizing(border-box);
label {
font-size: 22px;
font-weight: 700;
color: #fff;
text-shadow: none;
}
.form-topic-drop {
position: relative;
ul {
list-style: none;
margin: 0;
padding: 0;
}
}
.topic_dropdown_button {
position: relative;
z-index: 1000;
@include white-button;
height: 40px;
margin-top: 15px;
border-color: #444;
line-height: 36px;
.drop-arrow {
float: right;
color: #999;
line-height: 36px;
}
}
.topic_menu_wrapper {
display: none;
position: absolute;
top: 40px;
left: 0;
z-index: 9999;
width: 100%;
@include box-sizing(border-box);
background: #737373;
border: 1px solid #333;
box-shadow: 0 2px 50px rgba(0, 0, 0, .4);
}
.topic_menu {
max-height: 400px;
overflow-y: scroll;
a {
display: block;
padding: 10px 15px;
border-top: 1px solid #5f5f5f;
font-size: 14px;
font-weight: 700;
line-height: 18px;
color: #eee;
@include transition(none);
&:hover,
&.focused {
background-color: #666;
}
}
li li {
a {
padding-left: 39px;
background: url(../images/nested-icon.png) no-repeat 17px 10px;
}
}
li li li {
a {
padding-left: 63px;
background: url(../images/nested-icon.png) no-repeat 41px 10px;
}
}
}
.topic_menu_search {
padding: 10px;
border-bottom: 1px solid black;
}
.form-topic-drop-search-input {
width: 100%;
height: 30px;
padding: 0 15px;
@include box-sizing(border-box);
border-radius: 30px;
border: 1px solid #333;
box-shadow: 0 1px 3px rgba(0, 0, 0, .25) inset;
background: -webkit-linear-gradient(top, #eee, #fff);
font-size: 11px;
line-height: 16px;
color: #333;
outline: 0;
}
}
.right-column {
float: left;
width: 68%;
padding: 40px;
@include box-sizing(border-box);
}
.wmd-button {
background: none;
}
.wmd-button span {
background: url(../images/new-post-icons-full.png) no-repeat;
}
}
.edit-post-form {
width: 100%;
margin-bottom: 40px;
@include clearfix;
@include box-sizing(border-box);
h1 {
font-size: 20px;
}
.form-row {
margin-top: 20px;
}
.post-cancel {
@include white-button;
float: left;
margin: 10px 0 0 15px;
}
.post-update {
@include blue-button;
float: left;
height: 37px;
margin-top: 10px;
padding-bottom: 2px;
&:hover {
border-color: #222;
}
}
.edit-post-title, .edit-post-tags {
width: 100%;
height: 40px;
padding: 0 10px;
@include box-sizing(border-box);
border-radius: 3px;
border: 1px solid #aaa;
font-size: 16px;
font-family: $sans-serif;
color: #333;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15) inset;
}
.tagsinput {
padding: 10px;
@include box-sizing(border-box);
border: 1px solid #aaa;
border-radius: 3px;
background: #fff;
font-family: 'Monaco', monospace;
font-size: 13px;
line-height: 1.6;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3) inset;
span.tag {
margin-bottom: 0;
}
}
}
.new-post-form {
width: 100%;
margin-bottom: 20px;
border-radius: 3px;
background: rgba(0, 0, 0, .55);
color: #fff;
box-shadow: 0 1px 2px rgba(0, 0, 0, .5) inset, 0 1px 0 rgba(255, 255, 255, .5);
@include clearfix;
.form-row {
margin-bottom: 20px;
}
.new-post-body .wmd-input {
@include discussion-wmd-input;
position: relative;
width: 100%;
height: 200px;
z-index: 1;
padding: 10px;
@include box-sizing(border-box);
border: 1px solid #333;
border-radius: 3px 3px 0 0;
background: #fff;
font-family: 'Monaco', monospace;
font-size: 13px;
line-height: 1.6;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3) inset;
}
.tagsinput {
padding: 10px;
@include box-sizing(border-box);
border: 1px solid #333;
border-radius: 3px;
background: #fff;
font-family: 'Monaco', monospace;
font-size: 13px;
line-height: 1.6;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3) inset;
span.tag {
margin-bottom: 0;
}
}
.new-post-body .wmd-preview {
@include discussion-wmd-preview;
position: relative;
width: 100%;
//height: 50px;
margin-top: -1px;
padding: 25px 20px 10px 20px;
@include box-sizing(border-box);
border: 1px solid #333;
border-radius: 0 0 3px 3px;
background: #e6e6e6;
color: #333;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3) inset;
}
.new-post-preview-label {
position: absolute;
top: 4px;
left: 4px;
font-size: 11px;
color: #aaa;
text-transform: uppercase;
}
.new-post-title,
.new-post-tags {
width: 100%;
height: 40px;
padding: 0 10px;
@include box-sizing(border-box);
border-radius: 3px;
border: 1px solid #333;
font-size: 16px;
font-family: 'Open Sans', sans-serif;
color: #333;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3) inset;
}
.new-post-title {
font-weight: 700;
}
.submit {
@include blue-button;
float: left;
height: 37px;
margin-top: 10px;
padding-bottom: 2px;
border-color: #333;
&:hover {
border-color: #222;
}
}
.new-post-cancel {
@include white-button;
border-color: #444;
float: left;
margin: 10px 0 0 15px;
}
.options {
margin-top: 40px;
label {
display: inline;
margin-left: 8px;
font-size: 15px;
color: #fff;
text-shadow: none;
}
}
}
.thread-tags {
margin-top: 20px;
}
.thread-tag {
margin-right: 5px;
padding: 3px 10px 6px;
border-radius: 3px;
color: #333;
background: #c5eeff;
border: 1px solid #90c4d7;
font-size: 13px;
}
.thread-title {
display: block;
margin-bottom: 20px;
font-size: 21px;
color: #333;
font-weight: 700;
}
section.user-profile {
@extend .sidebar;
display: table-cell;
@include border-radius(3px 0 0 3px);
border-right: 1px solid #ddd;
@include box-shadow(none);
background-color: $sidebar-color;
.user-profile {
padding: 32px 36px;
}
.sidebar-username {
font-size: 18px;
font-weight: 700;
}
.sidebar-user-roles {
margin-top: 6px;
font-size: 13px;
font-style: italic;
}
.sidebar-threads-count {
margin-top: 14px;
}
.sidebar-threads-count span,
.sidebar-comments-count span {
font-weight: 700;
}
.sidebar-toggle-moderator-button {
@include blue-button;
text-align: center;
margin-top: 20px;
}
}
.wmd-panel {
width: 100%;
min-width: 500px;
}
.wmd-button-bar {
width: 100%;
background-color: Silver;
}
.wmd-input {
height: 150px;
width: 100%;
background-color: #e9e9e9;
border: 1px solid #c8c8c8;
font-family: Monaco, 'Lucida Console', monospace;
font-style: normal;
font-size: 0.8em;
line-height: 1.6em;
@include border-radius(3px 3px 0 0);
&::-webkit-input-placeholder {
color: #888;
}
}
.wmd-preview {
position: relative;
font-family: $sans-serif;
padding: 25px 20px 10px 20px;
margin-bottom: 5px;
@include box-sizing(border-box);
border: 1px solid #c8c8c8;
border-top-width: 0;
@include border-radius(0 0 3px 3px);
overflow: hidden;
@include transition(all, .2s, easeOut);
&:before {
content: 'PREVIEW';
position: absolute;
top: 3px;
left: 5px;
font-size: 11px;
color: #bbb;
}
p {
font-family: $sans-serif;
}
background-color: #fafafa;
}
.wmd-button-row {
position: relative;
margin-left: 5px;
margin-right: 5px;
margin-bottom: 5px;
margin-top: 10px;
padding: 0px;
height: 20px;
overflow: hidden;
@include transition(all, .2s, easeOut);
}
.wmd-spacer {
width: 1px;
height: 20px;
margin-left: 14px;
position: absolute;
background-color: Silver;
display: inline-block;
list-style: none;
}
.wmd-button {
width: 20px;
height: 20px;
padding-left: 2px;
padding-right: 3px;
position: absolute;
display: inline-block;
list-style: none;
cursor: pointer;
}
.wmd-button > span {
display: inline-block;
background-image: url('/static/images/wmd-buttons.png');
background-repeat: no-repeat;
background-position: 0px 0px;
width: 20px;
height: 20px;
}
.wmd-spacer1 {
left: 50px;
}
.wmd-spacer2 {
left: 175px;
}
.wmd-spacer3 {
left: 300px;
}
.wmd-prompt-background {
background-color: Black;
}
.wmd-prompt-dialog {
@extend .modal;
background: #fff;
}
.wmd-prompt-dialog {
padding: 20px;
> div {
font-size: 0.8em;
font-family: arial, helvetica, sans-serif;
}
b {
font-size: 16px;
}
> form > input[type="text"] {
border-radius: 3px;
color: #333;
}
> form > input[type="button"] {
border: 1px solid #888;
font-family: $sans-serif;
font-size: 14px;
}
> form > input[type="file"] {
margin-bottom: 18px;
}
}
}
$comment-margin-left: 30px;
$discussion-title-size: 1.6em;
$comment-title-size: 1.0em;
$post-font-size: 0.9em;
$comment-info-size: 0.75em;
$comment-font-size: 0.8em;
$discussion-input-width: 100%;
.container .discussion-body {
display: block;
line-height: 1.4;
background: transparent;
box-shadow: none;
border: none;
@include clearfix;
.sidebar {
float: left;
@include box-sizing(border-box);
width: 31%;
height: 550px;
border: 1px solid #aaa;
box-shadow: 0 1px 2px rgba(0, 0, 0, .05);
background: #f6f6f6;
border-radius: 3px;
border-right: 1px solid #bcbcbc;
$tag-background-color: #e7ecdd;
$tag-border-color: #babdb3;
$tag-text-color: #5b614f;
&.fixed {
@include box-sizing(border-box);
position: fixed;
top: 0px;
width: 32%;
}
}
.browse-search {
display: block;
position: relative;
height: 60px;
border-bottom: 1px solid #a3a3a3;
border-radius: 3px 0 0 0;
/*** Mixins ***/
@mixin discussion-font {
font-family: inherit;
}
.browse,
.search {
position: relative;
float: left;
width: 20%;
height: 100%;
@include linear-gradient(top, rgba(255, 255, 255, .5), rgba(255, 255, 255, 0));
background-color: #dcdcdc;
@include transition(all .2s ease-out);
@mixin discussion-clickable {
color: black;
&:hover {
text-decoration: none;
background-color: #e9e9e9;
}
}
@mixin standard-discussion-link {
text-decoration: none;
&:hover {
color: #1C71DD;
text-decoration: none;
&.is-open {
width: 80%;
}
}
}
.discussion-loading {
background-image: url(../images/discussion/loading.gif);
width: 15px;
height: 15px;
margin-left: 2px;
display: inline-block;
}
.browse {
border-radius: 3px 0 0 0;
box-shadow: -1px 0 0 #aaa inset;
&.is-open {
.browse-topic-drop-btn span {
opacity: 1;
}
.browse-topic-drop-icon {
opacity: 0;
}
&.is-dropped {
.browse-topic-drop-btn {
span {
color: #fff;
text-shadow: none;
}
border-color: #4b4b4b;
}
}
}
&.is-dropped {
.browse-topic-drop-btn {
background-color: #616161;
}
}
&.is-dropped {
.browse-topic-drop-icon {
background-position: 0 -16px;
}
}
}
.search {
cursor: pointer;
border-radius: 0 3px 0 0;
&.is-open {
cursor: auto;
.post-search {
padding: 0 10px;
max-width: 1000px;
}
.post-search-field {
cursor: text;
pointer-events: auto;
&::-webkit-input-placeholder,
&:-moz-placeholder,
&:-ms-input-placeholder {
opacity: 1;
}
}
}
}
.browse-topic-drop-btn {
display: block;
position: absolute;
top: -1px;
left: -1px;
z-index: 50;
width: 100%;
height: 100%;
border-radius: 3px 0 0 0;
border: 1px solid transparent;
text-align: center;
overflow: hidden;
@include transition(none);
/*** Discussions ***/
.current-board {
white-space: nowrap;
}
.discussion {
span {
font-size: 14px;
font-weight: 700;
line-height: 58px;
color: #333;
text-shadow: 0 1px 0 rgba(255, 255, 255, .8);
opacity: 0;
@include transition(opacity .2s);
}
}
#open_close_accordion {
.browse-topic-drop-icon {
display: block;
position: absolute;
top: 21px;
left: 50%;
z-index: 100;
width: 29px;
height: 16px;
margin-left: -12px;
background: url(../images/browse-icon.png) no-repeat;
opacity: 1;
@include transition(none);
}
.browse-topic-drop-menu-wrapper {
display: none;
position: absolute;
top: 60px;
left: -1px;
z-index: 9999;
width: 100%;
background: #737373;
border: 1px solid #4b4b4b;
border-left: none;
border-radius: 0 0 3px 3px;
@include box-shadow(1px 0 0 #4b4b4b inset);
.browse-topic-drop-menu {
max-height: 400px;
overflow-y: scroll;
}
p + p, ul + p, ol + p {
margin-top: 0;
ul {
position: inline;
}
/*** Sidebar ***/
> li:first-child a {
border-top: none;
}
.sidebar-module {
a {
display: block;
padding: 0 20px;
border-top: 1px solid #5f5f5f;
font-size: 14px;
font-weight: 700;
line-height: 22px;
color: #fff;
@include clearfix;
padding: 0 26px 24px;
margin-bottom: 24px;
border-bottom: 1px solid #d3d3d3;
font-size: 13px;
@include transition(none);
header {
margin-bottom: 14px;
@include clearfix;
&.hidden {
display: none;
}
h4 {
&:hover,
&.focused {
background-color: #636363;
}
.board-name {
float: left;
font-size: 15px;
font-weight: bold;
width: 80%;
margin: 13px 0;
color: #fff;
}
.unread {
float: right;
padding: 0 5px;
margin-top: 13px;
font-size: 11px;
line-height: 22px;
border-radius: 2px;
@include linear-gradient(top, #4c4c4c, #5a5a5a);
}
}
li li {
a {
padding-left: 44px;
background: url(../images/nested-icon.png) no-repeat 22px 14px;
}
}
li li li {
a {
padding-left: 68px;
background: url(../images/nested-icon.png) no-repeat 46px 14px;
}
}
}
.browse-topic-drop-search {
padding: 10px;
}
.sidebar-new-post-button, .sidebar-promote-moderator-button {
@include button;
.browse-topic-drop-search-input {
width: 100%;
height: 30px;
padding: 0 15px;
@include box-sizing(border-box);
border-radius: 30px;
border: 1px solid #333;
box-shadow: 0 1px 3px rgba(0, 0, 0, .25) inset;
background: -webkit-linear-gradient(top, #eee, #fff);
font-size: 11px;
line-height: 16px;
color: #333;
outline: 0;
}
.sidebar-revoke-moderator-button {
@include button(simple, gray);
.post-search {
width: 100%;
max-width: 30px;
margin: auto;
@include box-sizing(border-box);
@include transition(all .2s);
}
.sidebar-new-post-button, .sidebar-promote-moderator-button, .sidebar-revoke-moderator-button {
.post-search-field {
display: block;
box-sizing: border-box;
width: 100%;
margin: 20px 0;
padding: 11px;
font-size: 1.1em;
height: 30px;
padding: 0;
margin: 14px auto;
@include box-sizing(border-box);
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 7px center #fff;
font-family: 'Open Sans', sans-serif;
font-weight: 400;
font-size: 13px;
line-height: 20px;
text-indent: 30px;
color: #333;
outline: 0;
cursor: pointer;
pointer-events: none;
@include transition(all .2s ease-out);
&::-webkit-input-placeholder,
&:-moz-placeholder,
&:-ms-input-placeholder {
opacity: 0;
@include transition(opacity .2s);
}
&:focus {
border-color: #4697c1;
}
}
}
.sort-bar {
height: 27px;
border-bottom: 1px solid #a3a3a3;
@include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0));
background-color: #aeaeae;
box-shadow: 0 1px 0 rgba(255, 255, 255, .2) inset;
span,
a {
font-size: 9px;
font-weight: bold;
line-height: 25px;
color: #333;
text-transform: uppercase;
text-shadow: 0 1px 0 rgba(255, 255, 255, .4);
}
.sort-label {
display: block;
float: left;
margin: 0 10px;
}
li {
float: left;
margin: 4px 4px 0 0;
}
a {
display: block;
height: 18px;
padding: 0 9px;
border-radius: 19px;
color: #333;
line-height: 17px;
&:hover {
@include linear-gradient(top, rgba(255, 255, 255, .4), rgba(255, 255, 255, .2));
color: #333;
}
&.active {
@include linear-gradient(top, rgba(0, 0, 0, .3), rgba(0, 0, 0, 0));
background-color: #999;
color: #fff;
text-shadow: 0 -1px 0 rgba(0, 0, 0, .3);
box-shadow: 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 1px rgba(0, 0, 0, .2) inset;
}
}
}
.post-list-wrapper {
overflow-y: scroll;
overflow-x: hidden;
border-right: 1px solid transparent;
}
.post-list {
background-color: #ddd;
.loading {
padding: 15px 0;
background: #f6f6f6;
.loading-animation {
background-image: url(../images/spinner-on-grey.gif);
}
}
.more-pages a {
background: #eee;
font-size: 12px;
line-height: 33px;
text-align: center;
&:hover {
text-decoration: none;
background-image: none;
background-color: #e6e6e6;
}
}
a {
position: relative;
display: block;
height: 36px;
padding: 0 10px 0 18px;
margin-bottom: 1px;
margin-right: -1px;
@include linear-gradient(top, rgba(255, 255, 255, .7), rgba(255, 255, 255, 0));
background-color: #fff;
&:hover {
@include linear-gradient(top, rgba(255, 255, 255, .7), rgba(255, 255, 255, 0));
background-color: #eee;
}
&.staff-post.staff-response {
.staff-post-icon {
top: 5px;
}
.sidebar-new-post-button {
margin: 40px 0 20px 0;
.staff-response-icon {
top: 18px;
}
}
.sidebar-view-all {
float: right;
.staff-post-icon,
.staff-response-icon {
position: absolute;
top: 11px;
left: 3px;
width: 13px;
height: 13px;
background: url(../images/staff-icons.png) no-repeat;
}
.staff-post-icon {
left: 2px;
background-position: 0 0;
}
.staff-response-icon {
background-position: -13px 0;
}
.title {
font-size: 13px;
line-height: 1.6em;
@include standard-discussion-link;
font-weight: 700;
line-height: 34px;
color: #333;
}
.discussion-sidebar-following-list {
li {
@include clearfix;
margin-bottom: 8px;
border: none;
&.read .title {
font-weight: 400;
color: #737373;
}
a {
@include standard-discussion-link;
background: none;
&.resolved:before {
content: '';
position: absolute;
top: 12px;
right: 75px;
width: 9px;
height: 8px;
background: url(../images/sidebar-resolved-icons.png) no-repeat;
}
&.followed:after {
content: '';
position: absolute;
top: 0;
right: 1px;
width: 10px;
height: 12px;
background: url(../images/following-flag.png) no-repeat;
}
&.active {
@include linear-gradient(top, #96e0fd, #61c7fc);
border-color: #4697c1;
box-shadow: 0 1px 0 #4697c1, 0 -1px 0 #4697c1;
.title {
color: #333;
}
.staff-post-icon {
background-position: 0 -13px;
}
.staff-response-icon {
background-position: -13px -13px;
}
.votes-count,
.comments-count {
@include linear-gradient(top, #3994c7, #4da7d3);
color: #fff;
&:after {
background-position: 0 0;
}
}
&.followed:after {
background-position: 0 -12px;
}
&.resolved:before {
background-position: 0 -8px;
}
}
}
.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;
@include linear-gradient(top, #d4d4d4, #dfdfdf);
font-size: 11px;
font-weight: 700;
line-height: 16px;
text-align: center;
color: #767676;
}
.comments-count {
position: relative;
width: 25px;
margin-left: 4px;
&:after {
content: '';
display: block;
position: absolute;
top: 16px;
right: 3px;
width: 5px;
height: 5px;
background: url(../images/comment-icon-bottoms.png) no-repeat;
background-position: 0 -5px;
}
&.new {
@include linear-gradient(top, #84d7fe, #99e0fe);
color: #333;
&:after {
color: #99e0fe;
}
}
}
}
.bottom-post-status {
padding: 30px;
font-size: 20px;
font-weight: 700;
color: #ccc;
text-align: center;
}
.discussion-column {
float: right;
@include box-sizing(border-box);
width: 68%;
max-width: 800px;
min-height: 500px;
border: 1px solid #aaa;
border-radius: 3px;
background: #fff;
box-shadow: 0 1px 2px rgba(0, 0, 0, .05);
&.sidebar-fixed {
margin-left: 32%;
}
}
.blank-slate h1 {
margin-top: 195px;
text-align: center;
color: #ccc;
}
.blank-slate,
.discussion-article {
position: relative;
padding: 40px;
min-height: 468px;
h1 {
margin-bottom: 10px;
font-size: 28px;
font-weight: 700;
letter-spacing: 0;
line-height: 1.2;
}
.posted-details {
font-size: 12px;
font-style: italic;
color: #888;
.username {
display: block;
font-size: 16px;
font-weight: 700;
}
span {
line-height: 1.3;
font-style: italic;
}
}
.post-context{
margin-top: 20px;
font-size: 12px;
color: #888;
}
.discussion-sidebar-tags-list li {
@include clearfix;
border-bottom: none;
p + p {
margin-top: 20px;
}
.sidebar-tag-count {
color: #9a9a9a;
font-size: .85em;
line-height: 3em;
.dogear {
display: block;
position: absolute;
top: -1px;
right: -1px;
width: 52px;
height: 51px;
background: url(../images/follow-dog-ear.png) 0 -52px no-repeat;
@include transition(none);
&.is-followed {
background-position: 0 0;
}
.sidebar-following-name {
float: left;
width: 80%;
}
.sidebar-vote-count {
float: right;
width: 20%;
text-align: right;
color: #9a9a9a;
}
//user profile
.discussion-post {
padding: 10px 20px;
.user-profile {
@extend .sidebar;
margin-top: 24px;
> header .vote-btn {
position: relative;
z-index: 100;
margin-top: 5px;
}
.sidebar-username {
font-size: 1.5em;
font-weight: bold;
line-height: 1.5em;
margin-top: 20px;
}
.post-tools {
@include clearfix;
margin-top: 15px;
.sidebar-user-roles {
color: darkGray;
font-style: italic;
margin-bottom: 15px;
}
.ui-icon {
display: inline;
float: left;
width: 13px;
height: 13px;
margin-right: 4px;
background: url(../images/small-grey-arrows.png) no-repeat;
.sidebar-threads-count, .sidebar-comments-count {
&.expand {
margin-top: 5px;
background-position: 0 0;
}
span {
font-size: 1.5em;
font-weight: bold;
line-height: 1.5em;
margin-right: 10px;
&.collapse {
margin-top: 6px;
background-position: -13px 0;
}
}
}
}
.discussion-non-content {
margin-left: flex-gutter();
.discussion-post header,
.responses li header {
margin-bottom: 20px;
}
/*** Post ***/
.responses {
list-style: none;
margin-top: 40px;
padding: 0;
.discussion-title {
@include discussion-font;
@include discussion-clickable;
display: inline-block;
font-size: $discussion-title-size;
font-weight: bold;
margin-bottom: flex-gutter(6);
> li {
position: relative;
margin: 0 -10px 30px;
padding: 26px 30px 20px;
border-radius: 3px;
border: 1px solid #b2b2b2;
box-shadow: 0 1px 3px rgba(0, 0, 0, .15);
@include animation(fadeIn .3s);
&.staff {
padding-top: 38px;
border-color: #009fe2;
}
.discussion-title-wrapper {
.discussion-watch-discussion, .discussion-unwatch-discussion {
@include discussion-font;
display: none;
font-size: $comment-info-size;
margin-left: 5px;
.staff-banner {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 14px;
padding: 1px 5px;
@include box-sizing(border-box);
border-radius: 2px 2px 0 0;
background: #009fe2;
font-size: 9px;
font-weight: 700;
color: #fff;
text-transform: uppercase;
}
&.loading {
height: 0;
margin: 0;
padding: 0;
border: none;
box-shadow: none;
}
}
.discussion-right-wrapper {
min-height: 40px;
margin: 24px 0 24px 68px;
.posted-by {
font-weight: 700;
}
.admin-actions {
float: right;
margin: 0.4em 1em 0 2em;
padding: 0;
li {
margin-bottom: 6px !important;
}
a {
.vote-btn {
position: relative;
z-index: 100;
float: right;
display: block;
height: 25px;
padding-left: 25px;
border-radius: 50%;
background: url(../images/admin-actions-sprite.png) no-repeat;
font-size: .8em;
height: 27px;
padding: 0 8px;
border-radius: 5px;
border: 1px solid #b2b2b2;
@include 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: #b8b8b8;
@include transition(color, .1s);
color: #333;
&:hover {
text-decoration: none;
.plus-icon {
float: left;
display: block;
width: 10px;
height: 10px;
margin: 8px 6px 0 0;
background: url(../images/vote-plus-icon.png) no-repeat;
font-size: 18px;
text-indent: -9999px;
color: #17b429;
overflow: hidden;
}
&.admin-endorse {
background-position: 0 0;
&.is-cast {
border-color: #379a42;
@include 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);
&:hover {
color: #63b141;
background-position: 0 -75px;
.plus-icon {
background-position: 0 -10px;
color: #336a39;
text-shadow: 0 1px 0 rgba(255, 255, 255, .4);
}
}
}
&.admin-edit {
background-position: 0 -25px;
.endorse-btn {
display: block;
float: right;
width: 27px;
height: 27px;
margin-right: 10px;
border-radius: 27px;
border: 1px solid #a0a0a0;
@include linear-gradient(top, #fff 35%, #ebebeb);
box-shadow: 0 1px 1px rgba(0, 0, 0, .1);
&:hover {
color: #009fe2;
background-position: 0 -100px;
}
.check-icon {
display: block;
width: 13px;
height: 12px;
margin: 8px auto;
background: url(../images/endorse-icon.png) no-repeat;
}
&.admin-delete {
background-position: 0 -50px;
&.is-endorsed {
border: 1px solid #4697c1;
@include 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;
&:hover {
color: #d45050;
background-position: 0 -125px;
}
.check-icon {
background-position: 0 -12px;
}
}
}
.comments {
.admin-actions {
margin-top: 0;
list-style: none;
margin-top: 20px;
padding: 0;
border-top: 1px solid #ddd;
li {
margin-bottom: 2px !important;
background: #f6f6f6;
border-bottom: 1px solid #ddd;
}
a {
width: 20px;
height: 20px;
padding-left: 0;
overflow: hidden;
text-indent: -9999px;
&.admin-endorse {
background-position: 0 -150px;
.comment-form {
background: #eee;
@include clearfix;
&:hover {
background-position: 0 -225px;
.comment-form-input {
padding: 5px 10px;
background-color: #fff;
font-size: 14px;
}
.discussion-submit-comment {
@include blue-button;
float: left;
margin-top: 8px;
}
&.admin-edit {
background-position: 0 -175px;
.wmd-input {
height: 40px;
}
&:hover {
background-position: 0 -250px;
.discussion-errors {
margin: 0px;
}
}
&.admin-delete {
background-position: 0 -200px;
.response-body {
font-size: 13px;
padding: 10px 20px;
&:hover {
background-position: 0 -275px;
p + p {
margin-top: 12px;
}
}
.posted-details {
margin: 0 20px 10px;
font-size: 11px;
}
.staff-label {
margin-left: 2px;
padding: 0 4px;
border-radius: 2px;
background: #009FE2;
font-size: 9px;
font-weight: 700;
font-style: normal;
color: white;
text-transform: uppercase;
}
}
.comment-form {
padding: 8px 20px;
.wmd-input {
@include transition(all .2s);
}
.wmd-button {
background: transparent;
/*** thread ***/
.thread {
//display: none;
.search-highlight {
display: inline;
font-weight: bold;
background-color: lightyellow;
span {
background-image: url(../images/wmd-buttons-transparent.png);
}
}
.thread-title {
@include discussion-font;
@include discussion-clickable;
display: block;
margin-bottom: 1em;
font-size: $comment-title-size;
font-weight: bold;
line-height: 1.4em;
}
.thread-body, .content-body {
@include discussion-font;
font-size: $post-font-size;
margin-bottom: 4px;
margin-top: 3px;
.comment-form-input {
width: 100%;
height: 31px;
padding: 0 10px;
@include box-sizing(border-box);
border: 1px solid #b2b2b2;
border-radius: 3px;
box-shadow: 0 1px 3px rgba(0, 0, 0, .1) inset;
@include transition(border-color .1s);
outline: 0;
p {
@include discussion-font;
&:focus {
border-color: #4697c1;
}
}
.thread-tags {
display: inline-block;
}
.moderator-actions {
margin: 0;
margin-top: 20px;
padding: 0;
@include clearfix;
.context{
margin-top: 1em;
font-size: $comment-font-size;
li {
float: left;
margin-right: 8px;
list-style: none;
}
.info {
@include discussion-font;
color: gray;
font-size: $comment-info-size;
font-style: italic;
margin-top: 1em;
a {
display: block;
height: 26px;
padding: 0 12px;
border-radius: 3px;
border: 1px solid #b2b2b2;
@include 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);
a:hover {
text-decoration: none;
color: #1C71DD;
.delete-icon {
display: block;
float: left;
width: 10px;
height: 10px;
margin: 8px 4px 0 0;
background: url(../images/moderator-delete-icon.png) no-repeat;
}
.comment-time {
display: inline;
float: right;
margin-right: 1em;
.edit-icon {
display: block;
float: left;
width: 10px;
height: 10px;
margin: 7px 4px 0 0;
background: url(../images/moderator-edit-icon.png) no-repeat;
}
}
.show-comments-wrapper {
display: inline;
margin-right: 20px;
}
.discussion-actions {
display: inline;
margin: 0;
padding: 0;
li {
display: inline;
margin-right: 20px;
}
}
.discussion-link {
@include discussion-font;
color: #1d9dd9;
display: inline;
&.discussion-unfollow-thread {
color: #dea03e;
.main-article.new {
display: none;
padding: 50px;
}
.new-post-form {
margin-top: 20px;
@include clearfix;
}
.author-moderator:after{
content: " (moderator)"
.new-post-form .submit {
@include blue-button;
float: left;
margin-top: 10px;
padding-bottom: 2px;
}
.author-administrator:after{
content: " (instructor)"
.new-post-form .options {
float: right;
margin-top: 20px;
font-size: 14px;
label {
margin-left: 4px;
}
}
.discussion-content {
border-top: lightgray 1px solid;
overflow: hidden;
// padding: 1.5% 0;
.discussion-reply-new {
@include discussion-font;
margin-left: 68px;
.reply-body {
@include discussion-font;
display: block;
font-size: $post-font-size;
margin-top: 10px;
width: 95%;
}
.reply-post-control {
margin-top: 1%;
}
}
}
// Role based styles
.role-moderator{
background-color: #eafcfc;
.discussion-reply-new {
padding: 20px;
@include clearfix;
@include transition(opacity .2s);
h4 {
font-size: 16px;
font-weight: 700;
}
.role-administrator{
background-color: #eafcea;
}
//COMMENT STYLES
.comments {
overflow: hidden;
.discussion-votes {
margin-top: 8px;
.wmd-button-bar {
border: none;
}
.discussion-right-wrapper {
margin: 10px 0 10px 68px;
.wmd-input {
@include discussion-wmd-input;
}
.comment {
margin-left: 68px;
.comment-body, .content-body {
@include discussion-font;
color: black;
display: block;
font-size: $comment-font-size;
margin-top: 3px;
.wmd-preview {
@include discussion-wmd-preview;
}
&.endorsed {
> .discussion-content {
background-color: #fcfcea;
}
}
.reply-post-control {
margin-top: 20px;
}
.discussion-submit-post {
@include blue-button;
float: left;
}
.wmd-button {
width: 15px;
}
}
.global-discussion-actions {
height: 60px;
@include linear-gradient(top, #ebebeb, #d9d9d9);
border-radius: 0 3px 0 0;
border-bottom: 1px solid #bcbcbc;
}
/*** Sorting ***/
.discussion-sort {
float: right;
font-size: 0.8em;
margin-top: -36px;
.discussion-label {
display: block;
float: left;
padding: 0 14px;
line-height: 34px;
.discussion-module {
@extend .discussion-body;
position: relative;
margin: 20px 0;
padding: 20px;
background: #f6f6f6 !important;
border-radius: 3px;
header {
.anonymous{
font-size: 15px;
}
}
.discussion-sort-link {
display: block;
float: left;
padding: 0 14px;
line-height: 34px;
.responses {
margin-top: 40px;
&:hover {
color: #1C71DD;
text-decoration: none;
> li {
margin: 0 20px 20px !important;
padding: 26px 30px 20px !important;
}
}
.discussion-sort-link.sorted {
color: #000;
border-bottom: 2px solid #000;
}
.loading-animation {
background-image: url(../images/spinner-on-grey.gif);
}
/*** Search ***/
.discussion-show {
position: relative;
top: 3px;
font-size: 14px;
text-align: center;
.search-wrapper-inline {
display: inline-block;
margin-top: 3%;
width: 80%;
&.shown {
.show-hide-discussion-icon {
background-position: 0 0;
}
.search-wrapper {
margin-bottom: 50px;
margin-left: .5%;
}
.discussion-search-form {
.show-hide-discussion-icon {
display: inline-block;
margin-bottom: 1%;
width: flex-grid(12);
position: relative;
top: 5px;
margin-right: 6px;
width: 21px;
height: 19px;
background: url(../images/show-hide-discussion-icon.png) no-repeat;
background-position: -21px 0;
}
}
.discussion-link {
@include button(simple, #999);
color: white;
.new-post-btn {
display: inline-block;
font-size: inherit;
font-weight: bold;
margin-left: 1%;
padding-top: 4px;
padding-bottom: 2px;
text-decoration: none;
float: right;
}
.discussion-search-text {
@include discussion-font;
}
section.discussion {
margin-top: 30px;
.search-input {
float: left;
font: inherit;
font-style: normal;
// width: 72%;
width: flex-grid(8);
margin-left: flex-grid(1);
}
.threads {
margin-top: 20px;
}
.search-within {
display: block;
margin-bottom: 3%;
}
/* Course content p has a default margin-bottom of 1.416em, this is just to reset that */
.discussion-thread {
padding: 0;
@include transition(all .25s);
.discussion-search-within-board {
font: inherit;
font-size: $post-font-size;
font-style: normal;
.dogear {
display: none;
}
/*** buttons ***/
&.expanded {
padding: 20px 0;
.control-button {
@include button;
@include discussion-font;
background-color: #959595;
@include background-image(linear-gradient(top, #959595, #7B7B7B));
border: 1px solid #6F6F6F;
@include box-shadow(inset 0 1px 0 #A2A2A2, 0 0 3px #CCC);
color: white;
display: inline-block;
font-size: inherit;
font-weight: bold;
margin-bottom: 3%;
padding-top: 9px;
width: inherit;
text-decoration: none;
text-shadow: none;
.dogear{
display: block;
}
&:hover {
background-color: #A2A2A2;
@include background-image(linear-gradient(top, #A2A2A2, #7B7B7B));
border: 1px solid #555;
@include box-shadow(inset 0 1px 0 #BBB, 0 0 3px #CCC);
.discussion-article {
border: 1px solid #b2b2b2;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
border-radius: 3px;
}
}
.follow-wrapper {
display: inline;
p {
margin-bottom: 0em;
}
/*** votes ***/
.discussion-article {
border: 1px solid #ddd;
border-bottom-width: 0;
background: #fff;
min-height: 0;
padding: 10px 10px 15px 10px;
box-shadow: 0 1px 0 #ddd;
@include transition(all .2s);
.discussion-votes {
float: left;
width: 60px;
margin-top: 18px;
text-align: center;
.discussion-post {
padding: 12px 20px 0 20px;
@include clearfix;
.discussion-vote {
.inline-comment-count {
position: relative;
z-index: 100;
float: right;
display: block;
width: 50px;
height: 17px;
margin: auto;
background: url(../images/vote-arrows.png) no-repeat;
font-size: 15px;
font-weight: bold;
color: black;
@include hide-text;
@include transition(all, 0, easeOut);
}
.discussion-vote-up {
margin-bottom: 5px;
background-position: -50px -3px;
&:hover {
background-position: -50px -5px;
@include transition-duration(0.05s);
}
&.voted {
background-position: 0 -3px;
color: #1C71DD;
@include transition-duration(0);
}
height: 27px;
margin-top: 6px;
margin-right: 8px;
padding: 0 8px;
border-radius: 5px;
font-size: 12px;
font-weight: 400;
line-height: 25px;
color: #888;
}
.discussion-vote-down {
margin-top: 7px;
background-position: -50px -30px;
header {
padding-bottom: 0;
margin-bottom: 15px;
&:hover {
background-position: -50px -28px;
@include transition-duration(0.05s);
}
.posted-details {
margin-top: 4px;
&.voted {
background-position: 0 -30px;
color: #1C71DD;
@include transition-duration(0);
.username {
display: inline;
font-size: 14px;
font-weight: 700;
}
}
.discussion-vote-count {
@include discussion-font;
font-size: $post-font-size;
h3 {
font-size: 19px;
font-weight: 700;
margin-bottom: 0px;
}
.discussion-votes-point {
font-size: 1.1em;
font-weight: bold;
color: #9a9a9a;
h4 {
font-size: 16px;
}
}
.post-body {
font-size: 14px;
clear: both;
}
}
/*** new post ***/
.post-tools {
margin-left: 20px;
margin-top: 5px;
.new-post-form, .discussion-thread-edit {
a {
display: block;
font-size: 12px;
line-height: 30px;
.title-input, .body-input {
display: block !important;
font: inherit;
font-style: normal;
//width: $discussion-input-width !important;
&.expand-post:before {
content: '▾ ';
}
.new-post-similar-posts-wrapper {
@include border-radius(3px);
border: 1px solid #EEE;
font-size: $post-font-size;
line-height: 150%;
margin-top: 1%;
padding: 1% 1.5%;
&.collapse-post:before {
content: '▴ ';
}
.hide-similar-posts {
float: right;
&.collapse-post {
display: none;
}
.new-post-similar-posts {
font: inherit;
.similar-post {
display: block;
line-height: 150%;
}
}
.discussion-errors {
color: #8F0E0E;
display: block;
margin-left: -5%;
}
.responses {
margin-top: 10px;
.new-post-body {
}
header {
padding-bottom: 0;
margin-bottom: 15px;
.tagsinput {
background: #FAFAFA;
border: 1px solid #C8C8C8;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
-ms-border-radius: 3px;
-o-border-radius: 3px;
border-radius: 3px;
-webkit-box-shadow: 0 1px 0 0 rgba(255, 255, 255, 0.6), inset 0 0 3px 0 rgba(0, 0, 0, 0.1);
-moz-box-shadow: 0 1px 0 0 rgba(255, 255, 255, 0.6), inset 0 0 3px 0 rgba(0, 0, 0, 0.1);
box-shadow: 0 1px 0 0 rgba(255, 255, 255, 0.6), inset 0 0 3px 0 rgba(0, 0, 0, 0.1);
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
margin-top: flex-gutter();
vertical-align: top;
-webkit-font-smoothing: antialiased;
.posted-by {
float: left;
margin-right: 5px;
font-size: 16px;
}
}
.discussion-content-edit, .discussion-reply-new, .new-post-form {
margin: 10px 0 10px 0;
.discussion-errors {
color: #8F0E0E;
display: block;
font: inherit;
font-size: $post_font_size;
list-style: none;
margin-left: -3%;
padding-left: 2em;
.response-body {
margin-bottom: 0.2em;
font-size: 14px;
}
a:hover {
color: #1C71DD;
text-decoration: none;
}
.new-post-title {
display: block;
padding: 5px 12px;
border-width: 1px;
width: 100%;
.discussion-reply-new {
.wmd-input {
height: 120px;
}
.thread-title-edit {
width: 100%;
}
&.collapsed {
.new-post-title {
// Content that is hidden by default in the inline view
.post-extended-content{
display: none;
visibility: hidden;
}
.wmd-button-row {
height: 0;
}
.wmd-input {
height: 100px;
@include border-radius(3px);
}
.wmd-preview {
height: 0;
padding: 0;
border-width: 0;
}
.post-options {
height: 0;
}
.post-control {
.new-post-article {
display: none;
}
margin-top: 20px;
.tagsinput {
display: none;
}
.inner-wrapper {
max-width: 1180px;
min-width: 760px;
margin: auto;
}
.new-post-control {
margin-left: 75%;
margin-top: 1%;
.new-post-form {
width: 100%;
margin-bottom: 20px;
padding: 30px;
border-radius: 3px;
background: rgba(0, 0, 0, .55);
color: #fff;
box-shadow: none;
@include clearfix;
@include box-sizing(border-box);
.form-row {
margin-bottom: 20px;
}
.discussion-cancel-post {
margin-right: 1.5%;
.new-post-body .wmd-input {
@include discussion-wmd-input;
position: relative;
width: 100%;
height: 200px;
z-index: 1;
padding: 10px;
@include box-sizing(border-box);
border: 1px solid #333;
border-radius: 3px 3px 0 0;
background: #fff;
font-family: 'Monaco', monospace;
font-size: 13px;
line-height: 1.6;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3) inset;
}
.new-post-body .wmd-preview {
@include discussion-wmd-preview;
position: relative;
width: 100%;
//height: 50px;
margin-top: -1px;
padding: 25px 20px 10px 20px;
@include box-sizing(border-box);
border: 1px solid #333;
border-radius: 0 0 3px 3px;
background: #e6e6e6;
color: #333;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3) inset;
}
.reply-post-control {
.new-post-preview-label {
position: absolute;
top: 4px;
left: 4px;
font-size: 11px;
color: #aaa;
text-transform: uppercase;
}
.discussion-cancel-post {
margin-right: 1.5%;
.new-post-title,
.new-post-tags {
width: 100%;
height: 40px;
padding: 0 10px;
@include box-sizing(border-box);
border-radius: 3px;
border: 1px solid #333;
font-size: 16px;
font-family: 'Open Sans', sans-serif;
color: #333;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3) inset;
}
.new-post-title {
font-weight: 700;
}
.edit-post-control {
margin-top: 1%;
.tagsinput {
padding: 10px;
@include box-sizing(border-box);
border: 1px solid #333;
border-radius: 3px;
background: #fff;
font-family: 'Monaco', monospace;
font-size: 13px;
line-height: 1.6;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3) inset;
.discussion-cancel-update {
margin-right: 1.5%;
span.tag {
margin-bottom: 0;
}
}
.control-button {
@include button;
@include discussion-font;
margin-right: 16px;
padding-top: 9px;
color: white;
display: inline-block;
font-size: inherit;
font-weight: bold;
text-decoration: none;
width: inherit;
.submit {
@include blue-button;
float: left;
height: 37px;
margin-top: 10px;
padding-bottom: 2px;
border-color: #333;
&:hover {
color: white;
border-color: #222;
}
}
.new-post-cancel {
@include white-button;
float: left;
margin: 10px 0 0 15px;
border-color: #444;
}
.options {
margin-top: 5px;
label {
display: inline;
font-family: $sans-serif;
font-size: .8em;
font-style: normal;
font-weight: 400;
margin-left: 8px;
font-size: 15px;
color: #fff;
text-shadow: none;
}
}
.discussion-content-edit {
margin: 3%;
}
.new-post-form {
margin: 10px 0 40px 0;
.thread-tags {
margin: 20px 0;
}
.discussion-reply-new {
.thread-tag {
padding: 3px 10px 6px;
margin-right: 5px;
border-radius: 3px;
color: #333;
background: #c5eeff;
border: 1px solid #90c4d7;
font-size: 13px;
}
.discussion-auto-watch {
margin-left: 2%;
.thread-title {
display: block;
margin-bottom: 20px;
font-size: 21px;
color: #333;
font-weight: 700;
}
}
.thread-tag {
background: $tag-background-color;
border: 1px solid $tag-border-color;
-moz-border-radius: 2px;
-webkit-border-radius: 2px;
color: $tag-text-color;
float: left;
.new-post-btn {
@include blue-button;
display: inline-block;
font-size: 13px;
margin: 5px 7px 5px 0;
padding: 5px 7px;
text-decoration: none;
margin-right: 4px;
}
&:hover {
border-color: #7b8761;
color: #2f381c;
text-decoration: none;
.new-post-icon {
display: block;
float: left;
width: 16px;
height: 17px;
margin: 8px 7px 0 0;
background: url(../images/new-post-icon.png) no-repeat;
}
.moderator-actions {
padding-left: 0 !important;
}
/*** pagination ***/
section.pagination {
margin-top: 30px;
.discussion-paginator {
font-size: $post-font-size;
margin-bottom: 10px;
margin-top: 20px;
text-align: center;
nav.discussion-paginator {
float: right;
div {
ol {
li {
list-style: none;
display: inline-block;
font-weight: bold;
margin: 0 5px;
padding-right: 0.5em;
a {
background: #EEE;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
-ms-border-radius: 3px;
-o-border-radius: 3px;
border-radius: 3px;
color: black;
font-weight: normal;
padding: 4px 10px;
text-decoration: none;
&:hover {
background: #DDD;
}
}
@include white-button;
}
}
&.inline-discussion, .forum-discussion, .user-discussion {
.new-post-form {
margin: 24px 60px;
.post-options {
margin: 8px 0 16px 0;
overflow: hidden;
label {
margin-right: 15px;
display: inline;
}
li.current-page{
height: 35px;
padding: 0 15px;
border: 1px solid #ccc;
border-radius: 3px;
font-size: 13px;
font-weight: 700;
line-height: 32px;
color: #333;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6);
}
.post-control {
overflow: hidden;
margin: 0 0 5% 0;
}
}
}
}
/*** base editor styles ***/
.wmd-panel {
.new-post-body {
.wmd-panel {
width: 100%;
min-width: 500px;
}
}
.wmd-button-bar {
.wmd-button-bar {
width: 100%;
background-color: Silver;
}
}
.wmd-input {
.wmd-input {
height: 150px;
width: 100%;
background-color: #e9e9e9;
......@@ -938,14 +2134,14 @@ $tag-text-color: #5b614f;
&::-webkit-input-placeholder {
color: #888;
}
}
}
.wmd-preview {
.wmd-preview {
position: relative;
font-family: $sans-serif;
padding: 25px 20px 10px 20px;
margin-bottom: 5px;
box-sizing: border-box;
@include box-sizing(border-box);
border: 1px solid #c8c8c8;
border-top-width: 0;
@include border-radius(0 0 3px 3px);
......@@ -965,9 +2161,9 @@ $tag-text-color: #5b614f;
font-family: $sans-serif;
}
background-color: #fafafa;
}
}
.wmd-button-row {
.wmd-button-row {
position: relative;
margin-left: 5px;
margin-right: 5px;
......@@ -977,9 +2173,9 @@ $tag-text-color: #5b614f;
height: 20px;
overflow: hidden;
@include transition(all, .2s, easeOut);
}
}
.wmd-spacer {
.wmd-spacer {
width: 1px;
height: 20px;
margin-left: 14px;
......@@ -988,9 +2184,9 @@ $tag-text-color: #5b614f;
background-color: Silver;
display: inline-block;
list-style: none;
}
}
.wmd-button {
.wmd-button {
width: 20px;
height: 20px;
padding-left: 2px;
......@@ -999,51 +2195,166 @@ $tag-text-color: #5b614f;
display: inline-block;
list-style: none;
cursor: pointer;
}
background: none;
}
.wmd-button > span {
background-image: url('/static/images/wmd-buttons.png');
.wmd-button > span {
display: inline-block;
background-image: url(../images/new-post-icons-full.png);
background-repeat: no-repeat;
background-position: 0px 0px;
width: 20px;
height: 20px;
display: inline-block;
}
}
.wmd-spacer1 {
.wmd-spacer1 {
left: 50px;
}
.wmd-spacer2 {
}
.wmd-spacer2 {
left: 175px;
}
}
.wmd-spacer3 {
.wmd-spacer3 {
left: 300px;
}
}
.wmd-prompt-background {
.wmd-prompt-background {
background-color: Black;
}
}
.wmd-prompt-dialog {
border: 1px solid #999999;
background-color: #F5F5F5;
}
.wmd-prompt-dialog {
@extend .modal;
background: #fff;
}
.wmd-prompt-dialog > div {
.wmd-prompt-dialog {
padding: 20px;
> div {
font-size: 0.8em;
font-family: arial, helvetica, sans-serif;
}
}
b {
font-size: 16px;
}
> form > input[type="text"] {
border-radius: 3px;
color: #333;
}
> form > input[type="button"] {
border: 1px solid #888;
font-family: $sans-serif;
font-size: 14px;
}
> form > input[type="file"] {
margin-bottom: 18px;
}
}
}
.wmd-button-row {
// this is being hidden now because the inline styles to position the icons are not being written
position: relative;
height: 12px;
}
.wmd-button {
span {
width: 20px;
height: 20px;
background-image: url("/static/images/wmd-buttons.png");
display: inline-block;
}
}
.wmd-spacer1 {
left: 50px;
}
.wmd-spacer2 {
left: 175px;
}
.wmd-spacer3 {
left: 300px;
}
.edit-post-form {
width: 100%;
margin-bottom: 20px;
@include clearfix;
@include box-sizing(border-box);
.form-row {
margin-top: 20px;
}
.post-cancel {
@include white-button;
float: left;
margin: 10px 0 0 15px;
}
.post-update {
@include blue-button;
float: left;
height: 37px;
margin-top: 10px;
padding-bottom: 2px;
&:hover {
border-color: #222;
}
}
.edit-post-title, .edit-post-tags {
width: 100%;
height: 40px;
padding: 0 10px;
@include box-sizing(border-box);
border-radius: 3px;
border: 1px solid #aaa;
font-size: 16px;
font-family: $sans-serif;
color: #333;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15) inset;
}
.tagsinput {
padding: 10px;
@include box-sizing(border-box);
border: 1px solid #aaa;
border-radius: 3px;
background: #fff;
font-family: 'Monaco', monospace;
font-size: 13px;
line-height: 1.6;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3) inset;
span.tag {
margin-bottom: 0;
}
}
}
.thread-tags {
margin: 20px 0;
}
.wmd-prompt-dialog > form > input[type="text"] {
border: 1px solid #999999;
color: black;
.thread-tag {
padding: 3px 10px 6px;
margin-right: 5px;
border-radius: 3px;
color: #333;
background: #c5eeff;
border: 1px solid #90c4d7;
font-size: 13px;
}
}
.wmd-prompt-dialog > form > input[type="button"] {
border: 1px solid #888888;
font-family: trebuchet MS, helvetica, sans-serif;
font-size: 0.8em;
font-weight: bold;
.discussion-user-threads {
@extend .discussion-module
}
......@@ -45,6 +45,7 @@ span {
font: normal 1em/1.6em $sans-serif;
color: $base-font-color;
}
/* Fix for CodeMirror: prevent top-level span from affecting deeply-embedded span in CodeMirror */
.CodeMirror span {
font: inherit;
......@@ -136,3 +137,23 @@ span.edx {
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 @@
// Course base / layout styles
@import 'course/layout/courseware_header';
@import 'course/layout/footer';
@import 'course/base/mixins';
@import 'course/base/base';
@import 'course/base/extends';
@import 'module/module-styles.scss';
......
.discussion-module {
@extend .discussion-body;
margin: 20px 0;
padding: 20px 20px 28px 20px;
background: #f6f6f6 !important;
border-radius: 3px;
.responses {
margin-top: 40px;
> li {
margin: 0 20px 30px;
}
}
.discussion-show {
display: block;
width: 200px;
margin: auto;
font-size: 14px;
text-align: center;
&.shown {
.show-hide-discussion-icon {
background-position: 0 0;
}
}
.show-hide-discussion-icon {
display: inline-block;
position: relative;
top: 5px;
margin-right: 6px;
width: 21px;
height: 19px;
background: url(../images/show-hide-discussion-icon.png) no-repeat;
background-position: -21px 0;
}
}
.new-post-btn {
display: inline-block;
}
section.discussion {
margin-top: 20px;
.threads {
margin-top: 20px;
}
/* Course content p has a default margin-bottom of 1.416em, this is just to reset that */
.discussion-thread {
padding: 0;
@include transition(all .25s);
.dogear,
.vote-btn {
display: none;
}
&.expanded {
padding: 20px 0;
.dogear,
.vote-btn {
display: block;
}
.discussion-article {
border: 1px solid #b2b2b2;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
border-radius: 3px;
}
}
p {
margin-bottom: 0em;
}
.discussion-article {
border: 1px solid #ddd;
border-bottom-width: 0;
background: #fff;
min-height: 0;
padding: 10px 10px 15px 10px;
box-shadow: 0 1px 0 #ddd;
@include transition(all .2s);
.discussion-post {
padding: 12px 20px 0 20px;
@include clearfix;
header {
padding-bottom: 0;
margin-bottom: 15px;
h3 {
font-size: 19px;
font-weight: 700;
margin-bottom: 0px;
}
h4 {
font-size: 16px;
}
}
.post-body {
font-size: 14px;
clear: both;
}
}
.post-tools {
margin-left: 20px;
a {
display: block;
font-size: 12px;
line-height: 30px;
&.expand-post:before {
content: '▾ ';
}
&.collapse-post:before {
content: '▴ ';
}
&.collapse-post {
display: none;
}
}
}
.responses {
margin-top: 10px;
header {
padding-bottom: 0em;
margin-bottom: 5px;
.posted-by {
font-size: 0.8em;
}
}
.response-body {
margin-bottom: 0.2em;
font-size: 14px;
}
}
.discussion-reply-new {
.wmd-input {
height: 120px;
}
}
// Content that is hidden by default in the inline view
.post-extended-content{
display: none;
}
}
}
}
.new-post-article {
display: none;
margin-top: 20px;
.inner-wrapper {
max-width: 1180px;
min-width: 760px;
margin: auto;
}
.new-post-form {
width: 100%;
margin-bottom: 20px;
padding: 30px;
border-radius: 3px;
background: rgba(0, 0, 0, .55);
color: #fff;
box-shadow: none;
@include clearfix;
@include box-sizing(border-box);
.form-row {
margin-bottom: 20px;
}
.new-post-body .wmd-input {
@include discussion-wmd-input;
position: relative;
width: 100%;
height: 200px;
z-index: 1;
padding: 10px;
box-sizing: border-box;
border: 1px solid #333;
border-radius: 3px 3px 0 0;
background: #fff;
font-family: 'Monaco', monospace;
font-size: 13px;
line-height: 1.6;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3) inset;
}
.new-post-body .wmd-preview {
@include discussion-wmd-preview;
position: relative;
width: 100%;
//height: 50px;
margin-top: -1px;
padding: 25px 20px 10px 20px;
box-sizing: border-box;
border: 1px solid #333;
border-radius: 0 0 3px 3px;
background: #e6e6e6;
color: #333;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3) inset;
}
.new-post-preview-label {
position: absolute;
top: 4px;
left: 4px;
font-size: 11px;
color: #aaa;
text-transform: uppercase;
}
.new-post-title,
.new-post-tags {
width: 100%;
height: 40px;
padding: 0 10px;
box-sizing: border-box;
border-radius: 3px;
border: 1px solid #333;
font-size: 16px;
font-family: 'Open Sans', sans-serif;
color: #333;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3) inset;
}
.new-post-title {
font-weight: 700;
}
.tagsinput {
padding: 10px;
box-sizing: border-box;
border: 1px solid #333;
border-radius: 3px;
background: #fff;
font-family: 'Monaco', monospace;
font-size: 13px;
line-height: 1.6;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3) inset;
span.tag {
margin-bottom: 0;
}
}
.submit {
@include blue-button;
float: left;
height: 37px;
margin-top: 10px;
padding-bottom: 2px;
border-color: #333;
&:hover {
border-color: #222;
}
}
.new-post-cancel {
@include white-button;
float: left;
margin: 10px 0 0 15px;
border-color: #444;
}
.options {
margin-top: 5px;
label {
display: inline;
margin-left: 8px;
font-size: 15px;
color: #fff;
text-shadow: none;
}
}
}
.thread-tags {
margin-top: 20px;
}
.thread-tag {
padding: 3px 10px 6px;
border-radius: 3px;
color: #333;
background: #c5eeff;
border: 1px solid #90c4d7;
font-size: 13px;
}
.thread-title {
display: block;
margin-bottom: 20px;
font-size: 21px;
color: #333;
font-weight: 700;
}
}
.new-post-btn {
@include blue-button;
display: inline-block;
font-size: 13px;
margin-right: 4px;
}
.new-post-icon {
display: block;
float: left;
width: 16px;
height: 17px;
margin: 8px 7px 0 0;
background: url(../images/new-post-icon.png) no-repeat;
}
.moderator-actions {
padding-left: 0 !important;
}
section.pagination {
margin-top: 30px;
nav.discussion-paginator {
float: right;
ol {
li {
list-style: none;
display: inline-block;
padding-right: 0.5em;
a {
@include white-button;
}
}
li.current-page{
height: 35px;
padding: 0 15px;
border: 1px solid #ccc;
border-radius: 3px;
font-size: 13px;
font-weight: 700;
line-height: 32px;
color: #333;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6);
}
}
}
}
.new-post-body {
.wmd-panel {
width: 100%;
min-width: 500px;
}
.wmd-button-bar {
width: 100%;
}
.wmd-input {
height: 150px;
width: 100%;
background-color: #e9e9e9;
border: 1px solid #c8c8c8;
font-family: Monaco, 'Lucida Console', monospace;
font-style: normal;
font-size: 0.8em;
line-height: 1.6em;
@include border-radius(3px 3px 0 0);
&::-webkit-input-placeholder {
color: #888;
}
}
.wmd-preview {
position: relative;
font-family: $sans-serif;
padding: 25px 20px 10px 20px;
margin-bottom: 5px;
box-sizing: border-box;
border: 1px solid #c8c8c8;
border-top-width: 0;
@include border-radius(0 0 3px 3px);
overflow: hidden;
@include transition(all, .2s, easeOut);
&:before {
content: 'PREVIEW';
position: absolute;
top: 3px;
left: 5px;
font-size: 11px;
color: #bbb;
}
p {
font-family: $sans-serif;
}
background-color: #fafafa;
}
.wmd-button-row {
position: relative;
margin-left: 5px;
margin-right: 5px;
margin-bottom: 5px;
margin-top: 10px;
padding: 0px;
height: 20px;
overflow: hidden;
@include transition(all, .2s, easeOut);
}
.wmd-spacer {
width: 1px;
height: 20px;
margin-left: 14px;
position: absolute;
background-color: Silver;
display: inline-block;
list-style: none;
}
.wmd-button {
width: 20px;
height: 20px;
padding-left: 2px;
padding-right: 3px;
position: absolute;
display: inline-block;
list-style: none;
cursor: pointer;
background: none;
}
.wmd-button > span {
display: inline-block;
background-image: url(../images/new-post-icons-full.png);
background-repeat: no-repeat;
background-position: 0px 0px;
width: 20px;
height: 20px;
}
.wmd-spacer1 {
left: 50px;
}
.wmd-spacer2 {
left: 175px;
}
.wmd-spacer3 {
left: 300px;
}
.wmd-prompt-background {
background-color: Black;
}
.wmd-prompt-dialog {
@extend .modal;
background: #fff;
}
.wmd-prompt-dialog {
padding: 20px;
> div {
font-size: 0.8em;
font-family: arial, helvetica, sans-serif;
}
b {
font-size: 16px;
}
> form > input[type="text"] {
border-radius: 3px;
color: #333;
}
> form > input[type="button"] {
border: 1px solid #888;
font-family: $sans-serif;
font-size: 14px;
}
> form > input[type="file"] {
margin-bottom: 18px;
}
}
}
.wmd-button-row {
// this is being hidden now because the inline styles to position the icons are not being written
display: none;
position: relative;
height: 12px;
}
.wmd-button {
span {
background-image: url("/static/images/wmd-buttons.png");
display: inline-block;
}
}
}
\ No newline at end of file
......@@ -39,11 +39,6 @@ a {
}
}
form {
label {
display: block;
......@@ -102,3 +97,90 @@ img {
background: #444;
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):
% if staff_access:
<li class="instructor"><a href="${reverse('instructor_dashboard', args=[course.id])}" class="${url_class('instructor')}">Instructor</a></li>
% endif
<%block name="extratabs" />
</ol>
</div>
</nav>
......@@ -5,7 +5,7 @@
</%def>
<%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_comments(content.get('children', []), *args, **kwargs)}
</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">
<a class="discussion-show control-button" href="javascript:void(0)" discussion_id="${discussion_id | h}">Show Discussion</a>
<%include file="_underscore_templates.html" />
<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>
<%! 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"/>
<%! from django_comment_client.mustache_helpers import url_for_user %>
<section class="discussion" _id="${discussion_id | h}">
<a class="discussion-title" href="javascript:void(0)">Discussion</a>
<div class="threads">
${renderer.render_content_with_comments(thread)}
<article class="discussion-article" data-id="${discussion_id| h}">
<a href="#" class="dogear"></a>
<div class="discussion-post">
<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>
</section>
</div>
<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" />
\ 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 @@
<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-user-roles">
......@@ -15,7 +15,7 @@
<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 "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:
<a href="javascript:void(0)" class="sidebar-toggle-moderator-button sidebar-promote-moderator-button">Promote to Moderator</a>
% endif
......
<%! import django_comment_client.helpers as helpers %>
<%! from django.template.defaultfilters import escapejs %>
<%! from django.core.urlresolvers import reverse %>
<%inherit file="../main.html" />
<%namespace name='static' file='../static_content.html'/>
<%block name="bodyclass">discussion</%block>
......@@ -13,26 +17,23 @@
<%static:js group='discussion'/>
</%block>
<%include file="../courseware/course_navigation.html" args="active_page='discussion'" />
<section class="container">
<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="_discussion_course_navigation.html" args="active_page='discussion'" />
<%include file="_recent_active_posts.html" />
<%include file="_new_post.html" />
<%include file="_trending_tags.html" />
<script type="text/javascript" src="${static.url('js/discussions-temp.js')}"></script>
</nav>
</section>
<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">
<div class="sidebar"></div>
<div class="discussion-column">
<div class="discussion-article blank-slate">
<h1>${course.title} Discussion</h1>
<section class="course-content">
${content.decode('utf-8')}
</section>
</div>
</div>
</div>
</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 @@
<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-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">
<input type="checkbox" class="discussion-post-anonymously" id="discussion-post-anonymously-${discussion_id}">
<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" />
<%namespace name='static' file='../static_content.html'/>
<%block name="bodyclass">discussion</%block>
......@@ -11,26 +16,20 @@
<%block name="js_extra">
<%include file="_js_body_dependencies.html" />
<%static:js group='discussion'/>
</%block>
<%include file="../courseware/course_navigation.html" args="active_page='discussion'" />
<script type="text/javascript" src="${static.url('js/discussions-temp.js')}"></script>
<section class="container">
<div class="course-wrapper">
<section aria-label="Course Navigation" class="course-index">
<nav>
<article class="sidebar-module discussion-sidebar">
</article>
<%include file="_recent_active_posts.html" />
</%block>
<%include file="_trending_tags.html" />
<%include file="_discussion_course_navigation.html" args="active_page='discussion'"/>
</nav>
</section>
<%include file="_new_post.html" />
<section class="course-content">
${content.decode('utf-8')}
</section>
<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}">
<div class="discussion-body">
<div class="sidebar"></div>
<div class="discussion-column"></div>
</div>
</section>
<%include file="_underscore_templates.html" />
<%include file="_thread_list_template.html" />
......@@ -29,8 +29,8 @@
</nav>
</section>
<section class="course-content">
${content.decode('utf-8')}
<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}">
<h2>Active Threads</h2>
</section>
</div>
</section>
......@@ -39,3 +39,4 @@
var $$profiled_user_id = "${django_user.id | escapejs}";
var $$course_id = "${course.id | escapejs}";
</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