summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rwxr-xr-x.gitignore1
-rw-r--r--askbot/__init__.py2
-rw-r--r--askbot/forms.py22
-rw-r--r--askbot/management/commands/askbot_add_test_content.py16
-rw-r--r--askbot/media/js/post.js609
-rw-r--r--askbot/media/js/utils.js17
-rw-r--r--askbot/media/style/style.less49
-rw-r--r--askbot/migrations/0032_auto__del_field_badgedata_multiple__del_field_badgedata_description__d.py2
-rw-r--r--askbot/models/post.py12
-rw-r--r--askbot/models/repute.py30
-rw-r--r--askbot/templates/badge.html6
-rw-r--r--askbot/templates/badges.html6
-rw-r--r--askbot/templates/meta/bottom_scripts.html1
-rw-r--r--askbot/templates/question.html56
-rw-r--r--askbot/templates/question/content.html6
-rw-r--r--askbot/templates/question/javascript.html2
-rw-r--r--askbot/templates/question/new_answer_form.html3
-rw-r--r--askbot/templates/user_profile/user_recent.html4
-rw-r--r--askbot/templates/user_profile/user_stats.html4
-rw-r--r--askbot/utils/html.py10
-rw-r--r--askbot/views/commands.py7
-rw-r--r--askbot/views/writers.py73
22 files changed, 623 insertions, 315 deletions
diff --git a/.gitignore b/.gitignore
index 2f360b2a..34a75654 100755
--- a/.gitignore
+++ b/.gitignore
@@ -19,6 +19,7 @@ lint
env
/static
django
+tinymce
lamson
django/*
nbproject
diff --git a/askbot/__init__.py b/askbot/__init__.py
index 0d9156aa..f2cd6c5b 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(' &ndash; ');
+ // 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 53573280..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;
@@ -273,6 +280,7 @@ body.user-messages {
#userToolsNav {/* Navigation bar containing login link or user information, check widgets/user_navigation.html*/
height: 20px;
padding-bottom:5px;
+ white-space: nowrap;
a {
height: 35px;
@@ -1776,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;
@@ -1944,9 +1956,9 @@ ul#related-tags li {
/* ----- Question template ----- */
-.question-page{
+.question-page {
- h1{
+ h1 {
padding-top:0px;
font-family:@main-font;
@@ -2009,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;
@@ -2289,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;
@@ -2301,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;
@@ -2311,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;
@@ -3106,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 }}">&#9679;</span>&nbsp;{% 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() }}">&#9679;</span>&nbsp;{% 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 }}">&#9679;</span>&nbsp;{{badge.name}}</a><strong>
+ title="{{ badge.get_type_display() }} : {{ badge.get_description() }}"
+ class="medal"><span class="{{ badge.get_css_class() }}">&#9679;</span>&nbsp;{{ badge.get_name() }}</a><strong>
&#215; {{ 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/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 }}">&#9679;</span>&nbsp;{% trans name=act.badge.name %}{{name}}{% endtrans %}
+ <span class="{{ act.badge.get_css_class() }}">&#9679;</span>&nbsp;{% 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 }}">&#9679;</span>&nbsp;{% trans name=badge.name %}{{name}}{% endtrans %}
+ ><span class="{{ badge.get_css_class() }}">&#9679;</span>&nbsp;{% trans name=badge.get_name() %}{{name}}{% endtrans %}
</a>&nbsp;
<span class="tag-number">&#215;
<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,