diff options
author | Evgeny Fadeev <evgeny.fadeev@gmail.com> | 2014-07-16 17:31:50 -0300 |
---|---|---|
committer | Evgeny Fadeev <evgeny.fadeev@gmail.com> | 2014-07-16 17:31:50 -0300 |
commit | 490243b93e7237312ee913eff9b1b296f764df8d (patch) | |
tree | 290d8c19741d3bd3ddadaf18065d62d2d48245aa | |
parent | bfd4497f930c0c6e804eb7eaae24fb95ccb64789 (diff) | |
parent | ac721d1aa97372eef3b7d1772dc7f40b7c8a860d (diff) | |
download | askbot-490243b93e7237312ee913eff9b1b296f764df8d.tar.gz askbot-490243b93e7237312ee913eff9b1b296f764df8d.tar.bz2 askbot-490243b93e7237312ee913eff9b1b296f764df8d.zip |
Merge branch 'master' into g-plus
49 files changed, 2092 insertions, 946 deletions
@@ -18,7 +18,7 @@ All documentation is in the directory askbot/doc To contribute code, please fork and make pull requests. If you are planning to add a new feature, please bring it up for discussion at our forum -(http://askbot.org/en/questions/) and mention that are willing to develop this feature. +(http://askbot.org/en/questions/) and mention that you are willing to develop this feature. We will merge obvious bug fixes without questions, for more complex fixes please add a test case that fails before and passes after applying your fix. diff --git a/askbot/__init__.py b/askbot/__init__.py index 0e4ba506..3515cdba 100644 --- a/askbot/__init__.py +++ b/askbot/__init__.py @@ -38,6 +38,7 @@ REQUIREMENTS = { 'longerusername': 'longerusername', 'bs4': 'beautifulsoup4', 'picklefield': 'django-picklefield==0.3.0', + #'stopforumspam': 'stopforumspam' } if platform.system() != 'Windows': diff --git a/askbot/conf/moderation.py b/askbot/conf/moderation.py index b537663f..a9e12afe 100644 --- a/askbot/conf/moderation.py +++ b/askbot/conf/moderation.py @@ -4,29 +4,30 @@ from askbot.conf.settings_wrapper import settings from askbot.conf.super_groups import DATA_AND_FORMATTING from askbot.deps.livesettings import ConfigurationGroup from askbot.deps.livesettings import BooleanValue +from askbot.deps.livesettings import StringValue from django.core.cache import cache from django.utils.translation import ugettext_lazy as _ -def empty_cache_callback(old_value, new_value): - """used to clear cache on change of certain values""" - if old_value != new_value: - #todo: change this to warmup cache - cache.clear() - return new_value - MODERATION = ConfigurationGroup( 'MODERATION', _('Content moderation'), - super_group = DATA_AND_FORMATTING + super_group=DATA_AND_FORMATTING ) +CONTENT_MODERATION_MODE_CHOICES = ( + ('flags', _('audit flagged posts')), + ('audit', _('audit flagged posts and watched users')), + ('premoderation', _('pre-moderate watched users and audit flagged posts')), +) + settings.register( - BooleanValue( + StringValue( MODERATION, - 'ENABLE_CONTENT_MODERATION', - default = False, - description = _('Enable content moderation'), - update_callback = empty_cache_callback + 'CONTENT_MODERATION_MODE', + choices=CONTENT_MODERATION_MODE_CHOICES, + default='flags', + description=_('Content moderation method'), + help_text=_("Audit is made after the posts are published, pre-moderation prevents publishing before moderator's decision.") ) ) @@ -34,9 +35,9 @@ settings.register( BooleanValue( MODERATION, 'ENABLE_TAG_MODERATION', - default = False, - description = _('Enable tag moderation'), - help_text = _( + default=False, + description=_('Enable tag moderation'), + help_text=_( 'If enabled, any new tags will not be applied ' 'to the questions, but emailed to the moderators. ' 'To use this feature, tags must be optional.' diff --git a/askbot/const/message_keys.py b/askbot/const/message_keys.py index bb990d5e..315c2400 100644 --- a/askbot/const/message_keys.py +++ b/askbot/const/message_keys.py @@ -45,3 +45,4 @@ CANNOT_PERFORM_ACTION_UNTIL = _('Sorry, you will be able to %(perform_action)s a MODERATORS_OR_AUTHOR_CAN_PEFROM_ACTION = _( 'Sorry, only moderators or the %(post_author)s %(perform_action)s' ) +PUNISHED_USER_INFO = _('Your account might be blocked in error - please contact the site administrators, if you think so.') diff --git a/askbot/context.py b/askbot/context.py index 7de6cf0d..b0855b0b 100644 --- a/askbot/context.py +++ b/askbot/context.py @@ -37,6 +37,7 @@ def application_settings(request): settings.ASKBOT_ALLOWED_UPLOAD_FILE_TYPES my_settings['ASKBOT_URL'] = settings.ASKBOT_URL my_settings['STATIC_URL'] = settings.STATIC_URL + my_settings['IP_MODERATION_ENABLED'] = getattr(settings, 'ASKBOT_IP_MODERATION_ENABLED', False) my_settings['ASKBOT_CSS_DEVEL'] = getattr( settings, 'ASKBOT_CSS_DEVEL', diff --git a/askbot/doc/source/changelog.rst b/askbot/doc/source/changelog.rst index 67928fbc..308ee4c6 100644 --- a/askbot/doc/source/changelog.rst +++ b/askbot/doc/source/changelog.rst @@ -3,6 +3,9 @@ Changes in Askbot Development master branch (only on github) ------------------------------------------ +* Improved moderation modes: flags, audit, premoderation. + Watched user status, IP blocking, mass content removal. +* Allow bulk deletion of user content simultaneously with blocking * Allow custom destination url under the logo * Option to allow asking without registration (Egil Moeller) * Implemented Mozilla Persona authentication diff --git a/askbot/forms.py b/askbot/forms.py index e3c6c253..e7e869cd 100644 --- a/askbot/forms.py +++ b/askbot/forms.py @@ -640,6 +640,7 @@ class ChangeUserStatusForm(forms.Form): """ user_status = forms.ChoiceField(label=_('Change status to')) + delete_content = forms.CharField(widget=forms.HiddenInput, initial='false') def __init__(self, *arg, **kwarg): @@ -676,6 +677,15 @@ class ChangeUserStatusForm(forms.Form): self.moderator = moderator self.subject = subject + def clean_delete_content(self): + delete = self.cleaned_data.get('delete_content', False) + if delete == 'true': + delete = True + else: + delete = False + self.cleaned_data['delete_content'] = delete + return self.cleaned_data['delete_content'] + def clean(self): #if moderator is looking at own profile - do not #let change status @@ -717,6 +727,10 @@ class ChangeUserStatusForm(forms.Form): ) % {'username': self.subject.username} raise forms.ValidationError(msg) + if user_status not in ('s', 'b'):#not blocked or suspended + if self.cleaned_data['delete_content'] == True: + self.cleaned_data['delete_content'] = False + return self.cleaned_data @@ -761,8 +775,9 @@ class FeedbackForm(forms.Form): def clean(self): super(FeedbackForm, self).clean() if self.user and self.user.is_anonymous(): - if not self.cleaned_data['no_email'] \ - and not self.cleaned_data['email']: + need_email = not bool(self.cleaned_data.get('no_email', False)) + email = self.cleaned_data.get('email', '').strip() + if need_email and email == '': msg = _('Please mark "I dont want to give my mail" field.') self._errors['email'] = self.error_class([msg]) @@ -1143,7 +1158,7 @@ class AnswerForm(PostAsSomeoneForm, PostPrivatelyForm): return len(stripped_text) > 0 #People can override this function to save their additional fields to db - def save(self, question, user): + def save(self, question, user, ip_addr=None): wiki = self.cleaned_data['wiki'] text = self.cleaned_data['text'] is_private = self.cleaned_data['post_privately'] @@ -1154,6 +1169,7 @@ class AnswerForm(PostAsSomeoneForm, PostPrivatelyForm): wiki = wiki, is_private = is_private, timestamp = datetime.datetime.now(), + ip_addr=ip_addr ) class VoteForm(forms.Form): diff --git a/askbot/importers/stackexchange/management/commands/load_stackexchange.py b/askbot/importers/stackexchange/management/commands/load_stackexchange.py index 902bc988..5cefd93f 100644 --- a/askbot/importers/stackexchange/management/commands/load_stackexchange.py +++ b/askbot/importers/stackexchange/management/commands/load_stackexchange.py @@ -573,7 +573,7 @@ it may be helpful to split this procedure in two:\n def mark_activity(self,p,u,t): """p,u,t - post, user, timestamp """ - p.thread.set_last_activity(last_activity_by=u, last_activity_at=t) + p.thread.set_last_activity_info(last_activity_by=u, last_activity_at=t) def _process_post_rollback_revision_group(self, rev_group): #todo: don't know what to do here as there were no diff --git a/askbot/management/commands/send_email_alerts.py b/askbot/management/commands/send_email_alerts.py index 1f04690f..b7b594ef 100644 --- a/askbot/management/commands/send_email_alerts.py +++ b/askbot/management/commands/send_email_alerts.py @@ -143,7 +143,7 @@ class Command(NoArgsCommand): thread__closed=True ).order_by('-thread__last_activity_at') - if askbot_settings.ENABLE_CONTENT_MODERATION: + if askbot_settings.CONTENT_MODERATION_MODE == 'premoderation': base_qs = base_qs.filter(approved = True) #todo: for some reason filter on did not work as expected ~Q(viewed__who=user) | # Q(viewed__who=user,viewed__when__lt=F('thread__last_activity_at')) diff --git a/askbot/media/js/post.js b/askbot/media/js/post.js index d077ce9e..2efe0bf8 100644 --- a/askbot/media/js/post.js +++ b/askbot/media/js/post.js @@ -539,8 +539,6 @@ var Vote = function(){ var subscribeAnonymousMessage = gettext('anonymous users cannot subscribe to questions') + pleaseLogin; var voteAnonymousMessage = gettext('anonymous users cannot vote') + pleaseLogin; //there were a couple of more messages... - var offensiveConfirmation = gettext('please confirm offensive'); - var removeOffensiveConfirmation = gettext('please confirm removal of offensive flag'); var offensiveAnonymousMessage = gettext('anonymous users cannot flag offensive posts') + pleaseLogin; var removeConfirmation = gettext('confirm delete'); var removeAnonymousMessage = gettext('anonymous users cannot delete/undelete') + pleaseLogin; @@ -1069,10 +1067,8 @@ var Vote = function(){ ); return false; } - if (confirm(offensiveConfirmation)){ - postId = object.id.substr(object.id.lastIndexOf('-') + 1); - submit(object, voteType, callback_offensive); - } + postId = object.id.substr(object.id.lastIndexOf('-') + 1); + submit(object, voteType, callback_offensive); }, //remove flag offensive remove_offensive: function(object, voteType){ @@ -1089,10 +1085,8 @@ var Vote = function(){ ); return false; } - if (confirm(removeOffensiveConfirmation)){ - postId = object.id.substr(object.id.lastIndexOf('-') + 1); - submit(object, voteType, callback_remove_offensive); - } + postId = object.id.substr(object.id.lastIndexOf('-') + 1); + submit(object, voteType, callback_remove_offensive); }, remove_all_offensive: function(object, voteType){ if (!currentUserId || currentUserId.toUpperCase() == "NONE"){ @@ -1108,10 +1102,8 @@ var Vote = function(){ ); return false; } - if (confirm(removeOffensiveConfirmation)){ - postId = object.id.substr(object.id.lastIndexOf('-') + 1); - submit(object, voteType, callback_remove_all_offensive); - } + postId = object.id.substr(object.id.lastIndexOf('-') + 1); + submit(object, voteType, callback_remove_all_offensive); }, //delete question or answer (comments are deleted separately) remove: function(object, voteType){ diff --git a/askbot/media/js/user.js b/askbot/media/js/user.js index 90f35f2f..a116876c 100644 --- a/askbot/media/js/user.js +++ b/askbot/media/js/user.js @@ -1,141 +1,16 @@ -//todo: refactor this into "Inbox" object or more specialized var setup_inbox = function(){ - - var getSelected = function(){ - - var id_list = new Array(); - var elements = $('#responses input:checked').parent(); - - elements.each(function(index, element){ - var id = $(element).attr('id').replace(/^re_/,''); - id_list.push(id); - }); - - if (id_list.length === 0){ - alert(gettext('Please select at least one item')); - } - - return {id_list: id_list, elements: elements}; - }; - - var submit = function(id_list, elements, action_type){ - if (action_type == 'delete' || action_type == 'mark_new' || action_type == 'mark_seen' || action_type == 'remove_flag' || action_type == 'delete_post'){ - $.ajax({ - type: 'POST', - cache: false, - dataType: 'json', - data: JSON.stringify({memo_list: id_list, action_type: action_type}), - url: askbot['urls']['manageInbox'], - success: function(response_data){ - if (response_data['success'] == true){ - if (action_type == 'delete' || action_type == 'remove_flag' || action_type == 'delete_post'){ - elements.remove(); - } - else if (action_type == 'mark_new'){ - elements.addClass('highlight'); - elements.addClass('new'); - elements.removeClass('seen'); - } - else if (action_type == 'mark_seen'){ - elements.removeClass('highlight'); - elements.addClass('seen'); - elements.removeClass('new'); - } - } - else { - showMessage($('#responses'), response_data['message']); - } - } - }); - } - }; - - var startAction = function(action_type){ - var data = getSelected(); - if (data['id_list'].length === 0){ - return; - } - if (action_type == 'delete'){ - msg = ngettext('Delete this notification?', - 'Delete these notifications?', data['id_list'].length); - if (confirm(msg) === false){ - return; - } - } - if (action_type == 'close'){ - msg = ngettext('Close this entry?', - 'Close these entries?', data['id_list'].length); - if (confirm(msg) === false){ - return; - } - } - if (action_type == 'remove_flag'){ - msg = ngettext( - 'Remove all flags and approve this entry?', - 'Remove all flags and approve these entries?', - data['id_list'].length - ); - if (confirm(msg) === false){ - return; - } - } - submit(data['id_list'], data['elements'], action_type); - }; - setupButtonEventHandlers($('#re_mark_seen'), function(){startAction('mark_seen')}); - setupButtonEventHandlers($('#re_mark_new'), function(){startAction('mark_new')}); - setupButtonEventHandlers($('#re_dismiss'), function(){startAction('delete')}); - setupButtonEventHandlers($('#re_remove_flag'), function(){startAction('remove_flag')}); - //setupButtonEventHandlers($('#re_close'), function(){startAction('close')}); - setupButtonEventHandlers( - $('#sel_all'), - function(){ - setCheckBoxesIn('#responses .new', true); - setCheckBoxesIn('#responses .seen', true); - } - ); - setupButtonEventHandlers( - $('#sel_seen'), - function(){ - setCheckBoxesIn('#responses .seen', true); - } - ); - setupButtonEventHandlers( - $('#sel_new'), - function(){ - setCheckBoxesIn('#responses .new', true); - } - ); - setupButtonEventHandlers( - $('#sel_none'), - function(){ - setCheckBoxesIn('#responses .new', false); - setCheckBoxesIn('#responses .seen', false); - } - ); - - var rejectPostDialog = new RejectPostDialog(); - rejectPostDialog.decorate($('#reject-edit-modal')); - rejectPostDialog.setSelectedEditDataReader(function(){ - return getSelected(); - }); - setupButtonEventHandlers( - $('#re_delete_post'), - function(){ - if (rejectPostDialog.readSelectedEditData()) { - rejectPostDialog.show(); - } + var page = $('.inbox-flags'); + if (page.length) { + var modControls = new PostModerationControls(); + modControls.decorate(page); + } + var page = $('.inbox-forum'); + if (page.length) { + var clearNotifs = $('.clear-messages'); + if (clearNotifs.length) { + var inbox = new ResponseNotifs(); + inbox.decorate(clearNotifs); } - ); - - if ($('body').hasClass('inbox-flags')) { - var responses = $('.response-parent'); - responses.each(function(idx, response) { - var control = new PostModerationControls(); - control.setParent($(response)); - control.setReasonsDialog(rejectPostDialog); - rejectPostDialog.addPostModerationControl(control); - $(response).append(control.getElement()); - }); } }; @@ -157,196 +32,337 @@ var setup_badge_details_toggle = function(){ }); }; -var PostModerationControls = function() { +var ResponseNotifs = function() { WrappedElement.call(this); }; -inherits(PostModerationControls, WrappedElement); +inherits(ResponseNotifs, WrappedElement); -PostModerationControls.prototype.setParent = function(parent_element) { - this._parent_element = parent_element; +ResponseNotifs.prototype.clearNewNotifs = function() { + var news = $('.new'); + $('#ab-responses').fadeOut(); + this._element.fadeOut(function() { + news.removeClass('new highlight'); + }); }; -PostModerationControls.prototype.setReasonsDialog = function(dialog) { - this._reasonsDialog = dialog; +ResponseNotifs.prototype.makeHandler = function() { + var me = this; + return function() { + $.ajax({ + type: 'POST', + cache: false, + dataType: 'json', + url: askbot['urls']['clearNewNotifications'], + success: function(response_data){ + if (response_data['success']) { + me.clearNewNotifs(); + } + } + }); + }; }; -PostModerationControls.prototype.getMemoId = function() { - return this._parent_element.data('responseId'); +ResponseNotifs.prototype.decorate = function(element) { + this._element = element; + var btn = element.find('a'); + setupButtonEventHandlers(btn, this.makeHandler()); }; -PostModerationControls.prototype.getMemoElement = function() { - var reId = this.getMemoId(); - return $('#re_' + reId); +/** +* the dropdown menu with selection of reasons +* to reject posts and a button that starts menu to +* manage the list of reasons +*/ +var DeclineAndExplainMenu = function() { + WrappedElement.call(this); }; +inherits(DeclineAndExplainMenu, WrappedElement); -PostModerationControls.prototype.removeMemo = function() { - this.getMemoElement().remove(); +DeclineAndExplainMenu.prototype.setupDeclinePostHandler = function(button) { + var me = this; + var reasonId = button.data('reasonId'); + var controls = this.getControls(); + var handler = controls.getModHandler('decline-with-reason', ['posts'], reasonId); + setupButtonEventHandlers(button, handler); }; -PostModerationControls.prototype.markMemo = function() { - var memo = this.getMemoElement(); - var checkbox = memo.find('input[type="checkbox"]'); - checkbox.attr('checked', true); +DeclineAndExplainMenu.prototype.addReason = function(id, title) { + var li = this.makeElement('li'); + var button = this.makeElement('a'); + li.append(button); + button.html(title); + button.data('reasonId', id); + button.attr('data-reason-id', id); + this._addReasonBtn.parent().before(li); + + this.setupDeclinePostHandler(button); }; -PostModerationControls.prototype.addReason = function(id, title) { - var li = this.makeElement('li'); - var anchor = this.makeElement('a'); - anchor.html(title); - anchor.data('postId', id); - li.append(anchor); - var adderLink = this._reasonList.children().last(); - adderLink.before(li); - //attach event handler - var me = this; - setupButtonEventHandlers(anchor, function() { me.moderatePost(id, 'delete_post') }); +DeclineAndExplainMenu.prototype.removeReason = function(id) { + var btn = this._element.find('a[data-reason-id="' + id + '"]'); + btn.parent().remove(); +}; + +DeclineAndExplainMenu.prototype.setControls = function(controls) { + this._controls = controls; }; -PostModerationControls.prototype.moderatePost = function(reasonId, actionType){ +DeclineAndExplainMenu.prototype.getControls = function() { + return this._controls; +}; + +DeclineAndExplainMenu.prototype.decorate = function(element) { + this._element = element; + //activate dropdown menu + element.dropdown(); + + var declineBtns = element.find('.decline-with-reason'); var me = this; - var data = { - reject_reason_id: reasonId, - memo_list: [me.getMemoId()], - action_type: actionType - }; - $.ajax({ - type: 'POST', - dataType: 'json', - cache: false, - data: JSON.stringify(data), - url: askbot['urls']['manageInbox'], - success: function(data){ - if (data['success']){ - me.removeMemo(); - me.dispose(); - if (actionType === 'delete') { - notify.show(gettext('Post deleted')); - } else if (actionType === 'remove_flag') { - notify.show(gettext('Post approved')); - } - } else { - notify.show(data['message']); - } - } + declineBtns.each(function(idx, elem) { + me.setupDeclinePostHandler($(elem)); }); + + this._reasonList = element.find('ul'); + + var addReasonBtn = element.find('.manage-reasons'); + this._addReasonBtn = addReasonBtn; + + var manageReasonsDialog = new ManageRejectReasonsDialog(); + manageReasonsDialog.decorate($('#manage-reject-reasons-modal')); + this._manageReasonsDialog = manageReasonsDialog; + manageReasonsDialog.setMenu(this); + + setupButtonEventHandlers(addReasonBtn, function() { manageReasonsDialog.show(); }); }; +/** +* Buttons to moderate posts +* and the list of edits +*/ +var PostModerationControls = function() { + WrappedElement.call(this); +}; +inherits(PostModerationControls, WrappedElement); -PostModerationControls.prototype.createDom = function() { - var toolbar = this.makeElement('div'); - toolbar.addClass('btn-toolbar post-moderation-controls'); - this._element = toolbar; +/** +* displays feedback message +*/ +PostModerationControls.prototype.showMessage = function(message) { + this._notification.html(message); + this._notification.parent().fadeIn('fast'); +}; - var div = this.makeElement('div'); - div.addClass('btn-group'); - toolbar.append(div); +PostModerationControls.prototype.hideMessage = function() { + this._notification.parent().hide(); +}; - var acceptBtn = this.makeElement('a'); - acceptBtn.addClass('btn save-reason'); - acceptBtn.html(gettext('Accept')); - div.append(acceptBtn); +/** +* removes entries from the moderation screen +*/ +PostModerationControls.prototype.removeEntries = function(entryIds) { + for (var i = 0; i < entryIds.length; i++) { + var id = entryIds[i]; + var elem = this._element.find('.message[data-message-id="' + id + '"]'); + if (elem.length) { + elem.fadeOut('fast', function() { elem.remove() }); + } + } +}; - div = this.makeElement('div'); - div.addClass('btn-group dropdown'); - toolbar.append(div); +PostModerationControls.prototype.setEntryCount = function(count) { + this._entryCount.html(count); +}; - var toggle = this.makeElement('button'); - toggle.addClass('btn btn-danger dropdown-toggle'); - toggle.append($('<span>' + gettext('Reject') + '</span>')); - toggle.append($('<span class="caret"></span>')); - div.append(toggle); +PostModerationControls.prototype.getEntryCount = function() { + return this.getCheckBoxes().length; +}; - toggle.dropdown(); +PostModerationControls.prototype.getCheckBoxes = function() { + return this._element.find('.messages input[type="checkbox"]'); +}; - var ul = this.makeElement('ul'); - ul.addClass('dropdown-menu'); - div.append(ul); +PostModerationControls.prototype.getSelectedEditIds = function() { + var checkBoxes = this.getCheckBoxes(); + var num = checkBoxes.length; + var idList = []; + for (var i = 0; i < num; i++) { + var cb = $(checkBoxes[i]); + if (cb.is(':checked')) { + var msg = cb.closest('.message-details'); + var msgId = msg.data('messageId'); + idList.push(msgId); + } + } + return idList; +}; - this._reasonList = ul; +/** +* action - one of 'decline-with-reason', 'approve', 'block' +* items - a list of items ['posts', 'users', 'ips'] +* not all combinations of action and items are supported +* optReason must be used with 'decline-with-reason' action +*/ +PostModerationControls.prototype.getModHandler = function(action, items, optReason) { + var me = this; + return function() { + var selectedEditIds = me.getSelectedEditIds(); + if (selectedEditIds.length == 0) { + me.showMessage(gettext('Please select at least one item')); + return; + } + //@todo: implement undo + var postData = { + 'edit_ids': selectedEditIds,//revision ids + 'action': action, + 'items': items,//affected items - users, posts, ips + 'reason': optReason || 'none' + }; + $.ajax({ + type: 'POST', + cache: false, + dataType: 'json', + data: JSON.stringify(postData), + url: askbot['urls']['moderatePostEdits'], + success: function(response_data){ + if (response_data['success'] == true){ + me.removeEntries(response_data['memo_ids']); + me.setEntryCount(response_data['memo_count']); + } - //reason adder - var li = this.makeElement('li'); - var anchor = this.makeElement('a'); - anchor.html(gettext('add new reject reason')); - li.append(anchor); - ul.append(li); + var message = response_data['message'] || ''; + if (me.getEntryCount() < 10 && response_data['memo_count'] > 9) { + if (message) { + message += '. ' + } + var junk = $('#junk-mod'); + if (junk.length == 0) { + junk = me.makeElement('div'); + junk.attr('id', 'junk-mod'); + junk.hide(); + $(document).append(junk); + } + var a = me.makeElement('a'); + a.attr('href', window.location.href); + a.text(gettext('Load more items.')); + junk.append(a); + message += a[0].outerHTML; + } + if (message) { + me.showMessage(message); + } + } + }); + }; +}; - //append menu items +PostModerationControls.prototype.getSelectAllHandler = function(selected) { var me = this; - $.each(askbot['data']['postRejectReasons'], function(idx, reason) { - me.addReason(reason['id'], reason['title']); - }); + return function() { + var cb = me.getCheckBoxes(); + cb.prop('checked', selected); + }; +}; - var reasonsDlg = this._reasonsDialog; - setupButtonEventHandlers(anchor, function() { - me.markMemo();//mark current post - reasonsDlg.readSelectedEditData();//read data of selected edits - reasonsDlg.show();//open the "big" dialog - }); - setupButtonEventHandlers(acceptBtn, function() { - me.moderatePost(null, 'remove_flag'); - }); +PostModerationControls.prototype.decorate = function(element) { + this._element = element; + this._notification = element.find('.action-status span'); + this.hideMessage(); + + this._entryCount = $('.mod-memo-count'); + //approve posts button + var button = $('.approve-posts'); + setupButtonEventHandlers(button, this.getModHandler('approve', ['posts'])); + + //approve posts and users + button = $('.approve-posts-users'); + setupButtonEventHandlers(button, this.getModHandler('approve', ['posts', 'users'])); + + //decline and explain why + var reasonsMenuElem = $('.decline-reasons-menu'); + var declineAndExplainMenu = new DeclineAndExplainMenu(); + declineAndExplainMenu.setControls(this); + declineAndExplainMenu.decorate(reasonsMenuElem); + + //delete posts and block users + button = element.find('.decline-block-users'); + setupButtonEventHandlers(button, this.getModHandler('block', ['posts', 'users'])); + + //delete posts, block users and ips + button = element.find('.decline-block-users-ips'); + setupButtonEventHandlers(button, this.getModHandler('block', ['posts', 'users', 'ips'])); + + button = element.find('.sel-all'); + setupButtonEventHandlers(button, this.getSelectAllHandler(true)); + + button = element.find('.sel-none'); + setupButtonEventHandlers(button, this.getSelectAllHandler(false)); }; + /** * @constructor * manages post/edit reject reasons * in the post moderation view */ -var RejectPostDialog = function(){ +var ManageRejectReasonsDialog = function(){ WrappedElement.call(this); this._selected_edit_ids = null; this._selected_reason_id = null; - this._state = null;//'select', 'preview', 'add-new' + this._state = null;//'select', 'add-new' this._postModerationControls = []; this._selectedEditDataReader = undefined; }; -inherits(RejectPostDialog, WrappedElement); +inherits(ManageRejectReasonsDialog, WrappedElement); + +ManageRejectReasonsDialog.prototype.setMenu = function(menu) { + this._reasonsMenu = menu; +}; -RejectPostDialog.prototype.setSelectedEditDataReader = function(func) { +ManageRejectReasonsDialog.prototype.getMenu = function() { + return this._reasonsMenu; +}; + +ManageRejectReasonsDialog.prototype.setSelectedEditDataReader = function(func) { this._selectedEditDataReader = func; }; -RejectPostDialog.prototype.readSelectedEditData = function() { +ManageRejectReasonsDialog.prototype.readSelectedEditData = function() { var data = this._selectedEditDataReader(); this.setSelectedEditData(data); return data['id_list'].length > 0; }; -RejectPostDialog.prototype.setSelectedEditData = function(data){ +ManageRejectReasonsDialog.prototype.setSelectedEditData = function(data){ this._selected_edit_data = data; }; -RejectPostDialog.prototype.addPostModerationControl = function(control) { +ManageRejectReasonsDialog.prototype.addPostModerationControl = function(control) { this._postModerationControls.push(control); }; -RejectPostDialog.prototype.setState = function(state){ +ManageRejectReasonsDialog.prototype.setState = function(state){ this._state = state; this.clearErrors(); if (this._element){ this._selector.hide(); this._adder.hide(); - this._previewer.hide(); if (state === 'select'){ this._selector.show(); - } else if (state === 'preview'){ - this._previewer.show(); } else if (state === 'add-new'){ this._adder.show(); } } }; -RejectPostDialog.prototype.show = function(){ +ManageRejectReasonsDialog.prototype.show = function(){ $(this._element).modal('show'); }; -RejectPostDialog.prototype.hide = function(){ +ManageRejectReasonsDialog.prototype.hide = function(){ $(this._element).modal('hide'); }; -RejectPostDialog.prototype.resetInputs = function(){ +ManageRejectReasonsDialog.prototype.resetInputs = function(){ if (this._title_input){ this._title_input.reset(); } @@ -357,12 +373,12 @@ RejectPostDialog.prototype.resetInputs = function(){ selected.removeClass('selected'); }; -RejectPostDialog.prototype.clearErrors = function(){ +ManageRejectReasonsDialog.prototype.clearErrors = function(){ var error = this._element.find('.alert'); error.remove(); }; -RejectPostDialog.prototype.makeAlertBox = function(errors){ +ManageRejectReasonsDialog.prototype.makeAlertBox = function(errors){ //construct the alert box var alert_box = new AlertBox(); alert_box.setClass('alert-error'); @@ -393,7 +409,7 @@ RejectPostDialog.prototype.makeAlertBox = function(errors){ return alert_box; }; -RejectPostDialog.prototype.setAdderErrors = function(errors){ +ManageRejectReasonsDialog.prototype.setAdderErrors = function(errors){ //clear previous errors this.clearErrors(); var alert_box = this.makeAlertBox(errors); @@ -402,7 +418,7 @@ RejectPostDialog.prototype.setAdderErrors = function(errors){ .prepend(alert_box.getElement()); }; -RejectPostDialog.prototype.setSelectorErrors = function(errors){ +ManageRejectReasonsDialog.prototype.setSelectorErrors = function(errors){ this.clearErrors(); var alert_box = this.makeAlertBox(errors); this._element @@ -410,7 +426,7 @@ RejectPostDialog.prototype.setSelectorErrors = function(errors){ .prepend(alert_box.getElement()); }; -RejectPostDialog.prototype.setErrors = function(errors){ +ManageRejectReasonsDialog.prototype.setErrors = function(errors){ this.clearErrors(); var alert_box = this.makeAlertBox(errors); var current_state = this._state; @@ -419,7 +435,7 @@ RejectPostDialog.prototype.setErrors = function(errors){ .prepend(alert_box.getElement()); }; -RejectPostDialog.prototype.addSelectableReason = function(data){ +ManageRejectReasonsDialog.prototype.addSelectableReason = function(data){ var id = data['reason_id']; var title = data['title']; var details = data['details']; @@ -433,7 +449,7 @@ RejectPostDialog.prototype.addSelectableReason = function(data){ }); }; -RejectPostDialog.prototype.startSavingReason = function(callback){ +ManageRejectReasonsDialog.prototype.startSavingReason = function(callback){ var title_input = this._title_input; var details_input = this._details_input; @@ -455,8 +471,10 @@ RejectPostDialog.prototype.startSavingReason = function(callback){ title: title_input.getVal(), details: details_input.getVal() }; + var reasonIsNew = true; if (this._selected_reason_id){ data['reason_id'] = this._selected_reason_id; + reasonIsNew = false; } var me = this; @@ -471,6 +489,9 @@ RejectPostDialog.prototype.startSavingReason = function(callback){ if (data['success']){ //show current reason data and focus on it me.addSelectableReason(data); + if (reasonIsNew) { + me.getMenu().addReason(data['reason_id'], data['title']); + } if (callback){ callback(data); } else { @@ -483,59 +504,25 @@ RejectPostDialog.prototype.startSavingReason = function(callback){ }); }; -RejectPostDialog.prototype.rejectPost = function(reason_id){ - var me = this; - var memos = this._selected_edit_data['elements']; - var memo_ids = this._selected_edit_data['id_list']; - var data = { - reject_reason_id: reason_id, - memo_list: memo_ids, - action_type: 'delete_post' - }; - $.ajax({ - type: 'POST', - dataType: 'json', - cache: false, - data: JSON.stringify(data), - url: askbot['urls']['manageInbox'], - success: function(data){ - if (data['success']){ - $.each(memos, function(idx, memo) { - $(memo).next('.post-moderation-controls').remove(); - $(memo).remove(); - }); - me.hide(); - } else { - //only fatal errors here - me.setErrors(data['message']); - } - } - }); -}; - -RejectPostDialog.prototype.setPreviewerData = function(data){ - this._selected_reason_id = data['id']; - this._element.find('.selected-reason-title').html(data['title']); - this._element.find('.selected-reason-details').html(data['details']); -}; - -RejectPostDialog.prototype.startEditingReason = function(){ - var title = this._element.find('.selected-reason-title').html(); - var details = this._element.find('.selected-reason-details').html(); +ManageRejectReasonsDialog.prototype.startEditingReason = function(){ + var data = this._select_box.getSelectedItemData(); + var title = $(data['title']).text(); + var details = data['details']; this._title_input.setVal(title); this._details_input.setVal(details); + this._selected_reason_id = data['id']; this.setState('add-new'); }; -RejectPostDialog.prototype.resetSelectedReasonId = function(){ +ManageRejectReasonsDialog.prototype.resetSelectedReasonId = function(){ this._selected_reason_id = null; }; -RejectPostDialog.prototype.getSelectedReasonId = function(){ +ManageRejectReasonsDialog.prototype.getSelectedReasonId = function(){ return this._selected_reason_id; }; -RejectPostDialog.prototype.startDeletingReason = function(){ +ManageRejectReasonsDialog.prototype.startDeletingReason = function(){ var select_box = this._select_box; var data = select_box.getSelectedItemData(); var reason_id = data['id']; @@ -550,6 +537,8 @@ RejectPostDialog.prototype.startDeletingReason = function(){ success: function(data){ if (data['success']){ select_box.removeItem(reason_id); + me.hideEditButtons(); + me.getMenu().removeReason(reason_id); } else { me.setSelectorErrors(data['message']); } @@ -562,12 +551,21 @@ RejectPostDialog.prototype.startDeletingReason = function(){ } }; -RejectPostDialog.prototype.decorate = function(element){ +ManageRejectReasonsDialog.prototype.hideEditButtons = function() { + this._editButton.hide(); + this._deleteButton.hide(); +}; + +ManageRejectReasonsDialog.prototype.showEditButtons = function() { + this._editButton.show(); + this._deleteButton.show(); +}; + +ManageRejectReasonsDialog.prototype.decorate = function(element){ this._element = element; //set default state according to the # of available reasons this._selector = $(element).find('#reject-edit-modal-select'); this._adder = $(element).find('#reject-edit-modal-add-new'); - this._previewer = $(element).find('#reject-edit-modal-preview'); if (this._selector.find('li').length > 0){ this.setState('select'); this.resetInputs(); @@ -576,10 +574,9 @@ RejectPostDialog.prototype.decorate = function(element){ this.resetInputs(); } - $(this._element).find('.dropdown-toggle').dropdown(); - var select_box = new SelectBox(); select_box.decorate($(this._selector.find('.select-box'))); + select_box.setSelectHandler(function() { me.showEditButtons() }); this._select_box = select_box; //setup tipped-inputs @@ -588,8 +585,7 @@ RejectPostDialog.prototype.decorate = function(element){ title_input.decorate($(reject_title_input)); this._title_input = title_input; - var reject_details_input = $(this._element) - .find('textarea.reject-reason-details'); + var reject_details_input = $(this._element).find('textarea.reject-reason-details'); var details_input = new TippedInput(); details_input.decorate($(reject_details_input)); @@ -604,6 +600,7 @@ RejectPostDialog.prototype.decorate = function(element){ me.resetInputs(); me.resetSelectedReasonId(); me.setState('select'); + me.hideEditButtons(); } ); @@ -613,64 +610,25 @@ RejectPostDialog.prototype.decorate = function(element){ ); setupButtonEventHandlers( - $(this._element).find('.save-reason-and-reject'), - function(){ - me.startSavingReason( - function(data){ - me.rejectPost(data['reason_id']); - } - ); - } - ); - - setupButtonEventHandlers( - $(this._element).find('.reject'), - function(){ - me.rejectPost(me.getSelectedReasonId()); - } - ); - - setupButtonEventHandlers( - element.find('.select-other-reason'), - function(){ - me.resetInputs(); - me.setState('select'); - } - ) - - setupButtonEventHandlers( element.find('.add-new-reason'), function(){ me.resetSelectedReasonId(); me.resetInputs(); - me.setState('add-new') - } - ); - - setupButtonEventHandlers( - element.find('.select-this-reason'), - function(){ - var data = select_box.getSelectedItemData(); - if (data['id']){ - me.setState('preview'); - me.setPreviewerData(data); - } else { - me.setSelectorErrors( - gettext('A reason must be selected to reject post.') - ) - } + me.setState('add-new') ; } ); + this._editButton = element.find('.edit-this-reason'); setupButtonEventHandlers( - element.find('.edit-reason'), + this._editButton, function(){ me.startEditingReason(); } ); + this._deleteButton = element.find('.delete-this-reason'); setupButtonEventHandlers( - element.find('.delete-this-reason'), + this._deleteButton, function(){ me.startDeletingReason(); } diff --git a/askbot/media/js/utils.js b/askbot/media/js/utils.js index a3eb2028..7d877b4b 100644 --- a/askbot/media/js/utils.js +++ b/askbot/media/js/utils.js @@ -2487,11 +2487,15 @@ SelectBox.prototype.setSelectHandler = function(handler) { this._select_handler = handler; }; +SelectBox.prototype.getSelectHandlerInternal = function() { + return this._select_handler; +}; + SelectBox.prototype.getSelectHandler = function(item) { var me = this; - var handler = this._select_handler; return function(){ me.selectItem(item); + var handler = me.getSelectHandlerInternal(); handler(item.getData()); }; }; diff --git a/askbot/media/style/style.css b/askbot/media/style/style.css index 54b18fdd..e2428f8d 100644 --- a/askbot/media/style/style.css +++ b/askbot/media/style/style.css @@ -231,7 +231,6 @@ body.user-messages { .notify .notification { color: #424242; font-size: 16px; - height: 34px; line-height: 34px; margin: 0 !important; } @@ -2051,6 +2050,21 @@ ul#related-tags li { margin-bottom: 10px; } /* ----- Question template ----- */ +.answer .moderated, +.question .moderated { + font-weight: bold; + background: url(../images/dialog-warning.png) 2px 0 no-repeat; + text-decoration: underline; + line-height: 16px !important; + margin-bottom: -2px !important; + padding-left: 24px !important; +} +.answer .comment .moderated, +.question .comment .moderated { + background-position: 4px 0; + margin-bottom: -5px !important; + padding-left: 24px !important; +} .question-page h1 { padding-top: 0px; font-family: 'Open Sans Condensed', Arial, sans-serif; @@ -2871,17 +2885,58 @@ ul#related-tags li { .user-profile-page.inbox-group-join-requests td { padding-right: 10px; } -.inbox-flags.user-profile-page .re { +.user-profile-page.inbox-forum h2, +.user-profile-page.inbox-flags h2 { + line-height: 24px; + padding-bottom: 6px; +} +.user-profile-page.inbox-forum .message, +.user-profile-page.inbox-flags .message { + border-bottom: 1px solid #ccc; + padding: 12px 0; +} +.user-profile-page.inbox-forum .message:last-child, +.user-profile-page.inbox-flags .message:last-child { + border: none; +} +.user-profile-page.inbox-forum input[type="checkbox"] { + display: none; +} +.user-profile-page.inbox-forum .new { + background: #FFF8C6; +} +.reject-reason-title { + margin-bottom: 12px; +} +.user-profile-page.inbox-flags .re { width: 810px; } -.inbox-flags.user-profile-page .post-moderation-controls { +.user-profile-page.inbox-flags .post-moderation-controls { float: left; width: 150px; margin-top: 23px; text-align: right; } -.inbox-flags.user-profile-page .dropdown:hover ul.dropdown-menu { +.user-profile-page.inbox-flags .dropdown { + display: -moz-inline-stack; + display: inline-block; + height: 17px; +} +.user-profile-page.inbox-flags .dropdown:hover ul.dropdown-menu { display: block; + margin-top: 9px; +} +.user-profile-page.inbox-flags .highlight { + background: transparent; +} +.user-profile-page.inbox-flags .messages { + margin-bottom: 14px; +} +.user-profile-page.inbox-flags .select-items { + margin-bottom: 10px; +} +.user-profile-page.inbox-flags #responses div.face { + display: none; } .openid-signin form { margin-bottom: 5px; @@ -3019,6 +3074,9 @@ a:hover.medal { padding: 0; margin-top: -3px; } +.user-profile-page table.form-as-table { + margin: 5px 0 12px; +} .user-profile-page .submit-row { margin-bottom: 0; } @@ -3696,6 +3754,13 @@ button::-moz-focus-inner { font-size: 12px; padding: 0; } +.action-status a { + font-weight: bold; +} +.inbox-flags .action-status { + line-height: 38px; + height: 24px; +} .action-status span { padding: 3px 5px 3px 5px; background-color: #fff380; @@ -3858,6 +3923,7 @@ p.signup_p { margin-bottom: 15px; } #responses h2 { + line-height: 24px; margin: 0; padding: 0; } @@ -4246,6 +4312,13 @@ textarea.tipped-input { width: 515px; margin-bottom: 0px; } +.modal-body > input[type="text"] { + width: 515px; + font-style: normal; +} +.alert .close { + right: -38px; +} .tag-subscriptions { border-spacing: 10px; border-collapse: separate; diff --git a/askbot/media/style/style.less b/askbot/media/style/style.less index d26e21ba..a3d2d431 100644 --- a/askbot/media/style/style.less +++ b/askbot/media/style/style.less @@ -237,7 +237,6 @@ body.user-messages { .notification { color: #424242; font-size: 16px; - height: 34px; line-height: 34px; margin: 0 !important; } @@ -2161,6 +2160,22 @@ ul#related-tags li { /* ----- Question template ----- */ +.answer, .question { + .moderated { + font-weight: bold; + background: url(../images/dialog-warning.png) 2px 0 no-repeat; + text-decoration: underline; + line-height: 16px !important; + margin-bottom: -2px !important; + padding-left: 24px !important; + } + .comment .moderated { + background-position: 4px 0; + margin-bottom: -5px !important; + padding-left: 24px !important; + } +} + .question-page { h1 { @@ -2993,7 +3008,35 @@ ul#related-tags li { } } -.inbox-flags.user-profile-page { +.user-profile-page.inbox-forum, +.user-profile-page.inbox-flags { + h2 { + line-height: 24px; + padding-bottom: 6px; + } + .message { + border-bottom: 1px solid #ccc; + padding: 12px 0; + } + .message:last-child { + border: none; + } +} + +.user-profile-page.inbox-forum { + input[type="checkbox"] { + display: none; + } + .new { + background: #FFF8C6; + } +} + +.reject-reason-title { + margin-bottom: 12px; +} + +.user-profile-page.inbox-flags { .re { width: 810px; } @@ -3003,11 +3046,29 @@ ul#related-tags li { margin-top: 23px; text-align: right; } + .dropdown { + display: -moz-inline-stack; + display: inline-block; + height: 17px; + } .dropdown:hover { ul.dropdown-menu { display: block; + margin-top: 9px; } } + .highlight { + background: transparent; + } + .messages { + margin-bottom: 14px; + } + .select-items { + margin-bottom: 10px; + } + #responses div.face { + display: none; + } } .openid-signin form { @@ -3172,6 +3233,10 @@ a:hover.medal { margin-top: -3px; } + table.form-as-table { + margin: 5px 0 12px; + } + .submit-row { margin-bottom: 0; } @@ -3923,6 +3988,14 @@ button::-moz-focus-inner { line-height: 10px; font-size: 12px; padding: 0; + a { + font-weight: bold; + } +} + +.inbox-flags .action-status { + line-height: 38px; + height: 24px; } .action-status span { @@ -4118,6 +4191,7 @@ p.signup_p { line-height:18px; margin-bottom:15px; h2 { + line-height: 24px; margin: 0; padding: 0; } @@ -4453,6 +4527,13 @@ textarea.tipped-input { width: 515px; margin-bottom: 0px; } +.modal-body > input[type="text"] { + width: 515px; + font-style: normal; +} +.alert .close { + right: -38px; +} .tag-subscriptions { border-spacing: 10px; diff --git a/askbot/middleware/forum_mode.py b/askbot/middleware/forum_mode.py index 5fd2bda3..e01e5f19 100644 --- a/askbot/middleware/forum_mode.py +++ b/askbot/middleware/forum_mode.py @@ -53,7 +53,7 @@ class ForumModeMiddleware(object): resolver_match = ResolverMatch(resolve(request.path)) internal_ips = getattr(settings, 'ASKBOT_INTERNAL_IPS', None) - if internal_ips and request.META['REMOTE_ADDR'] in internal_ips: + if internal_ips and request.META.get('REMOTE_ADDR') in internal_ips: return None if is_view_allowed(resolver_match.func): diff --git a/askbot/migrations/0178_auto__add_field_postrevision_ip_addr.py b/askbot/migrations/0178_auto__add_field_postrevision_ip_addr.py new file mode 100644 index 00000000..fa231194 --- /dev/null +++ b/askbot/migrations/0178_auto__add_field_postrevision_ip_addr.py @@ -0,0 +1,425 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'PostRevision.ip_addr' + db.add_column('askbot_postrevision', 'ip_addr', + self.gf('django.db.models.fields.IPAddressField')(default='0.0.0.0', max_length=15), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'PostRevision.ip_addr' + db.delete_column('askbot_postrevision', 'ip_addr') + + + models = { + 'askbot.activity': { + 'Meta': {'object_name': 'Activity', 'db_table': "u'activity'"}, + 'active_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'activity_type': ('django.db.models.fields.SmallIntegerField', [], {}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_auditted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), + 'question': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Post']", 'null': 'True'}), + 'receiving_users': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'received_activity'", 'symmetrical': 'False', 'to': "orm['auth.User']"}), + 'recipients': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'incoming_activity'", 'symmetrical': 'False', 'through': "orm['askbot.ActivityAuditStatus']", 'to': "orm['auth.User']"}), + 'summary': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'askbot.activityauditstatus': { + 'Meta': {'unique_together': "(('user', 'activity'),)", 'object_name': 'ActivityAuditStatus'}, + 'activity': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Activity']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'status': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'askbot.anonymousanswer': { + 'Meta': {'object_name': 'AnonymousAnswer'}, + 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip_addr': ('django.db.models.fields.IPAddressField', [], {'max_length': '15'}), + 'question': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'anonymous_answers'", 'to': "orm['askbot.Post']"}), + 'session_key': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'text': ('django.db.models.fields.TextField', [], {}), + 'wiki': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) + }, + 'askbot.anonymousquestion': { + 'Meta': {'object_name': 'AnonymousQuestion'}, + 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip_addr': ('django.db.models.fields.IPAddressField', [], {'max_length': '15'}), + 'is_anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'session_key': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'tagnames': ('django.db.models.fields.CharField', [], {'max_length': '125'}), + 'text': ('django.db.models.fields.TextField', [], {}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '300'}), + 'wiki': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) + }, + 'askbot.askwidget': { + 'Meta': {'object_name': 'AskWidget'}, + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Group']", 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'include_text_field': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'inner_style': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'outer_style': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'tag': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Tag']", 'null': 'True', 'blank': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'askbot.award': { + 'Meta': {'object_name': 'Award', 'db_table': "u'award'"}, + 'awarded_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'badge': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'award_badge'", 'to': "orm['askbot.BadgeData']"}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'notified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'award_user'", 'to': "orm['auth.User']"}) + }, + 'askbot.badgedata': { + 'Meta': {'ordering': "('slug',)", 'object_name': 'BadgeData'}, + 'awarded_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'awarded_to': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'badges'", 'symmetrical': 'False', 'through': "orm['askbot.Award']", 'to': "orm['auth.User']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '50'}) + }, + 'askbot.bulktagsubscription': { + 'Meta': {'ordering': "['-date_added']", 'object_name': 'BulkTagSubscription'}, + 'date_added': ('django.db.models.fields.DateField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['askbot.Group']", 'symmetrical': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['askbot.Tag']", 'symmetrical': 'False'}), + 'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'symmetrical': 'False'}) + }, + 'askbot.draftanswer': { + 'Meta': {'object_name': 'DraftAnswer'}, + 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'draft_answers'", 'to': "orm['auth.User']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'text': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'thread': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'draft_answers'", 'to': "orm['askbot.Thread']"}) + }, + 'askbot.draftquestion': { + 'Meta': {'object_name': 'DraftQuestion'}, + 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'tagnames': ('django.db.models.fields.CharField', [], {'max_length': '125', 'null': 'True'}), + 'text': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '300', 'null': 'True'}) + }, + 'askbot.emailfeedsetting': { + 'Meta': {'unique_together': "(('subscriber', 'feed_type'),)", 'object_name': 'EmailFeedSetting'}, + 'added_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'feed_type': ('django.db.models.fields.CharField', [], {'max_length': '16'}), + 'frequency': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '8'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'reported_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'subscriber': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'notification_subscriptions'", 'to': "orm['auth.User']"}) + }, + 'askbot.favoritequestion': { + 'Meta': {'object_name': 'FavoriteQuestion', 'db_table': "u'favorite_question'"}, + 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Thread']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'user_favorite_questions'", 'to': "orm['auth.User']"}) + }, + 'askbot.group': { + 'Meta': {'object_name': 'Group', '_ormbases': ['auth.Group']}, + 'description': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'related_name': "'described_group'", 'unique': 'True', 'null': 'True', 'to': "orm['askbot.Post']"}), + 'group_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.Group']", 'unique': 'True', 'primary_key': 'True'}), + 'is_vip': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'logo_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True'}), + 'moderate_answers_to_enquirers': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'moderate_email': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'openness': ('django.db.models.fields.SmallIntegerField', [], {'default': '2'}), + 'preapproved_email_domains': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}), + 'preapproved_emails': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}), + 'read_only': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) + }, + 'askbot.groupmembership': { + 'Meta': {'object_name': 'GroupMembership', '_ormbases': ['auth.AuthUserGroups']}, + 'authusergroups_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.AuthUserGroups']", 'unique': 'True', 'primary_key': 'True'}), + 'level': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}) + }, + 'askbot.importedobjectinfo': { + 'Meta': {'object_name': 'ImportedObjectInfo'}, + 'extra_info': ('picklefield.fields.PickledObjectField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}), + 'new_id': ('django.db.models.fields.IntegerField', [], {}), + 'old_id': ('django.db.models.fields.IntegerField', [], {}), + 'run': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.ImportRun']"}) + }, + 'askbot.importrun': { + 'Meta': {'object_name': 'ImportRun'}, + 'command': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}) + }, + 'askbot.markedtag': { + 'Meta': {'object_name': 'MarkedTag'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'reason': ('django.db.models.fields.CharField', [], {'max_length': '16'}), + 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'user_selections'", 'to': "orm['askbot.Tag']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'tag_selections'", 'to': "orm['auth.User']"}) + }, + 'askbot.post': { + 'Meta': {'object_name': 'Post'}, + 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'approved': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}), + 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'posts'", 'to': "orm['auth.User']"}), + 'comment_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'deleted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'deleted_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'deleted_posts'", 'null': 'True', 'to': "orm['auth.User']"}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'group_posts'", 'symmetrical': 'False', 'through': "orm['askbot.PostToGroup']", 'to': "orm['askbot.Group']"}), + 'html': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'language_code': ('django.db.models.fields.CharField', [], {'default': "'en'", 'max_length': '16'}), + 'last_edited_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'last_edited_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'last_edited_posts'", 'null': 'True', 'to': "orm['auth.User']"}), + 'locked': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'locked_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'locked_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'locked_posts'", 'null': 'True', 'to': "orm['auth.User']"}), + 'offensive_flag_count': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'old_answer_id': ('django.db.models.fields.PositiveIntegerField', [], {'default': 'None', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'old_comment_id': ('django.db.models.fields.PositiveIntegerField', [], {'default': 'None', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'old_question_id': ('django.db.models.fields.PositiveIntegerField', [], {'default': 'None', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'comments'", 'null': 'True', 'to': "orm['askbot.Post']"}), + 'points': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_column': "'score'"}), + 'post_type': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'summary': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'text': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'thread': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'posts'", 'null': 'True', 'blank': 'True', 'to': "orm['askbot.Thread']"}), + 'vote_down_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'vote_up_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'wiki': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'wikified_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + 'askbot.postflagreason': { + 'Meta': {'object_name': 'PostFlagReason'}, + 'added_at': ('django.db.models.fields.DateTimeField', [], {}), + 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), + 'details': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'post_reject_reasons'", 'to': "orm['askbot.Post']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '128'}) + }, + 'askbot.postrevision': { + 'Meta': {'ordering': "('-revision',)", 'unique_together': "(('post', 'revision'),)", 'object_name': 'PostRevision'}, + 'approved': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'approved_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'approved_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), + 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'postrevisions'", 'to': "orm['auth.User']"}), + 'by_email': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'email_address': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip_addr': ('django.db.models.fields.IPAddressField', [], {'default': "'0.0.0.0'", 'max_length': '15'}), + 'is_anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'post': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'revisions'", 'null': 'True', 'to': "orm['askbot.Post']"}), + 'revised_at': ('django.db.models.fields.DateTimeField', [], {}), + 'revision': ('django.db.models.fields.PositiveIntegerField', [], {}), + 'summary': ('django.db.models.fields.CharField', [], {'max_length': '300', 'blank': 'True'}), + 'tagnames': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '125', 'blank': 'True'}), + 'text': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '300', 'blank': 'True'}) + }, + 'askbot.posttogroup': { + 'Meta': {'unique_together': "(('post', 'group'),)", 'object_name': 'PostToGroup', 'db_table': "'askbot_post_groups'"}, + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Post']"}) + }, + 'askbot.questionview': { + 'Meta': {'object_name': 'QuestionView'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'question': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'viewed'", 'to': "orm['askbot.Post']"}), + 'when': ('django.db.models.fields.DateTimeField', [], {}), + 'who': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'question_views'", 'to': "orm['auth.User']"}) + }, + 'askbot.questionwidget': { + 'Meta': {'object_name': 'QuestionWidget'}, + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Group']", 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'order_by': ('django.db.models.fields.CharField', [], {'default': "'-added_at'", 'max_length': '18'}), + 'question_number': ('django.db.models.fields.PositiveIntegerField', [], {'default': '7'}), + 'search_query': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '50', 'null': 'True', 'blank': 'True'}), + 'style': ('django.db.models.fields.TextField', [], {'default': '"\\n@import url(\'http://fonts.googleapis.com/css?family=Yanone+Kaffeesatz:300,400,700\');\\nbody {\\n overflow: hidden;\\n}\\n\\n#container {\\n width: 200px;\\n height: 350px;\\n}\\nul {\\n list-style: none;\\n padding: 5px;\\n margin: 5px;\\n}\\nli {\\n border-bottom: #CCC 1px solid;\\n padding-bottom: 5px;\\n padding-top: 5px;\\n}\\nli:last-child {\\n border: none;\\n}\\na {\\n text-decoration: none;\\n color: #464646;\\n font-family: \'Yanone Kaffeesatz\', sans-serif;\\n font-size: 15px;\\n}\\n"', 'blank': 'True'}), + 'tagnames': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'askbot.replyaddress': { + 'Meta': {'object_name': 'ReplyAddress'}, + 'address': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '25'}), + 'allowed_from_email': ('django.db.models.fields.EmailField', [], {'max_length': '150'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'post': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'reply_addresses'", 'null': 'True', 'to': "orm['askbot.Post']"}), + 'reply_action': ('django.db.models.fields.CharField', [], {'default': "'auto_answer_or_comment'", 'max_length': '32'}), + 'response_post': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'edit_addresses'", 'null': 'True', 'to': "orm['askbot.Post']"}), + 'used_at': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'askbot.repute': { + 'Meta': {'object_name': 'Repute', 'db_table': "u'repute'"}, + 'comment': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'negative': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'positive': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'question': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Post']", 'null': 'True', 'blank': 'True'}), + 'reputation': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'reputation_type': ('django.db.models.fields.SmallIntegerField', [], {}), + 'reputed_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'askbot.tag': { + 'Meta': {'ordering': "('-used_count', 'name')", 'unique_together': "(('name', 'language_code'),)", 'object_name': 'Tag', 'db_table': "u'tag'"}, + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'created_tags'", 'to': "orm['auth.User']"}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'deleted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'deleted_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'deleted_tags'", 'null': 'True', 'to': "orm['auth.User']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'language_code': ('django.db.models.fields.CharField', [], {'default': "'en'", 'max_length': '16'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'status': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}), + 'suggested_by': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'suggested_tags'", 'symmetrical': 'False', 'to': "orm['auth.User']"}), + 'tag_wiki': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'described_tag'", 'unique': 'True', 'null': 'True', 'to': "orm['askbot.Post']"}), + 'used_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}) + }, + 'askbot.tagsynonym': { + 'Meta': {'object_name': 'TagSynonym'}, + 'auto_rename_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'language_code': ('django.db.models.fields.CharField', [], {'default': "'en'", 'max_length': '16'}), + 'last_auto_rename_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'owned_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'tag_synonyms'", 'to': "orm['auth.User']"}), + 'source_tag_name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'target_tag_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}) + }, + 'askbot.thread': { + 'Meta': {'object_name': 'Thread'}, + 'accepted_answer': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['askbot.Post']"}), + 'added_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'answer_accepted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'answer_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'approved': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}), + 'close_reason': ('django.db.models.fields.SmallIntegerField', [], {'null': 'True', 'blank': 'True'}), + 'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'closed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'closed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'favorited_by': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'unused_favorite_threads'", 'symmetrical': 'False', 'through': "orm['askbot.FavoriteQuestion']", 'to': "orm['auth.User']"}), + 'favourite_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'followed_by': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'followed_threads'", 'symmetrical': 'False', 'to': "orm['auth.User']"}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'group_threads'", 'symmetrical': 'False', 'through': "orm['askbot.ThreadToGroup']", 'to': "orm['askbot.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'language_code': ('django.db.models.fields.CharField', [], {'default': "'en'", 'max_length': '16'}), + 'last_activity_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_activity_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'unused_last_active_in_threads'", 'to': "orm['auth.User']"}), + 'points': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_column': "'score'"}), + 'tagnames': ('django.db.models.fields.CharField', [], {'max_length': '125'}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'threads'", 'symmetrical': 'False', 'to': "orm['askbot.Tag']"}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '300'}), + 'view_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}) + }, + 'askbot.threadtogroup': { + 'Meta': {'unique_together': "(('thread', 'group'),)", 'object_name': 'ThreadToGroup', 'db_table': "'askbot_thread_groups'"}, + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Thread']"}), + 'visibility': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}) + }, + 'askbot.vote': { + 'Meta': {'unique_together': "(('user', 'voted_post'),)", 'object_name': 'Vote', 'db_table': "u'vote'"}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'votes'", 'to': "orm['auth.User']"}), + 'vote': ('django.db.models.fields.SmallIntegerField', [], {}), + 'voted_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'voted_post': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'votes'", 'to': "orm['askbot.Post']"}) + }, + 'auth.authusergroups': { + 'Meta': {'unique_together': "(('group', 'user'),)", 'object_name': 'AuthUserGroups', 'db_table': "'auth_user_groups'", 'managed': 'False'}, + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'about': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'avatar_type': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '1'}), + 'bronze': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'consecutive_days_visit_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'blank': 'True'}), + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}), + 'display_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'email_isvalid': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'email_key': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'email_signature': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'email_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'gold': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'gravatar': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ignored_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'interesting_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_fake': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'languages': ('django.db.models.fields.CharField', [], {'default': "'en'", 'max_length': '128'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'last_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'location': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'new_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'questions_per_page': ('django.db.models.fields.SmallIntegerField', [], {'default': '10'}), + 'real_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'reputation': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), + 'seen_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'show_country': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'show_marked_tags': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'social_sharing_mode': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}), + 'subscribed_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'twitter_access_token': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '256'}), + 'twitter_handle': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'website': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + } + } + + complete_apps = ['askbot']
\ No newline at end of file diff --git a/askbot/models/__init__.py b/askbot/models/__init__.py index 93c8d599..7d91216e 100644 --- a/askbot/models/__init__.py +++ b/askbot/models/__init__.py @@ -24,6 +24,7 @@ from django.db.models import signals as django_signals from django.template import Context from django.template.loader import get_template from django.utils.translation import get_language +from django.utils.translation import string_concat from django.utils.translation import ugettext as _ from django.utils.translation import ungettext from django.utils.safestring import mark_safe @@ -557,6 +558,12 @@ def get_or_create_anonymous_user(): user.save() return user +def user_needs_moderation(self): + if self.status not in ('a', 'm', 'd'): + choices = ('audit', 'premoderation') + return askbot_settings.CONTENT_MODERATION_MODE in choices + return False + def user_notify_users( self, notification_type=None, recipients=None, content_object=None ): @@ -622,6 +629,7 @@ def _assert_user_can( 'perform_action': action_display, 'your_account_is': _('your account is blocked') } + error_message = string_concat(error_message, '.</br> ', message_keys.PUNISHED_USER_INFO) elif post and owner_can and user == post.get_owner(): if user.is_suspended() and suspended_owner_cannot: error_message = _(message_keys.ACCOUNT_CANNOT_PERFORM_ACTION) % { @@ -1199,10 +1207,11 @@ def user_get_unused_votes_today(self): def user_post_comment( self, - parent_post = None, - body_text = None, - timestamp = None, - by_email = False + parent_post=None, + body_text=None, + timestamp=None, + by_email=False, + ip_addr=None, ): """post a comment on behalf of the user to parent_post @@ -1218,10 +1227,11 @@ def user_post_comment( self.assert_can_post_comment(parent_post = parent_post) comment = parent_post.add_comment( - user = self, - comment = body_text, - added_at = timestamp, - by_email = by_email + user=self, + comment=body_text, + added_at=timestamp, + by_email=by_email, + ip_addr=ip_addr, ) comment.add_to_groups([self.get_personal_group()]) @@ -1275,20 +1285,10 @@ def user_post_anonymous_askbot_content(user, session_key): aa.save() #maybe add pending posts message? else: - if user.is_blocked() or user.is_suspended(): - if user.is_blocked(): - account_status = _('your account is blocked') - elif user.is_suspended(): - account_status = _('your account is suspended') - user.message_set.create(message = _(message_keys.ACCOUNT_CANNOT_PERFORM_ACTION) % { - 'perform_action': _('make posts'), - 'your_account_is': account_status - }) - else: - for aq in aq_list: - aq.publish(user) - for aa in aa_list: - aa.publish(user) + for aq in aq_list: + aq.publish(user) + for aa in aa_list: + aa.publish(user) def user_mark_tags( @@ -1506,6 +1506,7 @@ def user_delete_answer( answer.save() answer.thread.update_answer_count() + answer.thread.update_last_activity_info() answer.thread.invalidate_cached_data() logging.debug('updated answer count to %d' % answer.thread.answer_count) @@ -1560,6 +1561,39 @@ def user_delete_question( ) +def user_delete_all_content_authored_by_user(self, author, timestamp=None): + """Deletes all questions, answers and comments made by the user""" + count = 0 + + #delete answers + answers = Post.objects.get_answers().filter(author=author) + timestamp = timestamp or datetime.datetime.now() + count += answers.update(deleted_at=timestamp, deleted_by=self, deleted=True) + + #delete questions + questions = Post.objects.get_questions().filter(author=author) + count += questions.update(deleted_at=timestamp, deleted_by=self, deleted=True) + + threads = Thread.objects.filter(last_activity_by=author) + for thread in threads: + thread.update_last_activity_info() + + #delete threads + thread_ids = questions.values_list('thread_id', flat=True) + #load second time b/c threads above are not quite real + threads = Thread.objects.filter(id__in=thread_ids) + threads.update(deleted=True) + for thread in threads: + thread.invalidate_cached_data() + + #delete comments + comments = Post.objects.get_comments().filter(author=author) + count += comments.count() + comments.delete() + + return count + + @auto_now_timestamp def user_close_question( self, @@ -1617,6 +1651,7 @@ def user_restore_post( post.thread.invalidate_cached_data() if post.post_type == 'answer': post.thread.update_answer_count() + post.thread.update_last_activity_info() else: #todo: make sure that these tags actually exist #some may have since been deleted for good @@ -1632,17 +1667,18 @@ def user_restore_post( def user_post_question( self, - title = None, - body_text = '', - tags = None, - wiki = False, - is_anonymous = False, - is_private = False, - group_id = None, - timestamp = None, - by_email = False, - email_address = None, - language = None + title=None, + body_text='', + tags=None, + wiki=False, + is_anonymous=False, + is_private=False, + group_id=None, + timestamp=None, + by_email=False, + email_address=None, + language=None, + ip_addr=None, ): """makes an assertion whether user can post the question then posts it and returns the question object""" @@ -1673,7 +1709,8 @@ def user_post_question( group_id=group_id, by_email=by_email, email_address=email_address, - language=language + language=language, + ip_addr=ip_addr ) question = thread._question_post() if question.author != self: @@ -1693,7 +1730,8 @@ def user_edit_comment( body_text=None, timestamp=None, by_email=False, - suppress_email=False + suppress_email=False, + ip_addr=None, ): """apply edit to a comment, the method does not change the comments timestamp and no signals are sent @@ -1706,7 +1744,8 @@ def user_edit_comment( edited_at=timestamp, edited_by=self, by_email=by_email, - suppress_email=suppress_email + suppress_email=suppress_email, + ip_addr=ip_addr, ) comment_post.thread.invalidate_cached_data() @@ -1718,6 +1757,7 @@ def user_edit_post(self, by_email=False, is_private=False, suppress_email=False, + ip_addr=None ): """a simple method that edits post body todo: unify it in the style of just a generic post @@ -1729,7 +1769,8 @@ def user_edit_post(self, comment_post=post, body_text=body_text, by_email=by_email, - suppress_email=suppress_email + suppress_email=suppress_email, + ip_addr=ip_addr ) elif post.post_type == 'answer': self.edit_answer( @@ -1738,7 +1779,8 @@ def user_edit_post(self, timestamp=timestamp, revision_comment=revision_comment, by_email=by_email, - suppress_email=suppress_email + suppress_email=suppress_email, + ip_addr=ip_addr ) elif post.post_type == 'question': self.edit_question( @@ -1749,6 +1791,7 @@ def user_edit_post(self, by_email=by_email, is_private=is_private, suppress_email=suppress_email, + ip_addr=ip_addr ) elif post.post_type == 'tag_wiki': post.apply_edit( @@ -1758,7 +1801,8 @@ def user_edit_post(self, #todo: summary name clash in question and question revision comment=revision_comment, wiki=True, - by_email=False + by_email=False, + ip_addr=ip_addr, ) else: raise NotImplementedError() @@ -1777,24 +1821,26 @@ def user_edit_question( timestamp=None, force=False,#if True - bypass the assert by_email=False, - suppress_email=False + suppress_email=False, + ip_addr=None, ): if force == False: self.assert_can_edit_question(question) question.apply_edit( - edited_at = timestamp, - edited_by = self, - title = title, - text = body_text, + edited_at=timestamp, + edited_by=self, + title=title, + text=body_text, #todo: summary name clash in question and question revision - comment = revision_comment, - tags = tags, - wiki = wiki, - edit_anonymously = edit_anonymously, - is_private = is_private, - by_email = by_email, - suppress_email=suppress_email + comment=revision_comment, + tags=tags, + wiki=wiki, + edit_anonymously=edit_anonymously, + is_private=is_private, + by_email=by_email, + suppress_email=suppress_email, + ip_addr=ip_addr ) question.thread.invalidate_cached_data() @@ -1818,6 +1864,7 @@ def user_edit_answer( force=False,#if True - bypass the assert by_email=False, suppress_email=False, + ip_addr=None, ): if force == False: self.assert_can_edit_answer(answer) @@ -1830,7 +1877,8 @@ def user_edit_answer( wiki=wiki, is_private=is_private, by_email=by_email, - suppress_email=suppress_email + suppress_email=suppress_email, + ip_addr=ip_addr, ) answer.thread.invalidate_cached_data() @@ -1885,13 +1933,14 @@ def user_edit_post_reject_reason( def user_post_answer( self, - question = None, - body_text = None, - follow = False, - wiki = False, - is_private = False, - timestamp = None, - by_email = False + question=None, + body_text=None, + follow=False, + wiki=False, + is_private=False, + timestamp=None, + by_email=False, + ip_addr=None, ): #todo: move this to assertion - user_assert_can_post_answer @@ -1953,14 +2002,15 @@ def user_post_answer( # wiki = wiki # ) answer_post = Post.objects.create_new_answer( - thread = question.thread, - author = self, - text = body_text, - added_at = timestamp, - email_notify = follow, - wiki = wiki, - is_private = is_private, - by_email = by_email + thread=question.thread, + author=self, + text=body_text, + added_at=timestamp, + email_notify=follow, + wiki=wiki, + is_private=is_private, + by_email=by_email, + ip_addr=ip_addr, ) #add to the answerer's group answer_post.add_to_groups([self.get_personal_group()]) @@ -2220,21 +2270,19 @@ def user_moderate_user_reputation( repute.positive = reputation_change repute.save() -def user_get_status_display(self, soft = False): - if self.is_administrator(): - return _('Site Adminstrator') +def user_get_status_display(self): + if self.is_approved(): + return _('Registered User') + elif self.is_administrator(): + return _('Adminstrator') elif self.is_moderator(): - return _('Forum Moderator') + return _('Moderator') elif self.is_suspended(): return _('Suspended User') elif self.is_blocked(): return _('Blocked User') - elif soft == True: - return _('Registered User') elif self.is_watched(): - return _('Watched User') - elif self.is_approved(): - return _('Approved User') + return _('New User') else: raise ValueError('Unknown user status') @@ -2666,22 +2714,36 @@ def user_approve_post_revision(user, post_revision, timestamp = None): post_revision.approved_by = user post_revision.approved_at = timestamp - post_revision.save() - post = post_revision.post - post.approved = True - post.save() - if post_revision.post.post_type == 'question': - thread = post.thread - thread.approved = True - thread.save() - post.thread.invalidate_cached_data() + #approval of unpublished revision + if post_revision.revision == 0: + post_revision.revision = post.get_latest_revision_number() + 1 - #send the signal of published revision - signals.post_revision_published.send( - None, revision = post_revision, was_approved = True - ) + post_revision.save() + + if post.approved == False: + if post.is_comment(): + post.parent.comment_count += 1 + post.parent.save() + elif post.is_answer(): + post.thread.answer_count += 1 + post.thread.save() + + post.approved = True + post.save() + + if post_revision.post.post_type == 'question': + thread = post.thread + thread.approved = True + thread.save() + + post.thread.invalidate_cached_data() + + #send the signal of published revision + signals.post_revision_published.send( + None, revision = post_revision, was_approved = True + ) @auto_now_timestamp def flag_post( @@ -2992,6 +3054,10 @@ User.add_to_class('get_unused_votes_today', user_get_unused_votes_today) User.add_to_class('delete_comment', user_delete_comment) User.add_to_class('delete_question', user_delete_question) User.add_to_class('delete_answer', user_delete_answer) +User.add_to_class( + 'delete_all_content_authored_by_user', + user_delete_all_content_authored_by_user +) User.add_to_class('restore_post', user_restore_post) User.add_to_class('close_question', user_close_question) User.add_to_class('reopen_question', user_reopen_question) @@ -3002,6 +3068,7 @@ User.add_to_class( user_update_wildcard_tag_selections ) User.add_to_class('approve_post_revision', user_approve_post_revision) +User.add_to_class('needs_moderation', user_needs_moderation) User.add_to_class('notify_users', user_notify_users) User.add_to_class('is_read_only', user_is_read_only) @@ -3670,6 +3737,16 @@ def add_missing_tag_subscriptions(sender, instance, created, **kwargs): instance.mark_tags(tagnames = tag_list, reason='subscribed', action='add') +def notify_punished_users(user, **kwargs): + try: + _assert_user_can( + user=user, + blocked_user_cannot=True, + suspended_user_cannot=True + ) + except django_exceptions.PermissionDenied, e: + user.message_set.create(message = unicode(e)) + def post_anonymous_askbot_content( sender, request, @@ -3681,7 +3758,10 @@ def post_anonymous_askbot_content( """signal handler, unfortunately extra parameters are necessary for the signal machinery, even though they are not used in this function""" - user.post_anonymous_askbot_content(session_key) + if user.is_blocked() or user.is_suspended(): + pass + else: + user.post_anonymous_askbot_content(session_key) def set_user_avatar_type_flag(instance, created, **kwargs): instance.user.update_avatar_type() @@ -3759,6 +3839,7 @@ signals.user_registered.connect(greet_new_user) signals.user_registered.connect(make_admin_if_first_user) signals.user_updated.connect(record_user_full_updated, sender=User) signals.user_logged_in.connect(complete_pending_tag_subscriptions)#todo: add this to fake onlogin middleware +signals.user_logged_in.connect(notify_punished_users) signals.user_logged_in.connect(post_anonymous_askbot_content) signals.post_updated.connect(record_post_update_activity) signals.new_answer_posted.connect(tweet_new_post) diff --git a/askbot/models/post.py b/askbot/models/post.py index 47af2a42..594e1d9f 100644 --- a/askbot/models/post.py +++ b/askbot/models/post.py @@ -194,12 +194,13 @@ class PostManager(BaseQuerySetManager): author, added_at, text, - parent = None, - wiki = False, - is_private = False, - email_notify = False, - post_type = None, - by_email = False + parent=None, + wiki=False, + is_private=False, + email_notify=False, + post_type=None, + by_email=False, + ip_addr=None, ): # TODO: Some of this code will go to Post.objects.create_new @@ -234,6 +235,15 @@ class PostManager(BaseQuerySetManager): parse_results = post.parse_and_save(author=author, is_private=is_private) + post.add_revision( + author=author, + revised_at=added_at, + text=text, + comment=unicode(const.POST_STATUS['default_version']), + by_email=by_email, + ip_addr=ip_addr + ) + from askbot.models import signals signals.post_updated.send( post=post, @@ -245,13 +255,6 @@ class PostManager(BaseQuerySetManager): sender=post.__class__ ) - post.add_revision( - author = author, - revised_at = added_at, - text = text, - comment = unicode(const.POST_STATUS['default_version']), - by_email = by_email - ) return post @@ -262,20 +265,22 @@ class PostManager(BaseQuerySetManager): author, added_at, text, - wiki = False, - is_private = False, - email_notify = False, - by_email = False + wiki=False, + is_private=False, + email_notify=False, + by_email=False, + ip_addr=None, ): answer = self.create_new( thread, author, added_at, text, - wiki = wiki, - is_private = is_private, - post_type = 'answer', - by_email = by_email + wiki=wiki, + is_private=is_private, + post_type='answer', + by_email=by_email, + ip_addr=ip_addr ) #set notification/delete if email_notify: @@ -285,9 +290,13 @@ class PostManager(BaseQuerySetManager): #update thread data #todo: this totally belongs to some `Thread` class method - thread.answer_count += 1 - thread.save() - thread.set_last_activity(last_activity_at=added_at, last_activity_by=author) # this should be here because it regenerates cached thread summary html + if answer.is_approved(): + thread.answer_count += 1 + thread.save() + thread.set_last_activity_info( + last_activity_at=added_at, + last_activity_by=author + ) # this should be here because it regenerates cached thread summary html return answer @@ -770,7 +779,7 @@ class Post(models.Model): self.remove_from_groups((Group.objects.get_global_group(),)) if len(groups) == 0: - message = 'Sharing did not work, because group is unknown' + message = _('Sharing did not work, because group is unknown') user.message_set.create(message=message) def make_public(self): @@ -787,18 +796,28 @@ class Post(models.Model): return not self.groups.filter(id=group.id).exists() return False + def set_runtime_needs_moderation(self): + """Used at runtime only, the value is not + stored in the database""" + self._is_approved = False + def is_approved(self): """``False`` only when moderation is ``True`` and post ``self.approved is False`` """ - if askbot_settings.ENABLE_CONTENT_MODERATION: - if self.approved == False: + if getattr(self, '_is_approved', True) == False: + return False + + if askbot_settings.CONTENT_MODERATION_MODE == 'premoderation': + if self.approved: + return True + if self.revisions.filter(revision=0).count() == 1: return False return True def needs_moderation(self): #todo: do we need this, can't we just use is_approved()? - return self.approved is False + return self.is_approved() is False def get_absolute_url(self, no_slug = False, question_post=None, thread=None): from askbot.utils.slug import slugify @@ -1026,12 +1045,19 @@ class Post(models.Model): self._cached_comments = list() return self._cached_comments + def add_cached_comment(self, comment): + comments = self.get_cached_comments() + if comment not in comments: + comments.append(comment) + def add_comment( self, comment=None, user=None, added_at=None, - by_email = False): + by_email=False, + ip_addr=None, + ): if added_at is None: added_at = datetime.datetime.now() @@ -1043,12 +1069,14 @@ class Post(models.Model): user, added_at, comment, - parent = self, - post_type = 'comment', - by_email = by_email + parent=self, + post_type='comment', + by_email=by_email, + ip_addr=ip_addr, ) - self.comment_count = self.comment_count + 1 - self.save() + if comment_post.is_approved(): + self.comment_count = self.comment_count + 1 + self.save() #tried to add this to bump updated question #in most active list, but it did not work @@ -1428,10 +1456,13 @@ class Post(models.Model): def get_latest_revision(self): - return self.revisions.order_by('-revised_at')[0] + return self.revisions.order_by('-revision')[0] def get_latest_revision_number(self): - return self.get_latest_revision().revision + try: + return self.get_latest_revision().revision + except IndexError: + return 0 def get_time_of_last_edit(self): if self.is_comment(): @@ -1620,7 +1651,8 @@ class Post(models.Model): def _question__assert_is_visible_to(self, user): """raises QuestionHidden""" if self.is_approved() is False: - raise exceptions.QuestionHidden() + if user != self.author: + raise exceptions.QuestionHidden(_('Sorry, this content is not available')) if self.deleted: message = _('Sorry, this content is no longer available') if user.is_anonymous(): @@ -1738,10 +1770,14 @@ class Post(models.Model): edit_anonymously=False, is_private=False, by_email=False, - suppress_email=False + suppress_email=False, + ip_addr=None, ): + + latest_rev = self.get_latest_revision() + if text is None: - text = self.get_latest_revision().text + text = latest_rev.text if edited_at is None: edited_at = datetime.datetime.now() if edited_by is None: @@ -1757,14 +1793,23 @@ class Post(models.Model): if self.wiki == False and wiki == True: self.wiki = True - #must add revision before saving the answer - self.add_revision( - author = edited_by, - revised_at = edited_at, - text = text, - comment = comment, - by_email = by_email - ) + #must add or update revision before saving the answer + if latest_rev.revision == 0: + #if post has only 0 revision, we just update the + #latest revision data + latest_rev.text = text + latest_rev.revised_at = edited_at + latest_rev.save() + else: + #otherwise we create a new revision + self.add_revision( + author=edited_by, + revised_at=edited_at, + text=text, + comment=comment, + by_email=by_email, + ip_addr=ip_addr, + ) parse_results = self.parse_and_save(author=edited_by, is_private=is_private) @@ -1791,6 +1836,7 @@ class Post(models.Model): is_private=False, by_email=False, suppress_email=False, + ip_addr=None, ): ##it is important to do this before __apply_edit b/c of signals!!! @@ -1808,18 +1854,31 @@ class Post(models.Model): wiki=wiki, by_email=by_email, is_private=is_private, - suppress_email=suppress_email + suppress_email=suppress_email, + ip_addr=ip_addr, ) if edited_at is None: edited_at = datetime.datetime.now() - self.thread.set_last_activity(last_activity_at=edited_at, last_activity_by=edited_by) + self.thread.set_last_activity_info( + last_activity_at=edited_at, + last_activity_by=edited_by + ) - def _question__apply_edit(self, edited_at=None, edited_by=None, title=None,\ - text=None, comment=None, tags=None, wiki=False,\ - edit_anonymously=False, is_private=False,\ - by_email=False, suppress_email=False - ): + def _question__apply_edit( + self, + edited_at=None, + edited_by=None, + title=None, + text=None, + comment=None, + tags=None, + wiki=False, + edit_anonymously=False, + is_private=False, + by_email=False, suppress_email=False, + ip_addr=None + ): #todo: the thread editing should happen outside of this #method, then we'll be able to unify all the *__apply_edit @@ -1860,10 +1919,14 @@ class Post(models.Model): edit_anonymously=edit_anonymously, is_private=is_private, by_email=by_email, - suppress_email=suppress_email + suppress_email=suppress_email, + ip_addr=ip_addr ) - self.thread.set_last_activity(last_activity_at=edited_at, last_activity_by=edited_by) + self.thread.set_last_activity_info( + last_activity_at=edited_at, + last_activity_by=edited_by + ) def apply_edit(self, *args, **kwargs): #todo: unify this, here we have unnecessary indirection @@ -1880,53 +1943,42 @@ class Post(models.Model): def __add_revision( self, - author = None, - revised_at = None, - text = None, - comment = None, - by_email = False + author=None, + revised_at=None, + text=None, + comment=None, + by_email=False, + ip_addr=None ): #todo: this may be identical to Question.add_revision if None in (author, revised_at, text): raise Exception('arguments author, revised_at and text are required') - rev_no = self.revisions.all().count() + 1 - if comment in (None, ''): - if rev_no == 1: - comment = unicode(const.POST_STATUS['default_version']) - else: - comment = 'No.%s Revision' % rev_no return PostRevision.objects.create( - post = self, - author = author, - revised_at = revised_at, - text = text, - summary = comment, - revision = rev_no, - by_email = by_email + post=self, + author=author, + revised_at=revised_at, + text=text, + summary=comment, + by_email=by_email, + ip_addr=ip_addr ) def _question__add_revision( self, - author = None, - is_anonymous = False, - text = None, - comment = None, - revised_at = None, - by_email = False, - email_address = None + author=None, + is_anonymous=False, + text=None, + comment=None, + revised_at=None, + by_email=False, + email_address=None, + ip_addr=None, ): if None in (author, text): raise Exception('author, text and comment are required arguments') - rev_no = self.revisions.all().count() + 1 - if comment in (None, ''): - if rev_no == 1: - comment = unicode(const.POST_STATUS['default_version']) - else: - comment = 'No.%s Revision' % rev_no return PostRevision.objects.create( post=self, - revision=rev_no, title=self.thread.title, author=author, is_anonymous=is_anonymous, @@ -1935,7 +1987,8 @@ class Post(models.Model): summary=unicode(comment), text=text, by_email=by_email, - email_address=email_address + email_address=email_address, + ip_addr=ip_addr ) def add_revision(self, *kargs, **kwargs): @@ -2089,9 +2142,51 @@ class Post(models.Model): class PostRevisionManager(models.Manager): - def create(self, *kargs, **kwargs): - revision = super(PostRevisionManager, self).create(*kargs, **kwargs) - revision.moderate_or_publish() + def create(self, *args, **kwargs): + #clean the "summary" field + kwargs.setdefault('summary', '') + if kwargs['summary'] is None: + kwargs['summary'] = '' + + author = kwargs['author'] + + moderate_email = False + if kwargs.get('email'): + from askbot.models.reply_by_email import emailed_content_needs_moderation + moderate_email = emailed_content_needs_moderation(kwargs['email']) + + needs_moderation = author.needs_moderation() or moderate_email + + #0 revision is not shown to the users + if askbot_settings.CONTENT_MODERATION_MODE == 'premoderation' and needs_moderation: + kwargs.update({ + 'approved': False, + 'approved_by': None, + 'approved_at': None, + 'revision': 0, + 'summary': kwargs['summary'] or _('Suggested edit') + }) + revision = super(PostRevisionManager, self).create(*args, **kwargs) + else: + post = kwargs['post'] + kwargs['revision'] = post.get_latest_revision_number() + 1 + revision = super(PostRevisionManager, self).create(*args, **kwargs) + + #set default summary + if revision.summary == '': + if revision.revision == 1: + revision.summary = unicode(const.POST_STATUS['default_version']) + else: + revision.summary = 'No.%s Revision' % revision.revision + revision.save() + + from askbot.models import signals + signals.post_revision_published.send(None, revision=revision) + + #audit or pre-moderation modes require placement of the post on the moderation queue + if needs_moderation: + revision.place_on_moderation_queue() + return revision class PostRevision(models.Model): @@ -2118,6 +2213,7 @@ class PostRevision(models.Model): title = models.CharField(max_length=300, blank=True, default='') tagnames = models.CharField(max_length=125, blank=True, default='') is_anonymous = models.BooleanField(default=False) + ip_addr = models.IPAddressField(max_length=21, default='0.0.0.0') objects = PostRevisionManager() @@ -2129,84 +2225,67 @@ class PostRevision(models.Model): ordering = ('-revision',) app_label = 'askbot' - def needs_moderation(self): - """``True`` if post needs moderation""" - if askbot_settings.ENABLE_CONTENT_MODERATION: - #todo: needs a lot of details - if self.author.is_administrator_or_moderator(): - return False - if self.approved: - return False - #if sent by email to group and group does not want moderation - if self.by_email and self.email_address: - group_name = self.email_address.split('@')[0] - from askbot.models.user import Group - try: - group = Group.objects.get(name = group_name, deleted = False) - return group.group.profile.moderate_email - except Group.DoesNotExist: - pass - return True - return False + def place_on_moderation_queue(self): + """Revision has number 0, which is + reserved for the moderated revisions. + Flag Post.is_approved = False is used only + for posts that have only one revision - the + moderated one - i.e. for the new posts - def place_on_moderation_queue(self): - """If revision is the first one, - keeps the post invisible until the revision - is aprroved. - If the revision is an edit, will autoapprove - but will still add it to the moderation queue. - - Eventually we might find a way to moderate every - edit as well.""" + The same applies to the brand new Threads + Thread.is_approved = False is set to brand new + threads, whose first revision is moderated + + If post has > 1 revision and one on moderation + the Post(and Thread).is_approved will be True, + but the latest displayed revision will be + the one with != 0 revision number. + + This allows us to moderate every revision + """ #this is run on "post-save" so for a new post #we'll have just one revision if self.post.revisions.count() == 1: - activity_type = const.TYPE_ACTIVITY_MODERATED_NEW_POST - - self.approved = False - self.approved_by = None - self.approved_at = None - self.post.approved = False self.post.save() if self.post.is_question(): self.post.thread.approved = False self.post.thread.save() - #above changes will hide post from the public display - if self.by_email: - #todo: move this to the askbot.mail module - from askbot.mail import send_mail - email_context = { - 'site': askbot_settings.APP_SHORT_NAME - } - body_text = _( - 'Thank you for your post to %(site)s. ' - 'It will be published after the moderators review.' - ) % email_context - send_mail( - subject_line = _('your post to %(site)s') % email_context, - body_text = body_text, - recipient_list = [self.author.email,], - ) - else: - message = _( - 'Your post was placed on the moderation queue ' - 'and will be published after the moderator approval.' - ) - self.author.message_set.create(message = message) + #give message to the poster + if askbot_settings.CONTENT_MODERATION_MODE == 'premoderation': + if self.by_email: + #todo: move this to the askbot.mail module + from askbot.mail import send_mail + email_context = { + 'site': askbot_settings.APP_SHORT_NAME + } + body_text = _( + 'Thank you for your post to %(site)s. ' + 'It will be published after the moderators review.' + ) % email_context + send_mail( + subject_line = _('your post to %(site)s') % email_context, + body_text = body_text, + recipient_list = [self.author.email,], + ) + + else: + message = _( + 'Your post was placed on the moderation queue ' + 'and will be published after the moderator approval.' + ) + self.author.message_set.create(message = message) + + activity_type = const.TYPE_ACTIVITY_MODERATED_NEW_POST else: - #In this case, for now we just flag the edit - #for the moderators. - #Ideally we'd need to hide the edit itself, - #but the complication is that when we have more - #than one edit in a row and then we'll need to deal with - #merging multiple edits. We don't have a solution for this yet. + #In this case, use different activity type, but perhaps there is no real need activity_type = const.TYPE_ACTIVITY_MODERATED_POST_EDIT + #Activity instance is the actual queue item from askbot.models import Activity activity = Activity( user = self.author, @@ -2215,18 +2294,8 @@ class PostRevision(models.Model): question = self.get_origin_post() ) activity.save() - #todo: make this group-sensitive activity.add_recipients(self.post.get_moderators()) - def moderate_or_publish(self): - """either place on moderation queue or announce - that this revision is published""" - if self.needs_moderation():#moderate - self.place_on_moderation_queue() - else:#auto-approve - from askbot.models import signals - signals.post_revision_published.send(None, revision = self) - def should_notify_author_about_publishing(self, was_approved = False): """True if author should get email about making own post""" if self.by_email: @@ -2257,12 +2326,8 @@ class PostRevision(models.Model): raise ValidationError('Post field has to be set.') def save(self, **kwargs): - # Determine the revision number, if not set - if not self.revision: - # TODO: Maybe use Max() aggregation? Or `revisions.count() + 1` - self.revision = self.parent().revisions.values_list( - 'revision', flat=True - )[0] + 1 + if self.ip_addr is None: + self.ip_addr = '0.0.0.0' self.full_clean() super(PostRevision, self).save(**kwargs) @@ -2304,7 +2369,7 @@ class PostRevision(models.Model): 'title': self.title, 'html': sanitized_html } - elif self.post.is_answer(): + else: return sanitized_html def get_snippet(self, max_length = 120): @@ -2345,7 +2410,8 @@ class AnonymousAnswer(DraftContent): author=user, added_at=added_at, wiki=self.wiki, - text=self.text + text=self.text, + ip_addr=self.ip_addr, ) self.question.thread.invalidate_cached_data() self.delete() diff --git a/askbot/models/question.py b/askbot/models/question.py index 65e1168a..84a358e5 100644 --- a/askbot/models/question.py +++ b/askbot/models/question.py @@ -2,6 +2,7 @@ import datetime import operator import re +from copy import copy from django.conf import settings as django_settings from django.db import models from django.db.models import F @@ -153,6 +154,7 @@ class ThreadManager(BaseQuerySetManager): by_email=False, email_address=None, language=None, + ip_addr=None, ): """creates new thread""" # TODO: Some of this code will go to Post.objects.create_new @@ -204,7 +206,8 @@ class ThreadManager(BaseQuerySetManager): comment=unicode(const.POST_STATUS['default_version']), revised_at=added_at, by_email=by_email, - email_address=email_address + email_address=email_address, + ip_addr=ip_addr ) author_group = author.get_personal_group() @@ -287,7 +290,7 @@ class ThreadManager(BaseQuerySetManager): # TODO: add a possibility to see deleted questions qs = self.filter(**primary_filter) - if askbot_settings.ENABLE_CONTENT_MODERATION: + if askbot_settings.CONTENT_MODERATION_MODE == 'premoderation': qs = qs.filter(approved = True) #if groups feature is enabled, filter out threads @@ -896,7 +899,7 @@ class Thread(models.Model): self.answer_accepted_at = timestamp self.save() - def set_last_activity(self, last_activity_at, last_activity_by): + def set_last_activity_info(self, last_activity_at, last_activity_by): self.last_activity_at = last_activity_at self.last_activity_by = last_activity_by self.save() @@ -904,6 +907,27 @@ class Thread(models.Model): self.update_summary_html() # regenerate question/thread summary html #################################################################### + def get_last_activity_info(self): + post_ids = self.get_answers().values_list('id', flat=True) + question = self._question_post() + post_ids = list(post_ids) + post_ids.append(question.id) + from askbot.models import PostRevision + revs = PostRevision.objects.filter( + post__id__in=post_ids, + revision__gt=0 + ).order_by('-id') + try: + rev = revs[0] + return rev.revised_at, rev.author + except IndexError: + return None, None + + def update_last_activity_info(self): + timestamp, user = self.get_last_activity_info() + if timestamp: + self.set_last_activity_info(timestamp, user) + def get_tag_names(self): "Creates a list of Tag names from the ``tagnames`` attribute." if self.tagnames.strip() == '': @@ -1030,7 +1054,102 @@ class Thread(models.Model): else: self.update_summary_html() - def get_cached_post_data(self, user = None, sort_method = None): + def get_post_data_for_question_view(self, user=None, sort_method=None): + """loads post data for use in the question details view + """ + post_data = self.get_cached_post_data(user=user, sort_method=sort_method) + if user.is_anonymous(): + return post_data + + if askbot_settings.CONTENT_MODERATION_MODE == 'premoderation' and user.is_watched(): + #in this branch we patch post_data with the edits suggested by the + #watched user + post_data = list(post_data) + post_ids = self.posts.filter(author=user).values_list('id', flat=True) + from askbot.models import PostRevision + suggested_revs = PostRevision.objects.filter( + author=user, + post__id__in=post_ids, + revision=0 + ) + #get ids of posts that we need to patch with suggested data + if len(suggested_revs): + #find posts that we need to patch + def find_posts(posts, need_ids): + """posts - is source list + need_ids - set of post ids + """ + found = dict() + for post in posts: + if post.id in need_ids: + found[post.id] = post + need_ids.remove(post.id) + comments = post.get_cached_comments() + found.update(find_posts(comments, need_ids)) + return found + + suggested_post_ids = set([rev.post_id for rev in suggested_revs]) + + question = post_data[0] + answers = post_data[1] + post_to_author = post_data[2] + + post_id_set = set(suggested_post_ids) + + all_posts = copy(answers) + if question: + all_posts.append(question) + posts = find_posts(all_posts, post_id_set) + + rev_map = dict(zip(suggested_post_ids, suggested_revs)) + + for post_id, post in posts.items(): + rev = rev_map[post_id] + #patching work + post.text = rev.text + post.html = post.parse_post_text()['html'] + post_to_author[post_id] = rev.author_id + post.set_runtime_needs_moderation() + + def post_type_ord(p): + """need to sort by post type""" + if p.is_question(): + return 0 + elif p.is_answer(): + return 1 + return 2 + + def cmp_post_types(a, b): + """need to sort by post type""" + at = post_type_ord(a) + bt = post_type_ord(b) + return cmp(at, bt) + + if len(post_id_set): + #brand new suggested posts + from askbot.models import Post + #order by insures that + posts = list(Post.objects.filter(id__in=post_id_set)) + for post in sorted(posts, cmp=cmp_post_types): + rev = rev_map[post.id] + post.text = rev.text + post.html = post.parse_post_text()['html'] + post_to_author[post.id] = rev.author_id + if post.is_comment(): + parents = find_posts(all_posts, set([post.parent_id])) + parent = parents.values()[0] + parent.add_cached_comment(post) + if post.is_answer(): + answers.insert(0, post) + all_posts.append(post)#add b/c there may be self-comments + if post.is_question(): + post_data[0] = post + all_posts.append(post) + + return post_data + + + def get_cached_post_data(self, user=None, sort_method=None): """returns cached post data, as calculated by the method get_post_data()""" sort_method = sort_method or askbot_settings.DEFAULT_ANSWER_SORT_METHOD @@ -1347,7 +1466,7 @@ class Thread(models.Model): self._question_post().make_private(user, group_id) if len(groups) == 0: - message = 'Sharing did not work, because group is unknown' + message = _('Sharing did not work, because group is unknown') user.message_set.create(message=message) def is_private(self): diff --git a/askbot/models/reply_by_email.py b/askbot/models/reply_by_email.py index 0b164d24..309743cb 100644 --- a/askbot/models/reply_by_email.py +++ b/askbot/models/reply_by_email.py @@ -10,6 +10,22 @@ from askbot.models.base import BaseQuerySetManager from askbot.conf import settings as askbot_settings from askbot import mail +def emailed_content_needs_moderation(email): + """True, if we moderate content and if email address + is marked for moderation + todo: maybe this belongs to a separate "moderation" module + """ + if askbot_settings.CONTENT_MODERATION_MODE == 'premoderation': + group_name = email.split('@')[0] + from askbot.models.user import Group + try: + group = Group.objects.get(name=group_name, deleted=False) + return group.group.profile.moderate_email + except Group.DoesNotExist: + pass + return False + + class ReplyAddressManager(BaseQuerySetManager): """A manager for the :class:`ReplyAddress` model""" diff --git a/askbot/sitemap.py b/askbot/sitemap.py index c50c64dc..d12d3cf2 100644 --- a/askbot/sitemap.py +++ b/askbot/sitemap.py @@ -5,7 +5,10 @@ class QuestionsSitemap(Sitemap): changefreq = 'daily' priority = 0.5 def items(self): - return Post.objects.get_questions().exclude(deleted=True) + questions = Post.objects.get_questions() + questions = questions.exclude(deleted=True) + questions = questions.exclude(approved=False) + return questions.select_related('thread__title', 'thread__last_activity_at') def lastmod(self, obj): return obj.thread.last_activity_at diff --git a/askbot/templates/email/change_settings_info.html b/askbot/templates/email/change_settings_info.html index 0f96caa4..fde9b659 100644 --- a/askbot/templates/email/change_settings_info.html +++ b/askbot/templates/email/change_settings_info.html @@ -3,7 +3,7 @@ {% if is_multilingual %} {% trans %}To change frequency, language and content of these alerts, please visit <a href="{{ url }}">your user profile</a>.{% endtrans %} {% else %} - {% trans %}To change freqency and content of these alerts, please visit <a href="{{ url }}">your user profile</a>.{% endtrans %} + {% trans %}To change frequency and content of these alerts, please visit <a href="{{ url }}">your user profile</a>.{% endtrans %} {% endif %} <br/> {% trans %}If you believe that this message was sent in an error, please email about it the forum administrator at <a href="mailto:{{ admin_email }}">{{ admin_email }}</a>.{% endtrans %} diff --git a/askbot/templates/livesettings/group_settings.html b/askbot/templates/livesettings/group_settings.html index 3c28d320..16fa3241 100644 --- a/askbot/templates/livesettings/group_settings.html +++ b/askbot/templates/livesettings/group_settings.html @@ -44,7 +44,7 @@ {{field}} {% endif %} {% endfor %} - <input type="submit" value="Save" class="default" /> + <input type="submit" value="{% trans %}Save{% endtrans %}" class="default" /> </form> {% else %} <p>{% trans %}You don't have permission to edit values.{% endtrans %}</p> diff --git a/askbot/templates/livesettings/site_settings.html b/askbot/templates/livesettings/site_settings.html index 13d8ea40..3e02656c 100644 --- a/askbot/templates/livesettings/site_settings.html +++ b/askbot/templates/livesettings/site_settings.html @@ -90,7 +90,7 @@ div.fieldcontainer { float: left; margin-right: 0; } </div> {% admin_site_views 'satchmo_site_settings' %} <br class="clear:both;" /> - <input type="submit" value="Save" class="default" /> + <input type="submit" value="{% trans %}Save{% endtrans %}" class="default" /> <p><a onclick="javascript:CollapsedFieldsets.uncollapse_all(); return false;" href="#">{% trans 'Uncollapse all' %}</a></p> <p><a href="{% url settings_export %}">Export</a></p> </form> diff --git a/askbot/templates/macros.html b/askbot/templates/macros.html index b314255c..942d150f 100644 --- a/askbot/templates/macros.html +++ b/askbot/templates/macros.html @@ -6,31 +6,31 @@ >{% if icon == False %}{% if site_label %}{{ site_label }}{% else %}{{ site }}{% endif %}{% endif %}</a> {%- endmacro -%} -{%- macro inbox_post_snippet(response, inbox_section) -%} -<div id="re_{{response.id}}" class="re{% if response.is_new %} new highlight{% else %} seen{% endif %}"> +{%- macro inbox_message_snippet(message) -%} +<div class="message-details" data-message-id="{{ message.id }}"> <input type="checkbox" /> - <div class="face"> - {{ gravatar(response.user, 48) }} - </div> - <div class="content"> - <a - style="font-size:12px" - href="{{ response.user.get_absolute_url() }}" - >{{ response.user.username|escape }}</a> - <a style="text-decoration:none;" href="{{ response.response_url }}"> - {{ response.response_type }} - ({{ timeago(response.timestamp) }}):<br/> - {% if inbox_section != 'flags' %} - {{ response.response_snippet }} - {% endif %} - </a> - {% if inbox_section == 'flags' %} - <a class="re_expand" href="{{ response.response_url }}"> - <!--div class="re_snippet">{{ response.response_snippet|escape }}</div--> - <div class="re_content">{{ response.response_content }}</div> - </a> - {% endif %} - </div> + {{ gravatar(message.user, 24) }} + <a class="username" href="{{ message.user.get_absolute_url() }}">{{ message.user.username|escape }}</a> + <a class="forum-post-link" href="{{ message.url }}">{{ message.message_type }}</a> + ({{ timeago(message.timestamp) }}) + {#<div class="snippet">{{ message.snippet }}</div>#} + <div class="post-content">{{ message.content }}</div> +</div> +{%- endmacro -%} + +{%- macro moderation_queue_message(message) -%} +<div class="message-details" data-message-id="{{ message.id }}"> + <input type="checkbox" /> + {{ gravatar(message.user, 24) }} + <a class="username" href="{{ message.user.get_absolute_url() }}">{{ message.user.username|escape }}</a> | + {% if message['memo_type'] == 'edit' %} + <a href="mailto:{{ message.user.email }}">{{ message.user.email }}</a> | + ip=<span class="ip-addr">{{ message.ip_addr }}</span> | + {% endif %} + <a class="forum-post-link" href="{{ message.url }}">{{ message.message_type }}</a> + ({{ timeago(message.timestamp) }}) + {#<div class="snippet">{{ message.snippet }}</div>#} + <div class="post-content">{{ message.content }}</div> </div> {%- endmacro -%} @@ -393,18 +393,19 @@ for the purposes of the AJAX comment editor #} ) -%} {% spaceless %} - {% if post.comment_count > 0 %} + {% if show_post == post and show_comment and show_comment_position > max_comments %} + {% set comments = post.get_cached_comments()[:show_comment_position] %} + {% else %} + {% set comments = post.get_cached_comments()[:max_comments] %} + {% endif %} + {% set comments_count = comments|length %} + {% if comments_count > 0 %} <h2 class="comment-title">{% trans %}Comments{% endtrans %}</h2> <div class="clean"></div> {% endif %} {% set widget_id = 'comments-for-' + post.post_type + '-' + post.id|string %} - <div class="comments{% if post.comment_count == 0 %} empty{% endif %}" id="{{ widget_id }}"> + <div class="comments{% if comments_count == 0 %} empty{% endif %}" id="{{ widget_id }}"> <div class="content"> - {% if show_post == post and show_comment and show_comment_position > max_comments %} - {% set comments = post.get_cached_comments()[:show_comment_position] %} - {% else %} - {% set comments = post.get_cached_comments()[:max_comments] %} - {% endif %} {% for comment in comments %} {# Warning! Any changes to the comment markup IN THIS `FOR` LOOP must be duplicated in post.js for the purposes of the AJAX comment editor #} @@ -435,6 +436,9 @@ for the purposes of the AJAX comment editor #} title="{% trans %}delete this comment{% endtrans %}" ></span> </div> + {% if comment.needs_moderation() %} + <p class="moderated">{% trans %}This post is awaiting moderation{% endtrans %}</p> + {% endif %} <div class="comment-body"> {{ comment.summary }} <a @@ -792,17 +796,12 @@ for the purposes of the AJAX comment editor #} {%- macro inbox_link(user) -%} - {% if user.new_response_count > 0 or user.seen_response_count > 0 %} + {% if user.new_response_count %} <a id='ab-responses' href="{{user.get_absolute_url()}}?sort=inbox§ion=forum"> <img alt="{% trans username=user.username|escape %}responses for {{username}}{% endtrans %}" - {% if user.new_response_count > 0 %} - src="{{ "/images/mail-envelope-full.png"|media }}" - title="{% trans response_count=user.new_response_count %}you have {{response_count}} new response{% pluralize %}you have {{response_count}} new responses{% endtrans %}" - {% elif user.seen_response_count > 0 %} - src="{{ "/images/mail-envelope-empty.png"|media }}" - title="{% trans %}no new responses yet{% endtrans %}" - {% endif %} + src="{{ "/images/mail-envelope-full.png"|media }}" + title="{% trans response_count=user.new_response_count %}you have {{response_count}} new response{% pluralize %}you have {{response_count}} new responses{% endtrans %}" /> </a> {% endif %} diff --git a/askbot/templates/moderation/manage_reject_reasons_dialog.html b/askbot/templates/moderation/manage_reject_reasons_dialog.html new file mode 100644 index 00000000..f9ef0e31 --- /dev/null +++ b/askbot/templates/moderation/manage_reject_reasons_dialog.html @@ -0,0 +1,43 @@ +<div class="modal" style="display:none" id="manage-reject-reasons-modal"> + <div class="modal-header"> + <a class="close" data-dismiss="modal">x</a> + <h3>{% trans %}Manage post flag/reject reasons{% endtrans %}</h3> + </div> + <div id="reject-edit-modal-add-new">{# create new reject reason #} + <div class="modal-body"> + <input + class="reject-reason-title tipped-input blank" + type="text" + value="{% trans %}1) Enter a brief description of why you are rejecting the post.{% endtrans %}" + /> + <textarea class="reject-reason-details tipped-input blank" + >{% trans %}2) Please enter details here. This text will be sent to the user.{% endtrans %}</textarea> + </div> + <div class="modal-footer"> + <div class="btn-toolbar"> + <a class="btn save-reason">{% trans %}Save reason{% endtrans %}</a> + <a class="btn">{% trans %}Cancel{% endtrans %}</a> + </div> + </div> + </div> + <div id="reject-edit-modal-select">{# select one of existing reasons #} + <div class="modal-body"> + <ul class="select-box"> + {% for reason in post_reject_reasons %} + <li + class="select-box-item" + data-original-title="{{reason.details.text|escape}}" + data-item-id="{{reason.id}}" + >{{reason.title|escape}}</li> + {% endfor %} + </ul> + </div> + <div class="modal-footer"> + <div class="btn-toolbar"> + <a class="btn edit-this-reason" style="display: none;">{% trans %}Edit this reason{% endtrans %}</a> + <a class="btn delete-this-reason" style="display: none;">{% trans %}Delete this reason{% endtrans %}</a> + <a class="btn add-new-reason">{% trans %}Add a new reason{% endtrans %}</a> + </div> + </div> + </div> +</div> diff --git a/askbot/templates/moderation/queue.html b/askbot/templates/moderation/queue.html new file mode 100644 index 00000000..31d0b717 --- /dev/null +++ b/askbot/templates/moderation/queue.html @@ -0,0 +1,65 @@ +{% extends "user_inbox/base.html" %} +{% import "macros.html" as macros %} +{% block profilesection %} + {% trans %}moderation queue{% endtrans %} +{% endblock %} +{% block inbox_content %} + <div class="tools"> + {#<div class="select-items"> + <strong>{% trans %}Select:{% endtrans %}</strong> + <a class="sel-all">{% trans %}all{% endtrans %}</a> | + <a class="sel-none">{% trans %}none{% endtrans %}</a> + </div>#} + <a class="btn approve-posts">{% trans %}approve posts{% endtrans %}</a> + <a class="btn approve-posts-users" id="re_approve_posts_users">{% trans %}approve posts and users{% endtrans %}</a> + <div class="btn-group dropdown decline-reasons-menu"> + <span class="btn btn-info dropdown-toggle"> + <span>{% trans %}decline and explain why{% endtrans %}</span> + <span class="caret"></span> + </span> + <ul class="dropdown-menu"> + {% for reason in post_reject_reasons %} + <li> + <a class="decline-with-reason" data-reason-id="{{ reason.id }}">{{ reason.title|escape }}</a> + </li> + {% endfor %} + <li> + <a class="manage-reasons">{% trans %}add/manage reject reasons{% endtrans %}</a> + </li> + </ul> + </div> + <a class="btn btn-danger decline-block-users">{% trans %}block spammers{% endtrans %}</a> + {% if settings.IP_MODERATION_ENABLED %} + <a class="btn btn-danger decline-block-users-ips">{% trans %}block spammers and IPs{% endtrans %}</a> + {% endif %} + </div> + <ul style="margin-top: 12px"> + <li>Approval of users removes them from the queue and approves ALL of their posts.</li> + <li>Blocking spammers denies them future access and deletes ALL their posts.</li> + {% if settings.IP_MODERATION_ENABLED %} + <li>Blocking IPs denies access by IP address and blocks all accounts using those IPs (and mass deletes content as above).</li> + {% endif %} + <ul> + {% include "moderation/manage_reject_reasons_dialog.html" %} + <div class="action-status"><span></span></div> + <div class="messages"> + {% for message in messages %}{# messages are grouped by question, using the "nested_messages" #} + <div + class="message{% if message.is_new %} highlight new{% else %} seen{% endif %}" + data-message-id="{{ message.id }}" + > + {#<h2>"{{ message.title.strip()|escape}}"</h2>#} + {{ macros.moderation_queue_message(message) }} + </div> + {# "nested" messages are further response messages to the same question #} + {% for followup_message in message.followup_messages %} + <div + class="message{% if followup_message.is_new %} highlight new{% else %} seen{% endif %}" + data-message-id="{{ followup_message.id }}" + > + {{ macros.moderation_queue_message(followup_message) }} + </div> + {% endfor %} + {% endfor %} + </div> +{% endblock %} diff --git a/askbot/templates/question/answer_card.html b/askbot/templates/question/answer_card.html index 9a833a35..b7ec2435 100644 --- a/askbot/templates/question/answer_card.html +++ b/askbot/templates/question/answer_card.html @@ -20,6 +20,9 @@ {% if answer.id in published_answer_ids %} <p><strong>{% trans %}This response is published{% endtrans %}</strong></p> {% endif %} + {% if answer.needs_moderation() %} + <p class="moderated">{% trans %}This post is awaiting moderation{% endtrans %}</p> + {% endif %} {{ answer.summary }} </div> <div class="answer-controls post-controls"> diff --git a/askbot/templates/question/question_card.html b/askbot/templates/question/question_card.html index d6c37260..c90489a5 100644 --- a/askbot/templates/question/question_card.html +++ b/askbot/templates/question/question_card.html @@ -13,6 +13,9 @@ <div class="post-update-info-container"> {% include "question/question_author_info.html" %} </div> + {% if question.needs_moderation() %} + <p class="moderated">{% trans %}This post is awaiting moderation{% endtrans %}</p> + {% endif %} {{ question.summary }} </div> <div id="question-controls" class="post-controls"> diff --git a/askbot/templates/user_inbox/base.html b/askbot/templates/user_inbox/base.html index db657b23..5a1dcb01 100644 --- a/askbot/templates/user_inbox/base.html +++ b/askbot/templates/user_inbox/base.html @@ -10,35 +10,37 @@ {% set re_count = request.user.new_response_count + request.user.seen_response_count %} - <div id="re_sections"> - {% trans %}Sections:{% endtrans %} - {% set sep = joiner('|') %} - {#{{ sep() }} - <a href="{{request.user.get_absolute_url()}}?sort=inbox§ion=messages" - {% if inbox_section == 'messages' %}class="on"{% endif %} - >{% trans %}messages{% endtrans %}</a>#} - {% if re_count > 0 %}{{ sep() }} - <a href="{{request.user.get_absolute_url()}}?sort=inbox§ion=forum" - {% if inbox_section == 'forum' %}class="on"{% endif %} + {% if need_inbox_sections_nav %} + <div id="re_sections"> + {% trans %}Sections:{% endtrans %} + {% set sep = joiner('|') %} + {#{{ sep() }} + <a href="{{request.user.get_absolute_url()}}?sort=inbox§ion=messages" + {% if inbox_section == 'messages' %}class="on"{% endif %} + >{% trans %}messages{% endtrans %}</a>#} + {% if re_count > 0 %}{{ sep() }} + <a href="{{request.user.get_absolute_url()}}?sort=inbox§ion=forum" + {% if inbox_section == 'forum' %}class="on"{% endif %} + > + {% trans %}forum responses (<span class="response-count">{{re_count}}</span>){% endtrans -%} + </a> + {% endif %} + {% if flags_count > 0 %}{{ sep() }} + <a href="{{request.user.get_absolute_url()}}?sort=inbox§ion=flags" + {% if inbox_section == 'flags' %}class="on"{% endif %} + > + {% trans %}flagged items (<span class="mod-memo-count">{{flags_count}}</span>){% endtrans %} + </a> + {% endif %} + {% if group_join_requests_count %}{{ sep() }} + <a href="{{request.user.get_absolute_url()}}?sort=inbox§ion=join_requests" + {% if inbox_section == 'join_requests' %}class="on"{% endif %} > - {% trans %}forum responses ({{re_count}}){% endtrans -%} + {% trans %}group join requests{% endtrans %} </a> - {% endif %} - {% if flags_count > 0 %}{{ sep() }} - <a href="{{request.user.get_absolute_url()}}?sort=inbox§ion=flags" - {% if inbox_section == 'flags' %}class="on"{% endif %} - > - {% trans %}flagged items ({{flags_count}}){% endtrans %} - </a> - {% endif %} - {% if group_join_requests_count %}{{ sep() }} - <a href="{{request.user.get_absolute_url()}}?sort=inbox§ion=join_requests" - {% if inbox_section == 'join_requests' %}class="on"{% endif %} - > - {% trans %}group join requests{% endtrans %} - </a> - {% endif %} - </div> + {% endif %} + </div> + {% endif %} {% block inbox_content %} {% endblock %} </div> @@ -48,6 +50,8 @@ var askbot = askbot || {}; askbot['urls'] = askbot['urls'] || {}; askbot['urls']['manageInbox'] = '{% url manage_inbox %}'; + askbot['urls']['clearNewNotifications'] = '{% url clear_new_notifications %}'; + askbot['urls']['moderatePostEdits'] = '{% url moderate_post_edits %}'; askbot['urls']['save_post_reject_reason'] = '{% url save_post_reject_reason %}'; askbot['urls']['delete_post_reject_reason'] = '{% url delete_post_reject_reason %}'; {% if request.user.is_administrator_or_moderator() %} diff --git a/askbot/templates/user_inbox/responses.html b/askbot/templates/user_inbox/responses.html new file mode 100644 index 00000000..828e839d --- /dev/null +++ b/askbot/templates/user_inbox/responses.html @@ -0,0 +1,28 @@ +{% extends "user_inbox/base.html" %} +{% import "macros.html" as macros %} +{% block profilesection %} + {% trans %}inbox - moderation queue{% endtrans %} +{% endblock %} +{% block inbox_content %} + {% if request.user.new_response_count %} + <p class="clear-messages"><a>{% trans %}Clear new notifications{% endtrans %}</a></p> + {% endif %} + <div class="messages"> + {% for message in messages %}{# messages are grouped by question, using the "nested_messages" #} + <div class="message{% if message.is_new %} highlight new{% else %} seen{% endif %}" + data-message-id="{{ message.id }}" + > + <h2>"{{ message.title.strip()|escape}}"</h2> + {{ macros.inbox_message_snippet(message) }} + </div> + {# "nested" messages are further response messages to the same question #} + {% for followup_message in message.followup_messages %} + <div class="message{% if message.is_new %} highlight new{% else %} seen{% endif %}" + data-message-id="{{ message.id }}" + > + {{ macros.inbox_message_snippet(followup_message) }} + </div> + {% endfor %} + {% endfor %} + </div> +{% endblock %} diff --git a/askbot/templates/user_inbox/responses_and_flags.html b/askbot/templates/user_inbox/responses_and_flags.html deleted file mode 100644 index 16599c1d..00000000 --- a/askbot/templates/user_inbox/responses_and_flags.html +++ /dev/null @@ -1,41 +0,0 @@ -{% extends "user_inbox/base.html" %} -{% import "macros.html" as macros %} -{% block profilesection %} - {% trans %}inbox - responses{% endtrans %} -{% endblock %} -{% block inbox_content %} - <div id="re_tools"> - <strong>{% trans %}select:{% endtrans %}</strong> - <a id="sel_all">{% trans %}all{% endtrans %}</a> | - <a id="sel_seen">{% trans %}seen{% endtrans %}</a> | - <a id="sel_new">{% trans %}new{% endtrans %}</a> | - <a id="sel_none">{% trans %}none{% endtrans %}</a><br /> - <div class="btn-group"> - {% if inbox_section == 'forum' %} - <a class="btn" id="re_mark_seen">{% trans %}mark as seen{% endtrans %}</a> - <a class="btn" id="re_mark_new">{% trans %}mark as new{% endtrans %}</a> - <a class="btn" id="re_dismiss">{% trans %}dismiss{% endtrans %}</a> - {% else %} - <a class="btn" id="re_remove_flag">{% trans %}remove flags/approve{% endtrans %}</a> - <a - class="btn" - id="re_delete_post" - >{% trans %}delete post{% endtrans %}</a> - {% endif %} - </div> - </div> - {% include "user_profile/reject_post_dialog.html" %} - <div id="responses"> - {% for response in responses %} - <div class="response-parent" data-response-id="{{response.id}}"> - <h2>"{{ response.response_title.strip()|escape}}"</h2> - {{ macros.inbox_post_snippet(response, inbox_section) }} - {% for nested_response in response.nested_responses %} - {{ macros.inbox_post_snippet(nested_response, inbox_section) }} - {%endfor%} - </div> - <div class="clearfix"></div> - {% endfor %} - </div> - </div> -{% endblock %} diff --git a/askbot/templates/user_profile/reject_post_dialog.html b/askbot/templates/user_profile/reject_post_dialog.html deleted file mode 100644 index 3483e83e..00000000 --- a/askbot/templates/user_profile/reject_post_dialog.html +++ /dev/null @@ -1,109 +0,0 @@ -<div class="modal" style="display:none" id="reject-edit-modal"> - <div class="modal-header"> - <a class="close" data-dismiss="modal">x</a> - <h3>{% trans %}Reject the post(s)?{% endtrans %}</h3> - </div> - <div id="reject-edit-modal-add-new">{# create new reject reason #} - <div class="modal-body"> - <input - class="reject-reason-title tipped-input blank" - type="text" - value="{% trans %}1) Enter a brief description of why you are rejecting the post.{% endtrans %}" - /> - <textarea class="reject-reason-details tipped-input blank" - >{% trans %}2) Please enter details here. This text will be sent to the user.{% endtrans %}</textarea> - </div> - <div class="modal-footer"> - <div class="btn-toolbar"> - <div class="btn-group dropup"> - <button class="btn btn-danger save-reason-and-reject" - >{% trans %}Use this reason & reject{% endtrans %}</button> - <button class="btn btn-danger dropdown-toggle" data-toggle="dropdown"> - <span class="caret"></span> - </button> - <ul class="dropdown-menu"> - <li> - <a class="select-other-reason" href="#" - >{% trans %}Use other reason{% endtrans %}</a> - </li> - </ul> - </div> - <div class="btn-group"> - <a class="btn save-reason" - >{% trans %}Save reason, but do not reject{% endtrans %}</a> - </div> - <div class="btn-group"> - <a class="btn cancel">{% trans %}Cancel{% endtrans %}</a> - </div> - </div> - </div> - </div> - <div id="reject-edit-modal-select">{# select one of existing reasons #} - <div class="modal-body"> - <p>{% trans %}Please, choose a reason for the rejection.{% endtrans %}</p> - <ul class="select-box"> - {% for reason in post_reject_reasons %} - <li - class="select-box-item" - data-original-title="{{reason.details.text|escape}}" - data-item-id="{{reason.id}}" - >{{reason.title|escape}}</li> - {% endfor %} - </ul> - </div> - <div class="modal-footer"> - <div class="btn-toolbar"> - <div class="btn-group dropup"> - <a class="btn select-this-reason" - >{% trans %}Select this reason{% endtrans %}</a> - <a class="btn dropdown-toggle" data-toggle="dropdown"> - <span class="caret"></span> - </a> - <ul class="dropdown-menu"> - <li> - <a class="delete-this-reason" - >{% trans %}Delete this reason{% endtrans %}</a> - </li> - </ul> - </div> - <div class="btn-group"> - <a class="btn add-new-reason" - >{% trans %}Add a new reason{% endtrans %}</a> - </div> - <div class="btn-group"> - <a class="btn cancel">{% trans %}Cancel{% endtrans %}</a> - </div> - </div> - </div> - </div> - <div id="reject-edit-modal-preview">{# preview reject reason #} - <div class="modal-body"> - <p>{% trans %}You have selected reason for the rejection <strong>"<span class="selected-reason-title"></span>"</strong>. The text below will be sent to the user and the post(s) will be deleted:{% endtrans %}</p> - <textarea disabled="disabled" class="selected-reason-details"></textarea> - </div> - <div class="modal-footer"> - <div class="btn-toolbar"> - <div class="btn-group dropup"> - <a class="btn btn-danger reject" - >{% trans %}Use this reason & reject{% endtrans %}</a> - <a class="btn btn-danger dropdown-toggle" data-toggle="dropdown"> - <span class="caret"></span> - </a> - <ul class="dropdown-menu"> - <li> - <a class="select-other-reason" - >{% trans %}Use other reason{% endtrans %}</a> - </li> - </ul> - </div> - <div class="btn-group"> - <a class="btn edit-reason" - >{% trans %}Edit this reason{% endtrans %}</a> - </div> - <div class="btn-group"> - <a class="btn cancel">{% trans %}Cancel{% endtrans %}</a> - </div> - </div> - </div> - </div> -</div> diff --git a/askbot/templates/user_profile/user_info.html b/askbot/templates/user_profile/user_info.html index 6a9ff91e..9e1adcca 100644 --- a/askbot/templates/user_profile/user_info.html +++ b/askbot/templates/user_profile/user_info.html @@ -46,7 +46,7 @@ {% endif %} <tr> <th colspan="2" align="left"> - <h3>{{user_status_for_display}}</h3> + <h3>{{ view_user.get_status_display() }}</h3> </th> </tr> {% if view_user.real_name %} diff --git a/askbot/templates/user_profile/user_moderate.html b/askbot/templates/user_profile/user_moderate.html index 75c6d6fe..b944b84a 100644 --- a/askbot/templates/user_profile/user_moderate.html +++ b/askbot/templates/user_profile/user_moderate.html @@ -8,16 +8,20 @@ <h3>{% trans username=view_user.username|escape, status=view_user.get_status_display() %}{{username}}'s current status is "{{status}}"{% endtrans %} </h3> {% if user_status_changed %} - <p class="action-status"><span>{% trans %}User status changed{% endtrans %}</span></p> + <p class="action-status"><span>{{ user_status_changed_message }}</span></p> {% endif %} <form method="post">{% csrf_token %} <input type="hidden" name="sort" value="moderate"/> <table class="form-as-table"> {{ change_user_status_form.as_table() }} </table> - <p id="id_user_status_info"> + <p id="id_user_status_info"></p> + <p> + <input type="submit" name="change_status" value="{% trans %}Change status{% endtrans %}" /> + {% if not view_user.is_blocked() %} + <input type="submit" name="hard_block" value="{% trans %}Block user and delete all content{% endtrans %}" /> + {% endif %} </p> - <input type="submit" name="change_status" value="{% trans %}Save{% endtrans %}" /> </form> {% endif %} <h3> @@ -89,5 +93,9 @@ $('#id_user_status_info').hide('slow'); } }) + $('input[name="hard_block"]').click(function() { + $('input[name="delete_content"]').val('true'); + $('select[name="user_status"]').val('b'); + }); </script> {% endblock %} diff --git a/askbot/templates/widgets/system_messages.html b/askbot/templates/widgets/system_messages.html index 69f6672f..48170857 100644 --- a/askbot/templates/widgets/system_messages.html +++ b/askbot/templates/widgets/system_messages.html @@ -2,7 +2,7 @@ <div class="content-wrapper"> {% if user_messages %} {% for message in user_messages %} - <p class="notification">{{ message }}</p> + {% if message %}<p class="notification">{{ message }}</p>{% endif %} {% endfor %} {% endif %} <a id="closeNotify" onclick="notify.close(true)"></a> diff --git a/askbot/tests/email_alert_tests.py b/askbot/tests/email_alert_tests.py index 902b810d..4c541395 100644 --- a/askbot/tests/email_alert_tests.py +++ b/askbot/tests/email_alert_tests.py @@ -1054,9 +1054,9 @@ class PostApprovalTests(utils.AskbotTestCase): def setUp(self): self.reply_by_email = askbot_settings.REPLY_BY_EMAIL askbot_settings.update('REPLY_BY_EMAIL', True) - self.enable_content_moderation = \ - askbot_settings.ENABLE_CONTENT_MODERATION - askbot_settings.update('ENABLE_CONTENT_MODERATION', True) + self.content_moderation_mode = \ + askbot_settings.CONTENT_MODERATION_MODE + askbot_settings.update('CONTENT_MODERATION_MODE', 'premoderation') self.self_notify_when = \ askbot_settings.SELF_NOTIFY_EMAILED_POST_AUTHOR_WHEN when = const.FOR_FIRST_REVISION @@ -1070,8 +1070,8 @@ class PostApprovalTests(utils.AskbotTestCase): 'REPLY_BY_EMAIL', self.reply_by_email ) askbot_settings.update( - 'ENABLE_CONTENT_MODERATION', - self.enable_content_moderation + 'CONTENT_MODERATION_MODE', + self.content_moderation_mode ) askbot_settings.update( 'SELF_NOTIFY_EMAILED_POST_AUTHOR_WHEN', @@ -1088,7 +1088,7 @@ class PostApprovalTests(utils.AskbotTestCase): self.assertEquals(outbox[0].recipients(), [self.u1.email]) def test_moderated_question_answerable_approval_notification(self): - u1 = self.create_user('user1', status = 'a') + u1 = self.create_user('user1', status = 'w') question = self.post_question(user = u1, by_email = True) self.assertEquals(question.approved, False) diff --git a/askbot/tests/form_tests.py b/askbot/tests/form_tests.py index c21ac5bf..9a82b42a 100644 --- a/askbot/tests/form_tests.py +++ b/askbot/tests/form_tests.py @@ -263,7 +263,7 @@ class AskFormTests(AskbotTestCase): class UserStatusFormTest(AskbotTestCase): def setup_data(self, status): - data = {'user_status': status} + data = {'user_status': status, 'delete_content': False} self.moderator = self.create_user('moderator_user') self.moderator.set_status('m') self.subject = self.create_user('normal_user') diff --git a/askbot/tests/page_load_tests.py b/askbot/tests/page_load_tests.py index 1143d056..9b16f58b 100644 --- a/askbot/tests/page_load_tests.py +++ b/askbot/tests/page_load_tests.py @@ -563,7 +563,7 @@ class PageLoadTestCase(AskbotTestCase): 'user_profile', kwargs={'id': asker.id, 'slug': slugify(asker.username)}, data={'sort':'inbox'}, - template='user_inbox/responses_and_flags.html', + template='user_inbox/responses.html', ) @with_settings(GROUPS_ENABLED=True) diff --git a/askbot/tests/post_model_tests.py b/askbot/tests/post_model_tests.py index 6d9233a2..2b08ecbf 100644 --- a/askbot/tests/post_model_tests.py +++ b/askbot/tests/post_model_tests.py @@ -29,6 +29,7 @@ class PostModelTests(AskbotTestCase): self.u3 = self.create_user(username='user3') def test_model_validation(self): + """ self.assertRaisesRegexp( AttributeError, r"'NoneType' object has no attribute 'revisions'", @@ -42,6 +43,7 @@ class PostModelTests(AskbotTestCase): } ) + #this test does not work post_revision = PostRevision( text='blah', author=self.u1, @@ -54,6 +56,7 @@ class PostModelTests(AskbotTestCase): r"{'__all__': \[u'Post field has to be set.'\]}", post_revision.save ) + """ question = self.post_question(user=self.u1) diff --git a/askbot/urls.py b/askbot/urls.py index 799ef0f5..93fb879c 100644 --- a/askbot/urls.py +++ b/askbot/urls.py @@ -206,6 +206,11 @@ urlpatterns = patterns('', name='moderate_group_join_request' ), service_url( + r'^moderate-post-edits/', + views.moderation.moderate_post_edits, + name='moderate_post_edits' + ), + service_url( r'^save-draft-question/', views.commands.save_draft_question, name = 'save_draft_question' @@ -489,6 +494,11 @@ urlpatterns = patterns('', name='manage_inbox' ), service_url(#ajax only + r'^clear-new-notifications/$', + views.users.clear_new_notifications, + name='clear_new_notifications' + ), + service_url(#ajax only r'^save-post-reject-reason/$', views.commands.save_post_reject_reason, name='save_post_reject_reason' diff --git a/askbot/utils/decorators.py b/askbot/utils/decorators.py index 815e6e2d..619cf9bc 100644 --- a/askbot/utils/decorators.py +++ b/askbot/utils/decorators.py @@ -195,7 +195,7 @@ def check_spam(field): if askbot_settings.USE_AKISMET and request.method == "POST": comment = smart_str(request.POST[field]) - data = {'user_ip': request.META["REMOTE_ADDR"], + data = {'user_ip': request.META.get('REMOTE_ADDR'), 'user_agent': request.environ['HTTP_USER_AGENT'], 'comment_author': smart_str(request.user.username), } diff --git a/askbot/views/__init__.py b/askbot/views/__init__.py index 481ca8ab..461c80df 100644 --- a/askbot/views/__init__.py +++ b/askbot/views/__init__.py @@ -9,6 +9,7 @@ from askbot.views import meta from askbot.views import sharing from askbot.views import widgets from askbot.views import api_v1 +from askbot.views import moderation from django.conf import settings if 'avatar' in settings.INSTALLED_APPS: from askbot.views import avatar_views diff --git a/askbot/views/commands.py b/askbot/views/commands.py index 2ccad9d5..d9866db9 100644 --- a/askbot/views/commands.py +++ b/askbot/views/commands.py @@ -24,9 +24,11 @@ from django.shortcuts import render from django.template.loader import get_template from django.views.decorators import csrf from django.utils import simplejson -from django.utils.html import escape from django.utils import translation +from django.utils.encoding import force_text +from django.utils.html import escape from django.utils.translation import ugettext as _ +from django.utils.translation import ungettext from django.utils.translation import string_concat from askbot.utils.slug import slugify from askbot import models @@ -45,7 +47,6 @@ from askbot.skins.loaders import render_text_into_skin from askbot.models.tag import get_tags_by_names - @csrf.csrf_exempt def manage_inbox(request): """delete, mark as new or seen user's @@ -53,7 +54,6 @@ def manage_inbox(request): request data is memo_list - list of integer id's of the ActivityAuditStatus items and action_type - string - one of delete|mark_new|mark_seen """ - response_data = dict() try: if request.is_ajax(): diff --git a/askbot/views/context.py b/askbot/views/context.py index eda8f6a7..339832ba 100644 --- a/askbot/views/context.py +++ b/askbot/views/context.py @@ -32,12 +32,11 @@ def get_for_inbox(user): return None #get flags count - flag_activity_types = (const.TYPE_ACTIVITY_MARK_OFFENSIVE,) - if askbot_settings.ENABLE_CONTENT_MODERATION: - flag_activity_types += ( - const.TYPE_ACTIVITY_MODERATED_NEW_POST, - const.TYPE_ACTIVITY_MODERATED_POST_EDIT - ) + flag_activity_types = ( + const.TYPE_ACTIVITY_MARK_OFFENSIVE, + const.TYPE_ACTIVITY_MODERATED_NEW_POST, + const.TYPE_ACTIVITY_MODERATED_POST_EDIT + ) #get group_join_requests_count group_join_requests_count = 0 @@ -48,10 +47,13 @@ def get_for_inbox(user): ) group_join_requests_count = pending_memberships.count() + re_count = user.new_response_count + user.seen_response_count + flags_count = user.get_notifications(flag_activity_types).count() return { - 're_count': user.new_response_count + user.seen_response_count, - 'flags_count': user.get_notifications(flag_activity_types).count(), - 'group_join_requests_count': group_join_requests_count + 're_count': re_count, + 'flags_count': flags_count, + 'group_join_requests_count': group_join_requests_count, + 'need_inbox_sections_nav': int(re_count > 0) + int(flags_count > 0) + int(group_join_requests_count) > 1 } def get_extra(context_module_setting, request, data): diff --git a/askbot/views/moderation.py b/askbot/views/moderation.py new file mode 100644 index 00000000..eb68d111 --- /dev/null +++ b/askbot/views/moderation.py @@ -0,0 +1,225 @@ +from askbot.utils import decorators +from askbot import const +from askbot import models +from askbot import mail +from datetime import datetime +from django.utils.translation import string_concat +from django.utils.translation import ungettext +from django.utils.translation import ugettext as _ +from django.template.loader import get_template +from django.conf import settings as django_settings +from django.contrib.contenttypes.models import ContentType +from django.db.models import Q +from django.utils.encoding import force_text +from django.template import RequestContext +from django.views.decorators import csrf +from django.utils.encoding import force_text +from django.core import exceptions +from django.utils import simplejson + +EDIT_ACTIVITY_TYPES = ( + const.TYPE_ACTIVITY_MODERATED_NEW_POST, + const.TYPE_ACTIVITY_MODERATED_POST_EDIT +) +MOD_ACTIVITY_TYPES = EDIT_ACTIVITY_TYPES + (const.TYPE_ACTIVITY_MARK_OFFENSIVE,) + +#some utility functions +def get_object(memo): + content_object = memo.activity.content_object + if isinstance(content_object, models.PostRevision): + return content_object.post + else: + return content_object + + +def get_editors(memo_set): + """returns editors corresponding to the memo set + some memos won't yeild editors - if the related object + is post and it has > 1 editor (in which case we don't know + who was the editor that we want to block!!! + this applies to flagged posts. + + todo: an inconvenience is that "offensive flags" are stored + differently in the Activity vs. "new moderated posts" or "post edits" + """ + editors = set() + for memo in memo_set: + obj = memo.activity.content_object + if isinstance(obj, models.PostRevision): + editors.add(obj.author) + elif isinstance(obj, models.Post): + rev_authors = set() + for rev in obj.revisions.all(): + rev_authors.add(rev.author) + + #if we have > 1 author we skip, b/c don't know + #which user we want to block + if len(rev_authors) == 1: + editors.update(rev_authors) + return editors + +def filter_admins(users): + filtered = set() + for user in users: + if not user.is_administrator_or_moderator(): + filtered.add(user) + return filtered + + +def concat_messages(message1, message2): + if message1: + message = string_concat(message1, ', ') + return string_concat(message, message2) + else: + return message2 + + +@csrf.csrf_exempt +@decorators.post_only +@decorators.ajax_only +def moderate_post_edits(request): + if request.user.is_anonymous(): + raise exceptions.PermissionDenied() + if not request.user.is_administrator_or_moderator(): + raise exceptions.PermissionDenied() + + post_data = simplejson.loads(request.raw_post_data) + #{'action': 'decline-with-reason', 'items': ['posts'], 'reason': 1, 'edit_ids': [827]} + + memo_set = models.ActivityAuditStatus.objects.filter(id__in=post_data['edit_ids']) + result = { + 'message': '', + 'memo_ids': set() + } + + #if we are approving or declining users we need to expand the memo_set + #to all of their edits of those users + if post_data['action'] in ('block', 'approve') and 'users' in post_data['items']: + editors = filter_admins(get_editors(memo_set)) + items = models.Activity.objects.filter( + activity_type__in=EDIT_ACTIVITY_TYPES, + user__in=editors + ) + memo_filter = Q(id__in=post_data['edit_ids']) | Q(user=request.user, activity__in=items) + memo_set = models.ActivityAuditStatus.objects.filter(memo_filter) + + memo_set.select_related('activity') + + if post_data['action'] == 'decline-with-reason': + #todo: bunch notifications - one per recipient + num_posts = 0 + for memo in memo_set: + post = get_object(memo) + request.user.delete_post(post) + reject_reason = models.PostFlagReason.objects.get(id=post_data['reason']) + template = get_template('email/rejected_post.html') + data = { + 'post': post.html, + 'reject_reason': reject_reason.details.html + } + body_text = template.render(RequestContext(request, data)) + mail.send_mail( + subject_line = _('your post was not accepted'), + body_text = unicode(body_text), + recipient_list = [post.author.email,] + ) + num_posts += 1 + + #message to moderator + if num_posts: + posts_message = ungettext('%d post deleted', '%d posts deleted', num_posts) % num_posts + result['message'] = concat_messages(result['message'], posts_message) + + elif post_data['action'] == 'approve': + num_posts = 0 + if 'posts' in post_data['items']: + for memo in memo_set: + if memo.activity.activity_type == const.TYPE_ACTIVITY_MARK_OFFENSIVE: + #unflag the post + content_object = memo.activity.content_object + request.user.flag_post(content_object, cancel_all=True, force=True) + num_posts += 1 + else: + revision = memo.activity.content_object + if isinstance(revision, models.PostRevision): + request.user.approve_post_revision(revision) + num_posts += 1 + + if num_posts > 0: + posts_message = ungettext('%d post approved', '%d posts approved', num_posts) % num_posts + result['message'] = concat_messages(result['message'], posts_message) + + if 'users' in post_data['items']: + editors = filter_admins(get_editors(memo_set)) + assert(request.user not in editors) + for editor in editors: + editor.set_status('a') + + num_editors = len(editors) + if num_editors: + users_message = ungettext('%d user approved', '%d users approved', num_editors) % num_editors + result['message'] = concat_messages(result['message'], users_message) + + elif post_data['action'] == 'block': + if 'users' in post_data['items']: + editors = filter_admins(get_editors(memo_set)) + assert(request.user not in editors) + num_posts = 0 + for editor in editors: + #block user + editor.set_status('b') + #delete all content by the user + num_posts += request.user.delete_all_content_authored_by_user(editor) + + if num_posts: + posts_message = ungettext('%d post deleted', '%d posts deleted', num_posts) % num_posts + result['message'] = concat_messages(result['message'], posts_message) + + num_editors = len(editors) + if num_editors: + users_message = ungettext('%d user blocked', '%d users blocked', num_editors) % num_editors + result['message'] = concat_messages(result['message'], users_message) + + moderate_ips = getattr(django_settings, 'ASKBOT_IP_MODERATION_ENABLED', False) + if moderate_ips and 'ips' in post_data['items']: + ips = set() + for memo in memo_set: + obj = memo.activity.content_object + if isinstance(obj, models.PostRevision): + ips.add(obj.ip_addr) + + #to make sure to not block the admin and + #in case REMOTE_ADDR is a proxy server - not + #block access to the site + my_ip = request.META.get('REMOTE_ADDR') + if my_ip in ips: + ips.remove(my_ip) + + from stopforumspam.models import Cache + already_blocked = Cache.objects.filter(ip__in=ips) + already_blocked.update(permanent=True) + already_blocked_ips = already_blocked.values_list('ip', flat=True) + ips = ips - set(already_blocked_ips) + for ip in ips: + cache = Cache(ip=ip, permanent=True) + cache.save() + + num_ips = len(ips) + if num_ips: + ips_message = ungettext('%d ip blocked', '%d ips blocked', num_ips) % num_ips + result['message'] = concat_messages(result['message'], ips_message) + + result['memo_ids'] = [memo.id for memo in memo_set]#why values_list() fails here? + result['message'] = force_text(result['message']) + + #delete items from the moderation queue + act_ids = memo_set.values_list('activity_id', flat=True) + acts = models.Activity.objects.filter( + id__in=act_ids, + activity_type__in=MOD_ACTIVITY_TYPES + ) + memo_set.delete() + acts.delete() + request.user.update_response_counts() + result['memo_count'] = request.user.get_notifications(MOD_ACTIVITY_TYPES).count() + return result diff --git a/askbot/views/readers.py b/askbot/views/readers.py index 80f1d2ec..79db9a12 100644 --- a/askbot/views/readers.py +++ b/askbot/views/readers.py @@ -382,6 +382,8 @@ def tags(request):#view showing a listing of available tags - plain list def question(request, id):#refactor - long subroutine. display question body, answers and comments """view that displays body of the question and all answers to it + + todo: convert this view into class """ #process url parameters #todo: fix inheritance of sort method from questions @@ -506,7 +508,7 @@ def question(request, id):#refactor - long subroutine. display question body, an #load answers and post id's->athor_id mapping #posts are pre-stuffed with the correctly ordered comments - updated_question_post, answers, post_to_author, published_answer_ids = thread.get_cached_post_data( + updated_question_post, answers, post_to_author, published_answer_ids = thread.get_post_data_for_question_view( sort_method=answer_sort_method, user=request.user ) @@ -514,7 +516,6 @@ def question(request, id):#refactor - long subroutine. display question body, an updated_question_post.get_cached_comments() ) - #Post.objects.precache_comments(for_posts=[question_post] + answers, visitor=request.user) user_votes = {} @@ -529,7 +530,7 @@ def question(request, id):#refactor - long subroutine. display question body, an #we can avoid making this query by iterating through #already loaded posts user_post_id_list = [ - id for id in post_to_author if post_to_author[id] == request.user.id + post_id for post_id in post_to_author if post_to_author[post_id] == request.user.id ] #resolve page number and comment number for permalinks diff --git a/askbot/views/users.py b/askbot/views/users.py index 91f210d9..c80ff4ec 100644 --- a/askbot/views/users.py +++ b/askbot/views/users.py @@ -27,7 +27,9 @@ from django.shortcuts import render from django.http import HttpResponse, HttpResponseForbidden from django.http import HttpResponseRedirect, Http404 from django.utils.translation import get_language +from django.utils.translation import string_concat from django.utils.translation import ugettext as _ +from django.utils.translation import ungettext from django.utils import simplejson from django.utils.html import strip_tags as strip_all_tags from django.views.decorators import csrf @@ -36,6 +38,7 @@ from askbot.utils.slug import slugify from askbot.utils.html import sanitize_html from askbot.mail import send_mail from askbot.utils.http import get_request_info +from askbot.utils import decorators from askbot.utils import functions from askbot import forms from askbot import const @@ -63,6 +66,23 @@ def owner_or_moderator_required(f): return f(request, profile_owner, context) return wrapped_func +@decorators.ajax_only +def clear_new_notifications(request): + """clears all new notifications for logged in user""" + user = request.user + if user.is_anonymous(): + raise django_exceptions.PermissionDenied + + activity_types = const.RESPONSE_ACTIVITY_TYPES_FOR_DISPLAY + activity_types += ( + const.TYPE_ACTIVITY_MENTION, + ) + memo_set = models.ActivityAuditStatus.objects.filter( + activity__activity_type__in=activity_types, + user=user + ) + memo_set.update(status = models.ActivityAuditStatus.STATUS_SEEN) + user.update_response_counts() def show_users(request, by_group=False, group_id=None, group_slug=None): """Users view, including listing of users by group""" @@ -88,11 +108,10 @@ def show_users(request, by_group=False, group_id=None, group_slug=None): else: try: group = models.Group.objects.get(id = group_id) - group_email_moderation_enabled = \ - ( - askbot_settings.GROUP_EMAIL_ADDRESSES_ENABLED \ - and askbot_settings.ENABLE_CONTENT_MODERATION - ) + group_email_moderation_enabled = ( + askbot_settings.GROUP_EMAIL_ADDRESSES_ENABLED \ + and askbot_settings.CONTENT_MODERATION_MODE == 'premoderation' + ) user_acceptance_level = group.get_openness_level_for_user( request.user ) @@ -214,13 +233,14 @@ def user_moderate(request, subject, context): user_rep_changed = False user_status_changed = False + user_status_changed_message = _('User status changed') message_sent = False email_error_message = None user_rep_form = forms.ChangeUserReputationForm() send_message_form = forms.SendMessageForm() if request.method == 'POST': - if 'change_status' in request.POST: + if 'change_status' in request.POST or 'hard_block' in request.POST: user_status_form = forms.ChangeUserStatusForm( request.POST, moderator = moderator, @@ -228,6 +248,11 @@ def user_moderate(request, subject, context): ) if user_status_form.is_valid(): subject.set_status( user_status_form.cleaned_data['user_status'] ) + if user_status_form.cleaned_data['delete_content'] == True: + num_deleted = request.user.delete_all_content_authored_by_user(subject) + if num_deleted: + num_deleted_message = ungettext('%d post deleted', '%d posts deleted', num_deleted) % num_deleted + user_status_changed_message = string_concat(user_status_changed_message, ', ', num_deleted_message) user_status_changed = True elif 'send_message' in request.POST: send_message_form = forms.SendMessageForm(request.POST) @@ -291,7 +316,8 @@ def user_moderate(request, subject, context): 'message_sent': message_sent, 'email_error_message': email_error_message, 'user_rep_changed': user_rep_changed, - 'user_status_changed': user_status_changed + 'user_status_changed': user_status_changed, + 'user_status_changed_message': user_status_changed_message } context.update(data) return render(request, 'user_profile/user_moderate.html', context) @@ -387,7 +413,7 @@ def user_stats(request, user, context): if request.user != user: question_filter['is_anonymous'] = False - if askbot_settings.ENABLE_CONTENT_MODERATION: + if askbot_settings.CONTENT_MODERATION_MODE == 'premoderation': question_filter['approved'] = True # @@ -533,7 +559,6 @@ def user_stats(request, user, context): 'support_custom_avatars': ('avatar' in django_settings.INSTALLED_APPS), 'tab_name' : 'stats', 'page_title' : _('user profile overview'), - 'user_status_for_display': user.get_status_display(soft = True), 'questions' : questions, 'question_count': question_count, 'q_paginator_context': q_paginator_context, @@ -722,7 +747,7 @@ def user_responses(request, user, context): activity_types += (const.TYPE_ACTIVITY_MENTION,) elif section == 'flags': activity_types = (const.TYPE_ACTIVITY_MARK_OFFENSIVE,) - if askbot_settings.ENABLE_CONTENT_MODERATION: + if askbot_settings.CONTENT_MODERATION_MODE in ('premoderation', 'audit'): activity_types += ( const.TYPE_ACTIVITY_MODERATED_NEW_POST, const.TYPE_ACTIVITY_MODERATED_POST_EDIT @@ -779,41 +804,62 @@ def user_responses(request, user, context): #3) "package" data for the output response_list = list() for memo in memo_set: - if memo.activity.content_object is None: + obj = memo.activity.content_object + if obj is None: + memo.activity.delete() continue#a temp plug due to bug in the comment deletion + + act = memo.activity + ip_addr = None + if act.activity_type == const.TYPE_ACTIVITY_MARK_OFFENSIVE: + #todo: two issues here - flags are stored differently + #from activity of new posts and edits + #second issue: on posts with many edits we don't know whom to block + act_user = act.content_object.author + act_message = _('post was flagged as offensive') + act_type = 'flag' + else: + act_user = act.user + act_message = act.get_activity_type_display() + act_type = 'edit' + if section == 'flags': + ip_addr = act.content_object.ip_addr + response = { 'id': memo.id, - 'timestamp': memo.activity.active_at, - 'user': memo.activity.user, + 'timestamp': act.active_at, + 'user': act_user, + 'ip_addr': ip_addr, 'is_new': memo.is_new(), - 'response_url': memo.activity.get_absolute_url(), - 'response_snippet': memo.activity.get_snippet(), - 'response_title': memo.activity.question.thread.title, - 'response_type': memo.activity.get_activity_type_display(), - 'response_id': memo.activity.question.id, - 'nested_responses': [], - 'response_content': memo.activity.content_object.html, + 'url': act.get_absolute_url(), + 'snippet': act.get_snippet(), + 'title': act.question.thread.title, + 'message_type': act_message, + 'memo_type': act_type, + 'question_id': act.question.id, + 'followup_messages': list(), + 'content': obj.html or obj.text, } response_list.append(response) #4) sort by response id - response_list.sort(lambda x,y: cmp(y['response_id'], x['response_id'])) + response_list.sort(lambda x,y: cmp(y['question_id'], x['question_id'])) #5) group responses by thread (response_id is really the question post id) - last_response_id = None #flag to know if the response id is different - filtered_response_list = list() - for i, response in enumerate(response_list): + last_question_id = None #flag to know if the question id is different + filtered_message_list = list() + for message in response_list: #todo: group responses by the user as well - if response['response_id'] == last_response_id: - original_response = dict.copy(filtered_response_list[len(filtered_response_list)-1]) - original_response['nested_responses'].append(response) - filtered_response_list[len(filtered_response_list)-1] = original_response + if message['question_id'] == last_question_id: + original_message = dict.copy(filtered_message_list[-1]) + original_message['followup_messages'].append(message) + filtered_message_list[-1] = original_message else: - filtered_response_list.append(response) - last_response_id = response['response_id'] + filtered_message_list.append(message) + last_question_id = message['question_id'] #6) sort responses by time - filtered_response_list.sort(lambda x,y: cmp(y['timestamp'], x['timestamp'])) + filtered_message_list.sort(lambda x,y: cmp(y['timestamp'], x['timestamp'])) reject_reasons = models.PostFlagReason.objects.all().order_by('title') data = { @@ -823,10 +869,14 @@ def user_responses(request, user, context): 'inbox_section': section, 'page_title' : _('profile - responses'), 'post_reject_reasons': reject_reasons, - 'responses' : filtered_response_list, + 'messages' : filtered_message_list, } context.update(data) - return render(request, 'user_inbox/responses_and_flags.html', context) + if section == 'flags': + template = 'moderation/queue.html' + else: + template = 'user_inbox/responses.html' + return render(request, template, context) def user_network(request, user, context): if 'followit' not in django_settings.INSTALLED_APPS: diff --git a/askbot/views/writers.py b/askbot/views/writers.py index a11fb941..55c11ee9 100644 --- a/askbot/views/writers.py +++ b/askbot/views/writers.py @@ -259,7 +259,8 @@ def ask(request):#view used to ask a new question is_private=post_privately, timestamp=timestamp, group_id=group_id, - language=language + language=language, + ip_addr=request.META.get('REMOTE_ADDR') ) signals.new_question_posted.send(None, question=question, @@ -282,7 +283,7 @@ def ask(request):#view used to ask a new question is_anonymous = ask_anonymously, text = text, added_at = timestamp, - ip_addr = request.META['REMOTE_ADDR'], + ip_addr = request.META.get('REMOTE_ADDR'), ) return HttpResponseRedirect(url_utils.get_login_url()) @@ -457,7 +458,8 @@ def edit_question(request, id): wiki = is_wiki, edit_anonymously = is_anon_edit, is_private = post_privately, - suppress_email=suppress_email + suppress_email=suppress_email, + ip_addr=request.META.get('REMOTE_ADDR') ) if 'language' in form.cleaned_data: @@ -556,7 +558,8 @@ def edit_answer(request, id): revision_comment=form.cleaned_data['summary'], wiki=form.cleaned_data.get('wiki', answer.wiki), is_private=is_private, - suppress_email=suppress_email + suppress_email=suppress_email, + ip_addr=request.META.get('REMOTE_ADDR') ) signals.answer_edited.send(None, @@ -631,7 +634,11 @@ def answer(request, id, form_class=forms.AnswerForm):#process a new answer drafts.delete() user = form.get_post_user(request.user) try: - answer = form.save(question, user) + answer = form.save( + question, + user, + ip_addr=request.META.get('REMOTE_ADDR') + ) signals.new_answer_posted.send(None, answer=answer, @@ -653,7 +660,7 @@ def answer(request, id, form_class=forms.AnswerForm):#process a new answer wiki=form.cleaned_data['wiki'], text=form.cleaned_data['text'], session_key=request.session.session_key, - ip_addr=request.META['REMOTE_ADDR'], + ip_addr=request.META.get('REMOTE_ADDR'), ) return HttpResponseRedirect(url_utils.get_login_url()) @@ -752,7 +759,9 @@ def post_comments(request):#generic ajax handler to load comments to an object raise exceptions.PermissionDenied(askbot_settings.READ_ONLY_MESSAGE) comment = user.post_comment( - parent_post=post, body_text=form.cleaned_data['comment'] + parent_post=post, + body_text=form.cleaned_data['comment'], + ip_addr=request.META.get('REMOTE_ADDR') ) signals.new_comment_posted.send(None, comment=comment, @@ -787,7 +796,8 @@ def edit_comment(request): request.user.edit_comment( comment_post=comment_post, body_text=form.cleaned_data['comment'], - suppress_email=form.cleaned_data['suppress_email'] + suppress_email=form.cleaned_data['suppress_email'], + ip_addr=request.META.get('REMOTE_ADDR'), ) is_deletable = template_filters.can_delete_comment( |