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

Commit

Permalink
Merge pull request #2 from epantry/follow-up-question
Browse files Browse the repository at this point in the history
Follow up question
  • Loading branch information
chrisclark committed Oct 6, 2014
2 parents 9b90997 + f36924a commit 7edd6f5
Show file tree
Hide file tree
Showing 18 changed files with 369 additions and 492 deletions.
3 changes: 3 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
include LICENSE
include README.md
recursive-include netpromoterscore/templates *
37 changes: 36 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,44 @@

Django Net Promoter Score is designed to help you find out what your customers think of your application. The net promoter score metric is based on user feedback to one question, "On a scale from 1 to 10 how likely are you to recommend us to a friend or colleague?". You can jazz this question up to fit your projects needs, and use Django Net Promoter Score to keep track of user response and detect when it is time to ask a user the question again. Django Net Promoter Score also features an administrative page that displays a breakdown of the net promoter score metric over a 13 month period.

## Installation ##

Few simple steps:

- Install `django-netpromoterscore` package:

$ pip install django-netpromoterscore

- Add `netpromoterscore` to your `INSTALLED_APPS`

- Add urls to your urls.py:

urlpatterns = patterns('',
...
url(r'^api/survey/$', SurveyView.as_view(), name='survey'),
url(r'^admin/net-promoter-score/', NetPromoterScoreView.as_view(), name='net-promoter-score'),
...
)

- You are done!

## API ##

GET /api/survey/

Returns `{ "survey_is_needed": true_or_false }`

POST /api/survey/

With json POST data without `"id"` like `{ "score": 9 }` creates new PromoterScore instance for current user.

If `"id"` is provided and POST data is like `{ "id": 15, "reason": "Awesome!"}`, updates existing PromoterScore instance.

Returns `{ "id": PROMOTER_SCORE_ID }`

## Information on NPS Metric ##
There is some fantastic information available online to help you understand the NPS metric and how to use it to create healthier relationships with your users.

The [Brain & Company website](http://netpromotersystem.com/ "Title") is a handy resource for gaining insight into your applications net promoter score.

## Features ##
Expand Down
6 changes: 0 additions & 6 deletions netpromoterscore/api_urls.py

This file was deleted.

5 changes: 0 additions & 5 deletions netpromoterscore/app_urls.py

This file was deleted.

19 changes: 19 additions & 0 deletions netpromoterscore/decorators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
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
10 changes: 10 additions & 0 deletions netpromoterscore/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from django import forms
from .models import PromoterScore

class PromoterScoreForm(forms.ModelForm):
score = forms.IntegerField(min_value=-1, max_value=10, required=False)
reason = forms.CharField(max_length=512, required=False)

class Meta:
model = PromoterScore
fields = ('score', 'reason', 'user')
261 changes: 26 additions & 235 deletions netpromoterscore/migrations/0001_initial.py
100755 → 100644

Large diffs are not rendered by default.

20 changes: 20 additions & 0 deletions netpromoterscore/migrations/0002_promoterscore_reason.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import models, migrations


class Migration(migrations.Migration):

dependencies = [
('netpromoterscore', '0001_initial'),
]

operations = [
migrations.AddField(
model_name='promoterscore',
name='reason',
field=models.TextField(null=True, blank=True),
preserve_default=True,
),
]
66 changes: 10 additions & 56 deletions netpromoterscore/models.py
Original file line number Diff line number Diff line change
@@ -1,79 +1,33 @@
import datetime
from django.db import models
from django.conf import settings
from utils import monthDict, get_many_previous_months, get_next_month
import datetime
from utils import get_next_month


class PromoterScoreManager(models.Manager):

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 promoters(self, scores):
return self.segment(scores, lambda x: 9 <= x <= 10)

def detractors(self, scores):
return self.segment(scores, lambda x: 1 <= x <= 6)

def passive(self, scores):
return self.segment(scores, lambda x: 7 <= x <= 8)

def skipped(self, scores):
return self.segment(scores, lambda x: x is None)

def segment(self, scores, test):
return sum([test(score) for score in scores.values()])

def _rolling(self, month):
def rolling(self, month):
month = get_next_month(month)
scores = self.filter(created_at__lt=datetime.date(year=month.year, month=month.month, day=1)).order_by('-created_at').values('user', 'score')
return self.recent_scores(scores)
scores = self.filter(created_at__lt=datetime.date(year=month.year, month=month.month, day=1))\
.order_by('-created_at').values('user', 'score')
return self._recent_scores(scores)

def _one_month_only(self, month):
def one_month_only(self, month):
scores = self.filter(created_at__month=month.month, created_at__year=month.year).values('user', 'score')
return self.recent_scores(scores)
return self._recent_scores(scores)

def recent_scores(self, scores):
def _recent_scores(self, scores):
most_recent_scores = {}
for score in scores:
if not score['user'] in most_recent_scores:
most_recent_scores[score['user']] = score['score']
return most_recent_scores

def _get_netpromoter(self, month, rolling):
label = monthDict[month.month] + ' ' + str(month.year)
scores = self._rolling(month) if rolling else self._one_month_only(month)
return NetPromoterScore(label, self.promoters(scores), self.detractors(scores), self.passive(scores), self.skipped(scores))


class NetPromoterScore(object):

def __init__(self, label, promoters, detractors, passive, skipped):
self.label = label
self.promoters = promoters
self.detractors = detractors
self.passive = passive
self.skipped = skipped

@property
def score(self):
total = self.promoters + self.detractors + self.passive
if total > 0:
promoter_percentage = float(self.promoters) / float(total)
detractor_percentage = float(self.detractors) / float(total)
return round((promoter_percentage - detractor_percentage) * 100, 2)
else:
return 'Not enough information.'


class PromoterScore(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL)
created_at = models.DateTimeField(auto_now_add=True)
score = models.IntegerField(null=True, blank=True)
reason = models.TextField(null=True, blank=True)

objects = PromoterScoreManager()

Expand Down
36 changes: 10 additions & 26 deletions netpromoterscore/templates/netpromoterscore/base.html
Original file line number Diff line number Diff line change
@@ -1,28 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Net Promoter Score</title>
<link href="//cdnjs.cloudflare.com/ajax/libs/codemirror/3.19.0/codemirror.min.css" rel="stylesheet">
{% extends 'admin/base_site.html'%}

{% block title %} Promoter Score {% endblock %}

{% block content%}
<link href="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.1.1/css/bootstrap.min.css" rel="stylesheet">
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script>
</head>
<body>
<div id="wrap" style="overflow: hidden;"> <!-- overflow hidden is for schema navigator -->
<div class="navbar navbar-default navbar-static-top" role="navigation">
<div class="container">
<div class="navbar-header">
<a class="navbar-brand">Net Promoter Score</a>
</div>
</div>
</div>
</div>
<div class="container">
<div class="starter-template">
<div class="table-responsive">
<table class="table table-bordered table-striped">
<table class="table">
<thead>
<tr>
<th>Month</th>
Expand All @@ -49,9 +34,9 @@
</div>
</div>
{% if rolling %}
<a href="{% url 'net-promoter-score'%}?rolling=0" type="button" class="btn btn-primary btn-lg">Switch to non-rolling scores</a>
<a href="{% url 'net-promoter-score'%}?rolling=0" type="button" class="btn btn-primary btn-lg" style="color: #fff">Switch to non-rolling scores</a>
{% else %}
<a href="{% url 'net-promoter-score'%}?rolling=1" type="button" class="btn btn-primary btn-lg">Switch to rolling scores</a>
<a href="{% url 'net-promoter-score'%}?rolling=1" type="button" class="btn btn-primary btn-lg" style="color: #fff">Switch to rolling scores</a>
{% endif %}
<br>
<!-- <p>Rolling scores use the most recent promoter scores from users. Rolling scores use scores from the start of collecting up to that month.</p> -->
Expand All @@ -60,10 +45,9 @@
<div id="footer" style="padding-top:20px;">
<div class="container">
<p class="text-muted">
Powered by <a href="https://www.github.com/epantry/django-promoterscore/">django-netpromoterscore</a> from
Powered by <a href="https://www.github.com/epantry/django-netpromoterscore/">django-netpromoterscore</a> from
<a href="https://www.epantry.com">ePantry</a>.
</p>
</div>
</div>
</body>
</html>
{% endblock %}
41 changes: 41 additions & 0 deletions netpromoterscore/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from datetime import datetime, timedelta
from django.contrib.auth import get_user_model
from netpromoterscore.models import PromoterScore


class TestMixin(object):

def create_users(self):
self.user_model = get_user_model()

self.user = self.user_model.objects.create_user(
username='jared', email='[email protected]', password='foobar123'
)
self.user2 = self.user_model.objects.create_user(
username='cole', email='[email protected]', password='foobar321'
)

self.user.save()
self.user2.save()

def create_promoter_scores(self):
self.now = datetime.now()

self.ps1 = PromoterScore(user=self.user, score=8)
self.ps1.save()

self.ps2 = PromoterScore(user=self.user, score=9)
self.ps2.save()
self.ps2.created_at = self.now - timedelta(4*365/12)
self.ps2.save()

self.ps3 = PromoterScore(user=self.user2, score=9)
self.ps3.save()
self.ps3.created_at = self.now - timedelta(7*365/12)
self.ps3.save()

def delete_users(self):
self.user_model.objects.all().delete()

def delete_promoter_scores(self):
PromoterScore.objects.all().delete()
54 changes: 13 additions & 41 deletions netpromoterscore/tests/test_models.py
Original file line number Diff line number Diff line change
@@ -1,51 +1,23 @@
import datetime
from django.contrib.auth.models import User
from django.test import TestCase
from netpromoterscore.models import PromoterScore, NetPromoterScore
from netpromoterscore.models import PromoterScore
from . import TestMixin

class TestPromoterScoreModels(TestCase, TestMixin):

class TestPromoterScoreModels(TestCase):
def setUp(self):
self.user1 = User.objects.create_user(username='jared', email='[email protected]', password='foobar123')
self.user1.save()
self.user2 = User.objects.create_user(username='cole', email='[email protected]', password='foobar321')
self.now = datetime.datetime.now()
self.ps1 = PromoterScore(user=self.user1, score=8)
self.ps1.save()
self.ps2 = PromoterScore(user=self.user1, score=9)
self.ps2.save()
self.ps2.created_at = self.now+datetime.timedelta(-4*365/12)
self.ps2.save()
self.ps3 = PromoterScore(user=self.user2, score=9)
self.ps3.save()
self.ps3.created_at = self.now+datetime.timedelta(-7*365/12)
self.ps3.save()
self.create_users()
self.create_promoter_scores()

def tearDown(self):
self.delete_promoter_scores()
self.delete_users()

def test_rolling_months(self):
got = PromoterScore.objects._rolling(month=self.now)
expected = {self.user1.pk: self.ps1.score, self.user2.pk: self.ps3.score}
got = PromoterScore.objects.rolling(month=self.now)
expected = {self.user.pk: self.ps1.score, self.user2.pk: self.ps3.score}
self.assertEqual(expected, got)

def test_one_month_only(self):
got = PromoterScore.objects._one_month_only(month=self.now)
expected = {self.user1.pk: self.ps1.score}
got = PromoterScore.objects.one_month_only(month=self.now)
expected = {self.user.pk: self.ps1.score}
self.assertEqual(expected, got)

def test_get_netpromoter_with_rolling(self):
got = PromoterScore.objects._get_netpromoter(month=self.now, rolling=True)
self.assertEqual([1, 1], [got.promoters, got.passive])

def test_get_netpromoter_without_rolling(self):
got = PromoterScore.objects._get_netpromoter(month=self.now, rolling=False)
self.assertEqual([0, 1], [got.promoters, got.passive])

class TestNetPromoterScoreModels(TestCase):

def test_net_promoter_score_calculation(self):
self.label = 'January 2014'
self.promoters = 50
self.detractors = 25
self.passive = 25
self.skipped = 0
nps = NetPromoterScore(self.label, self.promoters, self.detractors, self.passive, self.skipped)
self.assertEqual(float(25.0), nps.score)
11 changes: 9 additions & 2 deletions netpromoterscore/tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import datetime
from django.test import TestCase
from netpromoterscore.utils import get_many_previous_months, get_next_month
from netpromoterscore.utils import get_many_previous_months, get_next_month, count_score


class TestUtils(TestCase):
Expand All @@ -15,4 +15,11 @@ def test_get_next_month(self):
start_month = datetime.date(year=2013, month=12, day=5)
got = get_next_month(start_month)
expected = datetime.date(year=2014, month=1, day=1)
self.assertEqual(expected, got)
self.assertEqual(expected, got)

def test_net_promoter_score_calculation(self):
promoters = 50
detractors = 25
passive = 25
nps = count_score(promoters, detractors, passive)
self.assertEqual(float(25.0), nps)
Loading

0 comments on commit 7edd6f5

Please sign in to comment.