From 5d575b7dcd10c29c0d1946dd9a6d415501a90809 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Wed, 17 Sep 2014 11:53:54 +0700 Subject: added alert email for moderators and made moderation queue url common --- askbot/const/__init__.py | 11 ++ askbot/doc/source/changelog.rst | 1 + askbot/doc/source/management-commands.rst | 3 + askbot/mail/__init__.py | 10 +- .../commands/askbot_send_moderation_alerts.py | 113 +++++++++++++++++++++ askbot/media/js/user.js | 5 - askbot/media/style/style.css | 22 ++-- askbot/media/style/style.less | 8 +- askbot/templates/email/notify_moderator.html | 7 ++ askbot/templates/macros.html | 4 +- askbot/templates/moderation/queue.html | 41 +++++--- askbot/templates/one_column_body.html | 4 +- askbot/templates/user_inbox/base.html | 10 -- askbot/urls.py | 5 + askbot/views/moderation.py | 91 +++++++++++++++-- askbot/views/users.py | 34 +------ 16 files changed, 282 insertions(+), 87 deletions(-) create mode 100644 askbot/management/commands/askbot_send_moderation_alerts.py create mode 100644 askbot/templates/email/notify_moderator.html diff --git a/askbot/const/__init__.py b/askbot/const/__init__.py index 56da1c95..48324772 100644 --- a/askbot/const/__init__.py +++ b/askbot/const/__init__.py @@ -199,6 +199,7 @@ TYPE_ACTIVITY_UPDATE_REJECT_REASON = 27 TYPE_ACTIVITY_VALIDATION_EMAIL_SENT = 28 TYPE_ACTIVITY_POST_SHARED = 29 TYPE_ACTIVITY_ASK_TO_JOIN_GROUP = 30 +TYPE_ACTIVITY_MODERATION_ALERT_SENT = 31 #TYPE_ACTIVITY_EDIT_QUESTION = 17 #TYPE_ACTIVITY_EDIT_ANSWER = 18 @@ -257,7 +258,17 @@ TYPE_ACTIVITY = ( TYPE_ACTIVITY_VALIDATION_EMAIL_SENT, 'sent email address validation message'#don't translate, internal ), + ( + TYPE_ACTIVITY_MODERATION_ALERT_SENT, + 'sent moderation alert'#don't translate, internal + ) +) + +MODERATED_EDIT_ACTIVITY_TYPES = ( + TYPE_ACTIVITY_MODERATED_NEW_POST, + TYPE_ACTIVITY_MODERATED_POST_EDIT ) +MODERATED_ACTIVITY_TYPES = MODERATED_EDIT_ACTIVITY_TYPES + (TYPE_ACTIVITY_MARK_OFFENSIVE,) #MENTION activity is added implicitly, unfortunately diff --git a/askbot/doc/source/changelog.rst b/askbot/doc/source/changelog.rst index e6191b29..6fc390e2 100644 --- a/askbot/doc/source/changelog.rst +++ b/askbot/doc/source/changelog.rst @@ -3,6 +3,7 @@ Changes in Askbot Development master branch (only on github) ------------------------------------------ +* Added email alert for moderators `askbot_send_moderation_alerts` * Implemented Google Plus login * Allowed localized site settings * Added management command `askbot_clear_moderation_queue` diff --git a/askbot/doc/source/management-commands.rst b/askbot/doc/source/management-commands.rst index 46a10c88..ad8d1a32 100644 --- a/askbot/doc/source/management-commands.rst +++ b/askbot/doc/source/management-commands.rst @@ -166,6 +166,9 @@ Any configurable options, related to these commands are accessible via "Email" s | | of the live settings, as well as the appropriate delay | | | parameters may be set. | +-------------------------------------+-------------------------------------------------------------+ +| `askbot_send_moderation_alerts` | Sends alerts to moderators when there are items on the | +| | queue. | ++-------------------------------------+-------------------------------------------------------------+ Data repair commands ==================== diff --git a/askbot/mail/__init__.py b/askbot/mail/__init__.py index cb6f86a0..e4d9bc22 100644 --- a/askbot/mail/__init__.py +++ b/askbot/mail/__init__.py @@ -97,11 +97,19 @@ def _send_mail(subject_line, body_text, sender_email, recipient_list, headers=No else: message_class = mail.EmailMessage + from askbot.models import User + email_list = list() + for recipient in recipient_list: + if isinstance(recipient, User): + email_list.append(recipient.email) + else: + email_list.append(recipient) + msg = message_class( subject_line, get_text_from_html(body_text), sender_email, - recipient_list, + email_list, headers = headers ) if html_enabled: diff --git a/askbot/management/commands/askbot_send_moderation_alerts.py b/askbot/management/commands/askbot_send_moderation_alerts.py new file mode 100644 index 00000000..8cb129c8 --- /dev/null +++ b/askbot/management/commands/askbot_send_moderation_alerts.py @@ -0,0 +1,113 @@ +from django.core.management.base import NoArgsCommand +from django.template.loader import get_template +from django.utils.translation import ugettext as _ +from askbot.conf import settings as askbot_settings +from askbot import const +from askbot import mail +from askbot.models import Activity +from askbot.models import User + +def get_moderators(): + return User.objects.filter(status__in=('d', 'm')) + +def get_last_mod_alert_activity(): + atype = const.TYPE_ACTIVITY_MODERATION_ALERT_SENT + acts = Activity.objects.filter(activity_type=atype).order_by('-id') + count = len(acts) + if count == 0: + return None + last_act = acts[0] + + if count > 1: + #get last moderation activity and delete all others + acts = acts.exclude(id=last_act.id) + acts.delete() + + return last_act + + +def get_last_notified_user(): + last_act = get_last_mod_alert_activity() + if last_act: + return last_act.content_object + return None + + +def select_moderators_to_notify(candidates, num_needed): + candidates_count = candidates.count() + + #special case - if we need to notify the same number of + #moderators that are available, then we don't rotate them + #and notify all, b/c otherwise we would stop notifications + #because there are not enough moderators + if candidates_count <= num_needed: + return list(candidates) + + last_notified = get_last_notified_user() + if last_notified is None: + return candidates[:num_needed] + + mods = list(candidates.filter(id__gt=last_notified.id)) + num_mods = len(mods) + if num_mods >= num_needed: + return mods[:num_needed] + else: + #wrap around the end to the beginning + num_missing = num_needed - num_mods + more_mods = get_moderators().order_by('id') + more_mods = more_mods[:num_missing] + mods.extend(list(more_mods)) + return mods + + +def select_last_moderator(mods): + return max(mods, key=lambda item: item.id) + + +def remember_last_moderator(user): + act = get_last_mod_alert_activity() + if act: + act.content_object = user + act.save() + else: + act = Activity( + user=user, + content_object=user, + activity_type=const.TYPE_ACTIVITY_MODERATION_ALERT_SENT + ) + act.save() + + + +def notify_moderator(user): + template = get_template('email/notify_moderator.html') + subject_line = _('%s moderation alert') % askbot_settings.APP_SHORT_NAME, + mail.send_mail( + subject_line=subject_line, + body_text=template.render({'user': user}), + recipient_list=[user,] + ) + + +class Command(NoArgsCommand): + def handle_noargs(self, *args, **kwargs): + #get size of moderation queue + queue = Activity.objects.filter(activity_type__in=const.MODERATED_ACTIVITY_TYPES) + if queue.count() == 0: + return + + #get moderators + mods = get_moderators().order_by('id') + if mods.count() == 0: + return + + mods = select_moderators_to_notify(mods, 3) + + if len(mods) == 0: + return + + for mod in mods: + notify_moderator(mod) + + last_mod = select_last_moderator(mods) + remember_last_moderator(last_mod) diff --git a/askbot/media/js/user.js b/askbot/media/js/user.js index a116876c..5ece420c 100644 --- a/askbot/media/js/user.js +++ b/askbot/media/js/user.js @@ -1,9 +1,4 @@ var setup_inbox = function(){ - 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'); diff --git a/askbot/media/style/style.css b/askbot/media/style/style.css index 38c1d0b0..aecc5675 100644 --- a/askbot/media/style/style.css +++ b/askbot/media/style/style.css @@ -1533,8 +1533,7 @@ ul#related-tags li { float: left; } .mod-queue-info { - margin-top: 12px; - margin-bottom: 0; + margin: 12px 0 12px 20px; } .moderate-tags-page button { line-height: 18px; @@ -2916,34 +2915,37 @@ ul#related-tags li { .reject-reason-title { margin-bottom: 12px; } -.user-profile-page.inbox-flags .re { +.moderation-queue-page .re { width: 810px; } -.user-profile-page.inbox-flags .post-moderation-controls { +.moderation-queue-page .post-moderation-controls { float: left; width: 150px; margin-top: 23px; text-align: right; } -.user-profile-page.inbox-flags .dropdown { +.moderation-queue-page .dropdown { display: -moz-inline-stack; display: inline-block; height: 17px; } -.user-profile-page.inbox-flags .dropdown:hover ul.dropdown-menu { +.moderation-queue-page .dropdown:hover ul.dropdown-menu { display: block; margin-top: 9px; } -.user-profile-page.inbox-flags .highlight { +.moderation-queue-page .highlight { background: transparent; } -.user-profile-page.inbox-flags .messages { +.moderation-queue-page .messages { margin-bottom: 14px; } -.user-profile-page.inbox-flags .select-items { +.moderation-queue-page .message { + margin: 12px 0; +} +.moderation-queue-page .select-items { margin-bottom: 10px; } -.user-profile-page.inbox-flags #responses div.face { +.moderation-queue-page #responses div.face { display: none; } .openid-signin form { diff --git a/askbot/media/style/style.less b/askbot/media/style/style.less index 42ed7b44..ee88bedd 100644 --- a/askbot/media/style/style.less +++ b/askbot/media/style/style.less @@ -1622,8 +1622,7 @@ ul#related-tags li { } .mod-queue-info { - margin-top: 12px; - margin-bottom: 0; + margin: 12px 0 12px 20px; } .moderate-tags-page { @@ -3044,7 +3043,7 @@ ul#related-tags li { margin-bottom: 12px; } -.user-profile-page.inbox-flags { +.moderation-queue-page { .re { width: 810px; } @@ -3071,6 +3070,9 @@ ul#related-tags li { .messages { margin-bottom: 14px; } + .message { + margin: 12px 0; + } .select-items { margin-bottom: 10px; } diff --git a/askbot/templates/email/notify_moderator.html b/askbot/templates/email/notify_moderator.html new file mode 100644 index 00000000..d4ea3e02 --- /dev/null +++ b/askbot/templates/email/notify_moderator.html @@ -0,0 +1,7 @@ +{% extends "email/base_mail.html" %} +{% block content %} + {% trans mod_url='moderation_queue'|url(), site=settings.APP_SHORT_NAME -%} + There are new items in the {{ site }} moderation queue, please have a look + {%- endtrans %} +{% endblock %} +{% block footer %}{% include "email/footer.html" %}{% endblock %} diff --git a/askbot/templates/macros.html b/askbot/templates/macros.html index f0fe368e..7ed1162b 100644 --- a/askbot/templates/macros.html +++ b/askbot/templates/macros.html @@ -780,9 +780,7 @@ for the purposes of the AJAX comment editor #} {%- macro moderation_items_link(user, moderation_items) -%} {% if moderation_items %} - + {% if moderation_items['new_count'] > 0 %} 0 %} diff --git a/askbot/templates/moderation/queue.html b/askbot/templates/moderation/queue.html index fd1e402a..a03d8cc7 100644 --- a/askbot/templates/moderation/queue.html +++ b/askbot/templates/moderation/queue.html @@ -1,9 +1,10 @@ -{% extends "user_inbox/base.html" %} +{% extends "one_column_body.html" %} {% import "macros.html" as macros %} -{% block profilesection %} - {% trans %}moderation queue{% endtrans %} +{% block title %} + {% trans %}Moderation queue{% endtrans %} {% endblock %} -{% block inbox_content %} +{% block body %} +

{% trans %}Moderation queue{% endtrans %}

{#
{% trans %}Select:{% endtrans %} @@ -43,23 +44,33 @@ {% include "moderation/manage_reject_reasons_dialog.html" %}
- {% for message in messages %}{# messages are grouped by question, using the "nested_messages" #} + {% for message in messages %}
- {#

"{{ message.title.strip()|escape}}"

#} {{ macros.moderation_queue_message(message) }}
- {# "nested" messages are further response messages to the same question #} - {% for followup_message in message.followup_messages %} -
- {{ macros.moderation_queue_message(followup_message) }} -
- {% endfor %} {% endfor %}
{% endblock %} +{% block endjs %} + {# todo: factor out moderation.js file #} + + +{% endblock %} diff --git a/askbot/templates/one_column_body.html b/askbot/templates/one_column_body.html index 852f8fe5..27ced098 100644 --- a/askbot/templates/one_column_body.html +++ b/askbot/templates/one_column_body.html @@ -2,7 +2,7 @@ {% block body_class %}one-col{% endblock %} {% block body %}
- {% block content%} - {% endblock%} + {% block content %} + {% endblock %}
{% endblock %} diff --git a/askbot/templates/user_inbox/base.html b/askbot/templates/user_inbox/base.html index 5a1dcb01..bf0a904f 100644 --- a/askbot/templates/user_inbox/base.html +++ b/askbot/templates/user_inbox/base.html @@ -51,16 +51,6 @@ 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() %} - askbot['data']['postRejectReasons'] = [ - {% for reason in post_reject_reasons %} - {'id': {{reason.id}}, 'title': '{{reason.title|escapejs}}'}, - {% endfor %} - ]; - {% endif %} $(document).ready(function(){ $('body').addClass('inbox-{{ inbox_section }}'); setup_inbox(); diff --git a/askbot/urls.py b/askbot/urls.py index d06bff72..e752e4c6 100644 --- a/askbot/urls.py +++ b/askbot/urls.py @@ -205,6 +205,11 @@ urlpatterns = patterns('', views.commands.moderate_group_join_request, name='moderate_group_join_request' ), + service_url( + r'^%s$' % _('moderation-queue/'), + views.moderation.moderation_queue, + name='moderation_queue' + ), service_url( r'^moderate-post-edits/', views.moderation.moderate_post_edits, diff --git a/askbot/views/moderation.py b/askbot/views/moderation.py index 4084b845..52ee249b 100644 --- a/askbot/views/moderation.py +++ b/askbot/views/moderation.py @@ -1,28 +1,26 @@ from askbot.utils import decorators from askbot import const +from askbot.conf import settings as askbot_settings from askbot import models from askbot import mail from datetime import datetime +from django.http import Http404 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.auth.decorators import login_required from django.contrib.contenttypes.models import ContentType from django.db.models import Q from django.utils.encoding import force_text +from django.shortcuts import render 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 @@ -74,6 +72,83 @@ def concat_messages(message1, message2): return message2 +@login_required +def moderation_queue(request): + """Lists moderation queue items""" + if not request.user.is_administrator_or_moderator(): + raise Http404 + + activity_types = (const.TYPE_ACTIVITY_MARK_OFFENSIVE,) + if askbot_settings.CONTENT_MODERATION_MODE in ('premoderation', 'audit'): + activity_types += ( + const.TYPE_ACTIVITY_MODERATED_NEW_POST, + const.TYPE_ACTIVITY_MODERATED_POST_EDIT + ) + + #2) load the activity notifications according to activity types + #todo: insert pagination code here + memo_set = request.user.get_notifications(activity_types) + memo_set = memo_set.select_related( + 'activity', + 'activity__content_type', + 'activity__question__thread', + 'activity__user', + 'activity__user__gravatar', + ).order_by( + '-activity__active_at' + )[:const.USER_VIEW_DATA_SIZE] + + #3) "package" data for the output + queue = list() + for memo in memo_set: + 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 + 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' + ip_addr = None + else: + act_user = act.user + act_message = act.get_activity_type_display() + act_type = 'edit' + ip_addr = act.content_object.ip_addr + + item = { + 'id': memo.id, + 'timestamp': act.active_at, + 'user': act_user, + 'ip_addr': ip_addr, + 'is_new': memo.is_new(), + '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, + 'content': obj.html or obj.text, + } + queue.append(item) + + queue.sort(lambda x,y: cmp(y['timestamp'], x['timestamp'])) + reject_reasons = models.PostFlagReason.objects.all().order_by('title') + data = { + 'active_tab': 'users', + 'page_class': 'moderation-queue-page', + 'post_reject_reasons': reject_reasons, + 'messages' : queue, + } + template = 'moderation/queue.html' + return render(request, template, data) + + @csrf.csrf_exempt @decorators.post_only @decorators.ajax_only @@ -97,7 +172,7 @@ def moderate_post_edits(request): 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, + activity_type__in=const.MODERATED_EDIT_ACTIVITY_TYPES, user__in=editors ) memo_filter = Q(id__in=post_data['edit_ids']) | Q(user=request.user, activity__in=items) @@ -222,5 +297,5 @@ def moderate_post_edits(request): acts.delete() request.user.update_response_counts() - result['memo_count'] = request.user.get_notifications(MOD_ACTIVITY_TYPES).count() + result['memo_count'] = request.user.get_notifications(const.MODERATED_ACTIVITY_TYPES).count() return result diff --git a/askbot/views/users.py b/askbot/views/users.py index c80ff4ec..7316b170 100644 --- a/askbot/views/users.py +++ b/askbot/views/users.py @@ -738,20 +738,9 @@ def user_responses(request, user, context): #1) select activity types according to section section = request.GET.get('section', 'forum') - if section == 'flags' and not\ - (request.user.is_moderator() or request.user.is_administrator()): - raise Http404 - if section == 'forum': activity_types = const.RESPONSE_ACTIVITY_TYPES_FOR_DISPLAY activity_types += (const.TYPE_ACTIVITY_MENTION,) - elif section == 'flags': - activity_types = (const.TYPE_ACTIVITY_MARK_OFFENSIVE,) - if askbot_settings.CONTENT_MODERATION_MODE in ('premoderation', 'audit'): - activity_types += ( - const.TYPE_ACTIVITY_MODERATED_NEW_POST, - const.TYPE_ACTIVITY_MODERATED_POST_EDIT - ) elif section == 'join_requests': return show_group_join_requests(request, user, context) elif section == 'messages': @@ -810,26 +799,14 @@ def user_responses(request, user, context): 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 + act_user = act.user + act_message = act.get_activity_type_display() + act_type = 'edit' response = { 'id': memo.id, 'timestamp': act.active_at, 'user': act_user, - 'ip_addr': ip_addr, 'is_new': memo.is_new(), 'url': act.get_absolute_url(), 'snippet': act.get_snippet(), @@ -872,10 +849,7 @@ def user_responses(request, user, context): 'messages' : filtered_message_list, } context.update(data) - if section == 'flags': - template = 'moderation/queue.html' - else: - template = 'user_inbox/responses.html' + template = 'user_inbox/responses.html' return render(request, template, context) def user_network(request, user, context): -- cgit v1.2.3-1-g7c22