From 53dd30b74a0b805937dd3a8add59aeb8c5ffeee0 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Wed, 29 Jan 2020 09:12:17 +0100 Subject: [PATCH 1/1] Contact management. --- src/club/models.py | 17 ++- src/messaging/admin.py | 39 ++++- src/messaging/locale/pl/LC_MESSAGES/django.mo | Bin 1776 -> 2882 bytes src/messaging/locale/pl/LC_MESSAGES/django.po | 134 +++++++++++++++--- .../migrations/0003_auto_20200128_2230.py | 101 +++++++++++++ src/messaging/models.py | 63 +++++++- src/messaging/states.py | 36 +++-- 7 files changed, 348 insertions(+), 42 deletions(-) create mode 100644 src/messaging/migrations/0003_auto_20200128_2230.py diff --git a/src/club/models.py b/src/club/models.py index 8c26d03a1..f36aada20 100644 --- a/src/club/models.py +++ b/src/club/models.py @@ -2,6 +2,7 @@ # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # from datetime import datetime, timedelta +from django.apps import apps from django.conf import settings from django.contrib.sites.models import Site from django.core.mail import send_mail @@ -63,7 +64,8 @@ class Schedule(models.Model): def save(self, *args, **kwargs): if not self.key: self.key = get_random_hash(self.email) - return super(Schedule, self).save(*args, **kwargs) + super(Schedule, self).save(*args, **kwargs) + self.update_contact() def initiate_payment(self, request): return self.get_payment_method().initiate(request, self) @@ -108,6 +110,19 @@ class Schedule(models.Model): self.email_sent = True self.save() + def update_contact(self): + Contact = apps.get_model('messaging', 'Contact') + if not self.payed_at: + level = Contact.TRIED + since = self.started_at + else: + since = self.payed_at + if self.is_recurring(): + level = Contact.RECURRING + else: + level = Contact.SINGLE + Contact.update(self.email, level, since, self.expires_at) + class Membership(models.Model): """ Represents a user being recognized as a member of the club. """ diff --git a/src/messaging/admin.py b/src/messaging/admin.py index 1b2bac599..854079bee 100644 --- a/src/messaging/admin.py +++ b/src/messaging/admin.py @@ -1,9 +1,10 @@ from django.contrib import admin -from .models import EmailTemplate, EmailSent +from django.utils.translation import ugettext_lazy as _ +from . import models class EmailSentInline(admin.TabularInline): - model = EmailSent + model = models.EmailSent fields = ['timestamp', 'email', 'subject'] readonly_fields = ['timestamp', 'email', 'subject'] extra = 0 @@ -15,11 +16,26 @@ class EmailSentInline(admin.TabularInline): class EmailTemplateAdmin(admin.ModelAdmin): - list_display = ['state', 'days', 'subject', 'hour'] + list_display = ['state', 'min_days_since', 'subject', 'min_hour'] inlines = [EmailSentInline] - - -admin.site.register(EmailTemplate, EmailTemplateAdmin) + fieldsets = [ + (None, {"fields": [ + 'state', + ('min_days_since', 'max_days_since'), + 'is_active', + ]}), + (_('E-mail content'), {"fields": [ + 'subject', 'body' + ]}), + (_('Sending constraints'), {"fields": [ + ('min_day_of_month', 'max_day_of_month'), + ('dow_1', 'dow_2', 'dow_3', 'dow_4', 'dow_5', 'dow_6', 'dow_7'), + ('min_hour', 'max_hour'), + ]}), + ] + + +admin.site.register(models.EmailTemplate, EmailTemplateAdmin) class EmailSentAdmin(admin.ModelAdmin): @@ -30,5 +46,14 @@ class EmailSentAdmin(admin.ModelAdmin): change_links = ['template'] -admin.site.register(EmailSent, EmailSentAdmin) +admin.site.register(models.EmailSent, EmailSentAdmin) + + +class ContactAdmin(admin.ModelAdmin): + list_filter = ['level'] + list_display = ['email', 'level', 'since', 'expires_at'] + search_fields = ['email'] + date_hierarchy = 'since' + +admin.site.register(models.Contact, ContactAdmin) diff --git a/src/messaging/locale/pl/LC_MESSAGES/django.mo b/src/messaging/locale/pl/LC_MESSAGES/django.mo index da52dab18c0b644eca0e03f94c567e556f85e3e6..a53c4b67d92ac691dfed4eb8eacb2b516e96fc41 100644 GIT binary patch literal 2882 zcmbW1O^g&p6vqoi(Gf(!4-`KNWXY~<@6HSmahMqtVGWUW*WK|0FQlfscD8qVs>ZHr zh8`fnjT?+tLo~sda4;s`jAA@+bH|$=Jm8IkCUP<{+>96hue*n3$dV|PuKrcMdi7q_ zt5%g<% zM({)MKJZg;Gk6iCd6zQzWsvlH;QioFAldm9B)vaCeC$vB9s#d|yTEn#F!m_88>Idb zkj5i02Yv+7{PS7=Gmz|l3DUa010Mi?1Rnx_$;N*NX}#Ao`9GO_J&cijD+m$W0n)mk z2fsyJirM%z2sfku7ZKxYu_@Bq0&V~~xE&nJ3bkCFN=SDUMVgPm#ohV)AU( zr+84zsZcI$p~fpZ@bFVsp0uTMJ*?($x1OO6rxwYCZ%a>QEgc1(Q}25buJunyR>zi^8$@-k z2UcNL74nX6=V_TqNVq)N_Gy8sZaBE!%)+hG)l5h)wa^(@A`Bprcq7zNo5>`AOeSG8 z6vMP>Wvd+sD{twVv~PH#+7c%@g2MG2Z{Y;ySth`+uUwgQ=XDga77}GpF%{fUL^n}1 zND9eJnnY|m)RctI5zl!`h6dSE;<*{w)}eJKO~dz`gHgjcH4X9flrtkw_@v|!yB*w}af|k;%g= z&I@DtTyg;wY{YFip}RPaNcn0%cjrZDq^-PJn{f86=#j5E89Ij*?jlaL#P`*Go0|@z zPz26|4qK+gRXgdL%AWmjOCyzGRjCyC0iNe2KCFfc`D&%eckblWELZl7P&<+5%Y`C8 zeL5M)R|^<9zzdk9wqbr>sanaeOfHO&er9y+mmBQwfDm}ou*)~(WNwZ@>1!#FPoOzxE=kUH*4Urn$*3^(sX&$ zyBsf{;|Y&4QX!EH_1;0q3m5)K9`6_c~gho7|FP>yyWt3@>sbGK3dIC zx0}mLqd2g!J9w|&6&(>;O`dpq^#p7C%S%=^Sz9Y#dR<@i7STH-Lbn_DK5zxRF5tD% zq3G()@)GZ~dyB#@FVTbM(1SLh+8^a^+zfoTtDtCf9j`5$Y9JLMlFApn6PviV*z{TI zR0e{%U0NJ`cW^O)-i1(mg1wGaY18d3#<;`%MKRH|aYx0>rg398hT_|1mG!`+CSi#qb;p%l-u< CyZ?>= delta 700 zcmZ9|zi-n(6bJAZlBP)^v=GD(f#5(el!*XR5d%v%<}NIVi+!Z0#CMi`!8tFfDynV> z$z>}11+cXn8%nm0tgs*?goNnK#P_8}NIm)abH01;e)!Y*!`Arg+1eXITS8nxyhU8# zeg!YI5o{qmqK@zF6GS!m3SNW-p8I5|3A~D%*rN;2F3K@52s!3QQ= z=QHEUC2G1m*`^?$Zd%i~smrX$#G1`yl6!ncaBLANh=*gA{e1D*W+)TFG$4f$^i+2l;+dUiR+DJ;BG~`qy9sLCrpObn3 diff --git a/src/messaging/locale/pl/LC_MESSAGES/django.po b/src/messaging/locale/pl/LC_MESSAGES/django.po index b3d5a2d4b..e58763d06 100644 --- a/src/messaging/locale/pl/LC_MESSAGES/django.po +++ b/src/messaging/locale/pl/LC_MESSAGES/django.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-01-17 13:27+0100\n" -"PO-Revision-Date: 2020-01-17 13:27+0100\n" +"POT-Creation-Date: 2020-01-28 22:49+0100\n" +"PO-Revision-Date: 2020-01-28 22:51+0100\n" "Last-Translator: \n" "Language-Team: \n" "Language: pl\n" @@ -20,61 +20,145 @@ msgstr "" "%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n" "X-Generator: Poedit 2.2.4\n" +#: admin.py:27 +msgid "E-mail content" +msgstr "Zawartość e-maila" + +#: admin.py:30 +msgid "Sending constraints" +msgstr "Ograniczenia wysyłki" + #: models.py:11 msgid "state" msgstr "stan" -#: models.py:12 models.py:69 +#: models.py:12 models.py:79 msgid "subject" msgstr "temat" -#: models.py:13 models.py:70 +#: models.py:13 models.py:80 msgid "body" msgstr "treść" #: models.py:14 -msgid "days" -msgstr "dni" +msgid "min days since" +msgstr "dni po, od" #: models.py:15 -msgid "hour" -msgstr "godzina" +msgid "max days since" +msgstr "dni po, do" #: models.py:16 +msgid "max hour" +msgstr "do godziny" + +#: models.py:17 +msgid "min hour" +msgstr "od godziny" + +#: models.py:18 +msgid "min day of month" +msgstr "od dnia miesiąca" + +#: models.py:19 +msgid "max day of month" +msgstr "do dnia miesiąca" + +#: models.py:20 +msgid "Monday" +msgstr "poniedziałek" + +#: models.py:21 +msgid "Tuesday" +msgstr "wtorek" + +#: models.py:22 +msgid "Wednesday" +msgstr "środa" + +#: models.py:23 +msgid "Thursday" +msgstr "czwartek" + +#: models.py:24 +msgid "Friday" +msgstr "piątek" + +#: models.py:25 +msgid "Saturday" +msgstr "sobota" + +#: models.py:26 +msgid "Sunday" +msgstr "niedziela" + +#: models.py:27 msgid "active" msgstr "aktywny" -#: models.py:19 +#: models.py:30 msgid "email template" msgstr "szablon e-maila" -#: models.py:20 +#: models.py:31 msgid "email templates" msgstr "szablony e-maili" -#: models.py:68 +#: models.py:78 msgid "e-mail" msgstr "e-mail" -#: models.py:73 +#: models.py:83 msgid "email sent" msgstr "wysłany e-mail" -#: models.py:74 +#: models.py:84 msgid "emails sent" msgstr "wysłane e-maile" -#: states.py:47 -msgid "club membership expiring" -msgstr "członkostwo w Towarzystwie wygasa" +#: models.py:101 +msgid "Would-be donor" +msgstr "Niedoszły darczyńca" + +#: models.py:102 +msgid "One-time donor" +msgstr "Darczyńca z jednorazową wpłatą" + +#: models.py:103 +msgid "Recurring donor" +msgstr "Darczyńca z wpłatą cykliczną" + +#: models.py:104 +msgid "Cold" +msgstr "Lodówka" + +#: models.py:105 +msgid "Opt out" +msgstr "Opt-out" -#: states.py:62 -msgid "club payment unfinished" -msgstr "niedokończona płatność w Towarzystwie" +#: states.py:46 +msgid "club one-time donors" +msgstr "darczyńcy z jednorazową wpłatą" -#: states.py:74 -msgid "club recurring payment problem" -msgstr "problem z płatnością cykliczną w Towarzystwie" +#: states.py:52 +msgid "club one-time donors with donation expiring" +msgstr "darczyńcy z wygasającą jednorazową wpłatą" + +#: states.py:67 +msgid "club would-be donors" +msgstr "niedoszli darczyńcy" + +#: states.py:79 +msgid "club recurring donors" +msgstr "darczyńcy z wpłatą cykluczną" + +#: states.py:84 +msgid "club recurring donors with donation expired" +msgstr "darczyńcy z wygasającą wpłatą cykliczną" + +#: states.py:93 +msgid "cold group" +msgstr "lodówka" #: views.py:19 #, python-format @@ -93,6 +177,12 @@ msgstr "" "z notacji z kropką, np.:
\n" " {{ %(model_name)s.id }}." +#~ msgid "days" +#~ msgstr "dni" + +#~ msgid "club payment unfinished" +#~ msgstr "niedokończona płatność w Towarzystwie" + #~ msgid "New member" #~ msgstr "Nowy członek" diff --git a/src/messaging/migrations/0003_auto_20200128_2230.py b/src/messaging/migrations/0003_auto_20200128_2230.py new file mode 100644 index 000000000..431566911 --- /dev/null +++ b/src/messaging/migrations/0003_auto_20200128_2230.py @@ -0,0 +1,101 @@ +# Generated by Django 2.2.9 on 2020-01-28 21:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('messaging', '0002_auto_20200117_1326'), + ] + + operations = [ + migrations.CreateModel( + name='Contact', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('email', models.EmailField(max_length=254, unique=True)), + ('level', models.PositiveSmallIntegerField(choices=[(20, 'Tried'), (30, 'Single'), (40, 'Recurring'), (10, 'Cold'), (50, 'Opt out')])), + ('since', models.DateTimeField()), + ('expires_at', models.DateTimeField(blank=True, null=True)), + ], + ), + migrations.RemoveField( + model_name='emailtemplate', + name='days', + ), + migrations.RemoveField( + model_name='emailtemplate', + name='hour', + ), + migrations.AddField( + model_name='emailtemplate', + name='dow_1', + field=models.BooleanField(default=True, verbose_name='monday'), + ), + migrations.AddField( + model_name='emailtemplate', + name='dow_2', + field=models.BooleanField(default=True, verbose_name='tuesday'), + ), + migrations.AddField( + model_name='emailtemplate', + name='dow_3', + field=models.BooleanField(default=True, verbose_name='wednesday'), + ), + migrations.AddField( + model_name='emailtemplate', + name='dow_4', + field=models.BooleanField(default=True, verbose_name='thursday'), + ), + migrations.AddField( + model_name='emailtemplate', + name='dow_5', + field=models.BooleanField(default=True, verbose_name='friday'), + ), + migrations.AddField( + model_name='emailtemplate', + name='dow_6', + field=models.BooleanField(default=True, verbose_name='saturday'), + ), + migrations.AddField( + model_name='emailtemplate', + name='dow_7', + field=models.BooleanField(default=True, verbose_name='sunday'), + ), + migrations.AddField( + model_name='emailtemplate', + name='max_day_of_month', + field=models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='max day of month'), + ), + migrations.AddField( + model_name='emailtemplate', + name='max_days_since', + field=models.SmallIntegerField(blank=True, null=True, verbose_name='max_days_since'), + ), + migrations.AddField( + model_name='emailtemplate', + name='max_hour', + field=models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='max hour'), + ), + migrations.AddField( + model_name='emailtemplate', + name='min_day_of_month', + field=models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='min day of month'), + ), + migrations.AddField( + model_name='emailtemplate', + name='min_days_since', + field=models.SmallIntegerField(blank=True, null=True, verbose_name='min_days_since'), + ), + migrations.AddField( + model_name='emailtemplate', + name='min_hour', + field=models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='min hour'), + ), + migrations.AlterField( + model_name='emailtemplate', + name='state', + field=models.CharField(choices=[('cold', 'cold group'), ('club-payment-unfinished', 'club would-be donor'), ('club-single', 'club one-time donors'), ('club-membership-expiring', 'club one-time donors with donation expiring'), ('club-recurring', 'club recurring donor'), ('club-recurring-payment-problem', 'club recurring donors with donation expired')], help_text='?', max_length=128, verbose_name='state'), + ), + ] diff --git a/src/messaging/models.py b/src/messaging/models.py index c40abbbd9..a8c481ff9 100644 --- a/src/messaging/models.py +++ b/src/messaging/models.py @@ -11,8 +11,19 @@ class EmailTemplate(models.Model): state = models.CharField(_('state'), max_length=128, choices=[(s.slug, s.name) for s in states], help_text='?') subject = models.CharField(_('subject'), max_length=1024) body = models.TextField(_('body')) - days = models.SmallIntegerField(_('days'), null=True, blank=True) - hour = models.IntegerField(_('hour'), null=True, blank=True) + min_days_since = models.SmallIntegerField(_('min days since'), null=True, blank=True) + max_days_since = models.SmallIntegerField(_('max days since'), null=True, blank=True) + min_hour = models.PositiveSmallIntegerField(_('min hour'), null=True, blank=True) + max_hour = models.PositiveSmallIntegerField(_('max hour'), null=True, blank=True) + min_day_of_month = models.PositiveSmallIntegerField(_('min day of month'), null=True, blank=True) + max_day_of_month = models.PositiveSmallIntegerField(_('max day of month'), null=True, blank=True) + dow_1 = models.BooleanField(_('Monday'), default=True) + dow_2 = models.BooleanField(_('Tuesday'), default=True) + dow_3 = models.BooleanField(_('Wednesday'), default=True) + dow_4 = models.BooleanField(_('Thursday'), default=True) + dow_5 = models.BooleanField(_('Friday'), default=True) + dow_6 = models.BooleanField(_('Saturday'), default=True) + dow_7 = models.BooleanField(_('Sunday'), default=True) is_active = models.BooleanField(_('active'), default=False) class Meta: @@ -60,7 +71,6 @@ class EmailTemplate(models.Model): ) - class EmailSent(models.Model): template = models.ForeignKey(EmailTemplate, models.CASCADE) hash_value = models.CharField(max_length=1024) @@ -76,3 +86,50 @@ class EmailSent(models.Model): def __str__(self): return '%s %s' % (self.email, self.timestamp) + + +class Contact(models.Model): + COLD = 10 + TRIED = 20 + SINGLE = 30 + RECURRING = 40 + OPT_OUT = 50 + + email = models.EmailField(unique=True) + level = models.PositiveSmallIntegerField( + choices=[ + (TRIED, _('Would-be donor')), + (SINGLE, _('One-time donor')), + (RECURRING, _('Recurring donor')), + (COLD, _('Cold')), + (OPT_OUT, _('Opt out')), + ]) + since = models.DateTimeField() + expires_at = models.DateTimeField(null=True, blank=True) + + @classmethod + def update(cls, email, level, since, expires_at=None): + obj, created = cls.objects.get_or_create(email=email, defaults={ + "level": level, + "since": since, + "expires_at": expires_at + }) + if not created: + obj.ascend(level, since, expires_at) + + def ascend(self, level, since, expires_at=None): + if level < self.level: + return + if level == self.level: + self.since = min(since, self.since) + + if expires_at and self.expires_at: + self.expires_at = max(expires_at, self.expires_at) + else: + self.expires_at = expires_at + else: + self.level = level + self.since = since + self.expires_at = expires_at + self.save() + diff --git a/src/messaging/states.py b/src/messaging/states.py index 0bb63b442..36b5d05f1 100644 --- a/src/messaging/states.py +++ b/src/messaging/states.py @@ -41,10 +41,15 @@ class State: return ctx -class ClubMembershipExpiring(State): +class ClubSingle(State): + slug = 'club-single' + name = _('club one-time donors') + + +class ClubSingleExpired(State): slug = 'club-membership-expiring' allow_negative_offset = True - name = _('club membership expiring') + name = _('club one-time donors with donation expiring') def get_objects(self): from club.models import Schedule @@ -57,9 +62,9 @@ class ClubMembershipExpiring(State): return '%s:%s' % (obj.pk, obj.expires_at.isoformat()) -class ClubPaymentUnfinished(State): +class ClubTried(State): slug = 'club-payment-unfinished' - name = _('club payment unfinished') + name = _('club would-be donors') def get_objects(self): from club.models import Schedule @@ -69,18 +74,31 @@ class ClubPaymentUnfinished(State): ) -class ClubRecurringPaymentProblem(State): +class ClubRecurring(State): + slug = 'club-recurring' + name = _('club recurring donors') + + +class ClubRecurringExpired(State): slug = 'club-recurring-payment-problem' - name = _('club recurring payment problem') + name = _('club recurring donors with donation expired') def get_objects(self): from club.models import Schedule return Schedule.objects.none() +class Cold(State): + slug = 'cold' + name = _('cold group') + + states = [ - ClubMembershipExpiring, - ClubPaymentUnfinished, - ClubRecurringPaymentProblem, + Cold, + ClubTried, + ClubSingle, + ClubSingleExpired, + ClubRecurring, + ClubRecurringExpired, ] -- 2.20.1