diff options
author | Gustavo A. Gómez Farhat <gustavo.gomez.farhat@gmail.com> | 2013-04-08 09:04:47 -0500 |
---|---|---|
committer | Gustavo A. Gómez Farhat <gustavo.gomez.farhat@gmail.com> | 2013-04-08 09:04:47 -0500 |
commit | c3ef1da21c0b46699dbf6f3dc1e53e6d35bccadd (patch) | |
tree | d29686dba3f73660310a8e2d781cac49ea4344ca | |
parent | 80dc8af009a09d849004583fed36f47f6a33484f (diff) | |
parent | 0906b8d3e2e0c4097cb4ad5f1d21c2bf411c179e (diff) | |
download | askbot-c3ef1da21c0b46699dbf6f3dc1e53e6d35bccadd.tar.gz askbot-c3ef1da21c0b46699dbf6f3dc1e53e6d35bccadd.tar.bz2 askbot-c3ef1da21c0b46699dbf6f3dc1e53e6d35bccadd.zip |
Merge branch 'master' of github.com:ASKBOT/askbot-devel
34 files changed, 1414 insertions, 534 deletions
@@ -21,6 +21,7 @@ env /custom_settings /static django +tinymce lamson django/* nbproject diff --git a/askbot/conf/forum_data_rules.py b/askbot/conf/forum_data_rules.py index b318e4a2..362f4735 100644 --- a/askbot/conf/forum_data_rules.py +++ b/askbot/conf/forum_data_rules.py @@ -28,6 +28,21 @@ settings.register( ) ) +COMMENTS_EDITOR_CHOICES = ( + ('plain-text', 'Plain text editor'), + ('rich-text', 'Same editor as for questions and answers') +) + +settings.register( + livesettings.StringValue( + FORUM_DATA_RULES, + 'COMMENTS_EDITOR_TYPE', + default='plain-text', + choices=COMMENTS_EDITOR_CHOICES, + description=_('Editor for the comments') + ) +) + settings.register( livesettings.BooleanValue( FORUM_DATA_RULES, @@ -330,8 +345,12 @@ settings.register( livesettings.BooleanValue( FORUM_DATA_RULES, 'SAVE_COMMENT_ON_ENTER', - default = True, - description = _('Save comment by pressing <Enter> key') + default=False, + description=_('Save comment by pressing <Enter> key'), + help_text=_( + 'This may be useful when only one-line comments ' + 'are desired. Will not work with TinyMCE editor.' + ) ) ) diff --git a/askbot/context.py b/askbot/context.py index fba17b5f..abd283e1 100644 --- a/askbot/context.py +++ b/askbot/context.py @@ -55,8 +55,20 @@ def application_settings(request): my_settings['LOGOUT_REDIRECT_URL'] = url_utils.get_logout_redirect_url() my_settings['USE_ASKBOT_LOGIN_SYSTEM'] = 'askbot.deps.django_authopenid' \ in settings.INSTALLED_APPS + + current_language = get_language() + + #for some languages we will start searching for shorter words + if current_language == 'ja': + #we need to open the search box and show info message about + #the japanese lang search + min_search_word_length = 1 + else: + min_search_word_length = my_settings['MIN_SEARCH_WORD_LENGTH'] + context = { - 'current_language_code': get_language(), + 'min_search_word_length': min_search_word_length, + 'current_language_code': current_language, 'settings': my_settings, 'skin': get_skin(request), 'moderation_items': api.get_info_on_moderation_items(request.user), diff --git a/askbot/doc/source/changelog.rst b/askbot/doc/source/changelog.rst index 7b65ee07..a99864c7 100644 --- a/askbot/doc/source/changelog.rst +++ b/askbot/doc/source/changelog.rst @@ -3,7 +3,10 @@ Changes in Askbot Development version ------------------- -* Added instant search to the tags page +* Management command `askbot_import_jive` to import data from Jive forums. +* Added possibility to choose editor for comments: plain text, or same as + editor used for the questions or answers: WMD or TinyMCE. +* Added ajax search to the tags page * Added a placeholder template for the custom javascript on the question page * Allowed to disable the big "ask" button. * Some support for the media compression (Tyler Mandry) diff --git a/askbot/forms.py b/askbot/forms.py index 5e9a7850..0fafec53 100644 --- a/askbot/forms.py +++ b/askbot/forms.py @@ -295,9 +295,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': @@ -506,17 +508,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 ) @@ -1339,10 +1342,6 @@ class EditAnswerForm(PostAsSomeoneForm, PostPrivatelyForm): else: return False -class EditCommentForm(forms.Form): - comment_id = forms.IntegerField() - suppress_email = SuppressEmailField() - class EditTagWikiForm(forms.Form): text = forms.CharField(required=False) tag_id = forms.IntegerField() @@ -1689,5 +1688,18 @@ class BulkTagSubscriptionForm(forms.Form): 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_id = forms.IntegerField() + comment = forms.CharField() + suppress_email = SuppressEmailField() + + class DeleteCommentForm(forms.Form): 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/management/commands/askbot_import_jive.py b/askbot/management/commands/askbot_import_jive.py new file mode 100644 index 00000000..90bbfd98 --- /dev/null +++ b/askbot/management/commands/askbot_import_jive.py @@ -0,0 +1,139 @@ +from askbot import models +from askbot.conf import settings as askbot_settings +from askbot.utils.console import ProgressBar +from askbot.utils.slug import slugify +from bs4 import BeautifulSoup +from django.core.management.base import BaseCommand, CommandError +from django.db import transaction +from django.forms import EmailField, ValidationError +from datetime import datetime + +""" +Jive --> CategoryList --> Category --> ForumList --> Forum + <Name>ouaou</Name> + <CreationDate>2008-05-06-0249</CreationDate> + <ModifiedDate/> + <ThreadList> + <Thread id="4046"> + <CreationDate>2013/03/08 01:50:42.54 CST</CreationDate> + <ModifiedDate>2013/03/12 23:44:45.528 CDT</ModifiedDate> + <Message id="16809"> + <Subject>Need help setting up mirror space</Subject> + <Body>Body text</Body> + <Username>jfawcett</Username> + <CreationDate>2013/03/08 01:50:42.54 CST</CreationDate> + <ModifiedDate>2013/03/08 01:50:42.54 CST</ModifiedDate> + <MessageList> + </MessageList> + </Message> + </Thread> +""" + +def parse_date(date_str): + return datetime.strptime(date_str[:-8], '%Y/%m/%d %H:%M:%S') + +class Command(BaseCommand): + args = '<jive-dump.xml>' + + def __init__(self, *args, **kwargs): + super(Command, self).__init__(*args, **kwargs) + #relax certain settings + askbot_settings.update('LIMIT_ONE_ANSWER_PER_USER', False) + askbot_settings.update('MAX_COMMENT_LENGTH', 1000000) + askbot_settings.update('MIN_REP_TO_LEAVE_COMMENTS', 1) + self.bad_email_count = 0 + + def handle(self, *args, **kwargs): + assert len(args) == 1, 'Dump file name is required' + xml = open(args[0], 'r').read() + try: + import lxml + soup = BeautifulSoup(xml, 'lxml') + except ImportError: + soup = BeautifulSoup(xml) + + self.import_users(soup.find_all('user')) + self.import_forums(soup.find_all('forum')) + + @transaction.commit_manually + def import_users(self, user_soup): + """import users from jive to askbot""" + + message = 'Importing users:' + for user in ProgressBar(iter(user_soup), len(user_soup), message): + username = user.find('username').text + real_name = user.find('name').text + try: + email = EmailField().clean(user.find('email').text) + except ValidationError: + email = 'unknown%d@example.com' % self.bad_email_count + self.bad_email_count += 1 + + joined_timestamp = parse_date(user.find('creationdate').text) + user = models.User( + username=username, + email=email, + real_name=real_name, + date_joined=joined_timestamp + ) + user.save() + transaction.commit() + + def import_forums(self, forum_soup): + """import forums by associating each with a special tag, + and then importing all threads for the tag""" + admin = models.User.objects.get(id=1) + for forum in forum_soup: + threads_soup = forum.find_all('thread') + self.import_threads(threads_soup, forum.find('name').text) + + @transaction.commit_manually + def import_threads(self, threads, tag_name): + message = 'Importing threads for %s' % tag_name + for thread in ProgressBar(iter(threads), len(threads), message): + self.import_thread(thread, tag_name) + transaction.commit() + + def import_thread(self, thread, tag_name): + """import individual thread""" + question_soup = thread.message + title, body, timestamp, user = self.parse_post(question_soup) + #post question + question = user.post_question( + title=title, + body_text=body, + timestamp=timestamp, + tags=tag_name + ) + #post answers + if not question_soup.messagelist: + return + + for answer_soup in question_soup.messagelist.find_all('message', recursive=False): + title, body, timestamp, user = self.parse_post(answer_soup) + answer = user.post_answer( + question=question, + body_text=body, + timestamp=timestamp + ) + comments = answer_soup.find_all('message') + for comment in comments: + title, body, timestamp, user = self.parse_post(comment) + user.post_comment( + parent_post=answer, + body_text=body, + timestamp=timestamp + ) + + def parse_post(self, post): + title = post.find('subject').text + added_at = parse_date(post.find('creationdate').text) + username = post.find('username').text + try: + user = models.User.objects.get(username=username) + except models.User.DoesNotExist: + email = 'unknown%d@example.com' % self.bad_email_count + self.bad_email_count += 1 + user = models.User(username=username, email=email) + user.save() + return title, post.text, added_at, user diff --git a/askbot/media/js/live_search.js b/askbot/media/js/live_search.js index b744e69e..98e01179 100644 --- a/askbot/media/js/live_search.js +++ b/askbot/media/js/live_search.js @@ -149,11 +149,31 @@ SearchDropMenu.prototype.hideWaitIcon = function() { } }; +SearchDropMenu.prototype.hideHeader = function() { + if (this._header) { + this._header.hide(); + } +}; + +SearchDropMenu.prototype.showHeader = function() { + if (this._header) { + this._header.show(); + } +}; + SearchDropMenu.prototype.createDom = function() { this._element = this.makeElement('div'); this._element.addClass('search-drop-menu'); this._element.hide(); + if (askbot['data']['languageCode'] === 'ja') { + var warning = this.makeElement('p'); + this._header = warning; + warning.addClass('header'); + warning.html(gettext('To see search results, 2 or more characters may be required')); + this._element.append(warning); + } + this._resultsList = this.makeElement('ul'); this._element.append(this._resultsList); this._element.addClass('empty'); @@ -503,6 +523,9 @@ FullTextSearch.prototype.getSearchQuery = function() { FullTextSearch.prototype.renderTitleSearchResult = function(data) { var menu = this._dropMenu; menu.hideWaitIcon(); + if (data.length > 0) { + menu.hideHeader(); + } menu.setData(data); menu.render(); menu.show(); @@ -789,6 +812,7 @@ FullTextSearch.prototype.makeKeyDownHandler = function() { past the minimum length to trigger search */ dropMenu.show(); dropMenu.showWaitIcon(); + dropMenu.showHeader(); } else { //close drop menu if we were deleting the query dropMenu.reset(); diff --git a/askbot/media/js/post.js b/askbot/media/js/post.js index 1fb1203f..4e1e8da2 100644 --- a/askbot/media/js/post.js +++ b/askbot/media/js/post.js @@ -1410,50 +1410,160 @@ 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(); + this._minorEditBox.hide(); + this._element.hide(); + } else { + this._element.show(); + this._editor.getElement().show(); + this._submit_btn.show(); + this._cancel_btn.show(); + this._minorEditBox.show(); + } }; +EditCommentForm.prototype.getEditorType = function() { + if (askbot['settings']['commentsEditorType'] === 'rich-text') { + return askbot['settings']['editorType']; + } else { + return 'plain-text'; + } +}; + +EditCommentForm.prototype.startTinyMCEEditor = function() { + var editorId = this.makeId('comment-editor'); + var opts = { + mode: 'exact', + content_css: mediaUrl('media/style/tinymce/comments-content.css'), + elements: editorId, + plugins: 'autoresize', + theme: 'advanced', + theme_advanced_toolbar_location: 'top', + theme_advanced_toolbar_align: 'left', + 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); + this._editorBox.prepend(editor.getElement()); + editor.start(); + this._editor = editor; +}; + +EditCommentForm.prototype.startWMDEditor = function() { + var editor = new WMD(); + editor.setEnabledButtons('bold italic link code ol ul'); + editor.setPreviewerEnabled(false); + editor.setText(this._text); + this._editorBox.prepend(editor.getElement());//attach DOM before start + editor.start();//have to start after attaching DOM + this._editor = editor; +}; + +EditCommentForm.prototype.startSimpleEditor = function() { + this._editor = new SimpleEditor(); + this._editorBox.prepend(this._editor.getElement()); +}; + +EditCommentForm.prototype.startEditor = function() { + var editorType = this.getEditorType(); + if (editorType === 'tinymce') { + this.startTinyMCEEditor(); + //@todo: implement save on enter and character counter in tinyMCE + return; + } else if (editorType === 'markdown') { + this.startWMDEditor(); + } else { + this.startSimpleEditor(); + } + + //code below is common to SimpleEditor and WMD + var editorElement = this._editor.getElement(); + var updateCounter = this.getCounterUpdater(); + var escapeHandler = makeKeyHandler(27, this.getCancelHandler()); + //todo: try this on the div + var editor = this._editor; + //this should be set on the textarea! + editorElement.blur(updateCounter); + editorElement.focus(updateCounter); + editorElement.keyup(updateCounter) + editorElement.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')); + if (this._minorEditBox) { + this._minorEditBox.hide(); + } } else { this._submit_btn.html(gettext('save comment')); + if (this._minorEditBox) { + this._minorEditBox.show(); + } } + //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); } @@ -1462,46 +1572,58 @@ 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 true; }; 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()){ + } else if (this.confirmAbandon()){ return true; } - this.focus(); + this._editor.focus(); return false; }; EditCommentForm.prototype.getCancelHandler = function(){ var form = this; - return function(){ + return function(evt){ if (form.canCancel()){ form.detach(); + evt.preventDefault(); } return false; }; @@ -1514,12 +1636,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(){ @@ -1527,46 +1654,43 @@ 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._element.append(div); - /* - 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); + /** a stub container for the editor */ + this._editorBox = div; + /** + * editor itself will live at this._editor + * and will be initialized by the attachTo() + */ - this._help_text = $('<div></div>').attr('class', 'clearfix'); - div.append(this._help_text); - */ + this._controlsBox = this.makeElement('div'); + this._controlsBox.addClass('edit-comment-buttons'); + div.append(this._controlsBox); - this._element.append(div); - div.append(this._textarea); this._text_counter = $('<span></span>').attr('class', 'counter'); - div.append(this._text_counter); + this._controlsBox.append(this._text_counter); + this._submit_btn = $('<button class="submit"></button>'); - div.append(this._submit_btn); + this._controlsBox.append(this._submit_btn); this._cancel_btn = $('<button class="submit"></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._controlsBox.append(this._cancel_btn); + + //if email alerts are enabled, add a checkbox "suppress_email" + if (askbot['settings']['enableEmailAlerts'] === true) { + this._minorEditBox = this.makeElement('div'); + this._minorEditBox.addClass('checkbox'); + this._controlsBox.append(this._minorEditBox); + var checkBox = this.makeElement('input'); + checkBox.attr('type', 'checkbox'); + checkBox.attr('name', 'suppress_email'); + this._minorEditBox.append(checkBox); + var label = this.makeElement('label'); + label.attr('for', 'suppress_email'); + label.html(gettext("minor edit (don't send alerts)")); + this._minorEditBox.append(label); } - this._textarea.val(this._text); + }; EditCommentForm.prototype.isEnabled = function() { @@ -1586,38 +1710,60 @@ 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.getSuppressEmail = function() { + return this._element.find('input[name="suppress_email"]').is(':checked'); +}; + +EditCommentForm.prototype.setSuppressEmail = function(bool) { + this._element.find('input[name="suppress_email"]').prop('checked', bool); }; 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': editor.getHtml(), + 'text': text, + 'user_display_name': userName, + 'comment_added_at': timestamp + }); + me._comment.setDraftStatus(true); + me._comment.getContainerWidget().showButton(); + var post_data = { comment: text }; @@ -1625,6 +1771,8 @@ EditCommentForm.prototype.getSaveHandler = function(){ if (me._type == 'edit'){ post_data['comment_id'] = me._comment.getId(); post_url = askbot['urls']['editComment']; + post_data['suppress_email'] = me.getSuppressEmail(); + me.setSuppressEmail(false); } else { post_data['post_type'] = me._comment.getParentType(); @@ -1632,8 +1780,6 @@ EditCommentForm.prototype.getSaveHandler = function(){ post_url = askbot['urls']['postComments']; } - me.disableForm(); - $.ajax({ type: "POST", url: post_url, @@ -1641,19 +1787,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(); } @@ -1662,9 +1810,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; @@ -1674,6 +1819,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']; } @@ -1689,11 +1835,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; @@ -1715,12 +1888,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; }; @@ -1750,42 +1942,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']); @@ -1794,18 +2021,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; }; @@ -1872,20 +2103,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() }); } }; }; @@ -1978,28 +2201,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'); } } }; @@ -2169,15 +2429,89 @@ 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.getHtml = function() { + return '<div class="transient-comment">' + this.getText() + '</div>'; +}; + +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'); + this._element.addClass('wmd-container'); + var textarea = this.makeElement('textarea'); + this._element.append(textarea); + this._textarea = 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); + +//@todo: implement getHtml method that runs text through showdown renderer WMD.prototype.setEnabledButtons = function(buttons){ this._enabled_buttons = buttons; @@ -2200,21 +2534,21 @@ WMD.prototype.createDom = function(){ this._element.append(wmd_container); var wmd_buttons = this.makeElement('div') - .attr('id', 'wmd-button-bar') + .attr('id', this.makeId('wmd-button-bar')) .addClass('wmd-panel'); wmd_container.append(wmd_buttons); var editor = this.makeElement('textarea') - .attr('id', 'editor'); + .attr('id', this.makeId('editor')); 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') - .attr('id', 'previewer') + .attr('id', this.makeId('previewer')) .addClass('wmd-preview'); wmd_container.append(previewer); this._previewer = previewer; @@ -2223,19 +2557,8 @@ 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); + Attacklab.Util.startEditor(true, this._enabled_buttons, this.getIdSeed()); }; /** @@ -2244,60 +2567,77 @@ 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.getHtml = TinyMCE.prototype.getText; + +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); }; /** @@ -2460,12 +2800,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/tinymce/plugins/askbot_attachment/editor_plugin.js b/askbot/media/js/tinymce/plugins/askbot_attachment/editor_plugin.js index d1ef13b4..5f996804 100644 --- a/askbot/media/js/tinymce/plugins/askbot_attachment/editor_plugin.js +++ b/askbot/media/js/tinymce/plugins/askbot_attachment/editor_plugin.js @@ -27,11 +27,9 @@ }
};
- var modalMenuHeadline = gettext('Insert a file');
-
var createDialog = function() {
var dialog = new FileUploadDialog();
- dialog.setHeadingText(modalMenuHeadline);
+ dialog.setFileType('attachment');
dialog.setPostUploadHandler(insertIntoDom);
dialog.setInputId('askbot_attachment_input');
dialog.setUrlInputTooltip(gettext('Or paste file url here'));
diff --git a/askbot/media/js/tinymce/plugins/askbot_imageuploader/editor_plugin.js b/askbot/media/js/tinymce/plugins/askbot_imageuploader/editor_plugin.js index 7fa6b6be..0cd70473 100644 --- a/askbot/media/js/tinymce/plugins/askbot_imageuploader/editor_plugin.js +++ b/askbot/media/js/tinymce/plugins/askbot_imageuploader/editor_plugin.js @@ -27,11 +27,8 @@ }
};
- var modalMenuHeadline = gettext('Upload an image');
-
var createDialog = function() {
var dialog = new FileUploadDialog();
- dialog.setHeadingText(modalMenuHeadline);
dialog.setPostUploadHandler(insertIntoDom);
dialog.setUrlInputTooltip('Or paste image url here');
dialog.setInputId('askbot_imageuploader_input');
diff --git a/askbot/media/js/utils.js b/askbot/media/js/utils.js index 6a00d364..ee094691 100644 --- a/askbot/media/js/utils.js +++ b/askbot/media/js/utils.js @@ -28,6 +28,22 @@ var animateHashes = function(){ } }; +/** + * @param {string} id_token - any token + * @param {string} unique_seed - the unique part + * @returns {string} unique id that can be used in DOM + */ +var askbotMakeId = function(id_token, unique_seed) { + return id_token + '-' + unique_seed; +}; + +var getNewUniqueInt = 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(); @@ -137,6 +153,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]; @@ -307,12 +331,33 @@ var inherits = function(childCtor, parentCtor) { var WrappedElement = function(){ this._element = null; this._in_document = false; + this._idSeed = null; }; /* note that we do not call inherits() here * See TippedInput as an example of a subclass */ /** + * returns a unique integer for any instance of WrappedElement + * which can be used to construct a unique id for use in the DOM + * @return {string} + */ +WrappedElement.prototype.getIdSeed = function() { + var seed = this._idSeed || parseInt(getNewUniqueInt()); + this._idSeed = seed; + return seed; +}; + +/** + * returns unique ide based on the prefix and the id seed + * @param {string} prefix + * @return {string} + */ +WrappedElement.prototype.makeId = function(prefix) { + return askbotMakeId(prefix, this.getIdSeed()); +}; + +/** * notice that we use ObjCls.prototype.someMethod = function() * notation - as we use Javascript's prototypal inheritance * explicitly. The point of this is to be able to eventually @@ -363,7 +408,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; @@ -888,6 +933,7 @@ var ModalDialog = function() { var me = this; this._reject_handler = function() { me.hide(); }; this._content_element = undefined; + this._headerEnabled = true; }; inherits(ModalDialog, WrappedElement); @@ -950,19 +996,21 @@ ModalDialog.prototype.createDom = function() { element.addClass('modal'); //1) create header - var header = this.makeElement('div') - header.addClass('modal-header'); - element.append(header); + if (this._headerEnabled) { + var header = this.makeElement('div') + header.addClass('modal-header'); + element.append(header); + var close_link = this.makeElement('div'); + close_link.addClass('close'); + close_link.attr('data-dismiss', 'modal'); + close_link.html('x'); + header.append(close_link); + var title = this.makeElement('h3'); + title.html(this._heading_text); + header.append(title); + } - var close_link = this.makeElement('div'); - close_link.addClass('close'); - close_link.attr('data-dismiss', 'modal'); - close_link.html('x'); - header.append(close_link); - var title = this.makeElement('h3'); - title.html(this._heading_text); - header.append(title); //2) create content var body = this.makeElement('div') @@ -979,13 +1027,13 @@ ModalDialog.prototype.createDom = function() { element.append(footer); var accept_btn = this.makeElement('button'); - accept_btn.addClass('btn btn-primary'); + accept_btn.addClass('submit'); accept_btn.html(this._accept_button_text); footer.append(accept_btn); if (this._reject_button_text) { var reject_btn = this.makeElement('button'); - reject_btn.addClass('btn cancel'); + reject_btn.addClass('submit cancel'); reject_btn.html(this._reject_button_text); footer.append(reject_btn); } @@ -995,7 +1043,9 @@ ModalDialog.prototype.createDom = function() { if (this._reject_button_text) { setupButtonEventHandlers(reject_btn, this._reject_handler); } - setupButtonEventHandlers(close_link, this._reject_handler); + if (this._headerEnabled) { + setupButtonEventHandlers(close_link, this._reject_handler); + } this.hide(); }; @@ -1005,10 +1055,27 @@ ModalDialog.prototype.createDom = function() { */ var FileUploadDialog = function() { ModalDialog.call(this); - self._post_upload_handler = undefined; + this._post_upload_handler = undefined; + this._fileType = 'image'; + this._headerEnabled = false; }; inherits(FileUploadDialog, ModalDialog); +/** + * allowed values: 'image', 'attachment' + */ +FileUploadDialog.prototype.setFileType = function(fileType) { + this._fileType = fileType; +}; + +FileUploadDialog.prototype.getFileType = function() { + return this._fileType; +}; + +FileUploadDialog.prototype.setButtonText = function(text) { + this._fakeInput.val(text); +}; + FileUploadDialog.prototype.setPostUploadHandler = function(handler) { this._post_upload_handler = handler; }; @@ -1025,6 +1092,10 @@ FileUploadDialog.prototype.getInputId = function() { return this._input_id; }; +FileUploadDialog.prototype.setLabelText= function(text) { + this._label.html(text); +}; + FileUploadDialog.prototype.setUrlInputTooltip = function(text) { this._url_input_tooltip = text; }; @@ -1048,30 +1119,112 @@ FileUploadDialog.prototype.resetInputs = function() { this._upload_input.val(''); }; +FileUploadDialog.prototype.getInputElement = function() { + return $('#' + this.getInputId()); +}; + +FileUploadDialog.prototype.installFileUploadHandler = function(handler) { + var upload_input = this.getInputElement(); + upload_input.unbind('change'); + //todo: fix this - make event handler reinstall work + upload_input.change(handler); +}; + FileUploadDialog.prototype.show = function() { //hack around the ajaxFileUpload plugin FileUploadDialog.superClass_.show.call(this); - var upload_input = this._upload_input; - upload_input.unbind('change'); - //todo: fix this - make event handler reinstall work - upload_input.change(this.getStartUploadHandler()); + var handler = this.getStartUploadHandler(); + this.installFileUploadHandler(handler); }; -FileUploadDialog.prototype.getStartUploadHandler = function(){ - /* startUploadHandler is passed in to re-install the event handler - * which is removed by the ajaxFileUpload jQuery extension - */ +FileUploadDialog.prototype.getUrlInputElement = function() { + return this._url_input.getElement(); +}; + +/* + * argument startUploadHandler is very special it must + * be a function calling this one!!! Todo: see if there + * is a more civilized way to do this. + */ +FileUploadDialog.prototype.startFileUpload = function(startUploadHandler) { + var spinner = this._spinner; - var uploadInputId = this.getInputId(); - var urlInput = this._url_input; + var label = this._label; + + spinner.ajaxStart(function(){ + spinner.show(); + label.hide(); + }); + spinner.ajaxComplete(function(){ + spinner.hide(); + label.show(); + }); + + /* important!!! upload input must be loaded by id + * because ajaxFileUpload monkey-patches the upload form */ + var uploadInput = this.getInputElement(); + uploadInput.ajaxStart(function(){ uploadInput.hide(); }); + uploadInput.ajaxComplete(function(){ uploadInput.show(); }); + + //var localFilePath = upload_input.val(); + + var me = this; + + $.ajaxFileUpload({ + url: askbot['urls']['upload'], + secureuri: false,//todo: check on https + fileElementId: this.getInputId(), + dataType: 'xml', + success: function (data, status) { + + var fileURL = $(data).find('file_url').text(); + var origFileName = $(data).find('orig_file_name').text(); + var newStatus = interpolate( + gettext('Uploaded file: %s'), + [origFileName] + ); + /* + * hopefully a fix for the "fakepath" issue + * https://www.mediawiki.org/wiki/Special:Code/MediaWiki/83225 + */ + fileURL = fileURL.replace(/\w:.*\\(.*)$/,'$1'); + var error = $(data).find('error').text(); + if (error != ''){ + alert(error); + } else { + me.getUrlInputElement().attr('value', fileURL); + me.setLabelText(newStatus); + if (me.getFileType() === 'image') { + var buttonText = gettext('Choose a different image'); + } else { + var buttonText = gettext('Choose a different file'); + } + me.setButtonText(buttonText); + } + + /* re-install this as the upload extension + * will remove the handler to prevent double uploading + * this hack is a manipulation around the + * ajaxFileUpload jQuery plugin. */ + me.installFileUploadHandler(startUploadHandler); + }, + error: function (data, status, e) { + /* re-install this as the upload extension + * will remove the handler to prevent double uploading */ + me.installFileUploadHandler(startUploadHandler); + } + }); + return false; +}; + +FileUploadDialog.prototype.getStartUploadHandler = function(){ + var me = this; var handler = function() { - var options = { - 'spinner': spinner, - 'uploadInputId': uploadInputId, - 'urlInput': urlInput.getElement(), - 'startUploadHandler': handler//pass in itself - }; - return ajaxFileUpload(options); + /* the trick is that we need inside the function call + * to have a reference to itself + * in order to reinstall the handler later + * because ajaxFileUpload jquery extension might be destroying it */ + return me.startFileUpload(handler); }; return handler; }; @@ -1098,10 +1251,11 @@ FileUploadDialog.prototype.createDom = function() { superClass.createDom.call(this); var form = this.makeElement('form'); + form.addClass('ajax-file-upload'); form.css('margin-bottom', 0); this.prependContent(form); - // File upload button + // Browser native file upload field var upload_input = this.makeElement('input'); upload_input.attr({ id: this._input_id, @@ -1111,9 +1265,32 @@ FileUploadDialog.prototype.createDom = function() { }); form.append(upload_input); this._upload_input = upload_input; - form.append($('<br/>')); - // The url input text box + var fakeInput = this.makeElement('input'); + fakeInput.attr('type', 'button'); + fakeInput.addClass('submit'); + fakeInput.addClass('fake-file-input'); + if (this._fileType === 'image') { + var buttonText = gettext('Choose an image to insert'); + } else { + var buttonText = gettext('Choose a file to insert'); + } + fakeInput.val(buttonText); + this._fakeInput = fakeInput; + form.append(fakeInput); + + setupButtonEventHandlers(fakeInput, function() { upload_input.click() }); + + // Label which will also serve as status display + var label = this.makeElement('label'); + label.attr('for', this._input_id); + var types = askbot['settings']['allowedUploadFileTypes']; + types = types.join(', '); + label.html(gettext('Allowed file types are:') + ' ' + types + '.'); + form.append(label); + this._label = label; + + // The url input text box, probably unused in fact var url_input = new TippedInput(); url_input.setInstruction(this._url_input_tooltip || gettext('Or paste file url here')); var url_input_element = url_input.getElement(); @@ -1125,15 +1302,6 @@ FileUploadDialog.prototype.createDom = function() { //form.append($('<br/>')); this._url_input = url_input; - var label = this.makeElement('label'); - label.attr('for', this._input_id); - - var types = askbot['settings']['allowedUploadFileTypes']; - types = types.join(', '); - label.html(gettext('Allowed file types are:') + ' ' + types + '.'); - form.append(label); - form.append($('<br/>')); - /* //Description input box var descr_input = new TippedInput(); descr_input.setInstruction(gettext('Describe the image here')); @@ -1143,8 +1311,9 @@ FileUploadDialog.prototype.createDom = function() { this._description_input = descr_input; */ var spinner = this.makeElement('img'); - spinner.attr('src', mediaUrl('media/images/indicator.gif')); + spinner.attr('src', mediaUrl('media/images/ajax-loader.gif')); spinner.css('display', 'none'); + spinner.addClass('spinner'); form.append(spinner); this._spinner = spinner; diff --git a/askbot/media/js/wmd/wmd.css b/askbot/media/js/wmd/wmd.css index 678d70f3..eeb6adec 100644 --- a/askbot/media/js/wmd/wmd.css +++ b/askbot/media/js/wmd/wmd.css @@ -3,46 +3,39 @@ background-color: White } */ -.wmd-panel -{ +.wmd-panel { } -#wmd-button-bar -{ +.wmd-button-bar { background: url(images/editor-toolbar-background.png) repeat-x bottom; - height: 30px; + height: 25px; border: 0; display: block; } -#wmd-input -{ +.wmd-input { height: 500px; background-color: Gainsboro; border: 1px solid DarkGray; margin-top: -20px; } -#wmd-preview -{ - background-color: LightSkyBlue; +.wmd-preview { + background-color: #f5f5f5; } -#wmd-output -{ +.wmd-output { background-color: Pink; } -#wmd-button-row -{ +.wmd-button-row { position: relative; - margin: 10px 2px 0 2px; + margin: 5px 2px; padding: 0px; height: 20px; } -.wmd-spacer -{ +.wmd-spacer { width: 1px; height: 20px; margin-left: 2px; @@ -53,8 +46,7 @@ list-style: none; } -.wmd-button -{ +.wmd-button { width: 20px; height: 20px; margin-left: 5px; @@ -68,8 +60,7 @@ list-style: none; } -.wmd-button > a -{ +.wmd-button > a { width: 20px; height: 20px; margin-left: 5px; @@ -81,23 +72,23 @@ /* sprite button slicing style information */ -#wmd-button-bar #wmd-bold-button {left: 0px; background-position: 0px 0;} -#wmd-button-bar #wmd-italic-button {left: 25px; background-position: -20px 0;} -#wmd-button-bar #wmd-spacer1 {left: 50px;} -#wmd-button-bar #wmd-link-button {left: 75px; background-position: -40px 0;} -#wmd-button-bar #wmd-quote-button {left: 100px; background-position: -60px 0;} -#wmd-button-bar #wmd-code-button {left: 125px; background-position: -80px 0;} -#wmd-button-bar #wmd-image-button {left: 150px; background-position: -100px 0;} -#wmd-button-bar #wmd-attachment-button {left: 175px; background-position: -120px 0;} -#wmd-button-bar #wmd-spacer2 {left: 200px;} -#wmd-button-bar #wmd-olist-button {left: 225px; background-position: -140px 0;} -#wmd-button-bar #wmd-ulist-button {left: 250px; background-position: -160px 0;} -#wmd-button-bar #wmd-heading-button {left: 275px; background-position: -180px 0;} -#wmd-button-bar #wmd-hr-button {left: 300px; background-position: -200px 0;} -#wmd-button-bar #wmd-spacer3 {left: 325px;} -#wmd-button-bar #wmd-undo-button {left: 350px; background-position: -220px 0;} -#wmd-button-bar #wmd-redo-button {left: 375px; background-position: -240px 0;} -#wmd-button-bar #wmd-help-button {right: 0px; background-position: -260px 0;} +.wmd-button-bar .wmd-bold-button {left: 0px; background-position: 0px 0;} +.wmd-button-bar .wmd-italic-button {left: 25px; background-position: -20px 0;} +.wmd-button-bar .wmd-spacer1 {left: 50px;} +.wmd-button-bar .wmd-link-button {left: 75px; background-position: -40px 0;} +.wmd-button-bar .wmd-quote-button {left: 100px; background-position: -60px 0;} +.wmd-button-bar .wmd-code-button {left: 125px; background-position: -80px 0;} +.wmd-button-bar .wmd-image-button {left: 150px; background-position: -100px 0;} +.wmd-button-bar .wmd-attachment-button {left: 175px; background-position: -120px 0;} +.wmd-button-bar .wmd-spacer2 {left: 200px;} +.wmd-button-bar .wmd-olist-button {left: 225px; background-position: -140px 0;} +.wmd-button-bar .wmd-ulist-button {left: 250px; background-position: -160px 0;} +.wmd-button-bar .wmd-heading-button {left: 275px; background-position: -180px 0;} +.wmd-button-bar .wmd-hr-button {left: 300px; background-position: -200px 0;} +.wmd-button-bar .wmd-spacer3 {left: 325px;} +.wmd-button-bar .wmd-undo-button {left: 350px; background-position: -220px 0;} +.wmd-button-bar .wmd-redo-button {left: 375px; background-position: -240px 0;} +.wmd-button-bar .wmd-help-button {right: 0px; background-position: -260px 0;} .wmd-prompt-background diff --git a/askbot/media/js/wmd/wmd.js b/askbot/media/js/wmd/wmd.js index ad56aee3..02ebf0c9 100644 --- a/askbot/media/js/wmd/wmd.js +++ b/askbot/media/js/wmd/wmd.js @@ -79,10 +79,10 @@ Attacklab.wmdBase = function(){ // A collection of the important regions on the page. // Cached so we don't have to keep traversing the DOM. wmd.PanelCollection = function(){ - this.buttonBar = doc.getElementById("wmd-button-bar"); - this.preview = doc.getElementById("previewer"); - this.output = doc.getElementById("wmd-output"); - this.input = doc.getElementById("editor"); + this.buttonBar = doc.getElementById(util.makeId("wmd-button-bar")); + this.preview = doc.getElementById(util.makeId("previewer")); + this.output = doc.getElementById(util.makeId("wmd-output")); + this.input = doc.getElementById(util.makeId("editor")); }; // This PanelCollection object can't be filled until after the page @@ -195,7 +195,7 @@ Attacklab.wmdBase = function(){ var imgPath = imageDirectory + img; var elem = doc.createElement("img"); - elem.className = "wmd-button"; + elem.className = "wmd-button wmd-image-button"; elem.src = imgPath; return elem; @@ -276,24 +276,21 @@ util.prompt = function(text, defaultInputText, makeLinkMarkdown, dialogType){ // so we make the whole window transparent. // // Is this necessary on modern konqueror browsers? - if (global.isKonqueror){ + if (global.isKonqueror) { style.backgroundColor = "transparent"; - } - else if (global.isIE){ + } else if (global.isIE) { style.filter = "alpha(opacity=50)"; - } - else { + } else { style.opacity = "0.5"; } var pageSize = position.getPageSize(); style.height = pageSize[1] + "px"; - if(global.isIE){ + if(global.isIE) { style.left = doc.documentElement.scrollLeft; style.width = doc.documentElement.clientWidth; - } - else { + } else { style.left = "0"; style.width = "100%"; } @@ -333,7 +330,7 @@ util.prompt = function(text, defaultInputText, makeLinkMarkdown, dialogType){ // The input text box input = doc.createElement("input"); if(dialogType == 'image' || dialogType == 'file'){ - input.id = "image-url"; + input.id = util.makeId("image-url"); } input.type = "text"; if (dialogType == 'file'){ @@ -932,8 +929,8 @@ util.prompt = function(text, defaultInputText, makeLinkMarkdown, dialogType){ var setUndoRedoButtonStates = function(){ if(undoMgr){ - setupButton(document.getElementById("wmd-undo-button"), undoMgr.canUndo()); - setupButton(document.getElementById("wmd-redo-button"), undoMgr.canRedo()); + setupButton(document.getElementById(util.makeId("wmd-undo-button")), undoMgr.canUndo()); + setupButton(document.getElementById(util.makeId("wmd-redo-button")), undoMgr.canRedo()); } }; @@ -981,19 +978,21 @@ util.prompt = function(text, defaultInputText, makeLinkMarkdown, dialogType){ }; var makeSpritedButtonRow = function(){ - var buttonBar = document.getElementById("wmd-button-bar"); + var buttonBar = document.getElementById(util.makeId("wmd-button-bar")); + buttonBar.className = 'wmd-button-bar'; var normalYShift = "0px"; var disabledYShift = "-20px"; var highlightYShift = "-40px"; var buttonRow = document.createElement("ul"); - buttonRow.id = "wmd-button-row"; + buttonRow.className = 'wmd-button-row'; + buttonRow.id = util.makeId("wmd-button-row"); buttonRow = buttonBar.appendChild(buttonRow); if (isButtonUsed('bold')){ var boldButton = document.createElement("li"); - boldButton.className = "wmd-button"; - boldButton.id = "wmd-bold-button"; + boldButton.className = "wmd-button wmd-bold-button"; + boldButton.id = util.makeId("wmd-bold-button"); boldButton.title = toolbar_strong_label; boldButton.XShift = "0px"; boldButton.textOp = command.doBold; @@ -1003,8 +1002,8 @@ util.prompt = function(text, defaultInputText, makeLinkMarkdown, dialogType){ if (isButtonUsed('italic')){ var italicButton = document.createElement("li"); - italicButton.className = "wmd-button"; - italicButton.id = "wmd-italic-button"; + italicButton.className = "wmd-button wmd-italic-button"; + italicButton.id = util.makeId("wmd-italic-button"); italicButton.title = toolbar_emphasis_label; italicButton.XShift = "-20px"; italicButton.textOp = command.doItalic; @@ -1020,15 +1019,15 @@ util.prompt = function(text, defaultInputText, makeLinkMarkdown, dialogType){ isButtonUsed('attachment') ) { var spacer1 = document.createElement("li"); - spacer1.className = "wmd-spacer"; - spacer1.id = "wmd-spacer1"; + spacer1.className = "wmd-spacer wmd-spacer1"; + spacer1.id = util.makeId("wmd-spacer1"); buttonRow.appendChild(spacer1); } if (isButtonUsed('link')){ var linkButton = document.createElement("li"); - linkButton.className = "wmd-button"; - linkButton.id = "wmd-link-button"; + linkButton.className = "wmd-button wmd-link-button"; + linkButton.id = util.makeId("wmd-link-button"); linkButton.title = toolbar_hyperlink_label; linkButton.XShift = "-40px"; linkButton.textOp = function(chunk, postProcessing){ @@ -1040,8 +1039,8 @@ util.prompt = function(text, defaultInputText, makeLinkMarkdown, dialogType){ if (isButtonUsed('blockquote')){ var quoteButton = document.createElement("li"); - quoteButton.className = "wmd-button"; - quoteButton.id = "wmd-quote-button"; + quoteButton.className = "wmd-button wmd-quote-button"; + quoteButton.id = util.makeId("wmd-quote-button"); quoteButton.title = toolbar_blockquote_label; quoteButton.XShift = "-60px"; quoteButton.textOp = command.doBlockquote; @@ -1051,8 +1050,8 @@ util.prompt = function(text, defaultInputText, makeLinkMarkdown, dialogType){ if (isButtonUsed('code')){ var codeButton = document.createElement("li"); - codeButton.className = "wmd-button"; - codeButton.id = "wmd-code-button"; + codeButton.className = "wmd-button wmd-code-button"; + codeButton.id = util.makeId("wmd-code-button"); codeButton.title = toolbar_code_label; codeButton.XShift = "-80px"; codeButton.textOp = command.doCode; @@ -1062,8 +1061,8 @@ util.prompt = function(text, defaultInputText, makeLinkMarkdown, dialogType){ if (isButtonUsed('image')){ var imageButton = document.createElement("li"); - imageButton.className = "wmd-button"; - imageButton.id = "wmd-image-button"; + imageButton.className = "wmd-button wmd-image-button"; + imageButton.id = util.makeId("wmd-image-button"); imageButton.title = toolbar_image_label; imageButton.XShift = "-100px"; imageButton.textOp = function(chunk, postProcessing){ @@ -1075,8 +1074,8 @@ util.prompt = function(text, defaultInputText, makeLinkMarkdown, dialogType){ if (isButtonUsed('attachment')){ var attachmentButton = document.createElement("li"); - attachmentButton.className = "wmd-button"; - attachmentButton.id = "wmd-attachment-button"; + attachmentButton.className = "wmd-button wmd-attachment-button"; + attachmentButton.id = util.makeId("wmd-attachment-button"); attachmentButton.title = toolbar_attachment_label; attachmentButton.XShift = "-120px"; attachmentButton.textOp = function(chunk, postProcessing){ @@ -1093,15 +1092,15 @@ util.prompt = function(text, defaultInputText, makeLinkMarkdown, dialogType){ isButtonUsed('hr') ) { var spacer2 = document.createElement("li"); - spacer2.className = "wmd-spacer"; - spacer2.id = "wmd-spacer2"; + spacer2.className = "wmd-spacer wmd-spacer2"; + spacer2.id = util.makeId("wmd-spacer2"); buttonRow.appendChild(spacer2); } if (isButtonUsed('ol')) { var olistButton = document.createElement("li"); - olistButton.className = "wmd-button"; - olistButton.id = "wmd-olist-button"; + olistButton.className = "wmd-button wmd-olist-button"; + olistButton.id = util.makeId("wmd-olist-button"); olistButton.title = toolbar_numbered_label; olistButton.XShift = "-140px"; olistButton.textOp = function(chunk, postProcessing){ @@ -1113,8 +1112,8 @@ util.prompt = function(text, defaultInputText, makeLinkMarkdown, dialogType){ if (isButtonUsed('ul')) { var ulistButton = document.createElement("li"); - ulistButton.className = "wmd-button"; - ulistButton.id = "wmd-ulist-button"; + ulistButton.className = "wmd-button wmd-ulist-button"; + ulistButton.id = util.makeId("wmd-ulist-button"); ulistButton.title = toolbar_bulleted_label; ulistButton.XShift = "-160px"; ulistButton.textOp = function(chunk, postProcessing){ @@ -1126,8 +1125,8 @@ util.prompt = function(text, defaultInputText, makeLinkMarkdown, dialogType){ if (isButtonUsed('heading')) { var headingButton = document.createElement("li"); - headingButton.className = "wmd-button"; - headingButton.id = "wmd-heading-button"; + headingButton.className = "wmd-button wmd-heading-button"; + headingButton.id = util.makeId("wmd-heading-button"); headingButton.title = toolbar_heading_label; headingButton.XShift = "-180px"; headingButton.textOp = command.doHeading; @@ -1137,8 +1136,8 @@ util.prompt = function(text, defaultInputText, makeLinkMarkdown, dialogType){ if (isButtonUsed('hr')) { var hrButton = document.createElement("li"); - hrButton.className = "wmd-button"; - hrButton.id = "wmd-hr-button"; + hrButton.className = "wmd-button wmd-hr-button"; + hrButton.id = util.makeId("wmd-hr-button"); hrButton.title = toolbar_horizontal_label; hrButton.XShift = "-200px"; hrButton.textOp = command.doHorizontalRule; @@ -1148,13 +1147,13 @@ util.prompt = function(text, defaultInputText, makeLinkMarkdown, dialogType){ if (isButtonUsed('undo')){ var spacer3 = document.createElement("li"); - spacer3.className = "wmd-spacer"; - spacer3.id = "wmd-spacer3"; + spacer3.className = "wmd-spacer wmd-spacer3"; + spacer3.id = util.makeId("wmd-spacer3"); buttonRow.appendChild(spacer3); var undoButton = document.createElement("li"); - undoButton.className = "wmd-button"; - undoButton.id = "wmd-undo-button"; + undoButton.className = "wmd-button wmd-undo-button"; + undoButton.id = util.makeId("wmd-undo-button"); undoButton.title = toolbar_undo_label; undoButton.XShift = "-220px"; undoButton.execute = function(manager){ @@ -1164,8 +1163,8 @@ util.prompt = function(text, defaultInputText, makeLinkMarkdown, dialogType){ buttonRow.appendChild(undoButton); var redoButton = document.createElement("li"); - redoButton.className = "wmd-button"; - redoButton.id = "wmd-redo-button"; + redoButton.className = "wmd-button wmd-redo-button"; + redoButton.id = util.makeId("wmd-redo-button"); redoButton.title = toolbar_redo_label; if (/win/.test(nav.platform.toLowerCase())) { redoButton.title = toolbar_redo_label; @@ -1184,8 +1183,8 @@ util.prompt = function(text, defaultInputText, makeLinkMarkdown, dialogType){ } /* var helpButton = document.createElement("li"); - helpButton.className = "wmd-button"; - helpButton.id = "wmd-help-button"; + helpButton.className = "wmd-button wmd-help-button"; + helpButton.id = util.makeId("wmd-help-button"); helpButton.XShift = "-240px"; helpButton.isHelp = true; @@ -1239,44 +1238,44 @@ util.prompt = function(text, defaultInputText, makeLinkMarkdown, dialogType){ switch(keyCodeStr) { case "b": - doClick(document.getElementById("wmd-bold-button")); + doClick(document.getElementById(util.makeId("wmd-bold-button"))); break; case "i": - doClick(document.getElementById("wmd-italic-button")); + doClick(document.getElementById(util.makeId("wmd-italic-button"))); break; case "l": - doClick(document.getElementById("wmd-link-button")); + doClick(document.getElementById(util.makeId("wmd-link-button"))); break; case ".": - doClick(document.getElementById("wmd-quote-button")); + doClick(document.getElementById(util.makeId("wmd-quote-button"))); break; case "k": - doClick(document.getElementById("wmd-code-button")); + doClick(document.getElementById(util.makeId("wmd-code-button"))); break; case "g": - doClick(document.getElementById("wmd-image-button")); + doClick(document.getElementById(util.makeId("wmd-image-button"))); break; case "o": - doClick(document.getElementById("wmd-olist-button")); + doClick(document.getElementById(util.makeId("wmd-olist-button"))); break; case "u": - doClick(document.getElementById("wmd-ulist-button")); + doClick(document.getElementById(util.makeId("wmd-ulist-button"))); break; case "h": - doClick(document.getElementById("wmd-heading-button")); + doClick(document.getElementById(util.makeId("wmd-heading-button"))); break; case "r": - doClick(document.getElementById("wmd-hr-button")); + doClick(document.getElementById(util.makeId("wmd-hr-button"))); break; case "y": - doClick(document.getElementById("wmd-redo-button")); + doClick(document.getElementById(util.makeId("wmd-redo-button"))); break; case "z": if(key.shiftKey) { - doClick(document.getElementById("wmd-redo-button")); + doClick(document.getElementById(util.makeId("wmd-redo-button"))); } else { - doClick(document.getElementById("wmd-undo-button")); + doClick(document.getElementById(util.makeId("wmd-undo-button"))); } break; default: @@ -1877,8 +1876,17 @@ util.prompt = function(text, defaultInputText, makeLinkMarkdown, dialogType){ wmd.wmd.editor = wmd.editor; wmd.wmd.previewManager = wmd.previewManager; }; + + util.makeId = function(idToken) { + if (wmd.wmd_env['idSeed']) { + return askbotMakeId(idToken, wmd.wmd_env['idSeed']); + } + return idToken; + }; - util.startEditor = function(start_now, buttons){ + util.startEditor = function(start_now, buttons, idSeed){ + + wmd.wmd_env['idSeed'] = idSeed; if (wmd.wmd_env.autostart === false) { util.makeAPI(); diff --git a/askbot/media/style/style.css b/askbot/media/style/style.css index 3b52c293..2c4f4209 100644 --- a/askbot/media/style/style.css +++ b/askbot/media/style/style.css @@ -144,6 +144,12 @@ html { height: 0; visibility: hidden; } +.invisible { + margin: -1px 0 0 -1px; + height: 1px; + overflow: hidden; + width: 1px; +} .badges a { color: #763333; text-decoration: underline; @@ -635,19 +641,20 @@ input[type="submit"], input[type="button"], input[type="reset"], .button { + border: 0 !important; + border-top: #eaf2f3 1px solid; cursor: pointer; color: #4a757f; - height: 27px; font-family: 'Open Sans Condensed', Arial, sans-serif; font-size: 14px; font-weight: bold; + height: 27px; + margin-right: 10px; text-align: center; text-decoration: none; text-shadow: 0px 1px 0px #c6d9dd; -moz-text-shadow: 0px 1px 0px #c6d9dd; -webkit-text-shadow: 0px 1px 0px #c6d9dd; - border: 0 !important; - border-top: #eaf2f3 1px solid; background-color: #d1e2e5; background-repeat: no-repeat; background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#d1e2e5), color-stop(25%, #d1e2e5), to(#a9c2c7)); @@ -703,6 +710,39 @@ input[type="submit"].link { input[type="submit"].link:hover { text-decoration: underline; } +form.ajax-file-upload { + height: 60px; + position: relative; +} +form.ajax-file-upload input[type="file"], +form.ajax-file-upload input.fake-file-input { + cursor: pointer; + height: 32px; + position: absolute; + top: 0; + left: 0; +} +form.ajax-file-upload input[type="file"] { + z-index: 2; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + filter: alpha(opacity=0); + -moz-opacity: 0; + -khtml-opacity: 0; + opacity: 0; +} +form.ajax-file-upload input.fake-file-input { + z-index: 1; +} +form.ajax-file-upload label, +form.ajax-file-upload img.spinner { + bottom: 0; + left: 3px; + position: absolute; +} +form.ajax-file-upload img.spinner { + bottom: 6px; + left: 10px; +} #askButton { /* check blocks/secondary_header.html and widgets/ask_button.html*/ @@ -1775,6 +1815,13 @@ ul#related-tags li { width: 723px; width: 100%; } +.ask-page .post-comments .wmd-container, +.question-page .post-comments .wmd-container, +.edit-question-page .post-comments .wmd-container, +.edit-answer-page .post-comments .wmd-container { + margin-bottom: 8px; + margin-left: -2px; +} .ask-page #editor, .question-page #editor, .edit-question-page #editor, @@ -1992,6 +2039,10 @@ ul#related-tags li { width: 20px; vertical-align: top; } +.question-page .answer-table .mceEditor td, +.question-page #question-table .mceEditor td { + width: auto; +} .question-page .question-body, .question-page .answer-body { overflow: auto; @@ -2270,6 +2321,12 @@ ul#related-tags li { width: 100%; margin: 3px 0 20px 5px; } +.question-page .comments .edit-comment-buttons { + margin-left: -4px; +} +.question-page .comments .edit-comment-buttons .checkbox { + margin: 3px; +} .question-page .comments .controls a { border: none; color: #988e4c; @@ -2307,15 +2364,24 @@ ul#related-tags li { .question-page .comments textarea { box-sizing: border-box; border: #cce6ec 3px solid; + color: #666; font-family: Arial; font-size: 13px; height: 54px; line-height: 1.3; - margin: -1px 0 7px 1px; + margin: -1px 0 0 1px; outline: none; overflow: auto; - padding: 0px 19px 2px 3px; - width: 100%; + padding: 5px 19px 2px 3px; + width: 99.6%; +} +.question-page .comments .wmd-container textarea { + border: none; +} +.question-page .comments .transient-comment { + margin-bottom: 3px; + /* match paragraph style */ + } .question-page .comments input { margin-left: 10px; @@ -2323,6 +2389,13 @@ ul#related-tags li { vertical-align: top; width: 100px; } +.question-page .comments input[name="suppress_email"] { + margin: 4px 5px 0 0; + width: auto; +} +.question-page .comments label[for="suppress_email"] { + vertical-align: top; +} .question-page .comments button.submit { height: 26px; line-height: 26px; @@ -2333,7 +2406,6 @@ ul#related-tags li { display: inline-block; width: 245px; float: right; - color: #b6a475 !important; vertical-align: top; font-family: Arial; float: right; @@ -2349,6 +2421,9 @@ ul#related-tags li { font-size: 11px; min-height: 25px; } +.question-page .comments .comment:last-child { + border-bottom: none; +} .question-page .comments div.comment:hover { background-color: #efefef; } @@ -3115,16 +3190,16 @@ body.main-page ins { /* ----- Red Popup notification ----- */ .vote-notification { z-index: 1; + background-color: #8e0000; + color: white; cursor: pointer; display: none; - position: absolute; font-family: Arial; font-size: 14px; font-weight: normal; - color: white; - background-color: #8e0000; - text-align: center; padding-bottom: 10px; + position: absolute; + text-align: center; -webkit-box-shadow: 0px 2px 4px #370000; -moz-box-shadow: 0px 2px 4px #370000; box-shadow: 0px 2px 4px #370000; @@ -3141,6 +3216,7 @@ body.main-page ins { margin-bottom: 5px; border-top: #8e0000 1px solid; color: #fff; + line-height: 20px; font-weight: normal; border-top-right-radius: 4px; border-top-left-radius: 4px; diff --git a/askbot/media/style/style.less b/askbot/media/style/style.less index a4c3a6b2..5ee2eab3 100644 --- a/askbot/media/style/style.less +++ b/askbot/media/style/style.less @@ -146,6 +146,13 @@ html { visibility: hidden; } +.invisible { + margin: -1px 0 0 -1px; + height: 1px; + overflow: hidden; + width: 1px; +} + .badges a { color: #763333; text-decoration: underline; @@ -683,17 +690,18 @@ input[type="submit"], input[type="button"], input[type="reset"], .button { + border: 0 !important; + border-top: #eaf2f3 1px solid; cursor: pointer; color: @button-label; - height: 27px; font-family: @main-font; font-size: 14px; font-weight: bold; + height: 27px; + margin-right: 10px; text-align: center; text-decoration: none; .text-shadow(0px,1px,0px,#c6d9dd); - border: 0 !important; - border-top: #eaf2f3 1px solid; .linear-gradient(#d1e2e5,#a9c2c7); .rounded-corners(4px); .box-shadow(1px, 1px, 2px, #636363) @@ -727,6 +735,40 @@ input[type="submit"].link:hover { text-decoration: underline; } +form.ajax-file-upload { + height: 60px; + position: relative; + input[type="file"], + input.fake-file-input { + cursor: pointer; + height: 32px; + position: absolute; + top: 0; + left: 0; + } + input[type="file"] { + z-index: 2; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + filter: alpha(opacity=0); + -moz-opacity: 0; + -khtml-opacity: 0; + opacity: 0; + } + input.fake-file-input { + z-index: 1; + } + label, + img.spinner { + bottom: 0; + left: 3px; + position: absolute; + } + img.spinner { + bottom: 6px; + left: 10px; + } +} + #askButton { /* check blocks/secondary_header.html and widgets/ask_button.html*/ float:right; font-size: 20px; @@ -1873,6 +1915,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; @@ -2041,9 +2087,9 @@ ul#related-tags li { /* ----- Question template ----- */ -.question-page{ +.question-page { - h1{ + h1 { padding-top:0px; font-family:@main-font; @@ -2106,6 +2152,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; @@ -2364,6 +2414,13 @@ ul#related-tags li { width: 100%; margin: 3px 0 20px 5px; } + + .edit-comment-buttons { + margin-left: -4px; + .checkbox { + margin: 3px; + } + } .controls a { border: none; @@ -2402,15 +2459,22 @@ ul#related-tags li { textarea { box-sizing: border-box; border: #cce6ec 3px solid; + color: #666; font-family: @body-font; font-size: 13px; height: 54px; line-height: 1.3; - margin: -1px 0 7px 1px; + margin: -1px 0 0 1px; outline: none; overflow:auto; - padding: 0px 19px 2px 3px; - width:100%; + padding: 5px 19px 2px 3px; + width: 99.6%; + } + .wmd-container textarea { + border: none; + } + .transient-comment { + margin-bottom: 3px; /* match paragraph style */ } input { margin-left: 10px; @@ -2418,6 +2482,15 @@ ul#related-tags li { vertical-align: top; width: 100px; } + + input[name="suppress_email"] { + margin: 4px 5px 0 0; + width: auto; + } + label[for="suppress_email"] { + vertical-align: top; + } + button.submit { height: 26px; line-height: 26px; @@ -2428,7 +2501,6 @@ ul#related-tags li { display: inline-block; width: 245px; float:right; - color:#b6a475 !important; vertical-align: top; font-family:@body-font; float:right; @@ -2444,6 +2516,9 @@ ul#related-tags li { font-size: 11px; min-height: 25px; } + .comment:last-child { + border-bottom: none; + } div.comment:hover { background-color: #efefef; } @@ -3232,28 +3307,29 @@ body.main-page 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 65ea535d..42905f65 100644 --- a/askbot/models/post.py +++ b/askbot/models/post.py @@ -1,10 +1,8 @@ from collections import defaultdict import datetime import operator -import cgi import logging -from django.utils.html import strip_tags from django.contrib.sitemaps import ping_google from django.utils import html from django.conf import settings as django_settings @@ -35,7 +33,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 @@ -422,27 +420,8 @@ class Post(models.Model): removed_mentions - list of mention <Activity> objects - for removed ones """ - 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)) + text_converter = self.get_text_converter() + text = text_converter(self.text) #todo, add markdown parser call conditional on #self.use_markdown flag @@ -616,6 +595,25 @@ class Post(models.Model): return answer + def get_text_converter(self): + have_simple_comment = ( + self.is_comment() and + askbot_settings.COMMENTS_EDITOR_TYPE == 'plain-text' + ) + if have_simple_comment: + parser_type = 'plain-text' + else: + parser_type = askbot_settings.EDITOR_TYPE + + if parser_type == 'plain-text': + return markup.plain_text_input_converter + elif parser_type == 'markdown': + return markup.markdown_input_converter + elif parser_type == 'tinymce': + return markup.tinymce_input_converter + else: + raise NotImplementedError + def has_group(self, group): """true if post belongs to the group""" return self.groups.filter(id=group.id).exists() 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/search/postgresql/__init__.py b/askbot/search/postgresql/__init__.py index e42190a8..d71b824f 100644 --- a/askbot/search/postgresql/__init__.py +++ b/askbot/search/postgresql/__init__.py @@ -65,6 +65,11 @@ def run_full_text_search(query_set, query_text, text_search_vector_name): language_code = get_language() + #a hack with japanese search for the short queries + if language_code == 'ja' and len(query_text) in (1, 2): + mul = 4/len(query_text) #4 for 1 and 2 for 2 + query_text = (query_text + ' ')*mul + #the table name is a hack, because user does not have the language code is_multilingual = getattr(django_settings, 'ASKBOT_MULTILINGUAL', True) if is_multilingual and table_name == 'askbot_thread': 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/embed/ask_by_widget.html b/askbot/templates/embed/ask_by_widget.html index dc3db806..dae9f598 100644 --- a/askbot/templates/embed/ask_by_widget.html +++ b/askbot/templates/embed/ask_by_widget.html @@ -209,7 +209,7 @@ <script type="text/javascript" src='{{"/js/live_search_new_thread.js"|media}}'></script> <script type="text/javascript" charset="utf-8"> - askbot['settings']['minSearchWordLength'] = {{settings.MIN_SEARCH_WORD_LENGTH}}; + askbot['settings']['minSearchWordLength'] = {{ min_search_word_length }}; askbot['urls']['titleSearch'] = '{% url title_search %}'; askbot['urls']['upload'] = '{% url upload %}'; $(document).ready(function(){ diff --git a/askbot/templates/meta/bottom_scripts.html b/askbot/templates/meta/bottom_scripts.html index 86a2490b..aa63560f 100644 --- a/askbot/templates/meta/bottom_scripts.html +++ b/askbot/templates/meta/bottom_scripts.html @@ -27,9 +27,19 @@ askbot['urls']['questions'] = '{% url "questions" %}'; askbot['settings']['groupsEnabled'] = {{ settings.GROUPS_ENABLED|as_js_bool }}; askbot['settings']['static_url'] = '{{ settings.STATIC_URL }}'; - askbot['settings']['minSearchWordLength'] = {{ settings.MIN_SEARCH_WORD_LENGTH }}; + askbot['settings']['minSearchWordLength'] = {{ min_search_word_length }}; askbot['settings']['mathjaxEnabled'] = {{ settings.ENABLE_MATHJAX|as_js_bool }}; askbot['settings']['sharingSuffixText'] = '{{ settings.SHARING_SUFFIX_TEXT|escape }}'; + askbot['data']['maxCommentLength'] = {{ settings.MAX_COMMENT_LENGTH }}; + askbot['settings']['editorType'] = '{{ settings.EDITOR_TYPE }}'; + askbot['settings']['commentsEditorType'] = '{{ settings.COMMENTS_EDITOR_TYPE }}'; + {% if settings.ALLOWED_UPLOAD_FILE_TYPES %} + askbot['settings']['allowedUploadFileTypes'] = [ + "{{ settings.ALLOWED_UPLOAD_FILE_TYPES|join('", "')|replace('.','') }}" + ]; + {% else %} + askbot['settings']['allowedUploadFileTypes'] = []; + {% endif %} askbot['data']['haveFlashNotifications'] = {{ user_messages|as_js_bool }}; askbot['data']['activeTab'] = '{{ active_tab }}'; {% if search_state %} @@ -58,6 +68,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 var activeTab = askbot['data']['activeTab']; diff --git a/askbot/templates/meta/html_head_javascript.html b/askbot/templates/meta/html_head_javascript.html index 67f0ec88..965dd350 100644 --- a/askbot/templates/meta/html_head_javascript.html +++ b/askbot/templates/meta/html_head_javascript.html @@ -3,6 +3,7 @@ var askbot = {}; askbot['data'] = {}; askbot['data']['userIsAuthenticated'] = {{ request.user.is_authenticated()|as_js_bool }}; + askbot['data']['languageCode'] = '{{ current_language_code }}'; {% if request.user.is_authenticated() %} askbot['data']['userId'] = {{ request.user.id }}; askbot['data']['userName'] = '{{ request.user.username }}'; @@ -12,17 +13,8 @@ {% else %} askbot['data']['userReputation'] = 0; {% endif %} - askbot['data']['maxCommentLength'] = {{ settings.MAX_COMMENT_LENGTH }}; askbot['urls'] = {}; askbot['settings'] = {}; - askbot['settings']['editorType'] = '{{ settings.EDITOR_TYPE }}'; - {% if settings.ALLOWED_UPLOAD_FILE_TYPES %} - askbot['settings']['allowedUploadFileTypes'] = [ - "{{ settings.ALLOWED_UPLOAD_FILE_TYPES|join('", "')|replace('.','') }}" - ]; - {% else %} - askbot['settings']['allowedUploadFileTypes'] = []; - {% endif %} askbot['messages'] = {}; </script> <script type="text/javascript" src="{% url django.views.i18n.javascript_catalog %}"></script> diff --git a/askbot/templates/question.html b/askbot/templates/question.html index 98cb2502..c11fba04 100644 --- a/askbot/templates/question.html +++ b/askbot/templates/question.html @@ -178,48 +178,15 @@ } } 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']){ @@ -312,6 +279,7 @@ askbot['settings']['saveCommentOnEnter'] = {{ settings.SAVE_COMMENT_ON_ENTER|as_js_bool }}; askbot['settings']['tagSource'] = '{{ settings.TAG_SOURCE }}'; askbot['settings']['enableSharingGoogle'] = {{ settings.ENABLE_SHARING_GOOGLE|as_js_bool }}; + askbot['settings']['enableEmailAlerts'] = {{ settings.ENABLE_EMAIL_ALERTS|as_js_bool }}; </script> {% include "meta/editor_data.html" %} {% compress js %} @@ -339,4 +307,3 @@ </script> #} {% endblock %} - diff --git a/askbot/templates/question/content.html b/askbot/templates/question/content.html index 82185919..d07c3727 100644 --- a/askbot/templates/question/content.html +++ b/askbot/templates/question/content.html @@ -24,11 +24,17 @@ {# buttons below cannot be cached yet #} {% if user_already_gave_answer %} +<div style="margin-top: 15px"> <a class="button submit" 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 class="invisible"> + {# hidden because we still need js from the tinymce widget #} + {% include "question/new_answer_form.html" %} + </div> +</div> {% else %} {% include "question/new_answer_form.html" %} {% endif %} 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/utils/markup.py b/askbot/utils/markup.py index ac96ec74..b96cf42d 100644 --- a/askbot/utils/markup.py +++ b/askbot/utils/markup.py @@ -7,6 +7,8 @@ import re import logging from askbot import const from askbot.conf import settings as askbot_settings +from askbot.utils.html import sanitize_html, strip_tags +from django.utils.html import urlize from markdown2 import Markdown #url taken from http://regexlib.com/REDetails.aspx?regexp_id=501 by Brian Bothwell URL_RE = re.compile("((?<!(href|.src|data)=['\"])((http|https|ftp)\://([a-zA-Z0-9\.\-]+(\:[a-zA-Z0-9\.&%\$\-]+)*@)*((25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9])\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9]|0)\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9]|0)\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[0-9])|localhost|([a-zA-Z0-9\-]+\.)*[a-zA-Z0-9\-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(\:[0-9]+)*(/($|[a-zA-Z0-9\.\,\?\'\\\+&%\$#\=~_\-]+))*))") @@ -189,3 +191,18 @@ def mentionize_text(text, anticipated_authors): #append the rest of text that did not have @ symbols output += text return mentioned_authors, output + +def plain_text_input_converter(text): + """plain text to html converter""" + return sanitize_html(urlize('<p>' + text + '</p>')) + +def markdown_input_converter(text): + """markdown to html converter""" + text = urlize(text) + text = get_parser().convert(text) + return sanitize_html(text) + +def tinymce_input_converter(text): + """tinymce input to production html converter""" + text = urlize(text) + return strip_tags(text, ['script', 'style', 'link']) diff --git a/askbot/views/commands.py b/askbot/views/commands.py index f810a750..133ef70e 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 4ad7ea7b..e8d50cea 100644 --- a/askbot/views/writers.py +++ b/askbot/views/writers.py @@ -62,7 +62,6 @@ ANSWERS_PAGE_SIZE = 10 def upload(request):#ajax upload file to a question or answer """view that handles file upload via Ajax """ - # check upload permission result = '' error = '' @@ -81,10 +80,11 @@ def upload(request):#ajax upload file to a question or answer raise exceptions.PermissionDenied('invalid upload file name prefix') #todo: check file type - f = request.FILES['file-upload']#take first file + uploaded_file = request.FILES['file-upload']#take first file + orig_file_name = uploaded_file.name #todo: extension checking should be replaced with mimetype checking #and this must be part of the form validation - file_extension = os.path.splitext(f.name)[1].lower() + file_extension = os.path.splitext(orig_file_name)[1].lower() if not file_extension in settings.ASKBOT_ALLOWED_UPLOAD_FILE_TYPES: file_types = "', '".join(settings.ASKBOT_ALLOWED_UPLOAD_FILE_TYPES) msg = _("allowed file types are '%(file_types)s'") % \ @@ -93,7 +93,7 @@ def upload(request):#ajax upload file to a question or answer # generate new file name and storage object file_storage, new_file_name, file_url = store_file( - f, file_name_prefix + uploaded_file, file_name_prefix ) # check file size # byte @@ -123,7 +123,7 @@ def upload(request):#ajax upload file to a question or answer #}) #return HttpResponse(data, mimetype = 'application/json') xml_template = "<result><msg><![CDATA[%s]]></msg><error><![CDATA[%s]]></error><file_url>%s</file_url><orig_file_name><![CDATA[%s]]></orig_file_name></result>" - xml = xml_template % (result, error, file_url, f.name) + xml = xml_template % (result, error, file_url, orig_file_name) return HttpResponse(xml, mimetype="application/xml") @@ -658,7 +658,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 @@ -687,6 +687,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', '') @@ -695,11 +699,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(): @@ -708,47 +728,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')) form = forms.EditCommentForm(request.POST) if form.is_valid() == False: - return HttpResponseBadRequest() - - comment_id = form.cleaned_data['comment_id'] - suppress_email = form.cleaned_data['suppress_email'] + raise exceptions.PermissionDenied('This content is forbidden') - comment_post = models.Post.objects.get(post_type='comment', id=comment_id) + comment_post = models.Post.objects.get( + post_type='comment', + id=form.cleaned_data['comment_id'] + ) request.user.edit_comment( comment_post=comment_post, - body_text = request.POST['comment'], - suppress_email=suppress_email + body_text=form.cleaned_data['comment'], + suppress_email=form.cleaned_data['suppress_email'] ) - is_deletable = template_filters.can_delete_comment(comment_post.author, comment_post) - 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, |