diff --git a/kitsune/questions/cron.py b/kitsune/questions/cron.py index 2836b47c928..7246241b2fb 100644 --- a/kitsune/questions/cron.py +++ b/kitsune/questions/cron.py @@ -1,15 +1,18 @@ import logging +import textwrap import time -from datetime import datetime, timedelta +from datetime import date, datetime, timedelta from django.conf import settings +from django.contrib.auth.models import Group +from django.core.mail import send_mail from django.db import connection, transaction import cronjobs from kitsune.questions import config from kitsune.questions.models import ( - Question, QuestionVote, QuestionMappingType, QuestionVisits) + Question, QuestionVote, QuestionMappingType, QuestionVisits, Answer) from kitsune.questions.tasks import ( escalate_question, update_question_vote_chunk) from kitsune.search.es_utils import ES_EXCEPTIONS, get_documents @@ -151,3 +154,57 @@ def escalate_questions(): escalate_question.delay(question.id) return len(qs_no_replies_yet) + + +@cronjobs.register +def report_employee_answers(): + """Send an email about employee answered questions. + + We report on the users in the "Support Forum Tracked" group. + We send the email to the users in the "Support Forum Metrics" group. + """ + tracked_group = Group.objects.get(name='Support Forum Tracked') + report_group = Group.objects.get(name='Support Forum Metrics') + + tracked_users = tracked_group.user_set.all() + report_recipients = report_group.user_set.all() + + if len(tracked_users) == 0 or len(report_recipients) == 0: + return + + yesterday = date.today() - timedelta(days=1) + day_before_yesterday = yesterday - timedelta(days=1) + + # Total number of questions asked the day before yesterday + questions = Question.objects.filter( + creator__is_active=True, + created__gte=day_before_yesterday, + created__lt=yesterday) + num_questions = questions.count() + + # Total number of answered questions day before yesterday + num_answered = questions.filter(num_answers__gt=0).count() + + # Total number of questions answered by user in tracked_group + num_answered_by_tracked = Answer.objects.filter( + question__in=questions, + creator__in=tracked_users).values_list('question_id').distinct().count() + + email_subject = 'Support Forum answered report for {date}'.format(date=day_before_yesterday) + + email_body_tmpl = textwrap.dedent("""\ + Date: {date} + Number of questions asked: {num_questions} + Number of questions answered: {num_answered} + Number of questions with answer from 'Support Forum Tracked' users: {num_by_tracked} + """) + email_body = email_body_tmpl.format( + date=day_before_yesterday, + num_questions=num_questions, + num_answered=num_answered, + num_by_tracked=num_answered_by_tracked) + + email_addresses = [u.email for u in report_recipients] + + send_mail(email_subject, email_body, settings.TIDINGS_FROM_ADDRESS, email_addresses, + fail_silently=False) diff --git a/kitsune/questions/tests/test_cron.py b/kitsune/questions/tests/test_cron.py index 4c61ac41f83..2b7d1329373 100644 --- a/kitsune/questions/tests/test_cron.py +++ b/kitsune/questions/tests/test_cron.py @@ -1,14 +1,18 @@ from datetime import datetime, timedelta +from django.core import mail + import mock from nose.tools import eq_ from kitsune.products.tests import product import kitsune.questions.tasks from kitsune.questions import config -from kitsune.questions.cron import escalate_questions +from kitsune.questions.cron import escalate_questions, report_employee_answers from kitsune.questions.tests import answer, question from kitsune.sumo.tests import TestCase +from kitsune.users.models import Group +from kitsune.users.tests import user class TestEscalateCron(TestCase): @@ -87,3 +91,48 @@ def test_escalate_questions_cron(self, submit_ticket): # Run the cron job and verify only 3 questions were escalated. eq_(len(questions_to_escalate), escalate_questions()) + + +class TestEmployeeReportCron(TestCase): + + def test_report_employee_answers(self): + # Note: This depends on two groups that are created in migrations. + # If we fix the tests to not run migrations, we'll need to create the + # two groups here: 'Support Forum Tracked', 'Support Forum Metrics' + + tracked_group = Group.objects.get(name='Support Forum Tracked') + tracked_user = user(save=True) + tracked_user.groups.add(tracked_group) + + report_group = Group.objects.get(name='Support Forum Metrics') + report_user = user(save=True) + report_user.groups.add(report_group) + + # An unanswered question that should get reported + question(created=datetime.now() - timedelta(days=2), save=True) + + # An answered question that should get reported + q = question(created=datetime.now() - timedelta(days=2), save=True) + answer(question=q, save=True) + + # A question answered by a tracked user that should get reported + q = question(created=datetime.now() - timedelta(days=2), save=True) + answer(creator=tracked_user, question=q, save=True) + + # More questions that shouldn't get reported + q = question(created=datetime.now() - timedelta(days=3), save=True) + answer(creator=tracked_user, question=q, save=True) + q = question(created=datetime.now() - timedelta(days=1), save=True) + answer(question=q, save=True) + question(save=True) + + report_employee_answers() + + # Get the last email and verify contents + email = mail.outbox[len(mail.outbox) - 1] + + assert 'Number of questions asked: 3' in email.body + assert 'Number of questions answered: 2' in email.body + assert 'users: 1' in email.body + + eq_([report_user.email], email.to) diff --git a/kitsune/users/migrations/0011_create_employee_metrics_groups.py b/kitsune/users/migrations/0011_create_employee_metrics_groups.py new file mode 100644 index 00000000000..54ef117567e --- /dev/null +++ b/kitsune/users/migrations/0011_create_employee_metrics_groups.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models + +class Migration(DataMigration): + + def forwards(self, orm): + """Create groups for Support Forum tracking and reporting.""" + Group = orm['auth.Group'] + Group.objects.create(name='Support Forum Tracked') + Group.objects.create(name='Support Forum Metrics') + + def backwards(self, orm): + """Remove the groups created.""" + orm['auth.Group'].objects.filter( + name__in=['Support Forum Tracked', 'Support Forum Metrics']).delete() + + models = { + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'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': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + u'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'}), + u'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'}) + }, + u'users.deactivation': { + 'Meta': {'object_name': 'Deactivation'}, + 'date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'moderator': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'deactivations'", 'to': u"orm['auth.User']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'to': u"orm['auth.User']"}) + }, + u'users.emailchange': { + 'Meta': {'object_name': 'EmailChange'}, + 'activation_key': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'db_index': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'unique': 'True'}) + }, + u'users.profile': { + 'Meta': {'object_name': 'Profile'}, + 'avatar': ('django.db.models.fields.files.ImageField', [], {'max_length': '250', 'null': 'True', 'blank': 'True'}), + 'bio': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'city': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'country': ('django.db.models.fields.CharField', [], {'max_length': '2', 'null': 'True', 'blank': 'True'}), + 'facebook': ('django.db.models.fields.URLField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'irc_handle': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'locale': ('kitsune.sumo.models.LocaleField', [], {'default': "'en-US'", 'max_length': '7'}), + 'mozillians': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'public_email': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'timezone': ('timezones.fields.TimeZoneField', [], {'null': 'True', 'blank': 'True'}), + 'twitter': ('django.db.models.fields.CharField', [], {'max_length': '15', 'null': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.User']", 'unique': 'True', 'primary_key': 'True'}), + 'website': ('django.db.models.fields.URLField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}) + }, + u'users.registrationprofile': { + 'Meta': {'object_name': 'RegistrationProfile'}, + 'activation_key': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'unique': 'True'}) + }, + u'users.setting': { + 'Meta': {'unique_together': "(('user', 'name'),)", 'object_name': 'Setting'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'settings'", 'to': u"orm['auth.User']"}), + 'value': ('django.db.models.fields.CharField', [], {'max_length': '60', 'blank': 'True'}) + } + } + + complete_apps = ['auth', 'users'] + symmetrical = True diff --git a/scripts/crontab/crontab.tpl b/scripts/crontab/crontab.tpl index 033c7a25c3f..b9a9a0efc0e 100644 --- a/scripts/crontab/crontab.tpl +++ b/scripts/crontab/crontab.tpl @@ -44,6 +44,7 @@ HOME = /tmp 30 3 * * * root {{ rscripts }} scripts/l10n_completion.py --truncate 30 locale media/uploads/l10n_history.json media/uploads/l10n_summary.json 30 3 * * * {{ cron }} send_postatus_errors 30 1 * * * {{ cron }} reindex_users_that_contributed_yesterday +11 1 * * * {{ cron }} report_employee_answers # Once per week. 21 03 * * 3 {{ django }} purge_hashes