Compare commits
4 Commits
master
..
dev-jannik
| Author | SHA1 | Date | |
|---|---|---|---|
| 66417ac9ff | |||
| 7dcd5a5d30 | |||
| 34eaa24e36 | |||
| 7807dad400 |
@@ -6,7 +6,6 @@ source =
|
|||||||
dav_registration
|
dav_registration
|
||||||
omit =
|
omit =
|
||||||
dav_base/tests/generic.py
|
dav_base/tests/generic.py
|
||||||
dav_base/tests/utils.py
|
|
||||||
dav_auth/tests/generic.py
|
dav_auth/tests/generic.py
|
||||||
dav_events/tests/generic.py
|
dav_events/tests/generic.py
|
||||||
dav_registration/tests/generic.py
|
dav_registration/tests/generic.py
|
||||||
|
|||||||
@@ -3,11 +3,6 @@ from dav_base.config.apps import AppConfig as _AppConfig, DefaultSetting
|
|||||||
DEFAULT_SETTINGS = (
|
DEFAULT_SETTINGS = (
|
||||||
DefaultSetting('login_redirect_url', 'root'),
|
DefaultSetting('login_redirect_url', 'root'),
|
||||||
DefaultSetting('logout_redirect_url', 'root'),
|
DefaultSetting('logout_redirect_url', 'root'),
|
||||||
DefaultSetting('auto_password_length', 32),
|
|
||||||
DefaultSetting('auto_password_characters', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
|
||||||
'abcdefghijklmnopqrstuvwxyz'
|
|
||||||
'0123456789'
|
|
||||||
'#$%&@^~.,:;/_-*+!?'),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,2 @@
|
|||||||
# LOGIN_REDIRECT_URL = 'root'
|
# LOGIN_REDIRECT_URL = 'root'
|
||||||
# LOGOUT_REDIRECT_URL = 'root'
|
# LOGOUT_REDIRECT_URL = 'root'
|
||||||
# AUTO_PASSWORD_LENGTH = 32
|
|
||||||
# AUTO_PASSWORD_CHARACTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789#$%&@^~.,:;/_-*+!?'
|
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
{% load bootstrap3 %}
|
|
||||||
{% load i18n %}
|
|
||||||
<br />
|
|
||||||
<p>
|
|
||||||
Dein Passwort entspricht nicht mehr den aktuellen Passwortrichtlinien.<br />
|
|
||||||
Bitte hilf uns die Daten deiner Teilnehmer zu schützen und ändere dein Passwort.<br />
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<a href="{% url 'dav_auth:set_password' %}">{% trans 'Passwort ändern' %}</a>
|
|
||||||
</p>
|
|
||||||
<br />
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
|
from six import string_types
|
||||||
|
|
||||||
from dav_base.tests.generic import AppSetting, AppsTestCase
|
from dav_base.tests.generic import AppSetting, AppsTestCase
|
||||||
|
|
||||||
@@ -7,11 +8,6 @@ class TestCase(AppsTestCase):
|
|||||||
app_config = apps.get_app_config('dav_auth')
|
app_config = apps.get_app_config('dav_auth')
|
||||||
|
|
||||||
settings = (
|
settings = (
|
||||||
AppSetting('login_redirect_url', 'root', str),
|
AppSetting('login_redirect_url', string_types),
|
||||||
AppSetting('logout_redirect_url', 'root', str),
|
AppSetting('logout_redirect_url', string_types),
|
||||||
AppSetting('auto_password_length', 32, int),
|
|
||||||
AppSetting('auto_password_characters', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
|
||||||
'abcdefghijklmnopqrstuvwxyz'
|
|
||||||
'0123456789'
|
|
||||||
'#$%&@^~.,:;/_-*+!?', str),
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ class SetPasswordFormTestCase(FormsTestCase):
|
|||||||
|
|
||||||
def test_save(self):
|
def test_save(self):
|
||||||
new_passwords = [
|
new_passwords = [
|
||||||
'"ä§ Mellon12',
|
'"ä§ Mellon12'
|
||||||
'mellon12' * 128,
|
'mellon12' * 128,
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -162,7 +162,7 @@ class SetPasswordFormTestCase(FormsTestCase):
|
|||||||
@skip('Function is implemented in SetPasswordView instead of SetPasswordForm')
|
@skip('Function is implemented in SetPasswordView instead of SetPasswordForm')
|
||||||
def test_save_with_mail(self): # pragma: no cover
|
def test_save_with_mail(self): # pragma: no cover
|
||||||
new_passwords = [
|
new_passwords = [
|
||||||
'"ä§ Mellon12',
|
'"ä§ Mellon12'
|
||||||
'mellon12' * 128,
|
'mellon12' * 128,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ from django.test import TestCase, Client
|
|||||||
|
|
||||||
|
|
||||||
class ModelsTestCase(TestCase):
|
class ModelsTestCase(TestCase):
|
||||||
@skip('user.save() should raise an ValidationError when the user name is to long.'
|
@skip('I do not know, why the user.save() does not raise an exception')
|
||||||
' But it does not. I do not know why.')
|
|
||||||
def test_username_length(self):
|
def test_username_length(self):
|
||||||
max_length = 150 # Hard coded in django.contrib.auth.models.AbstractUser
|
max_length = 150 # Hard coded in django.contrib.auth.models.AbstractUser
|
||||||
username_ok = 'u' * max_length
|
username_ok = 'u' * max_length
|
||||||
@@ -17,7 +16,6 @@ class ModelsTestCase(TestCase):
|
|||||||
first_name='A',
|
first_name='A',
|
||||||
last_name='User',
|
last_name='User',
|
||||||
email='a.user@example.com')
|
email='a.user@example.com')
|
||||||
with self.assertRaises(Exception):
|
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
def test_last_login(self):
|
def test_last_login(self):
|
||||||
|
|||||||
@@ -9,8 +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_STRONG_PASSWORD = 'me||ön 21ABll'
|
TEST_PASSWORD = 'me||ön 21ABll'
|
||||||
TEST_WEAK_PASSWORD = 'mellon'
|
|
||||||
TEST_EMAIL = TEST_USERNAME
|
TEST_EMAIL = TEST_USERNAME
|
||||||
|
|
||||||
|
|
||||||
@@ -21,11 +20,10 @@ class TestCase(ScreenshotTestCase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
# Need a test user
|
# Need a test user
|
||||||
# (we start with a weak password, because we want to see the weak password warning)
|
|
||||||
self.test_username = TEST_USERNAME
|
self.test_username = TEST_USERNAME
|
||||||
self.test_password = TEST_WEAK_PASSWORD
|
self.test_password = TEST_PASSWORD
|
||||||
model = get_user_model()
|
model = get_user_model()
|
||||||
self.user = model.objects.create_user(username=TEST_USERNAME, password=TEST_WEAK_PASSWORD, email=TEST_EMAIL)
|
self.user = model.objects.create_user(username=TEST_USERNAME, password=TEST_PASSWORD, email=TEST_EMAIL)
|
||||||
|
|
||||||
def test_screenshots(self):
|
def test_screenshots(self):
|
||||||
sequence_name = 'walkthrough'
|
sequence_name = 'walkthrough'
|
||||||
@@ -88,7 +86,7 @@ class TestCase(ScreenshotTestCase):
|
|||||||
self.user.is_active = True
|
self.user.is_active = True
|
||||||
self.user.save()
|
self.user.save()
|
||||||
|
|
||||||
# Login -> save success message and weak password warning
|
# Login -> save success message
|
||||||
username_field = c.find_element(By.ID, 'id_username')
|
username_field = c.find_element(By.ID, 'id_username')
|
||||||
username_field.clear()
|
username_field.clear()
|
||||||
username_field.send_keys(self.test_username)
|
username_field.send_keys(self.test_username)
|
||||||
@@ -97,7 +95,7 @@ class TestCase(ScreenshotTestCase):
|
|||||||
password_field.send_keys(self.test_password)
|
password_field.send_keys(self.test_password)
|
||||||
password_field.send_keys(Keys.RETURN)
|
password_field.send_keys(Keys.RETURN)
|
||||||
alert_button = self.wait_on_presence(c, (By.CSS_SELECTOR, '#messages .alert-success button.close'))
|
alert_button = self.wait_on_presence(c, (By.CSS_SELECTOR, '#messages .alert-success button.close'))
|
||||||
self.save_screenshot('login_weak_password_succeed', sequence=sequence_name)
|
self.save_screenshot('login_succeed', sequence=sequence_name)
|
||||||
alert_button.click()
|
alert_button.click()
|
||||||
|
|
||||||
# Open user dropdown menu -> save menu
|
# Open user dropdown menu -> save menu
|
||||||
@@ -124,9 +122,9 @@ class TestCase(ScreenshotTestCase):
|
|||||||
|
|
||||||
# New passwords mismatch -> save error message
|
# New passwords mismatch -> save error message
|
||||||
password_field.clear()
|
password_field.clear()
|
||||||
password_field.send_keys(TEST_STRONG_PASSWORD)
|
password_field.send_keys(self.test_password)
|
||||||
password2_field.clear()
|
password2_field.clear()
|
||||||
password2_field.send_keys(TEST_WEAK_PASSWORD)
|
password2_field.send_keys(self.test_password[::-1])
|
||||||
password2_field.send_keys(Keys.RETURN)
|
password2_field.send_keys(Keys.RETURN)
|
||||||
self.wait_until_stale(c, password2_field)
|
self.wait_until_stale(c, password2_field)
|
||||||
self.save_screenshot('error_mismatch', sequence=sequence_name)
|
self.save_screenshot('error_mismatch', sequence=sequence_name)
|
||||||
@@ -168,7 +166,7 @@ class TestCase(ScreenshotTestCase):
|
|||||||
self.save_screenshot('error_too_similar', sequence=sequence_name)
|
self.save_screenshot('error_too_similar', sequence=sequence_name)
|
||||||
|
|
||||||
# Change password -> save success message
|
# Change password -> save success message
|
||||||
password = TEST_STRONG_PASSWORD
|
password = self.test_password[::-1]
|
||||||
password_field = c.find_element(By.ID, 'id_new_password')
|
password_field = c.find_element(By.ID, 'id_new_password')
|
||||||
password_field.clear()
|
password_field.clear()
|
||||||
password_field.send_keys(password)
|
password_field.send_keys(password)
|
||||||
@@ -178,7 +176,6 @@ class TestCase(ScreenshotTestCase):
|
|||||||
password2_field.send_keys(Keys.RETURN)
|
password2_field.send_keys(Keys.RETURN)
|
||||||
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)
|
||||||
self.test_password = password
|
|
||||||
|
|
||||||
# Get password recreate 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
|
||||||
@@ -199,31 +196,6 @@ 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)
|
||||||
|
|
||||||
# Login again, this time with a new strong password
|
|
||||||
link = c.find_element(By.CSS_SELECTOR, '#login-widget a')
|
|
||||||
link.click()
|
|
||||||
self.wait_on_presence(c, (By.ID, 'id_username'))
|
|
||||||
username_field = c.find_element(By.ID, 'id_username')
|
|
||||||
username_field.clear()
|
|
||||||
username_field.send_keys(self.test_username)
|
|
||||||
password_field = c.find_element(By.ID, 'id_password')
|
|
||||||
password_field.clear()
|
|
||||||
password_field.send_keys(self.test_password)
|
|
||||||
password_field.send_keys(Keys.RETURN)
|
|
||||||
alert_button = self.wait_on_presence(c, (By.CSS_SELECTOR, '#messages .alert-success button.close'))
|
|
||||||
self.save_screenshot('login_strong_password_succeed', sequence=sequence_name)
|
|
||||||
alert_button.click()
|
|
||||||
|
|
||||||
# Logout again
|
|
||||||
dropdown_button = self.wait_on_presence(c, (By.ID, 'user_dropdown_button'))
|
|
||||||
dropdown_button.click()
|
|
||||||
user_menu = c.find_element(By.CSS_SELECTOR, '#login-widget ul')
|
|
||||||
#link = user_menu.find_element(By.PARTIAL_LINK_TEXT, gettext('Logout'))
|
|
||||||
#link.click()
|
|
||||||
button = c.find_element(By.ID, 'id_logout_button')
|
|
||||||
button.click()
|
|
||||||
self.wait_until_stale(c, user_menu)
|
|
||||||
|
|
||||||
# Click on 'login' to access password recreate 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()
|
||||||
@@ -235,7 +207,7 @@ class TestCase(ScreenshotTestCase):
|
|||||||
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_recreate_password_form', sequence=sequence_name)
|
self.save_screenshot('empty_recreate_password_form', sequence=sequence_name)
|
||||||
|
|
||||||
# Enter invalid username -> save result (login form, success message - do not reveal user does not exist)
|
# 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)
|
||||||
|
|||||||
@@ -26,9 +26,12 @@ class PasswordScoreValidatorTestCase(SimpleTestCase):
|
|||||||
validator = PasswordScoreValidator(min_classes=0)
|
validator = PasswordScoreValidator(min_classes=0)
|
||||||
|
|
||||||
for password in passwords:
|
for password in passwords:
|
||||||
with self.assertRaises(ValidationError, msg=password) as cm:
|
try:
|
||||||
validator.validate(password)
|
validator.validate(password)
|
||||||
self.assertEqual('too_little_score', cm.exception.code)
|
except ValidationError as e:
|
||||||
|
self.assertEqual('too_little_score', e.code)
|
||||||
|
else:
|
||||||
|
self.fail('%s: no validation error was raised' % password)
|
||||||
|
|
||||||
def test_too_few_classes(self):
|
def test_too_few_classes(self):
|
||||||
passwords = [
|
passwords = [
|
||||||
@@ -46,9 +49,12 @@ class PasswordScoreValidatorTestCase(SimpleTestCase):
|
|||||||
validator = PasswordScoreValidator(min_score=0, min_classes=4)
|
validator = PasswordScoreValidator(min_score=0, min_classes=4)
|
||||||
|
|
||||||
for password in passwords:
|
for password in passwords:
|
||||||
with self.assertRaises(ValidationError, msg=password) as cm:
|
try:
|
||||||
validator.validate(password)
|
validator.validate(password)
|
||||||
self.assertEqual('too_few_classes', cm.exception.code)
|
except ValidationError as e:
|
||||||
|
self.assertEqual('too_few_classes', e.code)
|
||||||
|
else:
|
||||||
|
self.fail('%s: no validation error was raised' % password)
|
||||||
|
|
||||||
def test_valid(self):
|
def test_valid(self):
|
||||||
passwords = [
|
passwords = [
|
||||||
@@ -69,15 +75,8 @@ class PasswordScoreValidatorTestCase(SimpleTestCase):
|
|||||||
for password in passwords:
|
for password in passwords:
|
||||||
try:
|
try:
|
||||||
validator.validate(password)
|
validator.validate(password)
|
||||||
except ValidationError as e: # pragma: no cover
|
except ValidationError as e:
|
||||||
self.fail('%s: %s' % (password, e))
|
self.fail(e)
|
||||||
|
|
||||||
def test_help_text(self):
|
|
||||||
validator = PasswordScoreValidator()
|
|
||||||
help_text = validator.get_help_text()
|
|
||||||
self.assertIn('The password must get a minimum score of 18 points.', help_text)
|
|
||||||
self.assertIn('Also the password must contain characters from 2 different character classes'
|
|
||||||
' (i.e. lower, upper, digits, others).', help_text)
|
|
||||||
|
|
||||||
|
|
||||||
class CustomWordlistPasswordValidatorTestCase(SimpleTestCase):
|
class CustomWordlistPasswordValidatorTestCase(SimpleTestCase):
|
||||||
@@ -115,7 +114,7 @@ class CustomWordlistPasswordValidatorTestCase(SimpleTestCase):
|
|||||||
for error in errors:
|
for error in errors:
|
||||||
self.assertIn(error, expected_errors)
|
self.assertIn(error, expected_errors)
|
||||||
else:
|
else:
|
||||||
self.fail('%s: no validation error was raised' % password) # pragma: no cover
|
self.fail('%s: no validation error was raised' % password)
|
||||||
|
|
||||||
def test_valid(self):
|
def test_valid(self):
|
||||||
passwords = [
|
passwords = [
|
||||||
@@ -129,15 +128,9 @@ class CustomWordlistPasswordValidatorTestCase(SimpleTestCase):
|
|||||||
for password in passwords:
|
for password in passwords:
|
||||||
try:
|
try:
|
||||||
validator.validate(password)
|
validator.validate(password)
|
||||||
except ValidationError as e: # pragma: no cover
|
except ValidationError as e:
|
||||||
self.fail(e)
|
self.fail(e)
|
||||||
|
|
||||||
def test_help_text(self):
|
|
||||||
validator = CustomWordlistPasswordValidator()
|
|
||||||
help_text = validator.get_help_text()
|
|
||||||
self.assertIn('The password must not contain some specific words,', help_text)
|
|
||||||
self.assertIn('All words are matched case insensitive.', help_text)
|
|
||||||
|
|
||||||
|
|
||||||
class CharacterClassPasswordValidatorTestCase(SimpleTestCase):
|
class CharacterClassPasswordValidatorTestCase(SimpleTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@@ -222,7 +215,7 @@ class CharacterClassPasswordValidatorTestCase(SimpleTestCase):
|
|||||||
for error in errors:
|
for error in errors:
|
||||||
self.assertIn(error, expected_errors)
|
self.assertIn(error, expected_errors)
|
||||||
else:
|
else:
|
||||||
self.fail('%s: no validation error was raised' % password) # pragma: no cover
|
self.fail('%s: no validation error was raised' % password)
|
||||||
|
|
||||||
def test_valid(self):
|
def test_valid(self):
|
||||||
valid_passwords = ['abCD12+-']
|
valid_passwords = ['abCD12+-']
|
||||||
@@ -230,14 +223,5 @@ class CharacterClassPasswordValidatorTestCase(SimpleTestCase):
|
|||||||
for password in valid_passwords:
|
for password in valid_passwords:
|
||||||
try:
|
try:
|
||||||
validator.validate(password)
|
validator.validate(password)
|
||||||
except ValidationError as e: # pragma: no cover
|
except ValidationError as e:
|
||||||
self.fail(e)
|
self.fail(e)
|
||||||
|
|
||||||
def test_help_text(self):
|
|
||||||
validator = self.validator
|
|
||||||
help_text = validator.get_help_text()
|
|
||||||
self.assertIn('The password must contain at least 2 characters from a-z.', help_text)
|
|
||||||
self.assertIn('The password must contain at least 2 characters from A-Z.', help_text)
|
|
||||||
self.assertIn('The password must contain at least 2 digits from 0-9.', help_text)
|
|
||||||
self.assertIn('The password must contain at least 2 non alpha numeric characters.', help_text)
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.auth.password_validation import validate_password
|
|
||||||
from django.contrib.messages import get_messages
|
|
||||||
from django.core import mail as django_mail
|
from django.core import mail as django_mail
|
||||||
from django.shortcuts import resolve_url
|
from django.shortcuts import resolve_url
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
@@ -12,8 +10,7 @@ from django.urls import reverse
|
|||||||
from ..forms import LoginForm, SetPasswordForm, CreateAndSendPasswordForm
|
from ..forms import LoginForm, SetPasswordForm, CreateAndSendPasswordForm
|
||||||
|
|
||||||
TEST_USERNAME = 'root@localhost'
|
TEST_USERNAME = 'root@localhost'
|
||||||
TEST_STRONG_PASSWORD = 'me||ön 21ABll'
|
TEST_PASSWORD = 'me||ön 21ABll'
|
||||||
TEST_WEAK_PASSWORD = 'mellon'
|
|
||||||
TEST_EMAIL = TEST_USERNAME
|
TEST_EMAIL = TEST_USERNAME
|
||||||
|
|
||||||
|
|
||||||
@@ -34,22 +31,16 @@ class ViewsTestCase(TestCase):
|
|||||||
|
|
||||||
# Some messages
|
# Some messages
|
||||||
cls.wrong_credentials_message = gettext('Benutzername oder Passwort falsch.')
|
cls.wrong_credentials_message = gettext('Benutzername oder Passwort falsch.')
|
||||||
cls.login_message = gettext('Benutzer angemeldet: %(username)s')
|
|
||||||
cls.logout_message = gettext('Benutzer abgemeldet.')
|
cls.logout_message = gettext('Benutzer abgemeldet.')
|
||||||
cls.set_password_message = gettext('Passwort gespeichert.')
|
cls.set_password_message = gettext('Passwort gespeichert.')
|
||||||
cls.weak_password_warning_message = gettext('Dein Passwort entspricht nicht mehr den aktuellen Passwortrichtlinien.')
|
|
||||||
cls.new_password_sent_message = gettext('Neues Passwort versendet.')
|
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
# Need a test user
|
# Need a test user
|
||||||
self.test_username = TEST_USERNAME
|
self.test_username = TEST_USERNAME
|
||||||
self.test_password = TEST_STRONG_PASSWORD
|
self.test_password = TEST_PASSWORD
|
||||||
self.test_email = TEST_EMAIL
|
|
||||||
model = get_user_model()
|
model = get_user_model()
|
||||||
self.user = model.objects.create_user(username=self.test_username,
|
self.user = model.objects.create_user(username=TEST_USERNAME, password=TEST_PASSWORD, email=TEST_EMAIL)
|
||||||
password=self.test_password,
|
|
||||||
email=self.test_email)
|
|
||||||
|
|
||||||
def test_integrated_login_get(self):
|
def test_integrated_login_get(self):
|
||||||
response = self.client.get(self.login_url)
|
response = self.client.get(self.login_url)
|
||||||
@@ -63,12 +54,6 @@ class ViewsTestCase(TestCase):
|
|||||||
field = response.context['form'].fields['password']
|
field = response.context['form'].fields['password']
|
||||||
self.assertTrue(field.required)
|
self.assertTrue(field.required)
|
||||||
|
|
||||||
def test_integrated_login_invalid_user(self):
|
|
||||||
response = self.client.post(self.login_url, {'username': 'toor', 'password': self.test_password})
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertFormError(response.context['form'], None, self.wrong_credentials_message)
|
|
||||||
self.assertFalse(response.context['user'].is_authenticated, 'User is logged in')
|
|
||||||
|
|
||||||
def test_integrated_login_inactive_user(self):
|
def test_integrated_login_inactive_user(self):
|
||||||
user = self.user
|
user = self.user
|
||||||
user.is_active = False
|
user.is_active = False
|
||||||
@@ -89,44 +74,14 @@ class ViewsTestCase(TestCase):
|
|||||||
|
|
||||||
def test_integrated_login_succeed(self):
|
def test_integrated_login_succeed(self):
|
||||||
username = self.user.username
|
username = self.user.username
|
||||||
expected_message = self.login_message % {'username': username}
|
message = gettext('Benutzer angemeldet: %(username)s') % {'username': username}
|
||||||
|
|
||||||
response = self.client.post(self.login_url, {'username': username, 'password': self.test_password})
|
response = self.client.post(self.login_url, {'username': username, 'password': self.test_password})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.url, self.login_redirect_url)
|
self.assertEqual(response.url, self.login_redirect_url)
|
||||||
|
|
||||||
response = self.client.get(response.url)
|
response = self.client.get(response.url)
|
||||||
messages = list(get_messages(response.wsgi_request))
|
self.assertContains(response, message)
|
||||||
self.assertEqual(len(messages), 1)
|
|
||||||
self.assertEqual(messages[0].message, expected_message)
|
|
||||||
self.assertContains(response, expected_message)
|
|
||||||
|
|
||||||
self.assertTrue(response.context['user'].is_authenticated, 'Login failed')
|
|
||||||
|
|
||||||
def test_integrated_login_weak_password(self):
|
|
||||||
username = self.user.username
|
|
||||||
password = TEST_WEAK_PASSWORD
|
|
||||||
|
|
||||||
expected_message = self.login_message % {'username': username}
|
|
||||||
|
|
||||||
user_model = get_user_model()
|
|
||||||
user = user_model.objects.get(username=username)
|
|
||||||
user.set_password(password)
|
|
||||||
user.save()
|
|
||||||
|
|
||||||
with self.assertLogs('dav_auth.views', level='WARNING') as cm:
|
|
||||||
response = self.client.post(self.login_url, {'username': username, 'password': password})
|
|
||||||
self.assertStartsWith(cm.output[0], 'WARNING:dav_auth.views:Detected weak password for user id')
|
|
||||||
self.assertEqual(response.status_code, 302)
|
|
||||||
self.assertEqual(response.url, self.login_redirect_url)
|
|
||||||
|
|
||||||
response = self.client.get(response.url)
|
|
||||||
messages = list(get_messages(response.wsgi_request))
|
|
||||||
self.assertEqual(len(messages), 2)
|
|
||||||
self.assertEqual(messages[0].message, expected_message)
|
|
||||||
self.assertContains(response, expected_message)
|
|
||||||
self.assertIn(self.weak_password_warning_message, messages[1].message)
|
|
||||||
self.assertContains(response, self.weak_password_warning_message)
|
|
||||||
|
|
||||||
self.assertTrue(response.context['user'].is_authenticated, 'Login failed')
|
self.assertTrue(response.context['user'].is_authenticated, 'Login failed')
|
||||||
|
|
||||||
@@ -138,8 +93,6 @@ class ViewsTestCase(TestCase):
|
|||||||
self.assertEqual(response.url, self.logout_redirect_url)
|
self.assertEqual(response.url, self.logout_redirect_url)
|
||||||
|
|
||||||
response = self.client.get(response.url)
|
response = self.client.get(response.url)
|
||||||
messages = list(get_messages(response.wsgi_request))
|
|
||||||
self.assertEqual(messages[0].message, self.logout_message)
|
|
||||||
self.assertContains(response, self.logout_message)
|
self.assertContains(response, self.logout_message)
|
||||||
|
|
||||||
self.assertFalse(response.context['user'].is_authenticated, 'Logout failed')
|
self.assertFalse(response.context['user'].is_authenticated, 'Logout failed')
|
||||||
@@ -189,8 +142,6 @@ class ViewsTestCase(TestCase):
|
|||||||
self.assertEqual(len(django_mail.outbox), 0)
|
self.assertEqual(len(django_mail.outbox), 0)
|
||||||
|
|
||||||
response = self.client.get(response.url)
|
response = self.client.get(response.url)
|
||||||
messages = list(get_messages(response.wsgi_request))
|
|
||||||
self.assertEqual(messages[0].message, self.set_password_message)
|
|
||||||
self.assertContains(response, self.set_password_message)
|
self.assertContains(response, self.set_password_message)
|
||||||
|
|
||||||
self.client.logout()
|
self.client.logout()
|
||||||
@@ -222,8 +173,6 @@ class ViewsTestCase(TestCase):
|
|||||||
self.assertIn(new_password, mail.body)
|
self.assertIn(new_password, mail.body)
|
||||||
|
|
||||||
response = self.client.get(response.url)
|
response = self.client.get(response.url)
|
||||||
messages = list(get_messages(response.wsgi_request))
|
|
||||||
self.assertEqual(messages[0].message, self.set_password_message)
|
|
||||||
self.assertContains(response, self.set_password_message)
|
self.assertContains(response, self.set_password_message)
|
||||||
|
|
||||||
self.client.logout()
|
self.client.logout()
|
||||||
@@ -249,10 +198,6 @@ class ViewsTestCase(TestCase):
|
|||||||
location = self.recreate_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})
|
||||||
new_password = response.context['password']
|
|
||||||
messages = list(get_messages(response.wsgi_request))
|
|
||||||
self.assertEqual(len(messages), 1)
|
|
||||||
self.assertEqual(messages[0].message, self.new_password_sent_message)
|
|
||||||
self.assertRedirects(response, self.login_url)
|
self.assertRedirects(response, self.login_url)
|
||||||
|
|
||||||
self.assertEqual(len(django_mail.outbox), 1)
|
self.assertEqual(len(django_mail.outbox), 1)
|
||||||
@@ -261,53 +206,9 @@ class ViewsTestCase(TestCase):
|
|||||||
recipients = mail.recipients()
|
recipients = mail.recipients()
|
||||||
self.assertIn(recipient, recipients)
|
self.assertIn(recipient, recipients)
|
||||||
self.assertEqual(len(recipients), 1)
|
self.assertEqual(len(recipients), 1)
|
||||||
self.assertIn(new_password, mail.body)
|
|
||||||
|
|
||||||
response = self.client.get(location)
|
response = self.client.get(location)
|
||||||
self.assertFalse(response.context['user'].is_authenticated, 'User is logged in')
|
self.assertFalse(response.context['user'].is_authenticated, 'User is logged in')
|
||||||
|
|
||||||
self.assertFalse(self.client.login(username=self.test_username, password=self.test_password),
|
self.assertFalse(self.client.login(username=self.test_username, password=self.test_password),
|
||||||
'Old password still valid')
|
'Old password still valid')
|
||||||
|
|
||||||
def test_recreate_password_invalid_user(self):
|
|
||||||
location = self.recreate_password_url
|
|
||||||
|
|
||||||
response = self.client.post(location, {'username': 'toor'})
|
|
||||||
messages = list(get_messages(response.wsgi_request))
|
|
||||||
self.assertEqual(len(messages), 1)
|
|
||||||
self.assertEqual(messages[0].message, self.new_password_sent_message)
|
|
||||||
self.assertRedirects(response, self.login_url)
|
|
||||||
|
|
||||||
self.assertEqual(len(django_mail.outbox), 0)
|
|
||||||
|
|
||||||
def test_new_password_length(self):
|
|
||||||
location = self.recreate_password_url
|
|
||||||
|
|
||||||
expected_password_length = 32
|
|
||||||
|
|
||||||
response = self.client.post(location, {'username': self.user.username})
|
|
||||||
new_password = response.context['password']
|
|
||||||
self.assertEqual(len(new_password), expected_password_length)
|
|
||||||
|
|
||||||
expected_password_length = 12
|
|
||||||
self.app_settings.auto_password_length = expected_password_length
|
|
||||||
|
|
||||||
response = self.client.post(location, {'username': self.user.username})
|
|
||||||
new_password = response.context['password']
|
|
||||||
self.assertEqual(len(new_password), expected_password_length)
|
|
||||||
|
|
||||||
def test_new_password_chars(self):
|
|
||||||
location = self.recreate_password_url
|
|
||||||
|
|
||||||
password_chars = 'xYz0'
|
|
||||||
self.app_settings.auto_password_characters = password_chars
|
|
||||||
|
|
||||||
response = self.client.post(location, {'username': self.user.username})
|
|
||||||
new_password = response.context['password']
|
|
||||||
self.assertTrue(all(c in password_chars for c in new_password))
|
|
||||||
|
|
||||||
def test_new_password_is_valid(self):
|
|
||||||
location = self.recreate_password_url
|
|
||||||
response = self.client.post(location, {'username': self.user.username})
|
|
||||||
new_password = response.context['password']
|
|
||||||
validate_password(new_password)
|
|
||||||
|
|||||||
+13
-23
@@ -1,6 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import logging
|
import logging
|
||||||
import secrets
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
@@ -8,7 +7,6 @@ from django.contrib.auth import views as auth_views, get_user_model
|
|||||||
from django.contrib.auth.password_validation import validate_password
|
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.template.loader import render_to_string
|
|
||||||
from django.urls import reverse_lazy, reverse
|
from django.urls import reverse_lazy, reverse
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
@@ -24,7 +22,6 @@ logger = logging.getLogger(__name__)
|
|||||||
class LoginView(auth_views.LoginView):
|
class LoginView(auth_views.LoginView):
|
||||||
form_class = forms.LoginForm
|
form_class = forms.LoginForm
|
||||||
template_name = 'dav_auth/forms/login.html'
|
template_name = 'dav_auth/forms/login.html'
|
||||||
weak_password_warning_template_name = 'dav_auth/includes/weak_password_warning.html'
|
|
||||||
|
|
||||||
def get_redirect_url(self):
|
def get_redirect_url(self):
|
||||||
url = super().get_redirect_url()
|
url = super().get_redirect_url()
|
||||||
@@ -38,8 +35,14 @@ class LoginView(auth_views.LoginView):
|
|||||||
try:
|
try:
|
||||||
validate_password(form.cleaned_data['password'])
|
validate_password(form.cleaned_data['password'])
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
logger.warning('Detected weak password for user id %d: %s', self.request.user.pk, e)
|
logger.warning('Weak password (%d): %s', self.request.user.pk, e)
|
||||||
message = render_to_string(self.weak_password_warning_template_name)
|
message = '<br />\n<p>\n'
|
||||||
|
message += 'Dein Passwort entspricht nicht mehr den aktuellen Passwortrichtlinien.<br />\n'
|
||||||
|
message += 'Bitte hilf uns die Daten deiner Teilnehmer zu schützen und ändere dein Passwort.<br />\n'
|
||||||
|
message += '</p>\n'
|
||||||
|
message += '<p>\n'
|
||||||
|
message += '<a href="%(href)s">Passwort ändern</a>\n' % {'href': reverse('dav_auth:set_password')}
|
||||||
|
message += '</p>\n<br />\n'
|
||||||
messages.warning(self.request, mark_safe(message))
|
messages.warning(self.request, mark_safe(message))
|
||||||
return r
|
return r
|
||||||
|
|
||||||
@@ -68,7 +71,7 @@ class SetPasswordView(auth_views.PasswordChangeView):
|
|||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
r = super().form_valid(form)
|
r = super().form_valid(form)
|
||||||
messages.success(self.request, _('Passwort gespeichert.'))
|
messages.success(self.request, _('Passwort gespeichert.'))
|
||||||
logger.info('Changed password for user \'%s\'', self.request.user)
|
logger.info('Changed Password for user \'%s\'', self.request.user)
|
||||||
if form.cleaned_data.get('send_password_mail', False):
|
if form.cleaned_data.get('send_password_mail', False):
|
||||||
email = emails.PasswordSetEmail(self.request.user, form.cleaned_data['new_password'])
|
email = emails.PasswordSetEmail(self.request.user, form.cleaned_data['new_password'])
|
||||||
email.send()
|
email.send()
|
||||||
@@ -80,33 +83,20 @@ class CreateAndSendPasswordView(generic.FormView):
|
|||||||
template_name = 'dav_auth/forms/recreate_password.html'
|
template_name = 'dav_auth/forms/recreate_password.html'
|
||||||
success_url = reverse_lazy('dav_auth:login')
|
success_url = reverse_lazy('dav_auth:login')
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _create_new_password(length=None, characters=None):
|
|
||||||
if length is None:
|
|
||||||
length = app_config.settings.auto_password_length
|
|
||||||
if characters is None:
|
|
||||||
characters = app_config.settings.auto_password_characters
|
|
||||||
return ''.join(secrets.choice(characters) for i in range(length))
|
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
username = form.cleaned_data.get('username')
|
username = form.cleaned_data.get('username')
|
||||||
user_model = get_user_model()
|
user_model = get_user_model()
|
||||||
|
|
||||||
# Generate a new password (even if the user does not exist, to avoid revealing that fact).
|
|
||||||
random_password = self._create_new_password()
|
|
||||||
|
|
||||||
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=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()
|
||||||
logger.info('Recreated password for user \'%s\'', username)
|
|
||||||
except user_model.DoesNotExist:
|
|
||||||
logger.warning('Recreated password for unknown user \'%s\'', username)
|
|
||||||
|
|
||||||
# Show message, that we sent an email, even we did not, so we do not reveal that the user doesn't exist.
|
|
||||||
messages.success(self.request, _('Neues Passwort versendet.'))
|
messages.success(self.request, _('Neues Passwort versendet.'))
|
||||||
|
logger.info('Password recreated for user \'%s\'', username)
|
||||||
|
except user_model.DoesNotExist:
|
||||||
|
logger.warning('Password recreated for unknown user \'%s\'', username)
|
||||||
|
|
||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,2 @@
|
|||||||
from .constants import DJANGO_MAIN_MODULE, MODULE_APP_SETTINGS_PREFIX, MODULES_CONFIG_FILE_NAME
|
|
||||||
from . import apps
|
from . import apps
|
||||||
from . import modules
|
from . import modules
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import re
|
|||||||
from django.apps import AppConfig as _AppConfig
|
from django.apps import AppConfig as _AppConfig
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
|
||||||
from ..config import DJANGO_MAIN_MODULE, MODULE_APP_SETTINGS_PREFIX
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@@ -20,8 +18,7 @@ class DefaultSetting: # pylint: disable=too-few-public-methods
|
|||||||
self.validator = validator
|
self.validator = validator
|
||||||
|
|
||||||
def validate(self, value):
|
def validate(self, value):
|
||||||
if self.validator is None:
|
if hasattr(self, 'validator') and self.validator is not None:
|
||||||
return
|
|
||||||
if callable(self.validator):
|
if callable(self.validator):
|
||||||
if not self.validator(value):
|
if not self.validator(value):
|
||||||
raise ImproperlyConfigured('Validator callback {clb} returned False'.format(clb=self.validator))
|
raise ImproperlyConfigured('Validator callback {clb} returned False'.format(clb=self.validator))
|
||||||
@@ -32,9 +29,7 @@ class DefaultSetting: # pylint: disable=too-few-public-methods
|
|||||||
|
|
||||||
class AppSettings: # pylint: disable=too-few-public-methods
|
class AppSettings: # pylint: disable=too-few-public-methods
|
||||||
def __init__(self, app_name, defaults):
|
def __init__(self, app_name, defaults):
|
||||||
settings_name = '{main_module}.{prefix}{app_name}'.format(main_module=DJANGO_MAIN_MODULE,
|
settings_name = 'main.settings-' + app_name
|
||||||
prefix=MODULE_APP_SETTINGS_PREFIX,
|
|
||||||
app_name=app_name)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
settings_module = importlib.import_module(settings_name)
|
settings_module = importlib.import_module(settings_name)
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
DJANGO_MAIN_MODULE = 'main'
|
|
||||||
MODULES_CONFIG_FILE_NAME = 'module_config.json'
|
|
||||||
MODULE_APP_SETTINGS_PREFIX = 'settings-'
|
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from importlib.resources import files as resource_files
|
import pkg_resources
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.urls import re_path, include
|
from django.urls import re_path, include
|
||||||
|
|
||||||
from ..config import DJANGO_MAIN_MODULE, MODULES_CONFIG_FILE_NAME
|
DJANGO_MAIN_MODULE = 'main'
|
||||||
|
MODULE_CONFIG_FILE_NAME = 'module_config.json'
|
||||||
|
|
||||||
|
|
||||||
class ModuleConfigError(Exception):
|
class ModuleConfigError(Exception):
|
||||||
@@ -16,12 +17,11 @@ class ModuleMeta:
|
|||||||
_json_file = 'module.json'
|
_json_file = 'module.json'
|
||||||
_root_url_name = 'root'
|
_root_url_name = 'root'
|
||||||
|
|
||||||
def __init__(self, package_name, load=True):
|
def __init__(self, package_name):
|
||||||
self._package_name = package_name
|
self._package_name = package_name
|
||||||
self._app_config = None
|
self._app_config = None
|
||||||
self._additional_apps = []
|
self._additional_apps = []
|
||||||
self._url_prefix = None
|
self._url_prefix = None
|
||||||
if load:
|
|
||||||
self._load_from_package()
|
self._load_from_package()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@@ -63,13 +63,9 @@ class ModuleMeta:
|
|||||||
url_conf = self._package_name + '.urls'
|
url_conf = self._package_name + '.urls'
|
||||||
return re_path(url_pattern, include(url_conf, self.url_namespace))
|
return re_path(url_pattern, include(url_conf, self.url_namespace))
|
||||||
|
|
||||||
@property
|
|
||||||
def root_url_name(self):
|
|
||||||
return '{}:{}'.format(self.url_namespace, self._root_url_name)
|
|
||||||
|
|
||||||
def _load_from_package(self):
|
def _load_from_package(self):
|
||||||
package_name = self._package_name
|
package_name = self._package_name
|
||||||
json_text = resource_files(package_name).joinpath(self._json_file).read_bytes()
|
json_text = pkg_resources.resource_string(package_name, self._json_file)
|
||||||
meta_dict = json.loads(json_text)
|
meta_dict = json.loads(json_text)
|
||||||
meta_dict['package'] = package_name
|
meta_dict['package'] = package_name
|
||||||
self.load_from_dict(meta_dict)
|
self.load_from_dict(meta_dict)
|
||||||
@@ -100,13 +96,13 @@ class ModuleConfig:
|
|||||||
if config_file_path is None:
|
if config_file_path is None:
|
||||||
if django_base_dir is None:
|
if django_base_dir is None:
|
||||||
django_base_dir = settings.BASE_DIR
|
django_base_dir = settings.BASE_DIR
|
||||||
config_file_path = os.path.join(django_base_dir, DJANGO_MAIN_MODULE, MODULES_CONFIG_FILE_NAME)
|
config_file_path = os.path.join(django_base_dir, DJANGO_MAIN_MODULE, MODULE_CONFIG_FILE_NAME)
|
||||||
self._config_file_path = config_file_path
|
self._config_file_path = config_file_path
|
||||||
|
|
||||||
self._modules = {}
|
self._modules = {}
|
||||||
|
|
||||||
self._loaded = False
|
self._loaded = False
|
||||||
if not self._lazy_load: # pragma: no cover
|
if not self._lazy_load:
|
||||||
self._load()
|
self._load()
|
||||||
|
|
||||||
def _lazy_init(self):
|
def _lazy_init(self):
|
||||||
@@ -127,7 +123,7 @@ class ModuleConfig:
|
|||||||
if 'modules' in data:
|
if 'modules' in data:
|
||||||
for meta_dict in data['modules']:
|
for meta_dict in data['modules']:
|
||||||
module_name = meta_dict['package']
|
module_name = meta_dict['package']
|
||||||
self._modules[module_name] = ModuleMeta(module_name, load=False)
|
self._modules[module_name] = ModuleMeta(module_name)
|
||||||
self._modules[module_name].load_from_dict(meta_dict)
|
self._modules[module_name].load_from_dict(meta_dict)
|
||||||
|
|
||||||
self._loaded = True
|
self._loaded = True
|
||||||
|
|||||||
@@ -2,11 +2,10 @@ import argparse
|
|||||||
import os
|
import os
|
||||||
import posix
|
import posix
|
||||||
import sys
|
import sys
|
||||||
from importlib.resources import files as resource_files
|
import pkg_resources
|
||||||
from django.core.management import execute_from_command_line
|
from django.core.management import execute_from_command_line
|
||||||
|
|
||||||
from dav_base.config import DJANGO_MAIN_MODULE, MODULE_APP_SETTINGS_PREFIX
|
from dav_base.config.modules import DJANGO_MAIN_MODULE, ModuleConfig
|
||||||
from dav_base.config.modules import ModuleConfig
|
|
||||||
|
|
||||||
VERSION = '0.1'
|
VERSION = '0.1'
|
||||||
|
|
||||||
@@ -95,48 +94,46 @@ class AdminCommand: # pylint: disable=too-few-public-methods
|
|||||||
config = ModuleConfig(django_base_dir=django_base_dir)
|
config = ModuleConfig(django_base_dir=django_base_dir)
|
||||||
config.save()
|
config.save()
|
||||||
|
|
||||||
input_file = resource_files(__package__).joinpath('django_project_config',
|
input_file = os.path.join('django_project_config', 'additional_settings.py')
|
||||||
'additional_settings.py')
|
|
||||||
output_file = os.path.join(django_base_dir, django_main_module, 'settings.py')
|
output_file = os.path.join(django_base_dir, django_main_module, 'settings.py')
|
||||||
with open(output_file, 'ab') as f:
|
with open(output_file, 'ab') as f:
|
||||||
f.write(input_file.read_bytes())
|
f.write(pkg_resources.resource_string(__package__, input_file))
|
||||||
|
|
||||||
input_file = resource_files(__package__).joinpath('django_project_config',
|
input_file = os.path.join('django_project_config', 'urls.py')
|
||||||
'urls.py')
|
|
||||||
output_file = os.path.join(django_base_dir, django_main_module, 'urls.py')
|
output_file = os.path.join(django_base_dir, django_main_module, 'urls.py')
|
||||||
with open(output_file, 'wb') as f:
|
with open(output_file, 'wb') as f:
|
||||||
f.write(input_file.read_bytes())
|
f.write(pkg_resources.resource_string(__package__, input_file))
|
||||||
|
|
||||||
input_file = resource_files(__package__).joinpath('django_project_config',
|
input_file = os.path.join('django_project_config', 'settings-dav_base.py')
|
||||||
'{prefix}dav_base.py'.format(prefix=MODULE_APP_SETTINGS_PREFIX))
|
output_file = os.path.join(django_base_dir, django_main_module, 'settings-dav_base.py')
|
||||||
output_file = os.path.join(django_base_dir,
|
|
||||||
django_main_module,
|
|
||||||
'{prefix}dav_base.py'.format(prefix=MODULE_APP_SETTINGS_PREFIX))
|
|
||||||
with open(output_file, 'wb') as f:
|
with open(output_file, 'wb') as f:
|
||||||
f.write(input_file.read_bytes())
|
f.write(pkg_resources.resource_string(__package__, input_file))
|
||||||
|
|
||||||
return posix.EX_OK
|
return posix.EX_OK
|
||||||
|
|
||||||
def _subcmd_enable_module(self, cmd_args):
|
def _subcmd_enable_module(self, cmd_args):
|
||||||
|
django_main_module = DJANGO_MAIN_MODULE
|
||||||
django_base_dir = cmd_args.path
|
django_base_dir = cmd_args.path
|
||||||
module_name = cmd_args.module
|
module_name = cmd_args.module
|
||||||
sys.path.append(django_base_dir)
|
sys.path.append(django_base_dir)
|
||||||
os.environ['DJANGO_SETTINGS_MODULE'] = '{}.settings'.format(DJANGO_MAIN_MODULE)
|
os.environ['DJANGO_SETTINGS_MODULE'] = '{}.settings'.format(django_main_module)
|
||||||
execute_from_command_line(['manage.py', 'enable_module', module_name])
|
execute_from_command_line(['manage.py', 'enable_module', module_name])
|
||||||
return posix.EX_OK
|
return posix.EX_OK
|
||||||
|
|
||||||
def _subcmd_disable_module(self, cmd_args):
|
def _subcmd_disable_module(self, cmd_args):
|
||||||
|
django_main_module = DJANGO_MAIN_MODULE
|
||||||
django_base_dir = cmd_args.path
|
django_base_dir = cmd_args.path
|
||||||
module_name = cmd_args.module
|
module_name = cmd_args.module
|
||||||
sys.path.append(django_base_dir)
|
sys.path.append(django_base_dir)
|
||||||
os.environ['DJANGO_SETTINGS_MODULE'] = '{}.settings'.format(DJANGO_MAIN_MODULE)
|
os.environ['DJANGO_SETTINGS_MODULE'] = '{}.settings'.format(django_main_module)
|
||||||
execute_from_command_line(['manage.py', 'disable_module', module_name])
|
execute_from_command_line(['manage.py', 'disable_module', module_name])
|
||||||
return posix.EX_OK
|
return posix.EX_OK
|
||||||
|
|
||||||
def _subcmd_list_modules(self, cmd_args):
|
def _subcmd_list_modules(self, cmd_args):
|
||||||
|
django_main_module = DJANGO_MAIN_MODULE
|
||||||
django_base_dir = cmd_args.path
|
django_base_dir = cmd_args.path
|
||||||
sys.path.append(django_base_dir)
|
sys.path.append(django_base_dir)
|
||||||
os.environ['DJANGO_SETTINGS_MODULE'] = '{}.settings'.format(DJANGO_MAIN_MODULE)
|
os.environ['DJANGO_SETTINGS_MODULE'] = '{}.settings'.format(django_main_module)
|
||||||
execute_from_command_line(['manage.py', 'list_modules'])
|
execute_from_command_line(['manage.py', 'list_modules'])
|
||||||
return posix.EX_OK
|
return posix.EX_OK
|
||||||
|
|
||||||
|
|||||||
+5
-2
@@ -48,7 +48,7 @@ class AbstractMail: # pylint: disable=too-few-public-methods
|
|||||||
def _get_reply_to(self):
|
def _get_reply_to(self):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def send(self):
|
def send(self, fail_silently=False):
|
||||||
subject = self._get_subject()
|
subject = self._get_subject()
|
||||||
body = self._get_body()
|
body = self._get_body()
|
||||||
sender = self._get_sender()
|
sender = self._get_sender()
|
||||||
@@ -56,5 +56,8 @@ class AbstractMail: # pylint: disable=too-few-public-methods
|
|||||||
reply_to = self._get_reply_to()
|
reply_to = self._get_reply_to()
|
||||||
|
|
||||||
email = EmailMessage(subject=subject, body=body, from_email=sender, to=recipients, reply_to=reply_to)
|
email = EmailMessage(subject=subject, body=body, from_email=sender, to=recipients, reply_to=reply_to)
|
||||||
|
if fail_silently:
|
||||||
|
logger.info('Fake sending %s to %s', self.__class__.__name__, recipients)
|
||||||
|
else:
|
||||||
logger.info('Send %s to %s', self.__class__.__name__, recipients)
|
logger.info('Send %s to %s', self.__class__.__name__, recipients)
|
||||||
email.send()
|
email.send(fail_silently=fail_silently)
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import os
|
import os
|
||||||
from importlib.resources import files as resource_files
|
import pkg_resources
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.management.base import BaseCommand, CommandError
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
|
|
||||||
from dav_base.config import DJANGO_MAIN_MODULE, MODULE_APP_SETTINGS_PREFIX
|
from dav_base.config.modules import DJANGO_MAIN_MODULE, ModuleMeta
|
||||||
from dav_base.config.modules import ModuleMeta
|
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
@@ -23,15 +22,13 @@ class Command(BaseCommand):
|
|||||||
if module_name in config.modules.keys():
|
if module_name in config.modules.keys():
|
||||||
raise CommandError('Module \'{}\' is already enabled'.format(module_name))
|
raise CommandError('Module \'{}\' is already enabled'.format(module_name))
|
||||||
|
|
||||||
settings_file_name = '{prefix}{module_name}.py'.format(prefix=MODULE_APP_SETTINGS_PREFIX,
|
settings_file_name = 'settings-{}.py'.format(module_name)
|
||||||
module_name=module_name)
|
input_file = os.path.join('django_project_config', settings_file_name)
|
||||||
|
if pkg_resources.resource_exists(module_name, input_file):
|
||||||
input_file = resource_files(module_name).joinpath('django_project_config', settings_file_name)
|
|
||||||
if input_file.is_file():
|
|
||||||
output_file = os.path.join(django_base_dir, django_main_module, settings_file_name)
|
output_file = os.path.join(django_base_dir, django_main_module, settings_file_name)
|
||||||
if not os.path.exists(output_file):
|
if not os.path.exists(output_file):
|
||||||
with open(output_file, 'wb') as f:
|
with open(output_file, 'wb') as f:
|
||||||
f.write(input_file.read_bytes())
|
f.write(pkg_resources.resource_string(module_name, input_file))
|
||||||
|
|
||||||
module_meta_obj = ModuleMeta(module_name)
|
module_meta_obj = ModuleMeta(module_name)
|
||||||
config.modules[module_name] = module_meta_obj
|
config.modules[module_name] = module_meta_obj
|
||||||
|
|||||||
@@ -13,8 +13,7 @@
|
|||||||
* purple #866dac #566088 #e1d8f0 #c2b0e1 #a694c2 #866dac #8067a8 #2f263c #5a4876 -------- MTB - ----- ------
|
* purple #866dac #566088 #e1d8f0 #c2b0e1 #a694c2 #866dac #8067a8 #2f263c #5a4876 -------- MTB - ----- ------
|
||||||
* plum #aa6c95 #784c69 #f0d4e7 #e6b0d4 #be91af #aa6c95 #a66691 #3c2635 #764867 -------- ----- famil ------
|
* plum #aa6c95 #784c69 #f0d4e7 #e6b0d4 #be91af #aa6c95 #a66691 #3c2635 #764867 -------- ----- famil ------
|
||||||
* brown #??? #??? #??? #??? #925f36 #??? #??? #??? #??? -------- ----- ----- ------
|
* brown #??? #??? #??? #??? #925f36 #??? #??? #??? #??? -------- ----- ----- ------
|
||||||
* black #333 #??? #333 #??? #??? #??? #??? #??? #??? -------- ----- ----- clear
|
* black #??? #??? #??? #??? #??? #??? #??? #??? #??? -------- ----- ----- clear
|
||||||
* gray #??? #??? #878787 #??? #??? #??? #??? #??? #??? -------- ----- ----- ------
|
|
||||||
* white #??? #??? #??? #??? #??? #??? #??? #??? #??? -------- ----- ----- ------
|
* white #??? #??? #??? #??? #??? #??? #??? #??? #??? -------- ----- ----- ------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{# This template is used by software tests #}{{ var1 }}{{ var2 }}MAILBODY
|
{# This template is used by software tests #}MAILBODY
|
||||||
@@ -10,7 +10,7 @@ def do_include_if_exist(parser, token):
|
|||||||
|
|
||||||
include_if_exist support an optional keyword 'default', which must be followed by the name of a default template.
|
include_if_exist support an optional keyword 'default', which must be followed by the name of a default template.
|
||||||
The default template will be included, if the first template does not exist.
|
The default template will be included, if the first template does not exist.
|
||||||
If also the default template does not exist, the behavior is the same as for the original (builtin) include tag.
|
If also the default template does not exist, the behaviour is the same as for the original (builtin) include tag.
|
||||||
|
|
||||||
If no default template is given and the first template does not exist an empty string will be returned.
|
If no default template is given and the first template does not exist an empty string will be returned.
|
||||||
"""
|
"""
|
||||||
@@ -27,7 +27,7 @@ def do_include_if_exist(parser, token):
|
|||||||
token = template.base.Token(token.token_type, ' '.join(bits))
|
token = template.base.Token(token.token_type, ' '.join(bits))
|
||||||
token2 = template.base.Token(token.token_type, bits[0] + ' ' + default_template + ' '.join(bits[2:]))
|
token2 = template.base.Token(token.token_type, bits[0] + ' ' + default_template + ' '.join(bits[2:]))
|
||||||
default_node = template.loader_tags.do_include(parser, token2)
|
default_node = template.loader_tags.do_include(parser, token2)
|
||||||
# TODO: I believe, this ist not the correct way to do things.
|
# TODO: I belive, this ist not the correct way to do things.
|
||||||
# But without setting default_node.origin here the following AttributeError will be risen within the tests:
|
# But without setting default_node.origin here the following AttributeError will be risen within the tests:
|
||||||
# AttributeError: 'IncludeNode' object has no attribute 'origin'
|
# AttributeError: 'IncludeNode' object has no attribute 'origin'
|
||||||
default_node.origin = template.Origin(name='<unknown_source>', template_name=None)
|
default_node.origin = template.Origin(name='<unknown_source>', template_name=None)
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"url_prefix": "test",
|
|
||||||
"app_config": ".apps.AppConfig"
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
from django.urls import re_path
|
|
||||||
from django.views import generic
|
|
||||||
|
|
||||||
app_name = 'fake_app'
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
re_path(r'^$', generic.TemplateView.as_view(), name='root'),
|
|
||||||
]
|
|
||||||
@@ -12,13 +12,10 @@ from selenium import webdriver
|
|||||||
from selenium.webdriver.support.ui import WebDriverWait
|
from selenium.webdriver.support.ui import WebDriverWait
|
||||||
from selenium.webdriver.support import expected_conditions as ExpectedConditions
|
from selenium.webdriver.support import expected_conditions as ExpectedConditions
|
||||||
|
|
||||||
from ..config.apps import DefaultSetting
|
|
||||||
|
|
||||||
|
|
||||||
class AppSetting: # pylint: disable=too-few-public-methods
|
class AppSetting: # pylint: disable=too-few-public-methods
|
||||||
def __init__(self, name, default, of=None):
|
def __init__(self, name, of=None):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.default = default
|
|
||||||
self.of = of
|
self.of = of
|
||||||
|
|
||||||
|
|
||||||
@@ -29,29 +26,16 @@ class AppsTestCase(SimpleTestCase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
if self.app_config:
|
if self.app_config:
|
||||||
self.default_settings = self.app_config.default_settings
|
|
||||||
self.configured_settings = self.app_config.settings
|
self.configured_settings = self.app_config.settings
|
||||||
else:
|
else:
|
||||||
self.default_settings = ()
|
|
||||||
self.configured_settings = None
|
self.configured_settings = None
|
||||||
|
|
||||||
def test_defaults(self):
|
def test_settings(self):
|
||||||
defaults = {}
|
config = self.configured_settings
|
||||||
for d in self.default_settings:
|
|
||||||
self.assertIsInstance(d, DefaultSetting)
|
|
||||||
defaults[d.name] = d.value
|
|
||||||
|
|
||||||
for setting in self.settings:
|
for setting in self.settings:
|
||||||
name = setting.name
|
name = setting.name
|
||||||
self.assertIn(name, defaults.keys())
|
self.assertTrue(hasattr(config, name), 'Settings do not contain {}'.format(name))
|
||||||
self.assertEqual(defaults[name], setting.default, 'Default value of {} is not correct'.format(name))
|
value = getattr(config, name)
|
||||||
|
|
||||||
def test_configured_settings(self):
|
|
||||||
for setting in self.settings:
|
|
||||||
name = setting.name
|
|
||||||
self.assertTrue(hasattr(self.configured_settings, name), 'Settings do not contain {}'.format(name))
|
|
||||||
|
|
||||||
value = getattr(self.configured_settings, name)
|
|
||||||
of = setting.of
|
of = setting.of
|
||||||
if of is not None:
|
if of is not None:
|
||||||
self.assertIsInstance(value, of)
|
self.assertIsInstance(value, of)
|
||||||
@@ -86,8 +70,8 @@ class EmailTestMixin:
|
|||||||
for expected_recipient in recipients:
|
for expected_recipient in recipients:
|
||||||
if isinstance(expected_recipient, AbstractUser):
|
if isinstance(expected_recipient, AbstractUser):
|
||||||
expected_recipient = '"%s" <%s>' % (expected_recipient.get_full_name(), expected_recipient.email)
|
expected_recipient = '"%s" <%s>' % (expected_recipient.get_full_name(), expected_recipient.email)
|
||||||
real_recipients = mail.recipients()
|
recipients = mail.recipients()
|
||||||
self.assertIn(expected_recipient, real_recipients)
|
self.assertIn(expected_recipient, recipients)
|
||||||
|
|
||||||
def assertSubject(self, mail, subject): # pylint: disable=invalid-name
|
def assertSubject(self, mail, subject): # pylint: disable=invalid-name
|
||||||
expected_subject = '{} {}'.format(self.email_subject_prefix, subject)
|
expected_subject = '{} {}'.format(self.email_subject_prefix, subject)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
|
from six import string_types
|
||||||
|
|
||||||
from .generic import AppSetting, AppsTestCase
|
from .generic import AppSetting, AppsTestCase
|
||||||
|
|
||||||
@@ -7,7 +8,7 @@ class TestCase(AppsTestCase):
|
|||||||
app_config = apps.get_app_config('dav_base')
|
app_config = apps.get_app_config('dav_base')
|
||||||
|
|
||||||
settings = (
|
settings = (
|
||||||
AppSetting('email_sender', None, str),
|
AppSetting('email_sender', string_types),
|
||||||
AppSetting('email_base_url', None, str),
|
AppSetting('email_base_url', string_types),
|
||||||
AppSetting('email_subject_prefix', '', str),
|
AppSetting('email_subject_prefix', string_types),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -169,3 +169,15 @@ class AppConfigTestCase(SimpleTestCase):
|
|||||||
self.assertEqual(app_config.__class__, RealAppConfig)
|
self.assertEqual(app_config.__class__, RealAppConfig)
|
||||||
self.assertIsInstance(app_config, AppConfig)
|
self.assertIsInstance(app_config, AppConfig)
|
||||||
self.assertEqual(app_config.settings.__class__, AppSettings)
|
self.assertEqual(app_config.settings.__class__, AppSettings)
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleMetaTestCase(SimpleTestCase):
|
||||||
|
def test_some(self):
|
||||||
|
# TODO
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleConfigTestCase(SimpleTestCase):
|
||||||
|
def test_some(self):
|
||||||
|
# TODO
|
||||||
|
pass
|
||||||
@@ -1,240 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
import datetime
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
from unittest.mock import patch
|
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
|
||||||
from django.test import SimpleTestCase
|
|
||||||
from django.urls import URLResolver
|
|
||||||
|
|
||||||
from ..config.modules import ModuleMeta, ModuleConfig
|
|
||||||
|
|
||||||
from .utils import mkdtemp
|
|
||||||
|
|
||||||
TMP_BASE_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'tmp')
|
|
||||||
|
|
||||||
|
|
||||||
class ModuleMetaTestCase(SimpleTestCase):
|
|
||||||
def test_init(self):
|
|
||||||
mm = ModuleMeta('dav_base.tests.fake_app1')
|
|
||||||
self.assertEqual(mm.package, 'dav_base.tests.fake_app1')
|
|
||||||
self.assertEqual(mm.app, 'dav_base.tests.fake_app1.apps.AppConfig')
|
|
||||||
self.assertEqual(mm.additional_apps, [])
|
|
||||||
self.assertEqual(mm.url_prefix, 'test')
|
|
||||||
self.assertEqual(mm.url_namespace, 'dav_base_tests_fake_app1')
|
|
||||||
self.assertEqual(mm.root_url_name, 'dav_base_tests_fake_app1:root')
|
|
||||||
pattern = mm.url_conf_pattern
|
|
||||||
self.assertIsInstance(pattern, URLResolver)
|
|
||||||
self.assertEqual('^test/', str(pattern.pattern))
|
|
||||||
|
|
||||||
def test_module_not_exists(self):
|
|
||||||
with self.assertRaises(ModuleNotFoundError):
|
|
||||||
_ = ModuleMeta('dav_base.tests.non_existent_app')
|
|
||||||
|
|
||||||
def test_init_without_load(self):
|
|
||||||
app_name = 'dav_base.tests.non_existent_app'
|
|
||||||
app_namespace = app_name.replace('.', '_')
|
|
||||||
mm = ModuleMeta(app_name, load=False)
|
|
||||||
self.assertEqual(mm.package, app_name)
|
|
||||||
self.assertEqual(mm.app, app_name)
|
|
||||||
self.assertEqual(mm.url_prefix, app_name)
|
|
||||||
self.assertEqual(mm.url_namespace, app_namespace)
|
|
||||||
self.assertEqual(mm.root_url_name, app_namespace + ':root')
|
|
||||||
|
|
||||||
def test_url_config_not_exists(self):
|
|
||||||
mm = ModuleMeta('dav_base.tests.fake_app1')
|
|
||||||
dd = {'package': 'dav_base'}
|
|
||||||
mm.load_from_dict(dd)
|
|
||||||
with self.assertRaises(ImproperlyConfigured):
|
|
||||||
_ = mm.url_conf_pattern
|
|
||||||
|
|
||||||
def test_str(self):
|
|
||||||
app_name = 'dav_base.tests.fake_app1'
|
|
||||||
mm = ModuleMeta(app_name)
|
|
||||||
self.assertEqual(str(mm), '- {}'.format(app_name))
|
|
||||||
|
|
||||||
def test_load_from_dict(self):
|
|
||||||
mm = ModuleMeta('dav_base.tests.fake_app1')
|
|
||||||
dd = {'package': 'dav_base2.foo',
|
|
||||||
'app_config': 'MyApp.MyAppConfig',
|
|
||||||
'additional_apps': ['test1', 'test2.subtest'],
|
|
||||||
'url_prefix': 'test_url_prefix',
|
|
||||||
}
|
|
||||||
expected_namespace = dd['package'].replace('.', '_')
|
|
||||||
mm.load_from_dict(dd)
|
|
||||||
self.assertEqual(mm.package, dd['package'])
|
|
||||||
self.assertEqual(mm.app, dd['app_config'])
|
|
||||||
self.assertEqual(mm.additional_apps, dd['additional_apps'])
|
|
||||||
self.assertEqual(mm.url_prefix, dd['url_prefix'])
|
|
||||||
self.assertEqual(mm.url_namespace, expected_namespace)
|
|
||||||
self.assertEqual(mm.root_url_name, expected_namespace + ':root')
|
|
||||||
|
|
||||||
dd = {'package': 'dav_base2.bar'}
|
|
||||||
expected_namespace = dd['package'].replace('.', '_')
|
|
||||||
mm.load_from_dict(dd)
|
|
||||||
self.assertEqual(mm.url_prefix, dd['package'])
|
|
||||||
self.assertEqual(mm.app, dd['package'])
|
|
||||||
self.assertEqual(mm.additional_apps, [])
|
|
||||||
self.assertEqual(mm.url_prefix, dd['package'])
|
|
||||||
self.assertEqual(mm.url_namespace, expected_namespace)
|
|
||||||
self.assertEqual(mm.root_url_name, expected_namespace + ':root')
|
|
||||||
|
|
||||||
def test_dump_as_dict(self):
|
|
||||||
mm = ModuleMeta('dav_base.tests.fake_app1')
|
|
||||||
dd_in = {'package': 'dav_base2.foo',
|
|
||||||
'app_config': '.mymod.MyAppConfig',
|
|
||||||
'additional_apps': ['test1'],
|
|
||||||
'url_prefix': 'test_url_prefix',
|
|
||||||
}
|
|
||||||
mm.load_from_dict(dd_in)
|
|
||||||
dd_out = mm.dump_as_dict()
|
|
||||||
self.assertEqual(dd_in, dd_out)
|
|
||||||
|
|
||||||
|
|
||||||
class ModuleConfigTestCase(SimpleTestCase):
|
|
||||||
def setUp(self):
|
|
||||||
prefix = 'dav_base.tests.test_config_modules-{datetime}-'.format(
|
|
||||||
datetime=datetime.datetime.now().strftime('%Y%m%d-%H%M')
|
|
||||||
)
|
|
||||||
self.temp_dir = mkdtemp(prefix=prefix, base_dir=TMP_BASE_DIR)
|
|
||||||
|
|
||||||
self.config_dir = os.path.join(self.temp_dir, 'main')
|
|
||||||
os.makedirs(self.config_dir)
|
|
||||||
self.config_file = os.path.join(self.config_dir, 'module_config.json')
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
shutil.rmtree(self.temp_dir)
|
|
||||||
|
|
||||||
def test_default_config_file_not_exists(self):
|
|
||||||
with patch('dav_base.config.modules.settings') as mock_settings:
|
|
||||||
mock_settings.BASE_DIR = self.temp_dir
|
|
||||||
mc = ModuleConfig()
|
|
||||||
self.assertEqual(mc.modules, {})
|
|
||||||
|
|
||||||
def test_default_config_file_exists(self):
|
|
||||||
config_data = {
|
|
||||||
'modules': [
|
|
||||||
{'package': 'pkg1'},
|
|
||||||
{'package': 'pkg2'},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
with open(self.config_file, 'w', encoding='ascii') as f:
|
|
||||||
json.dump(config_data, f)
|
|
||||||
|
|
||||||
with patch('dav_base.config.modules.settings') as mock_settings:
|
|
||||||
mock_settings.BASE_DIR = self.temp_dir
|
|
||||||
mc = ModuleConfig()
|
|
||||||
self.assertEqual(list(mc.modules.keys()), ['pkg1', 'pkg2'])
|
|
||||||
|
|
||||||
def test_django_base_dir_parameter(self):
|
|
||||||
config_data = {
|
|
||||||
'modules': [
|
|
||||||
{'package': 'pkgA'},
|
|
||||||
{'package': 'pkgB'},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
with open(self.config_file, 'w', encoding='ascii') as f:
|
|
||||||
json.dump(config_data, f)
|
|
||||||
|
|
||||||
with patch('dav_base.config.modules.settings') as mock_settings:
|
|
||||||
mock_settings.BASE_DIR = self.config_dir # No config file there
|
|
||||||
|
|
||||||
mc = ModuleConfig()
|
|
||||||
self.assertEqual(mc.modules, {})
|
|
||||||
|
|
||||||
mc = ModuleConfig(django_base_dir=self.temp_dir)
|
|
||||||
self.assertEqual(list(mc.modules.keys()), ['pkgA', 'pkgB'])
|
|
||||||
|
|
||||||
def test_custom_config_file_path(self):
|
|
||||||
config_data = {
|
|
||||||
'modules': [
|
|
||||||
{'package': 'pkgX'},
|
|
||||||
{'package': 'pkgY'},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
config_file = os.path.join(self.config_dir, 'test.json')
|
|
||||||
with open(config_file, 'w', encoding='ascii') as f:
|
|
||||||
json.dump(config_data, f)
|
|
||||||
|
|
||||||
with patch('dav_base.config.modules.settings') as mock_settings:
|
|
||||||
mock_settings.BASE_DIR = self.temp_dir
|
|
||||||
mc = ModuleConfig(config_file_path=config_file)
|
|
||||||
self.assertEqual(list(mc.modules.keys()), ['pkgX', 'pkgY'])
|
|
||||||
|
|
||||||
def test_custom_config_file_path_and_django_base_dir(self):
|
|
||||||
config_data = {
|
|
||||||
'modules': [
|
|
||||||
{'package': 'pkg11'},
|
|
||||||
{'package': 'pkg22'},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
config_file = os.path.join(self.config_dir, 'test.json')
|
|
||||||
with open(config_file, 'w', encoding='ascii') as f:
|
|
||||||
json.dump(config_data, f)
|
|
||||||
|
|
||||||
with patch('dav_base.config.modules.settings') as mock_settings:
|
|
||||||
mock_settings.BASE_DIR = self.temp_dir
|
|
||||||
mc = ModuleConfig(config_file_path=config_file, django_base_dir=self.config_dir)
|
|
||||||
self.assertEqual(list(mc.modules.keys()), ['pkg11', 'pkg22'])
|
|
||||||
|
|
||||||
def test_save(self):
|
|
||||||
config_data = {
|
|
||||||
'modules': [
|
|
||||||
{'package': 'pkg_foo'},
|
|
||||||
{'package': 'pkg_bar'},
|
|
||||||
{'package': 'pkg_baz'},
|
|
||||||
{'package': 'pkg_quux', 'url_prefix': 'quux'},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
copy_modules = ['pkg_bar', 'pkg_quux']
|
|
||||||
expected_config_data = {'modules': [d for d in config_data['modules'] if d['package'] in copy_modules]}
|
|
||||||
|
|
||||||
config_file = os.path.join(self.config_dir, 'test.json')
|
|
||||||
with open(config_file, 'w', encoding='ascii') as f:
|
|
||||||
json.dump(config_data, f)
|
|
||||||
|
|
||||||
with patch('dav_base.config.modules.settings') as mock_settings:
|
|
||||||
mock_settings.BASE_DIR = self.temp_dir
|
|
||||||
mc1 = ModuleConfig()
|
|
||||||
mc2 = ModuleConfig(config_file_path=config_file)
|
|
||||||
for module_name in copy_modules:
|
|
||||||
mc1.modules[module_name] = mc2.modules[module_name]
|
|
||||||
mc1.save()
|
|
||||||
|
|
||||||
with open(self.config_file, 'r', encoding='ascii') as f:
|
|
||||||
config_data = json.load(f)
|
|
||||||
|
|
||||||
self.assertEqual(config_data, expected_config_data)
|
|
||||||
|
|
||||||
def test_save_empty(self):
|
|
||||||
self.assertFalse(os.path.exists(self.config_file))
|
|
||||||
with patch('dav_base.config.modules.settings') as mock_settings:
|
|
||||||
mock_settings.BASE_DIR = self.temp_dir
|
|
||||||
mc = ModuleConfig()
|
|
||||||
mc.save()
|
|
||||||
self.assertTrue(os.path.isfile(self.config_file))
|
|
||||||
|
|
||||||
with open(self.config_file, 'r', encoding='ascii') as f:
|
|
||||||
config_data = json.load(f)
|
|
||||||
|
|
||||||
self.assertEqual(config_data, {'modules': []})
|
|
||||||
|
|
||||||
def test_save_overwrite(self):
|
|
||||||
with patch('dav_base.config.modules.settings') as mock_settings:
|
|
||||||
mock_settings.BASE_DIR = self.temp_dir
|
|
||||||
mc = ModuleConfig()
|
|
||||||
mc.save()
|
|
||||||
self.assertTrue(os.path.isfile(self.config_file))
|
|
||||||
mc.modules['mod1'] = ModuleMeta('mod1', load=False)
|
|
||||||
mc.save()
|
|
||||||
|
|
||||||
with open(self.config_file, 'r', encoding='ascii') as f:
|
|
||||||
config_data = json.load(f)
|
|
||||||
|
|
||||||
self.assertEqual(config_data, {'modules': [{'package': 'mod1'}]})
|
|
||||||
|
|
||||||
@@ -43,24 +43,4 @@ class TestCase(EmailTestMixin, SimpleTestCase):
|
|||||||
self.assertSender(mail)
|
self.assertSender(mail)
|
||||||
self.assertRecipients(mail, [recipient])
|
self.assertRecipients(mail, [recipient])
|
||||||
self.assertSubject(mail, '')
|
self.assertSubject(mail, '')
|
||||||
self.assertBody(mail, 'MAILBODY\n')
|
self.assertBody(mail, 'MAILBODY')
|
||||||
|
|
||||||
def test_send_extra_context(self):
|
|
||||||
recipient = 'root@localhost'
|
|
||||||
|
|
||||||
class ConcreteMail(AbstractMail): # pylint: disable=too-few-public-methods
|
|
||||||
_template_name = MAIL_TEMPLATE
|
|
||||||
|
|
||||||
def _get_recipients(self):
|
|
||||||
return [recipient]
|
|
||||||
|
|
||||||
def _get_body(self, context=None):
|
|
||||||
context = {'var1': 'Here is',
|
|
||||||
'var2': ' extra context. '}
|
|
||||||
return super()._get_body(context)
|
|
||||||
|
|
||||||
email = ConcreteMail()
|
|
||||||
email.send()
|
|
||||||
|
|
||||||
mail = django_mail.outbox[0]
|
|
||||||
self.assertBody(mail, 'Here is extra context. MAILBODY\n')
|
|
||||||
|
|||||||
@@ -6,23 +6,18 @@ from django.test import SimpleTestCase
|
|||||||
class TemplateTagsTestCase(SimpleTestCase):
|
class TemplateTagsTestCase(SimpleTestCase):
|
||||||
def test_include_if_exist_without_argument(self):
|
def test_include_if_exist_without_argument(self):
|
||||||
template_name = 'dav_base/tests/include_if_exist_without_argument.html'
|
template_name = 'dav_base/tests/include_if_exist_without_argument.html'
|
||||||
with self.assertRaises(TemplateSyntaxError) as cm:
|
with self.assertRaises(TemplateSyntaxError):
|
||||||
get_template(template_name)
|
get_template(template_name)
|
||||||
self.assertEqual(str(cm.exception), '\'include_if_exist\' tag takes at least one argument:'
|
|
||||||
' the name of the template to be included')
|
|
||||||
|
|
||||||
def test_include_if_exist_with_unexpected_argument(self):
|
def test_include_if_exist_with_unexpected_argument(self):
|
||||||
template_name = 'dav_base/tests/include_if_exist_with_unexpected_argument.html'
|
template_name = 'dav_base/tests/include_if_exist_with_unexpected_argument.html'
|
||||||
with self.assertRaises(TemplateSyntaxError) as cm:
|
with self.assertRaises(TemplateSyntaxError):
|
||||||
get_template(template_name)
|
get_template(template_name)
|
||||||
self.assertEqual(str(cm.exception), 'Unknown argument for \'include_if_exist\' tag: "\'bogus\'".')
|
|
||||||
|
|
||||||
def test_include_if_exist_default_without_argument(self):
|
def test_include_if_exist_default_without_argument(self):
|
||||||
template_name = 'dav_base/tests/include_if_exist_default_without_argument.html'
|
template_name = 'dav_base/tests/include_if_exist_default_without_argument.html'
|
||||||
with self.assertRaises(TemplateSyntaxError) as cm:
|
with self.assertRaises(TemplateSyntaxError):
|
||||||
get_template(template_name)
|
get_template(template_name)
|
||||||
self.assertEqual(str(cm.exception), '\'default\' keyword in \'include_if_exist\' tag requires another arguments:'
|
|
||||||
' the name of the default template')
|
|
||||||
|
|
||||||
def test_include_if_exist_default_missing(self):
|
def test_include_if_exist_default_missing(self):
|
||||||
template_name = 'dav_base/tests/include_if_exist_default_missing.html'
|
template_name = 'dav_base/tests/include_if_exist_default_missing.html'
|
||||||
|
|||||||
@@ -10,49 +10,28 @@ class DAVNumberValidatorTestCase(ValidatorTestMixin, SimpleTestCase):
|
|||||||
|
|
||||||
def test_valid_data(self):
|
def test_valid_data(self):
|
||||||
data = (
|
data = (
|
||||||
'001/00/1*0001*1869*1869*01011800',
|
|
||||||
'999/99/999999*9999*2025*2026',
|
|
||||||
'131/00/654321*1000*1999',
|
|
||||||
'131/00/54321*1000',
|
|
||||||
'131/00/1',
|
'131/00/1',
|
||||||
'076/22/012345',
|
|
||||||
'1',
|
'1',
|
||||||
'23',
|
'22',
|
||||||
'4567',
|
'333',
|
||||||
'678912',
|
'1/22/22',
|
||||||
'999999*9999',
|
'54321/54321/54321*54321',
|
||||||
'54321x0001x2004',
|
'54321/54321/54321*54321*4321*4321',
|
||||||
'54321 0001 2004 2014',
|
'54321/54321/54321*54321*4321*4321*87654321',
|
||||||
'4321*0202x1999 2022*30121999',
|
'54321*54321',
|
||||||
|
'54321*54321*4321*4321',
|
||||||
|
'54321*54321*4321*4321*87654321',
|
||||||
)
|
)
|
||||||
self.assertValid(self.validator, data)
|
self.assertValid(self.validator, data)
|
||||||
|
|
||||||
def test_invalid_data(self):
|
def test_invalid_data(self):
|
||||||
data = (
|
data = (
|
||||||
'1/00/1', # Sektionsnummer nicht dreistellig
|
'131/00/',
|
||||||
'21/00/1', # Sektionsnummer nicht dreistellig
|
'1/1/1',
|
||||||
'4321/00/1', # Sektionsnummer nicht dreistellig
|
'1/1',
|
||||||
'131/1/1', # Ortsgruppennummer nicht zweistellig
|
'abc',
|
||||||
'131/321/1', # Ortsgruppennummer nicht zweistellig
|
'131/00/abc',
|
||||||
'131/00/', # Fehlende Mitgliedsnummer
|
'abc/00/131',
|
||||||
'131/00', # Fehlende Mitgliedsnummer
|
'131/ab/131',
|
||||||
'7654321', # Mitgliedsnummer mehr als sechs Stellen
|
|
||||||
'999999*321', # Kategorienummer nicht vierstellig
|
|
||||||
'999999*54321', # Kategorienummer nicht vierstellig
|
|
||||||
'999999*9999*321', # DAV-Eintrittsjahr nicht vierstellig
|
|
||||||
'999999*9999*54321', # DAV-Eintrittsjahr nicht vierstellig
|
|
||||||
'999999*9999*9999*321', # Sektions-Eintrittsjahr nicht vierstellig
|
|
||||||
'999999*9999*9999*54321', # Sektions-Eintrittsjahr nicht vierstellig
|
|
||||||
'999999*9999*9999*9999*7654321', # Geburtsdatum nicht achtstellig
|
|
||||||
'999999*9999*9999*9999*987654321', # Geburtsdatum nicht achtstellig
|
|
||||||
'', # Leerstring
|
|
||||||
' 1', # Leerzeichen am Anfang
|
|
||||||
'54321 0001 2004 2014 ', # Leerzeichen am Ende
|
|
||||||
'abc', # Nicht numerisch
|
|
||||||
'131/00/abc', # Nicht numerisch
|
|
||||||
'abc/00/131', # Nicht numerisch
|
|
||||||
'131/ab/131', # Nicht numerisch
|
|
||||||
'131-00-131', # - statt /
|
|
||||||
'131/00/131-0001', # - statt * oder x oder Leerzeichen
|
|
||||||
)
|
)
|
||||||
self.assertInvalid(self.validator, data)
|
self.assertInvalid(self.validator, data)
|
||||||
|
|||||||
@@ -1,47 +1,10 @@
|
|||||||
from unittest.mock import patch
|
from django.test import SimpleTestCase
|
||||||
from django.test import SimpleTestCase, override_settings
|
|
||||||
from django.urls import NoReverseMatch
|
|
||||||
|
|
||||||
from ..views import RootView
|
from ..views import RootView
|
||||||
|
|
||||||
|
|
||||||
class DummyModuleMeta:
|
|
||||||
def __init__(self, package):
|
|
||||||
self.package = package
|
|
||||||
|
|
||||||
@property
|
|
||||||
def root_url_name(self):
|
|
||||||
return self.package.replace('.', '_') + ':root'
|
|
||||||
|
|
||||||
class DummyModuleConfig:
|
|
||||||
def __init__(self, modules):
|
|
||||||
# modules: dict-like mapping name -> DummyMeta
|
|
||||||
self._modules = modules
|
|
||||||
|
|
||||||
@property
|
|
||||||
def modules(self):
|
|
||||||
return self._modules
|
|
||||||
|
|
||||||
|
|
||||||
class ViewsTestCase(SimpleTestCase):
|
class ViewsTestCase(SimpleTestCase):
|
||||||
def test_root(self):
|
def test_root(self):
|
||||||
modules = {
|
|
||||||
'module1': DummyModuleMeta('pkg1'),
|
|
||||||
'module2': DummyModuleMeta('pkg2'),
|
|
||||||
'moduleC': DummyModuleMeta('pkgC'),
|
|
||||||
'moduleD': DummyModuleMeta('pkgD'),
|
|
||||||
}
|
|
||||||
expected_root_urls = [
|
|
||||||
('pkg1', 'pkg1:root'), ('pkg2', 'pkg2:root'), ('pkgD', 'pkgD:root')
|
|
||||||
]
|
|
||||||
|
|
||||||
def fake_reverse(name):
|
|
||||||
if name == 'pkgC:root':
|
|
||||||
raise NoReverseMatch()
|
|
||||||
return '/'
|
|
||||||
|
|
||||||
with override_settings(MODULE_CONFIG=DummyModuleConfig(modules)):
|
|
||||||
with patch('dav_base.views.reverse', side_effect=fake_reverse) as mocked_reverse:
|
|
||||||
view = RootView()
|
view = RootView()
|
||||||
template_names = view.get_template_names()
|
template_names = view.get_template_names()
|
||||||
self.assertEqual(len(template_names), 1)
|
self.assertEqual(len(template_names), 1)
|
||||||
@@ -49,16 +12,13 @@ class ViewsTestCase(SimpleTestCase):
|
|||||||
context = view.get_context_data()
|
context = view.get_context_data()
|
||||||
self.assertIn('root_urls', context)
|
self.assertIn('root_urls', context)
|
||||||
self.assertIsInstance(context['root_urls'], list)
|
self.assertIsInstance(context['root_urls'], list)
|
||||||
self.assertEqual(context['root_urls'], expected_root_urls)
|
|
||||||
called_names = [call.args[0] for call in mocked_reverse.call_args_list]
|
|
||||||
self.assertEqual(len(called_names), len(modules))
|
|
||||||
for m in modules.values():
|
|
||||||
self.assertIn(m.root_url_name, called_names)
|
|
||||||
|
|
||||||
def test_integrated_root(self):
|
def test_integrated_root(self):
|
||||||
with override_settings(MODULE_CONFIG=DummyModuleConfig({})):
|
|
||||||
response = self.client.get('/')
|
response = self.client.get('/')
|
||||||
self.assertTemplateUsed(response, 'dav_base/root.html')
|
self.assertTemplateUsed(response, 'dav_base/root.html')
|
||||||
self.assertIn('root_urls', response.context, '\'root_urls\' not in context of root view')
|
self.assertIn('root_urls', response.context, '\'root_urls\' not in context of root view')
|
||||||
self.assertIsInstance(response.context['root_urls'], list)
|
self.assertIsInstance(response.context['root_urls'], list)
|
||||||
self.assertEqual(response.context['root_urls'], [])
|
|
||||||
|
# TODO
|
||||||
|
# Maybe we should set a defined module config, so we could
|
||||||
|
# test the content of the root_urls context variable.
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
import os
|
import os
|
||||||
from tempfile import mkdtemp as _mkdtemp
|
from tempfile import mkdtemp as _mkdtemp
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
|
|
||||||
|
|
||||||
DAVNumberValidator = RegexValidator(r'^'
|
DAVNumberValidator = RegexValidator(r'^'
|
||||||
r'([0-9]{3}/[0-9]{2}/)?' # Optional: <Sektionsnummer 3-stellig>/<Ortsgruppennummer 2-stellig>/
|
r'([0-9]{1,10}/[0-9]{2,10}/)?'
|
||||||
r'[0-9]{1,6}' # <Mitgliedsnummer 1- bis 6-stellig>
|
r'[0-9]{1,10}'
|
||||||
r'([*x ][0-9]{4})?' # Optional: *<Kategorienummer 4-stellig>
|
r'(\*[0-9]{1,10})?'
|
||||||
r'([*x ][0-9]{4}[*x ][0-9]{4})?' # Optional: *<Jahreszahl DAV-Eintritt>*<Jahreszahl Sektionseintritt>
|
r'(\*[0-9]{4}\*[0-9]{4})?'
|
||||||
r'([*x ][0-9]{8})?' # Optional: *<Geburtsdatum YYYYMMDD>
|
r'([* ][0-9]{8})?'
|
||||||
r'$',
|
r'$',
|
||||||
_('Ungültiges Format.'))
|
_('Ungültiges Format.'))
|
||||||
|
|||||||
+3
-1
@@ -10,7 +10,9 @@ class RootView(generic.TemplateView):
|
|||||||
c = super().get_context_data(**kwargs)
|
c = super().get_context_data(**kwargs)
|
||||||
root_urls = []
|
root_urls = []
|
||||||
for module_meta_obj in settings.MODULE_CONFIG.modules.values():
|
for module_meta_obj in settings.MODULE_CONFIG.modules.values():
|
||||||
root_url_name = module_meta_obj.root_url_name
|
root_url_name = 'root'
|
||||||
|
if module_meta_obj.url_namespace:
|
||||||
|
root_url_name = '%s:%s' % (module_meta_obj.url_namespace, root_url_name)
|
||||||
try:
|
try:
|
||||||
reverse(root_url_name)
|
reverse(root_url_name)
|
||||||
root_urls.append((module_meta_obj.package, root_url_name))
|
root_urls.append((module_meta_obj.package, root_url_name))
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ class ChoiceSet(object):
|
|||||||
self._codes = list()
|
self._codes = list()
|
||||||
self._labels = dict()
|
self._labels = dict()
|
||||||
for code, label in choices:
|
for code, label in choices:
|
||||||
if code in self._codes:
|
|
||||||
raise ValueError(u'Code not unique: {}'.format(code))
|
|
||||||
self._codes.append(code)
|
self._codes.append(code)
|
||||||
self._labels[code] = label
|
self._labels[code] = label
|
||||||
|
|
||||||
@@ -78,6 +76,7 @@ LEVEL_CHOICES = ChoiceSet([
|
|||||||
('beginner', _(u'Anfänger')),
|
('beginner', _(u'Anfänger')),
|
||||||
('advanced', _(u'Fortgeschrittene')),
|
('advanced', _(u'Fortgeschrittene')),
|
||||||
('family', _(u'Familien')),
|
('family', _(u'Familien')),
|
||||||
|
('senior', _(u'Senioren'))
|
||||||
])
|
])
|
||||||
|
|
||||||
MEALS_CHOICES = ChoiceSet([
|
MEALS_CHOICES = ChoiceSet([
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ ADDITIONAL_COSTS_MAX_LENGTH = COMMON_CHAR_FIELD_LENGTH
|
|||||||
|
|
||||||
|
|
||||||
class FieldInitial(object):
|
class FieldInitial(object):
|
||||||
_constraint_re = re.compile(r'^(?P<field>[a-z_]+)(?P<op>==)(?P<value>[^= ].*)?$')
|
_constraint_re = re.compile(r'^(?P<field>[a-z_]+)(?P<op>[=]+)(?P<value>.*)$')
|
||||||
|
|
||||||
def __init__(self, *args):
|
def __init__(self, *args):
|
||||||
self._tuples = []
|
self._tuples = []
|
||||||
@@ -61,10 +61,8 @@ class FieldInitial(object):
|
|||||||
if c_field not in parameters:
|
if c_field not in parameters:
|
||||||
logger.error('FieldInitial: Invalid field: \'%s\'', sub_constraint)
|
logger.error('FieldInitial: Invalid field: \'%s\'', sub_constraint)
|
||||||
continue
|
continue
|
||||||
c_value = c.group('value')
|
|
||||||
if c_value is None:
|
|
||||||
c_value = ''
|
|
||||||
c_op = c.group('op')
|
c_op = c.group('op')
|
||||||
|
c_value = c.group('value')
|
||||||
if c_op == '==':
|
if c_op == '==':
|
||||||
if parameters[c_field] == c_value:
|
if parameters[c_field] == c_value:
|
||||||
match = True
|
match = True
|
||||||
@@ -72,7 +70,7 @@ class FieldInitial(object):
|
|||||||
else:
|
else:
|
||||||
match = False
|
match = False
|
||||||
break
|
break
|
||||||
else: # pragma: no cover
|
else:
|
||||||
logger.error('FieldInitial: Invalid operator: \'%s\'', sub_constraint)
|
logger.error('FieldInitial: Invalid operator: \'%s\'', sub_constraint)
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
|
|||||||
+19
-31
@@ -2,6 +2,7 @@ import datetime
|
|||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import pytz
|
import pytz
|
||||||
|
from six import string_types
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -23,17 +24,15 @@ class Iso8601Serializer:
|
|||||||
r')?'
|
r')?'
|
||||||
r'(?P<offset>(?P<offdir>[\-+])(?P<offhours>([01][0-9])|(2[0123]))(:(?P<offmins>[0-5][0-9]))?)?$')
|
r'(?P<offset>(?P<offdir>[\-+])(?P<offhours>([01][0-9])|(2[0123]))(:(?P<offmins>[0-5][0-9]))?)?$')
|
||||||
|
|
||||||
def __init__(self, value):
|
def __init__(self, instance=None, text=None):
|
||||||
if isinstance(value, datetime.datetime) \
|
if instance is not None:
|
||||||
or isinstance(value, datetime.date) \
|
self.instance = instance
|
||||||
or isinstance(value, datetime.time):
|
elif text is not None:
|
||||||
dt_obj = value
|
self.instance = Iso8601Serializer.deserialize(text)
|
||||||
else:
|
|
||||||
dt_obj = Iso8601Serializer.deserialize(value)
|
|
||||||
self._serialized = Iso8601Serializer.serialize(dt_obj)
|
|
||||||
|
|
||||||
def __str__(self):
|
@property
|
||||||
return self._serialized
|
def str(self):
|
||||||
|
return Iso8601Serializer.serialize(self.instance)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def serialize(cls, value, ignore_unsupported_input=False):
|
def serialize(cls, value, ignore_unsupported_input=False):
|
||||||
@@ -56,26 +55,11 @@ class Iso8601Serializer:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def deserialize(cls, value, ignore_unsupported_input=False):
|
def deserialize(cls, value, ignore_unsupported_input=False):
|
||||||
prefix = '{marker}{sep}'.format(marker=cls.marker, sep=cls.separator)
|
prefix = '{marker}{sep}'.format(marker=cls.marker, sep=cls.separator)
|
||||||
|
if isinstance(value, string_types) and value.startswith(prefix):
|
||||||
if not isinstance(value, str):
|
|
||||||
if ignore_unsupported_input:
|
|
||||||
return value
|
|
||||||
raise TypeError('Expected string type, not {}'.format(value.__class__.__name__))
|
|
||||||
|
|
||||||
if not value.startswith(prefix):
|
|
||||||
if ignore_unsupported_input:
|
|
||||||
return value
|
|
||||||
raise ValueError('String must begin with \'{prefix}\''.format(prefix=prefix))
|
|
||||||
|
|
||||||
haystack = value[len(prefix):]
|
haystack = value[len(prefix):]
|
||||||
match = cls._re.match(haystack)
|
m = cls._re.match(haystack)
|
||||||
|
if m is not None:
|
||||||
if match is None:
|
gd = m.groupdict()
|
||||||
if ignore_unsupported_input:
|
|
||||||
return value
|
|
||||||
raise ValueError('Format not recognized \'{str}\''.format(str=haystack))
|
|
||||||
|
|
||||||
gd = match.groupdict()
|
|
||||||
if gd['hour'] is None:
|
if gd['hour'] is None:
|
||||||
value = datetime.date(int(gd['year']), int(gd['mon']), int(gd['day']))
|
value = datetime.date(int(gd['year']), int(gd['mon']), int(gd['day']))
|
||||||
elif gd['year'] is None:
|
elif gd['year'] is None:
|
||||||
@@ -91,8 +75,12 @@ class Iso8601Serializer:
|
|||||||
value -= offset
|
value -= offset
|
||||||
elif gd['offdir'] == '-':
|
elif gd['offdir'] == '-':
|
||||||
value += offset
|
value += offset
|
||||||
else: # pragma no cover
|
else:
|
||||||
raise ValueError('Offset format not recognized \'{str}\''.format(str=gd['offset']))
|
raise ValueError('Offset format not recognized \'{str}\''.format(str=gd['offset']))
|
||||||
value = value.replace(tzinfo=pytz.UTC)
|
value = value.replace(tzinfo=pytz.UTC)
|
||||||
|
elif not ignore_unsupported_input:
|
||||||
|
raise ValueError('Format not recognized \'{str}\''.format(str=haystack))
|
||||||
|
elif not ignore_unsupported_input:
|
||||||
|
raise ValueError('Expected string type,'
|
||||||
|
' not {}'.format(value.__class__.__name__))
|
||||||
return value
|
return value
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
# Generated by Django 5.2.13 on 2026-06-16 13:21
|
|
||||||
|
|
||||||
import django.core.validators
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('dav_events', '0044_alter_event_level'),
|
|
||||||
]
|
|
||||||
|
|
||||||
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'), ('dav-purple', 'dav-purple'), ('dav-lime', 'dav-lime'), ('dav-cyan', 'dav-cyan'), ('dav-caramel', 'dav-caramel'), ('dav-mandarin', 'dav-mandarin'), ('dav-brown', 'dav-brown'), ('orange', 'orange'), ('green', 'green'), ('blue', 'blue'), ('yellow', 'yellow'), ('red', 'red'), ('mandarin', 'mandarin'), ('lime', 'lime'), ('cyan', 'cyan'), ('caramel', 'caramel'), ('purple', 'purple'), ('plum', 'plum'), ('black', 'black'), ('white', 'white')], max_length=20),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='participant',
|
|
||||||
name='dav_number',
|
|
||||||
field=models.CharField(blank=True, max_length=62, validators=[django.core.validators.RegexValidator('^([0-9]{3}/[0-9]{2}/)?[0-9]{1,6}([*x ][0-9]{4})?([*x ][0-9]{4}[*x ][0-9]{4})?([*x ][0-9]{8})?$', 'Ungültiges Format.')], verbose_name='DAV Mitgliedsnummer'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='trashedparticipant',
|
|
||||||
name='dav_number',
|
|
||||||
field=models.CharField(blank=True, max_length=62, validators=[django.core.validators.RegexValidator('^([0-9]{3}/[0-9]{2}/)?[0-9]{1,6}([*x ][0-9]{4})?([*x ][0-9]{4}[*x ][0-9]{4})?([*x ][0-9]{8})?$', 'Ungültiges Format.')], verbose_name='DAV Mitgliedsnummer'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -11,25 +11,13 @@ BOOTSTRAP_CONTEXT_CHOICES = (
|
|||||||
('info', 'info'),
|
('info', 'info'),
|
||||||
('warning', 'warning'),
|
('warning', 'warning'),
|
||||||
('danger', 'danger'),
|
('danger', 'danger'),
|
||||||
|
('black', 'black'),
|
||||||
('dav-purple', 'dav-purple'),
|
('dav-purple', 'dav-purple'),
|
||||||
('dav-lime', 'dav-lime'),
|
('dav-lime', 'dav-lime'),
|
||||||
('dav-cyan', 'dav-cyan'),
|
('dav-cyan', 'dav-cyan'),
|
||||||
('dav-caramel', 'dav-caramel'),
|
('dav-caramel', 'dav-caramel'),
|
||||||
('dav-mandarin', 'dav-mandarin'),
|
('dav-mandarin', 'dav-mandarin'),
|
||||||
('dav-brown', 'dav-brown'),
|
('dav-brown', 'dav-brown'),
|
||||||
('orange', 'orange'),
|
|
||||||
('green', 'green'),
|
|
||||||
('blue', 'blue'),
|
|
||||||
('yellow', 'yellow'),
|
|
||||||
('red', 'red'),
|
|
||||||
('mandarin', 'mandarin'),
|
|
||||||
('lime', 'lime'),
|
|
||||||
('cyan', 'cyan'),
|
|
||||||
('caramel', 'caramel'),
|
|
||||||
('purple', 'purple'),
|
|
||||||
('plum', 'plum'),
|
|
||||||
('black', 'black'),
|
|
||||||
('white', 'white'),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,10 @@ class AbstractParticipant(models.Model):
|
|||||||
privacy_policy_accepted = models.BooleanField(default=False,
|
privacy_policy_accepted = models.BooleanField(default=False,
|
||||||
verbose_name=_('Einwilligung zur Datenspeicherung'))
|
verbose_name=_('Einwilligung zur Datenspeicherung'))
|
||||||
|
|
||||||
|
participation_conditions_accepted = models.BooleanField(default=False,
|
||||||
|
verbose_name=_('Einwilligung der Teilnahmebedingungen'))
|
||||||
|
|
||||||
|
|
||||||
paid = models.BooleanField('Teilnehmerbeitrag bezahlt', default=False)
|
paid = models.BooleanField('Teilnehmerbeitrag bezahlt', default=False)
|
||||||
|
|
||||||
purge_at = models.DateTimeField()
|
purge_at = models.DateTimeField()
|
||||||
|
|||||||
@@ -3,8 +3,6 @@
|
|||||||
{{ normalized_short_date }}
|
{{ normalized_short_date }}
|
||||||
{% if alt_normalized_short_date %}({% trans 'Ersatztermin' %}: {{ alt_normalized_short_date }})
|
{% if alt_normalized_short_date %}({% trans 'Ersatztermin' %}: {{ alt_normalized_short_date }})
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if trainer_email or trainer_phone %}{% if trainer_email %}{{ trainer_email }}{% endif %}{% if trainer_email and trainer_phone %}, {% endif %}{% if trainer_phone %}{{ trainer_phone }}{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
{{ description }}
|
{{ description }}
|
||||||
{% if mode == 'training' %}
|
{% if mode == 'training' %}
|
||||||
{% trans 'Kursinhalte' %}:
|
{% trans 'Kursinhalte' %}:
|
||||||
@@ -46,5 +44,5 @@
|
|||||||
{% endif %}{% if charge > 0 or additional_costs %}{% trans 'Kosten' %}: {% if charge > 0 %}{{ charge|floatformat:'-2' }} € {% trans 'Teilnahmegebühr' %}{% endif %}{% if additional_costs %}{% if charge > 0 %} {% trans 'zzgl.' %} {% endif %}{{ additional_costs }}{% endif %}
|
{% endif %}{% if charge > 0 or additional_costs %}{% trans 'Kosten' %}: {% if charge > 0 %}{{ charge|floatformat:'-2' }} € {% trans 'Teilnahmegebühr' %}{% endif %}{% if additional_costs %}{% if charge > 0 %} {% trans 'zzgl.' %} {% endif %}{{ additional_costs }}{% endif %}
|
||||||
{% endif %}{% if registration_required and deadline %}{% trans 'Anmeldeschluss' %}: {{ deadline|date:'D, j. N Y' }}
|
{% endif %}{% if registration_required and deadline %}{% trans 'Anmeldeschluss' %}: {{ deadline|date:'D, j. N Y' }}
|
||||||
{% endif %}{% if trainer_2_fullname %}{% if mode == 'training' %}{% trans 'Ausbildungsteam' %}:{% else %}{% trans 'Team' %}:{% endif %} {{ trainer_firstname }} {{ trainer_familyname }}, {{ trainer_2_fullname }}{% if trainer_3_fullname %}, {{ trainer_3_fullname }}{% endif %}
|
{% endif %}{% if trainer_2_fullname %}{% if mode == 'training' %}{% trans 'Ausbildungsteam' %}:{% else %}{% trans 'Team' %}:{% endif %} {{ trainer_firstname }} {{ trainer_familyname }}, {{ trainer_2_fullname }}{% if trainer_3_fullname %}, {{ trainer_3_fullname }}{% endif %}
|
||||||
{% endif %}{% if trainer_familyname %}{% trans 'Leitung' %}: {{ trainer_firstname }} {{ trainer_familyname }}
|
{% endif %}{% if trainer_familyname %}{% trans 'Leitung' %}: {{ trainer_firstname }} {{ trainer_familyname }}{% if trainer_email or trainer_phone %} ({% if trainer_email %}{{ trainer_email }}{% endif %}{% if trainer_email and trainer_phone %}, {% endif %}{% if trainer_phone %}{{ trainer_phone }}{% endif %}){% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -183,9 +183,9 @@
|
|||||||
{% if not event.registration_required %}
|
{% if not event.registration_required %}
|
||||||
<span class="label label-success">{% trans 'Anmeldung nicht erforderlich' %}</span>
|
<span class="label label-success">{% trans 'Anmeldung nicht erforderlich' %}</span>
|
||||||
{% elif is_canceled %}
|
{% elif is_canceled %}
|
||||||
<span class="label label-{{ event.workflow.get_status_list.0.bootstrap_context|default:'default' }}">{% trans 'Veranstaltung abgesagt' %}</span>
|
<span class="label label-dav-mandarin">{% trans 'Veranstaltung abgesagt' %}</span>
|
||||||
{% elif is_realized or is_expired %}
|
{% elif is_realized or is_expired %}
|
||||||
<span class="label label-{{ event.workflow.get_status_list.0.bootstrap_context|default:'default' }}">{% trans 'Veranstaltung beendet' %}</span>
|
<span class="label label-dav-lime">{% trans 'Veranstaltung beendet' %}</span>
|
||||||
{% elif event.registration_closed %}
|
{% elif event.registration_closed %}
|
||||||
<span class="label label-danger">{% trans 'Anmeldung geschlossen' %}</span>
|
<span class="label label-danger">{% trans 'Anmeldung geschlossen' %}</span>
|
||||||
{% elif event.is_deadline_expired %}
|
{% elif event.is_deadline_expired %}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
|
||||||
|
|
||||||
from dav_base.tests.generic import AppSetting, AppsTestCase
|
from dav_base.tests.generic import AppSetting, AppsTestCase
|
||||||
|
|
||||||
@@ -8,22 +7,22 @@ class TestCase(AppsTestCase):
|
|||||||
app_config = apps.get_app_config('dav_events')
|
app_config = apps.get_app_config('dav_events')
|
||||||
|
|
||||||
settings = (
|
settings = (
|
||||||
AppSetting('enable_email_on_status_update', False, bool),
|
AppSetting('enable_email_on_status_update', bool),
|
||||||
AppSetting('enable_email_on_update', False, bool),
|
AppSetting('enable_email_on_update', bool),
|
||||||
AppSetting('enable_email_on_registration_closed', False, bool),
|
AppSetting('enable_email_on_registration_closed', bool),
|
||||||
AppSetting('groups_manager_super', [], list),
|
AppSetting('groups_manager_super', list),
|
||||||
AppSetting('groups_manager_w', [], list),
|
AppSetting('groups_manager_w', list),
|
||||||
AppSetting('groups_manager_s', [], list),
|
AppSetting('groups_manager_s', list),
|
||||||
AppSetting('groups_manager_m', [], list),
|
AppSetting('groups_manager_m', list),
|
||||||
AppSetting('groups_manager_k', [], list),
|
AppSetting('groups_manager_k', list),
|
||||||
AppSetting('groups_manager_b', [], list),
|
AppSetting('groups_manager_b', list),
|
||||||
AppSetting('groups_publisher_print', [], list),
|
AppSetting('groups_publisher_print', list),
|
||||||
AppSetting('groups_publisher_web', [], list),
|
AppSetting('groups_publisher_web', list),
|
||||||
AppSetting('groups_publisher_facebook', [], list),
|
AppSetting('groups_publisher_facebook', list),
|
||||||
AppSetting('forms_development_init', False, bool),
|
AppSetting('forms_development_init', bool),
|
||||||
AppSetting('form_initials', {}, dict),
|
AppSetting('form_initials', dict),
|
||||||
AppSetting('matrix_config', ImproperlyConfigured, dict),
|
AppSetting('matrix_config', dict),
|
||||||
AppSetting('publish_before_begin_days', 10, int),
|
AppSetting('publish_before_begin_days', int),
|
||||||
AppSetting('publish_before_deadline_days', 7, int),
|
AppSetting('publish_before_deadline_days', int),
|
||||||
AppSetting('publish_issues', [], list),
|
AppSetting('publish_issues', list),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,191 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from django.test import SimpleTestCase
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
from ..choices import ChoiceSet
|
|
||||||
|
|
||||||
|
|
||||||
class ChoiceSetTestCase(SimpleTestCase):
|
|
||||||
def setUp(self):
|
|
||||||
# We need at least three tuples with code and label.
|
|
||||||
# Codes (i.e. first field of the tuples) must not be in sorted order!
|
|
||||||
self.tuples = [
|
|
||||||
('4', 'A single digit number'),
|
|
||||||
('b', 'A lowercase bee'),
|
|
||||||
('B', 'A capital bee'),
|
|
||||||
('W', _('Wanderung')),
|
|
||||||
('bee', 'A B'),
|
|
||||||
('CO2', 'Carbon dioxide'),
|
|
||||||
('H2o', 'Water with mixed case'),
|
|
||||||
('h2O', 'Water with mixed case'),
|
|
||||||
('H-O-H', 'Water molecule'),
|
|
||||||
('O=C=O', 'Carbon dioxide'),
|
|
||||||
('.dot', 'Label with a dot'),
|
|
||||||
('_underscore', 'Label with underscore'),
|
|
||||||
('miXed.ALL_to-gether-98_76.543210', 'Label with mixed characters'),
|
|
||||||
('üÄö', 'Mixed case Umlaute'),
|
|
||||||
(' ', 'single blank space as code'),
|
|
||||||
('', 'Empty code'),
|
|
||||||
('empty', ''),
|
|
||||||
('A long code without umlaute for a short label', 'ÜäÖ'),
|
|
||||||
('True', 'Yes'),
|
|
||||||
('False', 'No'),
|
|
||||||
('None', 'Unknown'),
|
|
||||||
('NONE', 'Often used'),
|
|
||||||
('OTHER', 'Often used'),
|
|
||||||
]
|
|
||||||
self.choiceset = ChoiceSet(self.tuples)
|
|
||||||
self.code_not_in_choiceset = 'A'
|
|
||||||
|
|
||||||
def test_testdata(self):
|
|
||||||
codes = [c for c, l in self.tuples]
|
|
||||||
self.assertNotIn(self.code_not_in_choiceset, codes, "Improper test data")
|
|
||||||
|
|
||||||
sorted_codes = sorted(codes)
|
|
||||||
self.assertNotEqual(codes, sorted_codes, "Improper test data:"
|
|
||||||
" codes of test tuples must not be in sorted order")
|
|
||||||
|
|
||||||
def test_code_not_unique(self):
|
|
||||||
with self.assertRaises(ValueError) as cm:
|
|
||||||
_ = ChoiceSet([('A', 'First Label'), ('A', 'Second Label')])
|
|
||||||
self.assertEqual(str(cm.exception), 'Code not unique: A')
|
|
||||||
|
|
||||||
def test_len(self):
|
|
||||||
cs = ChoiceSet([])
|
|
||||||
self.assertEqual(len(cs), 0)
|
|
||||||
cs = ChoiceSet([('X', 'Single')])
|
|
||||||
self.assertEqual(len(cs), 1)
|
|
||||||
cs = self.choiceset
|
|
||||||
self.assertEqual(len(cs), len(self.tuples))
|
|
||||||
|
|
||||||
def test_getitem(self):
|
|
||||||
cs = ChoiceSet([])
|
|
||||||
with self.assertRaises(IndexError):
|
|
||||||
_ = cs[0]
|
|
||||||
|
|
||||||
cs = self.choiceset
|
|
||||||
code, label = cs[0]
|
|
||||||
self.assertEqual(code, self.tuples[0][0])
|
|
||||||
self.assertEqual(label, self.tuples[0][1])
|
|
||||||
code, label = cs[1]
|
|
||||||
self.assertEqual(code, self.tuples[1][0])
|
|
||||||
self.assertEqual(label, self.tuples[1][1])
|
|
||||||
code, label = cs[-1]
|
|
||||||
self.assertEqual(code, self.tuples[-1][0])
|
|
||||||
self.assertEqual(label, self.tuples[-1][1])
|
|
||||||
code, label = cs[-2]
|
|
||||||
self.assertEqual(code, self.tuples[-2][0])
|
|
||||||
self.assertEqual(label, self.tuples[-2][1])
|
|
||||||
|
|
||||||
with self.assertRaises(IndexError):
|
|
||||||
_ = cs[len(cs) + 1]
|
|
||||||
with self.assertRaises(IndexError):
|
|
||||||
_ = cs[-len(cs) - 1]
|
|
||||||
|
|
||||||
def test_iter_returns_tuples(self):
|
|
||||||
items = list(self.choiceset)
|
|
||||||
self.assertEqual(items, self.tuples)
|
|
||||||
|
|
||||||
def test_iter_empty_choiceset(self):
|
|
||||||
items = list(ChoiceSet([]))
|
|
||||||
self.assertEqual(items, [])
|
|
||||||
|
|
||||||
def test_iter_with_for_loop(self):
|
|
||||||
expected_codes = [c for c, l in self.tuples]
|
|
||||||
expected_labels = [l for c, l in self.tuples]
|
|
||||||
|
|
||||||
codes = []
|
|
||||||
labels = []
|
|
||||||
for code, label in self.choiceset:
|
|
||||||
codes.append(code)
|
|
||||||
labels.append(label)
|
|
||||||
|
|
||||||
self.assertEqual(codes, expected_codes)
|
|
||||||
self.assertEqual(labels, expected_labels)
|
|
||||||
|
|
||||||
def test_iter_multiple_times(self):
|
|
||||||
first_iteration = list(self.choiceset)
|
|
||||||
second_iteration = list(self.choiceset)
|
|
||||||
self.assertEqual(first_iteration, second_iteration)
|
|
||||||
|
|
||||||
def test_contains(self):
|
|
||||||
for c, l in self.tuples:
|
|
||||||
self.assertIn((c, l), self.choiceset)
|
|
||||||
self.assertIn((c, l[::-1]), self.choiceset)
|
|
||||||
|
|
||||||
self.assertNotIn((self.code_not_in_choiceset, self.tuples[0][1]), self.choiceset)
|
|
||||||
|
|
||||||
def test_contains_empty_choiceset(self):
|
|
||||||
cs = ChoiceSet([])
|
|
||||||
self.assertNotIn(self.tuples[0], cs)
|
|
||||||
|
|
||||||
def test_codes(self):
|
|
||||||
expected_codes = []
|
|
||||||
codes = ChoiceSet([]).codes
|
|
||||||
self.assertEqual(codes, expected_codes)
|
|
||||||
self.assertIsInstance(codes, list)
|
|
||||||
|
|
||||||
expected_codes = [c for c, l in self.tuples]
|
|
||||||
codes = self.choiceset.codes
|
|
||||||
self.assertEqual(codes, expected_codes)
|
|
||||||
self.assertIsInstance(codes, list)
|
|
||||||
|
|
||||||
def test_get_label_valid_code(self):
|
|
||||||
for c, l in self.tuples:
|
|
||||||
self.assertEqual(self.choiceset.get_label(c), l)
|
|
||||||
|
|
||||||
def test_get_label_invalid_code_raises_keyerror(self):
|
|
||||||
with self.assertRaises(KeyError):
|
|
||||||
self.choiceset.get_label(self.code_not_in_choiceset)
|
|
||||||
|
|
||||||
def test_get_label_case_sensitive(self):
|
|
||||||
cs = ChoiceSet([('UPPER', 'Label')])
|
|
||||||
with self.assertRaises(KeyError):
|
|
||||||
cs.get_label('upper')
|
|
||||||
|
|
||||||
def test_sort_numeric_codes(self):
|
|
||||||
cs = ChoiceSet([('3', 'Three'), ('1', 'One'), ('2', 'Two'), ('10', 'Ten')])
|
|
||||||
cs.sort()
|
|
||||||
self.assertEqual(cs.codes, ['1', '10', '2', '3'])
|
|
||||||
|
|
||||||
def test_sort_alphanumeric_codes(self):
|
|
||||||
cs = ChoiceSet([
|
|
||||||
('B1', 'Three'),
|
|
||||||
('A2', 'Two'),
|
|
||||||
('B2', 'Four'),
|
|
||||||
('A1', 'One'),
|
|
||||||
('a20', 'Five'),
|
|
||||||
('a3', 'Six'),
|
|
||||||
('b1', 'Seven'),
|
|
||||||
])
|
|
||||||
cs.sort()
|
|
||||||
self.assertEqual(cs.codes, ['A1', 'A2', 'B1', 'B2', 'a20', 'a3', 'b1'])
|
|
||||||
|
|
||||||
def test_sort_unsortable(self):
|
|
||||||
cs = ChoiceSet([])
|
|
||||||
cs.sort()
|
|
||||||
self.assertEqual(cs.codes, [])
|
|
||||||
|
|
||||||
cs = ChoiceSet([('X', 'Single')])
|
|
||||||
cs.sort()
|
|
||||||
self.assertEqual(cs.codes, ['X'])
|
|
||||||
|
|
||||||
def test_sort_already_sorted(self):
|
|
||||||
cs = ChoiceSet([('1', 'One'), ('2', 'Two'), ('3', 'Three')])
|
|
||||||
cs.sort()
|
|
||||||
self.assertEqual(cs.codes, ['1', '2', '3'])
|
|
||||||
|
|
||||||
def test_sort_preserves_labels(self):
|
|
||||||
cs = ChoiceSet([('B', 'Two'), ('A', 'One'), ('C', 'Three')])
|
|
||||||
cs.sort()
|
|
||||||
|
|
||||||
self.assertEqual(cs.get_label('A'), 'One')
|
|
||||||
self.assertEqual(cs.get_label('B'), 'Two')
|
|
||||||
self.assertEqual(cs.get_label('C'), 'Three')
|
|
||||||
|
|
||||||
def test_sort_test_data(self):
|
|
||||||
expected_codes = [c for c, l in self.tuples]
|
|
||||||
expected_codes.sort()
|
|
||||||
cs = ChoiceSet(self.tuples)
|
|
||||||
cs.sort()
|
|
||||||
self.assertEqual(cs.codes, expected_codes)
|
|
||||||
@@ -1,345 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
import datetime
|
|
||||||
from unittest import TestCase
|
|
||||||
|
|
||||||
from ..config import FieldInitial
|
|
||||||
|
|
||||||
|
|
||||||
class FieldInitialTestCase(TestCase):
|
|
||||||
def test_single_argument(self):
|
|
||||||
value = 'a value'
|
|
||||||
fi = FieldInitial(value)
|
|
||||||
session = {'mode': 'a mode'}
|
|
||||||
output = fi.get_value(session)
|
|
||||||
self.assertEqual(output, value)
|
|
||||||
|
|
||||||
def test_single_argument_empty_session(self):
|
|
||||||
value = 'a value'
|
|
||||||
fi = FieldInitial(value)
|
|
||||||
session = {}
|
|
||||||
output = fi.get_value(session)
|
|
||||||
self.assertEqual(output, value)
|
|
||||||
|
|
||||||
def test_non_string_value(self):
|
|
||||||
values = (42, False, None)
|
|
||||||
for value in values:
|
|
||||||
fi = FieldInitial(value)
|
|
||||||
session = {}
|
|
||||||
output = fi.get_value(session)
|
|
||||||
self.assertEqual(output, value)
|
|
||||||
|
|
||||||
def test_single_simple_constraint_session_match(self):
|
|
||||||
args = (
|
|
||||||
'sport==heli skiing', 'not allowed',
|
|
||||||
)
|
|
||||||
fi = FieldInitial(*args)
|
|
||||||
|
|
||||||
session = {'sport': 'heli skiing'}
|
|
||||||
output = fi.get_value(session)
|
|
||||||
self.assertEqual(output, 'not allowed')
|
|
||||||
|
|
||||||
def test_single_simple_constraint_session_not_match(self):
|
|
||||||
args = (
|
|
||||||
'sport==heli skiing', 'not allowed',
|
|
||||||
)
|
|
||||||
fi = FieldInitial(*args)
|
|
||||||
|
|
||||||
session = {'sport': 'Heli Skiing'}
|
|
||||||
output = fi.get_value(session)
|
|
||||||
self.assertEqual(output, None)
|
|
||||||
|
|
||||||
session = {'sport': 'heli'}
|
|
||||||
output = fi.get_value(session)
|
|
||||||
self.assertEqual(output, None)
|
|
||||||
|
|
||||||
session = {'mode': 'heli skiing', 'sport': 'biking'}
|
|
||||||
output = fi.get_value(session)
|
|
||||||
self.assertEqual(output, None)
|
|
||||||
|
|
||||||
def test_single_simple_constraint_empty_session(self):
|
|
||||||
args = (
|
|
||||||
'sport==heli skiing', 'not allowed',
|
|
||||||
)
|
|
||||||
fi = FieldInitial(*args)
|
|
||||||
|
|
||||||
session = {}
|
|
||||||
output = fi.get_value(session)
|
|
||||||
self.assertEqual(output, None)
|
|
||||||
|
|
||||||
def test_empty_value_in_constraint(self):
|
|
||||||
args = (
|
|
||||||
'sport==', 'lazy',
|
|
||||||
)
|
|
||||||
fi = FieldInitial(*args)
|
|
||||||
|
|
||||||
session = {'sport': ''}
|
|
||||||
output = fi.get_value(session)
|
|
||||||
self.assertEqual(output, 'lazy')
|
|
||||||
|
|
||||||
session = {'sport': 'any'}
|
|
||||||
output = fi.get_value(session)
|
|
||||||
self.assertEqual(output, None)
|
|
||||||
|
|
||||||
def test_invalid_constraints(self):
|
|
||||||
invalid_constraints = (
|
|
||||||
'sport', # Not in <key><operator><value> form
|
|
||||||
'sport ==hiking', # Blank before operator
|
|
||||||
'sport== hiking', # Blank after operator
|
|
||||||
'sport=hiking', # Invalid operator
|
|
||||||
'sport===hiking', # Invalid operator
|
|
||||||
'sport!=hiking', # Invalid operator
|
|
||||||
'==hiking', # Missing key
|
|
||||||
)
|
|
||||||
for constraint in invalid_constraints:
|
|
||||||
fi = FieldInitial(constraint, 'ignored', 'mode==joint', 'recognized')
|
|
||||||
with self.assertLogs('dav_events.config', 'ERROR') as cm:
|
|
||||||
output = fi.get_value({'mode': 'joint'})
|
|
||||||
self.assertEqual(output, 'recognized')
|
|
||||||
self.assertStartsWith(cm.output[0], 'ERROR:dav_events.config:FieldInitial: Invalid constraint: ')
|
|
||||||
|
|
||||||
def test_valid_constraint_keys(self):
|
|
||||||
valid_keys = ('mode', 'sport', 'level', 'overnight', 'country', 'terrain', 'transport')
|
|
||||||
for key in valid_keys:
|
|
||||||
fi = FieldInitial('{}==True'.format(key), 'matched')
|
|
||||||
if key == 'overnight':
|
|
||||||
session = {'last_day': 'True'}
|
|
||||||
else:
|
|
||||||
session = {key: 'True'}
|
|
||||||
with self.assertNoLogs('dav_events.config'):
|
|
||||||
output = fi.get_value(session)
|
|
||||||
self.assertEqual(output, 'matched', key)
|
|
||||||
|
|
||||||
def test_ignore_invalid_constraint_keys(self):
|
|
||||||
invalid_keys = ('ski_lift', 'last_day', 'location')
|
|
||||||
for key in invalid_keys:
|
|
||||||
fi = FieldInitial('{}==True'.format(key), 'ignored', 'mode==joint', 'matched')
|
|
||||||
session = {key: 'True', 'mode': 'joint'}
|
|
||||||
with self.assertLogs('dav_events.config', 'ERROR') as cm:
|
|
||||||
output = fi.get_value(session)
|
|
||||||
self.assertEqual(output, 'matched')
|
|
||||||
self.assertStartsWith(cm.output[0], 'ERROR:dav_events.config:FieldInitial: Invalid field: ')
|
|
||||||
|
|
||||||
def test_multiple_simple_constraints(self):
|
|
||||||
args = (
|
|
||||||
'mode==guided', 'guide',
|
|
||||||
'sport==biking', 'bicycle',
|
|
||||||
)
|
|
||||||
fi = FieldInitial(*args)
|
|
||||||
|
|
||||||
session = {}
|
|
||||||
output = fi.get_value(session)
|
|
||||||
self.assertEqual(output, None)
|
|
||||||
|
|
||||||
session = {'mode': 'joint'}
|
|
||||||
output = fi.get_value(session)
|
|
||||||
self.assertEqual(output, None)
|
|
||||||
|
|
||||||
session = {'sport': 'biking'}
|
|
||||||
output = fi.get_value(session)
|
|
||||||
self.assertEqual(output, 'bicycle')
|
|
||||||
|
|
||||||
session = {'mode': 'joint', 'sport': 'hiking'}
|
|
||||||
output = fi.get_value(session)
|
|
||||||
self.assertEqual(output, None)
|
|
||||||
|
|
||||||
session = {'mode': 'joint', 'sport': 'biking'}
|
|
||||||
output = fi.get_value(session)
|
|
||||||
self.assertEqual(output, 'bicycle')
|
|
||||||
|
|
||||||
session = {'mode': 'guided', 'sport': 'hiking'}
|
|
||||||
output = fi.get_value(session)
|
|
||||||
self.assertEqual(output, 'guide')
|
|
||||||
|
|
||||||
session = {'mode': 'guided', 'sport': 'biking'}
|
|
||||||
output = fi.get_value(session)
|
|
||||||
self.assertEqual(output, 'guide')
|
|
||||||
|
|
||||||
def test_or(self):
|
|
||||||
args = (
|
|
||||||
'sport==hiking', 'shoes',
|
|
||||||
'sport==biking', 'bicycle',
|
|
||||||
)
|
|
||||||
fi = FieldInitial(*args)
|
|
||||||
|
|
||||||
session = {'sport': 'hiking'}
|
|
||||||
output = fi.get_value(session)
|
|
||||||
self.assertEqual(output, 'shoes')
|
|
||||||
|
|
||||||
session = {'sport': 'biking'}
|
|
||||||
output = fi.get_value(session)
|
|
||||||
self.assertEqual(output, 'bicycle')
|
|
||||||
|
|
||||||
session = {'sport': 'swimming'}
|
|
||||||
output = fi.get_value(session)
|
|
||||||
self.assertEqual(output, None)
|
|
||||||
|
|
||||||
def test_and(self):
|
|
||||||
args = []
|
|
||||||
for mode in ('0', '1'):
|
|
||||||
for sport in ('0', '1'):
|
|
||||||
for level in ('0', '1'):
|
|
||||||
constraint_arg = 'mode=={},sport=={},level=={}'.format(mode, sport, level)
|
|
||||||
value = 'mode{}sport{}level{}'.format(mode, sport, level)
|
|
||||||
args.append(constraint_arg)
|
|
||||||
args.append(value)
|
|
||||||
fi = FieldInitial(*args)
|
|
||||||
|
|
||||||
for mode in ('0', '1', ''):
|
|
||||||
for sport in ('0', '1', '*'):
|
|
||||||
for level in ('0', '1', None):
|
|
||||||
session = {'mode': mode, 'sport': sport, 'level': level}
|
|
||||||
if mode in ('0', '1') and sport in ('0', '1') and level in ('0', '1'):
|
|
||||||
expected_value = 'mode{}sport{}level{}'.format(mode, sport, level)
|
|
||||||
else:
|
|
||||||
expected_value = None
|
|
||||||
output = fi.get_value(session)
|
|
||||||
self.assertEqual(output, expected_value)
|
|
||||||
|
|
||||||
session = {'mode': '0', 'sport': '0'}
|
|
||||||
output = fi.get_value(session)
|
|
||||||
self.assertEqual(output, None)
|
|
||||||
|
|
||||||
def test_empty_constraint_only(self):
|
|
||||||
fi = FieldInitial(
|
|
||||||
'', 'default_value'
|
|
||||||
)
|
|
||||||
|
|
||||||
session = {}
|
|
||||||
output = fi.get_value(session)
|
|
||||||
self.assertEqual(output, 'default_value')
|
|
||||||
|
|
||||||
session = {'mode': 'guided'}
|
|
||||||
output = fi.get_value(session)
|
|
||||||
self.assertEqual(output, 'default_value')
|
|
||||||
|
|
||||||
def test_empty_constraint_match_always(self):
|
|
||||||
# XXX: Unwanted behavior
|
|
||||||
|
|
||||||
fi = FieldInitial(
|
|
||||||
'', 'default_value',
|
|
||||||
'mode==guided', 'guide',
|
|
||||||
)
|
|
||||||
|
|
||||||
session = {}
|
|
||||||
output = fi.get_value(session)
|
|
||||||
self.assertEqual(output, 'default_value')
|
|
||||||
|
|
||||||
session = {'mode': 'joint'}
|
|
||||||
output = fi.get_value(session)
|
|
||||||
self.assertEqual(output, 'default_value')
|
|
||||||
|
|
||||||
session = {'mode': 'guided'}
|
|
||||||
output = fi.get_value(session)
|
|
||||||
self.assertEqual(output, 'default_value')
|
|
||||||
|
|
||||||
def test_empty_constraint_as_default(self):
|
|
||||||
fi = FieldInitial(
|
|
||||||
'mode==guided', 'guide',
|
|
||||||
'', 'default_value',
|
|
||||||
)
|
|
||||||
|
|
||||||
session = {}
|
|
||||||
output = fi.get_value(session)
|
|
||||||
self.assertEqual(output, 'default_value')
|
|
||||||
|
|
||||||
session = {'mode': 'joint'}
|
|
||||||
output = fi.get_value(session)
|
|
||||||
self.assertEqual(output, 'default_value')
|
|
||||||
|
|
||||||
session = {'mode': 'guided'}
|
|
||||||
output = fi.get_value(session)
|
|
||||||
self.assertEqual(output, 'guide')
|
|
||||||
|
|
||||||
def test_missing_return_value(self):
|
|
||||||
# XXX: Unwanted behavior
|
|
||||||
|
|
||||||
args = (
|
|
||||||
'sport==hiking', 'shoes',
|
|
||||||
'sport==biking',
|
|
||||||
)
|
|
||||||
fi = FieldInitial(*args)
|
|
||||||
|
|
||||||
session = {'sport': 'hiking'}
|
|
||||||
output = fi.get_value(session)
|
|
||||||
self.assertEqual(output, 'shoes')
|
|
||||||
|
|
||||||
session = {'sport': 'biking'}
|
|
||||||
output = fi.get_value(session)
|
|
||||||
self.assertEqual(output, None)
|
|
||||||
|
|
||||||
def test_overnight_affected_by_last_day(self):
|
|
||||||
fi = FieldInitial(
|
|
||||||
'overnight==True', 'overnight_value',
|
|
||||||
'overnight==False', 'not_overnight_value',
|
|
||||||
)
|
|
||||||
|
|
||||||
session = {'last_day': True}
|
|
||||||
output = fi.get_value(session)
|
|
||||||
self.assertEqual(output, 'overnight_value')
|
|
||||||
|
|
||||||
session = {'last_day': 'Yesterday'}
|
|
||||||
output = fi.get_value(session)
|
|
||||||
self.assertEqual(output, 'overnight_value')
|
|
||||||
|
|
||||||
session = {'last_day': datetime.date(1900, 1, 1)}
|
|
||||||
output = fi.get_value(session)
|
|
||||||
self.assertEqual(output, 'overnight_value')
|
|
||||||
|
|
||||||
# XXX: Unwanted behavior
|
|
||||||
session = {'last_day': 'False'}
|
|
||||||
output = fi.get_value(session)
|
|
||||||
self.assertEqual(output, 'overnight_value')
|
|
||||||
|
|
||||||
session = {'last_day': False}
|
|
||||||
output = fi.get_value(session)
|
|
||||||
self.assertEqual(output, 'not_overnight_value')
|
|
||||||
|
|
||||||
session = {}
|
|
||||||
output = fi.get_value(session)
|
|
||||||
self.assertEqual(output, 'not_overnight_value')
|
|
||||||
|
|
||||||
def test_real_world_example(self):
|
|
||||||
fi = FieldInitial(
|
|
||||||
'sport==hiking,level==beginner', 'hike_for_beginners',
|
|
||||||
'sport==hiking,level==advanced', 'hike_for_pros',
|
|
||||||
'sport==hiking', 'hike_for_any',
|
|
||||||
'sport==climbing,level==beginner', '3-5',
|
|
||||||
'sport==climbing,level==advanced', '5-8',
|
|
||||||
'sport==biking', 'pedal'
|
|
||||||
)
|
|
||||||
|
|
||||||
# Advanced hiking
|
|
||||||
self.assertEqual(
|
|
||||||
fi.get_value({'sport': 'hiking', 'level': 'advanced'}),
|
|
||||||
'hike_for_pros'
|
|
||||||
)
|
|
||||||
|
|
||||||
# Beginner hiking
|
|
||||||
self.assertEqual(
|
|
||||||
fi.get_value({'sport': 'hiking', 'level': 'beginner'}),
|
|
||||||
'hike_for_beginners'
|
|
||||||
)
|
|
||||||
|
|
||||||
# Hiking on unknown level
|
|
||||||
self.assertEqual(
|
|
||||||
fi.get_value({'sport': 'hiking', 'level': 'elite'}),
|
|
||||||
'hike_for_any'
|
|
||||||
)
|
|
||||||
|
|
||||||
# Hiking but level missing
|
|
||||||
self.assertEqual(
|
|
||||||
fi.get_value({'sport': 'hiking'}),
|
|
||||||
'hike_for_any'
|
|
||||||
)
|
|
||||||
|
|
||||||
# Climbing on unknown level
|
|
||||||
self.assertEqual(
|
|
||||||
fi.get_value({'sport': 'climbing', 'level': 'elite'}),
|
|
||||||
None
|
|
||||||
)
|
|
||||||
|
|
||||||
# Biking
|
|
||||||
self.assertEqual(
|
|
||||||
fi.get_value({'sport': 'biking'}),
|
|
||||||
'pedal'
|
|
||||||
)
|
|
||||||
@@ -27,44 +27,7 @@ class Iso8601SerializerTestCase(TestCase):
|
|||||||
obj = datetime.datetime.combine(date, time)
|
obj = datetime.datetime.combine(date, time)
|
||||||
yield (obj, text)
|
yield (obj, text)
|
||||||
|
|
||||||
def test_init_without_arg(self):
|
def test_serialize(self):
|
||||||
with self.assertRaises(TypeError) as cm:
|
|
||||||
_ = Iso8601Serializer()
|
|
||||||
self.assertEqual(str(cm.exception), 'Iso8601Serializer.__init__()'
|
|
||||||
' missing 1 required positional argument: \'value\'')
|
|
||||||
|
|
||||||
def test_init_with_date(self):
|
|
||||||
date_obj = datetime.date(1976, 2, 1)
|
|
||||||
serializer = Iso8601Serializer(date_obj)
|
|
||||||
self.assertEqual(str(serializer), 'ISO8601:1976-02-01')
|
|
||||||
|
|
||||||
def test_init_with_datetime(self):
|
|
||||||
datetime_obj = datetime.datetime(1976, 2, 1, 12, 34, 56)
|
|
||||||
serializer = Iso8601Serializer(datetime_obj)
|
|
||||||
self.assertEqual(str(serializer), 'ISO8601:1976-02-01T12:34:56')
|
|
||||||
|
|
||||||
def test_init_with_time(self):
|
|
||||||
time_obj = datetime.time(12, 34, 56)
|
|
||||||
serializer = Iso8601Serializer(time_obj)
|
|
||||||
self.assertEqual(str(serializer), 'ISO8601:12:34:56')
|
|
||||||
|
|
||||||
def test_init_with_valid_text(self):
|
|
||||||
text = 'ISO8601:1976-02-01'
|
|
||||||
serializer = Iso8601Serializer(text)
|
|
||||||
self.assertEqual(str(serializer), text)
|
|
||||||
|
|
||||||
def test_init_with_invalid_format(self):
|
|
||||||
text = 'ISO8601:02.01.1976'
|
|
||||||
with self.assertRaises(ValueError) as cm:
|
|
||||||
_ = Iso8601Serializer(text)
|
|
||||||
self.assertEqual(str(cm.exception), 'Format not recognized \'02.01.1976\'')
|
|
||||||
|
|
||||||
def test_init_with_invalid_string(self):
|
|
||||||
text = '1976-02-01'
|
|
||||||
with self.assertRaisesRegex(ValueError, 'String must begin with \'ISO8601:\''):
|
|
||||||
_ = Iso8601Serializer(text)
|
|
||||||
|
|
||||||
def test_serialize_valid(self):
|
|
||||||
test_data = []
|
test_data = []
|
||||||
|
|
||||||
# Check date objects
|
# Check date objects
|
||||||
@@ -80,12 +43,10 @@ class Iso8601SerializerTestCase(TestCase):
|
|||||||
serialized = Iso8601Serializer.serialize(obj)
|
serialized = Iso8601Serializer.serialize(obj)
|
||||||
self.assertEqual(serialized, expected)
|
self.assertEqual(serialized, expected)
|
||||||
|
|
||||||
def test_serialize_invalid(self):
|
# Check invalid input
|
||||||
invalid_values = (
|
invalid_values = (
|
||||||
None,
|
None, True, False,
|
||||||
True,
|
'2019-03-01',
|
||||||
False,
|
|
||||||
'ISO8601:1976-02-01',
|
|
||||||
)
|
)
|
||||||
for value in invalid_values:
|
for value in invalid_values:
|
||||||
emsg = ('Expected datetime.datetime, datetime.date or datetime.time,'
|
emsg = ('Expected datetime.datetime, datetime.date or datetime.time,'
|
||||||
@@ -95,7 +56,7 @@ class Iso8601SerializerTestCase(TestCase):
|
|||||||
serialized = Iso8601Serializer.serialize(value, ignore_unsupported_input=True)
|
serialized = Iso8601Serializer.serialize(value, ignore_unsupported_input=True)
|
||||||
self.assertEqual(serialized, value)
|
self.assertEqual(serialized, value)
|
||||||
|
|
||||||
def test_deserialize_valid(self):
|
def test_deserialize(self):
|
||||||
test_data = []
|
test_data = []
|
||||||
|
|
||||||
# Check '<YYYY>-<MM>-<DD>' format
|
# Check '<YYYY>-<MM>-<DD>' format
|
||||||
@@ -110,43 +71,3 @@ class Iso8601SerializerTestCase(TestCase):
|
|||||||
deserialized = Iso8601Serializer.deserialize(text)
|
deserialized = Iso8601Serializer.deserialize(text)
|
||||||
self.assertIsInstance(deserialized, obj.__class__)
|
self.assertIsInstance(deserialized, obj.__class__)
|
||||||
self.assertEqual(deserialized, obj)
|
self.assertEqual(deserialized, obj)
|
||||||
|
|
||||||
def test_deserialize_with_valid_offset(self):
|
|
||||||
test_data = (
|
|
||||||
('1976-02-01T12:34:56+02', '1976-02-01 10:34:56+00:00'),
|
|
||||||
('1976-02-01T12:34:56+10:04', '1976-02-01 02:30:56+00:00'),
|
|
||||||
('1976-02-01T12:34:56-02', '1976-02-01 14:34:56+00:00'),
|
|
||||||
('1976-02-01T12:34:56-12:14', '1976-02-02 00:48:56+00:00'),
|
|
||||||
)
|
|
||||||
for input_text, dt_str in test_data:
|
|
||||||
text = 'ISO8601:{}'.format(input_text)
|
|
||||||
deserialized = Iso8601Serializer.deserialize(text)
|
|
||||||
self.assertEqual(str(deserialized), dt_str)
|
|
||||||
|
|
||||||
def test_deserialize_with_invalid_offset(self):
|
|
||||||
invalid_data = (
|
|
||||||
'1976-02-01T12:34:56+2',
|
|
||||||
'1976-02-01T12:34:56+02:02:02',
|
|
||||||
'1976-02-01T12:34:56+-02',
|
|
||||||
'1976-02-01T12:34:56=02',
|
|
||||||
'1976-02-01T12:34:56+CEST',
|
|
||||||
)
|
|
||||||
for input_text in invalid_data:
|
|
||||||
text = 'ISO8601:{}'.format(input_text)
|
|
||||||
with self.assertRaisesRegex(ValueError, 'Format not recognized \'.*\''):
|
|
||||||
Iso8601Serializer.deserialize(text)
|
|
||||||
|
|
||||||
def test_deserialize_invalid(self):
|
|
||||||
invalid_data = (
|
|
||||||
(None, TypeError, 'Expected string type, not NoneType'),
|
|
||||||
(True, TypeError, 'Expected string type, not bool'),
|
|
||||||
(False, TypeError, 'Expected string type, not bool'),
|
|
||||||
('1976-02-01', ValueError, 'String must begin with \'ISO8601:\''),
|
|
||||||
('ISO8601:02.01.1976', ValueError, 'Format not recognized \'02.01.1976\''),
|
|
||||||
)
|
|
||||||
for value, expected_exception, expected_msg in invalid_data:
|
|
||||||
with self.assertRaisesRegex(expected_exception, expected_msg):
|
|
||||||
Iso8601Serializer.deserialize(value)
|
|
||||||
serialized = Iso8601Serializer.deserialize(value, ignore_unsupported_input=True)
|
|
||||||
self.assertEqual(serialized, value)
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,13 @@
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.urls import reverse
|
|
||||||
|
|
||||||
from dav_base.tests.generic import Url, UrlsTestCase
|
from dav_base.tests.generic import Url, UrlsTestCase
|
||||||
from .generic import EventMixin
|
|
||||||
|
|
||||||
from .. import views
|
from .. import views
|
||||||
|
|
||||||
url_prefix = settings.MODULE_CONFIG.modules['dav_events'].url_prefix
|
url_prefix = settings.MODULE_CONFIG.modules['dav_events'].url_prefix
|
||||||
|
|
||||||
|
|
||||||
class TestCase(EventMixin, UrlsTestCase):
|
class TestCase(UrlsTestCase):
|
||||||
urls = (
|
urls = (
|
||||||
Url('/{}/home'.format(url_prefix), 'dav_events:root', views.base.HomeView.as_view()),
|
Url('/{}/home'.format(url_prefix), 'dav_events:root', views.base.HomeView.as_view()),
|
||||||
Url('/{}/'.format(url_prefix), 'dav_events:list', views.events.EventListView.as_view(),
|
Url('/{}/'.format(url_prefix), 'dav_events:list', views.events.EventListView.as_view(),
|
||||||
@@ -19,38 +16,3 @@ class TestCase(EventMixin, UrlsTestCase):
|
|||||||
redirect='/auth/login?next=/{}/export'.format(url_prefix)),
|
redirect='/auth/login?next=/{}/export'.format(url_prefix)),
|
||||||
Url('/{}/create'.format(url_prefix), 'dav_events:create', views.events.EventCreateView.as_view()),
|
Url('/{}/create'.format(url_prefix), 'dav_events:create', views.events.EventCreateView.as_view()),
|
||||||
)
|
)
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.event = self.create_event_by_model()
|
|
||||||
model = get_user_model()
|
|
||||||
self.username = 'root'
|
|
||||||
self.password = 'mellon'
|
|
||||||
self.user = model.objects.create_superuser(username=self.username, password=self.password,
|
|
||||||
email='root@localhost')
|
|
||||||
|
|
||||||
def test_registrations(self):
|
|
||||||
pk = self.event.pk
|
|
||||||
expected_location = '/{}/{}/registrations'.format(url_prefix, pk)
|
|
||||||
location = reverse('dav_events:registrations', kwargs={'pk': pk})
|
|
||||||
self.assertEqual(location, expected_location)
|
|
||||||
self.client.login(username=self.username, password=self.password)
|
|
||||||
response = self.client.get(location)
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
def test_update(self):
|
|
||||||
pk = self.event.pk
|
|
||||||
expected_location = '/{}/{}/edit'.format(url_prefix, pk)
|
|
||||||
location = reverse('dav_events:update', kwargs={'pk': pk})
|
|
||||||
self.assertEqual(location, expected_location)
|
|
||||||
self.client.login(username=self.username, password=self.password)
|
|
||||||
response = self.client.get(location)
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
def test_detail(self):
|
|
||||||
pk = self.event.pk
|
|
||||||
expected_location = '/{}/{}/'.format(url_prefix, pk)
|
|
||||||
location = reverse('dav_events:detail', kwargs={'pk': pk})
|
|
||||||
self.assertEqual(location, expected_location)
|
|
||||||
self.client.login(username=self.username, password=self.password)
|
|
||||||
response = self.client.get(location)
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|||||||
@@ -1,109 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
import datetime
|
|
||||||
from django.test import TestCase
|
|
||||||
from django.utils import timezone
|
|
||||||
|
|
||||||
from ..models import Participant, TrashedParticipant
|
|
||||||
from .. import utils
|
|
||||||
|
|
||||||
from .generic import EventMixin
|
|
||||||
|
|
||||||
|
|
||||||
class PurgeParticipantsTestCase(EventMixin, TestCase):
|
|
||||||
def test_purging_participants_only_with_elapsed_purge_date(self):
|
|
||||||
event = self.create_event_by_model()
|
|
||||||
|
|
||||||
test_dates = [
|
|
||||||
timezone.now(),
|
|
||||||
timezone.now() - datetime.timedelta(days=1),
|
|
||||||
timezone.now() + datetime.timedelta(days=1),
|
|
||||||
timezone.now() - datetime.timedelta(days=720),
|
|
||||||
timezone.now() + datetime.timedelta(days=720),
|
|
||||||
]
|
|
||||||
|
|
||||||
position = 0
|
|
||||||
for date in test_dates:
|
|
||||||
position += 1
|
|
||||||
participant = Participant(
|
|
||||||
event=event,
|
|
||||||
personal_names='Walter',
|
|
||||||
family_names='Bonatti',
|
|
||||||
address='Street 1',
|
|
||||||
postal_code='23032',
|
|
||||||
city='Bormio',
|
|
||||||
email_address='walter@farbemachtstark.de',
|
|
||||||
phone_number='555 1',
|
|
||||||
year_of_birth=1930,
|
|
||||||
dav_number='1',
|
|
||||||
position=position,
|
|
||||||
purge_at=date,
|
|
||||||
)
|
|
||||||
participant.save()
|
|
||||||
|
|
||||||
self.assertEqual(Participant.objects.count(), len(test_dates))
|
|
||||||
|
|
||||||
with self.assertLogs('dav_events.utils', level='INFO') as cm:
|
|
||||||
utils.purge_participants()
|
|
||||||
|
|
||||||
position = 0
|
|
||||||
expected_messages = 0
|
|
||||||
for date in test_dates:
|
|
||||||
position += 1
|
|
||||||
expected_purge = date <= timezone.now()
|
|
||||||
if expected_purge:
|
|
||||||
expected_messages += 1
|
|
||||||
self.assertEqual(expected_purge, not Participant.objects.filter(position=position).exists())
|
|
||||||
|
|
||||||
self.assertEqual(len(cm.output), expected_messages)
|
|
||||||
for i in range(expected_messages):
|
|
||||||
self.assertStartsWith(cm.output[i], 'INFO:dav_events.utils:Purge participant \'')
|
|
||||||
|
|
||||||
def test_purging_trashed_participants_only_with_elapsed_purge_date(self):
|
|
||||||
event = self.create_event_by_model()
|
|
||||||
|
|
||||||
test_dates = [
|
|
||||||
timezone.now(),
|
|
||||||
timezone.now() - datetime.timedelta(days=1),
|
|
||||||
timezone.now() + datetime.timedelta(days=1),
|
|
||||||
timezone.now() - datetime.timedelta(days=720),
|
|
||||||
timezone.now() + datetime.timedelta(days=720),
|
|
||||||
]
|
|
||||||
|
|
||||||
position = 0
|
|
||||||
for date in test_dates:
|
|
||||||
position += 1
|
|
||||||
participant = TrashedParticipant(
|
|
||||||
event=event,
|
|
||||||
personal_names='Walter',
|
|
||||||
family_names='Bonatti',
|
|
||||||
address='Street 1',
|
|
||||||
postal_code='23032',
|
|
||||||
city='Bormio',
|
|
||||||
email_address='walter@farbemachtstark.de',
|
|
||||||
phone_number='555 1',
|
|
||||||
year_of_birth=1930,
|
|
||||||
dav_number='1',
|
|
||||||
position=position,
|
|
||||||
created_at=timezone.now(),
|
|
||||||
purge_at=date,
|
|
||||||
)
|
|
||||||
participant.save()
|
|
||||||
|
|
||||||
self.assertEqual(TrashedParticipant.objects.count(), len(test_dates))
|
|
||||||
|
|
||||||
with self.assertLogs('dav_events.utils', level='INFO') as cm:
|
|
||||||
utils.purge_participants()
|
|
||||||
|
|
||||||
position = 0
|
|
||||||
expected_messages = 0
|
|
||||||
for date in test_dates:
|
|
||||||
position += 1
|
|
||||||
expected_purge = date <= timezone.now()
|
|
||||||
if expected_purge:
|
|
||||||
expected_messages += 1
|
|
||||||
self.assertEqual(expected_purge, not TrashedParticipant.objects.filter(position=position).exists())
|
|
||||||
|
|
||||||
self.assertEqual(len(cm.output), expected_messages)
|
|
||||||
for i in range(expected_messages):
|
|
||||||
self.assertStartsWith(cm.output[i], 'INFO:dav_events.utils:Purge participant from trash \'')
|
|
||||||
@@ -1,274 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from django.test import SimpleTestCase
|
|
||||||
|
|
||||||
from dav_base.tests.generic import ValidatorTestMixin
|
|
||||||
|
|
||||||
from ..validators import AlphanumericValidator, LowerAlphanumericValidator, IdStringValidator
|
|
||||||
|
|
||||||
|
|
||||||
class AlphanumericValidatorTestCase(ValidatorTestMixin, SimpleTestCase):
|
|
||||||
validator = AlphanumericValidator
|
|
||||||
|
|
||||||
def test_valid_data(self):
|
|
||||||
data = (
|
|
||||||
'', # Empty string is valid
|
|
||||||
'A', # Single uppercase letter
|
|
||||||
'a', # Single lowercase letter
|
|
||||||
'0', # Single digit
|
|
||||||
'abc', # Multiple lowercase letters
|
|
||||||
'ABCD', # Multiple uppercase letters
|
|
||||||
'AbCde', # Mixed case
|
|
||||||
'123456', # Multiple digits
|
|
||||||
'bcd2345', # Letters and digits
|
|
||||||
'CDEF3456', # Uppercase and digits
|
|
||||||
'AbcD123', # Mixed case and digits
|
|
||||||
'a1B2c3', # Alternating case and digits
|
|
||||||
'012345678909876543210', # Long digit sequence
|
|
||||||
'abcdefghijklmnopqrstuvwxyz', # All lowercase letters
|
|
||||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZ', # All uppercase letters
|
|
||||||
'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0123456789', # All valid chars
|
|
||||||
'EventCode2024', # Real world example
|
|
||||||
'USER123EVENT', # Real world example
|
|
||||||
'A1', # Single letter and digit
|
|
||||||
'1A', # Single digit and letter
|
|
||||||
)
|
|
||||||
self.assertValid(self.validator, data)
|
|
||||||
|
|
||||||
def test_invalid_data(self):
|
|
||||||
"""Test invalid strings with special characters, spaces, umlauts"""
|
|
||||||
data = (
|
|
||||||
' ', # Space
|
|
||||||
'abc ', # Trailing space
|
|
||||||
' abc', # Leading space
|
|
||||||
'a b c', # Space in the middle
|
|
||||||
'-', # Hyphen
|
|
||||||
'abc-def', # Letters with hyphen
|
|
||||||
'_', # Underscore
|
|
||||||
'abc_def', # Letters with underscore
|
|
||||||
'.', # Dot
|
|
||||||
'abc.def', # Letters with dot
|
|
||||||
',', # Comma
|
|
||||||
'abc,def', # Letters with comma
|
|
||||||
'!', # Exclamation mark
|
|
||||||
'abc!def', # Letters with exclamation
|
|
||||||
'@', # At sign
|
|
||||||
'abc@def', # Letters with at sign
|
|
||||||
'#', # Hash
|
|
||||||
'abc#def', # Letters with hash
|
|
||||||
'$', # Dollar sign
|
|
||||||
'abc$def', # Letters with dollar
|
|
||||||
'%', # Percent
|
|
||||||
'abc%def', # Letters with percent
|
|
||||||
'&', # Ampersand
|
|
||||||
'abc&def', # Letters with ampersand
|
|
||||||
'*', # Asterisk
|
|
||||||
'abc*def', # Letters with asterisk
|
|
||||||
'+', # Plus
|
|
||||||
'abc+def', # Letters with plus
|
|
||||||
'=', # Equals
|
|
||||||
'abc=def', # Letters with equals
|
|
||||||
'/', # Slash
|
|
||||||
'abc/def', # Letters with slash
|
|
||||||
'\\', # Backslash
|
|
||||||
'abc\\def', # Letters with backslash
|
|
||||||
'?', # Question mark
|
|
||||||
'abc?def', # Letters with question mark
|
|
||||||
':', # Colon
|
|
||||||
'abc:def', # Letters with colon
|
|
||||||
';', # Semicolon
|
|
||||||
'abc;def', # Letters with semicolon
|
|
||||||
'"', # Double quote
|
|
||||||
'abc"def', # Letters with double quote
|
|
||||||
"'", # Single quote
|
|
||||||
"abc'def", # Letters with single quote
|
|
||||||
'<', # Less than
|
|
||||||
'abc<def', # Letters with less than
|
|
||||||
'>', # Greater than
|
|
||||||
'abc>def', # Letters with greater than
|
|
||||||
'(', # Opening parenthesis
|
|
||||||
'abc(def)', # Letters with parentheses
|
|
||||||
'[', # Opening bracket
|
|
||||||
'abc[def]', # Letters with brackets
|
|
||||||
'{', # Opening brace
|
|
||||||
'abc{def}', # Letters with braces
|
|
||||||
'ä', # Umlaut
|
|
||||||
'äbc', # Letters with umlaut
|
|
||||||
'ö', # Umlaut o
|
|
||||||
'ö', # Umlaut O
|
|
||||||
'ü', # Umlaut u
|
|
||||||
'ü', # Umlaut U
|
|
||||||
'ß', # German sharp s
|
|
||||||
'Straße', # German word with umlaut
|
|
||||||
'café', # Accented character
|
|
||||||
'naïve', # Accented character
|
|
||||||
'中文', # Chinese characters
|
|
||||||
'русский', # Cyrillic characters
|
|
||||||
'العربية', # Arabic characters
|
|
||||||
)
|
|
||||||
self.assertInvalid(self.validator, data)
|
|
||||||
|
|
||||||
|
|
||||||
class LowerAlphanumericValidatorTestCase(ValidatorTestMixin, SimpleTestCase):
|
|
||||||
validator = LowerAlphanumericValidator
|
|
||||||
|
|
||||||
def test_valid_data(self):
|
|
||||||
data = (
|
|
||||||
'', # Empty string is valid
|
|
||||||
'a', # Single lowercase letter
|
|
||||||
'0', # Single digit
|
|
||||||
'abc', # Multiple lowercase letters
|
|
||||||
'123456', # Multiple digits
|
|
||||||
'bcd2345', # Letters and digits
|
|
||||||
'a1b2c3', # Alternating lowercase and digits
|
|
||||||
'012345678909876543210', # Long digit sequence
|
|
||||||
'abcdefghijklmnopqrstuvwxyz', # All lowercase letters
|
|
||||||
'eventcode2024', # Real world example (all lowercase)
|
|
||||||
'user123event', # Real world example (all lowercase)
|
|
||||||
'a1', # Single letter and digit
|
|
||||||
'1a', # Single digit and letter
|
|
||||||
)
|
|
||||||
self.assertValid(self.validator, data)
|
|
||||||
|
|
||||||
def test_invalid_data(self):
|
|
||||||
"""Test invalid strings with uppercase letters and other characters"""
|
|
||||||
data = (
|
|
||||||
'A', # Single uppercase letter
|
|
||||||
'ABC', # Multiple uppercase letters
|
|
||||||
'AbC', # Mixed case
|
|
||||||
'ABC123', # Uppercase and digits
|
|
||||||
'AbC123', # Mixed case (with uppercase)
|
|
||||||
' ', # Space
|
|
||||||
'abc ', # Trailing space
|
|
||||||
' abc', # Leading space
|
|
||||||
'a b c', # Space in the middle
|
|
||||||
'-', # Hyphen
|
|
||||||
'abc-def', # Letters with hyphen
|
|
||||||
'_', # Underscore
|
|
||||||
'abc_def', # Letters with underscore
|
|
||||||
'.', # Dot
|
|
||||||
'abc.def', # Letters with dot
|
|
||||||
',', # Comma
|
|
||||||
'abc,def', # Letters with comma
|
|
||||||
'!', # Exclamation mark
|
|
||||||
'abc!def', # Letters with exclamation
|
|
||||||
'@', # At sign
|
|
||||||
'abc@def', # Letters with at sign
|
|
||||||
'#', # Hash
|
|
||||||
'abc#def', # Letters with hash
|
|
||||||
'ä', # Umlaut
|
|
||||||
'äbc', # Letters with umlaut
|
|
||||||
'café', # Accented character
|
|
||||||
'naïve', # Accented character
|
|
||||||
)
|
|
||||||
self.assertInvalid(self.validator, data)
|
|
||||||
|
|
||||||
|
|
||||||
class IdStringValidatorTestCase(ValidatorTestMixin, SimpleTestCase):
|
|
||||||
validator = IdStringValidator
|
|
||||||
|
|
||||||
def test_valid_data(self):
|
|
||||||
data = (
|
|
||||||
'', # Empty string is valid
|
|
||||||
'a', # Single lowercase letter
|
|
||||||
'0', # Single digit
|
|
||||||
'abc', # Multiple lowercase letters
|
|
||||||
'123', # Multiple digits
|
|
||||||
'_', # Single underscore
|
|
||||||
'-', # Single hyphen
|
|
||||||
'.', # Single dot
|
|
||||||
'abc123', # Lowercase letters and digits
|
|
||||||
'a_b', # Underscore separator
|
|
||||||
'a-b', # Hyphen separator
|
|
||||||
'a.b', # Dot separator
|
|
||||||
'user_name', # Underscore in middle
|
|
||||||
'user-name', # Hyphen in middle
|
|
||||||
'user.name', # Dot in middle
|
|
||||||
'user_name_123', # Multiple underscores
|
|
||||||
'user-name-123', # Multiple hyphens
|
|
||||||
'user.name.123', # Multiple dots
|
|
||||||
'mixed_name-with.numbers123', # Mixed separators
|
|
||||||
'example.com', # Domain-like format
|
|
||||||
'v1.0.0', # Version string
|
|
||||||
'2024-01-15', # Date-like format
|
|
||||||
'snake_case', # Snake case convention
|
|
||||||
'kebab-case', # Kebab case convention
|
|
||||||
'dot.case', # Dot case convention
|
|
||||||
'a1b2c3d4e5f6g7h8i9j0', # Alternating
|
|
||||||
'test_123-abc.xyz', # Complex mixed format
|
|
||||||
'_leading_underscore', # Leading underscore
|
|
||||||
'-leading-hyphen', # Leading hyphen
|
|
||||||
'.leading-dot', # Leading dot
|
|
||||||
'trailing_underscore_', # Trailing underscore
|
|
||||||
'trailing-hyphen-', # Trailing hyphen
|
|
||||||
'trailing.dot.', # Trailing dot
|
|
||||||
'___', # Multiple underscores only
|
|
||||||
'---', # Multiple hyphens only
|
|
||||||
'...', # Multiple dots only
|
|
||||||
'a_b-c.d_e-f.g', # All separators mixed
|
|
||||||
)
|
|
||||||
self.assertValid(self.validator, data)
|
|
||||||
|
|
||||||
def test_invalid_data(self):
|
|
||||||
data = (
|
|
||||||
'A', # Uppercase letter
|
|
||||||
'ABC', # Multiple uppercase letters
|
|
||||||
'AbC', # Mixed case
|
|
||||||
'ABC123', # Uppercase and digits
|
|
||||||
'aBC123', # Mixed case (any uppercase)
|
|
||||||
' ', # Space
|
|
||||||
'abc ', # Trailing space
|
|
||||||
' abc', # Leading space
|
|
||||||
'a b c', # Space in the middle
|
|
||||||
'a b-c', # Space mixed with valid chars
|
|
||||||
'@', # At sign
|
|
||||||
'abc@def', # At sign in the middle
|
|
||||||
'#', # Hash
|
|
||||||
'abc#def', # Hash in the middle
|
|
||||||
'$', # Dollar sign
|
|
||||||
'abc$def', # Dollar in the middle
|
|
||||||
'%', # Percent
|
|
||||||
'abc%def', # Percent in the middle
|
|
||||||
'&', # Ampersand
|
|
||||||
'abc&def', # Ampersand in the middle
|
|
||||||
'*', # Asterisk
|
|
||||||
'abc*def', # Asterisk in the middle
|
|
||||||
'+', # Plus
|
|
||||||
'abc+def', # Plus in the middle
|
|
||||||
'=', # Equals
|
|
||||||
'abc=def', # Equals in the middle
|
|
||||||
'/', # Slash
|
|
||||||
'abc/def', # Slash in the middle
|
|
||||||
'\\', # Backslash
|
|
||||||
'abc\\def', # Backslash in the middle
|
|
||||||
'?', # Question mark
|
|
||||||
'abc?def', # Question mark in the middle
|
|
||||||
':', # Colon
|
|
||||||
'abc:def', # Colon in the middle
|
|
||||||
';', # Semicolon
|
|
||||||
'abc;def', # Semicolon in the middle
|
|
||||||
'"', # Double quote
|
|
||||||
'abc"def', # Double quote in the middle
|
|
||||||
"'", # Single quote
|
|
||||||
"abc'def", # Single quote in the middle
|
|
||||||
'<', # Less than
|
|
||||||
'abc<def', # Less than in the middle
|
|
||||||
'>', # Greater than
|
|
||||||
'abc>def', # Greater than in the middle
|
|
||||||
'(', # Opening parenthesis
|
|
||||||
'abc(def)', # Parentheses in the middle
|
|
||||||
'[', # Opening bracket
|
|
||||||
'abc[def]', # Brackets in the middle
|
|
||||||
'{', # Opening brace
|
|
||||||
'abc{def}', # Braces in the middle
|
|
||||||
'!', # Exclamation mark
|
|
||||||
'abc!def', # Exclamation mark in the middle
|
|
||||||
',', # Comma
|
|
||||||
'abc,def', # Comma in the middle
|
|
||||||
'ä', # Umlaut
|
|
||||||
'äbc', # Letters with umlaut
|
|
||||||
'café', # Accented character
|
|
||||||
'naïve', # Accented character
|
|
||||||
'中文', # Chinese characters
|
|
||||||
'русский', # Cyrillic characters
|
|
||||||
)
|
|
||||||
self.assertInvalid(self.validator, data)
|
|
||||||
+11
-11
@@ -21,18 +21,18 @@ oneday = datetime.timedelta(1)
|
|||||||
|
|
||||||
DEFAULT_EVENT_STATI = {
|
DEFAULT_EVENT_STATI = {
|
||||||
'void': (0, _(u'Ungültig'), None),
|
'void': (0, _(u'Ungültig'), None),
|
||||||
'draft': (10, _(u'Entwurf'), 'blue'),
|
'draft': (10, _(u'Entwurf'), 'info'),
|
||||||
'submitted': (30, _(u'Eingereicht'), 'red'),
|
'submitted': (30, _(u'Eingereicht'), 'danger'),
|
||||||
'accepted': (50, _(u'Freigegeben'), 'yellow'),
|
'accepted': (50, _(u'Freigegeben'), 'warning'),
|
||||||
'publishing_facebook': (68, _(u'Veröffentlichung (Facebook)'), 'yellow'),
|
'publishing_facebook': (68, _(u'Veröffentlichung (Facebook)'), 'warning'),
|
||||||
'publishing_web': (69, _(u'Veröffentlichung (Web)'), 'yellow'),
|
'publishing_web': (69, _(u'Veröffentlichung (Web)'), 'warning'),
|
||||||
'publishing': (70, _(u'Veröffentlichung'), 'yellow'),
|
'publishing': (70, _(u'Veröffentlichung'), 'warning'),
|
||||||
'published_facebook': (78, _(u'Veröffentlicht (Facebook)'), 'green'),
|
'published_facebook': (78, _(u'Veröffentlicht (Facebook)'), 'success'),
|
||||||
'published_web': (79, _(u'Veröffentlicht (Web)'), 'green'),
|
'published_web': (79, _(u'Veröffentlicht (Web)'), 'success'),
|
||||||
'published': (80, _(u'Veröffentlicht'), 'green'),
|
'published': (80, _(u'Veröffentlicht'), 'success'),
|
||||||
'expired': (100, _(u'Ausgelaufen'), None),
|
'expired': (100, _(u'Ausgelaufen'), None),
|
||||||
'canceled': (101, _(u'Abgesagt'), 'mandarin'),
|
'canceled': (101, _(u'Abgesagt'), 'dav-mandarin'),
|
||||||
'realized': (102, _(u'Durchgeführt'), 'lime'),
|
'realized': (102, _(u'Durchgeführt'), 'dav-lime'),
|
||||||
'cleared': (110, _(u'Abgerechnet'), 'black'),
|
'cleared': (110, _(u'Abgerechnet'), 'black'),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ class RegistrationForm(forms.ModelForm):
|
|||||||
' Das finden wir gut,'
|
' Das finden wir gut,'
|
||||||
' aber bitte melde dich besser mal per E-Mail bei uns.'),
|
' aber bitte melde dich besser mal per E-Mail bei uns.'),
|
||||||
params={'max_age': max_age},
|
params={'max_age': max_age},
|
||||||
code='to_old',
|
code='too_old',
|
||||||
)
|
)
|
||||||
return val
|
return val
|
||||||
|
|
||||||
@@ -79,6 +79,16 @@ class RegistrationForm(forms.ModelForm):
|
|||||||
)
|
)
|
||||||
return val
|
return val
|
||||||
|
|
||||||
|
def clean_participation_conditions_accepted(self):
|
||||||
|
val = self.cleaned_data.get('participation_conditions_accepted')
|
||||||
|
if not val:
|
||||||
|
raise forms.ValidationError(
|
||||||
|
ugettext('Um an dieser Veranstaltung teilnehmen zu können,'
|
||||||
|
' musst du die Teilnahmebedingungen akzeptieren.'),
|
||||||
|
code='participation_conditions_not_accepted',
|
||||||
|
)
|
||||||
|
return val
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
dav_member = self.cleaned_data.get('dav_member')
|
dav_member = self.cleaned_data.get('dav_member')
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
# Generated by Django 5.2.13 on 2026-06-16 13:21
|
|
||||||
|
|
||||||
import django.core.validators
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('dav_registration', '0012_alter_registrationstatus_accepted'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
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]{3}/[0-9]{2}/)?[0-9]{1,6}([*x ][0-9]{4})?([*x ][0-9]{4}[*x ][0-9]{4})?([*x ][0-9]{8})?$', 'Ungültiges Format.')], verbose_name='DAV Mitgliedsnummer'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -84,6 +84,8 @@ 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'))
|
||||||
|
|
||||||
|
participation_conditions_accepted = models.BooleanField(default=False, verbose_name=_('Einwilligung der Teilnahmebedingungen'))
|
||||||
|
|
||||||
answered_obsolete = models.BooleanField(default=False, verbose_name=_('Durch Tourleitung beantwortet'))
|
answered_obsolete = models.BooleanField(default=False, verbose_name=_('Durch Tourleitung beantwortet'))
|
||||||
|
|
||||||
def approx_age(self):
|
def approx_age(self):
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
<button id="btn-filter-All" type="button" class="btn btn-xs btn-green">Alle Touren</button>
|
<button id="btn-filter-All" type="button" class="btn btn-xs btn-green">Alle Touren</button>
|
||||||
<button id="btn-filter-B" type="button" class="btn btn-xs btn-sport-B btn-white">Bergsteigen</button>
|
<button id="btn-filter-B" type="button" class="btn btn-xs btn-sport-B btn-white">Bergsteigen</button>
|
||||||
<button id="btn-filter-family" type="button" class="btn btn-xs btn-level-family btn-white">Familien</button>
|
<button id="btn-filter-family" type="button" class="btn btn-xs btn-level-family btn-white">Familien</button>
|
||||||
|
<button id="btn-filter-senior" type="button" class="btn btn-xs btn-level-senior btn-white">Senioren</button>
|
||||||
<button id="btn-filter-K" type="button" class="btn btn-xs btn-sport-K btn-white">Klettern</button>
|
<button id="btn-filter-K" type="button" class="btn btn-xs btn-sport-K btn-white">Klettern</button>
|
||||||
<button id="btn-filter-M" type="button" class="btn btn-xs btn-sport-M btn-white">Mountainbike</button>
|
<button id="btn-filter-M" type="button" class="btn btn-xs btn-sport-M btn-white">Mountainbike</button>
|
||||||
<button id="btn-filter-S" type="button" class="btn btn-xs btn-sport-S btn-white">Ski</button>
|
<button id="btn-filter-S" type="button" class="btn btn-xs btn-sport-S btn-white">Ski</button>
|
||||||
@@ -127,7 +128,7 @@
|
|||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
function filter_table(table, filter) {
|
function filter_table(table, filter) {
|
||||||
const sport_choices = ["B", "K", "M", "S", "W"];
|
const sport_choices = ["B", "K", "M", "S", "W"];
|
||||||
const level_choices = ["beginner", "advanced", "family"];
|
const level_choices = ["beginner", "advanced", "family", "senior"];
|
||||||
var choices = sport_choices.concat(level_choices);
|
var choices = sport_choices.concat(level_choices);
|
||||||
var filter_cleaned = [];
|
var filter_cleaned = [];
|
||||||
var filter_expr;
|
var filter_expr;
|
||||||
@@ -200,6 +201,9 @@
|
|||||||
$("#btn-filter-family").on("click", function() {
|
$("#btn-filter-family").on("click", function() {
|
||||||
toggle_filter(table, filter, "family");
|
toggle_filter(table, filter, "family");
|
||||||
} );
|
} );
|
||||||
|
$("#btn-filter-senior").on("click", function() {
|
||||||
|
toggle_filter(table, filter, "senior");
|
||||||
|
} );
|
||||||
$("#searchfield").on( "keyup change", function() {
|
$("#searchfield").on( "keyup change", function() {
|
||||||
table.column(0).search( this.value ).draw();
|
table.column(0).search( this.value ).draw();
|
||||||
} );
|
} );
|
||||||
|
|||||||
@@ -182,6 +182,23 @@
|
|||||||
Der/die Tourenleiter/in wird dir also persönlich per E-Mail zu- oder absagen.
|
Der/die Tourenleiter/in wird dir also persönlich per E-Mail zu- oder absagen.
|
||||||
</small></p>
|
</small></p>
|
||||||
</div>
|
</div>
|
||||||
|
{% if form.participation_conditions_accepted.errors %}
|
||||||
|
<div class="has-error">
|
||||||
|
{% endif %}
|
||||||
|
<div class="checkbox">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="participation_conditions_accepted" {% if form.participation_conditions_accepted.value %}checked="checked"{% endif %}>
|
||||||
|
{% trans 'Ich habe den oben erläuterten Teilnahmevorbehalt und die <a href="https://www.alpenverein-karlsruhe.de/programm/teilnahmebedingungen" target="_blank" rel="noreferrer noopener">Teilnahmebedingungen</a> gelesen und akzeptiert.' %}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{% if form.participation_conditions_accepted.errors %}
|
||||||
|
<div class="help-block">
|
||||||
|
{% for error in form.participation_conditions_accepted.errors %}
|
||||||
|
<p>{{ error }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row"> </div>
|
<div class="row"> </div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from six import string_types
|
||||||
|
|
||||||
from dav_base.tests.generic import AppSetting, AppsTestCase
|
from dav_base.tests.generic import AppSetting, AppsTestCase
|
||||||
|
|
||||||
@@ -8,5 +8,5 @@ class TestCase(AppsTestCase):
|
|||||||
app_config = apps.get_app_config('dav_registration')
|
app_config = apps.get_app_config('dav_registration')
|
||||||
|
|
||||||
settings = (
|
settings = (
|
||||||
AppSetting('privacy_policy', ImproperlyConfigured, str),
|
AppSetting('privacy_policy', string_types),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ class EmailsTestCase(EmailTestMixin, EventMixin, RegistrationMixin, TestCase):
|
|||||||
'year_of_birth': 1976,
|
'year_of_birth': 1976,
|
||||||
'apply_reduced_fee': True,
|
'apply_reduced_fee': True,
|
||||||
'dav_member': False,
|
'dav_member': False,
|
||||||
'dav_number': '131/00/007*1234',
|
'dav_number': '131/00/007*12345',
|
||||||
'emergency_contact': 'Call 911!',
|
'emergency_contact': 'Call 911!',
|
||||||
'experience': 'Yes, we can!',
|
'experience': 'Yes, we can!',
|
||||||
'note': 'Automatischer Software Test\nGruß\n heinzel =u}',
|
'note': 'Automatischer Software Test\nGruß\n heinzel =u}',
|
||||||
@@ -277,7 +277,7 @@ class EmailsTestCase(EmailTestMixin, EventMixin, RegistrationMixin, TestCase):
|
|||||||
'year_of_birth': THIS_YEAR,
|
'year_of_birth': THIS_YEAR,
|
||||||
'apply_reduced_fee': True,
|
'apply_reduced_fee': True,
|
||||||
'dav_member': False,
|
'dav_member': False,
|
||||||
'dav_number': '131/00/007*1234',
|
'dav_number': '131/00/007*12345',
|
||||||
'emergency_contact': 'Call 911!',
|
'emergency_contact': 'Call 911!',
|
||||||
'experience': 'Yes, we can!',
|
'experience': 'Yes, we can!',
|
||||||
'note': 'Automatischer Software Test\nGruß\n heinzel =u}',
|
'note': 'Automatischer Software Test\nGruß\n heinzel =u}',
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
babel
|
babel
|
||||||
django<6
|
django<5.1
|
||||||
django-bootstrap3
|
django-bootstrap3
|
||||||
django-countries
|
django-countries
|
||||||
django-datetime-widget2
|
django-datetime-widget2
|
||||||
|
|||||||
+2
-1
@@ -8,8 +8,9 @@ import django
|
|||||||
from django.test.utils import get_runner
|
from django.test.utils import get_runner
|
||||||
|
|
||||||
from dav_base.tests.utils import mkdtemp
|
from dav_base.tests.utils import mkdtemp
|
||||||
from dav_base.config import DJANGO_MAIN_MODULE
|
|
||||||
|
|
||||||
|
# from dav_base.console_scripts.admin import DJANGO_MAIN_MODULE
|
||||||
|
DJANGO_MAIN_MODULE = 'main'
|
||||||
TMP_BASE_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'tmp')
|
TMP_BASE_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'tmp')
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -27,11 +27,3 @@ description = Report test coverage
|
|||||||
deps = {[testenv:fresh]deps}
|
deps = {[testenv:fresh]deps}
|
||||||
commands = python -m coverage report --skip-covered
|
commands = python -m coverage report --skip-covered
|
||||||
|
|
||||||
[testenv:lint]
|
|
||||||
description = Run pylint
|
|
||||||
deps = {[testenv]deps}
|
|
||||||
pylint-django
|
|
||||||
commands_pre = {[testenv]commands_pre}
|
|
||||||
python -m pylint --version
|
|
||||||
commands = python -m pylint --fail-under=8 --django-settings-module='tests.settings' dav_*
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user