diff --git a/kitsune/kpi/api.py b/kitsune/kpi/api.py index aef2c422ccb..92760eba057 100644 --- a/kitsune/kpi/api.py +++ b/kitsune/kpi/api.py @@ -302,6 +302,18 @@ def merge_results(metrics_qs, label): results_list, key=itemgetter('date'), reverse=True)] +class CSATMetricList(CachedAPIView): + """The API list view for contributor CSAT metrics""" + code = None + + def get_objects(self, request): + kind = MetricKind.objects.get(code=self.code) + since = date.today() - timedelta(days=30) + metrics = Metric.objects.filter(start__gte=since, kind=kind).order_by('-start') + + return [{'date': m.start, 'csat': m.value} for m in metrics] + + def _daily_qs_for(model_cls): """Return the daily grouped queryset we need for model_cls.""" # Limit to newer than 2011/1/1 and active creators. diff --git a/kitsune/kpi/cron.py b/kitsune/kpi/cron.py index b6b4e60cbcf..4e62574720d 100644 --- a/kitsune/kpi/cron.py +++ b/kitsune/kpi/cron.py @@ -1,3 +1,4 @@ +import json import operator from datetime import datetime, date, timedelta from functools import reduce @@ -6,6 +7,7 @@ from django.db.models import Count, F, Q import cronjobs +import requests from statsd import statsd from kitsune.customercare.models import Reply @@ -17,7 +19,10 @@ SUPPORT_FORUM_CONTRIBUTORS_METRIC_CODE, VISITORS_METRIC_CODE, SEARCH_SEARCHES_METRIC_CODE, SEARCH_CLICKS_METRIC_CODE, EXIT_SURVEY_YES_CODE, EXIT_SURVEY_NO_CODE, EXIT_SURVEY_DONT_KNOW_CODE, CONTRIBUTOR_COHORT_CODE, KB_ENUS_CONTRIBUTOR_COHORT_CODE, - KB_L10N_CONTRIBUTOR_COHORT_CODE, SUPPORT_FORUM_HELPER_COHORT_CODE, AOA_CONTRIBUTOR_COHORT_CODE) + KB_L10N_CONTRIBUTOR_COHORT_CODE, SUPPORT_FORUM_HELPER_COHORT_CODE, AOA_CONTRIBUTOR_COHORT_CODE, + CONTRIBUTORS_CSAT_METRIC_CODE, AOA_CONTRIBUTORS_CSAT_METRIC_CODE, + KB_ENUS_CONTRIBUTORS_CSAT_METRIC_CODE, KB_L10N_CONTRIBUTORS_CSAT_METRIC_CODE, + SUPPORT_FORUM_CONTRIBUTORS_CSAT_METRIC_CODE) from kitsune.kpi.surveygizmo_utils import ( get_email_addresses, add_email_to_campaign, get_exit_survey_results, SURVEYS) @@ -581,3 +586,119 @@ def is_in_cohort(u): cohort |= set(filter(is_in_cohort, potential_users)) return cohort + + +@cronjobs.register +def calculate_csat_metrics(): + user = settings.SURVEYGIZMO_USER + password = settings.SURVEYGIZMO_PASSWORD + startdate = date.today() - timedelta(days=2) + enddate = date.today() - timedelta(days=1) + page = 1 + more_pages = True + survey_id = SURVEYS['general']['community_health'] + + csat = { + CONTRIBUTORS_CSAT_METRIC_CODE: 0, + SUPPORT_FORUM_CONTRIBUTORS_CSAT_METRIC_CODE: 0, + AOA_CONTRIBUTORS_CSAT_METRIC_CODE: 0, + KB_ENUS_CONTRIBUTORS_CSAT_METRIC_CODE: 0, + KB_L10N_CONTRIBUTORS_CSAT_METRIC_CODE: 0, + } + + counts = { + CONTRIBUTORS_CSAT_METRIC_CODE: 0, + SUPPORT_FORUM_CONTRIBUTORS_CSAT_METRIC_CODE: 0, + AOA_CONTRIBUTORS_CSAT_METRIC_CODE: 0, + KB_ENUS_CONTRIBUTORS_CSAT_METRIC_CODE: 0, + KB_L10N_CONTRIBUTORS_CSAT_METRIC_CODE: 0, + } + + while more_pages: + response = requests.get( + 'https://restapi.surveygizmo.com/v2/survey/{survey}' + '/surveyresponse?' + 'filter[field][0]=datesubmitted' + '&filter[operator][0]=>=&filter[value][0]={start}+0:0:0' + '&filter[field][1]=datesubmitted' + '&filter[operator][1]=<&filter[value][1]={end}+0:0:0' + '&filter[field][2]=status&filter[operator][2]==' + '&filter[value][2]=Complete' + '&resultsperpage=500' + '&page={page}' + '&user:pass={user}:{password}'.format( + survey=survey_id, start=startdate, + end=enddate, page=page, user=user, password=password), + timeout=300) + + results = json.loads(response.content) + total_pages = results['total_pages'] + more_pages = page < total_pages + + for r in results['data']: + try: + rating = int(r['[question(3)]']) + except ValueError: + # CSAT question was not answered + pass + else: + csat[CONTRIBUTORS_CSAT_METRIC_CODE] += rating + counts[CONTRIBUTORS_CSAT_METRIC_CODE] += 1 + + if len(r['[question(4), option(10010)]']): # Army of Awesome + csat[AOA_CONTRIBUTORS_CSAT_METRIC_CODE] += rating + counts[AOA_CONTRIBUTORS_CSAT_METRIC_CODE] += 1 + + if len(r['[question(4), option(10011)]']): # Support Forum + csat[SUPPORT_FORUM_CONTRIBUTORS_CSAT_METRIC_CODE] += rating + counts[SUPPORT_FORUM_CONTRIBUTORS_CSAT_METRIC_CODE] += 1 + + if len(r['[question(4), option(10012)]']): # KB EN-US + csat[KB_ENUS_CONTRIBUTORS_CSAT_METRIC_CODE] += rating + counts[KB_ENUS_CONTRIBUTORS_CSAT_METRIC_CODE] += 1 + + if len(r['[question(4), option(10013)]']): # KB L10N + csat[KB_L10N_CONTRIBUTORS_CSAT_METRIC_CODE] += rating + counts[KB_L10N_CONTRIBUTORS_CSAT_METRIC_CODE] += 1 + + page += 1 + + for code in csat: + metric_kind, _ = MetricKind.objects.get_or_create(code=code) + value = csat[code] / counts[code] if counts[code] else 50 # If no responses assume neutral + Metric.objects.update_or_create(kind=metric_kind, start=startdate, end=enddate, + defaults={'value': value}) + + +@cronjobs.register +def csat_survey_emails(): + querysets = [(Revision.objects.all(), ('creator', 'reviewer',)), + (Answer.objects.not_by_asker(), ('creator',)), + (Reply.objects.all(), ('user',))] + + end = datetime.today().replace(hour=0, minute=0, second=0, microsecond=0) + start = end - timedelta(days=30) + + users = _get_cohort(querysets, (start, end)) + + for u in users: + p = u.profile + if p.csat_email_sent is None or p.csat_email_sent < start: + user = settings.SURVEYGIZMO_USER + password = settings.SURVEYGIZMO_PASSWORD + + survey_id = SURVEYS['general']['community_health'] + campaign_id = SURVEYS['general']['community_health_campaign_id'] + + try: + requests.put( + 'https://restapi.surveygizmo.com/v2/survey/{survey}/surveycampaign/' + '{campaign}/contact?semailaddress={email}&user:pass={user}:{password}'.format( + survey=survey_id, campaign=campaign_id, + email=u.email, user=user, password=password), + timeout=30) + except requests.exceptions.Timeout: + print 'Timed out adding: %s' % u.email + else: + p.csat_email_sent = datetime.now() + p.save() diff --git a/kitsune/kpi/models.py b/kitsune/kpi/models.py index 4fe281eec05..f0cec16c637 100644 --- a/kitsune/kpi/models.py +++ b/kitsune/kpi/models.py @@ -16,6 +16,12 @@ EXIT_SURVEY_NO_CODE = 'exit-survey:no' EXIT_SURVEY_DONT_KNOW_CODE = 'exit-survey:dont-know' +CONTRIBUTORS_CSAT_METRIC_CODE = 'csat contributors' +AOA_CONTRIBUTORS_CSAT_METRIC_CODE = 'csat contributors:aoa' +SUPPORT_FORUM_CONTRIBUTORS_CSAT_METRIC_CODE = 'csat contributors:supportforum' +KB_ENUS_CONTRIBUTORS_CSAT_METRIC_CODE = 'csat contributors:kb:en-US' +KB_L10N_CONTRIBUTORS_CSAT_METRIC_CODE = 'csat contributors:kb-l10n' + CONTRIBUTOR_COHORT_CODE = 'contributor' KB_ENUS_CONTRIBUTOR_COHORT_CODE = 'contributor:kb:en-US' KB_L10N_CONTRIBUTOR_COHORT_CODE = 'contributor:kb:l10n' diff --git a/kitsune/kpi/surveygizmo_utils.py b/kitsune/kpi/surveygizmo_utils.py index cf4672c579a..b3da5643d7b 100644 --- a/kitsune/kpi/surveygizmo_utils.py +++ b/kitsune/kpi/surveygizmo_utils.py @@ -11,6 +11,8 @@ 'email_collection_survey_id': 1002970, 'exit_survey_id': 991425, 'exit_survey_campaign_id': 878533, + 'community_health': 2466367, + 'community_health_campaign_id': 3369235, }, 'questions': { # This is for users that are browsing questions. 'email_collection_survey_id': 1717268, diff --git a/kitsune/kpi/urls_api.py b/kitsune/kpi/urls_api.py index 9ecf456e81a..0c4ff45c127 100644 --- a/kitsune/kpi/urls_api.py +++ b/kitsune/kpi/urls_api.py @@ -2,12 +2,31 @@ from rest_framework import routers from kitsune.kpi import api +from kitsune.kpi.models import (CONTRIBUTORS_CSAT_METRIC_CODE, AOA_CONTRIBUTORS_CSAT_METRIC_CODE, + SUPPORT_FORUM_CONTRIBUTORS_CSAT_METRIC_CODE, + KB_ENUS_CONTRIBUTORS_CSAT_METRIC_CODE, + KB_L10N_CONTRIBUTORS_CSAT_METRIC_CODE) router = routers.SimpleRouter() router.register(r'cohort', api.CohortViewSet) urlpatterns = patterns( '', + url(r'^api/v1/kpi/csat-contributors/?$', + api.CSATMetricList.as_view(code=CONTRIBUTORS_CSAT_METRIC_CODE), + name='api.kpi.csat-contributors'), + url(r'^api/v1/kpi/csat-contributors/aoa/?$', + api.CSATMetricList.as_view(code=AOA_CONTRIBUTORS_CSAT_METRIC_CODE), + name='api.kpi.csat-contributors-aoa'), + url(r'^api/v1/kpi/csat-contributors/support-forum/?$', + api.CSATMetricList.as_view(code=SUPPORT_FORUM_CONTRIBUTORS_CSAT_METRIC_CODE), + name='api.kpi.csat-contributors-support-forum'), + url(r'^api/v1/kpi/csat-contributors/kb-enus/?$', + api.CSATMetricList.as_view(code=KB_ENUS_CONTRIBUTORS_CSAT_METRIC_CODE), + name='api.kpi.csat-contributors-kb-enus'), + url(r'^api/v1/kpi/csat-contributors/kb-l10n/?$', + api.CSATMetricList.as_view(code=KB_L10N_CONTRIBUTORS_CSAT_METRIC_CODE), + name='api.kpi.csat-contributors-kb-l10n'), url(r'^api/v1/kpi/l10n-coverage/?$', api.L10nCoverageMetricList.as_view(), name='api.kpi.l10n-coverage'), url(r'^api/v1/kpi/exit-survey/?$', api.ExitSurveyMetricList.as_view(), diff --git a/kitsune/users/migrations/0011_add_csat_email_sent_field.py b/kitsune/users/migrations/0011_add_csat_email_sent_field.py new file mode 100644 index 00000000000..b81dd45306c --- /dev/null +++ b/kitsune/users/migrations/0011_add_csat_email_sent_field.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0010_auto_20151110_1307'), + ] + + operations = [ + migrations.AddField( + model_name='profile', + name='csat_email_sent', + field=models.DateField(null=True, verbose_name='When the user was sent a community health survey', blank=True), + preserve_default=True, + ), + ] diff --git a/kitsune/users/models.py b/kitsune/users/models.py index 14e1fd34123..8378e8e65d7 100644 --- a/kitsune/users/models.py +++ b/kitsune/users/models.py @@ -74,6 +74,9 @@ class Profile(ModelBase, SearchMixin): default=False, help_text=_lazy(u'Has been sent a first revision contribution email.')) involved_from = models.DateField(null=True, blank=True, verbose_name=_lazy(u'Involved with Mozilla from')) + csat_email_sent = models.DateField(null=True, blank=True, + verbose_name=_lazy(u'When the user was sent a community ' + u'health survey')) class Meta(object): permissions = (('view_karma_points', 'Can view karma points'), diff --git a/scripts/crontab/crontab.tpl b/scripts/crontab/crontab.tpl index 79db40d8ffe..ba3466875a8 100644 --- a/scripts/crontab/crontab.tpl +++ b/scripts/crontab/crontab.tpl @@ -28,6 +28,7 @@ HOME = /tmp 00 00 * * * {{ cron }} rebuild_kb 42 00 * * * {{ cron }} update_top_contributors 00 01 * * * {{ cron }} update_l10n_coverage_metrics +00 01 * * * {{ cron }} calculate_csat_metrics 11 01 * * * {{ cron }} report_employee_answers 30 01 * * * {{ cron }} reindex_users_that_contributed_yesterday 40 01 * * * {{ cron }} update_weekly_votes