Commit 36b35245 by Arjun Singh

Merge branch 'feature/tomg/new-discussions' of github.com:MITx/mitx into…

Merge branch 'feature/tomg/new-discussions' of github.com:MITx/mitx into feature/tomg/new-discussions
parents 242f523d a40c8fad
......@@ -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):
......@@ -214,7 +213,7 @@ def undo_vote_for_thread(request, course_id, thread_id):
thread = cc.Thread.find(thread_id)
user.unvote(thread)
return JsonResponse(utils.safe_content(thread.to_dict()))
@require_POST
@login_required
......@@ -288,7 +287,7 @@ def update_moderator_status(request, course_id, user_id):
course = get_course_with_access(request.user, course_id, 'load')
discussion_user = cc.User(id=user_id, course_id=course_id)
context = {
'course': course,
'course': course,
'course_id': course_id,
'user': request.user,
'django_user': user,
......@@ -327,7 +326,7 @@ def tags_autocomplete(request, course_id):
@require_POST
@login_required
@csrf.csrf_exempt
def upload(request, course_id):#ajax upload file to a question or answer
def upload(request, course_id):#ajax upload file to a question or answer
"""view that handles file upload via Ajax
"""
......@@ -337,7 +336,7 @@ def upload(request, course_id):#ajax upload file to a question or answer
new_file_name = ''
try:
# TODO authorization
#may raise exceptions.PermissionDenied
#may raise exceptions.PermissionDenied
#if request.user.is_anonymous():
# msg = _('Sorry, anonymous users cannot upload files')
# raise exceptions.PermissionDenied(msg)
......@@ -357,7 +356,7 @@ def upload(request, course_id):#ajax upload file to a question or answer
new_file_name = str(
time.time()
).replace(
'.',
'.',
str(random.randint(0,100000))
) + file_extension
......@@ -386,7 +385,7 @@ def upload(request, course_id):#ajax upload file to a question or answer
parsed_url = urlparse.urlparse(file_url)
file_url = urlparse.urlunparse(
urlparse.ParseResult(
parsed_url.scheme,
parsed_url.scheme,
parsed_url.netloc,
parsed_url.path,
'', '', ''
......
......@@ -136,10 +136,16 @@ def inline_discussion(request, course_id, discussion_id):
# html = render_inline_discussion(request, course_id, threads, discussion_id=discussion_id, \
# query_params=query_params)
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), {})
return utils.JsonResponse({
# 'html': html,
'discussion_data': map(utils.safe_content, threads),
'user_info': user_info,
'annotated_content_info': annotated_content_info
})
def render_search_bar(request, course_id, discussion_id=None, text=''):
......
......@@ -100,24 +100,24 @@ def initialize_discussion_info(course):
unexpanded_category_map[category].append({"title": title, "id": id,
"sort_key": sort_key})
category_map = {"entries": defaultdict(dict), "subcategories": defaultdict(dict)}
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("/")]
for level in path[:-1]:
if level not in node:
node[level] = {"subcategories": defaultdict(dict),
node[level] = {"subcategories": defaultdict(dict),
"entries": defaultdict(dict),
"sort_key": level}
"sort_key": level}
node = node[level]["subcategories"]
level = path[-1]
if level not in node:
node[level] = {"subcategories": defaultdict(dict),
"entries": defaultdict(dict),
node[level] = {"subcategories": defaultdict(dict),
"entries": defaultdict(dict),
"sort_key": level}
for entry in entries:
node[level]["entries"][entry["title"]] = {"id": entry["id"],
node[level]["entries"][entry["title"]] = {"id": entry["id"],
"sort_key": entry["sort_key"]}
for topic, entry in course.metadata.get('discussion_topics', {}).items():
......@@ -134,7 +134,7 @@ def initialize_discussion_info(course):
def get_courseware_context(content, course):
id_map = get_discussion_id_map(course)
id = content['commentable_id']
id = content['commentable_id']
content_info = None
if id in id_map:
location = id_map[id]["location"].url()
......@@ -149,21 +149,21 @@ 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=''):
super(HtmlResponse, self).__init__(html, content_type='text/plain')
class ViewNameMiddleware(object):
def process_view(self, request, view_func, view_args, view_kwargs):
class ViewNameMiddleware(object):
def process_view(self, request, view_func, view_args, view_kwargs):
request.view_name = view_func.__name__
class QueryCountDebugMiddleware(object):
......
......@@ -12,6 +12,7 @@ class Thread(models.Model):
'created_at', 'updated_at', 'comments_count',
'at_position_list', 'children', 'type',
'highlighted_title', 'highlighted_body',
'endorsed'
]
updatable_fields = [
......
......@@ -7,7 +7,6 @@ if Backbone?
item.discussion = @
@comparator = @sortByDateRecentFirst
@on "thread:remove", (thread) =>
console.log "remove triggered"
@remove(thread)
find: (id) ->
......@@ -24,8 +23,8 @@ if Backbone?
sortByDateRecentFirst: (thread) ->
-(new Date(thread.get("created_at")).getTime())
#return String.fromCharCode.apply(String,
# _.map(thread.get("created_at").split(""),
#return String.fromCharCode.apply(String,
# _.map(thread.get("created_at").split(""),
# ((c) -> return 0xffff - c.charChodeAt()))
#)
......@@ -134,7 +133,7 @@ if Backbone?
@$(".discussion-submit-post").click $.proxy(@submitNewPost, @)
@$(".discussion-cancel-post").click $.proxy(@cancelNewPost, @)
@$el.children(".blank").hide()
@$(".new-post-form").show()
......@@ -177,7 +176,7 @@ if Backbone?
threadView = new ThreadView el: $thread[0], model: thread
thread.updateInfo response.annotated_content_info
@cancelNewPost()
cancelNewPost: (event) ->
if @$el.hasClass("inline-discussion")
......
......@@ -2,6 +2,18 @@ if Backbone?
class @DiscussionModuleView extends Backbone.View
events:
"click .discussion-show": "toggleDiscussion"
"click .new-post-btn": "toggleNewPost"
"click .new-post-cancel": "hideNewPost"
initialize: ->
toggleNewPost: (event) ->
if @newPostForm.is(':hidden')
@newPostForm.slideDown(300)
else
@newPostForm.slideUp(300)
hideNewPost: (event) ->
@newPostForm.slideUp(300)
toggleDiscussion: (event) ->
if @showed
@$("section.discussion").hide()
......@@ -14,26 +26,39 @@ if Backbone?
@showed = true
else
$elem = $(event.target)
discussion_id = $elem.attr("discussion_id")
url = DiscussionUtil.urlFor 'retrieve_discussion', discussion_id
discussionId = $elem.data("discussion-id")
url = DiscussionUtil.urlFor 'retrieve_discussion', discussionId
DiscussionUtil.safeAjax
$elem: $elem
$loading: $elem
url: url
type: "GET"
dataType: 'json'
success: (response, textStatus) =>
#@$el.append(response.html)
window.user = new DiscussionUser(response.user_info)
$(event.target).html("Hide Discussion")
discussion = new Discussion()
discussion.reset(response.discussion_data, {silent: false})
$discussion = $(Mustache.render $("script#_inline_discussion").html(), {'threads':response.discussion_data})
$(".discussion-module").append($discussion)
discussion.each (thread) ->
element = $("article#thread_#{thread.id}")
dtv = new DiscussionThreadInlineView el: element, model: thread
dtv.render()
DiscussionUtil.bulkUpdateContentInfo(window.$$annotated_content_info)
@retrieved = true
@showed = true
success: (response, textStatus, jqXHR) => @createDiscussion(event, response, textStatus, discussionId)
createDiscussion: (event, response, textStatus, discussionId) =>
window.user = new DiscussionUser(response.user_info)
Content.loadContentInfos(response.annotated_content_info)
$(event.target).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})
$(".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
addThread: (thread, collection, options) =>
# TODO: When doing pagination, this will need to repaginate
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
......@@ -3,7 +3,7 @@ $ ->
window.$$contents = {}
$.fn.extend
loading: ->
@$_loading = $("<span class='discussion-loading'></span>")
@$_loading = $("<div class='loading-animation'></div>")
$(this).after(@$_loading)
loaded: ->
@$_loading.remove()
......@@ -107,6 +107,9 @@ class @DiscussionUtil
[event, selector] = eventSelector.split(' ')
$local(selector).unbind(event)[event] handler
@processTag: (text) ->
text.toLowerCase()
@tagsInputOptions: ->
autocomplete_url: @urlFor('tags_autocomplete')
autocomplete:
......@@ -116,6 +119,7 @@ class @DiscussionUtil
width: '100%'
defaultText: "Tag your post: press enter after each tag"
removeWithBackspace: true
preprocessTag: @processTag
@formErrorHandler: (errorsField) ->
(xhr, textStatus, error) ->
......@@ -123,7 +127,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()
......
......@@ -21,10 +21,6 @@ class @DiscussionThreadInlineView extends DiscussionContentView
@model.on "change", @updateModelDetails
render: ->
#TODO: Debugging, remove when done
if not window.$disc
window.$disc = []
window.$disc.push(@)
if not @model.has('abbreviatedBody')
@abbreviateBody()
@$el.html(Mustache.render(@template(), $.extend(@model.toJSON(),{expanded: @expanded}) ))
......@@ -38,8 +34,6 @@ class @DiscussionThreadInlineView extends DiscussionContentView
if @expanded
@makeWmdEditor "reply-body"
@renderResponses()
# @highlight @$(".post-body")
# @highlight @$("h1")
@
renderDogear: ->
......@@ -58,12 +52,13 @@ class @DiscussionThreadInlineView extends DiscussionContentView
convertMath: ->
element = @$(".post-body")
element.html DiscussionUtil.postMathJaxProcessor(element.html())
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'])
......@@ -192,16 +187,14 @@ class @DiscussionThreadInlineView extends DiscussionContentView
success: (response, textStatus) =>
@model.set('endorsed', not endorsed)
highlight: (el) ->
el.html(el.html().replace(/&lt;mark&gt;/g, "<mark>").replace(/&lt;\/mark&gt;/g, "</mark>"))
abbreviateBody: ->
abbreviated = DiscussionUtil.abbreviateString @model.get('body'), 140 # Because twitter
abbreviated = DiscussionUtil.abbreviateString @model.get('body'), 140
@model.set('abbreviatedBody', abbreviated)
expandPost: (event) ->
@expanded = true
@$el.find('.post-body').html(@model.get('body'))
@convertMath()
@$el.find('.expand-post').hide()
@$el.find('.collapse-post').show()
@$el.find('.post-extended-content').show()
......@@ -212,6 +205,7 @@ class @DiscussionThreadInlineView extends DiscussionContentView
collapsePost: (event) ->
@expanded = false
@$el.find('.post-body').html(@model.get('abbreviatedBody'))
@convertMath()
@$el.find('.collapse-post').hide()
@$el.find('.post-extended-content').hide()
@$el.find('.expand-post').show()
......@@ -15,6 +15,7 @@ class @DiscussionThreadListView extends Backbone.View
@collection.on "add", @addAndSelectThread
@sidebar_padding = 10
@sidebar_header_height = 87
@boardName
reloadDisplayedCollection: (thread) =>
thread_id = thread.get('id')
......@@ -41,8 +42,8 @@ class @DiscussionThreadListView extends Backbone.View
windowHeight = $(window).height();
discussionBody = $(".discussion-article")
discussionsBodyTop = if discussionBody[0] then discussionBody.offset().top;
discussionsBodyBottom = discussionsBodyTop + discussionBody.outerHeight();
discussionsBodyTop = if discussionBody[0] then discussionBody.offset().top
discussionsBodyBottom = discussionsBodyTop + discussionBody.outerHeight()
sidebar = $(".sidebar")
if scrollTop > discussionsBodyTop - @sidebar_padding
......@@ -62,10 +63,11 @@ class @DiscussionThreadListView extends Backbone.View
amount = Math.max(topOffset - discussionBottomOffset, 0)
sidebarHeight = sidebarHeight - @sidebar_padding - amount
sidebar.css 'height', Math.min(Math.max(sidebarHeight, 400), discussionBody.outerHeight())
sidebarHeight = Math.min(Math.max(sidebarHeight, 400), discussionBody.outerHeight())
sidebar.css 'height', sidebarHeight
postListWrapper = @$('.post-list-wrapper')
postListWrapper.css('height', (sidebarHeight - @sidebar_header_height - 4) + 'px');
postListWrapper.css('height', (sidebarHeight - @sidebar_header_height - 4) + 'px')
# Because we want the behavior that when the body is clicked the menu is
......@@ -101,6 +103,8 @@ class @DiscussionThreadListView extends Backbone.View
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)
......@@ -137,28 +141,66 @@ class @DiscussionThreadListView extends Backbone.View
@$(".browse").toggleClass('is-dropped')
if @$(".browse").hasClass('is-dropped')
@$(".browse-topic-drop-menu-wrapper").show()
$('body').bind 'click', @toggleTopicDrop
$('body').bind 'keydown', @setActiveItem
$(".browse-topic-drop-search-input").focus()
$("body").bind "click", @toggleTopicDrop
$("body").bind "keydown", @setActiveItem
else
@$(".browse-topic-drop-menu-wrapper").hide()
$('body').unbind 'click', @toggleTopicDrop
$('body').unbind 'keydown', @setActiveItem
$("body").unbind "click", @toggleTopicDrop
$("body").unbind "keydown", @setActiveItem
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(boardName)
@$(".current-board").html(@fitName(boardName))
fontSize = 16
@$(".current-board").css('font-size', '16px')
while @$(".current-board").width() > (@$el.width() * .8) - 40
fontSize--
if fontSize < 11
break
@$(".current-board").css('font-size', fontSize + 'px')
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) ->
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) ->
@setTopic(event)
item = $(event.target).closest('li')
......
......@@ -49,7 +49,7 @@ class @DiscussionThreadView extends DiscussionContentView
convertMath: ->
element = @$(".post-body")
element.html DiscussionUtil.postMathJaxProcessor(element.html())
element.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight element.html()
MathJax.Hub.Queue ["Typeset", MathJax.Hub, element[0]]
renderResponses: ->
......@@ -66,12 +66,17 @@ class @DiscussionThreadView extends DiscussionContentView
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)
addComment: =>
@model.comment()
endorseThread: (endorsed) =>
is_endorsed = @$el.find(".is-endorsed").length
@model.set 'endorsed', is_endorsed
toggleVote: (event) ->
event.preventDefault()
if window.user.voted(@model)
......
class @NewPostInlineView extends Backbone.View
initialize: () ->
@topicId = @$(".topic").first().data("discussion-id")
@maxNameWidth = 100
DiscussionUtil.makeWmdEditor @$el, $.proxy(@$, @), "new-post-body"
@$(".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()
tags = @$(".new-post-tags").val()
anonymous = false || @$("input.discussion-anonymous").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
tags: tags
anonymous: anonymous
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("")
@collection.add thread
......@@ -38,9 +38,10 @@ class @NewPostView extends Backbone.View
@menuOpen = true
@dropdownButton.addClass('dropped')
@topicMenu.show()
$(".form-topic-drop-search-input").focus()
$('body').bind 'keydown', @setActiveItem
$('body').bind 'click', @hideTopicDropdown
$("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
......@@ -52,8 +53,8 @@ class @NewPostView extends Backbone.View
@dropdownButton.removeClass('dropped')
@topicMenu.hide()
$('body').unbind 'keydown', @setActiveItem
$('body').unbind 'click', @hideTopicDropdown
$("body").unbind "keydown", @setActiveItem
$("body").unbind "click", @hideTopicDropdown
setTopic: (event) ->
$target = $(event.target)
......@@ -142,7 +143,7 @@ class @NewPostView extends Backbone.View
DiscussionUtil.clearFormErrors(@$(".new-post-form-errors"))
@$el.hide()
@$(".new-post-title").val("").attr("prev-text", "")
@$(".new-post-body").val("").attr("prev-text", "")
@$(".new-post-body textarea").val("").attr("prev-text", "")
@$(".new-post-tags").val("")
@$(".new-post-tags").importTags("")
@collection.add thread
......@@ -168,7 +169,7 @@ class @NewPostView extends Backbone.View
itemTop = $(items[index]).parent().offset().top
scrollTop = $(".topic_menu").scrollTop()
itemFromTop = $(".topic_menu").offset().top - itemTop
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)
......@@ -2,6 +2,7 @@ class @ResponseCommentView extends DiscussionContentView
tagName: "li"
template: _.template($("#response-comment-template").html())
initLocal: ->
# TODO .response-local is the parent of the comments so @$local is null, not sure what was intended here...
@$local = @$el.find(".response-local")
@$delegateElement = @$local
......@@ -14,8 +15,9 @@ class @ResponseCommentView extends DiscussionContentView
@convertMath()
@
convertMath: ->
body = @$(".response-body")
body = @$el.find(".response-body")
body.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight body.html()
# This removes paragraphs so that comments are more compact
body.children("p").each (index, elem) ->
$(elem).replaceWith($(elem).html())
MathJax.Hub.Queue ["Typeset", MathJax.Hub, body[0]]
......@@ -109,6 +109,7 @@ class @ThreadResponseView extends DiscussionContentView
endorsed = @model.get('endorsed')
data = { endorsed: not endorsed }
@model.set('endorsed', not endorsed)
@trigger "comment:endorse", not endorsed
DiscussionUtil.safeAjax
$elem: $elem
url: url
......
......@@ -58,7 +58,7 @@ $(document).ready(function() {
$('.new-post-btn').bind('click', newPost);
$('.new-post-cancel').bind('click', closeNewPost);
$('[data-tooltip]').bind({
$body.delegate('[data-tooltip]', {
'mouseover': showTooltip,
'mousemove': moveTooltip,
'mouseout': hideTooltip,
......@@ -66,14 +66,6 @@ $(document).ready(function() {
});
$body.delegate('.browse-topic-drop-search-input, .form-topic-drop-search-input', 'keyup', filterDrop);
// $(window).bind('resize', updateSidebar);
// $(window).bind('scroll', updateSidebar);
// $('.discussion-column').bind("input", function (e) {
// console.log("resized");
// updateSidebar();
// })
// updateSidebar();
});
function filterDrop(e) {
......@@ -276,6 +268,7 @@ function setTopic(e) {
function newPost(e) {
$newPost.slideDown(300);
$('.new-post-title').focus();
}
function closeNewPost(e) {
......
/*
jQuery Tags Input Plugin 1.3.3
Copyright (c) 2011 XOXCO, Inc
Documentation for this plugin lives here:
http://xoxco.com/clickable/jquery-tags-input
Licensed under the MIT license:
http://www.opensource.org/licenses/mit-license.php
......@@ -24,9 +24,9 @@
val = '',
input = $(this),
testSubject = $('#'+$(this).data('tester_id'));
if (val === (val = input.val())) {return;}
// Enter new content into testSubject
var escaped = val.replace(/&/g, '&amp;').replace(/\s/g,' ').replace(/</g, '&lt;').replace(/>/g, '&gt;');
testSubject.html(escaped);
......@@ -36,7 +36,7 @@
currentWidth = input.width(),
isValidWidthChange = (newWidth < currentWidth && newWidth >= minWidth)
|| (newWidth > minWidth && newWidth < maxWidth);
// Animate width
if (isValidWidthChange) {
input.width(newWidth);
......@@ -72,19 +72,24 @@
input.data('tester_id', testerId);
input.css('width', minWidth);
};
$.fn.addTag = function(value,options) {
options = jQuery.extend({focus:false,callback:true},options);
this.each(function() {
this.each(function() {
var id = $(this).attr('id');
var tagslist = $(this).val().split(delimiter[id]);
if (tagslist[0] == '') {
if (tagslist[0] == '') {
tagslist = new Array();
}
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) {
......@@ -92,10 +97,10 @@
$('#'+id+'_tag').addClass('not_valid');
}
} else {
var skipTag = false;
var skipTag = false;
}
if (value !='' && skipTag != true) {
if (value !='' && skipTag != true) {
$('<span>').addClass('tag').append(
$('<span>').text(value).append('&nbsp;&nbsp;'),
$('<a>', {
......@@ -108,16 +113,16 @@
).insertBefore('#' + id + '_addTag');
tagslist.push(value);
$('#'+id+'_tag').val('');
if (options.focus) {
$('#'+id+'_tag').focus();
} else {
} else {
$('#'+id+'_tag').blur();
}
$.fn.tagsInput.updateTagsField(this,tagslist);
if (options.callback && tags_callbacks[id] && tags_callbacks[id]['onAddTag']) {
var f = tags_callbacks[id]['onAddTag'];
f.call(this, value);
......@@ -127,29 +132,29 @@
var i = tagslist.length;
var f = tags_callbacks[id]['onChange'];
f.call(this, $(this), tagslist[i-1]);
}
}
}
});
});
return false;
};
$.fn.removeTag = function(value) {
$.fn.removeTag = function(value) {
value = unescape(value);
this.each(function() {
this.each(function() {
var id = $(this).attr('id');
var old = $(this).val().split(delimiter[id]);
$('#'+id+'_tagsinput .tag').remove();
str = '';
for (i=0; i< old.length; i++) {
if (old[i]!=value) {
for (i=0; i< old.length; i++) {
if (old[i]!=value) {
str = str + delimiter[id] +old[i];
}
}
$.fn.tagsInput.importTags(this,str);
if (tags_callbacks[id] && tags_callbacks[id]['onRemoveTag']) {
......@@ -157,24 +162,24 @@
f.call(this, value);
}
});
return false;
};
$.fn.tagExist = function(val) {
var id = $(this).attr('id');
var tagslist = $(this).val().split(delimiter[id]);
return (jQuery.inArray(val, tagslist) >= 0); //true when tag exists, false when not
};
// clear all existing tags and import new ones from a string
$.fn.importTags = function(str) {
id = $(this).attr('id');
$('#'+id+'_tagsinput .tag').remove();
$.fn.tagsInput.importTags(this,str);
}
$.fn.tagsInput = function(options) {
$.fn.tagsInput = function(options) {
var settings = jQuery.extend({
interactive:true,
defaultText:'add a tag',
......@@ -192,15 +197,15 @@
inputPadding: 6*2
},options);
this.each(function() {
if (settings.hide) {
$(this).hide();
this.each(function() {
if (settings.hide) {
$(this).hide();
}
var id = $(this).attr('id');
if (!id || delimiter[$(this).attr('id')]) {
id = $(this).attr('id', 'tags' + new Date().getTime()).attr('id');
}
var data = jQuery.extend({
pid:id,
real_input: '#'+id,
......@@ -208,57 +213,58 @@
input_wrapper: '#'+id+'_addTag',
fake_input: '#'+id+'_tag'
},settings);
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">';
if (settings.interactive) {
markup = markup + '<input id="'+id+'_tag" value="" data-default="'+settings.defaultText+'" />';
}
markup = markup + '</div><div class="tags_clear"></div></div>';
$(markup).insertAfter(this);
$(data.holder).css('width',settings.width);
$(data.holder).css('min-height',settings.height);
$(data.holder).css('height','100%');
if ($(data.real_input).val()!='') {
if ($(data.real_input).val()!='') {
$.fn.tagsInput.importTags($(data.real_input),$(data.real_input).val());
}
if (settings.interactive) {
}
if (settings.interactive) {
$(data.fake_input).val($(data.fake_input).attr('data-default'));
$(data.fake_input).css('color',settings.placeholderColor);
$(data.fake_input).resetAutosize(settings);
$(data.fake_input).doAutosize(settings);
$(data.holder).bind('click',data,function(event) {
$(event.data.fake_input).focus();
});
$(data.fake_input).bind('focus',data,function(event) {
if ($(event.data.fake_input).val()==$(event.data.fake_input).attr('data-default')) {
if ($(event.data.fake_input).val()==$(event.data.fake_input).attr('data-default')) {
$(event.data.fake_input).val('');
}
$(event.data.fake_input).css('color','#000000');
$(event.data.fake_input).css('color','#000000');
});
if (settings.autocomplete_url != undefined) {
autocomplete_options = {source: settings.autocomplete_url};
for (attrname in settings.autocomplete) {
autocomplete_options[attrname] = settings.autocomplete[attrname];
for (attrname in settings.autocomplete) {
autocomplete_options[attrname] = settings.autocomplete[attrname];
}
if (jQuery.Autocompleter !== undefined) {
onSelectCallback = settings.autocomplete.onItemSelect;
settings.autocomplete.onItemSelect = function() {
......@@ -278,18 +284,18 @@
$(data.fake_input).autocomplete(autocomplete_options);
$(data.fake_input).bind('autocompleteselect',data,function(event,ui) {
$(event.data.real_input).addTag(ui.item.value,{focus:true,unique:(settings.unique)});
return false;
});
}
} else {
// if a user tabs out of the field, create a new tag
// this is only available if autocomplete is not used.
$(data.fake_input).bind('blur',data,function(event) {
$(data.fake_input).bind('blur',data,function(event) {
var d = $(this).attr('data-default');
if ($(event.data.fake_input).val()!='' && $(event.data.fake_input).val()!=d) {
if ($(event.data.fake_input).val()!='' && $(event.data.fake_input).val()!=d) {
if( (event.data.minChars <= $(event.data.fake_input).val().length) && (!event.data.maxChars || (event.data.maxChars >= $(event.data.fake_input).val().length)) )
$(event.data.real_input).addTag($(event.data.fake_input).val(),{focus:true,unique:(settings.unique)});
} else {
......@@ -298,7 +304,7 @@
}
return false;
});
}
// if user types a comma, create a new tag
$(data.fake_input).bind('keypress',data,function(event) {
......@@ -326,7 +332,7 @@
}
});
$(data.fake_input).blur();
//Removes the not_valid class when user changes the value of the fake input
if(data.unique) {
$(data.fake_input).keydown(function(event){
......@@ -337,21 +343,21 @@
}
} // if settings.interactive
});
return this;
};
$.fn.tagsInput.updateTagsField = function(obj,tagslist) {
$.fn.tagsInput.updateTagsField = function(obj,tagslist) {
var id = $(obj).attr('id');
$(obj).val(tagslist.join(delimiter[id]));
};
$.fn.tagsInput.importTags = function(obj,val) {
$.fn.tagsInput.importTags = function(obj,val) {
$(obj).val('');
var id = $(obj).attr('id');
var tags = val.split(delimiter[id]);
for (i=0; i<tags.length; i++) {
for (i=0; i<tags.length; i++) {
$(obj).addTag(tags[i],{focus:false,callback:false});
}
if(tags_callbacks[id] && tags_callbacks[id]['onChange'])
......
......@@ -211,6 +211,28 @@
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;
......@@ -393,6 +415,22 @@ body.discussion {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3) inset;
}
.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;
}
}
.new-post-body .wmd-preview {
@include discussion-wmd-preview;
position: relative;
......@@ -713,7 +751,9 @@ body.discussion {
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;
......@@ -906,7 +946,7 @@ body.discussion {
position: relative;
display: block;
height: 36px;
padding: 0 10px;
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));
......@@ -917,6 +957,35 @@ body.discussion {
background-color: #eee;
}
&.staff-post.staff-response {
.staff-post-icon {
top: 5px;
}
.staff-response-icon {
top: 18px;
}
}
.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;
font-weight: 700;
......@@ -958,6 +1027,14 @@ body.discussion {
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);
......@@ -1363,38 +1440,6 @@ body.discussion {
.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);
}
}
.main-article.new {
display: none;
padding: 50px;
......@@ -1472,14 +1517,10 @@ body.discussion {
}
.discussion-module {
@extend .discussion-body
}
@extend .discussion-body;
/* For some reason I have to do this to get the SCSS to compile, can't stick it under the above .discussion-module */
.discussion-module {
section.discussion {
/* Course content p has a default margin-bottom of 1.416, this is just to reset that */
/* Course content p has a default margin-bottom of 1.416em, this is just to reset that */
.discussion-thread {
padding: 0.5em;
......@@ -1535,5 +1576,161 @@ body.discussion {
}
}
.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;
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;
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;
}
.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;
}
.options {
margin-top: 40px;
label {
display: inline;
margin-left: 8px;
font-size: 15px;
color: #fff;
text-shadow: none;
}
}
.wmd-button {
background: none;
}
.wmd-button span {
background: url(../images/new-post-icons-full.png) no-repeat;
}
}
.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;
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;
}
}
......@@ -102,3 +102,32 @@ 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);
}
}
\ No newline at end of file
<%include file="_underscore_templates.html" />
<div class="discussion-module">
<a class="discussion-show control-button" href="javascript:void(0)" discussion_id="${discussion_id | h}">Show Discussion</a>
<a class="discussion-show control-button" href="javascript:void(0)" data-discussion-id="${discussion_id | h}">Show Discussion</a>
</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>
<input type="checkbox" name="anonymous" class="discussion-anonymous" id="new-post-anonymous"><label for="new-post-anonymous">post anonymously</label>
</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>
<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>
......@@ -22,9 +22,7 @@
</%def>
<article class="new-post-article">
<div class="inner-wrapper">
<div class="new-post-form-errors">
</div>
<div class="inner-wrapper">
<form class="new-post-form">
<div class="left-column">
<label>Create new post about:</label>
......@@ -46,6 +44,7 @@
</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>
......
......@@ -27,7 +27,7 @@
<div class="discussion-body">
<div class="sidebar"></div>
<div class="discussion-column">
<div class="blank-slate">
<div class="discussion-article blank-slate">
<h1>${course.title} discussions</h1>
</div>
......
<section class="discussion">
{{#threads}}
<article class="discussion-thread" id="thread_{{id}}">
<section class="discussion" data-discussion-id="{{discussionId}}">
<a href="#" class="new-post-btn"><span class="new-post-icon"></span>New Post</a>
<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="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>
<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>
<input type="checkbox" name="anonymous" class="discussion-anonymous" id="new-post-anonymous"><label for="new-post-anonymous">post anonymously</label>
</div>
</div>
</form>
</div>
</article>
{{/threads}}
<section class="threads">
{{#threads}}
<article class="discussion-thread" id="thread_{{id}}">
</article>
{{/threads}}
</section>
</section>
<article class="discussion-article" data-id="{{id}}">
<div class="local"><a href="#" class="dogear action-follow"></a></div>
<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"><span class="plus-icon">+</span> <span class='votes-count-number'>{{votes.up_count}}</span></a>
<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>
<h1>{{title}}</h1>
<p class="posted-details">
<span class="timeago" title="{{created_at}}">sometime</span> by
<span class="timeago" title="{{created_at}}">{{created_at}}</span> by
<a href="{{user_url}}">{{username}}</a>
<span class="post-status-closed top-post-status" style="display: none">
&bull; This thread is closed.
</span>
</p>
<div class="local post-tools">
<a href="javascript:void(0)" class="expand-post">Expand...</a>
<a href="javascript:void(0)" class="collapse-post">Collapse...</a>
</div>
</header>
<div class="post-body">
{{abbreviatedBody}}
</div>
<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>
<ol class="responses post-extended-content">
<li class="loading"><div class="loading-animation"></div></li>
......
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