Contact management.
authorRadek Czajka <rczajka@rczajka.pl>
Wed, 29 Jan 2020 08:12:17 +0000 (09:12 +0100)
committerRadek Czajka <rczajka@rczajka.pl>
Wed, 29 Jan 2020 08:12:17 +0000 (09:12 +0100)
src/club/models.py
src/messaging/admin.py
src/messaging/locale/pl/LC_MESSAGES/django.mo
src/messaging/locale/pl/LC_MESSAGES/django.po
src/messaging/migrations/0003_auto_20200128_2230.py [new file with mode: 0644]
src/messaging/models.py
src/messaging/states.py

index 8c26d03..f36aada 100644 (file)
@@ -2,6 +2,7 @@
 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 #
 from datetime import datetime, timedelta
 # 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
 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)
     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)
 
     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()
 
         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. """
 
 class Membership(models.Model):
     """ Represents a user being recognized as a member of the club. """
index 1b2bac5..854079b 100644 (file)
@@ -1,9 +1,10 @@
 from django.contrib import admin
 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):
 
 
 class EmailSentInline(admin.TabularInline):
-    model = EmailSent
+    model = models.EmailSent
     fields = ['timestamp', 'email', 'subject']
     readonly_fields = ['timestamp', 'email', 'subject']
     extra = 0
     fields = ['timestamp', 'email', 'subject']
     readonly_fields = ['timestamp', 'email', 'subject']
     extra = 0
@@ -15,11 +16,26 @@ class EmailSentInline(admin.TabularInline):
 
 
 class EmailTemplateAdmin(admin.ModelAdmin):
 
 
 class EmailTemplateAdmin(admin.ModelAdmin):
-    list_display = ['state', 'days', 'subject', 'hour']
+    list_display = ['state', 'min_days_since', 'subject', 'min_hour']
     inlines = [EmailSentInline]
     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):
 
 
 class EmailSentAdmin(admin.ModelAdmin):
@@ -30,5 +46,14 @@ class EmailSentAdmin(admin.ModelAdmin):
     change_links = ['template']
 
 
     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)
index da52dab..a53c4b6 100644 (file)
Binary files a/src/messaging/locale/pl/LC_MESSAGES/django.mo and b/src/messaging/locale/pl/LC_MESSAGES/django.mo differ
index b3d5a2d..e58763d 100644 (file)
@@ -7,8 +7,8 @@ msgid ""
 msgstr ""
 "Project-Id-Version: \n"
 "Report-Msgid-Bugs-To: \n"
 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"
 "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"
 
 "%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:11
 msgid "state"
 msgstr "stan"
 
-#: models.py:12 models.py:69
+#: models.py:12 models.py:79
 msgid "subject"
 msgstr "temat"
 
 msgid "subject"
 msgstr "temat"
 
-#: models.py:13 models.py:70
+#: models.py:13 models.py:80
 msgid "body"
 msgstr "treść"
 
 #: models.py:14
 msgid "body"
 msgstr "treść"
 
 #: models.py:14
-msgid "days"
-msgstr "dni"
+msgid "min days since"
+msgstr "dni po, od"
 
 #: models.py:15
 
 #: models.py:15
-msgid "hour"
-msgstr "godzina"
+msgid "max days since"
+msgstr "dni po, do"
 
 #: models.py:16
 
 #: 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"
 
 msgid "active"
 msgstr "aktywny"
 
-#: models.py:19
+#: models.py:30
 msgid "email template"
 msgstr "szablon e-maila"
 
 msgid "email template"
 msgstr "szablon e-maila"
 
-#: models.py:20
+#: models.py:31
 msgid "email templates"
 msgstr "szablony e-maili"
 
 msgid "email templates"
 msgstr "szablony e-maili"
 
-#: models.py:68
+#: models.py:78
 msgid "e-mail"
 msgstr "e-mail"
 
 msgid "e-mail"
 msgstr "e-mail"
 
-#: models.py:73
+#: models.py:83
 msgid "email sent"
 msgstr "wysłany e-mail"
 
 msgid "email sent"
 msgstr "wysłany e-mail"
 
-#: models.py:74
+#: models.py:84
 msgid "emails sent"
 msgstr "wysłane e-maile"
 
 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
 
 #: views.py:19
 #, python-format
@@ -93,6 +177,12 @@ msgstr ""
 "z notacji z kropką, np.:<br>\n"
 "       <code>{{ %(model_name)s.id }}</code>."
 
 "z notacji z kropką, np.:<br>\n"
 "       <code>{{ %(model_name)s.id }}</code>."
 
+#~ msgid "days"
+#~ msgstr "dni"
+
+#~ msgid "club payment unfinished"
+#~ msgstr "niedokończona płatność w Towarzystwie"
+
 #~ msgid "New member"
 #~ msgstr "Nowy członek"
 
 #~ 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 (file)
index 0000000..4315669
--- /dev/null
@@ -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'),
+        ),
+    ]
index c40abbb..a8c481f 100644 (file)
@@ -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'))
     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:
     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)
 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)
 
     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()
+
index 0bb63b4..36b5d05 100644 (file)
@@ -41,10 +41,15 @@ class State:
         return ctx
 
 
         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
     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
 
     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())
 
 
         return '%s:%s' % (obj.pk, obj.expires_at.isoformat())
 
 
-class ClubPaymentUnfinished(State):
+class ClubTried(State):
     slug = 'club-payment-unfinished'
     slug = 'club-payment-unfinished'
-    name = _('club payment unfinished')
+    name = _('club would-be donors')
 
     def get_objects(self):
         from club.models import Schedule
 
     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'
     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()
 
 
 
     def get_objects(self):
         from club.models import Schedule
         return Schedule.objects.none()
 
 
+class Cold(State):
+    slug = 'cold'
+    name = _('cold group')
+
+
 states = [
 states = [
-    ClubMembershipExpiring,
-    ClubPaymentUnfinished,
-    ClubRecurringPaymentProblem,
+    Cold,
+    ClubTried,
+    ClubSingle,
+    ClubSingleExpired,
+    ClubRecurring,
+    ClubRecurringExpired,
 ]
 
 ]