summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEvgeny Fadeev <evgeny.fadeev@gmail.com>2014-07-16 17:31:50 -0300
committerEvgeny Fadeev <evgeny.fadeev@gmail.com>2014-07-16 17:31:50 -0300
commit490243b93e7237312ee913eff9b1b296f764df8d (patch)
tree290d8c19741d3bd3ddadaf18065d62d2d48245aa
parentbfd4497f930c0c6e804eb7eaae24fb95ccb64789 (diff)
parentac721d1aa97372eef3b7d1772dc7f40b7c8a860d (diff)
downloadaskbot-490243b93e7237312ee913eff9b1b296f764df8d.tar.gz
askbot-490243b93e7237312ee913eff9b1b296f764df8d.tar.bz2
askbot-490243b93e7237312ee913eff9b1b296f764df8d.zip
Merge branch 'master' into g-plus
-rw-r--r--README.rst2
-rw-r--r--askbot/__init__.py1
-rw-r--r--askbot/conf/moderation.py33
-rw-r--r--askbot/const/message_keys.py1
-rw-r--r--askbot/context.py1
-rw-r--r--askbot/doc/source/changelog.rst3
-rw-r--r--askbot/forms.py22
-rw-r--r--askbot/importers/stackexchange/management/commands/load_stackexchange.py2
-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.js648
-rw-r--r--askbot/media/js/utils.js6
-rw-r--r--askbot/media/style/style.css81
-rw-r--r--askbot/media/style/style.less85
-rw-r--r--askbot/middleware/forum_mode.py2
-rw-r--r--askbot/migrations/0178_auto__add_field_postrevision_ip_addr.py425
-rw-r--r--askbot/models/__init__.py261
-rw-r--r--askbot/models/post.py410
-rw-r--r--askbot/models/question.py129
-rw-r--r--askbot/models/reply_by_email.py16
-rw-r--r--askbot/sitemap.py5
-rw-r--r--askbot/templates/email/change_settings_info.html2
-rw-r--r--askbot/templates/livesettings/group_settings.html2
-rw-r--r--askbot/templates/livesettings/site_settings.html2
-rw-r--r--askbot/templates/macros.html77
-rw-r--r--askbot/templates/moderation/manage_reject_reasons_dialog.html43
-rw-r--r--askbot/templates/moderation/queue.html65
-rw-r--r--askbot/templates/question/answer_card.html3
-rw-r--r--askbot/templates/question/question_card.html3
-rw-r--r--askbot/templates/user_inbox/base.html58
-rw-r--r--askbot/templates/user_inbox/responses.html28
-rw-r--r--askbot/templates/user_inbox/responses_and_flags.html41
-rw-r--r--askbot/templates/user_profile/reject_post_dialog.html109
-rw-r--r--askbot/templates/user_profile/user_info.html2
-rw-r--r--askbot/templates/user_profile/user_moderate.html14
-rw-r--r--askbot/templates/widgets/system_messages.html2
-rw-r--r--askbot/tests/email_alert_tests.py12
-rw-r--r--askbot/tests/form_tests.py2
-rw-r--r--askbot/tests/page_load_tests.py2
-rw-r--r--askbot/tests/post_model_tests.py3
-rw-r--r--askbot/urls.py10
-rw-r--r--askbot/utils/decorators.py2
-rw-r--r--askbot/views/__init__.py1
-rw-r--r--askbot/views/commands.py6
-rw-r--r--askbot/views/context.py20
-rw-r--r--askbot/views/moderation.py225
-rw-r--r--askbot/views/readers.py7
-rw-r--r--askbot/views/users.py116
-rw-r--r--askbot/views/writers.py26
49 files changed, 2092 insertions, 946 deletions
diff --git a/README.rst b/README.rst
index 07cfe6b0..23ed59d8 100644
--- a/README.rst
+++ b/README.rst
@@ -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&section=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&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 (<span class="response-count">{{re_count}}</span>){% 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 (<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&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,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 &amp; 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 &amp; 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(