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