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

Commit

Permalink
Merge pull request #2 from rootart/ref-1
Browse files Browse the repository at this point in the history
ref #1, Implement bonus/reward system and click statistics
  • Loading branch information
Vasyl Dizhak authored Nov 16, 2017
2 parents b4900c2 + 594b2de commit c3b8f8d
Show file tree
Hide file tree
Showing 17 changed files with 497 additions and 52 deletions.
6 changes: 5 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,13 @@ after_success:
- codecov -e TOXENV,DJANGO

services:
- postgresql: "9.6"
- postgresql

addons:
postgresql: "9.6"

before_script:
- psql -c 'SHOW SERVER_VERSION;' -U postgres
- psql -c 'create database test_db;' -U postgres
notifications:
email: false
Expand Down
6 changes: 6 additions & 0 deletions referral_module/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class CampaignAdmin(admin.ModelAdmin):


class UserReferrerAdmin(admin.ModelAdmin):
list_display = ('__str__', 'reward', )
raw_id_fields = ('user', 'campaign', )


Expand All @@ -17,6 +18,11 @@ class ReferrerAdmin(admin.ModelAdmin):
raw_id_fields = ('registered_user', 'user_referrer', )


class UserReferrerStatsAdmin(admin.ModelAdmin):
raw_id_fields = ('user_referrer', )


admin.site.register(models.Campaign, CampaignAdmin)
admin.site.register(models.UserReferrer, UserReferrerAdmin)
admin.site.register(models.Referrer, ReferrerAdmin)
admin.site.register(models.UserReferrerStats, UserReferrerStatsAdmin)
28 changes: 28 additions & 0 deletions referral_module/middleware.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
from decimal import Decimal
from django.db.models.expressions import F
from django.http import HttpResponseBadRequest

from . import constants
from .models import (
UserReferrerStats, UserReferrer
)


class ReferrerStoreMiddleware(object):
Expand All @@ -23,5 +28,28 @@ def __call__(self, request):
campaign_key, user_referral_key
)
)
self.update_referrer_data(request, user_referral_key)

return response

def update_referrer_data(self, request, user_referral_key):
# store statistics
meta = request.META
try:
user_referrer = UserReferrer.objects.get(
key=user_referral_key
)
UserReferrerStats.objects.create(**{
'user_referrer': user_referrer,
'context': {
'HTTP_ACCEPT_LANGUAGE': meta.get('HTTP_ACCEPT_LANGUAGE'),
'HTTP_USER_AGENT': meta.get('HTTP_USER_AGENT'),
},
'ip': request.META['REMOTE_ADDR'],
})

# update user's reward
user_referrer.reward = F('reward') + Decimal(user_referrer.campaign.bonus_policy['click'])
user_referrer.save(update_fields=['reward', ])
except UserReferrer.DoesNotExist:
pass
37 changes: 37 additions & 0 deletions referral_module/migrations/0004_auto_20171102_1938.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2017-11-02 19:38
from __future__ import unicode_literals

import django.contrib.postgres.fields.jsonb
from django.db import migrations, models
import django.db.models.deletion
import referral_module.models


class Migration(migrations.Migration):

dependencies = [
('referral_module', '0003_auto_20171016_1258'),
]

operations = [
migrations.CreateModel(
name='UserReferrerStats',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True)),
('event_type', models.CharField(choices=[('click', 'click')], db_index=True, default=referral_module.models.StatsType('click'), max_length=50)),
('ip', models.GenericIPAddressField()),
('context', django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True)),
('user_referrer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='referral_module.UserReferrer', verbose_name='stats')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='campaign',
name='bonus_policy',
field=django.contrib.postgres.fields.jsonb.JSONField(default=referral_module.models.default_bonus_policy),
),
]
24 changes: 24 additions & 0 deletions referral_module/migrations/0005_auto_20171102_1957.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2017-11-02 19:57
from __future__ import unicode_literals

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('referral_module', '0004_auto_20171102_1938'),
]

operations = [
migrations.AlterModelOptions(
name='userreferrerstats',
options={'verbose_name': 'User referrer stats', 'verbose_name_plural': 'User referrer stats'},
),
migrations.AddField(
model_name='userreferrer',
name='reward',
field=models.DecimalField(decimal_places=3, default=0, max_digits=10),
),
]
70 changes: 61 additions & 9 deletions referral_module/models.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import logging

from decimal import Decimal

from django.db.models import F
from random import choice

from django.contrib.auth.models import User
from django.contrib.postgres.fields import JSONField
from django.db import models, transaction
from django.db.utils import IntegrityError
from django.dispatch import receiver
from django.http import HttpRequest
from registration.signals import user_registered

from referral_module.utils import ChoiceEnum
from . import constants

logger = logging.getLogger('bootcamp.{}'.format(__file__))
Expand All @@ -20,6 +26,13 @@ class Meta:
abstract = True


def default_bonus_policy():
return {
'click': 0,
'registration': 0,
}


class Campaign(CreatedDateModel):
key = models.CharField(
verbose_name='Campaign key',
Expand All @@ -32,6 +45,9 @@ class Campaign(CreatedDateModel):
is_active = models.BooleanField(
default=True
)
bonus_policy = JSONField(
default=default_bonus_policy
)
updated = models.DateTimeField(auto_now=True)

class Meta:
Expand All @@ -50,6 +66,10 @@ class UserReferrer(CreatedDateModel):
max_length=7,
unique=True
)
reward = models.DecimalField(
max_digits=10, decimal_places=3,
default=0
)

class Meta:
verbose_name = 'User referrer'
Expand All @@ -59,22 +79,50 @@ class Meta:
def save(self, *args, **kwargs):
attempts = 5

while not self.key and attempts:
self.key = "".join(
[choice(constants.KEY_ALPHABET) for i in range(7)]
)
with transaction.atomic():
try:
super(UserReferrer, self).save(*args, **kwargs)
except IntegrityError as e:
attempts -= 1
if not self.key:
while not self.key and attempts:
self.key = "".join(
[choice(constants.KEY_ALPHABET) for i in range(7)]
)
with transaction.atomic():
try:
super(UserReferrer, self).save(*args, **kwargs)
except IntegrityError as e:
attempts -= 1
else:
super(UserReferrer, self).save(*args, **kwargs)

def __str__(self):
return 'Campaign: {} {} -key-> {}'.format(
self.campaign.key, self.user.username, self.key
)


class StatsType(ChoiceEnum):
click = 'click'


class UserReferrerStats(CreatedDateModel):
user_referrer = models.ForeignKey(UserReferrer, verbose_name='stats')
event_type = models.CharField(
choices=StatsType.choices(), default=StatsType.click,
max_length=50, db_index=True
)
ip = models.GenericIPAddressField()
context = JSONField(
blank=True, null=True
)

class Meta:
verbose_name = 'User referrer stats'
verbose_name_plural = 'User referrer stats'

def __str__(self):
return "{} event at {}".format(
self.event_type, self.created
)


class Referrer(CreatedDateModel):
registered_user = models.ForeignKey('auth.User')
user_referrer = models.ForeignKey(UserReferrer)
Expand Down Expand Up @@ -108,6 +156,10 @@ def associate_registered_user_with_referral(sender, **kwargs):
registered_user=user,
user_referrer=user_referrer
)

# update user's reward
user_referrer.reward = F('reward') + Decimal(user_referrer.campaign.bonus_policy['registration'])
user_referrer.save(update_fields=['reward', ])
logger.debug(
'New referrer %s', referrer
)
6 changes: 0 additions & 6 deletions referral_module/tests.py

This file was deleted.

Empty file.
28 changes: 28 additions & 0 deletions referral_module/tests/factories.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from django.contrib.auth.models import User
import factory as factory_boy

from referral_module import models


class UserFactory(factory_boy.DjangoModelFactory):
username = factory_boy.Sequence(lambda n: 'user-{}'.format(n))
email = factory_boy.Sequence(lambda n: 'user-{}@moneypark.ch'.format(n))

class Meta:
model = User


class CampaignFactory(factory_boy.DjangoModelFactory):
key = factory_boy.Sequence(lambda n: 'key-{}'.format(n))
is_active = True

class Meta:
model = models.Campaign


class UserReferrerFactory(factory_boy.DjangoModelFactory):
campaign = factory_boy.SubFactory(CampaignFactory)
user = factory_boy.SubFactory(UserFactory)

class Meta:
model = models.UserReferrer
13 changes: 13 additions & 0 deletions referral_module/tests/test_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from django.test import TestCase

from .factories import UserReferrerFactory


class UserReferrerModelTestCase(TestCase):
def test_key_autogeneration(self):
user_referrer = UserReferrerFactory()
self.assertTrue(user_referrer.key)

user_referrer.key = None
user_referrer.save()
self.assertTrue(user_referrer.key)
18 changes: 18 additions & 0 deletions referral_module/tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from django.test import TestCase

from referral_module.utils import ChoiceEnum


class ColorChoice(ChoiceEnum):
green = 1
red = 2


class ChoicesEnumTestCase(TestCase):

def test_choices_classmethod(self):
assert ColorChoice.choices() == (
('green', 1),
('red', 2)
)
assert ColorChoice.green.value == 1
15 changes: 15 additions & 0 deletions referral_module/tests/test_views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from django.test import TestCase
from django.urls import reverse


class CampaignListTestCase(TestCase):
def setUp(self):
self.url = reverse('campaign-list')

def test_page_access(self):
response = self.client.get(self.url)
assert response.status_code == 200
self.assertTemplateUsed(
response,
'campaign/list.html'
)
7 changes: 7 additions & 0 deletions referral_module/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from enum import Enum


class ChoiceEnum(Enum):
@classmethod
def choices(cls):
return tuple((x.name, x.value) for x in cls)
4 changes: 0 additions & 4 deletions requirements-dev.txt

This file was deleted.

16 changes: 16 additions & 0 deletions requirements.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
django
django-debug-toolbar
django-extensions
django-registration
flake8
ipdb
psycopg2
pyflakes
werkzeug

tox
pytest
pytest-cov
pytest-django
codecov
factory_boy
Loading

0 comments on commit c3b8f8d

Please sign in to comment.