Skip to content
This repository has been archived by the owner on Aug 4, 2022. It is now read-only.

Commit

Permalink
Refactored calculation logic
Browse files Browse the repository at this point in the history
  • Loading branch information
Anton-Shutik committed Dec 15, 2014
1 parent 622d79e commit ad9e180
Show file tree
Hide file tree
Showing 6 changed files with 50 additions and 106 deletions.
10 changes: 9 additions & 1 deletion netpromoterscore/app_settings.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
from django.conf import settings

PROMOTERSCORE_PERMISSION_VIEW = getattr(settings, 'PROMOTERSCORE_PERMISSION_VIEW', lambda u: u.is_staff)
PROMOTERSCORE_USER_RANGES_DEFAULT = {
'promoters': [9, 10],
'passive': [7, 8],
'detractors': [1, 2, 3, 4, 5, 6],
'skipped': [None]
}

PROMOTERSCORE_PERMISSION_VIEW = getattr(settings, 'PROMOTERSCORE_PERMISSION_VIEW', lambda u: u.is_staff)
PROMOTERSCORE_USER_RANGES = getattr(settings, 'PROMOTERSCORE_USER_RANGES', PROMOTERSCORE_USER_RANGES_DEFAULT)
12 changes: 1 addition & 11 deletions netpromoterscore/decorators.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,9 @@
from django.http import HttpResponse
from .app_settings import PROMOTERSCORE_PERMISSION_VIEW
from .utils import safe_admin_login_prompt


def login_required(f):
def wrap(request, *args, **kwargs):
if request.user.is_anonymous():
return HttpResponse(status=401)
return f(request, *args, **kwargs)
return wrap


def admin_required(f):
def wrap(request, *args, **kwargs):
if not PROMOTERSCORE_PERMISSION_VIEW(request.user):
return safe_admin_login_prompt(request)
return f(request, *args, **kwargs)
return wrap
return wrap
12 changes: 4 additions & 8 deletions netpromoterscore/models.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
from django.core.validators import MinValueValidator, MaxValueValidator
from django.db import models
from django.conf import settings
from utils import get_next_month
from django.db.models import Count


class PromoterScoreManager(models.Manager):
def rolling(self, month):
month = get_next_month(month)
scores = self.filter(created_at__lt=month).order_by('-created_at').values_list('user', 'score')
return dict(scores)

def one_month_only(self, month):
scores = self.filter(created_at__month=month.month, created_at__year=month.year).values_list('user', 'score')
return dict(scores)
def group_by_period(self, period):
select = {'period': "date_trunc('%s', created_at)" % period}
return self.extra(select=select, order_by=['score', 'period']).values('score', 'period').annotate(count=Count('score'))


class PromoterScore(models.Model):
Expand Down
14 changes: 7 additions & 7 deletions netpromoterscore/templates/netpromoterscore/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@
</tr>
</thead>
<tbody>
{% for nps_info in nps_info_list %}
{% for period, score in scores %}
<tr>
<th>{{ nps_info.label }}</th>
<td>{{ nps_info.score }}</td>
<td>{{ nps_info.promoters }}</td>
<td>{{ nps_info.detractors }}</td>
<td>{{ nps_info.passive }}</td>
<td>{{ nps_info.skipped }}</td>
<th>{{ period|date:"M j Y" }}</th>
<td>{{ score.score }}</td>
<td>{{ score.promoters }}</td>
<td>{{ score.detractors }}</td>
<td>{{ score.passive }}</td>
<td>{{ score.skipped }}</td>
</tr>
{% endfor %}
</tbody>
Expand Down
55 changes: 13 additions & 42 deletions netpromoterscore/utils.py
Original file line number Diff line number Diff line change
@@ -1,50 +1,21 @@
import datetime
from django.contrib.admin.forms import AdminAuthenticationForm
from django.contrib.auth.views import login
from django.contrib.auth import REDIRECT_FIELD_NAME
from app_settings import PROMOTERSCORE_USER_RANGES


monthDict = {1: 'January', 2: 'February', 3: 'March', 4: 'April', 5: 'May', 6: 'June', 7: 'July', 8: 'August',
9: 'September', 10: 'October', 11: 'November', 12: 'December'}


def safe_admin_login_prompt(request):
defaults = {
'template_name': 'admin/login.html',
'authentication_form': AdminAuthenticationForm,
'extra_context': {
'title': 'Log in',
'app_path': request.get_full_path(),
REDIRECT_FIELD_NAME: request.get_full_path(),
},
}
return login(request, **defaults)
def count_score(score):
promoters, detractors, passive = score.get('promoters', 0), score.get('detractors', 0), score.get('passive', 0)
total = promoters + detractors + passive
if total > 0:
return round(((promoters - detractors) / float(total)) * 100, 2)
else:
return 'Not enough information'


def get_many_previous_months(month, total_months=12):
months = []
for x in xrange(total_months):
month = get_previous_month(month)
months.append(month)
return months

def get_previous_month(date):
month = date.month - 1
if month == 0:
return datetime.date(year=date.year-1, month=12, day=1)
return datetime.date(year=date.year, month=month, day=1)

def get_next_month(date):
month = date.month + 1
if month == 13:
return datetime.date(year=date.year+1, month=1, day=1)
return datetime.date(year=date.year, month=month, day=1)

def count_score(promoters, detractors, passive, skipped=None):
total = promoters + detractors + passive
if total > 0:
promoter_percentage = float(promoters) / float(total)
detractor_percentage = float(detractors) / float(total)
return round((promoter_percentage - detractor_percentage) * 100, 2)
else:
return 'Not enough information'
def get_range_name_by_score(score):
for r_name, range in PROMOTERSCORE_USER_RANGES.iteritems():
if score in range:
return r_name
raise Exception('Range not found')
53 changes: 16 additions & 37 deletions netpromoterscore/views.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,15 @@
from collections import defaultdict
import datetime
import json
from django.contrib.admin.views.decorators import staff_member_required
from django.http import JsonResponse
from django.shortcuts import render, get_object_or_404
from django.utils.decorators import method_decorator
from django.views.generic.base import View
from .models import PromoterScore
from .forms import PromoterScoreForm
from .decorators import login_required, admin_required
from .utils import get_many_previous_months, monthDict, count_score


CHECKS = {
'promoters': lambda x: 9 <= x <= 10,
'passive': lambda x: 7 <= x <= 8,
'detractors': lambda x: 1 <= x <= 6,
'skipped': lambda x: x is None
}
from .decorators import login_required
from .utils import count_score, get_range_name_by_score


class SurveyView(View):
Expand Down Expand Up @@ -60,36 +54,21 @@ def time_to_ask(self, promoter_score):

class NetPromoterScoreView(View):

@method_decorator(admin_required)
@method_decorator(staff_member_required)
def get(self, request):
context = self.get_context_data(request)
return render(request, 'netpromoterscore/base.html', context)

def get_context_data(self, request):
rolling = True if request.GET.get('rolling') and int(request.GET.get('rolling')) else False
return {'rolling': rolling, 'nps_info_list': self.get_list_view_context(rolling)}

def get_list_view_context(self, rolling):
now = datetime.date.today().replace(day=1)

months = [now] + get_many_previous_months(now)
scores_by_month = [self.get_netpromoter(month, rolling) for month in months]
return scores_by_month

def get_netpromoter(self, month, rolling):
label = monthDict[month.month] + ' ' + str(month.year)
if rolling:
scores = PromoterScore.objects.rolling(month)
else:
scores = PromoterScore.objects.one_month_only(month)

result = dict((k, 0) for k in CHECKS.iterkeys())
for val in scores.values():
for name, check in CHECKS.items():
if check(val):
result[name] += 1
break
result['score'] = count_score(**result)
result['label'] = label

return result
period = request.GET.get('period', 'month')
qs = PromoterScore.objects.group_by_period(period)
scores = defaultdict(dict)
for item in qs:
score, count, period = item['score'], item['count'], item['period']
user_range = get_range_name_by_score(score)
scores[period].update( {user_range: count} )
sort_scores = sorted(scores.iteritems(), key=lambda key_value: key_value[0], reverse=True)
for _, sc in sort_scores:
sc['score'] = count_score(sc)
return {'rolling': rolling, 'scores': sort_scores}

0 comments on commit ad9e180

Please sign in to comment.