# django
Django==2.2.5
fnpdjango==0.4
+docutils
#django-pipeline==1.6.13
# Because of https://github.com/jazzband/django-pipeline/pull/682 + https://github.com/getsentry/sentry-python/issues/436
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2019-03-04 11:36+0100\n"
-"PO-Revision-Date: 2019-06-17 15:08+0200\n"
+"POT-Creation-Date: 2019-09-30 15:58+0200\n"
+"PO-Revision-Date: 2019-09-30 16:09+0200\n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: pl\n"
"%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n"
"X-Generator: Poedit 2.0.6\n"
-#: models.py:25
+#: models.py:24
msgid "a month"
msgstr "miesięcznie"
-#: models.py:26
+#: models.py:25
msgid "a year"
msgstr "raz do roku"
-#: models.py:27
+#: models.py:26
msgid "in perpetuity"
msgstr "raz na zawsze"
-#: models.py:30
+#: models.py:29
msgid "inteval"
msgstr "okres"
-#: models.py:31
-msgid "min_amount"
+#: models.py:30
+msgid "min amount"
msgstr "minimalna kwota"
+#: models.py:31
+msgid "default amount"
+msgstr "domyślna kwota"
+
#: models.py:32
msgid "allow recurring"
msgstr "płatności cykliczne"
msgid "allow one time"
msgstr "płatności jednorazowe"
-#: models.py:36 models.py:69
+#: models.py:34
+msgid "active"
+msgstr "aktywny"
+
+#: models.py:37 models.py:69
msgid "plan"
msgstr "plan"
-#: models.py:37
+#: models.py:38
msgid "plans"
msgstr "plany"
msgid "email"
msgstr "email"
-#: models.py:68 models.py:123
+#: models.py:68 models.py:129
msgid "membership"
msgstr "członkostwo"
msgstr "metoda płatności"
#: models.py:72
-msgid "active"
-msgstr "aktywny"
-
-#: models.py:73
msgid "cancelled"
msgstr "anulowany"
+#: models.py:73
+msgid "payed at"
+msgstr "opłacona"
+
#: models.py:74
msgid "started at"
msgstr "start"
msgid "expires_at"
msgstr "wygasa"
-#: models.py:79 models.py:105
+#: models.py:79
msgid "schedule"
msgstr "harmonogram"
msgid "schedules"
msgstr "harmonogramy"
-#: models.py:106 models.py:120
-msgid "created at"
-msgstr "utworzone"
-
-#: models.py:107
-msgid "payed at"
-msgstr "opłacona"
-
-#: models.py:110
-msgid "payment"
-msgstr "płatność"
-
-#: models.py:111
-msgid "payments"
-msgstr "płatności"
-
-#: models.py:119
+#: models.py:123
msgid "user"
msgstr "użytkownik"
#: models.py:124
+msgid "created at"
+msgstr "utworzone"
+
+#: models.py:130
msgid "memberships"
msgstr "członkostwa"
-#: models.py:131
+#: models.py:152
msgid "days before"
msgstr "dni przed"
-#: models.py:132
+#: models.py:153
msgid "subject"
msgstr "temat"
-#: models.py:133
+#: models.py:154 payu/models.py:136
msgid "body"
msgstr "treść"
-#: models.py:136
+#: models.py:157
msgid "reminder email"
msgstr "email z przypomnieniem"
-#: models.py:137
+#: models.py:158
msgid "reminder emails"
msgstr "emaile z przypomnieniem"
-#: models.py:142
+#: models.py:163
#, python-format
msgid "a day before expiration"
msgid_plural "%d days before expiration"
msgstr[2] "%d dni przed wygaśnięciem"
msgstr[3] "%d dni przed wygaśnięciem"
-#: models.py:144
+#: models.py:165
#, python-format
msgid "a day after expiration"
msgid_plural "%d days after expiration"
msgstr[1] "%d dni po wygaśnięciu"
msgstr[2] "%d dni po wygaśnięciu"
msgstr[3] "%d dni przed wygaśnięciem"
+
+#: models.py:192
+msgid "Towarzystwo Wolnych Lektur"
+msgstr ""
+
+#: payu/models.py:16 payu/models.py:28
+msgid "POS id"
+msgstr ""
+
+#: payu/models.py:17
+msgid "disposable token"
+msgstr "token jednorazowy"
+
+#: payu/models.py:18
+msgid "reusable token"
+msgstr "token wielokrotnego użytku"
+
+#: payu/models.py:19
+msgid "created_at"
+msgstr "utworzone"
+
+#: payu/models.py:23
+msgid "PayU card token"
+msgstr "token PayU karty płatniczej"
+
+#: payu/models.py:24
+msgid "PayU card tokens"
+msgstr "tokeny PayU kart płatniczych"
+
+#: payu/models.py:29
+msgid "customer IP"
+msgstr "adres IP klienta"
+
+#: payu/models.py:30
+msgid "order ID"
+msgstr "ID zamówienia"
+
+#: payu/models.py:33
+msgid "Pending"
+msgstr "Czeka"
+
+#: payu/models.py:34
+msgid "Waiting for confirmation"
+msgstr "Czeka na potwierdzenie"
+
+#: payu/models.py:35
+msgid "Completed"
+msgstr "Ukończone"
+
+#: payu/models.py:36
+msgid "Canceled"
+msgstr "Anulowane"
+
+#: payu/models.py:37
+msgid "Rejected"
+msgstr "Odrzucone"
+
+#: payu/models.py:42
+msgid "PayU order"
+msgstr "zamówienie PayU"
+
+#: payu/models.py:43
+msgid "PayU orders"
+msgstr "zamówienia PayU"
+
+#: payu/models.py:137
+msgid "received_at"
+msgstr "odebrana"
+
+#: payu/models.py:141
+msgid "PayU notification"
+msgstr "notyfikacja PayU"
+
+#: payu/models.py:142
+msgid "PayU notifications"
+msgstr "notyfikacje PayU"
+
+#~ msgid "payment"
+#~ msgstr "płatność"
+
+#~ msgid "payments"
+#~ msgstr "płatności"
--- /dev/null
+from django.contrib import admin
+from .models import EmailTemplate, EmailSent
+
+
+class EmailSentInline(admin.TabularInline):
+ model = EmailSent
+ fields = ['timestamp', 'email', 'subject']
+ readonly_fields = ['timestamp', 'email', 'subject']
+ extra = 0
+ can_delete = False
+ show_change_link = True
+
+ def has_add_permission(self, request, obj):
+ return False
+
+
+class EmailTemplateAdmin(admin.ModelAdmin):
+ list_display = ['state', 'days', 'subject', 'hour']
+ inlines = [EmailSentInline]
+
+
+admin.site.register(EmailTemplate, EmailTemplateAdmin)
+
+
+class EmailSentAdmin(admin.ModelAdmin):
+ list_filter = ['template']
+ list_display = ['timestamp', 'template', 'email', 'subject']
+ fields = ['timestamp', 'template', 'email', 'subject', 'body', 'hash_value']
+ readonly_fields = fields
+ change_links = ['template']
+
+
+admin.site.register(EmailSent, EmailSentAdmin)
+
--- /dev/null
+from django.apps import AppConfig
+
+
+class MessagingConfig(AppConfig):
+ name = 'messaging'
--- /dev/null
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: \n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2019-09-30 16:09+0200\n"
+"PO-Revision-Date: 2019-09-30 16:10+0200\n"
+"Last-Translator: \n"
+"Language-Team: \n"
+"Language: pl\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n"
+"%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n"
+"%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n"
+"X-Generator: Poedit 2.0.6\n"
+
+#: models.py:11
+msgid "state"
+msgstr "stan"
+
+#: models.py:12 models.py:68
+msgid "subject"
+msgstr "temat"
+
+#: models.py:13 models.py:69
+msgid "body"
+msgstr "treść"
+
+#: models.py:14
+msgid "days"
+msgstr "dni"
+
+#: models.py:15
+msgid "hour"
+msgstr "godzina"
+
+#: models.py:18
+msgid "email template"
+msgstr "szablon e-maila"
+
+#: models.py:19
+msgid "email templates"
+msgstr "szablony e-maili"
+
+#: models.py:67
+msgid "e-mail"
+msgstr "e-mail"
+
+#: models.py:72
+msgid "email sent"
+msgstr "wysłany e-mail"
+
+#: models.py:73
+msgid "emails sent"
+msgstr "wysłane e-maile"
+
+#: states.py:47
+msgid "club membership expiring"
+msgstr "członkostwo w Towarzystwie wygasa"
+
+#: states.py:61
+msgid "club payment unfinished"
+msgstr "niedokończona płatność w Towarzystwie"
+
+#: views.py:19
+#, python-format
+msgid ""
+"Context:<br>\n"
+" <code>{{ %(model_name)s }}</code> – a <a href=\"%(docs_url)s\">"
+"%(verbose_name)s</a> object.<br>\n"
+" You can put it in in the fields <em>Subject</em> and <em>Body</em> "
+"using dot notation, like this:<br>\n"
+" <code>{{ %(model_name)s.id }}</code>."
+msgstr ""
+"Kontest:<br>\n"
+" <code>{{ %(model_name)s }}</code> – obiekt typu <a href=\"%(docs_url)s"
+"\">%(verbose_name)s</a>.<br>\n"
+" Możesz użyć go w polach <em>Temat</em> i <em>Treść</em>, korzystając "
+"z notacji z kropką, np.:<br>\n"
+" <code>{{ %(model_name)s.id }}</code>."
+
+#~ msgid "New member"
+#~ msgstr "Nowy członek"
+
+#~ msgid "event"
+#~ msgstr "zdarzenie"
+
+#~ msgid "Select an event to see more details."
+#~ msgstr "Wybierz zdarzenie by zobaczyć więcej szczegółów."
--- /dev/null
+from django.core.management.base import BaseCommand, CommandError
+from messaging.models import EmailTemplate
+
+
+class Command(BaseCommand):
+ help = 'Send emails defined in templates.'
+
+ def add_arguments(self, parser):
+ parser.add_argument('--dry-run', action='store_true', help='Dry run')
+
+ def handle(self, *args, **options):
+ for et in EmailTemplate.objects.all():
+ et.run(verbose=True, dry_run=options['dry_run'])
+
--- /dev/null
+# Generated by Django 2.2.5 on 2019-09-30 13:11
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='EmailTemplate',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('state', models.CharField(choices=[('club-membership-expiring', 'club membership expiring'), ('club-payment-unfinished', 'club payment unfinished')], help_text='?', max_length=128, verbose_name='state')),
+ ('subject', models.CharField(max_length=1024, verbose_name='subject')),
+ ('body', models.TextField(verbose_name='body')),
+ ('days', models.SmallIntegerField(blank=True, null=True, verbose_name='days')),
+ ('hour', models.IntegerField(blank=True, null=True, verbose_name='hour')),
+ ],
+ options={
+ 'verbose_name': 'email template',
+ 'verbose_name_plural': 'email templates',
+ },
+ ),
+ migrations.CreateModel(
+ name='EmailSent',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('hash_value', models.CharField(max_length=1024)),
+ ('timestamp', models.DateTimeField(auto_now_add=True)),
+ ('email', models.CharField(max_length=1024, verbose_name='e-mail')),
+ ('subject', models.CharField(max_length=1024, verbose_name='subject')),
+ ('body', models.TextField(verbose_name='body')),
+ ('template', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='messaging.EmailTemplate')),
+ ],
+ options={
+ 'verbose_name': 'email sent',
+ 'verbose_name_plural': 'emails sent',
+ 'ordering': ('-timestamp',),
+ },
+ ),
+ ]
--- /dev/null
+from django.conf import settings
+from django.core.mail import send_mail
+from django.db import models
+from django.template import Template, Context
+from django.utils.translation import ugettext_lazy as _
+from sentry_sdk import capture_exception
+from .states import states
+
+
+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)
+
+ class Meta:
+ verbose_name = _('email template')
+ verbose_name_plural = _('email templates')
+
+ def __str__(self):
+ return '%s (%+d)' % (self.get_state_display(), self.days)
+ return self.subject
+
+ def run(self, time=None, verbose=False, dry_run=False):
+ state = self.get_state()
+ recipients = state(time=time, offset=self.days).get_recipients()
+ hash_values = set(recipient.hash_value for recipient in recipients)
+ sent = set(EmailSent.objects.filter(
+ template=self, hash_value__in=hash_values
+ ).values_list('hash_value', flat=True))
+ for recipient in recipients:
+ if recipient.hash_value in sent:
+ continue
+ self.send(recipient, verbose=verbose, dry_run=dry_run)
+
+ def get_state(self):
+ for s in states:
+ if s.slug == self.state:
+ return s
+ raise ValueError('Unknown state', s.state)
+
+ def send(self, recipient, verbose=False, dry_run=False):
+ subject = Template(self.subject).render(Context(recipient.context))
+ body = Template(self.body).render(Context(recipient.context))
+ if verbose:
+ print(recipient.email, subject)
+ if not dry_run:
+ try:
+ send_mail(subject, body, settings.CONTACT_EMAIL, [recipient.email], fail_silently=False)
+ except:
+ capture_exception()
+ else:
+ self.emailsent_set.create(
+ hash_value=recipient.hash_value,
+ email=recipient.email,
+ subject=subject,
+ body=body,
+ )
+
+
+
+class EmailSent(models.Model):
+ template = models.ForeignKey(EmailTemplate, models.CASCADE)
+ hash_value = models.CharField(max_length=1024)
+ timestamp = models.DateTimeField(auto_now_add=True)
+ email = models.CharField(_('e-mail'), max_length=1024)
+ subject = models.CharField(_('subject'), max_length=1024)
+ body = models.TextField(_('body'))
+
+ class Meta:
+ verbose_name = _('email sent')
+ verbose_name_plural = _('emails sent')
+ ordering = ('-timestamp',)
+
+ def __str__(self):
+ return '%s %s' % (self.email, self.timestamp)
--- /dev/null
+class Recipient:
+ def __init__(self, email, hash_value, context):
+ self.email = email
+ self.hash_value = hash_value
+ self.context = context
+
--- /dev/null
+from datetime import timedelta
+from django.utils.timezone import now
+from django.utils.translation import ugettext_lazy as _
+from .recipient import Recipient
+
+
+class State:
+ allow_negative_offset = False
+ context_fields = []
+
+
+ def __init__(self, offset=0, time=None):
+ self.time = time or now()
+ if isinstance(offset, int):
+ offset = timedelta(offset)
+ self.offset = offset
+
+ def get_recipients(self):
+ return [
+ Recipient(
+ hash_value=self.get_hash_value(obj),
+ email=self.get_email(obj),
+ context=self.get_context(obj),
+ )
+ for obj in self.get_objects()
+ ]
+
+ def get_objects(self):
+ raise NotImplemented
+
+ def get_hash_value(self, obj):
+ return str(obj.pk)
+
+ def get_email(self, obj):
+ return obj.email
+
+ def get_context(self, obj):
+ ctx = {
+ obj._meta.model_name: obj,
+ }
+ return ctx
+
+
+class ClubMembershipExpiring(State):
+ slug = 'club-membership-expiring'
+ allow_negative_offset = True
+ name = _('club membership expiring')
+
+ def get_objects(self):
+ from club.models import Schedule
+ return Schedule.objects.filter(
+ is_active=True,
+ expires_at__lt=self.time - self.offset
+ )
+
+ def get_hashed_value(self, obj):
+ return '%s:%s' % (obj.pk, obj.expires_at.isoformat())
+
+class ClubPaymentUnfinished(State):
+ slug = 'club-payment-unfinished'
+ name = _('club payment unfinished')
+
+ def get_objects(self):
+ from club.models import Schedule
+ return Schedule.objects.filter(
+ payuorder=None,
+ started_at__lt=self.time - self.offset,
+ )
+
+
+states = [
+ ClubMembershipExpiring,
+ ClubPaymentUnfinished,
+]
+
--- /dev/null
+{% extends 'admin/change_form.html' %}
+
+
+{% block admin_change_form_document_ready %}
+{{ block.super }}
+
+ <script>
+(function($) {
+ $("#id_state").on('change', function() {
+ $this = $(this);
+ $.get("/messaging/states/" + $(this).val() + "/info.json", function(data) {
+ $(".help", $this.parent()).html(data.help);
+ }, "json");
+ });
+})(django.jQuery);
+
+ </script>
+
+{% endblock %}
--- /dev/null
+from django.urls import path
+from . import views
+
+
+urlpatterns = [
+ path('states/<slug>/info.json', views.state_info),
+]
--- /dev/null
+import json
+from django.http import JsonResponse
+from django.urls import reverse
+from django.shortcuts import render
+from django.utils.translation import ugettext as _
+from .states import states
+
+
+def state_info(request, slug):
+ for state in states:
+ if state.slug == slug:
+ break
+ else:
+ return JsonResponse({})
+
+ meta = state().get_objects().model._meta
+
+
+ help_text = _('''Context:<br>
+ <code>{{ %(model_name)s }}</code> – a <a href="%(docs_url)s">%(verbose_name)s</a> object.<br>
+ You can put it in in the fields <em>Subject</em> and <em>Body</em> using dot notation, like this:<br>
+ <code>{{ %(model_name)s.id }}</code>.''') % {
+ 'model_name': meta.model_name,
+ 'docs_url': reverse('django-admindocs-models-detail', args=(meta.app_label, meta.model_name)),
+ 'verbose_name': meta.verbose_name,
+ }
+
+ return JsonResponse({
+ "help": help_text,
+ })
+
'dictionary',
'infopages',
'lesmianator',
+ 'messaging',
'newtagging',
'opds',
'pdcounter',
url(r'^newsletter/', include('newsletter.urls')),
url(r'^formularz/', include('forms_builder.forms.urls')),
url(r'^isbn/', include('isbn.urls')),
+ url(r'^messaging/', include('messaging.urls')),
url(r'^paypal/app-form/$', RedirectView.as_view(
url='/towarzystwo/?app=1', permanent=False)),