summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEvgeny Fadeev <evgeny.fadeev@gmail.com>2014-07-08 06:38:09 -0300
committerEvgeny Fadeev <evgeny.fadeev@gmail.com>2014-07-08 06:38:09 -0300
commit85a833860e8915474abbbcb888ab99a1c2300e2c (patch)
tree9be897e77d2af2893aa0f8cf61a806a10bfeb348
parent7c72acc70ce7e7b399c58fd30d4f9d082c5cd2f2 (diff)
downloadaskbot-85a833860e8915474abbbcb888ab99a1c2300e2c.tar.gz
askbot-85a833860e8915474abbbcb888ab99a1c2300e2c.tar.bz2
askbot-85a833860e8915474abbbcb888ab99a1c2300e2c.zip
better post moderation works on first pass
-rw-r--r--askbot/__init__.py1
-rw-r--r--askbot/conf/moderation.py27
-rw-r--r--askbot/context.py1
-rw-r--r--askbot/management/commands/send_email_alerts.py2
-rw-r--r--askbot/media/js/post.js20
-rw-r--r--askbot/media/js/user.js336
-rw-r--r--askbot/media/style/style.css43
-rw-r--r--askbot/media/style/style.less47
-rw-r--r--askbot/models/__init__.py44
-rw-r--r--askbot/models/post.py70
-rw-r--r--askbot/models/question.py2
-rw-r--r--askbot/models/reply_by_email.py2
-rw-r--r--askbot/sitemap.py5
-rw-r--r--askbot/templates/macros.html32
-rw-r--r--askbot/templates/moderation/queue.html56
-rw-r--r--askbot/templates/moderation/reject_post_dialog.html (renamed from askbot/templates/user_profile/reject_post_dialog.html)2
-rw-r--r--askbot/templates/user_inbox/base.html57
-rw-r--r--askbot/templates/user_inbox/responses.html23
-rw-r--r--askbot/templates/user_inbox/responses_and_flags.html41
-rw-r--r--askbot/tests/email_alert_tests.py10
-rw-r--r--askbot/tests/page_load_tests.py2
-rw-r--r--askbot/urls.py5
-rw-r--r--askbot/views/__init__.py1
-rw-r--r--askbot/views/commands.py5
-rw-r--r--askbot/views/context.py20
-rw-r--r--askbot/views/moderation.py180
-rw-r--r--askbot/views/users.py57
27 files changed, 765 insertions, 326 deletions
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..f0e6d681 100644
--- a/askbot/conf/moderation.py
+++ b/askbot/conf/moderation.py
@@ -4,6 +4,7 @@ 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 _
@@ -17,16 +18,24 @@ def empty_cache_callback(old_value, 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'),
+ update_callback=empty_cache_callback,
+ help_text=_("Audit is made after the posts are published, pre-moderation prevents publishing before moderator's decision.")
)
)
@@ -34,9 +43,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/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/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..90ca2a7b 100644
--- a/askbot/media/js/user.js
+++ b/askbot/media/js/user.js
@@ -1,4 +1,5 @@
//todo: refactor this into "Inbox" object or more specialized
+/*
var setup_inbox = function(){
var getSelected = function(){
@@ -113,20 +114,6 @@ var setup_inbox = function(){
}
);
- var rejectPostDialog = new RejectPostDialog();
- rejectPostDialog.decorate($('#reject-edit-modal'));
- rejectPostDialog.setSelectedEditDataReader(function(){
- return getSelected();
- });
- setupButtonEventHandlers(
- $('#re_delete_post'),
- function(){
- if (rejectPostDialog.readSelectedEditData()) {
- rejectPostDialog.show();
- }
- }
- );
-
if ($('body').hasClass('inbox-flags')) {
var responses = $('.response-parent');
responses.each(function(idx, response) {
@@ -138,6 +125,14 @@ var setup_inbox = function(){
});
}
};
+*/
+var setup_inbox = function(){
+ var page = $('.inbox-flags');
+ if (page.length) {
+ var modControls = new PostModerationControls();
+ modControls.decorate(page);
+ }
+};
var setup_badge_details_toggle = function(){
$('.badge-context-toggle').each(function(idx, elem){
@@ -157,52 +152,34 @@ var setup_badge_details_toggle = function(){
});
};
-var PostModerationControls = function() {
+/**
+* response notifications
+*/
+var Inbox = function() {
WrappedElement.call(this);
};
-inherits(PostModerationControls, WrappedElement);
-
-PostModerationControls.prototype.setParent = function(parent_element) {
- this._parent_element = parent_element;
-};
+inherits(Inbox, WrappedElement);
-PostModerationControls.prototype.setReasonsDialog = function(dialog) {
- this._reasonsDialog = dialog;
-};
-
-PostModerationControls.prototype.getMemoId = function() {
+Inbox.prototype.getMemoId = function() {
return this._parent_element.data('responseId');
};
-PostModerationControls.prototype.getMemoElement = function() {
+Inbox.prototype.getMemoElement = function() {
var reId = this.getMemoId();
return $('#re_' + reId);
};
-PostModerationControls.prototype.removeMemo = function() {
+Inbox.prototype.removeMemo = function() {
this.getMemoElement().remove();
};
-PostModerationControls.prototype.markMemo = function() {
+Inbox.prototype.markMemo = function() {
var memo = this.getMemoElement();
var checkbox = memo.find('input[type="checkbox"]');
checkbox.attr('checked', true);
};
-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') });
-};
-
-PostModerationControls.prototype.moderatePost = function(reasonId, actionType){
+Inbox.prototype.moderatePost = function(reasonId, actionType){
var me = this;
var data = {
reject_reason_id: reasonId,
@@ -232,68 +209,205 @@ PostModerationControls.prototype.moderatePost = function(reasonId, actionType){
};
-PostModerationControls.prototype.createDom = function() {
- var toolbar = this.makeElement('div');
- toolbar.addClass('btn-toolbar post-moderation-controls');
- this._element = toolbar;
+/**
+* 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);
+
+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);
+};
+
+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);
+ this._addReasonBtn.parent().before(li);
- var div = this.makeElement('div');
- div.addClass('btn-group');
- toolbar.append(div);
+ this.setupDeclinePostHandler(button);
+};
- var acceptBtn = this.makeElement('a');
- acceptBtn.addClass('btn save-reason');
- acceptBtn.html(gettext('Accept'));
- div.append(acceptBtn);
+DeclineAndExplainMenu.prototype.setControls = function(controls) {
+ this._controls = controls;
+};
- div = this.makeElement('div');
- div.addClass('btn-group dropdown');
- toolbar.append(div);
+DeclineAndExplainMenu.prototype.getControls = function() {
+ return this._controls;
+};
- 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);
+DeclineAndExplainMenu.prototype.decorate = function(element) {
+ this._element = element;
+ //activate dropdown menu
+ element.dropdown();
- toggle.dropdown();
+ var declineBtns = element.find('.decline-with-reason');
+ var me = this;
+ declineBtns.each(function(idx, elem) {
+ me.setupDeclinePostHandler($(elem));
+ });
- var ul = this.makeElement('ul');
- ul.addClass('dropdown-menu');
- div.append(ul);
+ this._reasonList = element.find('ul');
- this._reasonList = ul;
+ var addReasonBtn = element.find('.manage-reasons');
+ this._addReasonBtn = addReasonBtn;
- //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 manageReasonsDialog = new ManageRejectReasonsDialog();
+ manageReasonsDialog.decorate($('#manage-reject-reasons-modal'));
+ this._manageReasonsDialog = manageReasonsDialog;
+ manageReasonsDialog.setMenu(this);
+};
+
+/**
+* Buttons to moderate posts
+* and the list of edits
+*/
+var PostModerationControls = function() {
+ WrappedElement.call(this);
+};
+inherits(PostModerationControls, WrappedElement);
+
+/**
+* displays feedback message
+*/
+PostModerationControls.prototype.showMessage = function(message) {
+ this._notification.html(message);
+ this._notification.parent().fadeIn('fast');
+};
- //append menu items
+PostModerationControls.prototype.hideMessage = function() {
+ this._notification.parent().hide();
+};
+
+/**
+* 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');
+ }
+ }
+};
+
+PostModerationControls.prototype.getCheckBoxes = function() {
+ return this._element.find('.messages input[type="checkbox"]');
+};
+
+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;
+};
+
+/**
+* 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;
- $.each(askbot['data']['postRejectReasons'], function(idx, reason) {
- me.addReason(reason['id'], reason['title']);
- });
+ 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(selectedEditIds);
+ }
+ if (response_data['message']) {
+ me.showMessage(response_data['message']);
+ }
+ }
+ });
+ };
+};
- 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.getSelectAllHandler = function(selected) {
+ var me = this;
+ return function() {
+ var cb = me.getCheckBoxes();
+ cb.prop('checked', selected);
+ };
};
+PostModerationControls.prototype.decorate = function(element) {
+ this._element = element;
+ this._notification = element.find('.action-status span');
+ this.hideMessage();
+ //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;
@@ -301,27 +415,31 @@ var RejectPostDialog = function(){
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.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){
@@ -338,15 +456,15 @@ RejectPostDialog.prototype.setState = function(state){
}
};
-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 +475,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 +511,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 +520,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 +528,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 +537,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 +551,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;
@@ -483,7 +601,7 @@ RejectPostDialog.prototype.startSavingReason = function(callback){
});
};
-RejectPostDialog.prototype.rejectPost = function(reason_id){
+ManageRejectReasonsDialog.prototype.rejectPost = function(reason_id){
var me = this;
var memos = this._selected_edit_data['elements'];
var memo_ids = this._selected_edit_data['id_list'];
@@ -513,13 +631,13 @@ RejectPostDialog.prototype.rejectPost = function(reason_id){
});
};
-RejectPostDialog.prototype.setPreviewerData = function(data){
+ManageRejectReasonsDialog.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(){
+ManageRejectReasonsDialog.prototype.startEditingReason = function(){
var title = this._element.find('.selected-reason-title').html();
var details = this._element.find('.selected-reason-details').html();
this._title_input.setVal(title);
@@ -527,15 +645,15 @@ RejectPostDialog.prototype.startEditingReason = function(){
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'];
@@ -562,7 +680,7 @@ RejectPostDialog.prototype.startDeletingReason = function(){
}
};
-RejectPostDialog.prototype.decorate = function(element){
+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');
diff --git a/askbot/media/style/style.css b/askbot/media/style/style.css
index 33df2b0a..7d1cd135 100644
--- a/askbot/media/style/style.css
+++ b/askbot/media/style/style.css
@@ -2871,17 +2871,49 @@ 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 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;
@@ -3699,6 +3731,10 @@ button::-moz-focus-inner {
font-size: 12px;
padding: 0;
}
+.inbox-flags .action-status {
+ line-height: 38px;
+ height: 24px;
+}
.action-status span {
padding: 3px 5px 3px 5px;
background-color: #fff380;
@@ -3861,6 +3897,7 @@ p.signup_p {
margin-bottom: 15px;
}
#responses h2 {
+ line-height: 24px;
margin: 0;
padding: 0;
}
diff --git a/askbot/media/style/style.less b/askbot/media/style/style.less
index f4ac2dfd..55f237ae 100644
--- a/askbot/media/style/style.less
+++ b/askbot/media/style/style.less
@@ -2993,7 +2993,28 @@ 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;
+ }
+}
+
+.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 +3024,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 {
@@ -3929,6 +3968,11 @@ button::-moz-focus-inner {
padding: 0;
}
+.inbox-flags .action-status {
+ line-height: 38px;
+ height: 24px;
+}
+
.action-status span {
padding: 3px 5px 3px 5px;
background-color: #fff380; /* nice yellow */
@@ -4122,6 +4166,7 @@ p.signup_p {
line-height:18px;
margin-bottom:15px;
h2 {
+ line-height: 24px;
margin: 0;
padding: 0;
}
diff --git a/askbot/models/__init__.py b/askbot/models/__init__.py
index 5eb79c2f..c80c57b5 100644
--- a/askbot/models/__init__.py
+++ b/askbot/models/__init__.py
@@ -558,9 +558,9 @@ def get_or_create_anonymous_user():
return user
def user_needs_moderation(self):
- """True, if user needs moderation"""
- if askbot_settings.ENABLE_CONTENT_MODERATION:
- return not (self.is_administrator_or_moderator() or self.is_approved())
+ 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(
@@ -2720,24 +2720,34 @@ def user_approve_post_revision(user, post_revision, timestamp = None):
post = post_revision.post
- assert(post_revision.revision == 0)
- post_revision.revision = post.get_latest_revision_number() + 1
+ #approval of unpublished revision
+ if post_revision.revision == 0:
+ post_revision.revision = post.get_latest_revision_number() + 1
- post_revision.save()
+ post_revision.save()
- post.approved = True
- post.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()
- if post_revision.post.post_type == 'question':
- thread = post.thread
- thread.approved = True
- thread.save()
- post.thread.invalidate_cached_data()
+ post.approved = True
+ post.save()
- #send the signal of published revision
- signals.post_revision_published.send(
- None, revision = post_revision, was_approved = True
- )
+ 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(
diff --git a/askbot/models/post.py b/askbot/models/post.py
index f1178473..94b96370 100644
--- a/askbot/models/post.py
+++ b/askbot/models/post.py
@@ -235,6 +235,14 @@ 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
+ )
+
from askbot.models import signals
signals.post_updated.send(
post=post,
@@ -246,13 +254,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
@@ -288,9 +289,10 @@ 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(last_activity_at=added_at, last_activity_by=author) # this should be here because it regenerates cached thread summary html
return answer
@@ -794,13 +796,16 @@ class Post(models.Model):
"""``False`` only when moderation is ``True`` and post
``self.approved is False``
"""
- if askbot_settings.ENABLE_CONTENT_MODERATION:
- return self.approved
+ 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
@@ -1052,8 +1057,9 @@ class Post(models.Model):
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
@@ -2110,16 +2116,22 @@ class PostRevisionManager(models.Manager):
author = kwargs['author']
moderate_email = False
- if kwargs.get('email') and kwargs.get('email'):
+ if kwargs.get('email'):
from askbot.models.reply_by_email import emailed_content_needs_moderation
moderate_email = emailed_content_needs_moderation(kwargs['email'])
- #in the moderate_or_publish() we determine the revision number
- #0 revision belongs to the moderation queue
- if author.needs_moderation() or moderate_email:
- kwargs['revision'] = 0
+ 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)
- revision.place_on_moderation_queue()
else:
post = kwargs['post']
kwargs['revision'] = post.get_latest_revision_number() + 1
@@ -2136,6 +2148,10 @@ class PostRevisionManager(models.Manager):
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):
@@ -2194,16 +2210,6 @@ class PostRevision(models.Model):
This allows us to moderate every revision
"""
-
- #moderated revisions have number 0
- self.revision = 0
- self.approved = False #todo: we probably don't need this field any more
- self.approved_by = None
- self.approved_at = None
- if self.summary == '':
- self.summary = _('Suggested edit')
- self.save()
-
#this is run on "post-save" so for a new post
#we'll have just one revision
if self.post.revisions.count() == 1:
@@ -2243,6 +2249,7 @@ class PostRevision(models.Model):
#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,
@@ -2251,7 +2258,6 @@ class PostRevision(models.Model):
question = self.get_origin_post()
)
activity.save()
-
activity.add_recipients(self.post.get_moderators())
def should_notify_author_about_publishing(self, was_approved = False):
diff --git a/askbot/models/question.py b/askbot/models/question.py
index 17899285..8336ac5d 100644
--- a/askbot/models/question.py
+++ b/askbot/models/question.py
@@ -289,7 +289,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
diff --git a/askbot/models/reply_by_email.py b/askbot/models/reply_by_email.py
index 5e36a347..309743cb 100644
--- a/askbot/models/reply_by_email.py
+++ b/askbot/models/reply_by_email.py
@@ -15,7 +15,7 @@ def emailed_content_needs_moderation(email):
is marked for moderation
todo: maybe this belongs to a separate "moderation" module
"""
- if askbot_settings.ENABLE_CONTENT_MODERATION:
+ if askbot_settings.CONTENT_MODERATION_MODE == 'premoderation':
group_name = email.split('@')[0]
from askbot.models.user import Group
try:
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/macros.html b/askbot/templates/macros.html
index b314255c..b6f64401 100644
--- a/askbot/templates/macros.html
+++ b/askbot/templates/macros.html
@@ -6,31 +6,15 @@
>{% 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 -%}
diff --git a/askbot/templates/moderation/queue.html b/askbot/templates/moderation/queue.html
new file mode 100644
index 00000000..e204b86d
--- /dev/null
+++ b/askbot/templates/moderation/queue.html
@@ -0,0 +1,56 @@
+{% 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 %}delete posts and block users{% endtrans %}</a>
+ {% if settings.IP_MODERATION_ENABLED %}
+ <a class="btn btn-danger decline-block-users-ips">{% trans %}delete posts, block users and IPs{% endtrans %}</a>
+ {% endif %}
+ </div>
+ <p style="margin-top: 12px;">Attention: approval of users removes them from the queue and approves ALL of their posts, blocking of the users
+ DELETES ALL OF THEIR POSTS. There is no easy undo at the moment.</p>
+ {% include "moderation/reject_post_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.inbox_message_snippet(message) }}
+ {# "nested" messages are further response messages to the same question #}
+ {% for followup_message in message.followup_messages %}
+ {{ macros.inbox_message_snippet(followup_message) }}
+ {% endfor %}
+ </div>
+ <div class="clearfix"></div>
+ {% endfor %}
+ </div>
+{% endblock %}
diff --git a/askbot/templates/user_profile/reject_post_dialog.html b/askbot/templates/moderation/reject_post_dialog.html
index 3483e83e..24c75769 100644
--- a/askbot/templates/user_profile/reject_post_dialog.html
+++ b/askbot/templates/moderation/reject_post_dialog.html
@@ -1,4 +1,4 @@
-<div class="modal" style="display:none" id="reject-edit-modal">
+<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 %}Reject the post(s)?{% endtrans %}</h3>
diff --git a/askbot/templates/user_inbox/base.html b/askbot/templates/user_inbox/base.html
index db657b23..0bfbf5e4 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&section=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&section=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&section=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&section=forum"
+ {% if inbox_section == 'forum' %}class="on"{% endif %}
+ >
+ {% trans %}forum responses ({{re_count}}){% endtrans -%}
+ </a>
+ {% endif %}
+ {% if flags_count > 0 %}{{ sep() }}
+ <a href="{{request.user.get_absolute_url()}}?sort=inbox&section=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&section=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&section=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&section=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,7 @@
var askbot = askbot || {};
askbot['urls'] = askbot['urls'] || {};
askbot['urls']['manageInbox'] = '{% url manage_inbox %}';
+ 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..57f4de00
--- /dev/null
+++ b/askbot/templates/user_inbox/responses.html
@@ -0,0 +1,23 @@
+{% extends "user_inbox/base.html" %}
+{% import "macros.html" as macros %}
+{% block profilesection %}
+ {% trans %}inbox - moderation queue{% endtrans %}
+{% endblock %}
+{% block inbox_content %}
+ <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) }}
+ {# "nested" messages are further response messages to the same question #}
+ {% for followup_message in message.followup_messages %}
+ {{ macros.inbox_message_snippet(followup_message) }}
+ {% endfor %}
+ </div>
+ <div class="clearfix"></div>
+ {% 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/tests/email_alert_tests.py b/askbot/tests/email_alert_tests.py
index ecd1dc54..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',
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/urls.py b/askbot/urls.py
index 799ef0f5..85f9675f 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'
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 934f5479..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
@@ -52,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..94561348
--- /dev/null
+++ b/askbot/views/moderation.py
@@ -0,0 +1,180 @@
+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.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
+
+#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, exclude=None):
+ editors = set()
+ for memo in memo_set:
+ post = get_object(memo)
+ editors.add(post.author)
+
+ if exclude in editors:
+ editors.remove(exclude)#make sure not to block yourself
+ return editors
+
+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']
+ ).select_related('activity')
+ result = {'message': ''}
+
+ if post_data['action'] == 'decline-with-reason':
+ 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
+
+ if num_posts:
+ posts_message = ungettext('%d post deleted', '%d posts deleted', num_posts) % num_posts
+ result['message'] = concat_messages(result['message'], posts_message)
+
+ if 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 'users' in post_data['items']:
+ editors = get_editors(memo_set)
+ 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)
+
+ #approve revisions by the authors
+ revisions = models.PostRevision.objects.filter(author__in=editors)
+ now = datetime.now()
+ revisions.update(approved=True, approved_at=now, approved_by=request.user)
+ ct = ContentType.objects.get_for_model(models.PostRevision)
+ mod_activity_types = (
+ const.TYPE_ACTIVITY_MARK_OFFENSIVE,
+ const.TYPE_ACTIVITY_MODERATED_NEW_POST,
+ const.TYPE_ACTIVITY_MODERATED_POST_EDIT
+ )
+ items = models.Activity.objects.filter(
+ content_type=ct,
+ object_id__in=revisions.values_list('id', flat=True),
+ activity_type__in=mod_activity_types
+ )
+ num_posts = items.count()
+ items.delete()
+
+ 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'] and post_data['action'] == 'block':
+ editors = get_editors(memo_set, exclude=request.user)
+ num_posts = 0
+ for editor in editors:
+ editor.set_status('b')
+ 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 and post_data['action'] == 'block':
+ 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
+ ips.remove(request.META['REMOTE_ADDR'])
+
+ 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)
+
+ memo_set.delete()
+ request.user.update_response_counts()
+ if result['message']:
+ result['message'] = force_text(result['message'])
+ return result
diff --git a/askbot/views/users.py b/askbot/views/users.py
index 32203998..504e654b 100644
--- a/askbot/views/users.py
+++ b/askbot/views/users.py
@@ -90,11 +90,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
)
@@ -396,7 +395,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
#
@@ -731,7 +730,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
@@ -795,34 +794,34 @@ def user_responses(request, user, context):
'timestamp': memo.activity.active_at,
'user': memo.activity.user,
'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': memo.activity.get_absolute_url(),
+ 'snippet': memo.activity.get_snippet(),
+ 'title': memo.activity.question.thread.title,
+ 'message_type': memo.activity.get_activity_type_display(),
+ 'question_id': memo.activity.question.id,
+ 'followup_messages': list(),
+ 'content': memo.activity.content_object.html,
}
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 = {
@@ -832,10 +831,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: