Commit 0f7450bb authored by Sébastiaan Versteeg's avatar Sébastiaan Versteeg

Add newsletter app

Squashed commits:
[c6cbd24] Remove fuzzy translation
[fd0ec11] Add newsletter app
Squashed commits:
[219ef97] Add newsletter from to settings
[00a2804] Add newsletter app
Squashed commits:
[dc13774[ Fix translations
[26f3bea] Prevent mass deletion from admin and fix add form
[2024827] Rename newsletter_optout to receive_newsletter
[baeb801] Squash migrations
[a8b50e7] Tests for email sending
[1d1f0b1] Move object creation and login to setUp
[3d0fda1] Add tests for newsletters that were not sent
[32e4196] Add more and fix tests
[82e5e6f] Thalia headers on the send page
[0d3b9ad] Send newsletter directly to user in BCC instead of using the mailman list
[8855730] Start writing tests
[8007c19] Add check in model for start and end times
[fbcd91c] Add rendering and sending email code
[507e932] Fix PEP styles
[1432cf0] Use main partner from the partners module
[31708d9] Add newsletter images to static
[299a873] Make frontend preview for newsletter
[e01e3d9] Change end date text
[c5661ba] Make newsletters multilingual
[4dc2579] Add help texts to newsletter fields
[337cf5e] Add introduction field to admin form
[262629b] Add introduction textfield to newsletter
[5a46afd] Re-add CASCADE on delete
[3270015] Add newsletter migrations
[bdb5b96] Add admin for newsletter models
[a613072] Finish models
[2e8946f] Add base files
parent 4e5ec886
# -*- coding: utf-8 -*-
# Generated by Django 1.10 on 2016-09-04 20:32
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('members', '0006_auto_20160824_2041'),
]
operations = [
migrations.AddField(
model_name='member',
name='receive_newsletter',
field=models.BooleanField(default=True, help_text='Receive the Thalia Newsletter', verbose_name='Receive newsletter'),
),
]
......@@ -260,6 +260,12 @@ class Member(models.Model):
default=True,
)
receive_newsletter = models.BooleanField(
verbose_name=_('Receive newsletter'),
help_text=_("Receive the Thalia Newsletter"),
default=True,
)
# --- Direct debit information ----
direct_debit_authorized = models.BooleanField(
......
from django.contrib import admin
from django.shortcuts import redirect
from utils.translation import TranslatedModelAdmin
from newsletters.models import (
Newsletter,
NewsletterEvent,
NewsletterItem
)
class NewsletterItemInline(admin.StackedInline):
model = NewsletterItem
class NewsletterEventInline(admin.StackedInline):
model = NewsletterEvent
@admin.register(Newsletter)
class NewsletterAdmin(TranslatedModelAdmin):
list_display = ('title', 'date', 'sent',)
inlines = (NewsletterItemInline, NewsletterEventInline,)
fieldsets = (
(None, {
'fields': (
'title', 'date', 'description'
)
}),
)
def change_view(self, request, object_id, form_url=''):
obj = Newsletter.objects.filter(id=object_id)[0]
if obj is not None and obj.sent is True:
return redirect(obj)
return super(NewsletterAdmin, self).change_view(
request, object_id, form_url, {'newsletter': obj})
def has_delete_permission(self, request, obj=None):
if obj is not None and obj.sent is True:
return False
return (super(NewsletterAdmin, self)
.has_delete_permission(request, obj=obj))
def get_actions(self, request):
actions = super(NewsletterAdmin, self).get_actions(request)
del actions['delete_selected']
return actions
from django.apps import AppConfig
class NewslettersConfig(AppConfig):
name = 'newsletters'
This diff was suppressed by a .gitattributes entry.
# 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: 2016-09-04 22:38+0200\n"
"PO-Revision-Date: 2016-09-05 14:18+0200\n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: nl\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 1.8.8\n"
#: models.py:13 models.py:49
msgid "Title"
msgstr "Titel\t"
#: models.py:14
msgid "The title is used for the email subject."
msgstr "De titel wordt gebruikt als onderwerp van de e-mail."
#: models.py:19
msgid "Date"
msgstr "Datum"
#: models.py:20
msgid ""
"This date is used to extract the week of this newsletter, best scenario:"
"always use the monday of the week the newsletter is for. If you leave it "
"empty no week is shown."
msgstr ""
"Deze datum wordt gebruikt om de week van de nieuwsbrief te bepalen, het "
"makkelijkste is dus om gewoon de maandag van de week in te vullen waar deze "
"nieuwsbrief voor is."
#: models.py:30
msgid "Introduction"
msgstr "Introductie"
#: models.py:31
msgid ""
"This is the text that starts the newsletter. It always begins with \"Dear "
"members\" and you can append whatever you want."
msgstr ""
"Dit is de tekst waarmee de nieuwsbrief start. Er wordt altijd begonnen met "
"“Beste Thalianen” en je kunt daarna alles schrijven wat je wilt."
#: models.py:56
msgid "Description"
msgstr "Beschrijving"
#: models.py:75 templates/newsletters/email.html:127
#: templates/newsletters/email.txt:20
msgid "What"
msgstr "Wat"
#: models.py:83 templates/newsletters/email.html:137
#: templates/newsletters/email.txt:21
msgid "Where"
msgstr "Waar"
#: models.py:89
msgid "Start date and time"
msgstr "Startdatum en -tijd"
#: models.py:95
msgid "End date and time"
msgstr "Einddatum en -tijd"
#: models.py:101
msgid "Price (in Euro)"
msgstr "Prijs (in Euro)"
#: models.py:108
msgid "Costs (in Euro)"
msgstr "Kosten (in Euro)"
#: models.py:112
msgid "This is the price that a member has to pay when he/she did not show up."
msgstr ""
"Dit is de prijs die een lid moet betalen als hij/zij niet aanwezig was."
#: models.py:120
msgid "Can't have an event travel back in time"
msgstr "Een evenement kan niet terug in de tijd reizen"
#: templates/admin/newsletters/change_form.html:14
msgid "Send newsletter to members"
msgstr "Verstuur nieuwsbrief naar leden"
#: templates/admin/newsletters/change_form.html:15
msgid "Show preview"
msgstr "Toon voorbeeld"
#: templates/newsletters/admin/send_confirm.html:4
msgid "Thalia site admin"
msgstr ""
#: templates/newsletters/admin/send_confirm.html:7
msgid "Thalia administration"
msgstr ""
#: templates/newsletters/admin/send_confirm.html:12
msgid "home"
msgstr "home"
#: templates/newsletters/admin/send_confirm.html:13
msgid "newsletters"
msgstr "nieuwsbrieven"
#: templates/newsletters/admin/send_confirm.html:15
msgid "Send newsletter"
msgstr "Verstuur nieuwsbrief"
#: templates/newsletters/admin/send_confirm.html:19
#, python-format
msgid "Send newsletter: %(newsletter)s"
msgstr "Verstuur nieuwsbrief: %(newsletter)s"
#: templates/newsletters/admin/send_confirm.html:22
#, python-format
msgid "Are you sure you want to send the newsletter '%(newsletter)s'?"
msgstr "Weet je zeker dat je de nieuwsbrief '%(newsletter)s' wilt verzenden?"
#: templates/newsletters/admin/send_confirm.html:30
msgid "No, take me back"
msgstr "Nee, ga terug"
#: templates/newsletters/email.html:26 templates/newsletters/email.txt:32
msgid "view this email in your browser"
msgstr "bekijk deze e-mail in je browser"
#: templates/newsletters/email.html:55 templates/newsletters/email.txt:3
msgid "dear members"
msgstr "beste thalianen"
#: templates/newsletters/email.html:66 templates/newsletters/email.txt:8
msgid "agenda"
msgstr "agenda"
#: templates/newsletters/email.html:113 templates/newsletters/email.txt:18
msgid "Attention"
msgstr "Let op"
#: templates/newsletters/email.html:113 templates/newsletters/email.txt:18
msgid "Registration deadline = unregistration deadline"
msgstr "Aanmelddeadline = Afmelddeadline"
#: templates/newsletters/email.html:114 templates/newsletters/email.txt:18
msgid "Thalia will recover the costs on you if you do not unregister on time"
msgstr ""
"Niet of niet op tijd afmelden betekent de door Thalia per persoon gemaakte "
"kosten betalen"
#: templates/newsletters/email.html:116 templates/newsletters/email.txt:18
msgid "These costs are"
msgstr "Deze bedragen"
#: templates/newsletters/email.html:147 templates/newsletters/email.txt:22
msgid "When"
msgstr "Wanneer"
#: templates/newsletters/email.html:163 templates/newsletters/email.txt:23
msgid "Price"
msgstr "Prijs\t"
#: templates/newsletters/email.html:168 templates/newsletters/email.txt:23
msgid "Free"
msgstr "Gratis"
#: templates/newsletters/email.html:206 templates/newsletters/email.txt:30
msgid "our main partner"
msgstr "onze hoofdpartner"
#: templates/newsletters/email.txt:1
msgid "newsletter"
msgstr "nieuwsbrief"
#: templates/newsletters/email.txt:26
msgid "room"
msgstr "kamer"
#: templates/newsletters/email.txt:27
msgid "website"
msgstr "website"
#: templates/newsletters/email.txt:28
msgid "email"
msgstr "e-mailadres"
# -*- coding: utf-8 -*-
# Generated by Django 1.10 on 2016-09-04 20:27
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
import tinymce.models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Newsletter',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date', models.DateField(blank=True, help_text='This date is used to extract the week of this newsletter, best scenario:always use the monday of the week the newsletter is for. If you leave it empty no week is shown.', null=True, verbose_name='Date')),
('sent', models.BooleanField(default=False)),
('title_en', models.CharField(help_text='The title is used for the email subject.', max_length=150, verbose_name='Title (EN)')),
('title_nl', models.CharField(help_text='The title is used for the email subject.', max_length=150, verbose_name='Title (NL)')),
('description_en', tinymce.models.HTMLField(help_text='This is the text that starts the newsletter. It always begins with "Dear members" and you can append whatever you want.', verbose_name='Introduction (EN)')),
('description_nl', tinymce.models.HTMLField(help_text='This is the text that starts the newsletter. It always begins with "Dear members" and you can append whatever you want.', verbose_name='Introduction (NL)')),
],
),
migrations.CreateModel(
name='NewsletterEvent',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title_en', models.CharField(max_length=150, verbose_name='Title (EN)')),
('title_nl', models.CharField(max_length=150, verbose_name='Title (NL)')),
('description_en', tinymce.models.HTMLField(verbose_name='Description (EN)')),
('description_nl', tinymce.models.HTMLField(verbose_name='Description (NL)')),
('start_datetime', models.DateTimeField(verbose_name='Start date and time')),
('end_datetime', models.DateTimeField(verbose_name='End date and time')),
('price', models.DecimalField(decimal_places=2, default=0, max_digits=5, verbose_name='Price (in Euro)')),
('penalty_costs', models.DecimalField(decimal_places=2, default=0, help_text='This is the price that a member has to pay when he/she did not show up.', max_digits=5, verbose_name='Costs (in Euro)')),
('where_en', models.CharField(max_length=150, verbose_name='Where (EN)')),
('where_nl', models.CharField(max_length=150, verbose_name='Where (NL)')),
('what_en', models.CharField(max_length=150, verbose_name='What (EN)')),
('what_nl', models.CharField(max_length=150, verbose_name='What (NL)')),
('newsletter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='newsletters.Newsletter')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='NewsletterItem',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title_en', models.CharField(max_length=150, verbose_name='Title (EN)')),
('title_nl', models.CharField(max_length=150, verbose_name='Title (NL)')),
('description_en', tinymce.models.HTMLField(verbose_name='Description (EN)')),
('description_nl', tinymce.models.HTMLField(verbose_name='Description (NL)')),
('newsletter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='newsletters.Newsletter')),
],
options={
'abstract': False,
},
),
]
from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _
from tinymce.models import HTMLField
from utils.translation import MultilingualField, ModelTranslateMeta
class Newsletter(models.Model, metaclass=ModelTranslateMeta):
title = MultilingualField(
models.CharField,
max_length=150,
verbose_name=_('Title'),
help_text=_('The title is used for the email subject.'),
blank=False,
)
date = models.DateField(
verbose_name=_('Date'),
help_text=_('This date is used to extract the week of this '
'newsletter, best scenario:'
'always use the monday of the week the newsletter is '
'for. If you leave it empty no week is shown.'),
blank=True,
null=True
)
description = MultilingualField(
HTMLField,
verbose_name=_('Introduction'),
help_text=_('This is the text that starts the newsletter. It always '
'begins with "Dear members" and you can append '
'whatever you want.'),
blank=False,
)
sent = models.BooleanField(
default=False
)
def get_absolute_url(self):
return reverse('newsletters:preview', args=(self.pk,))
class NewsletterContent(models.Model, metaclass=ModelTranslateMeta):
title = MultilingualField(
models.CharField,
max_length=150,
verbose_name=_('Title'),
blank=False,
null=False,
)
description = MultilingualField(
HTMLField,
verbose_name=_('Description'),
blank=False,
null=False,
)
newsletter = models.ForeignKey(Newsletter, on_delete=models.CASCADE)
class Meta:
abstract = True
class NewsletterItem(NewsletterContent):
pass
class NewsletterEvent(NewsletterContent):
what = MultilingualField(
models.CharField,
max_length=150,
verbose_name=_('What'),
blank=False,
null=False,
)
where = MultilingualField(
models.CharField,
max_length=150,
verbose_name=_('Where'),
blank=False,
null=False,
)
start_datetime = models.DateTimeField(
verbose_name=_('Start date and time'),
blank=False,
null=False,
)
end_datetime = models.DateTimeField(
verbose_name=_('End date and time'),
blank=False,
null=False,
)
price = models.DecimalField(
verbose_name=_('Price (in Euro)'),
max_digits=5,
decimal_places=2,
default=0,
)
penalty_costs = models.DecimalField(
verbose_name=_('Costs (in Euro)'),
max_digits=5,
decimal_places=2,
default=0,
help_text=_('This is the price that a member has to '
'pay when he/she did not show up.'),
)
def clean(self):
super().clean()
if self.end_datetime < self.start_datetime:
raise ValidationError({
'end': _("Can't have an event travel back in time")
})
.submit-row a.default {
text-transform: uppercase;
}
.submit-row a.button {
float: right;
padding: 10px 15px;
line-height: 15px;
margin: 0 0 0 8px;
}
.newsletters-row a.button.default {
background-color: #e62272;
}
.newsletters-row a.button.default:hover, .newsletters-row a.button.default:active {
background-color: #cd2167;
}
.newsletters-row a.button {
background-color: #e65c95;
}
.newsletters-row a.button:hover, .newsletters-row a.button:active {
background-color: #d25389;
}
\ No newline at end of file
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
{% extends "admin/change_form.html" %}
{% load i18n admin_urls static compress %}
{% block extrastyle %}
{{ block.super }}
<link rel="stylesheet" type="text/css" href="{% static 'admin/newsletters/css/forms.css' %}" />
{% endblock %}
{% block submit_buttons_bottom %}
{{ block.super }}
{% if newsletter %}
<div class="submit-row newsletters-row">
<a href="{% url 'newsletters:admin-send' pk=newsletter.pk %}" class="default button">{% trans "Send newsletter to members" %}</a>
<a target="_blank" href="{{ newsletter.get_absolute_url }}" class="button">{% trans "Show preview" %}</a>
</div>
{% endif %}
{% endblock %}
\ No newline at end of file
{% extends "admin/delete_selected_confirmation.html" %}
{% load i18n admin_urls static admin_modify %}
{% block title %}{{ newsletter.title }} | {{ site_title|default:_('Thalia site admin') }}{% endblock %}
{% block branding %}
<h1 id="site-name"><a href="{% url 'admin:index' %}">{{ site_header|default:_('Thalia administration') }}</a></h1>
{% endblock %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% trans 'home'|capfirst %}</a>
&rsaquo; <a href="{% url 'admin:app_list' app_label='newsletters' %}">{% trans 'newsletters'|capfirst %}</a>
&rsaquo; <a href="{% url 'admin:newsletters_newsletter_change' newsletter.pk %}">{{ newsletter.title }}</a>
&rsaquo; {% trans 'Send newsletter' %}
</div>
{% endblock %}
{% block content_title %}<h1>{% blocktrans with newsletter=newsletter.title %}Send newsletter: {{ newsletter }}{% endblocktrans %}</h1>{% endblock %}
{% block content %}
<p>{% blocktrans with newsletter=newsletter.title %}Are you sure you want to send the newsletter '{{ newsletter }}'?{% endblocktrans %}</p>
<form method="post">
{% csrf_token %}
<div>
<input type="hidden" name="post" value="yes">
<input type="submit" value="Yes, I'm sure">
<a href="{% url 'admin:newsletters_newsletter_change' newsletter.pk %}" class="button cancel-link">{% trans "No, take me back" %}</a>
</div>
</form>
<br class="clear">
{% endblock %}
\ No newline at end of file
This diff is collapsed.
{% load i18n baseurl %}{% load static from staticfiles %}THALIA {% trans "newsletter"|upper %}
{% trans "dear members"|capfirst %},
{{ newsletter.description|striptags }}
{% trans "agenda"|upper %}:
{% for item in newsletter.newsletterevent_set.all %} {{ item.title }} - {{ item.start_datetime|date:"d F" }}
{% endfor %}
{% for item in newsletter.newsletteritem_set.all %}{{ item.title|upper }}:
{{ item.description|striptags }}
{% endfor %}{% for event in newsletter.newsletterevent_set.all %}{{ event.title|upper }}:
{{ event.description|striptags }}
{% if event.price != 0 %}
{% trans "Attention" %}: {% trans "Registration deadline = unregistration deadline" %}! {% trans "Thalia will recover the costs on you if you do not unregister on time" %}. {% if event.penalty_costs != 0 %}{% trans "These costs are" %}: €{{ event.penalty_costs }}.{% endif %}
{% endif %}
{% trans "What"|upper %}: {{ event.what }}
{% trans "Where"|upper %}: {{ event.where }}
{% trans "When"|upper %}: {% if event.start_datetime == event.end_datetime %}{{ event.start_datetime }}{% elif event.start_datetime|date:'d-M-Y' == event.end_datetime|date:'d-M-Y' %}{{ event.start_datetime }} - {{ event.end_datetime|date:'H:i' }}{% else %}{{ event.start_datetime }} - {{ event.end_datetime }}{% endif %}
{% trans "Price"|upper %}: {% if event.price == 0 %}{% trans "Free" %}{% else %}€{{ event.price }}{% endif %}
{% endfor %}
{% trans "room"|capfirst %}: HG00.150
{% trans "website"|capfirst %}: www.thalia.nu
{% trans "email"|capfirst %}: info@thalia.nu
{% trans "our main partner"|capfirst %}: {{ main_partner.name }} - {{ main_partner.link }}
{% trans "view this email in your browser"|capfirst %}: {% baseurl %}{{ newsletter.get_absolute_url }}
\ No newline at end of file
from django.template import Library
register = Library()
@register.simple_tag(takes_context=True)
def baseurl(context):
"""
Return a BASE_URL template context for the current request.
"""
request = context['request']
if request.is_secure():
scheme = 'https://'
else:
scheme = 'http://'
return scheme + request.get_host()
"""
Template filters to partition lists into rows or columns.
A common use-case is for splitting a list into a table with columns::
{% load partition %}
<table>
{% for row in mylist|columns:3 %}
<tr>