Add messaging.
authorRadek Czajka <rczajka@rczajka.pl>
Mon, 30 Sep 2019 14:11:38 +0000 (16:11 +0200)
committerRadek Czajka <rczajka@rczajka.pl>
Mon, 30 Sep 2019 14:11:38 +0000 (16:11 +0200)
21 files changed:
requirements/requirements.txt
src/club/locale/pl/LC_MESSAGES/django.mo
src/club/locale/pl/LC_MESSAGES/django.po
src/messaging/__init__.py [new file with mode: 0644]
src/messaging/admin.py [new file with mode: 0644]
src/messaging/apps.py [new file with mode: 0644]
src/messaging/locale/pl/LC_MESSAGES/django.mo [new file with mode: 0644]
src/messaging/locale/pl/LC_MESSAGES/django.po [new file with mode: 0644]
src/messaging/management/__init__.py [new file with mode: 0644]
src/messaging/management/commands/__init__.py [new file with mode: 0644]
src/messaging/management/commands/messaging_send.py [new file with mode: 0644]
src/messaging/migrations/0001_initial.py [new file with mode: 0644]
src/messaging/migrations/__init__.py [new file with mode: 0644]
src/messaging/models.py [new file with mode: 0644]
src/messaging/recipient.py [new file with mode: 0644]
src/messaging/states.py [new file with mode: 0644]
src/messaging/templates/admin/messaging/emailtemplate/change_form.html [new file with mode: 0644]
src/messaging/urls.py [new file with mode: 0644]
src/messaging/views.py [new file with mode: 0644]
src/wolnelektury/settings/apps.py
src/wolnelektury/urls.py

index ad07ec9..4a9f0cc 100644 (file)
@@ -3,6 +3,7 @@
 # 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
index 921c37a..f57a55a 100644 (file)
Binary files a/src/club/locale/pl/LC_MESSAGES/django.mo and b/src/club/locale/pl/LC_MESSAGES/django.mo differ
index a631b73..6207cae 100644 (file)
@@ -7,8 +7,8 @@ msgid ""
 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"
@@ -20,26 +20,30 @@ msgstr ""
 "%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"
@@ -48,11 +52,15 @@ 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"
 
@@ -64,7 +72,7 @@ msgstr "klucz"
 msgid "email"
 msgstr "email"
 
-#: models.py:68 models.py:123
+#: models.py:68 models.py:129
 msgid "membership"
 msgstr "członkostwo"
 
@@ -77,13 +85,13 @@ msgid "method"
 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"
@@ -92,7 +100,7 @@ msgstr "start"
 msgid "expires_at"
 msgstr "wygasa"
 
-#: models.py:79 models.py:105
+#: models.py:79
 msgid "schedule"
 msgstr "harmonogram"
 
@@ -100,51 +108,39 @@ 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"
@@ -153,7 +149,7 @@ msgstr[1] "%d dni przed wygaśnięciem"
 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"
@@ -161,3 +157,85 @@ msgstr[0] "%d dzień po wygaśnięciu"
 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"
diff --git a/src/messaging/__init__.py b/src/messaging/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/messaging/admin.py b/src/messaging/admin.py
new file mode 100644 (file)
index 0000000..1b2bac5
--- /dev/null
@@ -0,0 +1,34 @@
+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)
+
diff --git a/src/messaging/apps.py b/src/messaging/apps.py
new file mode 100644 (file)
index 0000000..0c0cd1c
--- /dev/null
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class MessagingConfig(AppConfig):
+    name = 'messaging'
diff --git a/src/messaging/locale/pl/LC_MESSAGES/django.mo b/src/messaging/locale/pl/LC_MESSAGES/django.mo
new file mode 100644 (file)
index 0000000..318b4a7
Binary files /dev/null and b/src/messaging/locale/pl/LC_MESSAGES/django.mo differ
diff --git a/src/messaging/locale/pl/LC_MESSAGES/django.po b/src/messaging/locale/pl/LC_MESSAGES/django.po
new file mode 100644 (file)
index 0000000..d2a3c98
--- /dev/null
@@ -0,0 +1,95 @@
+# 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."
diff --git a/src/messaging/management/__init__.py b/src/messaging/management/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/messaging/management/commands/__init__.py b/src/messaging/management/commands/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/messaging/management/commands/messaging_send.py b/src/messaging/management/commands/messaging_send.py
new file mode 100644 (file)
index 0000000..6fb4a69
--- /dev/null
@@ -0,0 +1,14 @@
+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'])
+
diff --git a/src/messaging/migrations/0001_initial.py b/src/messaging/migrations/0001_initial.py
new file mode 100644 (file)
index 0000000..9af9760
--- /dev/null
@@ -0,0 +1,47 @@
+# 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',),
+            },
+        ),
+    ]
diff --git a/src/messaging/migrations/__init__.py b/src/messaging/migrations/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/messaging/models.py b/src/messaging/models.py
new file mode 100644 (file)
index 0000000..feb5daa
--- /dev/null
@@ -0,0 +1,77 @@
+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)
diff --git a/src/messaging/recipient.py b/src/messaging/recipient.py
new file mode 100644 (file)
index 0000000..e1e8313
--- /dev/null
@@ -0,0 +1,6 @@
+class Recipient:
+    def __init__(self, email, hash_value, context):
+        self.email = email
+        self.hash_value = hash_value
+        self.context = context
+
diff --git a/src/messaging/states.py b/src/messaging/states.py
new file mode 100644 (file)
index 0000000..317be21
--- /dev/null
@@ -0,0 +1,75 @@
+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,
+]
+
diff --git a/src/messaging/templates/admin/messaging/emailtemplate/change_form.html b/src/messaging/templates/admin/messaging/emailtemplate/change_form.html
new file mode 100644 (file)
index 0000000..89c95d2
--- /dev/null
@@ -0,0 +1,19 @@
+{% 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 %}
diff --git a/src/messaging/urls.py b/src/messaging/urls.py
new file mode 100644 (file)
index 0000000..8f1fc1f
--- /dev/null
@@ -0,0 +1,7 @@
+from django.urls import path
+from . import views
+
+
+urlpatterns = [
+    path('states/<slug>/info.json', views.state_info),
+]
diff --git a/src/messaging/views.py b/src/messaging/views.py
new file mode 100644 (file)
index 0000000..be83d95
--- /dev/null
@@ -0,0 +1,31 @@
+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,
+    })
+
index fa2f82f..5b55b04 100644 (file)
@@ -12,6 +12,7 @@ INSTALLED_APPS_OUR = [
     'dictionary',
     'infopages',
     'lesmianator',
+    'messaging',
     'newtagging',
     'opds',
     'pdcounter',
index 6188658..c328c83 100644 (file)
@@ -44,6 +44,7 @@ urlpatterns += [
     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)),