From f43509082288e652c26f878f79528f78eaf212c3 Mon Sep 17 00:00:00 2001 From: Max Yakovenko Date: Thu, 26 Jul 2018 00:46:55 +0300 Subject: [PATCH] add referral app --- referral/__init__.py | 1 + referral/admin.py | 108 +++++++++++++++++++++++ referral/apps.py | 10 +++ referral/forms.py | 46 ++++++++++ referral/middleware.py | 42 +++++++++ referral/mixins.py | 9 ++ referral/models.py | 116 +++++++++++++++++++++++++ referral/signals.py | 2 + referral/templatetags/__init__.py | 0 referral/templatetags/referral_tags.py | 7 ++ referral/tests.py | 3 + referral/urls.py | 6 ++ referral/utils.py | 15 ++++ referral/views.py | 0 14 files changed, 365 insertions(+) create mode 100644 referral/__init__.py create mode 100644 referral/admin.py create mode 100644 referral/apps.py create mode 100644 referral/forms.py create mode 100644 referral/middleware.py create mode 100644 referral/mixins.py create mode 100644 referral/models.py create mode 100644 referral/signals.py create mode 100644 referral/templatetags/__init__.py create mode 100644 referral/templatetags/referral_tags.py create mode 100644 referral/tests.py create mode 100644 referral/urls.py create mode 100644 referral/utils.py create mode 100644 referral/views.py diff --git a/referral/__init__.py b/referral/__init__.py new file mode 100644 index 0000000..71294fa --- /dev/null +++ b/referral/__init__.py @@ -0,0 +1 @@ +default_app_config = 'referral.apps.ReferralConfig' diff --git a/referral/admin.py b/referral/admin.py new file mode 100644 index 0000000..cfbd448 --- /dev/null +++ b/referral/admin.py @@ -0,0 +1,108 @@ +from django.contrib import admin +from django.contrib.admin import register +from django.urls import reverse_lazy +from django.utils.html import format_html +from django.utils.translation import ugettext_lazy as _ + +# Register your models here. +from rangefilter.filter import DateRangeFilter, DateTimeRangeFilter + +from core.admin import SafeModelAdmin +from .models import Referral, ReferralStats, PartnerStats +from .forms import ReferralAdminForm, ReferralStatsAdminForm, PartnerStatsAdminForm + + +class RefarralAdminInline(admin.TabularInline): + model = Referral + readonly_fields = ('code',) + + def has_add_permission(self, request): + return False + + def has_change_permission(self, request, obj=None): + return False + + def has_delete_permission(self, request, obj=None): + return False + + +@register(Referral) +class ReferralAdmin(SafeModelAdmin): + def has_add_permission(self, request): + return False + + def referral_stats(self, referral): + try: + link = reverse_lazy( + 'admin:{}_{}_changelist'.format(referral.referralstats._meta.app_label, + referral.referralstats._meta.object_name.lower()) + ) + link += '?q={}'.format(referral.code) + except Exception as e: + link = '#' + name = _('Details') + + return format_html('{}', link, name) + + referral_stats.short_description = _('Stats') + form = ReferralAdminForm + list_display = ('code', 'create_at', 'updated_at', 'status', 'referral_stats') + search_fields = ('code',) + list_filter = ('status', ('create_at', DateRangeFilter), ('updated_at', DateTimeRangeFilter)) + ordering = ('-create_at',) + + +@register(ReferralStats) +class ReferralStatsAdmin(SafeModelAdmin): + def has_add_permission(self, request): + return False + + def referral_code(self, stats): + return stats.referral.code + + referral_code.short_description = _('Code') + + def converted_earnings(self, stats): + return stats.earnings if stats.earnings > 0 else 0 + + converted_earnings.short_description = _('Earnings') + + form = ReferralStatsAdminForm + list_display = ('referral_code', 'visits', 'registrations') + list_select_related = ('referral',) + search_fields = ('referral__name', 'referral__code',) + ordering = ('-create_at',) + + +@register(PartnerStats) +class PartnerStatsAdmin(SafeModelAdmin): + def has_add_permission(self, request): + return False + + def stats_owner(self, stats): + try: + link = reverse_lazy( + 'admin:{}_{}_change'.format(stats.user._meta.app_label, stats.user._meta.object_name.lower()), + args=(stats.user.id,) + ) + name = stats.user.email + except Exception as e: + link = '#' + name = "None" + return format_html('{}', link, name) + + stats_owner.short_description = _('User') + + def formatted_total_conversion(self, stats): + return str(stats.formatted_total_conversion) + "%" + + formatted_total_conversion.short_description = _('Total conversion') + + def converted_total_earnings(self, stats): + return stats.total_earnings if stats.total_earnings > 0 else 0 + + converted_total_earnings.short_description = _('Total earnings') + + form = PartnerStatsAdminForm + list_display = ('stats_owner', 'total_visits', 'total_regs') + search_fields = ('user__email',) diff --git a/referral/apps.py b/referral/apps.py new file mode 100644 index 0000000..23786fc --- /dev/null +++ b/referral/apps.py @@ -0,0 +1,10 @@ +from django.apps import AppConfig +from django.utils.translation import ugettext_lazy as _ + + +class ReferralConfig(AppConfig): + name = 'referral' + verbose_name = _("Referrals") + + def ready(self): + import referral.signals diff --git a/referral/forms.py b/referral/forms.py new file mode 100644 index 0000000..0f61adb --- /dev/null +++ b/referral/forms.py @@ -0,0 +1,46 @@ +from django import forms +from django.forms import URLInput, ALL_FIELDS + +from .models import Referral, ReferralStats, PartnerStats + + +class ReferralAdminForm(forms.ModelForm): + class Meta: + model = Referral + fields = ('code',) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.disable_fields(self.fields) + + def disable_fields(self, fields): + for field_name, field in fields.items(): + field.disabled = True + + +class ReferralStatsAdminForm(forms.ModelForm): + class Meta: + model = ReferralStats + fields = ALL_FIELDS + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.disable_fields(self.fields) + + def disable_fields(self, fields): + for field_name, field in fields.items(): + field.disabled = True + + +class PartnerStatsAdminForm(forms.ModelForm): + class Meta: + model = PartnerStats + fields = ALL_FIELDS + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.disable_fields(self.fields) + + def disable_fields(self, fields): + for field_name, field in fields.items(): + field.disabled = True diff --git a/referral/middleware.py b/referral/middleware.py new file mode 100644 index 0000000..2052de1 --- /dev/null +++ b/referral/middleware.py @@ -0,0 +1,42 @@ +from django.http import HttpResponseRedirect +from django.urls import reverse + +from core.models import STATUS_ACTIVE +from .models import Referral +from .utils import set_cookie, pop_cookie, get_cookie + + +class ReferralMiddleware: + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + response = None + if hasattr(self,'process_request'): + response = self.process_request(request) + if not response: + response = self.get_response(request) + if hasattr(self,'process_response'): + response = self.process_response(request,response) + return response + + def process_request(self,request): + pass + + def process_response(self,request,response): + if not request.user.is_authenticated: + code = request.GET.get('ref') + cookie_code = get_cookie(request.COOKIES, 'referral') + if code != cookie_code: + referral_code = request.GET.get('ref') + referral = Referral.active.filter(code__exact=referral_code).first() + if referral and referral.is_active: + referral.referralstats.visits += 1 + referral.referralstats.save() + referral.user.partnerstats.total_visits += 1 + referral.user.partnerstats.save() + set_cookie(response, 'referral', referral_code) + elif request.user.is_authenticated: + pop_cookie(response, 'referral') + return response + diff --git a/referral/mixins.py b/referral/mixins.py new file mode 100644 index 0000000..80644cc --- /dev/null +++ b/referral/mixins.py @@ -0,0 +1,9 @@ +from decimal import Decimal + + +class StatsFormatterMixin(object): + def format_conversion(self, conversion, place=0): + return round(conversion * 100, place) + + def normalize_conversion(self, conversion): + return Decimal(conversion) / Decimal(100) diff --git a/referral/models.py b/referral/models.py new file mode 100644 index 0000000..a597eec --- /dev/null +++ b/referral/models.py @@ -0,0 +1,116 @@ +import logging +import string + +import random + +from django.conf import settings +from django.contrib.auth import get_user_model +from django.contrib.sites.models import Site +from django.db import models +from django.db.models.signals import post_save +from django.dispatch import receiver +from django.urls import reverse_lazy +from django.utils.translation import ugettext_lazy as _ + +# Create your models here. +from registration.signals import user_activated + +from core.models import AbstractStatusModel, STATUS_ACTIVE +from referral.mixins import StatsFormatterMixin +from referral.utils import get_cookie + +logger = logging.getLogger(__name__) + +# --------------------------------- Referral status list ----------------------------# + +REFERRAL_DEFAULT_STATUS = STATUS_ACTIVE + + +# @TODO: translate into english and use translation +class Referral(AbstractStatusModel): + CODE_LENGTH = 8 + + user = models.OneToOneField(get_user_model(),verbose_name=_('пользователь'), on_delete=models.CASCADE, primary_key=True + ) + code = models.CharField(_('код'), max_length=255) + + @classmethod + def create(cls, user, code): + referral = cls(user=user, code=code) + return referral + + @staticmethod + def generate_code(): + def _generate_code(): + return ''.join([random.choice(string.ascii_uppercase + string.digits) for n in range(Referral.CODE_LENGTH)]) + + code = _generate_code() + while Referral.active.filter(code=code).exists(): + code = _generate_code() + return code + + @property + def url(self): + path = reverse_lazy('index:index') + site_url = settings.DEFAULT_SITE_URL + return "{}{}".format(site_url, path) + + def __str__(self): + return self.code + + class Meta: + verbose_name = _('Реферал') + verbose_name_plural = _('Рефералы') + + +class ReferralStats(StatsFormatterMixin, AbstractStatusModel): + referral = models.OneToOneField(Referral, verbose_name=_('Реферал'), on_delete=models.CASCADE, primary_key=True) + visits = models.IntegerField(_('Посищения'), default=0) + registrations = models.IntegerField(_('Регистрации'), default=0) + + class Meta: + verbose_name = _('Реферальная статистика') + verbose_name_plural = _('Реферальная статистика') + + +class PartnerStats(StatsFormatterMixin, AbstractStatusModel): + user = models.OneToOneField(get_user_model(), verbose_name=_('username'), on_delete=models.CASCADE, + primary_key=True) + total_visits = models.BigIntegerField(_('Всего посещений'), default=0) + total_regs = models.BigIntegerField(_('Всего регистраций'), default=0) + + class Meta: + verbose_name = _('Партнеская статистика') + verbose_name_plural = _('Партнерская статистика') + + +@receiver(post_save, sender=get_user_model()) +def create_user_referral(sender, instance, created, **kwargs): + if created and Referral.objects.filter(user=instance).first() is None: + Referral.create(user=instance, code=Referral.generate_code()).save() + + +@receiver(post_save, sender=Referral) +def init_referral_stuff(sender, instance, created, *args, **kwargs): + if created: + if PartnerStats.objects.filter(user_id=instance.user_id).count() == 0: + PartnerStats.objects.create(user=instance.user).save() + if ReferralStats.active.filter(referral=instance).count() == 0: + ReferralStats.active.create(referral=instance).save() + + +@receiver(user_activated) +def update_ref_stats(sender, user, request, **kwargs): + referral_code = get_cookie(request.COOKIES, 'referral') + if referral_code: + referral = Referral.objects.filter(code__exact=referral_code).first() + if referral: + referral.referralstats.registrations += 1 + referral.referralstats.save() + referral.user.partnerstats.total_regs += 1 + referral.user.partnerstats.save() + user.referral = referral + user.referral_user = referral.user + user.save() + else: + logger.warning("Missing referral code in database: " + referral_code) diff --git a/referral/signals.py b/referral/signals.py new file mode 100644 index 0000000..ab60af6 --- /dev/null +++ b/referral/signals.py @@ -0,0 +1,2 @@ +import logging +logger = logging.getLogger(__name__) diff --git a/referral/templatetags/__init__.py b/referral/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/referral/templatetags/referral_tags.py b/referral/templatetags/referral_tags.py new file mode 100644 index 0000000..a91dede --- /dev/null +++ b/referral/templatetags/referral_tags.py @@ -0,0 +1,7 @@ +from django import template + +register = template.Library() + +@register.filter +def normalize_conversion(conversion, place = 0): + return round(conversion*100, place) diff --git a/referral/tests.py b/referral/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/referral/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/referral/urls.py b/referral/urls.py new file mode 100644 index 0000000..b422d57 --- /dev/null +++ b/referral/urls.py @@ -0,0 +1,6 @@ +from django.conf.urls import url +from django.urls import re_path + +urlpatterns = [ + # re_path() +] diff --git a/referral/utils.py b/referral/utils.py new file mode 100644 index 0000000..148ab19 --- /dev/null +++ b/referral/utils.py @@ -0,0 +1,15 @@ +import datetime + +def set_cookie(response, key, value, day_expire = 1): + max_age = day_expire * 24 * 60 * 60 + expires = datetime.datetime.strftime(datetime.datetime.utcnow() + datetime.timedelta(seconds=max_age), "%a, %d-%b-%Y %H:%M:%S GMT") + response.set_cookie(key=key,value=value,max_age=max_age,expires=expires) # @TODO: Connect cookie to domain + +def get_cookie(cookies, key): + return cookies.get(key) if cookies.get(key) else None + +def pop_cookie(response, key): + cookie = get_cookie(response.cookies, key) + if cookie: + response.delete_cookie(key) + return cookie diff --git a/referral/views.py b/referral/views.py new file mode 100644 index 0000000..e69de29