Merge pull request 'Merge with master' (#39) from master into production
All checks were successful
buildbot/tox Build done.

Reviewed-on: #39
This commit was merged in pull request #39.
This commit is contained in:
2020-12-22 19:45:11 +01:00
70 changed files with 2529 additions and 270 deletions

View File

@@ -62,7 +62,7 @@ class SetPasswordForm(forms.Form):
return self.user return self.user
class ResetPasswordForm(forms.Form): class CreateAndSendPasswordForm(forms.Form):
username = auth_forms.UsernameField( username = auth_forms.UsernameField(
max_length=254, max_length=254,
label=_(u'E-Mail-Adresse'), label=_(u'E-Mail-Adresse'),

View File

@@ -15,7 +15,8 @@
<form action="" method="post"> <form action="" method="post">
{% csrf_token %} {% csrf_token %}
{% bootstrap_form form %} {% bootstrap_form form %}
<div class="pull-right"><a href="{% url 'dav_auth:reset_password' %}">{% trans 'Passwort vergessen?' %}</a></div> <div class="pull-right"><a
href="{% url 'dav_auth:recreate_password' %}">{% trans 'Passwort vergessen?' %}</a></div>
{% buttons %} {% buttons %}
<button type="submit" class="btn btn-success"> <button type="submit" class="btn btn-success">
{% bootstrap_icon 'log-in' %}&thinsp; {% bootstrap_icon 'log-in' %}&thinsp;

View File

@@ -32,4 +32,38 @@
&nbsp; &nbsp;
</div> </div>
</div> </div>
<div class="row">
<div class="col-sm-2">
&nbsp;
</div>
<div class="col-sm-8">
<h3 class="top-most">{% trans 'Passwortrichtlinien' %}</h3>
<div class="well">
<p>
Damit die persönlichen Daten unserer Teilnehmer jederzeit geschützt sind, ist es unabdingbar,
dass eure Passwörter für das Touren- & Kurse-Portal nicht einfach zu erraten sind.
</p>
<p>
Je länger das Passwort ist und je mehr <i>ungewöhnliche</i> Zeichen darin enthalten sind,
umso unwahrscheinlicher ist es, dass das Passwort erraten werden kann.
</p>
<p>
Um sicher zu stellen, dass eure Passwörter ausreichend lang sind und nicht nur
aus einfachen Wörtern bestehen, wird die Güte eures Passwortes mit Punkten bewertet.<br />
Ausserdem dürfen bestimmte Wörter nicht enthalten sein (z.B. Karlsruhe).
</p>
<p>
Mehr Zeichen geben mehr Punkte. Und Großbuchstaben, Ziffern und Sonderzeichen geben Extrapunkte.
</p>
<p>
Im Regelfall sollte euer Passwort aus mindestens 12 Zeichen bestehen,
darunter mindestens je zwei Großbuchstaben, Ziffern und Sonderzeichen.<br />
Nehmt da ruhig alles, was eure Tastatur so hergibt.
</p>
</div>
</div>
<div class="col-sm-3">
&nbsp;
</div>
</div>
{% endblock page-container %} {% endblock page-container %}

View File

@@ -9,7 +9,7 @@ from ..emails import PasswordSetEmail
TEST_USERNAME = 'user' TEST_USERNAME = 'user'
TEST_PASSWORD = u'me||ön 2' TEST_PASSWORD = u'me||ön 21ABll'
TEST_EMAIL = 'root@localhost' TEST_EMAIL = 'root@localhost'
PASSWORD_EMAIL_TEMPLATE = u"""Hallo {fullname}, PASSWORD_EMAIL_TEMPLATE = u"""Hallo {fullname},

View File

@@ -6,10 +6,10 @@ from django.utils.translation import ugettext
from dav_base.tests.generic import FormDataSet, FormsTestCase from dav_base.tests.generic import FormDataSet, FormsTestCase
from ..forms import LoginForm, SetPasswordForm, ResetPasswordForm from ..forms import LoginForm, SetPasswordForm, CreateAndSendPasswordForm
TEST_USERNAME = 'root@localhost' TEST_USERNAME = 'root@localhost'
TEST_PASSWORD = u'me||ön 2' TEST_PASSWORD = u'me||ön 21ABll'
TEST_EMAIL = TEST_USERNAME TEST_EMAIL = TEST_USERNAME
USERNAME_MAX_LENGTH = 254 USERNAME_MAX_LENGTH = 254
@@ -108,7 +108,7 @@ class SetPasswordFormTestCase(FormsTestCase):
def test_mismatch(self): def test_mismatch(self):
data_sets = [ data_sets = [
FormDataSet({'new_password': 'mellon12', 'new_password_repeat': 'mellon13'}, FormDataSet({'new_password': 'mellonAB12+-', 'new_password_repeat': 'mellonAB13+-'},
[('new_password_repeat', 'password_mismatch')]), [('new_password_repeat', 'password_mismatch')]),
] ]
super(SetPasswordFormTestCase, self).test_invalid_data(data_sets=data_sets, form_kwargs={'user': self.user}) super(SetPasswordFormTestCase, self).test_invalid_data(data_sets=data_sets, form_kwargs={'user': self.user})
@@ -150,9 +150,10 @@ class SetPasswordFormTestCase(FormsTestCase):
def test_valid(self): def test_valid(self):
data_sets = [ data_sets = [
FormDataSet({'new_password': 'mellon12', 'new_password_repeat': 'mellon12'}), FormDataSet({'new_password': 'mellonAB12+-', 'new_password_repeat': 'mellonAB12+-'}),
FormDataSet({'new_password': 'mellon12', 'new_password_repeat': 'mellon12', 'send_password_mail': True}), FormDataSet({'new_password': 'mellonAB12+-', 'new_password_repeat': 'mellonAB12+-',
FormDataSet({'new_password': u'"ä§ Mellon12', 'new_password_repeat': u'"ä§ Mellon12'}), 'send_password_mail': True}),
FormDataSet({'new_password': u'"ä§ MellonAB12+-', 'new_password_repeat': u'"ä§ MellonAB12+-'}),
FormDataSet({'new_password': 'mellon12' * 128, 'new_password_repeat': 'mellon12' * 128}), FormDataSet({'new_password': 'mellon12' * 128, 'new_password_repeat': 'mellon12' * 128}),
] ]
super(SetPasswordFormTestCase, self).test_valid_data(data_sets=data_sets, form_kwargs={'user': self.user}) super(SetPasswordFormTestCase, self).test_valid_data(data_sets=data_sets, form_kwargs={'user': self.user})
@@ -193,8 +194,8 @@ class SetPasswordFormTestCase(FormsTestCase):
self.assertTrue(self.client.login(username=self.test_username, password=new_password)) self.assertTrue(self.client.login(username=self.test_username, password=new_password))
class ResetPasswordFormTestCase(FormsTestCase): class CreateAndSendPasswordFormTestCase(FormsTestCase):
form_class = ResetPasswordForm form_class = CreateAndSendPasswordForm
valid_data_sets = ( valid_data_sets = (
FormDataSet({'username': 'unittest@example.com'}), FormDataSet({'username': 'unittest@example.com'}),

View File

@@ -9,7 +9,7 @@ from selenium.webdriver.common.keys import Keys
from dav_base.tests.generic import ScreenshotTestCase from dav_base.tests.generic import ScreenshotTestCase
TEST_USERNAME = 'root@localhost' TEST_USERNAME = 'root@localhost'
TEST_PASSWORD = u'me||ön 2' TEST_PASSWORD = u'me||ön 21ABll'
TEST_EMAIL = TEST_USERNAME TEST_EMAIL = TEST_USERNAME
@@ -177,10 +177,10 @@ class TestCase(ScreenshotTestCase):
self.wait_until_stale(c, password2_field) self.wait_until_stale(c, password2_field)
self.save_screenshot('set_password_succeed', sequence=sequence_name) self.save_screenshot('set_password_succeed', sequence=sequence_name)
# Get password reset page -> since we are logged in, it should # Get password recreate page -> since we are logged in, it should
# redirect to set password page again -> save # redirect to set password page again -> save
html = c.find_element_by_tag_name('html') html = c.find_element_by_tag_name('html')
c.get(self.complete_url(reverse('dav_auth:reset_password'))) c.get(self.complete_url(reverse('dav_auth:recreate_password')))
self.wait_until_stale(c, html) self.wait_until_stale(c, html)
self.wait_on_presence(c, (By.ID, 'id_new_password')) self.wait_on_presence(c, (By.ID, 'id_new_password'))
self.save_screenshot('empty_set_password_form', sequence=sequence_name) self.save_screenshot('empty_set_password_form', sequence=sequence_name)
@@ -194,24 +194,24 @@ class TestCase(ScreenshotTestCase):
self.wait_until_stale(c, user_menu) self.wait_until_stale(c, user_menu)
self.save_screenshot('logout_succeed', sequence=sequence_name) self.save_screenshot('logout_succeed', sequence=sequence_name)
# Click on 'login' to access password reset link # Click on 'login' to access password recreate link
link = c.find_element_by_css_selector('#login-widget a') link = c.find_element_by_css_selector('#login-widget a')
link.click() link.click()
self.wait_on_presence(c, (By.ID, 'id_username')) self.wait_on_presence(c, (By.ID, 'id_username'))
# Locate password reset link, click it -> save password reset form # Locate password recreate link, click it -> save password recreate form
link = c.find_element_by_partial_link_text(ugettext(u'Passwort vergessen')) link = c.find_element_by_partial_link_text(ugettext(u'Passwort vergessen'))
link.click() link.click()
username_field = self.wait_on_presence(c, (By.ID, 'id_username')) username_field = self.wait_on_presence(c, (By.ID, 'id_username'))
self.save_screenshot('empty_reset_password_form', sequence=sequence_name) self.save_screenshot('empty_recreate_password_form', sequence=sequence_name)
# Enter invalid username -> save result (login form, no message) # Enter invalid username -> save result (login form, no message)
username_field.send_keys(self.test_username[::-1]) username_field.send_keys(self.test_username[::-1])
username_field.send_keys(Keys.RETURN) username_field.send_keys(Keys.RETURN)
self.wait_until_stale(c, username_field) self.wait_until_stale(c, username_field)
self.save_screenshot('reset_password_invalid_user', sequence=sequence_name) self.save_screenshot('recreate_password_invalid_user', sequence=sequence_name)
# Locate password reset link, click it # Locate password recreate link, click it
link = c.find_element_by_partial_link_text(ugettext(u'Passwort vergessen')) link = c.find_element_by_partial_link_text(ugettext(u'Passwort vergessen'))
link.click() link.click()
username_field = self.wait_on_presence(c, (By.ID, 'id_username')) username_field = self.wait_on_presence(c, (By.ID, 'id_username'))
@@ -220,4 +220,4 @@ class TestCase(ScreenshotTestCase):
username_field.send_keys(self.test_username) username_field.send_keys(self.test_username)
username_field.send_keys(Keys.RETURN) username_field.send_keys(Keys.RETURN)
self.wait_until_stale(c, username_field) self.wait_until_stale(c, username_field)
self.save_screenshot('reset_password_valid_user', sequence=sequence_name) self.save_screenshot('recreate_password_valid_user', sequence=sequence_name)

View File

@@ -12,16 +12,16 @@ from dav_base.tests.generic import SeleniumTestCase
from .generic import SeleniumAuthMixin from .generic import SeleniumAuthMixin
TEST_USERNAME = 'root@localhost' TEST_USERNAME = 'root@localhost'
TEST_PASSWORD = 'me||ön 2' TEST_PASSWORD = 'me||ön 21ABll'
TEST_EMAIL = TEST_USERNAME TEST_EMAIL = TEST_USERNAME
class TemplatesTestCase(SimpleTestCase): class TemplatesTestCase(SimpleTestCase):
def test_reset_link_in_login_form(self): def test_recreate_link_in_login_form(self):
login_url = reverse('dav_auth:login') login_url = reverse('dav_auth:login')
reset_url = reverse('dav_auth:reset_password') recreate_url = reverse('dav_auth:recreate_password')
text = ugettext('Passwort vergessen?') text = ugettext('Passwort vergessen?')
html = '<a href="{url}">{text}</a>'.format(url=reset_url, text=text) html = '<a href="{url}">{text}</a>'.format(url=recreate_url, text=text)
response = self.client.get(login_url) response = self.client.get(login_url)
self.assertInHTML(html, response.content.decode('utf-8')) self.assertInHTML(html, response.content.decode('utf-8'))

View File

@@ -9,5 +9,5 @@ class TestCase(UrlsTestCase):
Url('/auth/logout', 'dav_auth:logout', views.LogoutView.as_view(), status_code=302), Url('/auth/logout', 'dav_auth:logout', views.LogoutView.as_view(), status_code=302),
Url('/auth/password', 'dav_auth:set_password', views.SetPasswordView.as_view(), Url('/auth/password', 'dav_auth:set_password', views.SetPasswordView.as_view(),
redirect='/auth/login?next=/auth/password'), redirect='/auth/login?next=/auth/password'),
Url('/auth/password/reset', 'dav_auth:reset_password', views.ResetPasswordView.as_view()), Url('/auth/password/recreate', 'dav_auth:recreate_password', views.CreateAndSendPasswordView.as_view()),
) )

View File

@@ -0,0 +1,227 @@
# -*- coding: utf-8 -*-
from django.core.exceptions import ValidationError
from django.test import SimpleTestCase
from ..validators import PasswordScoreValidator, CustomWordlistPasswordValidator, CharacterClassPasswordValidator
class PasswordScoreValidatorTestCase(SimpleTestCase):
def test_too_little_score(self):
passwords = [
u'', # score = 0
u'abcdefghijklmnopq', # score = 17
u'Abcdefghijklmnop', # score = 16 + 1
u'ABcdefghijklmno', # score = 15 + 2
u'AB1defghijklmn', # score = 14 + 2 + 1
u'AB12efghijklm', # score = 13 + 2 + 2
u'AB12+fghijkl', # score = 12 + 2 + 2 + 1
u'AB12+-ghijk', # score = 11 + 2 + 2 + 2
u'AB12+-*hij', # score = 10 + 2 + 2 + 3
u'AB12+-*hi/', # score = 10 + 2 + 2 + 3
u'AB12+-*hi3', # score = 10 + 2 + 2 + 3
u'AB12+-*hiC', # score = 10 + 2 + 2 + 3
u'abcbbbgbbjklmnopqr', # score = 17
]
validator = PasswordScoreValidator(min_classes=0)
for password in passwords:
try:
validator.validate(password)
except ValidationError as e:
self.assertEqual('too_little_score', e.code)
else:
self.fail(u'%s: no validation error was raised' % password)
def test_too_few_classes(self):
passwords = [
u'', # classes = 0
u'abcdefgh', # classes = 1
u'abcdef+-', # classes = 2
u'abcd12gh', # classes = 2
u'abcd12-+', # classes = 3
u'abCDefgh', # classes = 2
u'abCDef+-', # classes = 3
u'abCD12gh', # classes = 3
u'ABCD12-+', # classes = 3
]
validator = PasswordScoreValidator(min_score=0, min_classes=4)
for password in passwords:
try:
validator.validate(password)
except ValidationError as e:
self.assertEqual('too_few_classes', e.code)
else:
self.fail(u'%s: no validation error was raised' % password)
def test_valid(self):
passwords = [
u'Abcdefghijklmnopq',
u'ABcdefghijklmnop',
u'AB1defghijklmno',
u'AB12efghijklmn',
u'AB12+fghijklm',
u'AB12+-ghijkl',
u'AB12+-*hijk',
u'ab1defghijklmnopq',
u'abcd+fghijklmnopq',
u'AB1CDEFGHIJKLMNOPQ',
]
validator = PasswordScoreValidator()
for password in passwords:
try:
validator.validate(password)
except ValidationError as e:
self.fail(e)
class CustomWordlistPasswordValidatorTestCase(SimpleTestCase):
def test_invalid(self):
invalid_passwords = [
(u'passwort', [
u'Das Passwort darf nicht die Zeichenfolge \'passwort\' enthalten.',
]),
(u'abcdDaVefgh', [
u'Das Passwort darf nicht die Zeichenfolge \'dav\' enthalten.',
]),
(u'abcdsektIonefgh', [
u'Das Passwort darf nicht die Zeichenfolge \'sektion\' enthalten.',
]),
(u'alpen12verein34KArlsruhE berge', [
u'Das Passwort darf nicht die Zeichenfolge \'karlsruhe\' enthalten.',
u'Das Passwort darf nicht die Zeichenfolge \'berge\' enthalten.',
]),
(u'heinzel@alpenverein-karlsruhe.de', [
u'Das Passwort darf nicht die Zeichenfolge \'heinzel\' enthalten.',
u'Das Passwort darf nicht die Zeichenfolge \'alpenverein\' enthalten.',
u'Das Passwort darf nicht die Zeichenfolge \'karlsruhe\' enthalten.',
]),
]
validator = CustomWordlistPasswordValidator()
for password, expected_errors in invalid_passwords:
try:
validator.validate(password)
except ValidationError as e:
errors = e.messages
for expected_error in expected_errors:
self.assertIn(expected_error, errors)
for error in errors:
self.assertIn(error, expected_errors)
else:
self.fail(u'%s: no validation error was raised' % password)
def test_valid(self):
passwords = [
u'',
u'password',
u'münchen',
]
validator = CustomWordlistPasswordValidator()
for password in passwords:
try:
validator.validate(password)
except ValidationError as e:
self.fail(e)
class CharacterClassPasswordValidatorTestCase(SimpleTestCase):
def setUp(self):
super(CharacterClassPasswordValidatorTestCase, self).setUp()
self.validator = CharacterClassPasswordValidator()
def test_invalid(self):
invalid_passwords = [
(u'', [
u'Das Passwort muss mindestens 2 Kleinbuchstaben enthalten.',
u'Das Passwort muss mindestens 2 Großbuchstaben enthalten.',
u'Das Passwort muss mindestens 2 Ziffern enthalten.',
u'Das Passwort muss mindestens 2 Sonderzeichen enthalten.',
]),
(u'A+-', [
u'Das Passwort muss mindestens 2 Kleinbuchstaben enthalten.',
u'Das Passwort muss mindestens 2 Großbuchstaben enthalten.',
u'Das Passwort muss mindestens 2 Ziffern enthalten.',
]),
(u'1234567890*', [
u'Das Passwort muss mindestens 2 Kleinbuchstaben enthalten.',
u'Das Passwort muss mindestens 2 Großbuchstaben enthalten.',
u'Das Passwort muss mindestens 2 Sonderzeichen enthalten.',
]),
(u'34*/()', [
u'Das Passwort muss mindestens 2 Kleinbuchstaben enthalten.',
u'Das Passwort muss mindestens 2 Großbuchstaben enthalten.',
]),
(u'AA', [
u'Das Passwort muss mindestens 2 Kleinbuchstaben enthalten.',
u'Das Passwort muss mindestens 2 Ziffern enthalten.',
u'Das Passwort muss mindestens 2 Sonderzeichen enthalten.',
]),
(u'CD0.,', [
u'Das Passwort muss mindestens 2 Kleinbuchstaben enthalten.',
u'Das Passwort muss mindestens 2 Ziffern enthalten.',
]),
(u'EF56', [
u'Das Passwort muss mindestens 2 Kleinbuchstaben enthalten.',
u'Das Passwort muss mindestens 2 Sonderzeichen enthalten.',
]),
(u'8GH?!8', [
u'Das Passwort muss mindestens 2 Kleinbuchstaben enthalten.',
]),
(u'bbX', [
u'Das Passwort muss mindestens 2 Großbuchstaben enthalten.',
u'Das Passwort muss mindestens 2 Ziffern enthalten.',
u'Das Passwort muss mindestens 2 Sonderzeichen enthalten.',
]),
(u'$cd%', [
u'Das Passwort muss mindestens 2 Großbuchstaben enthalten.',
u'Das Passwort muss mindestens 2 Ziffern enthalten.',
]),
(u'ef90', [
u'Das Passwort muss mindestens 2 Großbuchstaben enthalten.',
u'Das Passwort muss mindestens 2 Sonderzeichen enthalten.',
]),
(u'1g=h3~', [
u'Das Passwort muss mindestens 2 Großbuchstaben enthalten.',
]),
(u'Gi&jH', [
u'Das Passwort muss mindestens 2 Ziffern enthalten.',
u'Das Passwort muss mindestens 2 Sonderzeichen enthalten.',
]),
(u'IkK:i;', [
u'Das Passwort muss mindestens 2 Ziffern enthalten.',
]),
(u'mKn4L8', [
u'Das Passwort muss mindestens 2 Sonderzeichen enthalten.',
]),
]
validator = self.validator
for password, expected_errors in invalid_passwords:
try:
validator.validate(password)
except ValidationError as e:
errors = e.messages
for expected_error in expected_errors:
self.assertIn(expected_error, errors)
for error in errors:
self.assertIn(error, expected_errors)
else:
self.fail(u'%s: no validation error was raised' % password)
def test_valid(self):
valid_passwords = [u'abCD12+-']
validator = self.validator
for password in valid_passwords:
try:
validator.validate(password)
except ValidationError as e:
self.fail(e)

View File

@@ -7,10 +7,10 @@ from django.test import TestCase
from django.utils.translation import ugettext from django.utils.translation import ugettext
from django.urls import reverse from django.urls import reverse
from ..forms import LoginForm, SetPasswordForm, ResetPasswordForm from ..forms import LoginForm, SetPasswordForm, CreateAndSendPasswordForm
TEST_USERNAME = 'root@localhost' TEST_USERNAME = 'root@localhost'
TEST_PASSWORD = u'me||ön 2' TEST_PASSWORD = u'me||ön 21ABll'
TEST_EMAIL = TEST_USERNAME TEST_EMAIL = TEST_USERNAME
@@ -27,7 +27,7 @@ class ViewsTestCase(TestCase):
cls.logout_url = reverse('dav_auth:logout') cls.logout_url = reverse('dav_auth:logout')
cls.logout_redirect_url = resolve_url(cls.app_settings.logout_redirect_url) cls.logout_redirect_url = resolve_url(cls.app_settings.logout_redirect_url)
cls.set_password_url = reverse('dav_auth:set_password') cls.set_password_url = reverse('dav_auth:set_password')
cls.reset_password_url = reverse('dav_auth:reset_password') cls.recreate_password_url = reverse('dav_auth:recreate_password')
# Some messages # Some messages
cls.wrong_credentials_message = ugettext(u'Benutzername oder Passwort falsch.') cls.wrong_credentials_message = ugettext(u'Benutzername oder Passwort falsch.')
@@ -179,23 +179,23 @@ class ViewsTestCase(TestCase):
self.assertFalse(self.client.login(username=username, password=password), 'Old password still valid') self.assertFalse(self.client.login(username=username, password=password), 'Old password still valid')
self.assertTrue(self.client.login(username=username, password=new_password), 'New password not valid') self.assertTrue(self.client.login(username=username, password=new_password), 'New password not valid')
def test_reset_password_integrated_unauth_get(self): def test_recreate_password_integrated_unauth_get(self):
response = self.client.get(self.reset_password_url) response = self.client.get(self.recreate_password_url)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'dav_auth/forms/reset_password.html') self.assertTemplateUsed(response, 'dav_auth/forms/recreate_password.html')
self.assertIn('form', response.context) self.assertIn('form', response.context)
self.assertIsInstance(response.context['form'], ResetPasswordForm) self.assertIsInstance(response.context['form'], CreateAndSendPasswordForm)
field = response.context['form'].fields['username'] field = response.context['form'].fields['username']
self.assertTrue(field.required) self.assertTrue(field.required)
def test_reset_password_integrated_auth_get(self): def test_recreate_password_integrated_auth_get(self):
self.client.login(username=self.test_username, password=self.test_password) self.client.login(username=self.test_username, password=self.test_password)
response = self.client.get(self.reset_password_url) response = self.client.get(self.recreate_password_url)
self.assertRedirects(response, self.set_password_url) self.assertRedirects(response, self.set_password_url)
def test_reset_password_integrated_post(self): def test_recreate_password_integrated_post(self):
location = self.reset_password_url location = self.recreate_password_url
response = self.client.post(location, {'username': self.user.username}) response = self.client.post(location, {'username': self.user.username})
self.assertRedirects(response, self.login_url) self.assertRedirects(response, self.login_url)

View File

@@ -6,5 +6,5 @@ urlpatterns = [
url(r'^login$', views.LoginView.as_view(), name='login'), url(r'^login$', views.LoginView.as_view(), name='login'),
url(r'^logout$', views.LogoutView.as_view(), name='logout'), url(r'^logout$', views.LogoutView.as_view(), name='logout'),
url(r'^password$', views.SetPasswordView.as_view(), name='set_password'), url(r'^password$', views.SetPasswordView.as_view(), name='set_password'),
url(r'^password/reset$', views.ResetPasswordView.as_view(), name='reset_password'), url(r'^password/recreate$', views.CreateAndSendPasswordView.as_view(), name='recreate_password'),
] ]

200
dav_auth/validators.py Normal file
View File

@@ -0,0 +1,200 @@
# -*- coding: utf-8 -*-
import re
from django.core.exceptions import ValidationError
from django.utils.translation import gettext as _
class PasswordScoreValidator(object):
def _get_score(self, password, user=None):
score = 0
char_counters = {}
used_classes = []
credits = {
'lower': self.lcredit,
'upper': self.ucredit,
'digit': self.dcredit,
'other': self.ocredit,
}
for c in password:
if c not in char_counters:
char_counters[c] = 1
else:
char_counters[c] += 1
if (self.max_repeat > 0) and (char_counters[c] > self.max_repeat):
continue
score += 1
if c.isalpha() and c.islower():
char_class = 'lower'
elif c.isupper():
char_class = 'upper'
elif c.isdigit():
char_class = 'digit'
else:
char_class = 'other'
if char_class not in used_classes:
used_classes.append(char_class)
if credits[char_class] > 0:
score += 1
credits[char_class] -= 1
return score, len(used_classes)
def __init__(self, min_score=18, max_repeat=5, lcredit=0, ucredit=2, dcredit=2, ocredit=3, min_classes=2):
self.min_score = min_score
self.max_repeat = max_repeat
self.lcredit = lcredit
self.ucredit = ucredit
self.dcredit = dcredit
self.ocredit = ocredit
self.min_classes = min_classes
def validate(self, password, user=None):
score, used_classes = self._get_score(password, user=user)
if used_classes < self.min_classes:
raise ValidationError(_(u'Das Passwort muss Zeichen aus mindestens %(min_classes)d'
u' verschiedenen Arten von Zeichen bestehen'
u' (d.h. Kleinbuchstaben, Großbuchstaben, Ziffern und Sonderzeichen).'),
code='too_few_classes',
params={'min_classes': self.min_classes})
if score < self.min_score:
raise ValidationError(_(u'Dieses Passwort ist zu einfach. Benutze mehr Zeichen'
u' und gegebenenfalls auch Großbuchstaben, Ziffern und Sonderzeichen.'),
code='too_little_score')
return score
def get_help_text(self):
text = u'%s\n%s' % (
_(u'The password must get a minimum score of %d points.') % self.min_score,
_(u'For each character the score is increased by 1 point.'),
)
if self.lcredit > 0:
text += '\n'
text += _(u'The first %d lower characters increase the score by 1 point each.') % self.lcredit
if self.ucredit > 0:
text += '\n'
text += _(u'The first %d upper characters increase the score by 1 point each.') % self.ucredit
if self.dcredit > 0:
text += '\n'
text += _(u'The first %d digits increase the score by 1 point each.') % self.dcredit
if self.ocredit > 0:
text += '\n'
text += _(u'The first %d non alpha numeric characters'
u' increase the score by 1 point each.') % self.ocredit
if self.max_repeat > 0:
text += '\n'
text += _(u'If a particular character is used more than %d times,'
u' it will not increase the score anymore.') % self.max_repeat
if self.min_classes > 0:
text += '\n'
text += _(u'Also the password must contain characters from %d different character classes'
u' (i.e. lower, upper, digits, others).') % self.min_classes
return text
class CustomWordlistPasswordValidator(object):
context = 'the Sektion Karlsruhe'
words = (
u'dav',
u'berge',
u'sektion',
u'karlsruhe',
u'alpenverein',
u'heinzel',
u'passwort',
)
def validate(self, password, user=None):
errors = []
lower_pw = password.lower()
for word in self.words:
if word in lower_pw:
error = ValidationError(_(u'Das Passwort darf nicht die Zeichenfolge \'%(word)s\' enthalten.'),
code='forbidden_word',
params={'word': word})
errors.append(error)
if errors:
raise ValidationError(errors)
def get_help_text(self):
text = _(u'The password must not contain some specific words,'
u' that are common in context with %(context)s.') % {'context': self.context}
text += u'\n'
text += _(u'All words are matched case insensitive.')
return text
class CharacterClassPasswordValidator(object):
def _is_enough_lower(self, password):
lower = re.sub(r'[^a-z]', '', password)
if len(lower) < self.minimum_lower:
return False
return True
def _is_enough_upper(self, password):
upper = re.sub(r'[^A-Z]', '', password)
if len(upper) < self.minimum_upper:
return False
return True
def _is_enough_digits(self, password):
digits = re.sub(r'[^0-9]', '', password)
if len(digits) < self.minimum_digits:
return False
return True
def _is_enough_others(self, password):
others = re.sub(r'[a-zA-Z0-9]', '', password)
if len(others) < self.minimum_others:
return False
return True
def __init__(self, minimum_lower=2, minimum_upper=2, minimum_digits=2, minimum_others=2):
self.minimum_lower = minimum_lower
self.minimum_upper = minimum_upper
self.minimum_digits = minimum_digits
self.minimum_others = minimum_others
def validate(self, password, user=None):
errors = []
if not self._is_enough_lower(password):
error = ValidationError(_(u'Das Passwort muss mindestens %(min_lower)d Kleinbuchstaben enthalten.'),
code='too_few_lower_characters',
params={'min_lower': self.minimum_lower})
errors.append(error)
if not self._is_enough_upper(password):
error = ValidationError(_(u'Das Passwort muss mindestens %(min_upper)d Großbuchstaben enthalten.'),
code='too_few_upper_characters',
params={'min_upper': self.minimum_upper})
errors.append(error)
if not self._is_enough_digits(password):
error = ValidationError(_(u'Das Passwort muss mindestens %(min_digits)d Ziffern enthalten.'),
code='too_few_digits',
params={'min_digits': self.minimum_digits})
errors.append(error)
if not self._is_enough_others(password):
error = ValidationError(_(u'Das Passwort muss mindestens %(min_others)d'
u' Sonderzeichen enthalten.'),
code='too_few_other_characters',
params={'min_others': self.minimum_others})
errors.append(error)
if errors:
raise ValidationError(errors)
def get_help_text(self):
text = u'%s %s %s %s' % (
_('The password must contain at least %d characters from a-z.') % self.minimum_lower,
_('The password must contain at least %d characters from A-Z.') % self.minimum_upper,
_('The password must contain at least %d digits from 0-9.') % self.minimum_digits,
_('The password must contain at least %d non alpha numeric characters.') % self.minimum_others,
)
return text

View File

@@ -1,10 +1,14 @@
# -*- coding: utf-8 -*-
import logging import logging
from django.apps import apps from django.apps import apps
from django.core.exceptions import ValidationError
from django.contrib import messages from django.contrib import messages
from django.contrib.auth import views as auth_views, get_user_model from django.contrib.auth import views as auth_views, get_user_model
from django.contrib.auth.password_validation import validate_password
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.shortcuts import resolve_url from django.shortcuts import resolve_url
from django.urls import reverse_lazy, reverse from django.urls import reverse_lazy, reverse
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.views import generic from django.views import generic
@@ -28,6 +32,18 @@ class LoginView(auth_views.LoginView):
def form_valid(self, form): def form_valid(self, form):
r = super(LoginView, self).form_valid(form) r = super(LoginView, self).form_valid(form)
messages.success(self.request, _(u'Benutzer angemeldet: %(username)s') % {'username': form.get_user()}) messages.success(self.request, _(u'Benutzer angemeldet: %(username)s') % {'username': form.get_user()})
try:
validate_password(form.cleaned_data['password'])
except ValidationError as e:
logger.warning(u'Weak password (%d): %s', self.request.user.pk, e)
message = u'<br />\n<p>\n'
message += u'Dein Passwort entspricht nicht mehr den aktuellen Passwortrichtlinien.<br />\n'
message += u'Bitte hilf uns die Daten deiner Teilnehmer zu schützen und ändere dein Passwort.<br />\n'
message += u'</p>\n'
message += u'<p>\n'
message += u'<a href="%(href)s">Passwort ändern</a>\n' % {'href': reverse('dav_auth:set_password')}
message += u'</p>\n<br />\n'
messages.warning(self.request, mark_safe(message))
return r return r
@@ -60,9 +76,9 @@ class SetPasswordView(auth_views.PasswordChangeView):
return r return r
class ResetPasswordView(generic.FormView): class CreateAndSendPasswordView(generic.FormView):
form_class = forms.ResetPasswordForm form_class = forms.CreateAndSendPasswordForm
template_name = 'dav_auth/forms/reset_password.html' template_name = 'dav_auth/forms/recreate_password.html'
success_url = reverse_lazy('dav_auth:login') success_url = reverse_lazy('dav_auth:login')
def form_valid(self, form): def form_valid(self, form):
@@ -70,19 +86,19 @@ class ResetPasswordView(generic.FormView):
user_model = get_user_model() user_model = get_user_model()
try: try:
user = user_model.objects.get(username=username) user = user_model.objects.get(username=username)
random_password = user_model.objects.make_random_password(length=12) random_password = user_model.objects.make_random_password(length=32)
user.set_password(random_password) user.set_password(random_password)
user.save() user.save()
email = emails.PasswordSetEmail(user, random_password) email = emails.PasswordSetEmail(user, random_password)
email.send() email.send()
messages.success(self.request, _(u'Neues Passwort versendet.')) messages.success(self.request, _(u'Neues Passwort versendet.'))
logger.info('Password reset for user \'%s\'', username) logger.info('Password recreated for user \'%s\'', username)
except user_model.DoesNotExist: except user_model.DoesNotExist:
logger.warning('Password reset for unknown user \'%s\'', username) logger.warning('Password recreated for unknown user \'%s\'', username)
return super(ResetPasswordView, self).form_valid(form) return super(CreateAndSendPasswordView, self).form_valid(form)
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
if request.user.is_authenticated: if request.user.is_authenticated:
return HttpResponseRedirect(reverse('dav_auth:set_password')) return HttpResponseRedirect(reverse('dav_auth:set_password'))
return super(ResetPasswordView, self).get(request, *args, **kwargs) return super(CreateAndSendPasswordView, self).get(request, *args, **kwargs)

View File

@@ -52,6 +52,30 @@ DATABASES['default'] = {
'NAME': os.path.join(BASE_VAR_DIR, 'db', 'devel.sqlite3'), 'NAME': os.path.join(BASE_VAR_DIR, 'db', 'devel.sqlite3'),
} }
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
'OPTIONS': {
'min_length': 12,
},
},
{
'NAME': 'dav_auth.validators.PasswordScoreValidator',
},
{
'NAME': 'dav_auth.validators.CustomWordlistPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
STATIC_ROOT = os.path.join(BASE_VAR_DIR, 'www', 'static') STATIC_ROOT = os.path.join(BASE_VAR_DIR, 'www', 'static')
LANGUAGE_CODE = 'de' LANGUAGE_CODE = 'de'

View File

@@ -114,7 +114,9 @@
</div> </div>
</div> </div>
</div> </div>
<hr /> <hr />
<h4>{% trans 'Teilnehmer' %}</h4> <h4>{% trans 'Teilnehmer' %}</h4>
<div class="panel-group" id="form-accordion-participants" role="tablist" aria-multiselectable="true"> <div class="panel-group" id="form-accordion-participants" role="tablist" aria-multiselectable="true">
{% if registrations_support %} {% if registrations_support %}
@@ -131,7 +133,7 @@
<div id="collapseRegistrations" class="panel-collapse collapse" <div id="collapseRegistrations" class="panel-collapse collapse"
role="tabpanel" aria-labelledby="headingRegistrations"> role="tabpanel" aria-labelledby="headingRegistrations">
<div class="panel-body"> <div class="panel-body">
{% for registration in registrations %} {% for registration in registrations_pending %}
<form action="" method="post" class="form-inline"> <form action="" method="post" class="form-inline">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="registration" value="{{ registration.id }}"> <input type="hidden" name="registration" value="{{ registration.id }}">
@@ -147,9 +149,6 @@
</button> </button>
&nbsp; &nbsp;
{% endif %} {% endif %}
{% if registration.answered %}
<span class="text-muted">
{% endif %}
{{ registration.get_full_name }} {{ registration.get_full_name }}
(<a href="mailto:{{ registration.email_address }}">{{ registration.email_address }}</a>, (<a href="mailto:{{ registration.email_address }}">{{ registration.email_address }}</a>,
{{ registration.phone_number }}) {{ registration.phone_number }})
@@ -162,12 +161,15 @@
<span title="{{ registration.get_info }}"> <span title="{{ registration.get_info }}">
{% bootstrap_icon 'info-sign' %} {% bootstrap_icon 'info-sign' %}
</span> </span>
{% if registration.answered %} {% if registration.apply_reduced_fee %}
&nbsp;
<span class="text-info">
<strong>%</strong>{% bootstrap_icon 'piggy-bank' %} (reduzierte Gebühr)
</span> </span>
{% endif %} {% endif %}
</form> </form>
{% empty %} {% empty %}
{% trans 'Keine unbearbeiteten Anmeldungen vorhanden' %} {% trans 'Keine Anmeldungen vorhanden' %}
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
@@ -179,21 +181,46 @@
{% with position=participant.position %} {% with position=participant.position %}
<div class="panel {% if event.max_participants and position > event.max_participants %}panel-warning{% else %}panel-default{% endif %}"> <div class="panel {% if event.max_participants and position > event.max_participants %}panel-warning{% else %}panel-default{% endif %}">
<div id="headingParticipant_{{ participant.id }}" class="panel-heading" role="tab"> <div id="headingParticipant_{{ participant.id }}" class="panel-heading" role="tab">
<h5 class="panel-title"> <div>
<strong><span class="panel-title">
<a role="button" href="#collapseParticipant_{{ participant.id }}" <a role="button" href="#collapseParticipant_{{ participant.id }}"
data-toggle="collapse" data-toggle="collapse"
aria-expanded="true" aria-controls="collapseParticipant_{{ participant.id }}"> aria-expanded="true" aria-controls="collapseParticipant_{{ participant.id }}">
<span class="caret"></span>&nbsp;&nbsp; <span class="caret"></span>&nbsp;&nbsp;
{{ position }}. {{ participant.get_full_name }} {{ position }}. {{ participant.get_full_name }}
</a> </a>
</span></strong>
&nbsp;
<small> <small>
(<a href="mailto:{{ participant.email_address }}">{{ participant.email_address }}</a>, {{ participant.phone_number }}) (<a href="mailto:{{ participant.email_address }}">{{ participant.email_address }}</a>, {{ participant.phone_number }})
</small> </small>
&nbsp; {{ participant.dav_number }} &nbsp;
{% if participant.dav_member %}
{{ participant.dav_number|default:'Fehler! heinzel Bescheid geben!' }}
{% else %}
{% trans 'Nicht Mitglied' %}
{% endif %}
&nbsp;
<span class="text-info"
title="{{ participant.get_info }}
{% trans 'Zeitpunkt der automatischen Löschung' %}: {{ participant.purge_at|date:'d. F Y' }}">
{% bootstrap_icon 'info-sign' %}
</span>
<div class="pull-right"> <div class="pull-right">
<form action="" method="post" class="form-inline"> <form action="" method="post" class="form-inline">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="id" value="{{ participant.id }}"> <input type="hidden" name="id" value="{{ participant.id }}">
{% if event.charge and has_permission_payment %}
<button name="action" value="toggle_reduced_fee"
title="{% trans 'Hier klicken, um zwischen voller und reduzierter Teilnahmegebühr umzuschalten' %}"
class="btn btn-link no-padding">
<span class="text-primary">
{% if participant.apply_reduced_fee %}{% bootstrap_icon 'check' %}{% else %}{% bootstrap_icon 'unchecked' %}{% endif %}
Reduzierte Teilnamegebühr
</span>
</button>
&nbsp;
{% endif %}
{% if has_permission_update_participants %} {% if has_permission_update_participants %}
<button name="action" value="moveup_participant" <button name="action" value="moveup_participant"
title="{% trans 'Nach oben verschieben' %}" title="{% trans 'Nach oben verschieben' %}"
@@ -205,42 +232,47 @@
class="btn btn-link no-padding {% if forloop.last %}invisible{% endif %}"> class="btn btn-link no-padding {% if forloop.last %}invisible{% endif %}">
<span class="text-info">{% bootstrap_icon 'triangle-bottom' %}</span> <span class="text-info">{% bootstrap_icon 'triangle-bottom' %}</span>
</button> </button>
<button name="action" value="remove_participant" <button name="action" value="trash_participant"
title="{% trans 'Teilnehmer jetzt löschen' %} title="{% trans 'Eintrag in Papierkorb verschieben' %}"
({% trans 'erfolgt automatisch am' %} {{ participant.purge_at|date:'d. F Y' }})"
class="btn btn-link no-padding"> class="btn btn-link no-padding">
<span class="text-danger">{% bootstrap_icon 'remove-circle' %}</span> <span class="text-danger">{% bootstrap_icon 'trash' %}</span>
</button> </button>
&nbsp;
{% endif %} {% endif %}
{% if event.charge and participant.paid and has_permission_payment %} {% if event.charge and participant.paid and has_permission_payment %}
<button name="action" value="revoke_payment" &nbsp;
<span class="text-success {% if not participant.apply_reduced_fee %}invisible{% endif %}"
title="{% trans 'Reduzierte Teilnahmegebühr' %}"><strong>%</strong></span><button
name="action" value="revoke_payment"
title="{% trans 'Geldeingang wurde bestätigt' %} - {% trans 'Bestätigung des Geldeingangs zurückziehen' %}" title="{% trans 'Geldeingang wurde bestätigt' %} - {% trans 'Bestätigung des Geldeingangs zurückziehen' %}"
class="btn btn-link no-padding"> class="btn btn-link no-padding"><span class="text-success">{% bootstrap_icon 'piggy-bank' %}</span></button>
<span class="text-success">{% bootstrap_icon 'piggy-bank' %}</span>
</button>
{% elif event.charge and participant.paid %} {% elif event.charge and participant.paid %}
<span class="text-success" title="{% trans 'Geldeingang bestätigt' %}"> &nbsp;
{% bootstrap_icon 'piggy-bank' %} <span class="text-success {% if not participant.apply_reduced_fee %}invisible{% endif %}"
</span> title="{% trans 'Reduzierte Teilnahmegebühr' %}"><strong>%</strong></span><span
class="text-success"
title="{% trans 'Geldeingang bestätigt' %}">{% bootstrap_icon 'piggy-bank' %}</span>
{% elif event.charge and has_permission_payment %} {% elif event.charge and has_permission_payment %}
<button name="action" value="confirm_payment" &nbsp;
<span class="text-danger {% if not participant.apply_reduced_fee and participant.created_at|date:'U' > '1608662327' %}invisible{% endif %}"
title="{% trans 'Reduzierte Teilnahmegebühr' %}"><strong>{% if participant.apply_reduced_fee %}%{% else %}? {% endif %}</strong></span><button
name="action" value="confirm_payment"
title="{% trans 'Geldeingang bestätigen' %}" title="{% trans 'Geldeingang bestätigen' %}"
class="btn btn-link no-padding"> class="btn btn-link no-padding"><span class="text-danger">{% bootstrap_icon 'piggy-bank' %}</span></button>
<span class="text-danger">{% bootstrap_icon 'piggy-bank' %}</span>
</button>
{% elif event.charge %} {% elif event.charge %}
<span class="text-danger" title="{% trans 'Geldeingang unbestätigt' %}"> &nbsp;
{% bootstrap_icon 'piggy-bank' %} <span class="text-danger {% if not participant.apply_reduced_fee and participant.created_at|date:'U' > '1608662327' %}invisible{% endif %}"
</span> title="{% trans 'Reduzierte Teilnahmegebühr' %}"><strong>{% if participant.apply_reduced_fee %}%{% else %}? {% endif %}</strong></span><span
class="text-danger"
title="{% trans 'Geldeingang unbestätigt' %}">{% bootstrap_icon 'piggy-bank' %}</span>
{% else %} {% else %}
<span class="hidden" title="{% trans 'Keine Teilnehmergebühr gefordert' %}"> <span class="hidden {% if not participant.apply_reduced_fee %}invisible{% endif %}"
{% bootstrap_icon 'piggy-bank' %} title="{% trans 'Reduzierte Teilnahmegebühr' %}"><strong>%</strong></span><span
</span> class="hidden"
title="{% trans 'Keine Teilnehmergebühr gefordert' %}">{% bootstrap_icon 'piggy-bank' %}</span>
{% endif %} {% endif %}
</form> </form>
</div> </div>
</h5> </div>
</div> </div>
<div id="collapseParticipant_{{ participant.id }}" <div id="collapseParticipant_{{ participant.id }}"
class="panel-collapse collapse {% if form.errors %}in{% endif %}" class="panel-collapse collapse {% if form.errors %}in{% endif %}"
@@ -271,6 +303,11 @@
</div> </div>
</div> </div>
{% endif %} {% endif %}
<div class="panel panel-default">
<div class="panel-body">
<strong title="Summe der bestätigten Geldeingänge (theoretisch!, da wir hier nur die Gebühren, aber nicht die tatsächlich überwiesenen Beträge kennen)">{% trans 'Gebuchte Teilnahmegebühren' %}:</strong> {{ earnings|floatformat:2 }} €
</div>
</div>
{% if participant_emails %} {% if participant_emails %}
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-body"> <div class="panel-body">
@@ -280,6 +317,112 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
<hr />
<h4>{% trans 'Papierkorb' %}</h4>
<div class="panel-group" id="form-accordion-trash" role="tablist" aria-multiselectable="true">
{% if registrations_support and registrations_answered %}
<div class="panel panel-info">
<div id="headingAnsweredRegistrations" class="panel-heading" role="tab">
<h5 class="panel-title">
<a role="button" href="#collapseAnsweredRegistrations"
data-toggle="collapse"
aria-expanded="true" aria-controls="collapseAnsweredRegistrations">
<span class="caret"></span>&nbsp;&nbsp;{% trans 'Bearbeitete Anmeldungen' %}
</a>
</h5>
</div>
<div id="collapseAnsweredRegistrations" class="panel-collapse collapse"
role="tabpanel" aria-labelledby="headingAnsweredRegistrations">
<div class="panel-body">
{% for registration in registrations_answered %}
<div>
<button disabled="disabled"
class="btn btn-link no-padding" title="Anmeldung wurde bereits bearbeitet">
<span class="{% if registration.status.accepted == True %}text-success{% else %}text-muted{% endif %}">{% bootstrap_icon 'plus-sign' %}</span>
</button>
&nbsp;
<button disabled="disabled"
class="btn btn-link no-padding" title="Anmeldung wurde bereits bearbeitet">
<span class="{% if registration.status.accepted == False %}text-danger{% else %}text-muted{% endif %}">{% bootstrap_icon 'minus-sign' %}</span>
</button>
&nbsp;
<span class="text-muted">
{{ registration.get_full_name }}
(<a href="mailto:{{ registration.email_address }}">{{ registration.email_address }}</a>,
{{ registration.phone_number }})
&nbsp;
<span title="Anmeldezeitpunkt">
{% bootstrap_icon 'time' %}
{{ registration.created_at|date:'d. F Y, G:i' }}
</span>
&nbsp;
<span title="{{ registration.get_info }}">
{% bootstrap_icon 'info-sign' %}
</span>
</span>
</div>
{% endfor %}
</div>
</div>
</div>
{% endif %}
{% if participants_trash %}
<div class="panel panel-info">
<div id="headingTrashedParticipants" class="panel-heading" role="tab">
<h5 class="panel-title">
<a role="button" href="#collapseTrashedParticipants"
data-toggle="collapse"
aria-expanded="true" aria-controls="collapseTrashedParticipants">
<span class="caret"></span>&nbsp;&nbsp;{% trans 'Gelöschte Teilnehmer' %}
</a>
</h5>
</div>
<div id="collapseTrashedParticipants" class="panel-collapse collapse"
role="tabpanel" aria-labelledby="headingTrashedParticipants">
<div class="panel-body">
{% for participant in participants_trash %}
<div>
<span class="text-muted">
{{ participant.get_full_name }}
(<a href="mailto:{{ participant.email_address }}">{{ participant.email_address }}</a>,
{{ participant.phone_number }})
&nbsp;
{% if participant.dav_member %}
{{ participant.dav_number|default:'Fehler! heinzel Bescheid geben!' }}
{% else %}
{% trans 'Nicht Mitglied' %}
{% endif %}
&nbsp;
<span title="{{ participant.get_info }}
{% trans 'Zeitpunkt der automatischen Löschung' %}: {{ participant.purge_at|date:'d. F Y' }}">
{% bootstrap_icon 'info-sign' %}
</span>
{% if event.charge and participant.paid %}
&nbsp;
<span class="text-success {% if not participant.apply_reduced_fee %}invisible{% endif %}"
title="{% trans 'Reduzierte Teilnahmegebühr' %}"><strong>%</strong></span><span
class="text-success"
title="{% trans 'Geldeingang bestätigt' %}">{% bootstrap_icon 'piggy-bank' %}</span>
{% elif event.charge %}
&nbsp;
<span class="text-danger {% if not participant.apply_reduced_fee %}invisible{% endif %}"
title="{% trans 'Reduzierte Teilnahmegebühr' %}"><strong>%</strong></span><span
class="text-danger"
title="{% trans 'Geldeingang unbestätigt' %}">{% bootstrap_icon 'piggy-bank' %}</span>
{% endif %}
</span>
</div>
{% endfor %}
</div>
</div>
</div>
{% endif %}
{% if not registrations_answered and not participants_trash %}
<span class="text-muted small">{% trans 'Der Papierkorb ist leer.' %}</span>
{% endif %}
</div>
</div> </div>
</div> </div>
{% endblock page-container-fluid %} {% endblock page-container-fluid %}

View File

@@ -29,9 +29,20 @@
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-sm-6"> <div class="col-sm-3">
{% bootstrap_field form.year_of_birth %}
</div>
<div class="col-sm-9">
{% bootstrap_field form.apply_reduced_fee %}
</div>
</div>
<div class="row">
<div class="col-sm-3">
{% bootstrap_field form.dav_number %} {% bootstrap_field form.dav_number %}
</div> </div>
<div class="col-sm-3">
{% bootstrap_field form.dav_member %}
</div>
<div class="col-sm-6"> <div class="col-sm-6">
{% bootstrap_field form.emergency_contact %} {% bootstrap_field form.emergency_contact %}
</div> </div>

View File

@@ -24,7 +24,7 @@
<th>{% trans 'Nachname' %}</th> <th>{% trans 'Nachname' %}</th>
<th>{% trans 'Vorname' %}</th> <th>{% trans 'Vorname' %}</th>
<th>{% trans 'Mitgliedsnummer' %}</th> <th>{% trans 'Mitgliedsnummer' %}</th>
<th>{% trans 'Teilnehmergebühr' %}</th> <th>{% trans 'Teilnahmegebühr' %}</th>
</tr> </tr>
<tr> <tr>
<th><input type="text" placeholder="{% trans 'Filter' %}" /></th> <th><input type="text" placeholder="{% trans 'Filter' %}" /></th>
@@ -32,7 +32,7 @@
<th><input type="text" placeholder="{% trans 'Filter' %}" /></th> <th><input type="text" placeholder="{% trans 'Filter' %}" /></th>
<th><input type="text" placeholder="{% trans 'Filter' %}" /></th> <th><input type="text" placeholder="{% trans 'Filter' %}" /></th>
<th><input type="text" placeholder="{% trans 'Filter' %}" /></th> <th><input type="text" placeholder="{% trans 'Filter' %}" /></th>
<th><input type="text" placeholder="{% trans 'Filter' %} ({% trans 'Grün' %}/{% trans 'Rot' %})" /></th> <th><input type="text" placeholder="{% trans 'Filter' %} ({% trans 'Grün' %}/{% trans 'Rot' %}/{% trans 'Grau' %}/%)" /></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -55,10 +55,14 @@
{{ participant.personal_names }} {{ participant.personal_names }}
</td> </td>
<td> <td>
{{ participant.dav_number }} {% if participant.dav_member %}
{{ participant.dav_number|default:'Fehler! heinzel Bescheid geben!' }}
{% else %}
{% trans 'Nicht Mitglied' %}
{% endif %}
</td> </td>
<td data-order="{{ participant.paid }} {{ event.charge|floatformat:'-2' }}" <td data-order="{{ participant.paid }} {{ event.charge|floatformat:'-2' }}"
data-search="{% if participant.paid %}{% trans 'Grün' %}{% else %}{% trans 'Rot' %}{% endif %} {{ event.charge|floatformat:'-2' }}"> data-search="{% if not event.charge %}{% trans 'Grau' %}{% elif participant.paid %}{% trans 'Grün' %}{% else %}{% trans 'Rot' %}{% endif %} {% if participant.apply_reduced_fee %}%{% endif %} {{ event.charge|floatformat:'-2' }}">
<div class="pull-right"> <div class="pull-right">
<a title="{{ participant.email_address}}" href="mailto:{{ participant.email_address}}">{% bootstrap_icon 'envelope' %}</a> <a title="{{ participant.email_address}}" href="mailto:{{ participant.email_address}}">{% bootstrap_icon 'envelope' %}</a>
<span class="text-info" title="{{ participant.get_info }} <span class="text-info" title="{{ participant.get_info }}
@@ -67,26 +71,37 @@
<form action="" method="post" class="form-inline"> <form action="" method="post" class="form-inline">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="id" value="{{ participant.id }}"> <input type="hidden" name="id" value="{{ participant.id }}">
{% if event.charge and participant.paid %} {% if event.charge %}
<button name="action" value="revoke_payment" <button name="action" value="toggle_reduced_fee"
title="{% trans 'Geldeingang wurde bestätigt' %} - {% trans 'Bestätigung des Geldeingangs zurückziehen' %}" title="{% trans 'Hier klicken, um zwischen voller und reduzierter Teilnahmegebühr umzuschalten' %}"
class="btn btn-link no-padding"> class="btn btn-link no-padding">
<span class="text-success">{% bootstrap_icon 'piggy-bank' %}</span> <span class="text-primary">
</button> {% if participant.apply_reduced_fee %}{% bootstrap_icon 'check' %}{% else %}{% bootstrap_icon 'unchecked' %}{% endif %}
&nbsp;
({{ event.charge|floatformat:'-2' }} €)
{% elif event.charge %}
<button name="action" value="confirm_payment"
title="{% trans 'Geldeingang bestätigen' %}"
class="btn btn-link no-padding">
<span class="text-danger">{% bootstrap_icon 'piggy-bank' %}</span>
</button>
&nbsp;
({{ event.charge|floatformat:'-2' }} €)
{% else %}
<span class="text-muted" title="{% trans 'Keine Teilnehmergebühr gefordert' %}">
{% bootstrap_icon 'piggy-bank' %}
</span> </span>
</button>
&nbsp;
{% endif %}
{% if event.charge and participant.paid %}
<span class="text-success {% if not participant.apply_reduced_fee %}invisible{% endif %}"
title="{% trans 'Reduzierte Teilnahmegebühr' %}"><strong>%</strong></span><button
name="action" value="revoke_payment"
title="{% trans 'Geldeingang wurde bestätigt' %} - {% trans 'Bestätigung des Geldeingangs zurückziehen' %}"
class="btn btn-link no-padding"><span class="text-success">{% bootstrap_icon 'piggy-bank' %}</span></button>
&nbsp;
({{ event.charge|floatformat:'-2' }}{% if participant.apply_reduced_fee %} / 2{% endif %} €)
{% elif event.charge %}
<span class="text-danger {% if not participant.apply_reduced_fee and participant.created_at|date:'U' > '1608662327' %}invisible{% endif %}"
title="{% trans 'Reduzierte Teilnahmegebühr' %}"><strong>{% if participant.apply_reduced_fee %}%{% else %}? {% endif %}</strong></span><button
name="action" value="confirm_payment"
title="{% trans 'Geldeingang bestätigen' %}"
class="btn btn-link no-padding"><span class="text-danger">{% bootstrap_icon 'piggy-bank' %}</span></button>
&nbsp;
({{ event.charge|floatformat:'-2' }}{% if participant.apply_reduced_fee %} / 2{% endif %} €)
{% else %}
<span class="hidden {% if not participant.apply_reduced_fee %}invisible{% endif %}"
title="{% trans 'Reduzierte Teilnahmegebühr' %}"><strong>%</strong></span><span
class="hidden"
title="{% trans 'Keine Teilnehmergebühr gefordert' %}">{% bootstrap_icon 'piggy-bank' %}</span>
{% endif %} {% endif %}
</form> </form>
</td> </td>

View File

@@ -53,6 +53,11 @@ class ParticipantListView(generic.ListView):
participant = get_object_or_404(Participant, pk=participant_id) participant = get_object_or_404(Participant, pk=participant_id)
participant.paid = False participant.paid = False
participant.save() participant.save()
elif action == 'toggle_reduced_fee':
participant_id = request.POST.get('id')
participant = get_object_or_404(Participant, pk=participant_id)
participant.apply_reduced_fee = not participant.apply_reduced_fee
participant.save()
else: else:
messages.error(request, 'unsupported action: {}'.format(action)) messages.error(request, 'unsupported action: {}'.format(action))
return HttpResponseRedirect(reverse('dav_event_office:participant-list')) return HttpResponseRedirect(reverse('dav_event_office:participant-list'))

View File

@@ -1,6 +1,6 @@
from django.contrib import admin from django.contrib import admin
from .models import EventStatus, EventFlag, Event, OneClickAction, Participant from .models import EventStatus, EventFlag, Event, OneClickAction, Participant, TrashedParticipant
@admin.register(EventStatus) @admin.register(EventStatus)
@@ -31,3 +31,8 @@ class OneClickActionAdmin(admin.ModelAdmin):
@admin.register(Participant) @admin.register(Participant)
class ParticipantAdmin(admin.ModelAdmin): class ParticipantAdmin(admin.ModelAdmin):
pass pass
@admin.register(TrashedParticipant)
class TrashedParticipantAdmin(admin.ModelAdmin):
pass

View File

@@ -1,3 +1,4 @@
from . import generic from . import generic
from . import events from . import events
from . import participant from . import participant
from . import registration

View File

@@ -7,7 +7,9 @@ from ..models import Participant
class ParticipantForm(forms.ModelForm): class ParticipantForm(forms.ModelForm):
class Meta: class Meta:
model = Participant model = Participant
exclude = ['event', 'created_at', 'position', 'purge_at'] exclude = ['event', 'created_at', 'position',
'privacy_policy', 'privacy_policy_accepted',
'paid', 'purge_at']
widgets = { widgets = {
'emergency_contact': forms.Textarea(attrs={'rows': 4}), 'emergency_contact': forms.Textarea(attrs={'rows': 4}),
'experience': forms.Textarea(attrs={'rows': 5}), 'experience': forms.Textarea(attrs={'rows': 5}),

View File

@@ -0,0 +1,8 @@
# -*- coding: utf-8 -*-
from django import forms
from django.utils.translation import ugettext_lazy as _
class RegistrationResponseForm(forms.Form):
apply_reduced_fee = forms.BooleanField(required=False,
label=_(u'Reduzierte Teilnahmegebühr'))

View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.29 on 2020-09-25 13:43
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dav_events', '0032_auto_20190605_1400'),
]
operations = [
migrations.AlterField(
model_name='eventstatus',
name='bootstrap_context',
field=models.CharField(blank=True, choices=[('default', 'default'), ('primary', 'primary'), ('success', 'success'), ('info', 'info'), ('warning', 'warning'), ('danger', 'danger'), ('black', 'black')], max_length=20),
),
]

View File

@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.29 on 2020-10-15 15:38
from __future__ import unicode_literals
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dav_events', '0033_auto_20200925_1543'),
]
operations = [
migrations.AddField(
model_name='participant',
name='dav_member',
field=models.BooleanField(default=True, help_text='In Ausnahmefällen nehmen wir auch Nichtmitglieder mit.', verbose_name='DAV Mitglied'),
),
migrations.AlterField(
model_name='participant',
name='dav_number',
field=models.CharField(blank=True, max_length=62, validators=[django.core.validators.RegexValidator('^([0-9]{1,10}/[0-9]{2,10}/)?[0-9]{1,10}(\\*[0-9]{1,10})?(\\*[0-9]{4}\\*[0-9]{4})?([* ][0-9]{8})?$', 'Ungültiges Format.')], verbose_name='DAV Mitgliedsnummer'),
),
]

View File

@@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.29 on 2020-09-29 20:15
from __future__ import unicode_literals
import dav_events.models.eventchange
import dav_events.roles
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('dav_events', '0033_auto_20200925_1543'),
]
operations = [
migrations.CreateModel(
name='EventChange',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('timestamp', models.DateTimeField(default=django.utils.timezone.now)),
('operation', models.CharField(choices=[('update', 'Update'), ('set_flag', 'Raise Flag'), ('unset_flag', 'Lower Flag')], max_length=20)),
('content', models.TextField()),
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='changes', to='dav_events.Event')),
('user', models.ForeignKey(default=dav_events.models.eventchange.get_system_user_id, on_delete=models.SET(dav_events.roles.get_ghost_user), related_name='+', to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['event', 'timestamp'],
},
),
]

View File

@@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.29 on 2020-11-03 10:12
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('dav_events', '0034_auto_20201015_1738'),
('dav_events', '0034_eventchange'),
]
operations = [
]

View File

@@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.29 on 2020-12-03 10:44
from __future__ import unicode_literals
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('dav_events', '0035_merge_20201103_1112'),
]
operations = [
migrations.CreateModel(
name='TrashedParticipant',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('personal_names', models.CharField(max_length=1024, verbose_name='Vorname(n)')),
('family_names', models.CharField(max_length=1024, verbose_name='Familienname')),
('address', models.CharField(help_text='Straße, Hausnummer', max_length=1024, verbose_name='Anschrift')),
('postal_code', models.CharField(max_length=254, verbose_name='Postleitzahl')),
('city', models.CharField(max_length=1024, verbose_name='Ort')),
('email_address', models.EmailField(max_length=254, verbose_name='E-Mail-Adresse')),
('phone_number', models.CharField(max_length=254, verbose_name='Telefonnummer')),
('dav_member', models.BooleanField(default=True, help_text='In Ausnahmefällen nehmen wir auch Nichtmitglieder mit.', verbose_name='DAV Mitglied')),
('dav_number', models.CharField(blank=True, max_length=62, validators=[django.core.validators.RegexValidator('^([0-9]{1,10}/[0-9]{2,10}/)?[0-9]{1,10}(\\*[0-9]{1,10})?(\\*[0-9]{4}\\*[0-9]{4})?([* ][0-9]{8})?$', 'Ungültiges Format.')], verbose_name='DAV Mitgliedsnummer')),
('emergency_contact', models.TextField(blank=True, help_text='Kann frei gelassen werden.', verbose_name='Notfall-Kontakt')),
('experience', models.TextField(blank=True, help_text='Kann frei gelassen werden.', verbose_name='Erfahrung')),
('note', models.TextField(blank=True, help_text='Kann frei gelassen werden.', verbose_name='Anmerkung')),
('paid', models.BooleanField(default=False, verbose_name='Teilnehmerbeitrag bezahlt')),
('purge_at', models.DateTimeField()),
('created_at', models.DateTimeField()),
('trashed_at', models.DateTimeField(auto_now_add=True)),
('position', models.IntegerField(verbose_name='Listennummer')),
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='trashed_participants', to='dav_events.Event')),
],
options={
'verbose_name': 'Gelöschter Teilnehmer (Papierkorb)',
'verbose_name_plural': 'Gelöschte Teilnehmer (Papierkorb)',
'ordering': ['event', 'trashed_at'],
},
),
]

View File

@@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.29 on 2020-12-09 12:27
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dav_events', '0036_trashedparticipant'),
]
operations = [
migrations.AddField(
model_name='participant',
name='year_of_birth',
field=models.IntegerField(default=1870, verbose_name='Geburtsjahr'),
preserve_default=False,
),
migrations.AddField(
model_name='trashedparticipant',
name='year_of_birth',
field=models.IntegerField(default=1870, verbose_name='Geburtsjahr'),
preserve_default=False,
),
]

View File

@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.29 on 2020-12-09 14:42
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dav_events', '0037_auto_20201209_1327'),
]
operations = [
migrations.AddField(
model_name='participant',
name='apply_reduced_fee',
field=models.BooleanField(default=False, verbose_name='Antrag auf reduzierte Teilnahmegebühr'),
),
migrations.AddField(
model_name='trashedparticipant',
name='apply_reduced_fee',
field=models.BooleanField(default=False, verbose_name='Antrag auf reduzierte Teilnahmegebühr'),
),
]

View File

@@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.29 on 2020-12-15 10:55
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dav_events', '0038_auto_20201209_1542'),
]
operations = [
migrations.AlterField(
model_name='participant',
name='apply_reduced_fee',
field=models.BooleanField(default=False, help_text='Für Jugendliche und Junioren (bis zum vollendeten 25. Lebensjahr), sowie Mitglieder mit geringen finanziellen Mitteln (Nachweis durch "Karlsruher Pass"), wird die Teilnahmegebühr auf 50% ermäßigt.', verbose_name='Antrag auf reduzierte Teilnahmegebühr'),
),
migrations.AlterField(
model_name='participant',
name='year_of_birth',
field=models.IntegerField(help_text='Vierstellige Jahreszahl', verbose_name='Geburtsjahr'),
),
migrations.AlterField(
model_name='trashedparticipant',
name='apply_reduced_fee',
field=models.BooleanField(default=False, help_text='Für Jugendliche und Junioren (bis zum vollendeten 25. Lebensjahr), sowie Mitglieder mit geringen finanziellen Mitteln (Nachweis durch "Karlsruher Pass"), wird die Teilnahmegebühr auf 50% ermäßigt.', verbose_name='Antrag auf reduzierte Teilnahmegebühr'),
),
migrations.AlterField(
model_name='trashedparticipant',
name='year_of_birth',
field=models.IntegerField(help_text='Vierstellige Jahreszahl', verbose_name='Geburtsjahr'),
),
]

View File

@@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.29 on 2020-12-16 16:12
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dav_events', '0039_auto_20201215_1155'),
]
operations = [
migrations.AddField(
model_name='participant',
name='privacy_policy',
field=models.TextField(blank=True, verbose_name='Erklärung zur Datenspeicherung'),
),
migrations.AddField(
model_name='participant',
name='privacy_policy_accepted',
field=models.BooleanField(default=False, verbose_name='Einwilligung zur Datenspeicherung'),
),
migrations.AddField(
model_name='trashedparticipant',
name='privacy_policy',
field=models.TextField(blank=True, verbose_name='Erklärung zur Datenspeicherung'),
),
migrations.AddField(
model_name='trashedparticipant',
name='privacy_policy_accepted',
field=models.BooleanField(default=False, verbose_name='Einwilligung zur Datenspeicherung'),
),
]

View File

@@ -1,6 +1,8 @@
from ..roles import get_system_user, get_ghost_user from ..roles import get_system_user, get_ghost_user
from .event import Event from .event import Event
from .eventchange import EventChange
from .eventflag import EventFlag from .eventflag import EventFlag
from .eventstatus import EventStatus from .eventstatus import EventStatus
from .oneclickaction import OneClickAction from .oneclickaction import OneClickAction
from .participant import Participant from .participant import Participant
from .trash import TrashedParticipant

View File

@@ -2,6 +2,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import datetime import datetime
import difflib import difflib
import json
import logging import logging
import os import os
import re import re
@@ -14,14 +15,14 @@ from django.db import models
from django.template.loader import get_template from django.template.loader import get_template
from django.utils.encoding import python_2_unicode_compatible from django.utils.encoding import python_2_unicode_compatible
from django.utils.translation import get_language, ugettext_lazy as _ from django.utils.translation import get_language, ugettext_lazy as _
from django_countries.fields import CountryField from django_countries.fields import Country, CountryField
from . import get_ghost_user
from .. import choices from .. import choices
from .. import config from .. import config
from .. import signals from .. import signals
from ..workflow import DefaultWorkflow from ..workflow import DefaultWorkflow
from . import get_ghost_user
from .eventchange import EventChange
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -292,9 +293,8 @@ class Event(models.Model):
logger.warning('Event is not created by its owner (Current user: %s, Owner: %s)!', self.editor, owner) logger.warning('Event is not created by its owner (Current user: %s, Owner: %s)!', self.editor, owner)
self.owner = owner self.owner = owner
creating = True creating = True
elif not implicit_update: else:
original = Event.objects.get(id=self.id) original = Event.objects.get(id=self.id)
original_text = original.render_as_text(show_internal_fields=True)
if not self.editor or not self.editor.is_authenticated: if not self.editor or not self.editor.is_authenticated:
self.editor = self.owner self.editor = self.owner
@@ -305,13 +305,50 @@ class Event(models.Model):
logger.info('Event created: %s', self) logger.info('Event created: %s', self)
signals.event_created.send(sender=self.__class__, event=self) signals.event_created.send(sender=self.__class__, event=self)
self.workflow.update_status('draft', self.editor) self.workflow.update_status('draft', self.editor)
elif not implicit_update: else:
modified_text = self.render_as_text(show_internal_fields=True) change = EventChange(event=self, user=self.editor, operation=EventChange.UPDATE,
o_lines = original_text.split('\n') content=self.diff(original))
m_lines = modified_text.split('\n') change.save()
diff_lines = list(difflib.unified_diff(o_lines, m_lines, n=len(m_lines), lineterm='')) if not implicit_update:
logger.info('Event updated: %s', self) logger.info('Event updated: %s', self)
signals.event_updated.send(sender=self.__class__, event=self, diff=diff_lines, user=self.editor) signals.event_updated.send(sender=self.__class__, event=self, user=self.editor,
diff=self.diff(original, fmt='human_readable'))
def diff(self, event, fmt='json'):
if fmt == 'human_readable':
from_text = event.render_as_text(show_internal_fields=True)
to_text = self.render_as_text(show_internal_fields=True)
from_lines = from_text.split('\n')
to_lines = to_text.split('\n')
diff_lines = list(difflib.unified_diff(from_lines, to_lines, n=len(from_lines), lineterm=''))
diff_text = '\n'.join(diff_lines[3:])
elif fmt == 'json':
fields = self._meta.get_fields()
changes = []
for field in fields:
field_name = field.name
from_value = getattr(event, field_name)
try:
json.dumps(from_value)
except TypeError:
from_value = str(from_value)
to_value = getattr(self, field_name)
try:
json.dumps(to_value)
except TypeError:
to_value = str(to_value)
if from_value != to_value:
change = {
'field': field_name,
'refer': from_value,
'current': to_value,
}
changes.append(change)
diff_text = json.dumps(changes)
else:
raise ValueError("Event.diff(): Unsupported format: {}".format(fmt))
return diff_text
def is_deadline_expired(self): def is_deadline_expired(self):
today = datetime.date.today() today = datetime.date.today()
@@ -468,6 +505,7 @@ class Event(models.Model):
'course_goal_6': self.course_goal_6, 'course_goal_6': self.course_goal_6,
'planned_publication_date': self.planned_publication_date, 'planned_publication_date': self.planned_publication_date,
'internal_note': self.internal_note, 'internal_note': self.internal_note,
'registration_closed': self.registration_closed,
} }
if context is not None: if context is not None:
r.update(context) r.update(context)

View File

@@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.conf import settings
from django.db import models
from django.utils import timezone
from django.utils.encoding import python_2_unicode_compatible
from . import get_ghost_user, get_system_user
def get_system_user_id():
return get_system_user().id
@python_2_unicode_compatible
class EventChange(models.Model):
UPDATE = 'update'
RAISE_FLAG = 'set_flag'
LOWER_FLAG = 'unset_flag'
OPERATION_CHOICES = (
(UPDATE, 'Update'),
(RAISE_FLAG, 'Raise Flag'),
(LOWER_FLAG, 'Lower Flag'),
)
event = models.ForeignKey('dav_events.Event', related_name='changes')
timestamp = models.DateTimeField(default=timezone.now)
user = models.ForeignKey(settings.AUTH_USER_MODEL,
default=get_system_user_id,
on_delete=models.SET(get_ghost_user),
related_name='+')
operation = models.CharField(max_length=20, choices=OPERATION_CHOICES)
content = models.TextField()
class Meta:
ordering = ['event', 'timestamp']
def __str__(self):
s = '{timestamp} - {user} - {operation}'
return s.format(operation=self.operation, timestamp=self.timestamp.strftime('%d.%m.%Y %H:%M:%S %Z'),
user=self.user)

View File

@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
import datetime import datetime
from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.utils import timezone from django.utils import timezone
from django.utils.encoding import python_2_unicode_compatible from django.utils.encoding import python_2_unicode_compatible
@@ -12,12 +13,7 @@ midnight = datetime.time(00, 00, 00)
@python_2_unicode_compatible @python_2_unicode_compatible
class Participant(models.Model): class AbstractParticipant(models.Model):
event = models.ForeignKey('Event', related_name='participants')
created_at = models.DateTimeField(auto_now_add=True)
position = models.IntegerField(verbose_name='Listennummer')
personal_names = models.CharField(max_length=1024, personal_names = models.CharField(max_length=1024,
verbose_name=_('Vorname(n)')) verbose_name=_('Vorname(n)'))
family_names = models.CharField(max_length=1024, family_names = models.CharField(max_length=1024,
@@ -32,8 +28,20 @@ class Participant(models.Model):
email_address = models.EmailField(verbose_name=_('E-Mail-Adresse')) email_address = models.EmailField(verbose_name=_('E-Mail-Adresse'))
phone_number = models.CharField(max_length=254, phone_number = models.CharField(max_length=254,
verbose_name=_('Telefonnummer')) verbose_name=_('Telefonnummer'))
year_of_birth = models.IntegerField(verbose_name=_('Geburtsjahr'),
help_text=_('Vierstellige Jahreszahl'))
apply_reduced_fee = models.BooleanField(default=False,
verbose_name=_('Antrag auf reduzierte Teilnahmegebühr'),
help_text=_('Für Jugendliche und Junioren'
' (bis zum vollendeten 25. Lebensjahr),'
' sowie Mitglieder mit geringen finanziellen Mitteln'
' (Nachweis durch "Karlsruher Pass"),'
' wird die Teilnahmegebühr auf 50% ermäßigt.'))
dav_member = models.BooleanField(default=True,
verbose_name=_('DAV Mitglied'),
help_text=_('In Ausnahmefällen nehmen wir auch Nichtmitglieder mit.'))
dav_number = models.CharField(max_length=62, dav_number = models.CharField(max_length=62,
validators=[DAVNumberValidator], blank=True, validators=[DAVNumberValidator],
verbose_name=_('DAV Mitgliedsnummer')) verbose_name=_('DAV Mitgliedsnummer'))
emergency_contact = models.TextField(blank=True, emergency_contact = models.TextField(blank=True,
verbose_name=_('Notfall-Kontakt'), verbose_name=_('Notfall-Kontakt'),
@@ -45,22 +53,25 @@ class Participant(models.Model):
verbose_name=_('Anmerkung'), verbose_name=_('Anmerkung'),
help_text=_('Kann frei gelassen werden.')) help_text=_('Kann frei gelassen werden.'))
privacy_policy = models.TextField(blank=True,
verbose_name=_('Erklärung zur Datenspeicherung'))
privacy_policy_accepted = models.BooleanField(default=False,
verbose_name=_('Einwilligung zur Datenspeicherung'))
paid = models.BooleanField('Teilnehmerbeitrag bezahlt', default=False) paid = models.BooleanField('Teilnehmerbeitrag bezahlt', default=False)
purge_at = models.DateTimeField() purge_at = models.DateTimeField()
def approx_age(self):
now = datetime.datetime.now()
year_now = now.year
return year_now - self.year_of_birth
class Meta: class Meta:
unique_together = (('event', 'position'), ) abstract = True
verbose_name = _('Teilnehmer')
verbose_name_plural = _('Teilnehmer')
ordering = ['event', 'position']
def __str__(self): def __str__(self):
return '{eventnumber} - {position}. {name}'.format( return self.get_full_name()
eventnumber=self.event.get_number(),
position=self.position,
name=self.get_full_name(),
)
def get_full_name(self): def get_full_name(self):
return '{} {}'.format(self.personal_names, self.family_names) return '{} {}'.format(self.personal_names, self.family_names)
@@ -69,26 +80,56 @@ class Participant(models.Model):
text = """{fullname} text = """{fullname}
{address}, {postal_code} {city} {address}, {postal_code} {city}
DAV Mitglied: {dav_info}
Jahrgang: {year_of_birth} (ungefähres Alter: {approx_age})
Antrag auf reduzierte Teilnehmergebühr: {apply_reduced_fee_yesno}
Notfallkontakt: Notfallkontakt:
{emergency_contact} {emergency_contact}
Anmerkung: Anmerkung:
{note} {note}
""" """
if not self.dav_member:
dav_info = _('Nein')
else:
dav_info = self.dav_number
if self.apply_reduced_fee:
apply_reduced_fee_yesno = _('Ja')
else:
apply_reduced_fee_yesno = _('Nein')
return text.format( return text.format(
fullname=self.get_full_name(), fullname=self.get_full_name(),
address=self.address, address=self.address,
postal_code=self.postal_code, postal_code=self.postal_code,
city=self.city, city=self.city,
dav_info=dav_info,
year_of_birth=self.year_of_birth,
approx_age=self.approx_age(),
apply_reduced_fee_yesno=apply_reduced_fee_yesno,
emergency_contact=self.emergency_contact, emergency_contact=self.emergency_contact,
note=self.note, note=self.note,
) )
def get_data_dict(self):
data = {}
for field in self._meta.fields:
if not field.primary_key:
data[field.name] = getattr(self, field.name)
return data
def clean(self):
if self.dav_member and not self.dav_number:
raise ValidationError({'dav_number': _('Bei DAV Mitgliedern brauchen wir die Mitgliedsnummer.')})
def save(self, **kwargs): def save(self, **kwargs):
if not self.purge_at and self.event: if not self.purge_at and self.event:
self.purge_at = self.__class__.calc_purge_at(self.event) self.purge_at = self.__class__.calc_purge_at(self.event)
super(Participant, self).save(**kwargs) self.full_clean()
super(AbstractParticipant, self).save(**kwargs)
@staticmethod @staticmethod
def calc_purge_at(event): def calc_purge_at(event):
@@ -115,3 +156,23 @@ class Participant(models.Model):
purge_date = july_nextyear purge_date = july_nextyear
return timezone.make_aware(datetime.datetime.combine(purge_date, midnight)) return timezone.make_aware(datetime.datetime.combine(purge_date, midnight))
@python_2_unicode_compatible
class Participant(AbstractParticipant):
event = models.ForeignKey('Event', related_name='participants')
created_at = models.DateTimeField(auto_now_add=True)
position = models.IntegerField(verbose_name='Listennummer')
class Meta:
verbose_name = _('Teilnehmer')
verbose_name_plural = _('Teilnehmer')
unique_together = (('event', 'position'), )
ordering = ['event', 'position']
def __str__(self):
return '{eventnumber} - {position}. {name}'.format(
eventnumber=self.event.get_number(),
position=self.position,
name=self.get_full_name(),
)

View File

@@ -0,0 +1 @@
from .trashed_participant import TrashedParticipant

View File

@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models
from django.utils.encoding import python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _
from ..participant import AbstractParticipant
@python_2_unicode_compatible
class TrashedParticipant(AbstractParticipant):
event = models.ForeignKey('Event', related_name='trashed_participants')
created_at = models.DateTimeField()
trashed_at = models.DateTimeField(auto_now_add=True)
position = models.IntegerField(verbose_name='Listennummer')
class Meta:
verbose_name = _('Gelöschter Teilnehmer (Papierkorb)')
verbose_name_plural = _('Gelöschte Teilnehmer (Papierkorb)')
ordering = ['event', 'trashed_at']
def __str__(self):
return '{eventnumber} - {name}'.format(
eventnumber=self.event.get_number(),
name=self.get_full_name(),
)

View File

@@ -54,6 +54,7 @@
{% trans 'Schwierigkeitsnivau' %}: {{ event.get_level_display }} {% trans 'Schwierigkeitsnivau' %}: {{ event.get_level_display }}
{% if event.sport == 'S' %}{% trans 'Skiliftbenutzung' %}: {% if event.ski_list %}{% trans 'Ja' %}{% else %}{% trans 'Nein' %}{% endif %} {% if event.sport == 'S' %}{% trans 'Skiliftbenutzung' %}: {% if event.ski_list %}{% trans 'Ja' %}{% else %}{% trans 'Nein' %}{% endif %}
{% endif %}{% trans 'Gelände' %}: {{ event.get_terrain_display }} {% endif %}{% trans 'Gelände' %}: {{ event.get_terrain_display }}
{% trans 'Anmeldung' %}: {% if registration_required %}{% if registration_closed %}{% trans 'Geschlossen' %}{% else %}{% trans 'Erforderlich' %}{% endif %}{% else %}{% trans 'Nicht erforderlich' %}{% endif %}
{% trans 'Anreise des Kurs-/Tourenleiters am Vortag' %}: {% if event.arrival_previous_day %}{% trans 'Ja' %}{% else %}{% trans 'Nein' %}{% endif %} {% trans 'Anreise des Kurs-/Tourenleiters am Vortag' %}: {% if event.arrival_previous_day %}{% trans 'Ja' %}{% else %}{% trans 'Nein' %}{% endif %}
{% trans 'Veröffentlichung' %}: {% if planned_publication_date %}{{ planned_publication_date|date:'l, d. F Y' }}{% else %}{% trans 'sofort' %}{% endif %} {% trans 'Veröffentlichung' %}: {% if planned_publication_date %}{{ planned_publication_date|date:'l, d. F Y' }}{% else %}{% trans 'sofort' %}{% endif %}
{% if internal_note %} {% if internal_note %}

View File

@@ -40,11 +40,9 @@
{% bootstrap_icon 'remove' %}&thinsp; {% bootstrap_icon 'remove' %}&thinsp;
{% trans 'Abbrechen' %} {% trans 'Abbrechen' %}
</a> </a>
<!--
<button id="btn-form-save" type="submit" name="save" class="btn btn-info"> <button id="btn-form-save" type="submit" name="save" class="btn btn-info">
{% bootstrap_icon 'hdd' %}&thinsp; {% bootstrap_icon 'hdd' %}&thinsp;
{% trans 'Als Entwurf speichern' %} {% trans 'Als Entwurf speichern' %}
</button> </button>
-->
{% endbuttons %} {% endbuttons %}
{% endblock form-buttons %} {% endblock form-buttons %}

View File

@@ -1,6 +1,7 @@
{% extends 'dav_events/base.html' %} {% extends 'dav_events/base.html' %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% load i18n %} {% load i18n %}
{% load dav_events %}
{% block head-title %}{{ event }} - {{ block.super }}{% endblock head-title %} {% block head-title %}{{ event }} - {{ block.super }}{% endblock head-title %}
@@ -188,35 +189,77 @@
<div class="row"> <div class="row">
<div class="col-sm-12"> <div class="col-sm-12">
{{ event.render_as_html }} {{ event.render_as_html }}
<div class="panel panel-default">
<div class="panel-body">
<div class="row"> <div class="row">
<div class="col-sm-7"> <div class="col-sm-7">
<h5>Status-Log</h5> <div class="panel-group" id="log-accordion" role="tablist" aria-multiselectable="true">
<div class="panel panel-default">
<div id="headingStatusLog" class="panel-heading" role="tab">
<h5 class="panel-title">
<a role="button" href="#collapseStatusLog" data-toggle="collapse"
data-parent="#log-accordion" aria-expanded="true"
aria-controls="collapseStatusLog">
Status-Flags
</a>
</h5>
</div>
<div id="collapseStatusLog" class="panel-collapse collapse in"
role="tabpanel" aria-labelledby="headingStatusLog">
<div class="panel-body">
{% for flag in event.flags.all %} {% for flag in event.flags.all %}
<div class="row"> <div class="row">
<div class="col-sm-5"> <div class="col-sm-4">
<span class="text-{{ flag.status.bootstrap_context|default:'default' }}">{% bootstrap_icon 'check' %}</span> <span class="text-{{ flag.status.bootstrap_context|default:'default' }}">{% bootstrap_icon 'check' %}</span>
<strong>{{ flag.status.label }}:</strong> <strong>{{ flag.status.label }}:</strong>
</div> </div>
<div class="col-sm-7"> <div class="col-sm-8">
{{ flag.timestamp|date:'l, d. F Y, H:i' }} {% trans 'Uhr' %}<br /> {{ flag.timestamp|date:'l, d. F Y, H:i' }} {% trans 'Uhr' %}<br />
{% trans 'von' %} {{ flag.user.get_full_name|default:flag.user }} {% trans 'von' %} {{ flag.user.get_full_name|default:flag.user }}
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
</div>
</div>
<div class="panel panel-default">
<div id="headingChangeLog" class="panel-heading" role="tab">
<h5 class="panel-title">
<a role="button" href="#collapseChangeLog" data-toggle="collapse"
data-parent="#log-accordion" aria-expanded="true"
aria-controls="collapseChangeLog">
Change-Log
</a>
</h5>
</div>
<div id="collapseChangeLog" class="panel-collapse collapse"
role="tabpanel" aria-labelledby="headingChangeLog">
<div class="panel-body">
{% render_event_changelog event %}
</div>
</div>
</div>
</div>
</div>
<div class="col-sm-5"> <div class="col-sm-5">
<h5>{% trans 'Veröffentlichung' %}</h5> {% if event.internal_note %}
<div class="panel panel-danger">
<div class="panel-heading">
<h5 class="panel-title">{% trans 'Bearbeitungshinweis' %}</h5>
</div>
<div class="panel-body">
<div><small>{{ event.internal_note|linebreaksbr }}</small></div>
</div>
</div>
{% endif %}
<div class="panel panel-default">
<div class="panel-heading">
<h5 class="panel-title">{% trans 'Veröffentlichung' %}</h5>
</div>
<div class="panel-body">
{% if event.planned_publication_date %} {% if event.planned_publication_date %}
{{ event.planned_publication_date|date:'l, d. F Y' }} {{ event.planned_publication_date|date:'l, d. F Y' }}
{% else %} {% else %}
{% trans 'Unverzüglich' %} {% trans 'Unverzüglich' %}
{% endif %} {% endif %}
{% if event.internal_note %}
<h5 style="margin-top: 1em;">{% trans 'Bearbeitungshinweis' %}</h5>
<div class="well well-sm"><small>{{ event.internal_note|linebreaksbr }}</small></div>
{% endif %}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -222,12 +222,40 @@
</div> </div>
</div> </div>
</div> </div>
<hr /> <hr />
<div class="pull-right text-info" style="margin-right: 1em;"
title="Sobald du im Kasten 'Anmeldungen' bei einzelnen Personen auf das Plus-Symbol geklickt hast, oder jemanden mit dem Formular unter 'weiteren Teilnehmer hinzufügen' eingetragen hast, erscheinen diese Personen weiter unten als Teilnehmer (graue Kästen).
Diese Teilnehmerliste muss zum Zeitpunkt der Tour wirklich stimmen, da wir sie im Falle eines Unfalls brauchen!
Das rote oder grüne Sparschweinchensymbol am rechten Rand eines Teilnehmerkastens zeigt dir, ob die Geschäftstelle bereits den Teilnehmerbeitrag erhalten bzw. zugeordnet hat.
Wenn mehr Teilnehmer eingetragen sind, als in der Teilnehmerzahl der Ausschreibung angegeben, werden die entsprechenden Teilnehmer in gelben Kästen dargestellt.
Wichtig: das System verschickt keine Zu- oder Absagen an die Teilnehmer!
Das musst du selbst per E-Mail oder telefonisch machen.
">
{% bootstrap_icon 'question-sign' %}
</div>
<h4>{% trans 'Teilnehmer' %}</h4> <h4>{% trans 'Teilnehmer' %}</h4>
<div class="panel-group" id="form-accordion-participants" role="tablist" aria-multiselectable="true"> <div class="panel-group" id="form-accordion-participants" role="tablist" aria-multiselectable="true">
{% if registrations_support %} {% if registrations_support %}
<div class="panel panel-info"> <div class="panel panel-info">
<div id="headingRegistrations" class="panel-heading" role="tab"> <div id="headingRegistrations" class="panel-heading" role="tab">
<div class="pull-right text-info" title="Unter Anmeldungen siehst du, wer gerne mit auf deine Tour möchte.
- Wenn du jemanden davon mitnehmen möchtest, schicke ihm per E-Mail eine Zusage und klicke dann auf das Plus-Symbol.
Die Person wird dann automatisch auf die Teilnehmerliste übernommen.
- Wenn du jemanden nicht mitnehmen möchtest, schicke ihm per E-Mail eine Absage und klicke dann auf das Minus-Symbol.
Nach einem Klick auf Plus oder Minus werden die entsprechenden Zeilen in den Papierkorb verschoben.
Wichtig: das System verschickt keine Zu- oder Absagen an die Teilnehmer!
Das musst du selbst (per E-Mail oder telefonisch) machen.
">
{% bootstrap_icon 'question-sign' %}
</div>
<h5 class="panel-title"> <h5 class="panel-title">
<a role="button" href="#collapseRegistrations" <a role="button" href="#collapseRegistrations"
data-toggle="collapse" data-toggle="collapse"
@@ -236,18 +264,25 @@
</a> </a>
</h5> </h5>
</div> </div>
<div id="collapseRegistrations" class="panel-collapse collapse {% if registrations %}in{% endif %}" <div id="collapseRegistrations" class="panel-collapse collapse {% if registrations_pending %}in{% endif %}"
role="tabpanel" aria-labelledby="headingRegistrations"> role="tabpanel" aria-labelledby="headingRegistrations">
<div class="panel-body"> <div class="panel-body">
{% for registration in registrations %} {% for registration in registrations_pending %}
<form action="" method="post" class="form-inline"> <form action="" method="post" class="form-inline">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="registration" value="{{ registration.id }}"> <input type="hidden" name="registration" value="{{ registration.id }}">
{% if has_permission_update_participants %} {% if has_permission_update_participants %}
{% if registration.apply_reduced_fee %}
<a href="{% url 'dav_events:respond_registration' registration.pk %}"
class="btn btn-link no-padding" title="zur Teilnehmerliste hinzufügen">
<span class="text-success">{% bootstrap_icon 'plus-sign' %}</span>
</a>
{% else %}
<button type="submit" name="action" value="accept_registration" <button type="submit" name="action" value="accept_registration"
class="btn btn-link no-padding" title="zur Teilnehmerliste hinzufügen"> class="btn btn-link no-padding" title="zur Teilnehmerliste hinzufügen">
<span class="text-success">{% bootstrap_icon 'plus-sign' %}</span> <span class="text-success">{% bootstrap_icon 'plus-sign' %}</span>
</button> </button>
{% endif %}
&nbsp; &nbsp;
<button type="submit" name="action" value="reject_registration" <button type="submit" name="action" value="reject_registration"
class="btn btn-link no-padding" title="Anmeldung löschen"> class="btn btn-link no-padding" title="Anmeldung löschen">
@@ -255,9 +290,6 @@
</button> </button>
&nbsp; &nbsp;
{% endif %} {% endif %}
{% if registration.answered %}
<span class="text-muted">
{% endif %}
{{ registration.get_full_name }} {{ registration.get_full_name }}
(<a href="mailto:{{ registration.email_address }}">{{ registration.email_address }}</a>, (<a href="mailto:{{ registration.email_address }}">{{ registration.email_address }}</a>,
{{ registration.phone_number }}) {{ registration.phone_number }})
@@ -270,7 +302,10 @@
<span title="{{ registration.get_info }}"> <span title="{{ registration.get_info }}">
{% bootstrap_icon 'info-sign' %} {% bootstrap_icon 'info-sign' %}
</span> </span>
{% if registration.answered %} {% if registration.apply_reduced_fee %}
&nbsp;
<span class="text-info">
<strong title="{% trans 'reduzierte Teilnahmegebühr' %}">%</strong>{% bootstrap_icon 'piggy-bank' %} (reduzierte Gebühr)
</span> </span>
{% endif %} {% endif %}
</form> </form>
@@ -284,6 +319,14 @@
{% if has_permission_update_participants %} {% if has_permission_update_participants %}
<div class="panel panel-info"> <div class="panel panel-info">
<div id="headingAddParticipant" class="panel-heading" role="tab"> <div id="headingAddParticipant" class="panel-heading" role="tab">
<div class="pull-right text-info" title="Wenn du jemanden in die Teilnehmerliste aufnehmen möchtest,
der nicht unter 'Anmeldungen' steht, klicke auf 'weiteren Teilnehmer hinzufügen' um das Teilnehmerformular aufzuklappen.
Über das Formular kannst du dann die Teilnehmerdaten eintragen und speichern.
Wichtig: das System verschickt keine Bestätigung an dich oder den neuen Teilnehmer.
">
{% bootstrap_icon 'question-sign' %}
</div>
<h5 class="panel-title"> <h5 class="panel-title">
<a role="button" href="#collapseAddParticipant" <a role="button" href="#collapseAddParticipant"
data-toggle="collapse" data-toggle="collapse"
@@ -314,16 +357,25 @@
{% with position=participant.position %} {% with position=participant.position %}
<div class="panel {% if event.max_participants and position > event.max_participants %}panel-warning{% else %}panel-default{% endif %}"> <div class="panel {% if event.max_participants and position > event.max_participants %}panel-warning{% else %}panel-default{% endif %}">
<div id="headingParticipant_{{ participant.id }}" class="panel-heading" role="tab"> <div id="headingParticipant_{{ participant.id }}" class="panel-heading" role="tab">
<h5 class="panel-title"> <div>
<strong><span class="panel-title">
<a role="button" href="#collapseParticipant_{{ participant.id }}" <a role="button" href="#collapseParticipant_{{ participant.id }}"
data-toggle="collapse" data-toggle="collapse"
aria-expanded="true" aria-controls="collapseParticipant_{{ participant.id }}"> aria-expanded="true" aria-controls="collapseParticipant_{{ participant.id }}">
<span class="caret"></span>&nbsp;&nbsp; <span class="caret"></span>&nbsp;&nbsp;
{{ position }}. {{ participant.get_full_name }} {{ position }}. {{ participant.get_full_name }}
</a> </a>
</span></strong>
&nbsp;
<small> <small>
(<a href="mailto:{{ participant.email_address }}">{{ participant.email_address }}</a>, {{ participant.phone_number }}) (<a href="mailto:{{ participant.email_address }}">{{ participant.email_address }}</a>, {{ participant.phone_number }})
</small> </small>
&nbsp;
<span class="text-info"
title="{{ participant.get_info }}
{% trans 'Zeitpunkt der automatischen Löschung' %}: {{ participant.purge_at|date:'d. F Y' }}">
{% bootstrap_icon 'info-sign' %}
</span>
<div class="pull-right"> <div class="pull-right">
<form action="" method="post" class="form-inline"> <form action="" method="post" class="form-inline">
{% csrf_token %} {% csrf_token %}
@@ -339,42 +391,47 @@
class="btn btn-link no-padding {% if forloop.last %}invisible{% endif %}"> class="btn btn-link no-padding {% if forloop.last %}invisible{% endif %}">
<span class="text-info">{% bootstrap_icon 'triangle-bottom' %}</span> <span class="text-info">{% bootstrap_icon 'triangle-bottom' %}</span>
</button> </button>
<button name="action" value="remove_participant" <button name="action" value="trash_participant"
title="{% trans 'Teilnehmer jetzt löschen' %} title="{% trans 'Eintrag in Papierkorb verschieben' %}"
({% trans 'erfolgt automatisch am' %} {{ participant.purge_at|date:'d. F Y'}})"
class="btn btn-link no-padding"> class="btn btn-link no-padding">
<span class="text-danger">{% bootstrap_icon 'remove-circle' %}</span> <span class="text-danger">{% bootstrap_icon 'trash' %}</span>
</button> </button>
&nbsp;
{% endif %} {% endif %}
{% if event.charge and participant.paid and has_permission_payment %} {% if event.charge and participant.paid and has_permission_payment %}
<button name="action" value="revoke_payment" &nbsp;
<span class="text-success {% if not participant.apply_reduced_fee %}invisible{% endif %}"
title="{% trans 'Reduzierte Teilnahmegebühr' %}"><strong>%</strong></span><button
name="action" value="revoke_payment"
title="{% trans 'Geldeingang wurde bestätigt' %} - {% trans 'Bestätigung des Geldeingangs zurückziehen' %}" title="{% trans 'Geldeingang wurde bestätigt' %} - {% trans 'Bestätigung des Geldeingangs zurückziehen' %}"
class="btn btn-link no-padding"> class="btn btn-link no-padding"><span class="text-success">{% bootstrap_icon 'piggy-bank' %}</span></button>
<span class="text-success">{% bootstrap_icon 'piggy-bank' %}</span>
</button>
{% elif event.charge and participant.paid %} {% elif event.charge and participant.paid %}
<span class="text-success" title="{% trans 'Geldeingang bestätigt' %}"> &nbsp;
{% bootstrap_icon 'piggy-bank' %} <span class="text-success {% if not participant.apply_reduced_fee %}invisible{% endif %}"
</span> title="{% trans 'Reduzierte Teilnahmegebühr' %}"><strong>%</strong></span><span
class="text-success"
title="{% trans 'Geldeingang bestätigt' %}">{% bootstrap_icon 'piggy-bank' %}</span>
{% elif event.charge and has_permission_payment %} {% elif event.charge and has_permission_payment %}
<button name="action" value="confirm_payment" &nbsp;
<span class="text-danger {% if not participant.apply_reduced_fee %}invisible{% endif %}"
title="{% trans 'Reduzierte Teilnahmegebühr' %}"><strong>%</strong></span><button
name="action" value="confirm_payment"
title="{% trans 'Geldeingang bestätigen' %}" title="{% trans 'Geldeingang bestätigen' %}"
class="btn btn-link no-padding"> class="btn btn-link no-padding"><span class="text-danger">{% bootstrap_icon 'piggy-bank' %}</span></button>
<span class="text-danger">{% bootstrap_icon 'piggy-bank' %}</span>
</button>
{% elif event.charge %} {% elif event.charge %}
<span class="text-danger" title="{% trans 'Geldeingang unbestätigt' %}"> &nbsp;
{% bootstrap_icon 'piggy-bank' %} <span class="text-danger {% if not participant.apply_reduced_fee %}invisible{% endif %}"
</span> title="{% trans 'Reduzierte Teilnahmegebühr' %}"><strong>%</strong></span><span
class="text-danger"
title="{% trans 'Geldeingang unbestätigt' %}">{% bootstrap_icon 'piggy-bank' %}</span>
{% else %} {% else %}
<span class="hidden" title="{% trans 'Keine Teilnehmergebühr gefordert' %}"> <span class="hidden {% if not participant.apply_reduced_fee %}invisible{% endif %}"
{% bootstrap_icon 'piggy-bank' %} title="{% trans 'Reduzierte Teilnahmegebühr' %}"><strong>%</strong></span><span
</span> class="hidden"
title="{% trans 'Keine Teilnehmergebühr gefordert' %}">{% bootstrap_icon 'piggy-bank' %}</span>
{% endif %} {% endif %}
</form> </form>
</div> </div>
</h5> </div>
</div> </div>
<div id="collapseParticipant_{{ participant.id }}" <div id="collapseParticipant_{{ participant.id }}"
class="panel-collapse collapse {% if form.errors %}in{% endif %}" class="panel-collapse collapse {% if form.errors %}in{% endif %}"
@@ -401,6 +458,10 @@
{% else %} {% else %}
<div class="panel panel-info"> <div class="panel panel-info">
<div class="panel-body"> <div class="panel-body">
<div class="pull-right text-info" title="Sobald du im Kasten 'Anmeldungen' bei einzelnen Personen auf das Plus-Symbol geklickt hast, oder jemanden mit dem Formular unter 'weiteren Teilnehmer hinzufügen' eingetragen hast, erscheinen diese Personen hier als Teilnehmer.
">
{% bootstrap_icon 'question-sign' %}
</div>
<span class="text-info">{% trans 'Es wurden noch keine Teilnehmer hinzugefügt.' %}</span> <span class="text-info">{% trans 'Es wurden noch keine Teilnehmer hinzugefügt.' %}</span>
</div> </div>
</div> </div>
@@ -414,6 +475,159 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
<hr />
<div class="pull-right text-info" style="margin-right: 1em;"
title="Wenn du Anmeldungen mit einem Klick auf das Plus- oder Minus-Symbol 'bearbeitest', oder Teilnehmer aus der Teilnehmerliste entfernst, dann sind diese Einträge danach noch im Papierkorb zu finden.
">
{% bootstrap_icon 'question-sign' %}
</div>
<h4>{% trans 'Papierkorb' %}</h4>
<div class="panel-group" id="form-accordion-trash" role="tablist" aria-multiselectable="true">
{% if registrations_support and registrations_answered %}
<div class="panel panel-info">
<div id="headingAnsweredRegistrations" class="panel-heading" role="tab">
<div class="pull-right text-info" title="Wenn du unter 'Anmeldungen' auf das Plus- oder Minus-Symbol geklickt hast, dann sind diese Einträge hier zu sehen.
">
{% bootstrap_icon 'question-sign' %}
</div>
<h5 class="panel-title">
<a role="button" href="#collapseAnsweredRegistrations"
data-toggle="collapse"
aria-expanded="true" aria-controls="collapseAnsweredRegistrations">
<span class="caret"></span>&nbsp;&nbsp;{% trans 'Bearbeitete Anmeldungen' %}
</a>
</h5>
</div>
<div id="collapseAnsweredRegistrations" class="panel-collapse collapse"
role="tabpanel" aria-labelledby="headingAnsweredRegistrations">
<div class="panel-body">
{% for registration in registrations_answered %}
<div>
<button disabled="disabled"
class="btn btn-link no-padding" title="Bei dieser Anmeldung hast du bereits
am {{ registration.status.updated_at|date:'d. F Y, G:i' }}
auf {% if registration.status.accepted == True %}Plus{% elif registration.status.accepted == False %}Minus{% else %}Plus oder Minus{% endif %} geklickt.
">
<span class="{% if registration.status.accepted == True %}text-success{% else %}text-muted{% endif %}">{% bootstrap_icon 'plus-sign' %}</span>
</button>
&nbsp;
<button disabled="disabled"
class="btn btn-link no-padding" title="Bei dieser Anmeldung hast du bereits
am {{ registration.status.updated_at|date:'d. F Y, G:i' }}
auf {% if registration.status.accepted == True %}Plus{% elif registration.status.accepted == False %}Minus{% else %}Plus oder Minus{% endif %} geklickt.
">
<span class="{% if registration.status.accepted == False %}text-danger{% else %}text-muted{% endif %}">{% bootstrap_icon 'minus-sign' %}</span>
</button>
&nbsp;
<span class="text-muted">
{{ registration.get_full_name }}
(<a href="mailto:{{ registration.email_address }}">{{ registration.email_address }}</a>,
{{ registration.phone_number }})
&nbsp;
<span title="Anmeldezeitpunkt">
{% bootstrap_icon 'time' %}
{{ registration.created_at|date:'d. F Y, G:i' }}
</span>
&nbsp;
<span title="{{ registration.get_info }}">
{% bootstrap_icon 'info-sign' %}
</span>
</span>
{% if has_permission_update_registration %}
<div class="pull-right">
<form action="" method="post" class="form-inline">
{% csrf_token %}
<input type="hidden" name="registration" value="{{ registration.id }}">
<button type="submit" name="action" value="untrash_registration"
class="btn btn-link no-padding"
title="{% trans 'Eintrag in Anmeldungen zurückholen' %}">
<span class="text-success">&#9851;</span>
</button>
</form>
</div>
{% endif %}
</div>
{% endfor %}
</div>
</div>
</div>
{% endif %}
{% if participants_trash %}
<div class="panel panel-info">
<div id="headingTrashedParticipants" class="panel-heading" role="tab">
<div class="pull-right text-info" title="Wenn du Teilnehmer deiner Teilnehmerliste hinzugefügt hast und sie später wieder entfernt hast, dann tauchen diese Einträge hier auf.">
{% bootstrap_icon 'question-sign' %}
</div>
<h5 class="panel-title">
<a role="button" href="#collapseTrashedParticipants"
data-toggle="collapse"
aria-expanded="true" aria-controls="collapseTrashedParticipants">
<span class="caret"></span>&nbsp;&nbsp;{% trans 'Gelöschte Teilnehmer' %}
</a>
</h5>
</div>
<div id="collapseTrashedParticipants" class="panel-collapse collapse"
role="tabpanel" aria-labelledby="headingTrashedParticipants">
<div class="panel-body">
{% for participant in participants_trash %}
<div>
<button disabled="disabled"
class="btn btn-link no-padding" title="Diesen Teilnehmer hast du
am {{ participant.trashed_at|date:'d. F Y, G:i' }}
von Position {{ participant.position }} der Teilnehmerliste entfernt.
">
<span class="text-danger">{% bootstrap_icon 'trash' %}</span>
</button>
&nbsp;
<span class="text-muted">
{{ participant.get_full_name }}
(<a href="mailto:{{ participant.email_address }}">{{ participant.email_address }}</a>,
{{ participant.phone_number }})
&nbsp;
<span title="{{ participant.get_info }}
{% trans 'Zeitpunkt der automatischen Löschung' %}: {{ participant.purge_at|date:'d. F Y' }}">
{% bootstrap_icon 'info-sign' %}
</span>
{% if event.charge and participant.paid %}
&nbsp;
<span class="text-success {% if not participant.apply_reduced_fee %}invisible{% endif %}"
title="{% trans 'Reduzierte Teilnahmegebühr' %}"><strong>%</strong></span><span
class="text-success"
title="{% trans 'Geldeingang bestätigt' %}">{% bootstrap_icon 'piggy-bank' %}</span>
{% elif event.charge %}
&nbsp;
<span class="text-danger {% if not participant.apply_reduced_fee %}invisible{% endif %}"
title="{% trans 'Reduzierte Teilnahmegebühr' %}"><strong>%</strong></span><span
class="text-danger"
title="{% trans 'Geldeingang unbestätigt' %}">{% bootstrap_icon 'piggy-bank' %}</span>
{% endif %}
</span>
{% if has_permission_update_participants %}
<div class="pull-right">
<form action="" method="post" class="form-inline">
{% csrf_token %}
<input type="hidden" name="id" value="{{ participant.id }}">
<button name="action" value="untrash_participant"
title="{% trans 'Eintrag in Teilnehmerliste zurückholen' %}"
class="btn btn-link no-padding">
<span class="text-success">&#9851;</span>
</button>
</form>
</div>
{% endif %}
</div>
{% endfor %}
</div>
</div>
</div>
{% endif %}
{% if not registrations_answered and not participants_trash %}
<span class="text-muted small">{% trans 'Der Papierkorb ist leer.' %}</span>
{% endif %}
</div>
</div> </div>
</div> </div>
{% endblock page-container-fluid %} {% endblock page-container-fluid %}

View File

@@ -29,9 +29,20 @@
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-sm-6"> <div class="col-sm-3">
{% bootstrap_field form.year_of_birth %}
</div>
<div class="col-sm-9">
{% bootstrap_field form.apply_reduced_fee %}
</div>
</div>
<div class="row">
<div class="col-sm-3">
{% bootstrap_field form.dav_number %} {% bootstrap_field form.dav_number %}
</div> </div>
<div class="col-sm-3">
{% bootstrap_field form.dav_member %}
</div>
<div class="col-sm-6"> <div class="col-sm-6">
{% bootstrap_field form.emergency_contact %} {% bootstrap_field form.emergency_contact %}
</div> </div>

View File

@@ -0,0 +1,45 @@
{% extends 'dav_events/base.html' %}
{% load bootstrap3 %}
{% load i18n %}
{% block head-title %}
{% trans 'Anmeldung' %} {{ registration.get_full_name }} - {{ registration.event.number }} - {{ block.super }}
{% endblock head-title %}
{% block page-container %}
<div class="well">
<p>
Hallo {{ registration.event.trainer_firstname }},
</p>
<p>
du hast sicherlich schon gesehen, dass {{ registration.get_full_name }} angekreuzt hat,
die reduzierte Teilnahmegebühr zahlen zu wollen.
</p>
<p>
Für Jugendliche und Junioren sowie Mitglieder mit geringen finanziellen Mitteln (Nachweis durch Karlsruher Paß)
wird die Teilnahmegebühr auf 50% ermäßigt.
</p>
<p>
Wenn ihr bereits darüber gesprochen habt und ihr übereingekommen seid,
dass {{ registration.personal_names }} doch den vollen Betrag zahlen soll,
dann kannst du unten den Haken entfernen.
</p>
</div>
<div class="well">
<h6>
{{ registration.event.number }} - {{ registration.event.title }}<br />
{% trans 'Anmeldung' %} {{ registration.get_full_name }}
</h6>
<form action="" method="post">
{% csrf_token %}
{% bootstrap_form form %}
<button type="submit" name="action" value="accept_registration"
class="btn btn-success">
{% bootstrap_icon 'plus-sign' %} zur Teilnehmerliste hinzufügen
</button>
<a href="{% url 'dav_events:registrations' registration.event.pk %}" class="btn btn-danger">
{% bootstrap_icon 'remove' %} Zurück
</a>
</form>
</div>
{% endblock page-container %}

View File

@@ -1,7 +1,12 @@
# -*- coding: utf-8 -*-
import json
from django import template from django import template
from django.utils.html import format_html from django.utils.html import format_html
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils import timezone
from django.utils.translation import ugettext as _
from ..models.eventchange import EventChange
from ..models.eventstatus import EventStatus, get_or_create_event_status from ..models.eventstatus import EventStatus, get_or_create_event_status
register = template.Library() register = template.Library()
@@ -30,3 +35,86 @@ def render_event_status(event, show_void=True):
context=context) context=context)
return mark_safe(html) return mark_safe(html)
@register.simple_tag
def render_event_changelog(event):
change_templ = u'<li class="list-group-item">\n' \
u'\t<p class="list-group-item-heading">' \
u'<span class="glyphicon glyphicon-{icon}"></span>' \
u' {timestamp}' \
u' - ' \
u' {user}</p>\n' \
u'\t{content}\n' \
u'</li>\n'
update_sub_templ = u'<li class="list-group-item">\n' \
u'\t{field}:{separator1}\n' \
u'\t<span style="background-color: #ffe0e0;">{refer}</span>\n' \
u'\t{separator2}\n' \
u'\t<span style="background-color: #e0ffe0;">{current}</span>\n' \
u'</li>\n'
raise_flag_templ = u'<span class="text-success glyphicon glyphicon-plus"></span>' \
u' <span class="label label-{bcontext}">{label}</span>' \
u' <span class="text-success glyphicon glyphicon-plus"></span>'
lower_flag_templ = u'<span class="text-danger glyphicon glyphicon-minus"></span>' \
u' <del><span class="label label-{bcontext}">{label}</span></del>' \
u' <span class="text-danger glyphicon glyphicon-minus"></span>'
if event.changes.exists():
html = u'<ul class="list-group">\n'
for change in event.changes.all():
username = change.user.get_full_name()
if not username:
username = change.user
if change.operation == EventChange.UPDATE:
icon = u'pencil'
content_html = u'<ul class="list-group">'
subchanges = json.loads(change.content)
for subchange in subchanges:
field_label = event._meta.get_field(subchange['field']).verbose_name
try:
is_long_strings = (len(subchange['refer']) + len(subchange['current'])) > 20
except TypeError:
is_long_strings = False
if is_long_strings:
separator1 = u'<br />'
separator2 = u'<br />'
else:
separator1 = u' '
separator2 = u' -&gt; '
content_html += format_html(update_sub_templ,
field=field_label,
separator1=mark_safe(separator1),
refer=subchange['refer'],
separator2=mark_safe(separator2),
current=subchange['current'])
content_html += u'</ul>'
elif change.operation == EventChange.RAISE_FLAG:
icon = u'flag'
status = get_or_create_event_status(change.content)
content_html = format_html(raise_flag_templ,
bcontext=status.bootstrap_context or u'default',
label=status.label)
elif change.operation == EventChange.LOWER_FLAG:
icon = u'flag'
status = get_or_create_event_status(change.content)
content_html = format_html(lower_flag_templ,
bcontext=status.bootstrap_context or u'default',
label=status.label)
else:
icon = u'question-sign'
content_html = format_html(u'{content}', content=change.content)
html += format_html(change_templ,
icon=icon,
timestamp=timezone.localtime(change.timestamp).strftime('%Y-%m-%d %H:%M:%S %Z'),
user=username,
content=mark_safe(content_html))
html += u'</ul>\n'
else:
html = _(u'Keine Einträge')
return mark_safe(html)

View File

@@ -49,6 +49,7 @@ Link zur Veranstaltung:
Veranstaltungsart: gemeinschaftliche Tour Veranstaltungsart: gemeinschaftliche Tour
Schwierigkeitsnivau: Anfänger Schwierigkeitsnivau: Anfänger
Gelände: Kletterhalle Gelände: Kletterhalle
Anmeldung: Nicht erforderlich
Anreise des Kurs-/Tourenleiters am Vortag: Nein Anreise des Kurs-/Tourenleiters am Vortag: Nein
Veröffentlichung: sofort Veröffentlichung: sofort
""" """

View File

@@ -0,0 +1,67 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import datetime
import json
from django.test import TestCase
from ..models.eventchange import EventChange
from .generic import EventMixin
TEST_EVENT_DATA = {
'title': 'Täst',
'description': 'Teßt',
'mode': 'joint',
'sport': 'W',
'level': 'beginner',
'first_day': datetime.date(2019, 3, 1),
'country': 'DE',
'trainer_firstname': 'Übungsleiter',
'trainer_familyname': 'Weißalles',
'trainer_email': 'trainer@localhost',
}
class EventsTestCase(EventMixin, TestCase):
def test_changelog(self):
data = TEST_EVENT_DATA
event = self.create_event_by_model(data)
event.alt_first_day = event.first_day + datetime.timedelta(1)
event.sport = 'M'
event.ski_lift = True
event.save()
event.country = 'FR'
event.save()
event.trainer_familyname += '-Ömlaut'
event.max_participants = 8
event.save()
changes = event.changes
self.assertEqual(changes.count(), 4)
change = changes.get(pk=1)
self.assertEqual(change.operation, EventChange.RAISE_FLAG)
self.assertEqual(change.content, 'draft')
change = changes.get(pk=2)
self.assertEqual(change.operation, EventChange.UPDATE)
subchanges = json.loads(change.content)
self.assertEqual(len(subchanges), 3)
self.assertIn({'field': 'alt_first_day', 'refer': None, 'current': '2019-03-02'}, subchanges)
self.assertIn({'field': 'sport', 'refer': 'W', 'current': 'M'}, subchanges)
self.assertIn({'field': 'ski_lift', 'refer': False, 'current': True}, subchanges)
change = changes.get(pk=3)
self.assertEqual(change.operation, EventChange.UPDATE)
subchanges = json.loads(change.content)
self.assertEqual(len(subchanges), 1)
self.assertIn({'field': 'country', 'refer': 'DE', 'current': 'FR'}, subchanges)
change = changes.get(pk=4)
self.assertEqual(change.operation, EventChange.UPDATE)
subchanges = json.loads(change.content)
self.assertEqual(len(subchanges), 2)
self.assertIn({'field': 'trainer_familyname', 'refer': 'Weißalles', 'current': 'Weißalles-Ömlaut'}, subchanges)
self.assertIn({'field': 'max_participants', 'refer': 0, 'current': 8}, subchanges)

View File

@@ -121,7 +121,11 @@ class ActionTestCase(EmailTestMixin, RoleMixin, EventMixin, TestCase):
'user': user.get_full_name(), 'user': user.get_full_name(),
}) })
html = message.replace('\'', '&#39;') html = message.replace('\'', '&#39;')
# Sometimes this test fail, and we cannot see the tested content, so we create our own Exception
try:
self.assertInHTML(html, content) self.assertInHTML(html, content)
except AssertionError:
raise AssertionError('Not in HTML:\n{}\n-----\n{}\n'.format(html, content))
self.assertRegex(content, r'alert-success') self.assertRegex(content, r'alert-success')
def setUp(self): def setUp(self):

View File

@@ -14,7 +14,7 @@ from dav_auth.tests.generic import SeleniumAuthMixin
from .generic import RoleMixin from .generic import RoleMixin
TEST_TRAINER_EMAIL = 'trainer@localhost' TEST_TRAINER_EMAIL = 'trainer@localhost'
TEST_PASSWORD = u'me||ön 2' TEST_PASSWORD = u'me||ön 21ABll'
TEST_EVENT_DATA_S = { TEST_EVENT_DATA_S = {
'mode': 'training', 'mode': 'training',
'sport': 'S', 'sport': 'S',

View File

@@ -12,6 +12,7 @@ urlpatterns = [
views.events.EventUpdateStatusView.as_view(), name='updatestatus'), views.events.EventUpdateStatusView.as_view(), name='updatestatus'),
url(r'^(?P<pk>\d+)/edit', views.events.EventUpdateView.as_view(), name='update'), url(r'^(?P<pk>\d+)/edit', views.events.EventUpdateView.as_view(), name='update'),
url(r'^(?P<pk>\d+)/', views.events.EventDetailView.as_view(), name='detail'), url(r'^(?P<pk>\d+)/', views.events.EventDetailView.as_view(), name='detail'),
url(r'^registration/(?P<pk>\d+)/', views.events.RespondRegistrationView.as_view(), name='respond_registration'),
url(r'^action/(?P<pk>[a-fA-F0-9]{8}-([a-fA-F0-9]{4}-){3}[a-fA-F0-9]{12})/', url(r'^action/(?P<pk>[a-fA-F0-9]{8}-([a-fA-F0-9]{4}-){3}[a-fA-F0-9]{12})/',
views.actions.OneClickActionRunView.as_view(), name='action_run'), views.actions.OneClickActionRunView.as_view(), name='action_run'),
] ]

View File

@@ -1,7 +1,7 @@
import logging import logging
from django.utils import timezone from django.utils import timezone
from .models import Participant from .models import Participant, TrashedParticipant
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -11,3 +11,6 @@ def purge_participants():
for p in Participant.objects.filter(purge_at__lte=now): for p in Participant.objects.filter(purge_at__lte=now):
logger.info('Purge participant \'%s\'', p) logger.info('Purge participant \'%s\'', p)
p.delete() p.delete()
for p in TrashedParticipant.objects.filter(purge_at__lte=now):
logger.info('Purge participant from trash \'%s\'', p)
p.delete()

View File

@@ -2,6 +2,7 @@
import datetime import datetime
import logging import logging
import os import os
from django.apps import apps
from django.contrib import messages from django.contrib import messages
from django.contrib.auth import login from django.contrib.auth import login
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
@@ -215,6 +216,18 @@ class EventRegistrationsView(EventPermissionMixin, generic.DetailView):
participants = event.participants.all() participants = event.participants.all()
context['participants'] = participants context['participants'] = participants
participants_trash = event.trashed_participants.all()
context['participants_trash'] = participants_trash
earnings = 0
if event.charge:
for participant in participants:
if participant.paid:
if participant.apply_reduced_fee:
earnings += event.charge / 2
else:
earnings += event.charge
context['earnings'] = earnings
if participants.count() > 1: if participants.count() > 1:
email_list = [u'"{}" <{}>'.format(p.get_full_name(), p.email_address) for p in participants] email_list = [u'"{}" <{}>'.format(p.get_full_name(), p.email_address) for p in participants]
@@ -230,9 +243,13 @@ class EventRegistrationsView(EventPermissionMixin, generic.DetailView):
registrations_support = hasattr(event, 'registrations') registrations_support = hasattr(event, 'registrations')
context['registrations_support'] = registrations_support context['registrations_support'] = registrations_support
if registrations_support: if registrations_support:
registrations = event.registrations.filter(answered=False) registrations_all = event.registrations.all()
# registrations = event.registrations.all() registrations_pending = registrations_all.filter(~Q(status__answered=True))
context['registrations'] = registrations registrations_answered = registrations_all.filter(status__answered=True)
context['registrations_all'] = registrations_all
context['registrations_pending'] = registrations_pending
context['registrations_answered'] = registrations_answered
context['registrations'] = registrations_all
return context return context
@@ -265,34 +282,21 @@ class EventRegistrationsView(EventPermissionMixin, generic.DetailView):
messages.success(request, _(u'Der Anmeldeschluss wurde gelöscht')) messages.success(request, _(u'Der Anmeldeschluss wurde gelöscht'))
def _accept_registration(self, request, registration): def _accept_registration(self, request, registration):
event = registration.event data = registration.get_data_dict()
del data['created_at']
del data['answered_obsolete']
data['position'] = registration.event.participants.count() + 1
position = event.participants.count() + 1
data = {
'event': event,
'position': position,
'personal_names': registration.personal_names,
'family_names': registration.family_names,
'address': registration.address,
'postal_code': registration.postal_code,
'city': registration.city,
'email_address': registration.email_address,
'phone_number': registration.phone_number,
'dav_number': registration.dav_number,
'emergency_contact': registration.emergency_contact,
'experience': registration.experience,
'note': registration.note,
'purge_at': registration.purge_at,
}
participant = models.Participant.objects.create(**data) participant = models.Participant.objects.create(**data)
registration.answered = True
registration.save() registration.status.set_accepted()
messages.success(request, _(u'Teilnehmer hinzugefügt: {}'.format(participant.get_full_name()))) messages.success(request, _(u'Teilnehmer hinzugefügt: {}'.format(participant.get_full_name())))
def _reject_registration(self, registration): def _reject_registration(self, registration):
registration.answered = True registration.status.set_rejected()
registration.save()
def _reset_registration(self, registration):
registration.status.reset()
def _swap_participants_position(self, participant1, participant2): def _swap_participants_position(self, participant1, participant2):
event = participant1.event event = participant1.event
@@ -342,6 +346,20 @@ class EventRegistrationsView(EventPermissionMixin, generic.DetailView):
self._reject_registration(registration) self._reject_registration(registration)
else: else:
raise FieldDoesNotExist('Event has no registrations') raise FieldDoesNotExist('Event has no registrations')
elif action == 'untrash_registration':
self.enforce_permission(event, permission='update-registration')
if hasattr(event, 'registrations'):
registration_id = request.POST.get('registration')
registration = event.registrations.get(id=registration_id)
self._reset_registration(registration)
else:
raise FieldDoesNotExist('Event has no registrations')
elif action == 'toggle_reduced_fee':
self.enforce_permission(event, permission='payment')
participant_id = request.POST.get('id')
participant = event.participants.get(id=participant_id)
participant.apply_reduced_fee = not participant.apply_reduced_fee
participant.save()
elif action == 'confirm_payment': elif action == 'confirm_payment':
self.enforce_permission(event, permission='payment') self.enforce_permission(event, permission='payment')
participant_id = request.POST.get('id') participant_id = request.POST.get('id')
@@ -354,18 +372,33 @@ class EventRegistrationsView(EventPermissionMixin, generic.DetailView):
participant = event.participants.get(id=participant_id) participant = event.participants.get(id=participant_id)
participant.paid = False participant.paid = False
participant.save() participant.save()
elif action == 'remove_participant': elif action == 'trash_participant':
self.enforce_permission(event, permission='update-participants') self.enforce_permission(event, permission='update-participants')
participant_id = request.POST.get('id') participant_id = request.POST.get('id')
participant = event.participants.get(id=participant_id) participant = event.participants.get(id=participant_id)
full_name = participant.get_full_name() participants_below = event.participants.filter(position__gt=participant.position)
position = participant.position
data = participant.get_data_dict()
trashed = models.TrashedParticipant.objects.create(**data)
participant.delete() participant.delete()
qs = event.participants.filter(position__gt=position)
for participant in qs: for participant in participants_below:
participant.position -= 1 participant.position -= 1
participant.save() participant.save()
messages.success(request, _(u'Teilnehmer gelöscht: {}'.format(full_name)))
messages.success(request, _(u'Teilnehmer in den Papierkorb verschoben: {}'.format(trashed.get_full_name())))
elif action == 'untrash_participant':
self.enforce_permission(event, permission='update-participants')
trashed_id = request.POST.get('id')
trashed = event.trashed_participants.get(id=trashed_id)
trashed.position = event.participants.count() + 1
data = trashed.get_data_dict()
del data['trashed_at']
participant = models.Participant.objects.create(**data)
trashed.delete()
messages.success(request, _(u'Teilnehmer zurückgeholt: {}'.format(participant.get_full_name())))
elif action == 'moveup_participant': elif action == 'moveup_participant':
self.enforce_permission(event, permission='update-participants') self.enforce_permission(event, permission='update-participants')
participant_id = request.POST.get('id') participant_id = request.POST.get('id')
@@ -422,6 +455,62 @@ class EventRegistrationsView(EventPermissionMixin, generic.DetailView):
return super(EventRegistrationsView, self).dispatch(request, *args, **kwargs) return super(EventRegistrationsView, self).dispatch(request, *args, **kwargs)
class RespondRegistrationView(EventPermissionMixin, generic.DetailView, generic.FormView):
permission = 'update-participants'
context_object_name = 'registration'
template_name = 'dav_events/registration_response.html'
form_class = forms.registration.RegistrationResponseForm
def _accept_registration(self, request, registration):
data = registration.get_data_dict()
del data['created_at']
del data['answered_obsolete']
data['position'] = registration.event.participants.count() + 1
participant = models.Participant.objects.create(**data)
registration.status.set_accepted()
messages.success(request, _(u'Teilnehmer hinzugefügt: {}'.format(participant.get_full_name())))
def has_permission(self, permission, obj):
user = self.request.user
return obj.event.workflow.has_permission(user, permission)
def get_queryset(self):
model = apps.get_model(app_label='dav_registration', model_name='Registration')
return model.objects.all()
def get_success_url(self):
return reverse('dav_events:registrations', args=[self.object.event.pk])
def get_initial(self):
return {
'apply_reduced_fee': self.object.apply_reduced_fee,
}
def form_valid(self, form):
registration = self.object
registration.apply_reduced_fee = form.cleaned_data['apply_reduced_fee']
registration.save()
self._accept_registration(self.request, registration)
return HttpResponseRedirect(self.get_success_url())
def get(self, request, *args, **kwargs):
self.object = self.get_object()
self.enforce_permission(self.object)
context = self.get_context_data(object=self.object)
return self.render_to_response(context)
def post(self, request, *args, **kwargs):
self.object = self.get_object()
self.enforce_permission(self.object)
return super(RespondRegistrationView, self).post(request, *args, **kwargs)
@method_decorator(login_required)
def dispatch(self, request, *args, **kwargs):
return super(RespondRegistrationView, self).dispatch(request, *args, **kwargs)
class EventUpdateStatusView(EventPermissionMixin, generic.DetailView): class EventUpdateStatusView(EventPermissionMixin, generic.DetailView):
model = models.Event model = models.Event
@@ -454,6 +543,18 @@ class EventUpdateStatusView(EventPermissionMixin, generic.DetailView):
messages.error(request, message) messages.error(request, message)
return HttpResponseRedirect(event.get_absolute_url()) return HttpResponseRedirect(event.get_absolute_url())
if not event.workflow.has_reached_status('publishing*') and not event.workflow.has_reached_status('published*'):
cur_pub_date = event.planned_publication_date
real_pub_date, real_pub_issue = event.workflow.plan_publication(event.first_day, event.deadline)
if cur_pub_date != real_pub_date:
if real_pub_date is None:
real_pub_str = _(u'Unverzüglich')
else:
real_pub_str = u'%s (%s)' % (real_pub_date.strftime('%d.%m.%Y'), real_pub_issue)
event.planned_publication_date = real_pub_date
event.save()
messages.warning(request, _(u'Veröffentlichungsdatum wurde angepasst: %s') % real_pub_str)
event.workflow.update_status(status, request.user) event.workflow.update_status(status, request.user)
if status.startswith('submit'): if status.startswith('submit'):
@@ -513,7 +614,21 @@ class EventUpdateView(EventPermissionMixin, generic.UpdateView):
def form_valid(self, form): def form_valid(self, form):
form.instance.editor = self.request.user form.instance.editor = self.request.user
self.object = form.save() event = form.save()
self.object = event
if not event.workflow.has_reached_status('publishing*') and not event.workflow.has_reached_status('published*'):
cur_pub_date = event.planned_publication_date
real_pub_date, real_pub_issue = event.workflow.plan_publication(event.first_day, event.deadline)
if cur_pub_date != real_pub_date:
if real_pub_date is None:
real_pub_str = _(u'Unverzüglich')
else:
real_pub_str = u'%s (%s)' % (real_pub_date.strftime('%d.%m.%Y'), real_pub_issue)
event.planned_publication_date = real_pub_date
event.save()
messages.warning(self.request, _(u'Veröffentlichungsdatum wurde angepasst: %s') % real_pub_str)
return HttpResponseRedirect(self.get_success_url()) return HttpResponseRedirect(self.get_success_url())
@method_decorator(login_required) @method_decorator(login_required)
@@ -573,6 +688,31 @@ class EventCreateView(EventPermissionMixin, generic.FormView):
return self.render_to_response(self.get_context_data(form=next_form, event=event)) return self.render_to_response(self.get_context_data(form=next_form, event=event))
else: else:
event.editor = self.request.user event.editor = self.request.user
# Check for double submission (seems to happens accidentally if smartphone user reload the submit page)
possible_doublets = models.Event.objects.filter(owner=event.owner,
title=event.title,
first_day=event.first_day)
if possible_doublets.exists():
accident_period = datetime.datetime.now() - datetime.timedelta(hours=24)
possible_doublets = possible_doublets.filter(created_at__gt=accident_period)
if possible_doublets.exists():
doublet = possible_doublets.first()
doublet_created_at = doublet.created_at.strftime('%d.%m.%Y %H:%M:%S')
error_msg = _(u'Du hast bereits eine Veranstaltung'
u' mit dem Titel "%(title)s"'
u' für das Datum %(day)s angelegt'
u' (am %(created_at)s).') % {'title': event.title,
'day': event.first_day,
'created_at': doublet_created_at}
warn_msg = _(u'Deine eingegebenen Daten sind noch da,'
u' wenn du jetzt auf "Neue Veranstaltung anlegen" klickst.'
u' Du musst aber entweder den Titel oder das Startdatum ändern,'
u' um die Veranstaltung anlegen zu können.')
messages.error(self.request, error_msg)
messages.warning(self.request, warn_msg)
return HttpResponseRedirect(reverse('dav_events:list'))
event.save() event.save()
if 'submit' in form.data: if 'submit' in form.data:
event.workflow.update_status('submitted', event.owner) event.workflow.update_status('submitted', event.owner)

View File

@@ -9,6 +9,7 @@ from django.utils.translation import ugettext_lazy as _
from . import emails from . import emails
from . import signals from . import signals
from .models.eventchange import EventChange
from .models.eventflag import EventFlag from .models.eventflag import EventFlag
from .models.eventstatus import get_or_create_event_status from .models.eventstatus import get_or_create_event_status
from .roles import get_users_by_role, has_role from .roles import get_users_by_role, has_role
@@ -50,6 +51,8 @@ class BasicWorkflow(object):
kwargs['status'] = status kwargs['status'] = status
flag = EventFlag(**kwargs) flag = EventFlag(**kwargs)
flag.save() flag.save()
change = EventChange(event=event, user=flag.user, operation=EventChange.RAISE_FLAG, content=status.code)
change.save()
logger.info('Flagging status \'%s\' for %s', status.code, event) logger.info('Flagging status \'%s\' for %s', status.code, event)
return flag return flag
@@ -312,10 +315,9 @@ class BasicWorkflow(object):
if not app_config.settings.enable_email_on_update: if not app_config.settings.enable_email_on_update:
return return
if len(diff) < 1: if not diff:
logger.debug('send_emails_on_update(): No diff data -> Skip sending mails.') logger.debug('send_emails_on_update(): No diff data -> Skip sending mails.')
return return
diff_text = '\n'.join(diff[3:])
# Who should be informed about the update? # Who should be informed about the update?
recipients = [event.owner] recipients = [event.owner]
@@ -329,7 +331,7 @@ class BasicWorkflow(object):
for recipient in recipients: for recipient in recipients:
if recipient.email and recipient.email != updater.email: if recipient.email and recipient.email != updater.email:
email = emails.EventUpdatedMail(recipient=recipient, event=event, editor=updater, diff=diff_text) email = emails.EventUpdatedMail(recipient=recipient, event=event, editor=updater, diff=diff)
email.send() email.send()
def send_emails_on_status_update(self, flag): def send_emails_on_status_update(self, flag):
@@ -487,9 +489,8 @@ class BasicWorkflow(object):
# #
# Misc logic # Misc logic
# #
# TODO: is a class method a good idea? @staticmethod
@classmethod def plan_publication(first_day, deadline=None):
def plan_publication(cls, first_day, deadline=None):
app_config = apps.get_containing_app_config(__package__) app_config = apps.get_containing_app_config(__package__)
if deadline: if deadline:

View File

@@ -1,8 +1,12 @@
from django.contrib import admin from django.contrib import admin
from .models import Registration from .models import Registration, RegistrationStatus
class RegistrationStatusInline(admin.StackedInline):
model = RegistrationStatus
@admin.register(Registration) @admin.register(Registration)
class RegistrationAdmin(admin.ModelAdmin): class RegistrationAdmin(admin.ModelAdmin):
pass inlines = [RegistrationStatusInline]

View File

@@ -1,7 +1,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import datetime
import logging import logging
from django import forms from django import forms
from django.utils.translation import ugettext from django.utils.translation import ugettext, ugettext_lazy as _
from .models import Registration from .models import Registration
@@ -9,14 +10,50 @@ logger = logging.getLogger(__name__)
class RegistrationForm(forms.ModelForm): class RegistrationForm(forms.ModelForm):
not_dav_member = forms.BooleanField(required=False,
label=_(u'Ich bin noch kein DAV Mitglied.'),
help_text=u'%s<br />\n%s' % (
_(u'Wenn du noch kein DAV Mitglied bist,'
u' oder deine Aufnahme noch in Arbeit ist,'
u' kreuze dieses Feld hier an.'),
_(u'Spätestens zu Veranstaltungsbeginn muss'
u' jedoch eine Mitgliedschaft bestehen.')
))
class Meta: class Meta:
model = Registration model = Registration
exclude = ['event', 'created_at', 'privacy_policy', 'purge_at', 'answered'] exclude = ['event', 'created_at', 'privacy_policy', 'purge_at', 'answered']
widgets = { widgets = {
'dav_member': forms.HiddenInput(),
'emergency_contact': forms.Textarea(attrs={'rows': 4}), 'emergency_contact': forms.Textarea(attrs={'rows': 4}),
'experience': forms.Textarea(attrs={'rows': 5}), 'experience': forms.Textarea(attrs={'rows': 5}),
'note': forms.Textarea(attrs={'rows': 5}), 'note': forms.Textarea(attrs={'rows': 5}),
} }
labels = {
'apply_reduced_fee': _(u'Ich bin noch keine 25 Jahre alt oder besitze einen "Karlsruher Pass".'),
}
def clean_year_of_birth(self):
now = datetime.datetime.now()
year_now = now.year
max_age = 100
val = self.cleaned_data.get('year_of_birth')
if val > year_now:
raise forms.ValidationError(
ugettext(u'Dein Geburtsjahr liegt in der Zukunft?'
u' Das finden wir gut,'
u' aber bitte melde dich besser mal per E-Mail bei uns.'),
code='to_young',
)
elif val < (year_now - max_age):
raise forms.ValidationError(
ugettext(u'Du bist schon über %(max_age)d Jahre alt?'
u' Das finden wir gut,'
u' aber bitte melde dich besser mal per E-Mail bei uns.'),
params={'max_age': max_age},
code='to_old',
)
return val
def clean_experience(self): def clean_experience(self):
val = self.cleaned_data.get('experience') val = self.cleaned_data.get('experience')
@@ -41,3 +78,13 @@ class RegistrationForm(forms.ModelForm):
code='privacy_policy_not_accepted', code='privacy_policy_not_accepted',
) )
return val return val
def clean(self):
super(RegistrationForm, self).clean()
dav_member = self.cleaned_data.get('dav_member')
dav_number = self.cleaned_data.get('dav_number')
if dav_member and not dav_number:
error_msg = ugettext(u'Wenn du DAV Mitglied bist, brauchen wir deine Mitgliedsnummer.')
self.add_error('not_dav_member', error_msg)
raise forms.ValidationError(error_msg, code='dav_number_missing')
return self.cleaned_data

View File

@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.29 on 2020-10-15 15:38
from __future__ import unicode_literals
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dav_registration', '0004_auto_20190605_1400'),
]
operations = [
migrations.AddField(
model_name='registration',
name='dav_member',
field=models.BooleanField(default=True, verbose_name='DAV Mitglied'),
),
migrations.AlterField(
model_name='registration',
name='dav_number',
field=models.CharField(blank=True, help_text='Deine Mitgliedsnummer findest du unter dem Strichcode auf deinem DAV Ausweis.<br /> Beispiel: <tt>131/00/012345</tt> (der Teil bis zum ersten * genügt)', max_length=62, validators=[django.core.validators.RegexValidator('^([0-9]{1,10}/[0-9]{2,10}/)?[0-9]{1,10}(\\*[0-9]{1,10})?(\\*[0-9]{4}\\*[0-9]{4})?([* ][0-9]{8})?$', 'Ungültiges Format.')], verbose_name='DAV Mitgliedsnummer'),
),
]

View File

@@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.29 on 2020-12-03 10:44
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('dav_registration', '0005_auto_20201015_1738'),
]
operations = [
migrations.CreateModel(
name='RegistrationStatus',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('updated_at', models.DateTimeField(auto_now=True)),
('answered', models.BooleanField(default=False, verbose_name='Durch Tourleitung beantwortet')),
('accepted', models.NullBooleanField(verbose_name='Zusage erteilt')),
],
options={
'verbose_name': 'Anmeldungsstatus',
'verbose_name_plural': 'Anmeldungsstati',
'ordering': ['updated_at'],
},
),
migrations.AddField(
model_name='registrationstatus',
name='registration',
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='status', to='dav_registration.Registration'),
),
]

View File

@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.29 on 2020-12-08 17:53
from __future__ import unicode_literals
from django.db import migrations
def migrate_registration_status(apps, schema_editor):
Registration = apps.get_model('dav_registration', 'Registration')
RegistrationStatus = apps.get_model('dav_registration', 'RegistrationStatus')
db_alias = schema_editor.connection.alias
for r in Registration.objects.using(db_alias).all():
s = RegistrationStatus(registration=r)
s.answered = r.answered
s.save()
class Migration(migrations.Migration):
dependencies = [
('dav_registration', '0006_auto_20201203_1144'),
]
operations = [
migrations.RunPython(migrate_registration_status),
]

View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.29 on 2020-12-08 18:06
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('dav_registration', '0007_auto_20201208_1853'),
]
operations = [
migrations.RenameField(
model_name='registration',
old_name='answered',
new_name='answered_obsolete',
),
]

View File

@@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.29 on 2020-12-09 09:20
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dav_registration', '0008_auto_20201208_1906'),
]
operations = [
migrations.AddField(
model_name='registration',
name='year_of_birth',
field=models.IntegerField(default=1870, help_text='Manchmal müssen wir wissen, wie alt unsere Teilnehmer sind. Darum brauchen wir die vierstellige Jahreszahl, des Jahres in dem du geboren bist (zb. 1991).', verbose_name='Geburtsjahr'),
preserve_default=False,
),
]

View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.29 on 2020-12-09 14:16
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dav_registration', '0009_registration_year_of_birth'),
]
operations = [
migrations.AddField(
model_name='registration',
name='apply_reduced_fee',
field=models.BooleanField(default=False, help_text='Für Jugendliche und Junioren (bis zum vollendeten 25. Lebensjahr), sowie Mitglieder mit geringen finanziellen Mitteln (Nachweis durch "Karlsruher Pass"), wird die Teilnahmegebühr auf 50% ermäßigt.', verbose_name='Antrag auf reduzierte Teilnahmegebühr'),
),
]

View File

@@ -2,6 +2,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import datetime import datetime
import logging import logging
from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
@@ -39,8 +40,24 @@ class Registration(models.Model):
phone_number = models.CharField(max_length=254, phone_number = models.CharField(max_length=254,
verbose_name=_('Telefonnummer'), verbose_name=_('Telefonnummer'),
help_text=_('Idealerweise eine Mobilfunk-Nummer')) help_text=_('Idealerweise eine Mobilfunk-Nummer'))
year_of_birth = models.IntegerField(verbose_name=_('Geburtsjahr'),
help_text=_('Manchmal müssen wir wissen, wie alt unsere Teilnehmer sind.'
' Darum brauchen wir die vierstellige Jahreszahl,'
' des Jahres in dem du geboren bist (zb. 1991).'))
apply_reduced_fee = models.BooleanField(default=False,
verbose_name=_('Antrag auf reduzierte Teilnahmegebühr'),
help_text=_('Für Jugendliche und Junioren'
' (bis zum vollendeten 25. Lebensjahr),'
' sowie Mitglieder mit geringen finanziellen Mitteln'
' (Nachweis durch "Karlsruher Pass"),'
' wird die Teilnahmegebühr auf 50% ermäßigt.'))
dav_member = models.BooleanField(default=True,
verbose_name=_('DAV Mitglied'))
dav_number = models.CharField(max_length=62, dav_number = models.CharField(max_length=62,
validators=[DAVNumberValidator], blank=True, validators=[DAVNumberValidator],
verbose_name=_('DAV Mitgliedsnummer'), verbose_name=_('DAV Mitgliedsnummer'),
help_text='%s<br /> %s %s' % ( help_text='%s<br /> %s %s' % (
_('Deine Mitgliedsnummer findest du unter dem Strichcode' _('Deine Mitgliedsnummer findest du unter dem Strichcode'
@@ -69,7 +86,12 @@ class Registration(models.Model):
verbose_name=_('Einwilligung zur Datenspeicherung')) verbose_name=_('Einwilligung zur Datenspeicherung'))
purge_at = models.DateTimeField(_('Zeitpunkt der Datenlöschung')) purge_at = models.DateTimeField(_('Zeitpunkt der Datenlöschung'))
answered = models.BooleanField(_('Durch Tourleitung beantwortet'), default=False) answered_obsolete = models.BooleanField(default=False, verbose_name=_('Durch Tourleitung beantwortet'))
def approx_age(self):
now = datetime.datetime.now()
year_now = now.year
return year_now - self.year_of_birth
@staticmethod @staticmethod
def pk2hexstr(pk): def pk2hexstr(pk):
@@ -109,21 +131,50 @@ class Registration(models.Model):
text = """{fullname} text = """{fullname}
{address}, {postal_code} {city} {address}, {postal_code} {city}
DAV Mitglied: {dav_info}
Jahrgang: {year_of_birth} (ungefähres Alter: {approx_age})
Antrag auf reduzierte Teilnehmergebühr: {apply_reduced_fee_yesno}
Erfahrung: Erfahrung:
{experience} {experience}
Anmerkung: Anmerkung:
{note} {note}
""" """
if not self.dav_member:
dav_info = _('Nein')
else:
dav_info = self.dav_number
if self.apply_reduced_fee:
apply_reduced_fee_yesno = _('Ja')
else:
apply_reduced_fee_yesno = _('Nein')
return text.format( return text.format(
fullname=self.get_full_name(), fullname=self.get_full_name(),
address=self.address, address=self.address,
postal_code=self.postal_code, postal_code=self.postal_code,
city=self.city, city=self.city,
dav_info=dav_info,
year_of_birth=self.year_of_birth,
approx_age=self.approx_age(),
apply_reduced_fee_yesno=apply_reduced_fee_yesno,
experience=self.experience, experience=self.experience,
note=self.note, note=self.note,
) )
def get_data_dict(self):
data = {}
for field in self._meta.fields:
if not field.primary_key:
data[field.name] = getattr(self, field.name)
return data
def clean(self):
if self.dav_member and not self.dav_number:
raise ValidationError({'dav_number': _('Wenn du DAV Mitglied bist, brauchen wir deine Mitgliedsnummer.')})
def save(self, **kwargs): def save(self, **kwargs):
creating = False creating = False
if not self.id: if not self.id:
@@ -132,9 +183,12 @@ Anmerkung:
if not self.purge_at and self.event: if not self.purge_at and self.event:
self.purge_at = self.__class__.calc_purge_at(self.event) self.purge_at = self.__class__.calc_purge_at(self.event)
self.full_clean()
super(Registration, self).save(**kwargs) super(Registration, self).save(**kwargs)
if creating: if creating:
status = RegistrationStatus(registration=self)
status.save()
logger.info('Registration stored: %s', self) logger.info('Registration stored: %s', self)
signals.registration_created.send(sender=self.__class__, registration=self) signals.registration_created.send(sender=self.__class__, registration=self)
@@ -163,3 +217,42 @@ Anmerkung:
purge_date = july_nextyear purge_date = july_nextyear
return timezone.make_aware(datetime.datetime.combine(purge_date, midnight)) return timezone.make_aware(datetime.datetime.combine(purge_date, midnight))
@python_2_unicode_compatible
class RegistrationStatus(models.Model):
registration = models.OneToOneField(Registration, on_delete=models.CASCADE, related_name='status')
updated_at = models.DateTimeField(auto_now=True)
answered = models.BooleanField(_('Durch Tourleitung beantwortet'), default=False)
accepted = models.NullBooleanField(_('Zusage erteilt'))
class Meta:
verbose_name = _('Anmeldungsstatus')
verbose_name_plural = _('Anmeldungsstati')
ordering = ['updated_at']
def __str__(self):
return '{} (Updated: {})'.format(self.registration, self.updated_at.strftime('%d.%m.%Y %H:%M:%S'))
def clean(self):
if self.accepted is not None and self.answered is not True:
raise ValidationError({'answered': 'if accepted is not None, answered must be True'})
def save(self, **kwargs):
self.full_clean()
super(RegistrationStatus, self).save(**kwargs)
def set_accepted(self):
self.accepted = True
self.answered = True
self.save()
def set_rejected(self):
self.accepted = False
self.answered = True
self.save()
def reset(self):
self.accepted = None
self.answered = False
self.save()

View File

@@ -22,7 +22,9 @@ Personendaten
{{ registration.postal_code }} {{ registration.city }} {{ registration.postal_code }} {{ registration.city }}
Telefon: {{ registration.phone_number }} Telefon: {{ registration.phone_number }}
E-Mail: {{ registration.email_address }} E-Mail: {{ registration.email_address }}
DAV Mitgliedsnummer: {{ registration.dav_number }} Jahrgang: {{ registration.year_of_birth }}
Antrag auf reduzierte Teilnahmegebühr: {% if registration.apply_reduced_fee %}Ja{% else %}Nein{% endif %}
{% if registration.dav_member %}DAV Mitgliedsnummer: {{ registration.dav_number }}{% else %}DAV Mitglied: Nein{% endif %}
Notfall-Kontakt Notfall-Kontakt
--------------- ---------------

View File

@@ -11,7 +11,8 @@ Teilnehmer*in:
{{ registration.address }}, {{ registration.postal_code }} {{ registration.city }} {{ registration.address }}, {{ registration.postal_code }} {{ registration.city }}
{{ registration.phone_number }} {{ registration.phone_number }}
{{ registration.email_address }} {{ registration.email_address }}
{{ registration.dav_number }} {% if registration.dav_member %}{{ registration.dav_number }}{% else %}Nicht DAV Mitglied{% endif %}
Antrag auf reduzierte Teilnahmegebühr: {% if registration.apply_reduced_fee %}Ja{% else %}Nein{% endif %}
Notfall-Kontakt: Notfall-Kontakt:
{% if registration.emergency_contact %}{{ registration.emergency_contact }}{% else %}-{% endif %} {% if registration.emergency_contact %}{{ registration.emergency_contact }}{% else %}-{% endif %}
@@ -19,6 +20,8 @@ Notfall-Kontakt:
Erfahrung: Erfahrung:
{% if registration.experience %}{{ registration.experience }}{% else %}-{% endif %} {% if registration.experience %}{{ registration.experience }}{% else %}-{% endif %}
Jahrgang: {{ registration.year_of_birth }} (ungefähres Alter: {{ registration.approx_age }})
Anmerkung: Anmerkung:
{% if registration.note %}{{ registration.note }}{% else %}-{% endif %} {% if registration.note %}{{ registration.note }}{% else %}-{% endif %}

View File

@@ -17,7 +17,7 @@
<tr> <tr>
<td> <td>
<div class="pull-right" style="margin-left: 2em;"> <div class="pull-right" style="margin-left: 2em;">
<a role="button" id="controlCollapseDetails{{ event.id }}" data-toggle="collapse" <a role="button" id="controlChevronCollapseDetails{{ event.id }}" data-toggle="collapse"
href="#collapseDetails{{ event.id }}" href="#collapseDetails{{ event.id }}"
aria-expanded="false" aria-controls="collapseDetails{{ event.id }}"> aria-expanded="false" aria-controls="collapseDetails{{ event.id }}">
<span title="{% trans 'Details aufklappen' %}" class="glyphicon glyphicon-chevron-down"></span> <span title="{% trans 'Details aufklappen' %}" class="glyphicon glyphicon-chevron-down"></span>
@@ -46,9 +46,13 @@
{% endwith %} {% endwith %}
</div> </div>
<div> <div>
<strong> <strong><span class="panel-title">
<a role="button" id="controlCollapseDetails{{ event.id }}" data-toggle="collapse"
href="#collapseDetails{{ event.id }}"
aria-expanded="false" aria-controls="collapseDetails{{ event.id }}">
{{ event.get_number }} - {{ event.title }} {{ event.get_number }} - {{ event.title }}
</strong> </a>
</span></strong>
<p> <p>
{{ event.get_formated_date }} {{ event.get_formated_date }}
{% if event.get_alt_formated_date %} {% if event.get_alt_formated_date %}
@@ -81,12 +85,12 @@
</div> </div>
<script> <script>
$("#collapseDetails{{ event.id }}").on("shown.bs.collapse", function() { $("#collapseDetails{{ event.id }}").on("shown.bs.collapse", function() {
icon = $("#controlCollapseDetails{{ event.id }}").find(".glyphicon") icon = $("#controlChevronCollapseDetails{{ event.id }}").find(".glyphicon")
icon.removeClass("glyphicon-chevron-down").addClass("glyphicon-chevron-up"); icon.removeClass("glyphicon-chevron-down").addClass("glyphicon-chevron-up");
icon.attr("title", "{% trans 'Details verbergen' %}"); icon.attr("title", "{% trans 'Details verbergen' %}");
}); });
$("#collapseDetails{{ event.id }}").on("hidden.bs.collapse", function() { $("#collapseDetails{{ event.id }}").on("hidden.bs.collapse", function() {
icon = $("#controlCollapseDetails{{ event.id }}").find(".glyphicon") icon = $("#controlChevronCollapseDetails{{ event.id }}").find(".glyphicon")
icon.removeClass("glyphicon-chevron-up").addClass("glyphicon-chevron-down"); icon.removeClass("glyphicon-chevron-up").addClass("glyphicon-chevron-down");
icon.attr("title", "{% trans 'Details aufklappen' %}"); icon.attr("title", "{% trans 'Details aufklappen' %}");
}); });

View File

@@ -4,6 +4,51 @@
{% block head-title %}{% block form-title %}{% trans 'Anmeldung' %} - {{ event.number }}{% endblock form-title %} - {{ block.super }}{% endblock head-title %} {% block head-title %}{% block form-title %}{% trans 'Anmeldung' %} - {{ event.number }}{% endblock form-title %} - {{ block.super }}{% endblock head-title %}
{% block head-additional %}
<script type="text/javascript">
function init_not_dav_member_handler() {
var e_orig = $("#id_dav_member");
initial_str = e_orig.val();
initial_bool = (initial_str == 'True')
var e_inverted = $("#id_not_dav_member");
if(e_inverted != null) {
e_inverted.prop("checked", !initial_bool);
e_inverted.change(function(){ not_dav_member_handler(); });
}
}
function not_dav_member_handler() {
var e = $("#id_not_dav_member");
if(e != null)
checked = e.prop("checked");
else
checked = true;
$("#id_dav_number").prop("disabled", checked);
$("#id_dav_member").val(!checked);
if(checked) {
$("#id_dav_number").val("");
}
}
function year_of_birth_handler() {
var junior_age = 25
var year_now = new Date().getFullYear()
var year_of_birth = parseInt($("#id_year_of_birth").val());
if(year_of_birth > (year_now - junior_age)) {
$("#id_apply_reduced_fee").prop('checked', true);
}
}
$(document).ready(function(){
init_not_dav_member_handler();
var e = $("#id_year_of_birth");
e.change(function(){ year_of_birth_handler(); });
});
</script>
{% endblock head-additional %}
{% block page-container-fluid %} {% block page-container-fluid %}
<h3 class="top-most">{% trans 'Anmeldung' %}</h3> <h3 class="top-most">{% trans 'Anmeldung' %}</h3>
<form> <form>
@@ -40,10 +85,10 @@
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-sm-4"> <div class="col-sm-3">
{% bootstrap_field form.postal_code %} {% bootstrap_field form.postal_code %}
</div> </div>
<div class="col-sm-8"> <div class="col-sm-9">
{% bootstrap_field form.city %} {% bootstrap_field form.city %}
</div> </div>
</div> </div>
@@ -56,9 +101,22 @@
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-sm-6"> <div class="col-sm-3">
{% bootstrap_field form.year_of_birth %}
</div>
<div class="col-sm-9">
<strong>Antrag auf reduzierte Teilnahmegebühr</strong>
{% bootstrap_field form.apply_reduced_fee %}
</div>
</div>
<div class="row">
<div class="col-sm-3">
{% bootstrap_field form.dav_number %} {% bootstrap_field form.dav_number %}
</div> </div>
<div class="col-sm-3">
<strong>Nichtmitglieder</strong>
{% bootstrap_field form.not_dav_member %}
</div>
<div class="col-sm-6"> <div class="col-sm-6">
{% bootstrap_field form.emergency_contact %} {% bootstrap_field form.emergency_contact %}
</div> </div>

View File

@@ -1,5 +1,8 @@
from django.utils import timezone
from ..models import Registration from ..models import Registration
THIS_YEAR = timezone.now().year
class RegistrationMixin(object): class RegistrationMixin(object):
def create_registration(self, data): def create_registration(self, data):

View File

@@ -10,7 +10,7 @@ from django.utils.translation import get_language
from dav_base.tests.generic import EmailTestMixin from dav_base.tests.generic import EmailTestMixin
from dav_events.tests.generic import EventMixin from dav_events.tests.generic import EventMixin
from .generic import RegistrationMixin from .generic import THIS_YEAR, RegistrationMixin
MAIL_SELF_TEMPLATE = """Hallo {participant_full_name}, MAIL_SELF_TEMPLATE = """Hallo {participant_full_name},
@@ -32,11 +32,13 @@ Vorgang: {registration_hexstr} (wird nur gebraucht, wenn irgendwas schief geht)
Personendaten Personendaten
------------- -------------
{participant_full_name} {participant_full_name}
Here
1 Karlsruhe
Telefon: Telefon: 12
E-Mail: {participant_email} E-Mail: {participant_email}
DAV Mitgliedsnummer: Jahrgang: {year_of_birth}
Antrag auf reduzierte Teilnahmegebühr: {apply_reduced_fee_yesno}
DAV Mitgliedsnummer: 0
Notfall-Kontakt Notfall-Kontakt
--------------- ---------------
@@ -66,10 +68,11 @@ Vorgang: {registration_hexstr}
Teilnehmer*in: Teilnehmer*in:
{participant_full_name} {participant_full_name}
, Here, 1 Karlsruhe
12
{participant_email} {participant_email}
0
Antrag auf reduzierte Teilnahmegebühr: {apply_reduced_fee_yesno}
Notfall-Kontakt: Notfall-Kontakt:
- -
@@ -77,6 +80,8 @@ Notfall-Kontakt:
Erfahrung: Erfahrung:
- -
Jahrgang: {year_of_birth} (ungefähres Alter: {approx_age})
Anmerkung: Anmerkung:
- -
@@ -107,7 +112,13 @@ class EmailsTestCase(EmailTestMixin, EventMixin, RegistrationMixin, TestCase):
'event': event, 'event': event,
'personal_names': 'Participant', 'personal_names': 'Participant',
'family_names': 'One', 'family_names': 'One',
'address': 'Here',
'postal_code': '1',
'city': 'Karlsruhe',
'phone_number': '12',
'email_address': 'participant@localhost', 'email_address': 'participant@localhost',
'year_of_birth': THIS_YEAR - 10,
'dav_number': '0',
} }
registration = self.create_registration(registration_data) registration = self.create_registration(registration_data)
@@ -126,6 +137,8 @@ class EmailsTestCase(EmailTestMixin, EventMixin, RegistrationMixin, TestCase):
body = MAIL_SELF_TEMPLATE.format( body = MAIL_SELF_TEMPLATE.format(
participant_full_name=registration.get_full_name(), participant_full_name=registration.get_full_name(),
participant_email=registration.email_address, participant_email=registration.email_address,
year_of_birth=registration.year_of_birth,
apply_reduced_fee_yesno='Nein',
event_number=event.get_number(), event_number=event.get_number(),
event_title=event.title, event_title=event.title,
event_formated_date=event.get_formated_date(), event_formated_date=event.get_formated_date(),
@@ -150,6 +163,9 @@ class EmailsTestCase(EmailTestMixin, EventMixin, RegistrationMixin, TestCase):
'postal_code': '76131', 'postal_code': '76131',
'city': 'Karlsruhe', 'city': 'Karlsruhe',
'phone_number': '+49 721 1234567890 AB (Büro)', 'phone_number': '+49 721 1234567890 AB (Büro)',
'year_of_birth': 1976,
'apply_reduced_fee': True,
'dav_member': False,
'dav_number': '131/00/007*12345', 'dav_number': '131/00/007*12345',
'emergency_contact': 'Call 911!', 'emergency_contact': 'Call 911!',
'experience': 'Yes, we can!', 'experience': 'Yes, we can!',
@@ -167,7 +183,9 @@ class EmailsTestCase(EmailTestMixin, EventMixin, RegistrationMixin, TestCase):
search += '{} {}\n'.format(registration_data['postal_code'], registration_data['city']) search += '{} {}\n'.format(registration_data['postal_code'], registration_data['city'])
search += 'Telefon: {}\n'.format(registration_data['phone_number']) search += 'Telefon: {}\n'.format(registration_data['phone_number'])
search += 'E-Mail: {}\n'.format(registration_data['email_address']) search += 'E-Mail: {}\n'.format(registration_data['email_address'])
search += 'DAV Mitgliedsnummer: {}\n'.format(registration_data['dav_number']) search += 'Jahrgang: {}\n'.format(registration_data['year_of_birth'])
search += 'Antrag auf reduzierte Teilnahmegebühr: Ja\n'
search += 'DAV Mitglied: Nein\n'
self.assertIn(search, mail.body) self.assertIn(search, mail.body)
search = '\n' search = '\n'
@@ -204,7 +222,13 @@ class EmailsTestCase(EmailTestMixin, EventMixin, RegistrationMixin, TestCase):
'event': event, 'event': event,
'personal_names': 'Participant', 'personal_names': 'Participant',
'family_names': 'One', 'family_names': 'One',
'address': 'Here',
'postal_code': '1',
'city': 'Karlsruhe',
'phone_number': '12',
'email_address': 'participant@localhost', 'email_address': 'participant@localhost',
'year_of_birth': THIS_YEAR - 86,
'dav_number': '0',
} }
registration = self.create_registration(registration_data) registration = self.create_registration(registration_data)
@@ -224,6 +248,9 @@ class EmailsTestCase(EmailTestMixin, EventMixin, RegistrationMixin, TestCase):
body = MAIL_TRAINER_TEMPLATE.format( body = MAIL_TRAINER_TEMPLATE.format(
participant_full_name=registration.get_full_name(), participant_full_name=registration.get_full_name(),
participant_email=registration.email_address, participant_email=registration.email_address,
year_of_birth=registration.year_of_birth,
approx_age=registration.approx_age(),
apply_reduced_fee_yesno='Nein',
event_number=event.get_number(), event_number=event.get_number(),
event_title=event.title, event_title=event.title,
event_formated_date=event.get_formated_date(), event_formated_date=event.get_formated_date(),
@@ -247,6 +274,9 @@ class EmailsTestCase(EmailTestMixin, EventMixin, RegistrationMixin, TestCase):
'postal_code': '76131', 'postal_code': '76131',
'city': 'Karlsruhe', 'city': 'Karlsruhe',
'phone_number': '+49 721 1234567890 AB (Büro)', 'phone_number': '+49 721 1234567890 AB (Büro)',
'year_of_birth': THIS_YEAR,
'apply_reduced_fee': True,
'dav_member': False,
'dav_number': '131/00/007*12345', 'dav_number': '131/00/007*12345',
'emergency_contact': 'Call 911!', 'emergency_contact': 'Call 911!',
'experience': 'Yes, we can!', 'experience': 'Yes, we can!',
@@ -265,8 +295,8 @@ class EmailsTestCase(EmailTestMixin, EventMixin, RegistrationMixin, TestCase):
search += '\n' search += '\n'
search += registration_data['email_address'] search += registration_data['email_address']
search += '\n' search += '\n'
search += registration_data['dav_number'] search += 'Nicht DAV Mitglied\n'
search += '\n' search += 'Antrag auf reduzierte Teilnahmegebühr: Ja\n'
self.assertIn(search, mail.body) self.assertIn(search, mail.body)
search = '\n' search = '\n'
@@ -281,6 +311,12 @@ class EmailsTestCase(EmailTestMixin, EventMixin, RegistrationMixin, TestCase):
search += '\n' search += '\n'
self.assertIn(search, mail.body) self.assertIn(search, mail.body)
search = '\n'
search += 'Jahrgang: '
search += str(registration_data['year_of_birth'])
search += ' (ungefähres Alter: 0)\n'
self.assertIn(search, mail.body)
search = '\n' search = '\n'
search += 'Anmerkung:\n' search += 'Anmerkung:\n'
search += registration_data['note'] search += registration_data['note']

View File

@@ -0,0 +1,103 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.apps import apps
from django.core.exceptions import ValidationError
from django.test import TestCase
from dav_events.tests.generic import EventMixin
from .generic import THIS_YEAR, RegistrationMixin
class RegistrationTestCase(EventMixin, RegistrationMixin, TestCase):
def setUp(self):
super(RegistrationTestCase, self).setUp()
app_config = apps.get_app_config('dav_events')
app_config.settings.enable_email_on_status_update = False
self.event = self.create_event_by_model()
self.submit_event(self.event)
self.accept_event(self.event)
self.confirm_publication_event(self.event)
def test_create_members(self):
event = self.event
registration_data = {
'event': event,
'personal_names': 'Participant',
'family_names': 'One',
'address': 'Here',
'postal_code': '1',
'city': 'Karlsruhe',
'phone_number': '12',
'email_address': 'participant@localhost',
'year_of_birth': THIS_YEAR,
}
dav_numbers = ['0', '12345', '131/00/12345']
for n in dav_numbers:
d = registration_data
d['dav_number'] = n
self.create_registration(d)
def test_create_members_without_number(self):
event = self.event
registration_data = {
'event': event,
'personal_names': 'Participant',
'family_names': 'One',
'address': 'Here',
'postal_code': '1',
'city': 'Karlsruhe',
'phone_number': '12',
'email_address': 'participant@localhost',
'year_of_birth': THIS_YEAR,
}
with self.assertRaisesMessage(ValidationError,
'Wenn du DAV Mitglied bist, brauchen wir deine Mitgliedsnummer.'):
self.create_registration(registration_data)
registration_data['dav_number'] = ''
with self.assertRaisesMessage(ValidationError,
'Wenn du DAV Mitglied bist, brauchen wir deine Mitgliedsnummer.'):
self.create_registration(registration_data)
def test_create_members_with_invalid_numbers(self):
event = self.event
registration_data = {
'event': event,
'personal_names': 'Participant',
'family_names': 'One',
'address': 'Here',
'postal_code': '1',
'city': 'Karlsruhe',
'phone_number': '12',
'email_address': 'participant@localhost',
'year_of_birth': THIS_YEAR,
}
dav_numbers = ['Nein', '-', '13100123456789']
for n in dav_numbers:
d = registration_data
d['dav_number'] = n
with self.assertRaises(ValidationError) as context:
self.create_registration(d)
self.assertEqual(context.exception.messages, ['Ungültiges Format.'])
def test_create_non_member(self):
event = self.event
registration_data = {
'event': event,
'personal_names': 'Participant',
'family_names': 'One',
'address': 'Here',
'postal_code': '1',
'city': 'Karlsruhe',
'phone_number': '12',
'email_address': 'participant@localhost',
'year_of_birth': THIS_YEAR,
'dav_member': False,
}
self.create_registration(registration_data)

View File

@@ -9,7 +9,7 @@ from dav_events.tests.generic import EventMixin
from ..models import Registration from ..models import Registration
from ..utils import purge_registrations from ..utils import purge_registrations
from .generic import RegistrationMixin from .generic import THIS_YEAR, RegistrationMixin
class UtilsTestCase(RegistrationMixin, EventMixin, TestCase): class UtilsTestCase(RegistrationMixin, EventMixin, TestCase):
@@ -29,6 +29,17 @@ class UtilsTestCase(RegistrationMixin, EventMixin, TestCase):
'trainer_familyname': 'One', 'trainer_familyname': 'One',
'trainer_email': 'trainer@localhost', 'trainer_email': 'trainer@localhost',
} }
registration_data = {
'personal_names': 'Participant',
'family_names': 'P.',
'address': 'Am Fächerbad 2',
'postal_code': '76131',
'city': 'Karlsruhe',
'phone_number': '555 5555',
'email_address': 'participant@localhost',
'year_of_birth': THIS_YEAR - 44,
'dav_number': '1',
}
first_day = today - (one_day * 367) first_day = today - (one_day * 367)
while first_day < today: while first_day < today:
@@ -41,7 +52,9 @@ class UtilsTestCase(RegistrationMixin, EventMixin, TestCase):
self.accept_event(event) self.accept_event(event)
for i in range(0, registrations_per_event): for i in range(0, registrations_per_event):
self.create_registration({'event': event}) d = registration_data
d['event'] = event
self.create_registration(d)
purge_registrations() purge_registrations()