diff options
24 files changed, 622 insertions, 339 deletions
@@ -20,6 +20,7 @@ lint env /static django +tinymce lamson django/* nbproject diff --git a/askbot/__init__.py b/askbot/__init__.py index cf850e33..024c821d 100644 --- a/askbot/__init__.py +++ b/askbot/__init__.py @@ -33,7 +33,7 @@ REQUIREMENTS = { 'openid': 'python-openid', 'pystache': 'pystache==0.3.1', 'pytz': 'pytz', - 'tinymce': 'django-tinymce', + 'tinymce': 'django-tinymce==1.5.1b2', 'longerusername': 'longerusername', 'bs4': 'beautifulsoup4' } diff --git a/askbot/forms.py b/askbot/forms.py index daa4bd25..2bd3635b 100644 --- a/askbot/forms.py +++ b/askbot/forms.py @@ -288,9 +288,11 @@ class EditorField(forms.CharField): self.user = user editor_attrs = kwargs.pop('editor_attrs', {}) + widget_attrs = kwargs.pop('attrs', {}) + widget_attrs.setdefault('id', 'editor') + super(EditorField, self).__init__(*args, **kwargs) self.required = True - widget_attrs = {'id': 'editor'} if askbot_settings.EDITOR_TYPE == 'markdown': self.widget = forms.Textarea(attrs=widget_attrs) elif askbot_settings.EDITOR_TYPE == 'tinymce': @@ -499,17 +501,18 @@ class SummaryField(forms.CharField): 'field is optional)' ) - class EditorForm(forms.Form): """form with one field - `editor` the field must be created dynamically, so it's added in the __init__() function""" - def __init__(self, user=None, editor_attrs=None): + def __init__(self, attrs=None, user=None, editor_attrs=None): super(EditorForm, self).__init__() editor_attrs = editor_attrs or {} self.fields['editor'] = EditorField( - user=user, editor_attrs=editor_attrs + attrs=attrs, + editor_attrs=editor_attrs, + user=user ) @@ -1683,3 +1686,14 @@ class BulkTagSubscriptionForm(forms.Form): self.fields['users'] = forms.ModelMultipleChoiceField(queryset=User.objects.all()) if askbot_settings.GROUPS_ENABLED: self.fields['groups'] = forms.ModelMultipleChoiceField(queryset=Group.objects.exclude_personal()) + +class GetCommentsForPostForm(forms.Form): + post_id = forms.IntegerField() + +class NewCommentForm(forms.Form): + comment = forms.CharField() + post_id = forms.IntegerField() + +class EditCommentForm(forms.Form): + comment = forms.CharField() + comment_id = forms.IntegerField() diff --git a/askbot/management/commands/askbot_add_test_content.py b/askbot/management/commands/askbot_add_test_content.py index 0efd8c1f..a09fb086 100644 --- a/askbot/management/commands/askbot_add_test_content.py +++ b/askbot/management/commands/askbot_add_test_content.py @@ -1,3 +1,4 @@ +import sys from askbot.conf import settings as askbot_settings from askbot.models import User from askbot.utils.console import choice_dialog @@ -17,7 +18,10 @@ NUM_COMMENTS = 20 # karma. This can be calculated dynamically - max of MIN_REP_TO_... settings INITIAL_REPUTATION = 500 -BAD_STUFF = "<script>alert('hohoho')</script>" +if '--nospam' in sys.argv: + BAD_STUFF = '' +else: + BAD_STUFF = "<script>alert('hohoho')</script>" # Defining template inputs. USERNAME_TEMPLATE = BAD_STUFF + "test_user_%s" @@ -47,8 +51,14 @@ ALERT_SETTINGS_KEYS = ( class Command(NoArgsCommand): option_list = NoArgsCommand.option_list + ( - make_option('--noinput', action='store_false', dest='interactive', default=True, - help='Do not prompt the user for input of any kind.'), + make_option( + '--noinput', action='store_false', dest='interactive', default=True, + help='Do not prompt the user for input of any kind.' + ), + make_option( + '--nospam', action='store_true', dest='nospam', default=False, + help='Do not add XSS snippets' + ) ) def save_alert_settings(self): diff --git a/askbot/media/js/post.js b/askbot/media/js/post.js index 94e230a2..bbc4e672 100644 --- a/askbot/media/js/post.js +++ b/askbot/media/js/post.js @@ -1411,50 +1411,113 @@ DeletePostLink.prototype.decorate = function(element){ this.setHandler(this.getDeleteHandler()); } -//constructor for the form +/** + * Form for editing and posting new comment + * supports 3 editors: markdown, tinymce and plain textarea. + * There is only one instance of this form in use on the question page. + * It can be attached to any comment on the page, or to a new blank + * comment. + */ var EditCommentForm = function(){ WrappedElement.call(this); this._comment = null; this._comment_widget = null; this._element = null; + this._editorReady = false; this._text = ''; - this._id = 'edit-comment-form'; }; inherits(EditCommentForm, WrappedElement); -EditCommentForm.prototype.getElement = function(){ - EditCommentForm.superClass_.getElement.call(this); - this._textarea.val(this._text); - return this._element; +EditCommentForm.prototype.setWaitingStatus = function(isWaiting) { + if (isWaiting === true) { + this._editor.getElement().hide(); + this._submit_btn.hide(); + this._cancel_btn.hide(); + } else { + this._editor.getElement().show(); + this._submit_btn.show(); + this._cancel_btn.show(); + } +}; + +EditCommentForm.prototype.startEditor = function() { + var editorId = 'comment-editor-' + getNewInt(); + var opts = { + mode: 'exact', + content_css: mediaUrl('media/style/tinymce/comments-content.css'), + elements: editorId, + plugins: 'autoresize', + theme_advanced_buttons1: 'bold, italic, |, link, |, numlist, bullist', + theme_advanced_buttons2: '', + theme_advanced_path: false, + plugins: '', + width: '100%', + height: '60px' + }; + var editor = new TinyMCE(opts); + editor.setId(editorId); + editor.setText(this._text); + //@todo: remove global variable maxCommentLength + this._editorBox.prepend(editor.getElement()); + editor.start(); + editor.focus(); + this._editor = editor; + + return; + + //todo: make this work for tinymce + var updateCounter = this.getCounterUpdater(); + var escapeHandler = makeKeyHandler(27, this.getCancelHandler()); + + //todo: try this on the div + editor.getElement().blur(updateCounter) + .focus(updateCounter) + .keyup(updateCounter) + .keyup(escapeHandler); + + if (askbot['settings']['saveCommentOnEnter']){ + var save_handler = makeKeyHandler(13, this.getSaveHandler()); + editor.getElement().keydown(save_handler); + } }; +/** + * attaches comment editor to a particular comment + */ EditCommentForm.prototype.attachTo = function(comment, mode){ this._comment = comment; - this._type = mode; + this._type = mode;//action: 'add' or 'edit' this._comment_widget = comment.getContainerWidget(); this._text = comment.getText(); comment.getElement().after(this.getElement()); comment.getElement().hide(); - this._comment_widget.hideButton(); + this._comment_widget.hideButton();//hide add comment button + //fix up the comment submit button, depending on the mode if (this._type == 'add'){ this._submit_btn.html(gettext('add comment')); } else { this._submit_btn.html(gettext('save comment')); } + //enable the editor this.getElement().show(); this.enableForm(); - this.focus(); - putCursorAtEnd(this._textarea); + this.startEditor(); + this._editor.setText(this._text); + //this._editor.focus(); + //this._editor.putCursorAtEnd(); + setupButtonEventHandlers(this._submit_btn, this.getSaveHandler()); + setupButtonEventHandlers(this._cancel_btn, this.getCancelHandler()); }; EditCommentForm.prototype.getCounterUpdater = function(){ //returns event handler var counter = this._text_counter; + var editor = this._editor; var handler = function(){ - var textarea = $(this); - var length = textarea.val() ? textarea.val().length : 0; + var length = editor.getText().length; var length1 = maxCommentLength - 100; + if (length1 < 0){ length1 = Math.round(0.7*maxCommentLength); } @@ -1463,38 +1526,49 @@ EditCommentForm.prototype.getCounterUpdater = function(){ length2 = Math.round(0.9*maxCommentLength); } - //todo: - //1) use class instead of color - move color def to css + /* todo make smooth color transition, from gray to red + * or rather - from start color to end color */ var color = 'maroon'; var chars = 10; if (length === 0){ - var feedback = interpolate(gettext('%s title minchars'), [chars]); - } - else if (length < 10){ - var feedback = interpolate(gettext('enter %s more characters'), [chars - length]); - } - else { - color = length > length2 ? "#f00" : length > length1 ? "#f60" : "#999" - chars = maxCommentLength - length - var feedback = interpolate(gettext('%s characters left'), [chars]) + var feedback = interpolate(gettext('enter at least %s characters'), [chars]); + } else if (length < 10){ + var feedback = interpolate(gettext('enter at least %s more characters'), [chars - length]); + } else { + if (length > length2) { + color = '#f00'; + } else if (length > length1) { + color = '#f60'; + } else { + color = '#999'; + } + chars = maxCommentLength - length; + var feedback = interpolate(gettext('%s characters left'), [chars]); } - counter.html(feedback).css('color', color) + counter.html(feedback); + counter.css('color', color); }; return handler; }; +/** + * @todo: clean up this method so it does just one thing + */ EditCommentForm.prototype.canCancel = function(){ if (this._element === null){ return true; } - var ctext = $.trim(this._textarea.val()); + if (this._editor === undefined) { + return true; + }; + var ctext = this._editor.getText(); if ($.trim(ctext) == $.trim(this._text)){ return true; } else if (this.confirmAbandon()){ return true; } - this.focus(); + this._editor.focus(); return false; }; @@ -1515,12 +1589,17 @@ EditCommentForm.prototype.detach = function(){ this._comment.getContainerWidget().showButton(); if (this._comment.isBlank()){ this._comment.dispose(); - } - else { + } else { this._comment.getElement().show(); } this.reset(); this._element = this._element.detach(); + + this._editor.dispose(); + this._editor = undefined; + + removeButtonEventHandlers(this._submit_btn); + removeButtonEventHandlers(this._cancel_btn); }; EditCommentForm.prototype.createDom = function(){ @@ -1528,46 +1607,23 @@ EditCommentForm.prototype.createDom = function(){ this._element.attr('class', 'post-comments'); var div = $('<div></div>'); - this._textarea = $('<textarea></textarea>'); - this._textarea.attr('id', this._id); - - /* - this._help_text = $('<span></span>').attr('class', 'help-text'); - this._help_text.html(gettext('Markdown is allowed in the comments')); - div.append(this._help_text); + this._element.append(div); - this._help_text = $('<div></div>').attr('class', 'clearfix'); - div.append(this._help_text); - */ + /** a stub container for the editor */ + this._editorBox = div; + /** + * editor itself will live at this._editor + * and will be initialized by the attachTo() + */ - this._element.append(div); - div.append(this._textarea); this._text_counter = $('<span></span>').attr('class', 'counter'); div.append(this._text_counter); + this._submit_btn = $('<button class="submit small"></button>'); div.append(this._submit_btn); this._cancel_btn = $('<button class="submit small"></button>'); this._cancel_btn.html(gettext('cancel')); div.append(this._cancel_btn); - - setupButtonEventHandlers(this._submit_btn, this.getSaveHandler()); - setupButtonEventHandlers(this._cancel_btn, this.getCancelHandler()); - - var update_counter = this.getCounterUpdater(); - var escape_handler = makeKeyHandler(27, this.getCancelHandler()); - this._textarea.attr('name', 'comment') - .attr('cols', 60) - .attr('rows', 5) - .attr('maxlength', maxCommentLength) - .blur(update_counter) - .focus(update_counter) - .keyup(update_counter) - .keyup(escape_handler); - if (askbot['settings']['saveCommentOnEnter']){ - var save_handler = makeKeyHandler(13, this.getSaveHandler()); - this._textarea.keydown(save_handler); - } - this._textarea.val(this._text); }; EditCommentForm.prototype.isEnabled = function() { @@ -1587,38 +1643,51 @@ EditCommentForm.prototype.disableForm = function() { EditCommentForm.prototype.reset = function(){ this._comment = null; this._text = ''; - this._textarea.val(''); + this._editor.setText(''); this.enableForm(); }; EditCommentForm.prototype.confirmAbandon = function(){ - this.focus(true); - this._textarea.addClass('highlight'); - var answer = confirm(gettext("Are you sure you don't want to post this comment?")); - this._textarea.removeClass('highlight'); + this._editor.focus(); + this._editor.getElement().scrollTop(); + this._editor.setHighlight(true); + var answer = confirm( + gettext("Are you sure you don't want to post this comment?") + ); + this._editor.setHighlight(false); return answer; }; -EditCommentForm.prototype.focus = function(hard){ - this._textarea.focus(); - if (hard === true){ - $(this._textarea).scrollTop(); - } -}; - EditCommentForm.prototype.getSaveHandler = function(){ var me = this; + var editor = this._editor; return function(){ if (me.isEnabled() === false) {//prevent double submits return false; } - var text = me._textarea.val(); + me.disableForm(); + + var text = editor.getText(); if (text.length < 10){ - me.focus(); + editor.focus(); return false; } + //display the comment and show that it is not yet saved + me.setWaitingStatus(true); + me._comment.getElement().show(); + var commentData = me._comment.getData(); + var timestamp = commentData['comment_added_at'] || gettext('just now'); + var userName = commentData['user_display_name'] || askbot['data']['userName']; + me._comment.setContent({ + 'html': text, + 'user_display_name': userName, + 'comment_added_at': timestamp + }); + me._comment.setDraftStatus(true); + me._comment.getContainerWidget().showButton(); + var post_data = { comment: text }; @@ -1633,8 +1702,6 @@ EditCommentForm.prototype.getSaveHandler = function(){ post_url = askbot['urls']['postComments']; } - me.disableForm(); - $.ajax({ type: "POST", url: post_url, @@ -1642,19 +1709,21 @@ EditCommentForm.prototype.getSaveHandler = function(){ data: post_data, success: function(json) { //type is 'edit' or 'add' + me._comment.setDraftStatus(false); if (me._type == 'add'){ me._comment.dispose(); me._comment.getContainerWidget().reRenderComments(json); - } - else { + } else { me._comment.setContent(json); - me._comment.getElement().show(); } + me.setWaitingStatus(false); me.detach(); }, error: function(xhr, textStatus, errorThrown) { me._comment.getElement().show(); showMessage(me._comment.getElement(), xhr.responseText, 'after'); + me._comment.setDraftStatus(false); + me.setWaitingStatus(false); me.detach(); me.enableForm(); } @@ -1663,9 +1732,6 @@ EditCommentForm.prototype.getSaveHandler = function(){ }; }; -//a single instance to reuse -var editCommentForm = new EditCommentForm(); - var Comment = function(widget, data){ WrappedElement.call(this); this._container_widget = widget; @@ -1675,6 +1741,7 @@ var Comment = function(widget, data){ this._is_convertible = askbot['data']['userIsAdminOrMod']; this.convert_link = null; this._delete_prompt = gettext('delete this comment'); + this._editorForm = undefined; if (data && data['is_deletable']){ this._deletable = data['is_deletable']; } @@ -1690,11 +1757,38 @@ var Comment = function(widget, data){ }; inherits(Comment, WrappedElement); +Comment.prototype.getData = function() { + return this._data; +}; + +Comment.prototype.startEditing = function() { + var form = this._editorForm || new EditCommentForm(); + this._editorForm = form; + // if new comment: + if (this.isBlank()) { + form.attachTo(this, 'add'); + } else { + form.attachTo(this, 'edit'); + } +}; + Comment.prototype.decorate = function(element){ this._element = $(element); var parent_type = this._element.parent().parent().attr('id').split('-')[2]; var comment_id = this._element.attr('id').replace('comment-',''); this._data = {id: comment_id}; + + var timestamp = this._element.find('abbr.timeago'); + this._data['comment_added_at'] = timestamp.attr('title'); + var userLink = this._element.find('a.author'); + this._data['user_display_name'] = userLink.html(); + // @todo: read other data + + var commentBody = this._element.find('.comment-body'); + if (commentBody.length > 0) { + this._comment_body = commentBody; + } + var delete_img = this._element.find('span.delete-icon'); if (delete_img.length > 0){ this._deletable = true; @@ -1716,12 +1810,31 @@ Comment.prototype.decorate = function(element){ this._convert_link.decorate(convert_link); } + var deleter = this._element.find('.comment-delete'); + if (deleter.length > 0) { + this._comment_delete = deleter; + }; + var vote = new CommentVoteButton(this); vote.decorate(this._element.find('.comment-votes .upvote')); this._blank = false; }; +Comment.prototype.setDraftStatus = function(isDraft) { + return; + //@todo: implement nice feedback about posting in progress + //maybe it should be an element that lasts at least a second + //to avoid the possible brief flash + if (isDraft === true) { + this._normalBackground = this._element.css('background'); + this._element.css('background', 'rgb(255, 243, 195)'); + } else { + this._element.css('background', this._normalBackground); + } +}; + + Comment.prototype.isBlank = function(){ return this._blank; }; @@ -1751,42 +1864,77 @@ Comment.prototype.getParentId = function(){ return this._container_widget.getPostId(); }; +/** + * this function is basically an "updateDom" + * for which we don't have the convention + */ Comment.prototype.setContent = function(data){ - this._data = data || this._data; - this._element.html(''); - this._element.attr('class', 'comment'); + this._data = $.extend(this._data, data); + this._element.addClass('comment'); this._element.attr('id', 'comment-' + this._data['id']); - var votes = this.makeElement('div'); - votes.addClass('comment-votes'); + // 1) create the votes element if it is not there + var votesBox = this._element.find('.comment-votes'); + if (votesBox.length === 0) { + votesBox = this.makeElement('div'); + votesBox.addClass('comment-votes'); + this._element.append(votesBox); - var vote = new CommentVoteButton(this); - if (this._data['upvoted_by_user']){ - vote.setVoted(true); + var vote = new CommentVoteButton(this); + if (this._data['upvoted_by_user']){ + vote.setVoted(true); + } + vote.setScore(this._data['score']); + var voteElement = vote.getElement(); + + votesBox.append(vote.getElement()); + } + + // 2) create the comment deleter if it is not there + if (this._comment_delete === undefined) { + this._comment_delete = $('<div class="comment-delete"></div>'); + if (this._deletable){ + this._delete_icon = new DeleteIcon(this._delete_prompt); + this._delete_icon.setHandler(this.getDeleteHandler()); + this._comment_delete.append(this._delete_icon.getElement()); + } + this._element.append(this._comment_delete); } - vote.setScore(this._data['score']); - votes.append(vote.getElement()); - - this._element.append(votes); - this._comment_delete = $('<div class="comment-delete"></div>'); - if (this._deletable){ - this._delete_icon = new DeleteIcon(this._delete_prompt); - this._delete_icon.setHandler(this.getDeleteHandler()); - this._comment_delete.append(this._delete_icon.getElement()); + // 3) create or replace the comment body + if (this._comment_body === undefined) { + this._comment_body = $('<div class="comment-body"></div>'); + this._element.append(this._comment_body); + } + if (askbot['settings']['editorType'] === 'tinymce') { + var theComment = $('<div/>'); + theComment.html(this._data['html']); + //sanitize, just in case + this._comment_body.empty(); + this._comment_body.append(theComment); + this._data['text'] = this._data['html']; + } else { + this._comment_body.empty(); + this._comment_body.html(this._data['html']); } - this._element.append(this._comment_delete); - - this._comment_body = $('<div class="comment-body"></div>'); - this._comment_body.html(this._data['html']); //this._comment_body.append(' – '); + // 4) create user link if absent + if (this._user_link !== undefined) { + this._user_link.detach(); + this._user_link = undefined; + } this._user_link = $('<a></a>').attr('class', 'author'); this._user_link.attr('href', this._data['user_url']); this._user_link.html(this._data['user_display_name']); this._comment_body.append(' '); this._comment_body.append(this._user_link); + // 5) create or update the timestamp + if (this._comment_added_at !== undefined) { + this._comment_added_at.detach(); + this._comment_added_at = undefined; + } this._comment_body.append(' ('); this._comment_added_at = $('<abbr class="timeago"></abbr>'); this._comment_added_at.html(this._data['comment_added_at']); @@ -1795,18 +1943,22 @@ Comment.prototype.setContent = function(data){ this._comment_body.append(this._comment_added_at); this._comment_body.append(')'); - if (this._editable){ + if (this._editable) { + if (this._edit_link !== undefined) { + this._edit_link.dispose(); + } this._edit_link = new EditLink(); this._edit_link.setHandler(this.getEditHandler()) this._comment_body.append(this._edit_link.getElement()); } - if (this._is_convertible){ + if (this._is_convertible) { + if (this._convert_link !== undefined) { + this._convert_link.dispose(); + } this._convert_link = new CommentConvertLink(this._data['id']); this._comment_body.append(this._convert_link.getElement()); } - this._element.append(this._comment_body); - this._blank = false; }; @@ -1873,20 +2025,12 @@ Comment.prototype.getText = function(){ } Comment.prototype.getEditHandler = function(){ - var comment = this; + var me = this; return function(){ - if (editCommentForm.canCancel()){ - editCommentForm.detach(); - if (comment.hasText()){ - editCommentForm.attachTo(comment, 'edit'); - } - else { - comment.loadText( - function(){ - editCommentForm.attachTo(comment, 'edit'); - } - ); - } + if (me.hasText()){ + me.startEditing(); + } else { + me.loadText(function(){ me.startEditing() }); } }; }; @@ -1979,28 +2123,65 @@ PostCommentsWidget.prototype.showButton = function(){ } PostCommentsWidget.prototype.startNewComment = function(){ - var comment = new Comment(this); + var opts = { + 'is_deletable': true, + 'is_editable': true + }; + var comment = new Comment(this, opts); this._cbox.append(comment.getElement()); - editCommentForm.attachTo(comment, 'add'); + comment.startEditing(); }; PostCommentsWidget.prototype.needToReload = function(){ return this._is_truncated; }; +PostCommentsWidget.prototype.userCanPost = function() { + var data = askbot['data']; + if (data['userIsAuthenticated']) { + //true if admin, post owner or high rep user + if (data['userIsAdminOrMod']) { + return true; + } else if (data['userReputation'] >= askbot['settings']['minRepToPostComment']) { + return true; + } else if (this.getPostId() in data['user_posts']) { + return true; + } + } + return false; +}; + PostCommentsWidget.prototype.getActivateHandler = function(){ var me = this; + var button = this._activate_button; return function() { - if (editCommentForm.canCancel()){ - editCommentForm.detach(); - if (me.needToReload()){ - me.reloadAllComments(function(json){ - me.reRenderComments(json); - me.startNewComment(); - }); - } - else { + if (me.needToReload()){ + me.reloadAllComments(function(json){ + me.reRenderComments(json); + //2) change button text to "post a comment" + button.html(gettext('post a comment')); + }); + } + else { + //if user can't post, we tell him something and refuse + if (me.userCanPost()) { me.startNewComment(); + } else { + if (askbot['data']['userIsAuthenticated']) { + var template = gettext( + 'You can always leave comments under your own posts.<br/>' + + 'However, to post comments anywhere, karma should be at least %s,<br/> ' + + 'and at the moment your karma is %s.<br/>' + ); + var context = [ + askbot['settings']['minRepToPostComment'], + askbot['data']['userReputation'] + ]; + var message = interpolate(template, context); + } else { + var message = gettext('please sign in or register to post comments'); + } + showMessage(button, message, 'after'); } } }; @@ -2170,15 +2351,81 @@ QASwapper.prototype.startSwapping = function(){ /** * @constructor + * a simple textarea-based editor */ -var WMD = function(){ +var SimpleEditor = function(attrs) { WrappedElement.call(this); + attrs = attrs || {}; + this._rows = attrs['rows'] || 10; + this._cols = attrs['cols'] || 60; + this._maxlength = attrs['maxlength'] || 1000; +}; +inherits(SimpleEditor, WrappedElement); + +SimpleEditor.prototype.focus = function() { + this._textarea.focus(); +}; + +SimpleEditor.prototype.putCursorAtEnd = function() { + putCursorAtEnd(this._textarea); +}; + +/** + * a noop function + */ +SimpleEditor.prototype.start = function() {}; + +SimpleEditor.prototype.setHighlight = function(isHighlighted) { + if (isHighlighted === true) { + this._textarea.addClass('highlight'); + } else { + this._textarea.removeClass('highlight'); + } +}; + +SimpleEditor.prototype.getText = function() { + return $.trim(this._textarea.val()); +}; + +SimpleEditor.prototype.setText = function(text) { + this._text = text; + if (this._textarea) { + this._textarea.val(text); + }; +}; + +/** + * a textarea inside a div, + * the reason for this is that we subclass this + * in WMD, and that one requires a more complex structure + */ +SimpleEditor.prototype.createDom = function() { + this._element = this.makeElement('div'); + var textarea = this.makeElement('textarea'); + this._element.append(textarea); + if (this._text) { + textarea.val(this._text); + }; + textarea.attr({ + 'cols': this._cols, + 'rows': this._rows, + 'maxlength': this._maxlength + }); +} + + +/** + * @constructor + * a wrapper for the WMD editor + */ +var WMD = function(){ + SimpleEditor.call(this); this._text = undefined; this._enabled_buttons = 'bold italic link blockquote code ' + 'image attachment ol ul heading hr'; this._is_previewer_enabled = true; }; -inherits(WMD, WrappedElement); +inherits(WMD, SimpleEditor); WMD.prototype.setEnabledButtons = function(buttons){ this._enabled_buttons = buttons; @@ -2210,8 +2457,8 @@ WMD.prototype.createDom = function(){ wmd_container.append(editor); this._textarea = editor; - if (this._markdown){ - editor.val(this._markdown); + if (this._text){ + editor.val(this._text); } var previewer = this.makeElement('div') @@ -2224,17 +2471,6 @@ WMD.prototype.createDom = function(){ } }; -WMD.prototype.setText = function(text){ - this._markdown = text; - if (this._textarea){ - this._textarea.val(text); - } -}; - -WMD.prototype.getText = function(){ - return this._textarea.val(); -}; - WMD.prototype.start = function(){ Attacklab.Util.startEditor(true, this._enabled_buttons); }; @@ -2245,60 +2481,75 @@ WMD.prototype.start = function(){ var TinyMCE = function(config) { WrappedElement.call(this); this._config = config || {}; + this._id = 'editor';//desired id of the textarea }; inherits(TinyMCE, WrappedElement); /* 3 dummy functions to match WMD api */ TinyMCE.prototype.setEnabledButtons = function() {}; + TinyMCE.prototype.start = function() { - this.loadEditor(); + //copy the options, because we need to modify them + var opts = $.extend({}, this._config); + var me = this; + var extraOpts = { + 'mode': 'exact', + 'elements': this._id, + }; + opts = $.extend(opts, extraOpts); + tinyMCE.init(opts); + $('.mceStatusbar').remove(); }; TinyMCE.prototype.setPreviewerEnabled = function() {}; +TinyMCE.prototype.setHighlight = function() {}; +TinyMCE.prototype.putCursorAtEnd = function() {}; + +TinyMCE.prototype.focus = function() { + tinymce.execCommand('mceFocus', false, this._id); + + //@todo: make this general to all editors + var winH = $(window).height(); + var winY = $(window).scrollTop(); + var edY = this._element.offset().top; + var edH = this._element.height(); + + //if editor bottom is below viewport + var isBelow = ((edY + edH) > (winY + winH)); + var isAbove = (edY < winY); + if (isBelow || isAbove) { + //then center on screen + $(window).scrollTop(edY - edH/2 - winY/2); + } + +}; + +TinyMCE.prototype.setId = function(id) { + this._id = id; +}; TinyMCE.prototype.setText = function(text) { this._text = text; + if (this.isLoaded()) { + tinymce.get(this._id).setContent(text); + } }; TinyMCE.prototype.getText = function() { return tinyMCE.activeEditor.getContent(); }; -TinyMCE.prototype.loadEditor = function() { - var config = JSON.stringify(this._config); - var data = {config: config}; - var editorBox = this._element; - var me = this; - $.ajax({ - async: false, - type: 'GET', - dataType: 'json', - cache: false, - url: askbot['urls']['getEditor'], - data: data, - success: function(data) { - if (data['success']) { - editorBox.html(data['html']); - editorBox.find('textarea').val(me._text);//@todo: fixme - $.each(data['scripts'], function(idx, scriptData) { - var scriptElement = me.makeElement('script'); - scriptElement.attr('type', 'text/javascript'); - if (scriptData['src']) { - scriptElement.attr('src', scriptData['src']); - } - if (scriptData['contents']) { - scriptElement.html(scriptData['contents']); - } - $('head').append(scriptElement); - }); - } - } - }); +TinyMCE.prototype.isLoaded = function() { + return (tinymce.get(this._id) !== undefined); }; TinyMCE.prototype.createDom = function() { var editorBox = this.makeElement('div'); editorBox.addClass('wmd-container'); this._element = editorBox; + var textarea = this.makeElement('textarea'); + textarea.attr('id', this._id); + textarea.addClass('editor'); + this._element.append(textarea); }; /** @@ -2461,12 +2712,10 @@ TagWikiEditor.prototype.decorate = function(element){ var editor = new WMD(); } else { var editor = new TinyMCE({//override defaults - mode: 'exact', - elements: 'editor', theme_advanced_buttons1: 'bold, italic, |, link, |, numlist, bullist', theme_advanced_buttons2: '', - plugins: '', - width: '200' + theme_advanced_path: false, + plugins: '' }); } if (this._enabled_editor_buttons){ diff --git a/askbot/media/js/utils.js b/askbot/media/js/utils.js index 07368194..a95096ae 100644 --- a/askbot/media/js/utils.js +++ b/askbot/media/js/utils.js @@ -29,6 +29,13 @@ var animateHashes = function(){ } }; +var getNewInt = function() { + var num = askbot['data']['uniqueInt'] || 0; + num = num + 1; + askbot['data']['uniqueInt'] = num; + return num; +}; + var getUniqueValues = function(values) { var uniques = new Object(); var out = new Array(); @@ -126,6 +133,14 @@ var setupButtonEventHandlers = function(button, callback){ button.click(callback); }; +var removeButtonEventHandlers = function(button) { + button.unbind('click'); + button.unbind('keydown'); +}; + +var decodeHtml = function(encodedText) { + return $('<div/>').html(encodedText).text(); +}; var putCursorAtEnd = function(element){ var el = $(element).get()[0]; @@ -352,7 +367,7 @@ WrappedElement.prototype.getElement = function(){ return this._element; }; WrappedElement.prototype.inDocument = function(){ - return this._in_document; + return (this._element && this._element.is(':hidden') === false); }; WrappedElement.prototype.enterDocument = function(){ return this._in_document = true; diff --git a/askbot/media/style/style.less b/askbot/media/style/style.less index a0813f1a..0510b398 100644 --- a/askbot/media/style/style.less +++ b/askbot/media/style/style.less @@ -143,6 +143,13 @@ html { visibility: hidden; } +.invisible { + margin: -1px 0 0 -1px; + height: 1px; + overflow: hidden; + width: 1px; +} + .badges a { color: #763333; text-decoration: underline; @@ -1777,6 +1784,10 @@ ul#related-tags li { width: 723px; width: 100%; } + .post-comments .wmd-container { + margin-bottom: 8px; + margin-left: -2px; + } #editor { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; @@ -1945,9 +1956,9 @@ ul#related-tags li { /* ----- Question template ----- */ -.question-page{ +.question-page { - h1{ + h1 { padding-top:0px; font-family:@main-font; @@ -2010,6 +2021,10 @@ ul#related-tags li { width:20px; vertical-align:top; } + .answer-table .mceEditor td, + #question-table .mceEditor td { + width: auto; + } .question-body, .answer-body { overflow: auto; margin-top:10px; @@ -2290,7 +2305,7 @@ ul#related-tags li { } button{ line-height:25px; - margin-bottom:5px; + margin: 0 10px 5px -2px; .button-style(27px, 12px); font-family:@body-font; font-weight:bold; @@ -2302,7 +2317,6 @@ ul#related-tags li { display: inline-block; width: 245px; float:right; - color:#b6a475 !important; vertical-align: top; font-family:@body-font; float:right; @@ -2312,14 +2326,11 @@ ul#related-tags li { border-bottom: 1px solid #edeeeb; clear:both; margin: 0; - margin-top:8px; padding-bottom:4px; overflow: auto; font-family:@body-font; font-size:11px; min-height: 25px; - background:#fff url(../images/comment-background.png) bottom repeat-x; - .rounded-corners(5px); } div.comment:hover { background-color: #efefef; @@ -3107,28 +3118,29 @@ ins .post-tag, ins p, ins { .vote-notification { z-index: 1; + background-color: #8e0000; + color: white; cursor: pointer; display: none; - position: absolute; font-family:@secondary-font; font-size:14px; font-weight:normal; - color: white; - background-color: #8e0000; - text-align: center; padding-bottom:10px; + position: absolute; + text-align: center; .box-shadow(0px, 2px, 4px, #370000); .rounded-corners(4px); h3{ background:url(../images/notification.png) repeat-x top; - padding:10px 10px 10px 10px; - font-size:13px; - margin-bottom:5px; - border-top:#8e0000 1px solid; - color:#fff; - font-weight:normal; - .rounded-corners-top(4px); + padding:10px 10px 10px 10px; + font-size:13px; + margin-bottom:5px; + border-top:#8e0000 1px solid; + color:#fff; + line-height: 20px; + font-weight:normal; + .rounded-corners-top(4px); } a { color: #fb7321; diff --git a/askbot/migrations/0032_auto__del_field_badgedata_multiple__del_field_badgedata_description__d.py b/askbot/migrations/0032_auto__del_field_badgedata_multiple__del_field_badgedata_description__d.py index 70ef2f8d..2c58d82a 100644 --- a/askbot/migrations/0032_auto__del_field_badgedata_multiple__del_field_badgedata_description__d.py +++ b/askbot/migrations/0032_auto__del_field_badgedata_multiple__del_field_badgedata_description__d.py @@ -25,8 +25,8 @@ class Migration(SchemaMigration): # Changing field 'BadgeData.slug' db.alter_column('askbot_badgedata', 'slug', self.gf('django.db.models.fields.SlugField')(unique=True, max_length=50)) - # Adding unique constraint on 'BadgeData', fields ['slug'] + return try:#work around the South 0.7.3 bug db.start_transaction() db.create_unique('askbot_badgedata', ['slug']) diff --git a/askbot/models/post.py b/askbot/models/post.py index 66004ce4..625d262b 100644 --- a/askbot/models/post.py +++ b/askbot/models/post.py @@ -1,7 +1,6 @@ from collections import defaultdict import datetime import operator -import cgi import logging from django.utils.html import strip_tags @@ -35,7 +34,7 @@ from askbot.models.tag import tags_match_some_wildcard from askbot.conf import settings as askbot_settings from askbot import exceptions from askbot.utils import markup -from askbot.utils.html import sanitize_html +from askbot.utils.html import sanitize_html, strip_tags from askbot.models.base import BaseQuerySetManager, DraftContent #todo: maybe merge askbot.utils.markup and forum.utils.html @@ -425,25 +424,24 @@ class Post(models.Model): if self.post_type in ('question', 'answer', 'tag_wiki', 'reject_reason'): _urlize = False _use_markdown = (askbot_settings.EDITOR_TYPE == 'markdown') - _escape_html = False #markdow does the escaping elif self.is_comment(): _urlize = True _use_markdown = (askbot_settings.EDITOR_TYPE == 'markdown') - _escape_html = True else: raise NotImplementedError text = self.text - if _escape_html: - text = cgi.escape(text) - if _urlize: text = html.urlize(text) if _use_markdown: text = sanitize_html(markup.get_parser().convert(text)) + if askbot_settings.EDITOR_TYPE == 'tinymce': + #todo: see what can be done with the "object" tag + text = strip_tags(text, ['script', 'style', 'link']) + #todo, add markdown parser call conditional on #self.use_markdown flag post_html = text diff --git a/askbot/models/repute.py b/askbot/models/repute.py index e48773e6..5e9c295f 100644 --- a/askbot/models/repute.py +++ b/askbot/models/repute.py @@ -91,7 +91,9 @@ class BadgeData(models.Model): """Awarded for notable actions performed on the site by Users.""" slug = models.SlugField(max_length=50, unique=True) awarded_count = models.PositiveIntegerField(default=0) - awarded_to = models.ManyToManyField(User, through='Award', related_name='badges') + awarded_to = models.ManyToManyField( + User, through='Award', related_name='badges' + ) def _get_meta_data(self): """retrieves badge metadata stored @@ -99,16 +101,13 @@ class BadgeData(models.Model): from askbot.models import badges return badges.get_badge(self.slug) - @property - def name(self): + def get_name(self): return self._get_meta_data().name - @property - def description(self): + def get_description(self): return self._get_meta_data().description - @property - def css_class(self): + def get_css_class(self): return self._get_meta_data().css_class def get_type_display(self): @@ -125,19 +124,6 @@ class BadgeData(models.Model): def get_absolute_url(self): return '%s%s/' % (reverse('badge', args=[self.id]), self.slug) -class AwardManager(models.Manager): - def get_recent_awards(self): - awards = super(AwardManager, self).extra( - select={'badge_id': 'badge.id', 'badge_name':'badge.name', - 'badge_description': 'badge.description', 'badge_type': 'badge.type', - 'user_id': 'auth_user.id', 'user_name': 'auth_user.username' - }, - tables=['award', 'badge', 'auth_user'], - order_by=['-awarded_at'], - where=['auth_user.id=award.user_id AND badge_id=badge.id'], - ).values('badge_id', 'badge_name', 'badge_description', 'badge_type', 'user_id', 'user_name') - return awards - class Award(models.Model): """The awarding of a Badge to a User.""" user = models.ForeignKey(User, related_name='award_user') @@ -148,10 +134,8 @@ class Award(models.Model): awarded_at = models.DateTimeField(default=datetime.datetime.now) notified = models.BooleanField(default=False) - objects = AwardManager() - def __unicode__(self): - return u'[%s] is awarded a badge [%s] at %s' % (self.user.username, self.badge.name, self.awarded_at) + return u'[%s] is awarded a badge [%s] at %s' % (self.user.username, self.badge.get_name(), self.awarded_at) class Meta: app_label = 'askbot' diff --git a/askbot/templates/badge.html b/askbot/templates/badge.html index b2c4ce8b..aebf5450 100644 --- a/askbot/templates/badge.html +++ b/askbot/templates/badge.html @@ -2,11 +2,11 @@ {% import "macros.html" as macros %} {%from "macros.html" import gravatar %} <!-- template badge.html --> -{% block title %}{% spaceless %}{% trans name=badge.name %}{{name}}{% endtrans %} - {% trans %}Badge{% endtrans %}{% endspaceless %}{% endblock %} +{% block title %}{% spaceless %}{% trans name=badge.get_name() %}{{name}}{% endtrans %} - {% trans %}Badge{% endtrans %}{% endspaceless %}{% endblock %} {% block content %} -<h1 class="section-title">{% trans name=badge.name %}Badge "{{name}}"{% endtrans %}</h1> +<h1 class="section-title">{% trans name=badge.get_name() %}Badge "{{name}}"{% endtrans %}</h1> <p> - <a href="{{badge.get_absolute_url()}}" title="{{ badge.get_type_display() }} : {% trans description=badge.description %}{{description}}{% endtrans %}" class="medal"><span class="{{ badge.css_class }}">●</span> {% trans name=badge.name%}{{name}}{% endtrans %}</a> {% trans description=badge.description %}{{description}}{% endtrans %} + <a href="{{badge.get_absolute_url()}}" title="{{ badge.get_type_display() }} : {% trans description=badge.get_description() %}{{description}}{% endtrans %}" class="medal"><span class="{{ badge.get_css_class() }}">●</span> {% trans name=badge.get_name() %}{{name}}{% endtrans %}</a> {% trans description=badge.get_description() %}{{description}}{% endtrans %} </p> <div> {% if badge.awarded_count %} diff --git a/askbot/templates/badges.html b/askbot/templates/badges.html index e669b7d4..112adc61 100644 --- a/askbot/templates/badges.html +++ b/askbot/templates/badges.html @@ -17,11 +17,11 @@ {% endif %} <div style="float:left;width:230px;"> <a href="{{badge.get_absolute_url()}}" - title="{{badge.get_type_display()}} : {{badge.description}}" - class="medal"><span class="{{ badge.css_class }}">●</span> {{badge.name}}</a><strong> + title="{{ badge.get_type_display() }} : {{ badge.get_description() }}" + class="medal"><span class="{{ badge.get_css_class() }}">●</span> {{ badge.get_name() }}</a><strong> × {{ badge.awarded_count|intcomma }}</strong> </div> - <p style="float:left;margin-top:8px;">{{badge.description}}</p> + <p style="float:left;margin-top:8px;">{{ badge.get_description() }}</p> </div> {% endfor %} </div> diff --git a/askbot/templates/meta/bottom_scripts.html b/askbot/templates/meta/bottom_scripts.html index 1a910672..b37d2161 100644 --- a/askbot/templates/meta/bottom_scripts.html +++ b/askbot/templates/meta/bottom_scripts.html @@ -56,6 +56,7 @@ {% endif %} <script type="text/javascript"> /*<![CDATA[*/ + $('.mceStatusbar').remove();//a hack to remove the tinyMCE status bar $(document).ready(function(){ // focus input on the search bar endcomment {% if active_tab in ('users', 'questions', 'tags', 'badges') %} diff --git a/askbot/templates/question.html b/askbot/templates/question.html index e2e6f394..b3610bd0 100644 --- a/askbot/templates/question.html +++ b/askbot/templates/question.html @@ -87,12 +87,11 @@ if (data['userIsAdminOrMod']){ return;//all remaining functions stay on } - if (data['user_posts'] === undefined) { - return; - } - if (post_id in data['user_posts']){ - //todo: remove edit button from older comments - return;//same here + if (data['user_posts'] !== undefined) { + if (post_id in data['user_posts']){ + //todo: remove edit button from older comments + return;//same here + } } if (//maybe remove "delete" button data['userReputation'] < @@ -117,52 +116,21 @@ {{settings.MIN_REP_TO_RETAG_OTHERS_QUESTIONS}} ){ var retag_btn = document.getElementById('retag'); - retag_btn.parentNode.removeChild(retag_btn); + if (retag_btn) { + retag_btn.parentNode.removeChild(retag_btn); + } } } function render_add_comment_button(post_id, extra_comment_count){ - var can_add = false; - if (data['user_posts'] === undefined) { - return; - } - {% if user_can_post_comment %} - can_add = true; - {% else %} - if (data['user_posts'] && post_id in data['user_posts']){ - can_add = true; - } - {% endif %} - var add_comment_btn = document.getElementById( - 'add-comment-to-post-' + post_id - ); - if (can_add === false){ - add_comment_btn.parentNode.removeChild(add_comment_btn); - return; - } - - var text = ''; if (extra_comment_count > 0){ - if (can_add){ - text = - "{% trans %}post a comment / <strong>some</strong> more{% endtrans %}"; - } else { - text = - "{% trans %}see <strong>some</strong> more{% endtrans%}"; - } + var text = "{% trans %}see more comments{% endtrans%}"; } else { - if (can_add){ - text = "{% trans %}post a comment{% endtrans %}"; - } + var text = "{% trans %}post a comment{% endtrans %}"; } + var add_comment_btn = document.getElementById('add-comment-to-post-' + post_id); add_comment_btn.innerHTML = text; - //add the count - for (node in add_comment_btn.childNodes){ - if (node.nodeName === 'strong'){ - node.innerHTML = extra_comment_count; - break; - } - } } + function render_add_answer_button(){ var add_answer_btn = document.getElementById('add-answer-btn'); if (askbot['data']['userIsAuthenticated']){ diff --git a/askbot/templates/question/content.html b/askbot/templates/question/content.html index 7efc1d54..babda6b5 100644 --- a/askbot/templates/question/content.html +++ b/askbot/templates/question/content.html @@ -20,8 +20,6 @@ {{ macros.paginator(paginator_context, anchor='#sort-top') }} <div class="clean"></div> -{% else %} - {% include "question/sharing_prompt_phrase.html" %} {% endif %} {# buttons below cannot be cached yet #} @@ -31,6 +29,10 @@ href="{% url "edit_answer" previous_answer.id %}" >{% trans %}Edit Your Previous Answer{% endtrans %}</a> <span>{% trans %}(only one answer per question is allowed){% endtrans %}</span> + <div style="invisible"> + {# hidden because we still need js from the tinymce widget #} + {% include "question/new_answer_form.html" %} + </div> {% else %} {% include "question/new_answer_form.html" %} {% endif %} diff --git a/askbot/templates/question/javascript.html b/askbot/templates/question/javascript.html index 8b24655a..dc0c68f0 100644 --- a/askbot/templates/question/javascript.html +++ b/askbot/templates/question/javascript.html @@ -22,12 +22,14 @@ askbot['urls']['getGroupsList'] = '{% url get_groups_list %}'; askbot['urls']['publishAnswer'] = '{% url publish_answer %}'; askbot['data']['userIsThreadModerator'] = {% if user_is_thread_moderator %}true{% else %}false{% endif %}; + askbot['data']['userCanPostComment'] = {% if user_can_post_comment %}true{% else %}false{% endif %}; askbot['messages']['addComment'] = '{% trans %}post a comment{% endtrans %}'; {% if settings.SAVE_COMMENT_ON_ENTER %} askbot['settings']['saveCommentOnEnter'] = true; {% else %} askbot['settings']['saveCommentOnEnter'] = false; {% endif %} + askbot['settings']['minRepToPostComment'] = {{ settings.MIN_REP_TO_LEAVE_COMMENTS }}; askbot['settings']['tagSource'] = '{{ settings.TAG_SOURCE }}'; </script> <script type="text/javascript" src='{{"/bootstrap/js/bootstrap.js"|media}}'></script> diff --git a/askbot/templates/question/new_answer_form.html b/askbot/templates/question/new_answer_form.html index bc51f44a..90fe8786 100644 --- a/askbot/templates/question/new_answer_form.html +++ b/askbot/templates/question/new_answer_form.html @@ -3,9 +3,6 @@ action="{% url answer question.id %}" method="post" >{% csrf_token %} - {# ==== START: question/subscribe_by_email_prompt.html ==== #} - {% include "question/subscribe_by_email_prompt.html" %} - {# ==== END: question/subscribe_by_email_prompt.html ==== #} <div style="clear:both"></div> {% if request.user.is_anonymous() and settings.ALLOW_POSTING_BEFORE_LOGGING_IN == False %} {% if not thread.closed %} diff --git a/askbot/templates/question/sharing_prompt_phrase.html b/askbot/templates/question/sharing_prompt_phrase.html deleted file mode 100644 index 2e68d1f3..00000000 --- a/askbot/templates/question/sharing_prompt_phrase.html +++ /dev/null @@ -1,11 +0,0 @@ -{% set question_url=(settings.APP_URL|strip_path + question.get_absolute_url())|urlencode %} -<h2 class="share-question">{% trans %}Know someone who can answer? Share a <a href="{{ question_url }}">link</a> to this question via{% endtrans %} - {% if settings.ENABLE_SHARING_TWITTER %}{{ macros.share(site = 'twitter', site_label = 'Twitter') }},{% endif %} - {% if settings.ENABLE_SHARING_FACEBOOK %}{{ macros.share(site = 'facebook', site_label = 'Facebook') }},{% endif %} - {% if settings.ENABLE_SHARING_LINKEDIN %}{{ macros.share(site = 'linkedin', site_label = 'LinkedIn') }},{% endif %} - {% if settings.ENABLE_SHARING_IDENTICA %}{{ macros.share(site = 'identica', site_label = 'Identi.ca') }},{% endif %} - {%- if settings.ENABLE_SHARING_TWITTER or settings.ENABLE_SHARING_FACEBOOK or settings.ENABLE_SHARING_LINKEDIN or settings.ENABLE_SHARING_IDENTICA -%} - {% trans %} or{% endtrans %} - {% endif %} - <a href="mailto:?subject={{ settings.APP_SHORT_NAME|urlencode }}&body={{ question_url }}">{% trans %}email{% endtrans %}</a>. -</h2> diff --git a/askbot/templates/question/subscribe_by_email_prompt.html b/askbot/templates/question/subscribe_by_email_prompt.html deleted file mode 100644 index 6a77601c..00000000 --- a/askbot/templates/question/subscribe_by_email_prompt.html +++ /dev/null @@ -1,13 +0,0 @@ -{% if request.user.is_authenticated() %} - <p> - {{ answer.email_notify }} - <label for="question-subscribe-updates"> - {% trans %}Email me when there are any new answers{% endtrans %} - </label> - </p> -{% else %} - <p> - {{ answer.email_notify }} - <label>{% trans %}<span class='strong'>Here</span> (once you log in) you will be able to sign up for the periodic email updates about this question.{% endtrans %}</label> - </p> -{% endif %} diff --git a/askbot/templates/user_profile/user_recent.html b/askbot/templates/user_profile/user_recent.html index 8eae673d..deac051b 100644 --- a/askbot/templates/user_profile/user_recent.html +++ b/askbot/templates/user_profile/user_recent.html @@ -15,9 +15,9 @@ <div style="float:left;overflow:hidden;"> {% if act.is_badge %} <a href="{{act.badge.get_absolute_url()}}" - title="{{ act.badge.get_type_display() }} : {% trans description=act.badge.description %}{{description}}{% endtrans %}" + title="{{ act.badge.get_type_display() }} : {% trans description=act.badge.get_description() %}{{description}}{% endtrans %}" class="medal"> - <span class="{{ act.badge.css_class }}">●</span> {% trans name=act.badge.name %}{{name}}{% endtrans %} + <span class="{{ act.badge.get_css_class() }}">●</span> {% trans name=act.badge.get_name() %}{{name}}{% endtrans %} </a> {% if act.content_object.post_type == 'question' %} {% set question=act.content_object %} diff --git a/askbot/templates/user_profile/user_stats.html b/askbot/templates/user_profile/user_stats.html index c042b5fb..812f3411 100644 --- a/askbot/templates/user_profile/user_stats.html +++ b/askbot/templates/user_profile/user_stats.html @@ -115,9 +115,9 @@ {% for badge, badge_user_awards in badges %} <a href="{{badge.get_absolute_url()}}" - title="{% trans description=badge.description %}{{description}}{% endtrans %}" + title="{% trans description=badge.get_description() %}{{description}}{% endtrans %}" class="medal" - ><span class="{{ badge.css_class }}">●</span> {% trans name=badge.name %}{{name}}{% endtrans %} + ><span class="{{ badge.get_css_class() }}">●</span> {% trans name=badge.get_name() %}{{name}}{% endtrans %} </a> <span class="tag-number">× <span class="badge-context-toggle">{{ badge_user_awards|length|intcomma }}</span> diff --git a/askbot/utils/html.py b/askbot/utils/html.py index 1d76fdb7..549f22bf 100644 --- a/askbot/utils/html.py +++ b/askbot/utils/html.py @@ -94,7 +94,15 @@ def replace_links_with_text(html): link.replaceWith(format_url_replacement(url, text)) return unicode(soup.find('body').renderContents(), 'utf-8') - + +def strip_tags(html, tags=None): + """strips tags from given html output""" + assert(tags != None) + soup = BeautifulSoup(html) + for tag in tags: + tag_matches = soup.find_all(tag) + map(lambda v: v.replaceWith(''), tag_matches) + return unicode(soup.find('body').renderContents(), 'utf-8') def sanitize_html(html): """Sanitizes an HTML fragment.""" diff --git a/askbot/views/commands.py b/askbot/views/commands.py index cd54075e..b5afa890 100644 --- a/askbot/views/commands.py +++ b/askbot/views/commands.py @@ -1463,7 +1463,12 @@ def get_editor(request): if 'config' not in request.GET: return HttpResponseForbidden() config = simplejson.loads(request.GET['config']) - form = forms.EditorForm(editor_attrs=config, user=request.user) + element_id = request.GET.get('id', 'editor') + form = forms.EditorForm( + attrs={'id': element_id}, + editor_attrs=config, + user=request.user + ) editor_html = render_text_into_skin( '{{ form.media }} {{ form.editor }}', {'form': form}, diff --git a/askbot/views/writers.py b/askbot/views/writers.py index b581036c..74c96235 100644 --- a/askbot/views/writers.py +++ b/askbot/views/writers.py @@ -17,7 +17,11 @@ from django.shortcuts import get_object_or_404 from django.shortcuts import render from django.contrib.auth.decorators import login_required from django.contrib.auth.models import User -from django.http import HttpResponseRedirect, HttpResponse, HttpResponseForbidden, Http404 +from django.http import HttpResponse +from django.http import HttpResponseBadRequest +from django.http import HttpResponseForbidden +from django.http import HttpResponseRedirect +from django.http import Http404 from django.utils import simplejson from django.utils.html import strip_tags, escape from django.utils.translation import get_language @@ -611,7 +615,7 @@ def __generate_comments_json(obj, user):#non-view generates json data for the po is_deletable = True except exceptions.PermissionDenied: is_deletable = False - is_editable = template_filters.can_edit_comment(comment.author, comment) + is_editable = template_filters.can_edit_comment(user, comment) else: is_deletable = False is_editable = False @@ -640,6 +644,10 @@ def __generate_comments_json(obj, user):#non-view generates json data for the po @csrf.csrf_exempt @decorators.check_spam('comment') def post_comments(request):#generic ajax handler to load comments to an object + """todo: fixme: post_comments is ambigous: + means either get comments for post or + add a new comment to post + """ # only support get post comments by ajax now post_type = request.REQUEST.get('post_type', '') @@ -648,11 +656,27 @@ def post_comments(request):#generic ajax handler to load comments to an object user = request.user - id = request.REQUEST['post_id'] - obj = get_object_or_404(models.Post, id=id) + if request.method == 'POST': + form = forms.NewCommentForm(request.POST) + elif request.method == 'GET': + form = forms.GetCommentsForPostForm(request.GET) + + if form.is_valid() == False: + return HttpResponseBadRequest( + _('This content is forbidden'), + mimetype='application/json' + ) + + post_id = form.cleaned_data['post_id'] + try: + post = models.Post.objects.get(id=post_id) + except models.Post.DoesNotExist: + return HttpResponseBadRequest( + _('Post not found'), mimetype='application/json' + ) if request.method == "GET": - response = __generate_comments_json(obj, user) + response = __generate_comments_json(post, user) elif request.method == "POST": try: if user.is_anonymous(): @@ -661,37 +685,54 @@ def post_comments(request):#generic ajax handler to load comments to an object '<a href="%(sign_in_url)s">sign in</a>.') % \ {'sign_in_url': url_utils.get_login_url()} raise exceptions.PermissionDenied(msg) - user.post_comment(parent_post=obj, body_text=request.POST.get('comment')) - response = __generate_comments_json(obj, user) + user.post_comment( + parent_post=post, body_text=form.cleaned_data['comment'] + ) + response = __generate_comments_json(post, user) except exceptions.PermissionDenied, e: response = HttpResponseForbidden(unicode(e), mimetype="application/json") return response -@csrf.csrf_exempt +#@csrf.csrf_exempt @decorators.ajax_only -@decorators.check_spam('comment') +#@decorators.check_spam('comment') def edit_comment(request): if request.user.is_anonymous(): raise exceptions.PermissionDenied(_('Sorry, anonymous users cannot edit comments')) - comment_id = int(request.POST['comment_id']) - comment_post = models.Post.objects.get(post_type='comment', id=comment_id) + form = forms.EditCommentForm(request.POST) + if form.is_valid() == False: + raise exceptions.PermissionDenied('This content is forbidden') + + comment_id = form.cleaned_data['comment_id'] + comment_post = models.Post.objects.get( + post_type='comment', + id=comment_id + ) + + request.user.edit_comment( + comment_post=comment_post, + body_text=form.cleaned_data['comment'] + ) + + is_deletable = template_filters.can_delete_comment( + comment_post.author, comment_post) - request.user.edit_comment(comment_post=comment_post, body_text = request.POST['comment']) + is_editable = template_filters.can_edit_comment( + comment_post.author, comment_post) - is_deletable = template_filters.can_delete_comment(comment_post.author, comment_post) - is_editable = template_filters.can_edit_comment(comment_post.author, comment_post) tz = ' ' + template_filters.TIMEZONE_STR tz = template_filters.TIMEZONE_STR + timestamp = str(comment_post.added_at.replace(microsecond=0)) + tz return { 'id' : comment_post.id, 'object_id': comment_post.parent.id, - 'comment_added_at': str(comment_post.added_at.replace(microsecond = 0)) + tz, + 'comment_added_at': timestamp, 'html': comment_post.html, - 'user_display_name': comment_post.author.username, + 'user_display_name': escape(comment_post.author.username), 'user_url': comment_post.author.get_profile_url(), 'user_id': comment_post.author.id, 'is_deletable': is_deletable, |