Merge pull request 'Merge with master' (#39) from master into production
All checks were successful
buildbot/tox Build done.
All checks were successful
buildbot/tox Build done.
Reviewed-on: #39
This commit was merged in pull request #39.
This commit is contained in:
@@ -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'),
|
||||||
|
|||||||
@@ -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' %} 
|
{% bootstrap_icon 'log-in' %} 
|
||||||
|
|||||||
@@ -32,4 +32,38 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-2">
|
||||||
|
|
||||||
|
</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">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock page-container %}
|
{% endblock page-container %}
|
||||||
|
|||||||
@@ -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},
|
||||||
|
|||||||
@@ -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'}),
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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'))
|
||||||
|
|
||||||
|
|||||||
@@ -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()),
|
||||||
)
|
)
|
||||||
|
|||||||
227
dav_auth/tests/test_validators.py
Normal file
227
dav_auth/tests/test_validators.py
Normal 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)
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
200
dav_auth/validators.py
Normal 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
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
{% 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 %}
|
||||||
|
|
||||||
|
<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>
|
||||||
<a role="button" href="#collapseParticipant_{{ participant.id }}"
|
<strong><span class="panel-title">
|
||||||
data-toggle="collapse"
|
<a role="button" href="#collapseParticipant_{{ participant.id }}"
|
||||||
aria-expanded="true" aria-controls="collapseParticipant_{{ participant.id }}">
|
data-toggle="collapse"
|
||||||
<span class="caret"></span>
|
aria-expanded="true" aria-controls="collapseParticipant_{{ participant.id }}">
|
||||||
{{ position }}. {{ participant.get_full_name }}
|
<span class="caret"></span>
|
||||||
</a>
|
{{ position }}. {{ participant.get_full_name }}
|
||||||
|
</a>
|
||||||
|
</span></strong>
|
||||||
|
|
||||||
<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>
|
||||||
{{ participant.dav_number }}
|
|
||||||
|
{% if participant.dav_member %}
|
||||||
|
{{ participant.dav_number|default:'Fehler! heinzel Bescheid geben!' }}
|
||||||
|
{% else %}
|
||||||
|
{% trans 'Nicht Mitglied' %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{% 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>
|
||||||
|
|
||||||
{% 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"
|
|
||||||
title="{% trans 'Geldeingang wurde bestätigt' %} - {% trans 'Bestätigung des Geldeingangs zurückziehen' %}"
|
<span class="text-success {% if not participant.apply_reduced_fee %}invisible{% endif %}"
|
||||||
class="btn btn-link no-padding">
|
title="{% trans 'Reduzierte Teilnahmegebühr' %}"><strong>%</strong></span><button
|
||||||
<span class="text-success">{% bootstrap_icon 'piggy-bank' %}</span>
|
name="action" value="revoke_payment"
|
||||||
</button>
|
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>
|
||||||
{% elif event.charge and participant.paid %}
|
{% elif event.charge and participant.paid %}
|
||||||
<span class="text-success" title="{% trans 'Geldeingang bestätigt' %}">
|
|
||||||
{% 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"
|
|
||||||
title="{% trans 'Geldeingang bestätigen' %}"
|
<span class="text-danger {% if not participant.apply_reduced_fee and participant.created_at|date:'U' > '1608662327' %}invisible{% endif %}"
|
||||||
class="btn btn-link no-padding">
|
title="{% trans 'Reduzierte Teilnahmegebühr' %}"><strong>{% if participant.apply_reduced_fee %}%{% else %}? {% endif %}</strong></span><button
|
||||||
<span class="text-danger">{% bootstrap_icon 'piggy-bank' %}</span>
|
name="action" value="confirm_payment"
|
||||||
</button>
|
title="{% trans 'Geldeingang bestätigen' %}"
|
||||||
|
class="btn btn-link no-padding"><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' %}">
|
|
||||||
{% 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> {% 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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<span class="text-muted">
|
||||||
|
{{ registration.get_full_name }}
|
||||||
|
(<a href="mailto:{{ registration.email_address }}">{{ registration.email_address }}</a>,
|
||||||
|
{{ registration.phone_number }})
|
||||||
|
|
||||||
|
<span title="Anmeldezeitpunkt">
|
||||||
|
{% bootstrap_icon 'time' %}
|
||||||
|
{{ registration.created_at|date:'d. F Y, G:i' }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<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> {% 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 }})
|
||||||
|
|
||||||
|
{% if participant.dav_member %}
|
||||||
|
{{ participant.dav_number|default:'Fehler! heinzel Bescheid geben!' }}
|
||||||
|
{% else %}
|
||||||
|
{% trans 'Nicht Mitglied' %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<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 %}
|
||||||
|
|
||||||
|
<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 %}
|
||||||
|
|
||||||
|
<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 %}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
<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 %}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
{% if event.charge and participant.paid %}
|
{% if event.charge and participant.paid %}
|
||||||
<button name="action" value="revoke_payment"
|
<span class="text-success {% if not participant.apply_reduced_fee %}invisible{% endif %}"
|
||||||
title="{% trans 'Geldeingang wurde bestätigt' %} - {% trans 'Bestätigung des Geldeingangs zurückziehen' %}"
|
title="{% trans 'Reduzierte Teilnahmegebühr' %}"><strong>%</strong></span><button
|
||||||
class="btn btn-link no-padding">
|
name="action" value="revoke_payment"
|
||||||
<span class="text-success">{% bootstrap_icon 'piggy-bank' %}</span>
|
title="{% trans 'Geldeingang wurde bestätigt' %} - {% trans 'Bestätigung des Geldeingangs zurückziehen' %}"
|
||||||
</button>
|
class="btn btn-link no-padding"><span class="text-success">{% bootstrap_icon 'piggy-bank' %}</span></button>
|
||||||
|
|
||||||
({{ event.charge|floatformat:'-2' }} €)
|
({{ event.charge|floatformat:'-2' }}{% if participant.apply_reduced_fee %} / 2{% endif %} €)
|
||||||
{% elif event.charge %}
|
{% elif event.charge %}
|
||||||
<button name="action" value="confirm_payment"
|
<span class="text-danger {% if not participant.apply_reduced_fee and participant.created_at|date:'U' > '1608662327' %}invisible{% endif %}"
|
||||||
title="{% trans 'Geldeingang bestätigen' %}"
|
title="{% trans 'Reduzierte Teilnahmegebühr' %}"><strong>{% if participant.apply_reduced_fee %}%{% else %}? {% endif %}</strong></span><button
|
||||||
class="btn btn-link no-padding">
|
name="action" value="confirm_payment"
|
||||||
<span class="text-danger">{% bootstrap_icon 'piggy-bank' %}</span>
|
title="{% trans 'Geldeingang bestätigen' %}"
|
||||||
</button>
|
class="btn btn-link no-padding"><span class="text-danger">{% bootstrap_icon 'piggy-bank' %}</span></button>
|
||||||
|
|
||||||
({{ event.charge|floatformat:'-2' }} €)
|
({{ event.charge|floatformat:'-2' }}{% if participant.apply_reduced_fee %} / 2{% endif %} €)
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-muted" 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>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -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'))
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}),
|
||||||
|
|||||||
8
dav_events/forms/registration.py
Normal file
8
dav_events/forms/registration.py
Normal 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'))
|
||||||
20
dav_events/migrations/0033_auto_20200925_1543.py
Normal file
20
dav_events/migrations/0033_auto_20200925_1543.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
||||||
26
dav_events/migrations/0034_auto_20201015_1738.py
Normal file
26
dav_events/migrations/0034_auto_20201015_1738.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
35
dav_events/migrations/0034_eventchange.py
Normal file
35
dav_events/migrations/0034_eventchange.py
Normal 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'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
16
dav_events/migrations/0035_merge_20201103_1112.py
Normal file
16
dav_events/migrations/0035_merge_20201103_1112.py
Normal 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 = [
|
||||||
|
]
|
||||||
46
dav_events/migrations/0036_trashedparticipant.py
Normal file
46
dav_events/migrations/0036_trashedparticipant.py
Normal 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'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
27
dav_events/migrations/0037_auto_20201209_1327.py
Normal file
27
dav_events/migrations/0037_auto_20201209_1327.py
Normal 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,
|
||||||
|
),
|
||||||
|
]
|
||||||
25
dav_events/migrations/0038_auto_20201209_1542.py
Normal file
25
dav_events/migrations/0038_auto_20201209_1542.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
35
dav_events/migrations/0039_auto_20201215_1155.py
Normal file
35
dav_events/migrations/0039_auto_20201215_1155.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
35
dav_events/migrations/0040_auto_20201216_1712.py
Normal file
35
dav_events/migrations/0040_auto_20201216_1712.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
42
dav_events/models/eventchange.py
Normal file
42
dav_events/models/eventchange.py
Normal 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)
|
||||||
@@ -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(),
|
||||||
|
)
|
||||||
|
|||||||
1
dav_events/models/trash/__init__.py
Normal file
1
dav_events/models/trash/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .trashed_participant import TrashedParticipant
|
||||||
26
dav_events/models/trash/trashed_participant.py
Normal file
26
dav_events/models/trash/trashed_participant.py
Normal 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(),
|
||||||
|
)
|
||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -40,11 +40,9 @@
|
|||||||
{% bootstrap_icon 'remove' %} 
|
{% bootstrap_icon 'remove' %} 
|
||||||
{% 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' %} 
|
{% bootstrap_icon 'hdd' %} 
|
||||||
{% trans 'Als Entwurf speichern' %}
|
{% trans 'Als Entwurf speichern' %}
|
||||||
</button>
|
</button>
|
||||||
-->
|
|
||||||
{% endbuttons %}
|
{% endbuttons %}
|
||||||
{% endblock form-buttons %}
|
{% endblock form-buttons %}
|
||||||
|
|||||||
@@ -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="row">
|
||||||
<div class="panel-body">
|
<div class="col-sm-7">
|
||||||
<div class="row">
|
<div class="panel-group" id="log-accordion" role="tablist" aria-multiselectable="true">
|
||||||
<div class="col-sm-7">
|
<div class="panel panel-default">
|
||||||
<h5>Status-Log</h5>
|
<div id="headingStatusLog" class="panel-heading" role="tab">
|
||||||
{% for flag in event.flags.all %}
|
<h5 class="panel-title">
|
||||||
<div class="row">
|
<a role="button" href="#collapseStatusLog" data-toggle="collapse"
|
||||||
<div class="col-sm-5">
|
data-parent="#log-accordion" aria-expanded="true"
|
||||||
<span class="text-{{ flag.status.bootstrap_context|default:'default' }}">{% bootstrap_icon 'check' %}</span>
|
aria-controls="collapseStatusLog">
|
||||||
<strong>{{ flag.status.label }}:</strong>
|
Status-Flags
|
||||||
</div>
|
</a>
|
||||||
<div class="col-sm-7">
|
</h5>
|
||||||
{{ flag.timestamp|date:'l, d. F Y, H:i' }} {% trans 'Uhr' %}<br />
|
</div>
|
||||||
{% trans 'von' %} {{ flag.user.get_full_name|default:flag.user }}
|
<div id="collapseStatusLog" class="panel-collapse collapse in"
|
||||||
|
role="tabpanel" aria-labelledby="headingStatusLog">
|
||||||
|
<div class="panel-body">
|
||||||
|
{% for flag in event.flags.all %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-4">
|
||||||
|
<span class="text-{{ flag.status.bootstrap_context|default:'default' }}">{% bootstrap_icon 'check' %}</span>
|
||||||
|
<strong>{{ flag.status.label }}:</strong>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-8">
|
||||||
|
{{ flag.timestamp|date:'l, d. F Y, H:i' }} {% trans 'Uhr' %}<br />
|
||||||
|
{% trans 'von' %} {{ flag.user.get_full_name|default:flag.user }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-5">
|
<div class="panel panel-default">
|
||||||
<h5>{% trans 'Veröffentlichung' %}</h5>
|
<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">
|
||||||
|
{% 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>
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|
||||||
<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>
|
||||||
|
|
||||||
{% 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 %}
|
||||||
|
|
||||||
|
<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>
|
||||||
<a role="button" href="#collapseParticipant_{{ participant.id }}"
|
<strong><span class="panel-title">
|
||||||
data-toggle="collapse"
|
<a role="button" href="#collapseParticipant_{{ participant.id }}"
|
||||||
aria-expanded="true" aria-controls="collapseParticipant_{{ participant.id }}">
|
data-toggle="collapse"
|
||||||
<span class="caret"></span>
|
aria-expanded="true" aria-controls="collapseParticipant_{{ participant.id }}">
|
||||||
{{ position }}. {{ participant.get_full_name }}
|
<span class="caret"></span>
|
||||||
</a>
|
{{ position }}. {{ participant.get_full_name }}
|
||||||
|
</a>
|
||||||
|
</span></strong>
|
||||||
|
|
||||||
<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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
{% 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"
|
|
||||||
title="{% trans 'Geldeingang wurde bestätigt' %} - {% trans 'Bestätigung des Geldeingangs zurückziehen' %}"
|
<span class="text-success {% if not participant.apply_reduced_fee %}invisible{% endif %}"
|
||||||
class="btn btn-link no-padding">
|
title="{% trans 'Reduzierte Teilnahmegebühr' %}"><strong>%</strong></span><button
|
||||||
<span class="text-success">{% bootstrap_icon 'piggy-bank' %}</span>
|
name="action" value="revoke_payment"
|
||||||
</button>
|
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>
|
||||||
{% elif event.charge and participant.paid %}
|
{% elif event.charge and participant.paid %}
|
||||||
<span class="text-success" title="{% trans 'Geldeingang bestätigt' %}">
|
|
||||||
{% 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"
|
|
||||||
title="{% trans 'Geldeingang bestätigen' %}"
|
<span class="text-danger {% if not participant.apply_reduced_fee %}invisible{% endif %}"
|
||||||
class="btn btn-link no-padding">
|
title="{% trans 'Reduzierte Teilnahmegebühr' %}"><strong>%</strong></span><button
|
||||||
<span class="text-danger">{% bootstrap_icon 'piggy-bank' %}</span>
|
name="action" value="confirm_payment"
|
||||||
</button>
|
title="{% trans 'Geldeingang bestätigen' %}"
|
||||||
|
class="btn btn-link no-padding"><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' %}">
|
|
||||||
{% 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> {% 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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<span class="text-muted">
|
||||||
|
{{ registration.get_full_name }}
|
||||||
|
(<a href="mailto:{{ registration.email_address }}">{{ registration.email_address }}</a>,
|
||||||
|
{{ registration.phone_number }})
|
||||||
|
|
||||||
|
<span title="Anmeldezeitpunkt">
|
||||||
|
{% bootstrap_icon 'time' %}
|
||||||
|
{{ registration.created_at|date:'d. F Y, G:i' }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<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">♻</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> {% 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>
|
||||||
|
|
||||||
|
<span class="text-muted">
|
||||||
|
{{ participant.get_full_name }}
|
||||||
|
(<a href="mailto:{{ participant.email_address }}">{{ participant.email_address }}</a>,
|
||||||
|
{{ participant.phone_number }})
|
||||||
|
|
||||||
|
<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 %}
|
||||||
|
|
||||||
|
<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 %}
|
||||||
|
|
||||||
|
<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">♻</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 %}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
45
dav_events/templates/dav_events/registration_response.html
Normal file
45
dav_events/templates/dav_events/registration_response.html
Normal 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 %}
|
||||||
@@ -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' -> '
|
||||||
|
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)
|
||||||
|
|||||||
@@ -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
|
||||||
"""
|
"""
|
||||||
|
|||||||
67
dav_events/tests/test_models.py
Normal file
67
dav_events/tests/test_models.py
Normal 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)
|
||||||
@@ -121,7 +121,11 @@ class ActionTestCase(EmailTestMixin, RoleMixin, EventMixin, TestCase):
|
|||||||
'user': user.get_full_name(),
|
'user': user.get_full_name(),
|
||||||
})
|
})
|
||||||
html = message.replace('\'', ''')
|
html = message.replace('\'', ''')
|
||||||
self.assertInHTML(html, content)
|
# Sometimes this test fail, and we cannot see the tested content, so we create our own Exception
|
||||||
|
try:
|
||||||
|
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):
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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'),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
26
dav_registration/migrations/0005_auto_20201015_1738.py
Normal file
26
dav_registration/migrations/0005_auto_20201015_1738.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
35
dav_registration/migrations/0006_auto_20201203_1144.py
Normal file
35
dav_registration/migrations/0006_auto_20201203_1144.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
26
dav_registration/migrations/0007_auto_20201208_1853.py
Normal file
26
dav_registration/migrations/0007_auto_20201208_1853.py
Normal 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),
|
||||||
|
]
|
||||||
20
dav_registration/migrations/0008_auto_20201208_1906.py
Normal file
20
dav_registration/migrations/0008_auto_20201208_1906.py
Normal 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',
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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,
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
---------------
|
---------------
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|
||||||
|
|||||||
@@ -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">
|
||||||
{{ event.get_number }} - {{ event.title }}
|
<a role="button" id="controlCollapseDetails{{ event.id }}" data-toggle="collapse"
|
||||||
</strong>
|
href="#collapseDetails{{ event.id }}"
|
||||||
|
aria-expanded="false" aria-controls="collapseDetails{{ event.id }}">
|
||||||
|
{{ event.get_number }} - {{ event.title }}
|
||||||
|
</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' %}");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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']
|
||||||
|
|||||||
103
dav_registration/tests/test_models.py
Normal file
103
dav_registration/tests/test_models.py
Normal 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)
|
||||||
|
|
||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user