Merge branch 'main' into MK/meeting_checklist

pull/154/head
marius.klein 4 months ago
commit a8d6503b60

@ -6,3 +6,4 @@
omit = omit =
./jet/* ./jet/*
manage.py manage.py
jdav_web/wsgi.py

@ -1,7 +1,13 @@
from django.test import TestCase from django.test import TestCase
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib import admin
from django.db import models
from django.test import RequestFactory
from unittest.mock import Mock
from rules.contrib.models import RulesModelMixin, RulesModelBase
from contrib.models import CommonModel from contrib.models import CommonModel
from contrib.rules import has_global_perm from contrib.rules import has_global_perm
from contrib.admin import CommonAdminMixin
User = get_user_model() User = get_user_model()
@ -20,8 +26,6 @@ class CommonModelTestCase(TestCase):
# Test that CommonModel has the expected functionality # Test that CommonModel has the expected functionality
# Since it's abstract, we can't instantiate it directly # Since it's abstract, we can't instantiate it directly
# but we can check its metaclass and mixins # but we can check its metaclass and mixins
from rules.contrib.models import RulesModelMixin, RulesModelBase
self.assertTrue(issubclass(CommonModel, RulesModelMixin)) self.assertTrue(issubclass(CommonModel, RulesModelMixin))
self.assertEqual(CommonModel.__class__, RulesModelBase) self.assertEqual(CommonModel.__class__, RulesModelBase)
@ -54,3 +58,36 @@ class GlobalPermissionRulesTestCase(TestCase):
predicate = has_global_perm('auth.add_user') predicate = has_global_perm('auth.add_user')
result = predicate(self.user, None) result = predicate(self.user, None)
self.assertFalse(result) self.assertFalse(result)
class CommonAdminMixinTestCase(TestCase):
def setUp(self):
self.user = User.objects.create_user(username='testuser', password='testpass')
def test_formfield_for_dbfield_with_formfield_overrides(self):
"""Test formfield_for_dbfield when db_field class is in formfield_overrides"""
# Create a test admin instance that inherits from Django's ModelAdmin
class TestAdmin(CommonAdminMixin, admin.ModelAdmin):
formfield_overrides = {
models.ForeignKey: {'widget': Mock()}
}
# Create a mock model to use with the admin
class TestModel:
_meta = Mock()
_meta.app_label = 'test'
admin_instance = TestAdmin(TestModel, admin.site)
# Create a mock ForeignKey field to trigger the missing line 147
db_field = models.ForeignKey(User, on_delete=models.CASCADE)
# Create a test request
request = RequestFactory().get('/')
request.user = self.user
# Call the method to test formfield_overrides usage
result = admin_instance.formfield_for_dbfield(db_field, request, help_text='Test help text')
# Verify that the formfield_overrides were used
self.assertIsNotNone(result)

@ -1,3 +0,0 @@
from django.shortcuts import render
# Create your views here.

@ -1,2 +1,3 @@
from .admin import * from .admin import *
from .models import * from .models import *
from .rules import *

@ -0,0 +1,102 @@
from django.test import TestCase
from django.utils import timezone
from django.conf import settings
from django.contrib.auth.models import User
from unittest.mock import Mock
from finance.rules import is_creator, not_submitted, leads_excursion
from finance.models import Statement, Ledger
from members.models import Member, Group, Freizeit, GEMEINSCHAFTS_TOUR, MUSKELKRAFT_ANREISE, MALE, FEMALE
class FinanceRulesTestCase(TestCase):
def setUp(self):
self.group = Group.objects.create(name="Test Group")
self.ledger = Ledger.objects.create(name="Test Ledger")
self.user1 = User.objects.create_user(username="alice", password="test123")
self.member1 = Member.objects.create(
prename="Alice", lastname="Smith", birth_date=timezone.now().date(),
email=settings.TEST_MAIL, gender=FEMALE, user=self.user1
)
self.member1.group.add(self.group)
self.user2 = User.objects.create_user(username="bob", password="test123")
self.member2 = Member.objects.create(
prename="Bob", lastname="Jones", birth_date=timezone.now().date(),
email=settings.TEST_MAIL, gender=MALE, user=self.user2
)
self.member2.group.add(self.group)
self.freizeit = Freizeit.objects.create(
name="Test Excursion",
kilometers_traveled=100,
tour_type=GEMEINSCHAFTS_TOUR,
tour_approach=MUSKELKRAFT_ANREISE,
difficulty=2
)
self.freizeit.jugendleiter.add(self.member1)
self.statement = Statement.objects.create(
short_description="Test Statement",
explanation="Test explanation",
night_cost=27,
created_by=self.member1,
excursion=self.freizeit
)
self.statement.allowance_to.add(self.member1)
def test_is_creator_true(self):
"""Test is_creator predicate returns True when user created the statement"""
self.assertTrue(is_creator(self.user1, self.statement))
self.assertFalse(is_creator(self.user2, self.statement))
def test_not_submitted_statement(self):
"""Test not_submitted predicate returns True when statement is not submitted"""
self.statement.submitted = False
self.assertTrue(not_submitted(self.user1, self.statement))
self.statement.submitted = True
self.assertFalse(not_submitted(self.user1, self.statement))
def test_not_submitted_freizeit_with_statement(self):
"""Test not_submitted predicate with Freizeit having unsubmitted statement"""
self.freizeit.statement = self.statement
self.statement.submitted = False
self.assertTrue(not_submitted(self.user1, self.freizeit))
def test_not_submitted_freizeit_without_statement(self):
"""Test not_submitted predicate with Freizeit having no statement attribute"""
# Create a mock Freizeit that truly doesn't have the statement attribute
mock_freizeit = Mock(spec=Freizeit)
# Remove the statement attribute entirely
if hasattr(mock_freizeit, 'statement'):
delattr(mock_freizeit, 'statement')
self.assertTrue(not_submitted(self.user1, mock_freizeit))
def test_leads_excursion_freizeit_user_is_leader(self):
"""Test leads_excursion predicate returns True when user leads the Freizeit"""
self.assertTrue(leads_excursion(self.user1, self.freizeit))
self.assertFalse(leads_excursion(self.user2, self.freizeit))
def test_leads_excursion_statement_with_excursion(self):
"""Test leads_excursion predicate with statement having excursion led by user"""
result = leads_excursion(self.user1, self.statement)
self.assertTrue(result)
def test_leads_excursion_statement_no_excursion_attribute(self):
"""Test leads_excursion predicate with statement having no excursion attribute"""
mock_statement = Mock()
del mock_statement.excursion
result = leads_excursion(self.user1, mock_statement)
self.assertFalse(result)
def test_leads_excursion_statement_excursion_is_none(self):
"""Test leads_excursion predicate with statement having None excursion"""
statement_no_excursion = Statement.objects.create(
short_description="Test Statement No Excursion",
explanation="Test explanation",
night_cost=27,
created_by=self.member1,
excursion=None
)
result = leads_excursion(self.user1, statement_no_excursion)
self.assertFalse(result)

@ -1,3 +0,0 @@
from django.shortcuts import render
# Create your views here.

@ -11,4 +11,4 @@ app.config_from_object('django.conf:settings')
app.autodiscover_tasks() app.autodiscover_tasks()
if __name__ == '__main__': if __name__ == '__main__':
app.start() app.start() # pragma: no cover

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

@ -0,0 +1,2 @@
from .views import *
from .oauth import *

@ -0,0 +1,47 @@
from django.test import TestCase
from django.contrib.auth.models import User
from django.conf import settings
from unittest.mock import Mock
from logindata.oauth import CustomOAuth2Validator
from members.models import Member, MALE
class CustomOAuth2ValidatorTestCase(TestCase):
def setUp(self):
self.validator = CustomOAuth2Validator()
# Create user with member
self.user_with_member = User.objects.create_user(username="alice", password="test123")
self.member = Member.objects.create(
prename="Alice", lastname="Smith", birth_date="1990-01-01",
email=settings.TEST_MAIL, gender=MALE, user=self.user_with_member
)
# Create user without member
self.user_without_member = User.objects.create_user(username="bob", password="test123")
def test_get_additional_claims_with_member(self):
"""Test get_additional_claims when user has a member"""
request = Mock()
request.user = self.user_with_member
result = self.validator.get_additional_claims(request)
self.assertEqual(result['email'], settings.TEST_MAIL)
self.assertEqual(result['preferred_username'], 'alice')
def test_get_additional_claims_without_member(self):
"""Test get_additional_claims when user has no member"""
# ensure branch coverage, not possible under standard scenarios
request = Mock()
request.user = Mock()
request.user.member = None
self.assertEqual(len(self.validator.get_additional_claims(request)), 1)
request = Mock()
request.user = self.user_without_member
# The method will raise RelatedObjectDoesNotExist, which means the code
# should use hasattr or try/except. For now, test that it raises.
with self.assertRaises(User.member.RelatedObjectDoesNotExist):
self.validator.get_additional_claims(request)

@ -0,0 +1,154 @@
from http import HTTPStatus
from django.test import TestCase, Client
from django.urls import reverse
from django.utils import timezone
from django.utils.translation import gettext as _
from django.contrib.auth.models import User, Group
from members.models import Member, DIVERSE
from ..models import RegistrationPassword, initial_user_setup
class RegistrationPasswordTestCase(TestCase):
def test_str_method(self):
"""Test RegistrationPassword __str__ method returns password"""
reg_password = RegistrationPassword.objects.create(password="test123")
self.assertEqual(str(reg_password), "test123")
class RegisterViewTestCase(TestCase):
def setUp(self):
self.client = Client()
# Create a test member with invite key
self.member = Member.objects.create(
prename='Test',
lastname='User',
birth_date=timezone.now().date(),
email='test@example.com',
gender=DIVERSE,
invite_as_user_key='test_key_123'
)
# Create a registration password
self.registration_password = RegistrationPassword.objects.create(
password='test_password'
)
# Get or create Standard group for user setup
self.standard_group, created = Group.objects.get_or_create(name='Standard')
def test_register_get_without_key_redirects(self):
"""Test GET request without key redirects to startpage."""
url = reverse('logindata:register')
response = self.client.get(url)
self.assertEqual(response.status_code, HTTPStatus.FOUND)
def test_register_post_without_key_redirects(self):
"""Test POST request without key redirects to startpage."""
url = reverse('logindata:register')
response = self.client.post(url)
self.assertEqual(response.status_code, HTTPStatus.FOUND)
def test_register_get_with_empty_key_shows_failed(self):
"""Test GET request with empty key shows registration failed page."""
url = reverse('logindata:register')
response = self.client.get(url, {'key': ''})
self.assertEqual(response.status_code, HTTPStatus.OK)
self.assertContains(response, _('Something went wrong. The registration key is invalid or has expired.'))
def test_register_get_with_invalid_key_shows_failed(self):
"""Test GET request with invalid key shows registration failed page."""
url = reverse('logindata:register')
response = self.client.get(url, {'key': 'invalid_key'})
self.assertEqual(response.status_code, HTTPStatus.OK)
self.assertContains(response, _('Something went wrong. The registration key is invalid or has expired.'))
def test_register_get_with_valid_key_shows_password_form(self):
"""Test GET request with valid key shows password entry form."""
url = reverse('logindata:register')
response = self.client.get(url, {'key': self.member.invite_as_user_key})
self.assertEqual(response.status_code, HTTPStatus.OK)
self.assertContains(response, _('Set login data'))
self.assertContains(response, _('Welcome, '))
self.assertContains(response, self.member.prename)
def test_register_post_without_password_shows_failed(self):
"""Test POST request without password shows registration failed page."""
url = reverse('logindata:register')
response = self.client.post(url, {'key': self.member.invite_as_user_key})
self.assertEqual(response.status_code, HTTPStatus.OK)
self.assertContains(response, _('Something went wrong. The registration key is invalid or has expired.'))
def test_register_post_with_wrong_password_shows_error(self):
"""Test POST request with wrong password shows error message."""
url = reverse('logindata:register')
response = self.client.post(url, {
'key': self.member.invite_as_user_key,
'password': 'wrong_password'
})
self.assertEqual(response.status_code, HTTPStatus.OK)
self.assertContains(response, _('You entered a wrong password.'))
def test_register_post_with_correct_password_shows_form(self):
"""Test POST request with correct password shows user creation form."""
url = reverse('logindata:register')
response = self.client.post(url, {
'key': self.member.invite_as_user_key,
'password': self.registration_password.password
})
self.assertEqual(response.status_code, HTTPStatus.OK)
self.assertContains(response, _('Set login data'))
self.assertContains(response, self.member.suggested_username())
def test_register_post_with_save_and_invalid_form_shows_errors(self):
"""Test POST request with save but invalid form shows form errors."""
url = reverse('logindata:register')
response = self.client.post(url, {
'key': self.member.invite_as_user_key,
'password': self.registration_password.password,
'save': 'true',
'username': '', # Invalid - empty username
'password1': 'testpass123',
'password2': 'different_pass' # Invalid - passwords don't match
})
self.assertEqual(response.status_code, HTTPStatus.OK)
self.assertContains(response, _('Set login data'))
def test_register_post_with_save_and_valid_form_shows_success(self):
"""Test POST request with save and valid form shows success page."""
url = reverse('logindata:register')
response = self.client.post(url, {
'key': self.member.invite_as_user_key,
'password': self.registration_password.password,
'save': 'true',
'username': 'testuser',
'password1': 'testpass123',
'password2': 'testpass123'
})
self.assertEqual(response.status_code, HTTPStatus.OK)
self.assertContains(response, _('You successfully set your login data. You can now proceed to'))
# Verify user was created and associated with member
user = User.objects.get(username='testuser')
self.assertEqual(user.is_staff, True)
self.member.refresh_from_db()
self.assertEqual(self.member.user, user)
self.assertEqual(self.member.invite_as_user_key, '')
def test_register_post_with_save_and_no_standard_group_shows_failed(self):
"""Test POST request with save but no Standard group shows failed page."""
# Delete the Standard group
self.standard_group.delete()
url = reverse('logindata:register')
response = self.client.post(url, {
'key': self.member.invite_as_user_key,
'password': self.registration_password.password,
'save': 'true',
'username': 'testuser',
'password1': 'testpass123',
'password2': 'testpass123'
})
self.assertEqual(response.status_code, HTTPStatus.OK)
self.assertContains(response, _('Something went wrong. The registration key is invalid or has expired.'))

@ -1,3 +1,4 @@
from .models import * from .models import *
from .admin import * from .admin import *
from .views import * from .views import *
from .rules import *

@ -0,0 +1,337 @@
import json
import unittest
from http import HTTPStatus
from django.test import TestCase, override_settings
from django.contrib.admin.sites import AdminSite
from django.test import RequestFactory, Client
from django.contrib.auth.models import User, Permission
from django.utils import timezone
from django.contrib.sessions.middleware import SessionMiddleware
from django.contrib.messages.middleware import MessageMiddleware
from django.contrib.messages.storage.fallback import FallbackStorage
from django.contrib.messages import get_messages
from django.utils.translation import gettext_lazy as _
from django.urls import reverse, reverse_lazy
from django.http import HttpResponseRedirect, HttpResponse
from unittest.mock import Mock, patch
from django.test.utils import override_settings
from django.urls import path, include
from django.contrib import admin as django_admin
from django.conf import settings
from members.tests.utils import create_custom_user
from members.models import Member, MALE, DIVERSE, Group
from ..models import Message, Attachment, EmailAddress
from ..admin import MessageAdmin, submit_message
from ..mailutils import SENT, NOT_SENT, PARTLY_SENT
class AdminTestCase(TestCase):
def setUp(self, model, admin):
self.factory = RequestFactory()
self.model = model
if model is not None and admin is not None:
self.admin = admin(model, AdminSite())
superuser = User.objects.create_superuser(
username='superuser', password='secret'
)
standard = create_custom_user('standard', ['Standard'], 'Paul', 'Wulter')
trainer = create_custom_user('trainer', ['Standard', 'Trainings'], 'Lise', 'Lotte')
def _login(self, name):
c = Client()
res = c.login(username=name, password='secret')
# make sure we logged in
assert res
return c
def _add_middleware(self, request):
"""Add required middleware to request."""
# Session middleware
middleware = SessionMiddleware(lambda x: None)
middleware.process_request(request)
request.session.save()
# Messages middleware
messages_middleware = MessageMiddleware(lambda x: None)
messages_middleware.process_request(request)
request._messages = FallbackStorage(request)
class MessageAdminTestCase(AdminTestCase):
def setUp(self):
super().setUp(Message, MessageAdmin)
# Create test data
self.group = Group.objects.create(name='Test Group')
self.email_address = EmailAddress.objects.create(name='testmail')
# Create test member with internal email
self.internal_member = Member.objects.create(
prename='Internal',
lastname='User',
birth_date=timezone.now().date(),
email=f'internal@{settings.ALLOWED_EMAIL_DOMAINS_FOR_INVITE_AS_USER[0]}',
gender=DIVERSE
)
# Create test member with external email
self.external_member = Member.objects.create(
prename='External',
lastname='User',
birth_date=timezone.now().date(),
email='external@example.com',
gender=DIVERSE
)
# Create users for testing
self.user_with_internal_member = User.objects.create_user(username='testuser', password='secret')
self.user_with_internal_member.member = self.internal_member
self.user_with_internal_member.save()
self.user_with_external_member = User.objects.create_user(username='external_user', password='secret')
self.user_with_external_member.member = self.external_member
self.user_with_external_member.save()
self.user_without_member = User.objects.create_user(username='no_member_user', password='secret')
# Create test message
self.message = Message.objects.create(
subject='Test Message',
content='Test content'
)
self.message.to_groups.add(self.group)
self.message.to_members.add(self.internal_member)
def test_save_model_sets_created_by(self):
"""Test that save_model sets created_by when creating new message."""
request = self.factory.post('/admin/mailer/message/add/')
request.user = self.user_with_internal_member
# Create new message
new_message = Message(subject='New Message', content='New content')
# Test save_model for new object (change=False)
self.admin.save_model(request, new_message, None, change=False)
self.assertEqual(new_message.created_by, self.internal_member)
def test_save_model_does_not_change_created_by_on_update(self):
"""Test that save_model doesn't change created_by when updating."""
request = self.factory.post('/admin/mailer/message/1/change/')
request.user = self.user_with_internal_member
# Message already has created_by set
self.message.created_by = self.external_member
# Test save_model for existing object (change=True)
self.admin.save_model(request, self.message, None, change=True)
self.assertEqual(self.message.created_by, self.external_member)
@patch('mailer.models.Message.submit')
def test_submit_message_success(self, mock_submit):
"""Test submit_message with successful send."""
mock_submit.return_value = SENT
request = self.factory.post('/admin/mailer/message/')
request.user = self.user_with_internal_member
self._add_middleware(request)
# Test submit_message
submit_message(self.message, request)
# Verify submit was called with correct sender
mock_submit.assert_called_once_with(self.internal_member)
# Check success message
messages_list = list(get_messages(request))
self.assertEqual(len(messages_list), 1)
self.assertIn(str(_('Successfully sent message')), str(messages_list[0]))
@patch('mailer.models.Message.submit')
def test_submit_message_not_sent(self, mock_submit):
"""Test submit_message when sending fails."""
mock_submit.return_value = NOT_SENT
request = self.factory.post('/admin/mailer/message/')
request.user = self.user_with_internal_member
self._add_middleware(request)
# Test submit_message
submit_message(self.message, request)
# Check error message
messages_list = list(get_messages(request))
self.assertEqual(len(messages_list), 1)
self.assertIn(str(_('Failed to send message')), str(messages_list[0]))
@patch('mailer.models.Message.submit')
def test_submit_message_partly_sent(self, mock_submit):
"""Test submit_message when partially sent."""
mock_submit.return_value = PARTLY_SENT
request = self.factory.post('/admin/mailer/message/')
request.user = self.user_with_internal_member
self._add_middleware(request)
# Test submit_message
submit_message(self.message, request)
# Check warning message
messages_list = list(get_messages(request))
self.assertEqual(len(messages_list), 1)
self.assertIn(str(_('Failed to send some messages')), str(messages_list[0]))
def test_submit_message_user_has_no_member(self):
"""Test submit_message when user has no associated member."""
request = self.factory.post('/admin/mailer/message/')
request.user = self.user_without_member
self._add_middleware(request)
# Test submit_message
submit_message(self.message, request)
# Check error message
messages_list = list(get_messages(request))
self.assertEqual(len(messages_list), 1)
self.assertIn(str(_('Your account is not connected to a member. Please contact your system administrator.')), str(messages_list[0]))
def test_submit_message_user_has_external_email(self):
"""Test submit_message when user has external email."""
request = self.factory.post('/admin/mailer/message/')
request.user = self.user_with_external_member
self._add_middleware(request)
# Test submit_message
submit_message(self.message, request)
# Check error message
messages_list = list(get_messages(request))
self.assertEqual(len(messages_list), 1)
self.assertIn(str(_('Your email address is not an internal email address. Please use an email address with one of the following domains: %(domains)s.') % {'domains': ", ".join(settings.ALLOWED_EMAIL_DOMAINS_FOR_INVITE_AS_USER)}), str(messages_list[0]))
@patch('mailer.admin.submit_message')
def test_send_message_action_confirmed(self, mock_submit_message):
"""Test send_message action when confirmed."""
request = self.factory.post('/admin/mailer/message/', {'confirmed': 'true'})
request.user = self.user_with_internal_member
self._add_middleware(request)
queryset = Message.objects.filter(pk=self.message.pk)
# Test send_message action
result = self.admin.send_message(request, queryset)
# Verify submit_message was called for each message
mock_submit_message.assert_called_once_with(self.message, request)
# Should return None when confirmed (no template response)
self.assertIsNone(result)
def test_send_message_action_not_confirmed(self):
"""Test send_message action when not confirmed (shows confirmation page)."""
request = self.factory.post('/admin/mailer/message/')
request.user = self.user_with_internal_member
self._add_middleware(request)
queryset = Message.objects.filter(pk=self.message.pk)
# Test send_message action
result = self.admin.send_message(request, queryset)
# Should return HttpResponse with confirmation template
self.assertIsNotNone(result)
self.assertEqual(result.status_code, HTTPStatus.OK)
@patch('mailer.admin.submit_message')
def test_response_change_with_send(self, mock_submit_message):
"""Test response_change when _send is in POST."""
request = self.factory.post('/admin/mailer/message/1/change/', {'_send': 'Send'})
request.user = self.user_with_internal_member
self._add_middleware(request)
# Test response_change
with patch.object(self.admin.__class__.__bases__[2], 'response_change') as mock_super:
mock_super.return_value = HttpResponseRedirect('/admin/')
result = self.admin.response_change(request, self.message)
# Verify submit_message was called
mock_submit_message.assert_called_once_with(self.message, request)
# Verify super method was called
mock_super.assert_called_once()
@patch('mailer.admin.submit_message')
def test_response_change_without_send(self, mock_submit_message):
"""Test response_change when _send is not in POST."""
request = self.factory.post('/admin/mailer/message/1/change/', {'_save': 'Save'})
request.user = self.user_with_internal_member
self._add_middleware(request)
# Test response_change
with patch.object(self.admin.__class__.__bases__[2], 'response_change') as mock_super:
mock_super.return_value = HttpResponseRedirect('/admin/')
result = self.admin.response_change(request, self.message)
# Verify submit_message was NOT called
mock_submit_message.assert_not_called()
# Verify super method was called
mock_super.assert_called_once()
@patch('mailer.admin.submit_message')
def test_response_add_with_send(self, mock_submit_message):
"""Test response_add when _send is in POST."""
request = self.factory.post('/admin/mailer/message/add/', {'_send': 'Send'})
request.user = self.user_with_internal_member
self._add_middleware(request)
# Test response_add
with patch.object(self.admin.__class__.__bases__[2], 'response_add') as mock_super:
mock_super.return_value = HttpResponseRedirect('/admin/')
result = self.admin.response_add(request, self.message)
# Verify submit_message was called
mock_submit_message.assert_called_once_with(self.message, request)
# Verify super method was called
mock_super.assert_called_once()
def test_get_form_with_members_param(self):
"""Test get_form when members parameter is provided."""
# Create request with members parameter
members_ids = [self.internal_member.pk, self.external_member.pk]
request = self.factory.get(f'/admin/mailer/message/add/?members={json.dumps(members_ids)}')
request.user = self.user_with_internal_member
# Test get_form
form_class = self.admin.get_form(request)
form = form_class()
# Verify initial members are set
self.assertEqual(list(form.fields['to_members'].initial), [self.internal_member, self.external_member])
def test_get_form_with_invalid_members_param(self):
"""Test get_form when members parameter is not a list."""
# Create request with invalid members parameter
request = self.factory.get('/admin/mailer/message/add/?members="not_a_list"')
request.user = self.user_with_internal_member
# Test get_form
form_class = self.admin.get_form(request)
# Should return form without modification
self.assertIsNotNone(form_class)
def test_get_form_without_members_param(self):
"""Test get_form when no members parameter is provided."""
# Create request without members parameter
request = self.factory.get('/admin/mailer/message/add/')
request.user = self.user_with_internal_member
# Test get_form
form_class = self.admin.get_form(request)
# Should return form without modification
self.assertIsNotNone(form_class)

@ -124,7 +124,7 @@ class MessageTestCase(BasicMailerTestCase):
# Verify the message was not marked as sent # Verify the message was not marked as sent
self.message.refresh_from_db() self.message.refresh_from_db()
self.assertFalse(self.message.sent) self.assertFalse(self.message.sent)
# Note: The submit method always returns SENT due to line 190 in the code # Note: The submit method always returns SENT when an exception occurs
self.assertEqual(result, SENT) self.assertEqual(result, SENT)
@mock.patch('mailer.models.send') @mock.patch('mailer.models.send')
@ -236,6 +236,22 @@ class MessageTestCase(BasicMailerTestCase):
with self.assertRaises(Attachment.DoesNotExist): with self.assertRaises(Attachment.DoesNotExist):
attachment.refresh_from_db() attachment.refresh_from_db()
@mock.patch('mailer.models.send')
def test_submit_with_association_email_enabled(self, mock_send):
"""Test submit method when SEND_FROM_ASSOCIATION_EMAIL is True and sender has association_email"""
mock_send.return_value = SENT
# Mock settings to enable association email sending
with mock.patch.object(settings, 'SEND_FROM_ASSOCIATION_EMAIL', True):
result = self.message.submit(sender=self.sender)
# Check that send was called with sender's association email
self.assertTrue(mock_send.called)
call_args = mock_send.call_args
from_addr = call_args[0][2] # from_addr is the 3rd positional argument
expected_from = f"{self.sender.name} <{self.sender.association_email}>"
self.assertEqual(from_addr, expected_from)
class AttachmentTestCase(BasicMailerTestCase): class AttachmentTestCase(BasicMailerTestCase):
def setUp(self): def setUp(self):

@ -0,0 +1,31 @@
from django.test import TestCase
from django.conf import settings
from django.contrib.auth.models import User
from mailer.rules import is_creator
from mailer.models import Message
from members.models import Member, MALE
class MailerRulesTestCase(TestCase):
def setUp(self):
self.user1 = User.objects.create_user(username="alice", password="test123")
self.member1 = Member.objects.create(
prename="Alice", lastname="Smith", birth_date="1990-01-01",
email=settings.TEST_MAIL, gender=MALE, user=self.user1
)
self.message = Message.objects.create(
subject="Test Message",
content="Test content",
created_by=self.member1
)
def test_is_creator_returns_true_when_user_created_message(self):
"""Test is_creator predicate returns True when user created the message"""
result = is_creator(self.user1, self.message)
self.assertTrue(result)
def test_is_creator_returns_false_when_message_is_none(self):
"""Test is_creator predicate returns False when message is None"""
result = is_creator(self.user1, None)
self.assertFalse(result)

@ -1,8 +1,10 @@
from django.test import TestCase from django.test import TestCase, RequestFactory
from django.utils import timezone from django.utils import timezone
from datetime import date from datetime import date, datetime
from decimal import Decimal from decimal import Decimal
from material.models import MaterialCategory, MaterialPart, Ownership from unittest.mock import Mock
from material.models import MaterialCategory, MaterialPart, Ownership, yearsago
from material.admin import NotTooOldFilter, MaterialAdmin
from members.models import Member, MALE, FEMALE, DIVERSE from members.models import Member, MALE, FEMALE, DIVERSE
@ -75,6 +77,37 @@ class MaterialPartTestCase(TestCase):
self.assertTrue(hasattr(field, 'verbose_name')) self.assertTrue(hasattr(field, 'verbose_name'))
self.assertIsNotNone(field.verbose_name) self.assertIsNotNone(field.verbose_name)
def test_admin_thumbnail_with_photo(self):
"""Test admin_thumbnail when photo exists"""
mock_photo = Mock()
mock_photo.url = "/media/test.jpg"
self.material_part.photo = mock_photo
result = self.material_part.admin_thumbnail()
self.assertIn("/media/test.jpg", result)
self.assertIn("<img", result)
def test_admin_thumbnail_without_photo(self):
"""Test admin_thumbnail when no photo exists"""
self.material_part.photo = None
result = self.material_part.admin_thumbnail()
self.assertIn("kein Bild", result)
def test_ownership_overview(self):
"""Test ownership_overview method"""
Ownership.objects.create(material=self.material_part, owner=self.member, count=2)
result = self.material_part.ownership_overview()
self.assertIn(str(self.member), result)
self.assertIn("2", result)
def test_not_too_old(self):
"""Test not_too_old method"""
# Set a buy_date that makes the material old
old_date = date(2000, 1, 1)
self.material_part.buy_date = old_date
self.material_part.lifetime = Decimal('5')
result = self.material_part.not_too_old()
self.assertFalse(result)
class OwnershipTestCase(TestCase): class OwnershipTestCase(TestCase):
def setUp(self): def setUp(self):
@ -112,3 +145,94 @@ class OwnershipTestCase(TestCase):
ownerships = Ownership.objects.filter(material=self.material_part) ownerships = Ownership.objects.filter(material=self.material_part)
self.assertEqual(ownerships.count(), 1) self.assertEqual(ownerships.count(), 1)
self.assertEqual(ownerships.first(), self.ownership) self.assertEqual(ownerships.first(), self.ownership)
def test_str(self):
"""Test string representation of Ownership"""
result = str(self.ownership)
self.assertEqual(result, str(self.member))
class UtilityFunctionTestCase(TestCase):
def test_yearsago_with_from_date(self):
"""Test yearsago function with explicit from_date"""
test_date = timezone.make_aware(datetime(2020, 5, 15, 12, 0, 0))
result = yearsago(5, from_date=test_date)
expected = timezone.make_aware(datetime(2015, 5, 15, 12, 0, 0))
self.assertEqual(result, expected)
def test_yearsago_default_from_date(self):
"""Test yearsago function with default from_date (None)"""
# This will use timezone.now() internally
result = yearsago(1)
self.assertIsNotNone(result)
self.assertLess(result, timezone.now())
def test_yearsago_leap_year_edge_case(self):
"""Test yearsago function with leap year edge case (Feb 29)"""
# Feb 29, 2020 (leap year) minus 1 year should become Feb 28, 2019
leap_date = timezone.make_aware(datetime(2020, 2, 29, 12, 0, 0))
result = yearsago(1, from_date=leap_date)
expected = timezone.make_aware(datetime(2019, 2, 28, 12, 0, 0))
self.assertEqual(result, expected)
class NotTooOldFilterTestCase(TestCase):
def setUp(self):
self.factory = RequestFactory()
self.filter = NotTooOldFilter(None, {}, MaterialPart, MaterialAdmin)
# Create test data
self.member = Member.objects.create(
prename="Test", lastname="User", birth_date=date(1990, 1, 1),
email="test@example.com", gender=MALE
)
# Create old material (should be too old)
self.old_material = MaterialPart.objects.create(
name="Old Material",
description="Old material",
quantity=1,
buy_date=date(2000, 1, 1), # Very old
lifetime=Decimal('5')
)
# Create new material (should not be too old)
self.new_material = MaterialPart.objects.create(
name="New Material",
description="New material",
quantity=1,
buy_date=date.today(), # Today
lifetime=Decimal('10')
)
def test_not_too_old_filter_lookups(self):
"""Test NotTooOldFilter lookups method"""
request = self.factory.get('/')
lookups = self.filter.lookups(request, None)
self.assertEqual(len(lookups), 2)
self.assertEqual(lookups[0][0], 'too_old')
self.assertEqual(lookups[1][0], 'not_too_old')
def test_not_too_old_filter_queryset_too_old(self):
"""Test NotTooOldFilter queryset method with 'too_old' value"""
request = self.factory.get('/?age=too_old')
self.filter.used_parameters = {'age': 'too_old'}
queryset = MaterialPart.objects.all()
filtered = self.filter.queryset(request, queryset)
# Should return materials that are not too old (i.e., new materials)
self.assertIn(self.new_material, filtered)
self.assertNotIn(self.old_material, filtered)
def test_not_too_old_filter_queryset_not_too_old(self):
"""Test NotTooOldFilter queryset method with 'not_too_old' value"""
request = self.factory.get('/?age=not_too_old')
self.filter.used_parameters = {'age': 'not_too_old'}
queryset = MaterialPart.objects.all()
filtered = self.filter.queryset(request, queryset)
# Should return materials that are too old
self.assertIn(self.old_material, filtered)
self.assertNotIn(self.new_material, filtered)

@ -1,3 +0,0 @@
from django.shortcuts import render
# Create your views here.

@ -331,7 +331,8 @@ class MemberAdmin(CommonAdminMixin, admin.ModelAdmin):
return request.user.has_perm('%s.%s' % (self.opts.app_label, 'may_invite_as_user')) return request.user.has_perm('%s.%s' % (self.opts.app_label, 'may_invite_as_user'))
def invite_as_user_action(self, request, queryset): def invite_as_user_action(self, request, queryset):
if not request.user.has_perm('members.may_invite_as_user'): if not request.user.has_perm('members.may_invite_as_user'): # pragma: no cover
# this should be unreachable, because of allowed_permissions attribute
messages.error(request, _('Permission denied.')) messages.error(request, _('Permission denied.'))
return HttpResponseRedirect(reverse('admin:%s_%s_changelist' % (self.opts.app_label, self.opts.model_name))) return HttpResponseRedirect(reverse('admin:%s_%s_changelist' % (self.opts.app_label, self.opts.model_name)))
if "apply" in request.POST: if "apply" in request.POST:
@ -846,7 +847,7 @@ class GroupAdmin(CommonAdminMixin, admin.ModelAdmin):
return update_wrapper(wrapper, view) return update_wrapper(wrapper, view)
custom_urls = [ custom_urls = [
path('action/', self.action_view, name='members_group_action'), path('action/', wrap(self.action_view), name='members_group_action'),
] ]
return custom_urls + urls return custom_urls + urls

@ -1,6 +1,6 @@
from .models import * from .models import * # pragma: no cover
import re import re # pragma: no cover
import csv import csv # pragma: no cover
def import_from_csv(path, omit_groupless=True): # pragma: no cover def import_from_csv(path, omit_groupless=True): # pragma: no cover

@ -35,9 +35,6 @@ from utils import cvt_to_decimal, coming_midnight
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from schwifty import IBAN from schwifty import IBAN
def generate_random_key():
return uuid.uuid4().hex
GEMEINSCHAFTS_TOUR = MUSKELKRAFT_ANREISE = MALE = 0 GEMEINSCHAFTS_TOUR = MUSKELKRAFT_ANREISE = MALE = 0
FUEHRUNGS_TOUR = OEFFENTLICHE_ANREISE = FEMALE = 1 FUEHRUNGS_TOUR = OEFFENTLICHE_ANREISE = FEMALE = 1
@ -149,7 +146,7 @@ class Group(models.Model):
group_age = self.get_age_info() group_age = self.get_age_info()
else: else:
group_age = _("no information available") group_age = _("no information available")
return settings.INVITE_TEXT.format(group_time=group_time, return settings.INVITE_TEXT.format(group_time=group_time,
group_name=self.name, group_name=self.name,
group_age=group_age, group_age=group_age,
@ -213,7 +210,8 @@ class Contact(CommonModel):
for email_fd, confirmed_email_fd, confirm_mail_key_fd in self.email_fields: for email_fd, confirmed_email_fd, confirm_mail_key_fd in self.email_fields:
if getattr(self, confirmed_email_fd) and not rerequest: if getattr(self, confirmed_email_fd) and not rerequest:
continue continue
if not getattr(self, email_fd): if not getattr(self, email_fd): # pragma: no cover
# Only reachable with misconfigured `email_fields`
continue continue
requested_confirmation = True requested_confirmation = True
setattr(self, confirmed_email_fd, False) setattr(self, confirmed_email_fd, False)
@ -612,7 +610,16 @@ class Member(Person):
settings.DEFAULT_SENDING_MAIL, settings.DEFAULT_SENDING_MAIL,
jl.email) jl.email)
def filter_queryset_by_permissions(self, queryset=None, annotate=False, model=None): def filter_queryset_by_permissions(self, queryset=None, annotate=False, model=None): # pragma: no cover
"""
Filter the given queryset of objects of type `model` by the permissions of `self`.
For example, only returns `Message`s created by `self`.
This method is used by the `FilteredMemberFieldMixin` to filter the selection
in `ForeignKey` and `ManyToMany` fields.
"""
# This method is not used by all models listed below, so covering all cases in tests
# is hard and not useful. It is therefore exempt from testing.
name = model._meta.object_name name = model._meta.object_name
if queryset is None: if queryset is None:
queryset = Member.objects.all() queryset = Member.objects.all()
@ -1051,6 +1058,7 @@ class MemberWaitingList(Person):
@property @property
def waiting_confirmation_needed(self): def waiting_confirmation_needed(self):
"""Returns if person should be asked to confirm waiting status.""" """Returns if person should be asked to confirm waiting status."""
# TODO: Throws `NameError` (has skipped test).
return wait_confirmation_key is None \ return wait_confirmation_key is None \
and last_wait_confirmation < timezone.now() -\ and last_wait_confirmation < timezone.now() -\
timezone.timedelta(days=settings.WAITING_CONFIRMATION_FREQUENCY) timezone.timedelta(days=settings.WAITING_CONFIRMATION_FREQUENCY)
@ -1115,6 +1123,7 @@ class MemberWaitingList(Person):
return self.wait_confirmation_key return self.wait_confirmation_key
def may_register(self, key): def may_register(self, key):
# TODO: Throws a `TypeError` (has skipped test).
print("may_register", key) print("may_register", key)
try: try:
invitation = InvitationToGroup.objects.get(key=key) invitation = InvitationToGroup.objects.get(key=key)
@ -1188,11 +1197,13 @@ class NewMemberOnList(CommonModel):
@property @property
def skills(self): def skills(self):
# TODO: Throws a `NameError` (has skipped test).
activities = [a.name for a in memberlist.activity.all()] activities = [a.name for a in memberlist.activity.all()]
return {k: v for k, v in self.member.get_skills().items() if k in activities} return {k: v for k, v in self.member.get_skills().items() if k in activities}
@property @property
def qualities_tex(self): def qualities_tex(self):
# TODO: Throws a `NameError` (has skipped test).
qualities = [] qualities = []
for activity, value in self.skills: for activity, value in self.skills:
qualities.append("\\textit{%s:} %s" % (activity, value)) qualities.append("\\textit{%s:} %s" % (activity, value))

@ -1 +1,4 @@
from .basic import * from .basic import *
from .views import *
from .tasks import *
from .rules import *

@ -14,6 +14,11 @@ from django.conf import settings
from django.urls import reverse from django.urls import reverse
from django import template from django import template
from unittest import skip, mock from unittest import skip, mock
import os
from PIL import Image
from pypdf import PdfReader, PdfWriter, PageObject
from io import BytesIO
import tempfile
from members.models import Member, Group, PermissionMember, PermissionGroup, Freizeit, GEMEINSCHAFTS_TOUR,\ from members.models import Member, Group, PermissionMember, PermissionGroup, Freizeit, GEMEINSCHAFTS_TOUR,\
MUSKELKRAFT_ANREISE, FUEHRUNGS_TOUR, AUSBILDUNGS_TOUR, OEFFENTLICHE_ANREISE,\ MUSKELKRAFT_ANREISE, FUEHRUNGS_TOUR, AUSBILDUNGS_TOUR, OEFFENTLICHE_ANREISE,\
FAHRGEMEINSCHAFT_ANREISE,\ FAHRGEMEINSCHAFT_ANREISE,\
@ -23,9 +28,11 @@ from members.models import Member, Group, PermissionMember, PermissionGroup, Fre
TrainingCategory, Person TrainingCategory, Person
from members.admin import MemberWaitingListAdmin, MemberAdmin, FreizeitAdmin, MemberNoteListAdmin,\ from members.admin import MemberWaitingListAdmin, MemberAdmin, FreizeitAdmin, MemberNoteListAdmin,\
MemberUnconfirmedAdmin, RegistrationFilter, FilteredMemberFieldMixin,\ MemberUnconfirmedAdmin, RegistrationFilter, FilteredMemberFieldMixin,\
MemberAdminForm, StatementOnListForm, KlettertreffAdmin, GroupAdmin MemberAdminForm, StatementOnListForm, KlettertreffAdmin, GroupAdmin,\
from members.pdf import fill_pdf_form, render_tex, media_path, serve_pdf, find_template, merge_pdfs InvitationToGroupAdmin, AgeFilter, InvitedToGroupFilter
from mailer.models import EmailAddress from members.pdf import fill_pdf_form, render_tex, media_path, serve_pdf, find_template, merge_pdfs, render_docx, pdf_add_attachments, scale_pdf_page_to_a4, scale_pdf_to_a4
from members.excel import generate_ljp_vbk
from mailer.models import EmailAddress, Message
from finance.models import Statement, Bill from finance.models import Statement, Bill
from django.db import connection from django.db import connection
@ -58,9 +65,14 @@ class MemberTestCase(BasicMemberTestCase):
super().setUp() super().setUp()
p1 = PermissionMember.objects.create(member=self.fritz) p1 = PermissionMember.objects.create(member=self.fritz)
p1.list_members.add(self.lara)
p1.view_members.add(self.lara) p1.view_members.add(self.lara)
p1.change_members.add(self.lara) p1.change_members.add(self.lara)
p1.delete_members.add(self.lara)
p1.list_groups.add(self.spiel)
p1.view_groups.add(self.spiel) p1.view_groups.add(self.spiel)
p1.change_groups.add(self.spiel)
p1.delete_groups.add(self.spiel)
self.ja = Group.objects.create(name="Jugendausschuss") self.ja = Group.objects.create(name="Jugendausschuss")
self.peter = Member.objects.create(prename="Peter", lastname="Keks", birth_date=timezone.now().date(), self.peter = Member.objects.create(prename="Peter", lastname="Keks", birth_date=timezone.now().date(),
@ -75,32 +87,62 @@ class MemberTestCase(BasicMemberTestCase):
self.lisa = Member.objects.create(prename="Lisa", lastname="Keks", birth_date=timezone.now().date(), self.lisa = Member.objects.create(prename="Lisa", lastname="Keks", birth_date=timezone.now().date(),
email=settings.TEST_MAIL, gender=DIVERSE, email=settings.TEST_MAIL, gender=DIVERSE,
image=img, registration_form=pdf) image=img, registration_form=pdf)
self.lisa.confirmed_mail, self.lisa.confirmed_alternative_mail = True, True
self.peter.group.add(self.ja) self.peter.group.add(self.ja)
self.anna.group.add(self.ja) self.anna.group.add(self.ja)
self.lisa.group.add(self.ja) self.lisa.group.add(self.ja)
self.ex = Freizeit.objects.create(name='Wild trip', kilometers_traveled=120,
tour_type=GEMEINSCHAFTS_TOUR,
tour_approach=MUSKELKRAFT_ANREISE,
difficulty=1, date=timezone.localtime())
self.ex.jugendleiter.add(self.fritz)
self.ex.save()
p2 = PermissionGroup.objects.create(group=self.ja) p2 = PermissionGroup.objects.create(group=self.ja)
p2.list_members.add(self.lara)
p2.view_members.add(self.lara)
p2.change_members.add(self.lara)
p2.delete_members.add(self.lara)
p2.list_groups.add(self.ja) p2.list_groups.add(self.ja)
p2.list_groups.add(self.spiel)
p2.view_groups.add(self.spiel)
p2.change_groups.add(self.spiel)
p2.delete_groups.add(self.spiel)
def test_may(self): def test_may(self):
self.assertTrue(self.fritz.may_list(self.lara))
self.assertTrue(self.fritz.may_view(self.lara)) self.assertTrue(self.fritz.may_view(self.lara))
self.assertTrue(self.fritz.may_change(self.lara)) self.assertTrue(self.fritz.may_change(self.lara))
self.assertTrue(self.fritz.may_delete(self.lara))
self.assertTrue(self.fritz.may_list(self.fridolin))
self.assertTrue(self.fritz.may_view(self.fridolin)) self.assertTrue(self.fritz.may_view(self.fridolin))
self.assertFalse(self.fritz.may_change(self.fridolin)) self.assertTrue(self.fritz.may_change(self.fridolin))
self.assertTrue(self.fritz.may_delete(self.fridolin))
self.assertFalse(self.fritz.may_view(self.anna))
# every member should be able to list, view and change themselves # every member should be able to list, view and change themselves
for member in Member.objects.all(): for member in Member.objects.all():
self.assertTrue(member.may_list(member)) self.assertTrue(member.may_list(member))
self.assertTrue(member.may_view(member)) self.assertTrue(member.may_view(member))
self.assertTrue(member.may_change(member)) self.assertTrue(member.may_change(member))
self.assertTrue(member.may_delete(member))
# every member of Jugendausschuss should be able to view every other member of Jugendausschuss # every member of Jugendausschuss should be able to view every other member of Jugendausschuss
for member in self.ja.member_set.all(): for member in self.ja.member_set.all():
self.assertTrue(member.may_list(self.fridolin))
self.assertTrue(member.may_view(self.fridolin))
self.assertTrue(member.may_view(self.lara))
self.assertTrue(member.may_change(self.lara))
self.assertTrue(member.may_change(self.fridolin))
self.assertTrue(member.may_delete(self.lara))
self.assertTrue(member.may_delete(self.fridolin))
for other in self.ja.member_set.all(): for other in self.ja.member_set.all():
self.assertTrue(member.may_list(other)) self.assertTrue(member.may_list(other))
if member != other: if member != other:
self.assertFalse(member.may_view(other)) self.assertFalse(member.may_view(other))
self.assertFalse(member.may_change(other)) self.assertFalse(member.may_change(other))
self.assertFalse(member.may_delete(other))
def test_filter_queryset(self): def test_filter_queryset(self):
# lise may only list herself # lise may only list herself
@ -113,6 +155,42 @@ class MemberTestCase(BasicMemberTestCase):
self.assertEqual(set(member.filter_queryset_by_permissions(Member.objects.all(), model=Member)), self.assertEqual(set(member.filter_queryset_by_permissions(Member.objects.all(), model=Member)),
set(member.filter_queryset_by_permissions(model=Member))) set(member.filter_queryset_by_permissions(model=Member)))
def test_filter_members_by_permissions(self):
qs = Member.objects.all()
qs_a = self.anna.filter_members_by_permissions(qs, annotate=True)
# Anna may list Peter, because Peter is also in the Jugendausschuss.
self.assertIn(self.peter, qs_a)
# Anna may not view Peter.
self.assertNotIn(self.peter, qs_a.filter(_viewable=True))
def test_filter_messages_by_permissions(self):
good = Message.objects.create(subject='Good message', content='This is a test message',
created_by=self.fritz)
bad = Message.objects.create(subject='Bad message', content='This is a test message')
self.assertQuerysetEqual(self.fritz.filter_messages_by_permissions(Message.objects.all()),
[good], ordered=False)
def test_filter_statements_by_permissions(self):
st1 = Statement.objects.create(night_cost=42, subsidy_to=None, created_by=self.fritz)
st2 = Statement.objects.create(night_cost=42, subsidy_to=None, excursion=self.ex)
st3 = Statement.objects.create(night_cost=42, subsidy_to=None)
qs = Statement.objects.all()
self.assertQuerysetEqual(self.fritz.filter_statements_by_permissions(qs),
[st1, st2], ordered=False)
def test_annotate_view_permissions(self):
qs = Member.objects.all()
# if the model is not Member, the queryset should not change
self.assertQuerysetEqual(self.fritz.annotate_view_permission(qs, MemberWaitingList), qs,
ordered=False)
# Fritz can't view Anna.
qs_a = self.fritz.annotate_view_permission(qs, Member)
self.assertNotIn(self.anna, qs_a.filter(_viewable=True))
# Anna can't view Fritz.
qs_a = self.anna.annotate_view_permission(qs, Member)
self.assertNotIn(self.fritz, qs_a.filter(_viewable=True))
def test_compare_filter_queryset_may_list(self): def test_compare_filter_queryset_may_list(self):
# filter_queryset and filtering manually by may_list should be the same # filter_queryset and filtering manually by may_list should be the same
@ -211,11 +289,18 @@ class MemberTestCase(BasicMemberTestCase):
self.assertFalse(self.peter.has_internal_email()) self.assertFalse(self.peter.has_internal_email())
def test_invite_as_user(self): def test_invite_as_user(self):
# sucess
self.assertTrue(self.lara.has_internal_email()) self.assertTrue(self.lara.has_internal_email())
self.lara.user = None self.lara.user = None
self.assertTrue(self.lara.invite_as_user()) self.assertTrue(self.lara.invite_as_user())
# failure: already has user data
u = User.objects.create_user(username='user', password='secret', is_staff=True) u = User.objects.create_user(username='user', password='secret', is_staff=True)
self.peter.user = u self.lara.user = u
self.assertFalse(self.lara.invite_as_user())
# failure: no internal email
self.peter.email = 'foobar'
self.assertFalse(self.peter.invite_as_user()) self.assertFalse(self.peter.invite_as_user())
def test_birth_date_str(self): def test_birth_date_str(self):
@ -228,6 +313,29 @@ class MemberTestCase(BasicMemberTestCase):
def test_gender_str(self): def test_gender_str(self):
self.assertGreater(len(self.fritz.gender_str), 0) self.assertGreater(len(self.fritz.gender_str), 0)
def test_led_freizeiten(self):
self.assertGreater(len(self.fritz.led_freizeiten()), 0)
def test_create_from_registration(self):
self.lisa.confirmed = False
# Lisa's registration is ready, no more mail requests needed
self.assertFalse(self.lisa.create_from_registration(None, self.alp))
# After creating from registration, Lisa should be unconfirmed.
self.assertFalse(self.lisa.confirmed)
def test_validate_registration_form(self):
self.lisa.confirmed = False
self.assertIsNotNone(self.lisa.registration_form)
self.assertIsNone(self.lisa.validate_registration_form())
def test_send_upload_registration_form_link(self):
self.assertEqual(self.lisa.upload_registration_form_key, '')
self.assertIsNone(self.lisa.send_upload_registration_form_link())
def test_demote_to_waiter(self):
self.lisa.waitinglist_application_date = timezone.now()
self.lisa.demote_to_waiter()
class PDFTestCase(TestCase): class PDFTestCase(TestCase):
def setUp(self): def setUp(self):
@ -295,6 +403,76 @@ class PDFTestCase(TestCase):
context = self.ex.v32_fields() context = self.ex.v32_fields()
self._test_fill_pdf('members/V32-1_Themenorientierte_Bildungsmassnahmen.pdf', context) self._test_fill_pdf('members/V32-1_Themenorientierte_Bildungsmassnahmen.pdf', context)
def test_render_docx_save_only(self):
"""Test render_docx with save_only=True"""
context = dict(memberlist=self.ex, settings=settings, mode='basic')
fp = render_docx('Test DOCX', 'members/seminar_report.tex', context, save_only=True)
self.assertIsInstance(fp, str)
self.assertTrue(fp.endswith('.docx'))
def test_pdf_add_attachments_with_image(self):
"""Test pdf_add_attachments with non-PDF image files"""
# Create a simple test image
with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as tmp_file:
img = Image.new('RGB', (100, 100), color='red')
img.save(tmp_file.name, 'PNG')
tmp_file.flush()
# Create a PDF writer and test adding the image
writer = PdfWriter()
blank_page = PageObject.create_blank_page(width=595, height=842)
writer.add_page(blank_page)
# add image as attachment and verify page count
pdf_add_attachments(writer, [tmp_file.name])
self.assertGreater(len(writer.pages), 1)
# Clean up
os.unlink(tmp_file.name)
def test_scale_pdf_page_to_a4(self):
"""Test scale_pdf_page_to_a4 function"""
# Create a test page with different dimensions
original_page = PageObject.create_blank_page(width=200, height=300)
scaled_page = scale_pdf_page_to_a4(original_page)
# A4 dimensions are 595x842
self.assertEqual(float(scaled_page.mediabox.width), 595.0)
self.assertEqual(float(scaled_page.mediabox.height), 842.0)
def test_scale_pdf_to_a4(self):
"""Test scale_pdf_to_a4 function"""
# Create a simple PDF with multiple pages of different sizes
original_pdf = PdfWriter()
original_pdf.add_page(PageObject.create_blank_page(width=200, height=300))
original_pdf.add_page(PageObject.create_blank_page(width=400, height=600))
# Write to BytesIO to create a readable PDF
pdf_io = BytesIO()
original_pdf.write(pdf_io)
pdf_io.seek(0)
# Read it back and scale
pdf_reader = PdfReader(pdf_io)
scaled_pdf = scale_pdf_to_a4(pdf_reader)
# All pages should be A4 size (595x842)
for page in scaled_pdf.pages:
self.assertEqual(float(page.mediabox.width), 595.0)
self.assertEqual(float(page.mediabox.height), 842.0)
def test_merge_pdfs_serve(self):
"""Test merge_pdfs with save_only=False"""
# First create two PDF files to merge
context = dict(memberlist=self.ex, settings=settings, mode='basic')
fp1 = render_tex('Test PDF 1', 'members/seminar_report.tex', context, save_only=True)
fp2 = render_tex('Test PDF 2', 'members/seminar_report.tex', context, save_only=True)
# Test merge with save_only=False (should return HttpResponse)
response = merge_pdfs('Merged PDF', [fp1, fp2], save_only=False)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers['Content-Type'], 'application/pdf')
class AdminTestCase(TestCase): class AdminTestCase(TestCase):
def setUp(self, model, admin): def setUp(self, model, admin):
@ -312,8 +490,9 @@ class AdminTestCase(TestCase):
paul = standard.member paul = standard.member
self.staff = Group.objects.create(name='Jugendleiter') self.em = EmailAddress.objects.create(name='foobar')
cool_kids = Group.objects.create(name='cool kids') self.staff = Group.objects.create(name='Jugendleiter', contact_email=self.em)
cool_kids = Group.objects.create(name='cool kids', show_website=True)
super_kids = Group.objects.create(name='super kids') super_kids = Group.objects.create(name='super kids')
p1 = PermissionMember.objects.create(member=paul) p1 = PermissionMember.objects.create(member=paul)
@ -546,6 +725,16 @@ class MemberAdminTestCase(AdminTestCase):
self.assertEqual(response.status_code, HTTPStatus.OK) self.assertEqual(response.status_code, HTTPStatus.OK)
self.assertContains(response, _("%(name)s already has login data.") % {'name': str(self.fritz)}) self.assertContains(response, _("%(name)s already has login data.") % {'name': str(self.fritz)})
def test_invite_as_user_action_insufficient_permission(self):
url = reverse('admin:members_member_changelist')
# expect: confirmation view
c = self._login('trainer')
response = c.post(url, data={'action': 'invite_as_user_action',
'_selected_action': [self.fritz.pk]}, follow=True)
self.assertEqual(response.status_code, HTTPStatus.OK)
self.assertNotContains(response, _('Invite'))
def test_invite_as_user_action(self): def test_invite_as_user_action(self):
qs = Member.objects.all() qs = Member.objects.all()
url = reverse('admin:members_member_changelist') url = reverse('admin:members_member_changelist')
@ -597,6 +786,15 @@ class MemberAdminTestCase(AdminTestCase):
self.fritz._activity_score = i * 10 - 1 self.fritz._activity_score = i * 10 - 1
self.assertTrue('img' in self.admin.activity_score(self.fritz)) self.assertTrue('img' in self.admin.activity_score(self.fritz))
def test_unconfirm(self):
url = reverse('admin:members_member_changelist')
c = self._login('superuser')
response = c.post(url, data={'action': 'unconfirm',
'_selected_action': [self.fritz.pk]}, follow=True)
self.assertEqual(response.status_code, HTTPStatus.OK)
self.fritz.refresh_from_db()
self.assertFalse(self.fritz.confirmed)
class FreizeitTestCase(BasicMemberTestCase): class FreizeitTestCase(BasicMemberTestCase):
def setUp(self): def setUp(self):
@ -700,10 +898,10 @@ class FreizeitTestCase(BasicMemberTestCase):
def test_v32_fields(self): def test_v32_fields(self):
self.assertIn('Textfeld 61', self.ex2.v32_fields().keys()) self.assertIn('Textfeld 61', self.ex2.v32_fields().keys())
@skip("This currently throws a `RelatedObjectDoesNotExist` error.")
def test_no_statement(self): def test_no_statement(self):
self.assertEqual(self.ex.total_relative_costs, 0) self.assertEqual(self.ex.total_relative_costs, 0)
self.assertEqual(self.ex.payable_ljp_contributions, 0) self.assertEqual(self.ex.payable_ljp_contributions, 0)
self.assertEqual(self.ex.potential_ljp_contributions, 0)
def test_no_ljpproposal(self): def test_no_ljpproposal(self):
self.assertEqual(self.ex2.total_intervention_hours, 0) self.assertEqual(self.ex2.total_intervention_hours, 0)
@ -715,6 +913,8 @@ class FreizeitTestCase(BasicMemberTestCase):
def test_payable_ljp_contributions(self): def test_payable_ljp_contributions(self):
self.assertGreaterEqual(self.ex2.payable_ljp_contributions, 0) self.assertGreaterEqual(self.ex2.payable_ljp_contributions, 0)
self.st.ljp_to = self.fritz
self.assertGreaterEqual(self.ex2.payable_ljp_contributions, 0)
def test_get_tour_type(self): def test_get_tour_type(self):
self.ex2.tour_type = GEMEINSCHAFTS_TOUR self.ex2.tour_type = GEMEINSCHAFTS_TOUR
@ -743,6 +943,12 @@ class FreizeitTestCase(BasicMemberTestCase):
self.ex.end = timezone.datetime(2000, 1, 1, 12, 0, 0) self.ex.end = timezone.datetime(2000, 1, 1, 12, 0, 0)
self.assertEqual(self.ex.duration, 1) self.assertEqual(self.ex.duration, 1)
def test_generate_ljp_vbk_no_proposal_raises_error(self):
"""Test generate_ljp_vbk raises ValueError when excursion has no LJP proposal"""
with self.assertRaises(ValueError) as cm:
generate_ljp_vbk(self.ex)
self.assertIn("Excursion has no LJP proposal", str(cm.exception))
class PDFActionMixin: class PDFActionMixin:
def _test_pdf(self, name, pk, model='freizeit', invalid=False, username='superuser', post_data=None): def _test_pdf(self, name, pk, model='freizeit', invalid=False, username='superuser', post_data=None):
@ -799,6 +1005,11 @@ class FreizeitAdminTestCase(AdminTestCase, PDFActionMixin):
goal_strategy='my strategy', goal_strategy='my strategy',
not_bw_reason=LJPProposal.NOT_BW_ROOMS, not_bw_reason=LJPProposal.NOT_BW_ROOMS,
excursion=self.ex2) excursion=self.ex2)
self.st_ljp = Statement.objects.create(night_cost=11, subsidy_to=fr, ljp_to=fr,
excursion=self.ex2)
self.bill_no_proof = Bill.objects.create(statement=self.st_ljp, short_description='bla', explanation='bli',
amount=42.69, costs_covered=True, paid_by=fr)
def test_changelist(self): def test_changelist(self):
c = self._login('superuser') c = self._login('superuser')
@ -948,6 +1159,10 @@ class FreizeitAdminTestCase(AdminTestCase, PDFActionMixin):
}) })
self.assertEqual(response.status_code, HTTPStatus.OK) self.assertEqual(response.status_code, HTTPStatus.OK)
@skip('Throws `AttributeError`: `Freizeit.seminar_vbk` does not exist.')
def test_seminar_vbk(self):
self._test_pdf('seminar_vbk', self.ex.pk)
def test_crisis_intervention_list_post(self): def test_crisis_intervention_list_post(self):
self._test_pdf('crisis_intervention_list', self.ex.pk) self._test_pdf('crisis_intervention_list', self.ex.pk)
self._test_pdf('crisis_intervention_list', self.ex.pk, username='standard', invalid=True) self._test_pdf('crisis_intervention_list', self.ex.pk, username='standard', invalid=True)
@ -968,6 +1183,24 @@ class FreizeitAdminTestCase(AdminTestCase, PDFActionMixin):
self.assertEqual(response.status_code, HTTPStatus.OK) self.assertEqual(response.status_code, HTTPStatus.OK)
self.assertContains(response, _("No statement found. Please add a statement and then retry.")) self.assertContains(response, _("No statement found. Please add a statement and then retry."))
def test_finance_overview_invalid_post(self):
url = reverse('admin:members_freizeit_action', args=(self.ex2.pk,))
c = self._login('superuser')
# bill with missing proof
response = c.post(url, data={'finance_overview': '', 'apply': ''}, follow=True)
self.assertEqual(response.status_code, HTTPStatus.OK)
self.assertContains(response,
_("The excursion is configured to claim LJP contributions. In that case, for all bills, a proof must be uploaded. Please correct this and try again."))
# invalidate allowance_to
self.st_ljp.allowance_to.add(self.yl1)
response = c.post(url, data={'finance_overview': '', 'apply': ''}, follow=True)
self.assertEqual(response.status_code, HTTPStatus.OK)
self.assertContains(response,
_("The configured recipients of the allowance don't match the regulations. Please correct this and try again."))
def test_finance_overview_post(self): def test_finance_overview_post(self):
url = reverse('admin:members_freizeit_action', args=(self.ex.pk,)) url = reverse('admin:members_freizeit_action', args=(self.ex.pk,))
c = self._login('superuser') c = self._login('superuser')
@ -1008,12 +1241,18 @@ class MemberNoteListAdminTestCase(AdminTestCase, PDFActionMixin):
def test_wrong_action_membernotelist(self): def test_wrong_action_membernotelist(self):
return self._test_pdf('asdf', self.note.pk, invalid=True, model='membernotelist') return self._test_pdf('asdf', self.note.pk, invalid=True, model='membernotelist')
def test_change(self):
c = self._login('superuser')
url = reverse('admin:members_membernotelist_change', args=(self.note.pk,))
response = c.get(url)
self.assertEqual(response.status_code, 200, 'Response code is not 200.')
class MemberWaitingListAdminTestCase(AdminTestCase): class MemberWaitingListAdminTestCase(AdminTestCase):
def setUp(self): def setUp(self):
super().setUp(model=MemberWaitingList, admin=MemberWaitingListAdmin) super().setUp(model=MemberWaitingList, admin=MemberWaitingListAdmin)
self.waiter = MemberWaitingList.objects.create(**WAITER_DATA) self.waiter = MemberWaitingList.objects.create(**WAITER_DATA)
self.em = EmailAddress.objects.create(name='foobar')
for i in range(10): for i in range(10):
day = random.randint(1, 28) day = random.randint(1, 28)
month = random.randint(1, 12) month = random.randint(1, 12)
@ -1039,6 +1278,14 @@ class MemberWaitingListAdminTestCase(AdminTestCase):
self.assertEqual(m.birth_date_delta, m.age(), self.assertEqual(m.birth_date_delta, m.age(),
msg='Queryset based age calculation differs from python based age calculation for birth date {birth_date} compared to {today}.'.format(birth_date=m.birth_date, today=today)) msg='Queryset based age calculation differs from python based age calculation for birth date {birth_date} compared to {today}.'.format(birth_date=m.birth_date, today=today))
def test_invite_view_invalid(self):
c = self._login('superuser')
url = reverse('admin:members_memberwaitinglist_invite', args=(12312,))
response = c.get(url, follow=True)
self.assertEqual(response.status_code, HTTPStatus.OK)
self.assertContains(response, _("A waiter with this ID does not exist."))
def test_invite_view_post(self): def test_invite_view_post(self):
c = self._login('standard') c = self._login('standard')
url = reverse('admin:members_memberwaitinglist_invite', args=(self.waiter.pk,)) url = reverse('admin:members_memberwaitinglist_invite', args=(self.waiter.pk,))
@ -1050,6 +1297,9 @@ class MemberWaitingListAdminTestCase(AdminTestCase):
'group': 424242}) 'group': 424242})
self.assertEqual(response.status_code, HTTPStatus.FOUND) self.assertEqual(response.status_code, HTTPStatus.FOUND)
self.staff.contact_email = None
self.staff.save()
response = c.post(url, data={'apply': '', response = c.post(url, data={'apply': '',
'group': self.staff.pk}) 'group': self.staff.pk})
self.assertEqual(response.status_code, HTTPStatus.FOUND) self.assertEqual(response.status_code, HTTPStatus.FOUND)
@ -1075,7 +1325,10 @@ class MemberWaitingListAdminTestCase(AdminTestCase):
url = reverse('admin:members_memberwaitinglist_changelist') url = reverse('admin:members_memberwaitinglist_changelist')
qs = MemberWaitingList.objects.all() qs = MemberWaitingList.objects.all()
response = c.post(url, data={'action': 'ask_for_registration_action', response = c.post(url, data={'action': 'ask_for_registration_action',
'_selected_action': [qs[0].pk]}, follow=True) '_selected_action': [qs[0].pk],
'send': '',
'text_template': '',
'group': self.staff.pk}, follow=True)
self.assertEqual(response.status_code, HTTPStatus.OK) self.assertEqual(response.status_code, HTTPStatus.OK)
def test_age(self): def test_age(self):
@ -1092,6 +1345,19 @@ class MemberWaitingListAdminTestCase(AdminTestCase):
'_selected_action': [q.pk for q in qs]}, follow=True) '_selected_action': [q.pk for q in qs]}, follow=True)
self.assertEqual(response.status_code, HTTPStatus.OK) self.assertEqual(response.status_code, HTTPStatus.OK)
def test_request_mail_confirmation(self):
c = self._login('superuser')
url = reverse('admin:members_memberwaitinglist_changelist')
qs = MemberWaitingList.objects.all()
response = c.post(url, data={'action': 'request_mail_confirmation',
'_selected_action': [q.pk for q in qs]}, follow=True)
self.assertEqual(response.status_code, HTTPStatus.OK)
response = c.post(url, data={'action': 'request_required_mail_confirmation',
'_selected_action': [q.pk for q in qs]}, follow=True)
self.assertEqual(response.status_code, HTTPStatus.OK)
class MemberUnconfirmedAdminTestCase(AdminTestCase): class MemberUnconfirmedAdminTestCase(AdminTestCase):
def setUp(self): def setUp(self):
@ -1100,6 +1366,20 @@ class MemberUnconfirmedAdminTestCase(AdminTestCase):
for i in range(10): for i in range(10):
MemberUnconfirmedProxy.objects.create(**REGISTRATION_DATA, confirmed=False) MemberUnconfirmedProxy.objects.create(**REGISTRATION_DATA, confirmed=False)
def test_get_queryset(self):
request = self.factory.get('/')
request.user = User.objects.get(username='superuser')
qs = self.admin.get_queryset(request)
self.assertQuerysetEqual(qs, MemberUnconfirmedProxy.objects.all(), ordered=False)
request.user = User.objects.create(username='test', password='secret')
qs = self.admin.get_queryset(request)
self.assertQuerysetEqual(qs, MemberUnconfirmedProxy.objects.none(), ordered=False)
request.user = User.objects.get(username='standard')
qs = self.admin.get_queryset(request)
self.assertQuerysetEqual(qs, MemberUnconfirmedProxy.objects.none(), ordered=False)
def test_demote_to_waiter(self): def test_demote_to_waiter(self):
c = self._login('superuser') c = self._login('superuser')
url = reverse('admin:members_memberunconfirmedproxy_demote', args=(self.reg.pk,)) url = reverse('admin:members_memberunconfirmedproxy_demote', args=(self.reg.pk,))
@ -1157,6 +1437,16 @@ class MemberUnconfirmedAdminTestCase(AdminTestCase):
self.assertEqual(response.status_code, HTTPStatus.OK) self.assertEqual(response.status_code, HTTPStatus.OK)
self.assertContains(response, _("Successfully requested mail confirmation from selected registrations.")) self.assertContains(response, _("Successfully requested mail confirmation from selected registrations."))
def test_request_required_mail_confirmation(self):
c = self._login('superuser')
url = reverse('admin:members_memberunconfirmedproxy_changelist')
qs = MemberUnconfirmedProxy.objects.all()
response = c.post(url, data={'action': 'request_required_mail_confirmation',
'_selected_action': [qs[0].pk]}, follow=True)
self.assertEqual(response.status_code, HTTPStatus.OK)
self.assertContains(response,
_("Successfully re-requested missing mail confirmations from selected registrations."))
def test_changelist(self): def test_changelist(self):
c = self._login('standard') c = self._login('standard')
url = reverse('admin:members_memberunconfirmedproxy_changelist') url = reverse('admin:members_memberunconfirmedproxy_changelist')
@ -1754,6 +2044,14 @@ class TestRegistrationFilterTestCase(AdminTestCase):
fil = RegistrationFilter(None, {}, Member, self.admin) fil = RegistrationFilter(None, {}, Member, self.admin)
self.assertQuerysetEqual(fil.queryset(None, qs), qs, ordered=False) self.assertQuerysetEqual(fil.queryset(None, qs), qs, ordered=False)
def test_choices(self):
fil = RegistrationFilter(None, {'registration_complete': 'True'}, Member, self.admin)
request = RequestFactory().get("/", {})
request.user = User.objects.get(username='superuser')
changelist = self.admin.get_changelist_instance(request)
choices = list(fil.choices(changelist))
self.assertEqual(choices[0]['display'], _('Yes'))
@skip("Currently errors, because 'registration_complete' is not a field.") @skip("Currently errors, because 'registration_complete' is not a field.")
def test_queryset_filter(self): def test_queryset_filter(self):
qs = Member.objects.all() qs = Member.objects.all()
@ -1837,12 +2135,18 @@ class KlettertreffAdminTestCase(AdminTestCase):
'_selected_action': [kl.pk for kl in qs]}, follow=True) '_selected_action': [kl.pk for kl in qs]}, follow=True)
self.assertEqual(response.status_code, HTTPStatus.OK) self.assertEqual(response.status_code, HTTPStatus.OK)
# expect: success and filtered by group, this does not work @skip('Members are not filtered by group, because group attribute is retrieved from GET data.')
def test_overview_filtered(self):
qs = Klettertreff.objects.all()
url = reverse('admin:members_klettertreff_changelist')
# expect: success and filtered by group
c = self._login('superuser') c = self._login('superuser')
response = c.post(url, data={'action': 'overview', response = c.post(url, data={'action': 'overview',
'group__name': 'cool kids', 'group__name': 'cool kids',
'_selected_action': [kl.pk for kl in qs]}, follow=True) '_selected_action': [kl.pk for kl in qs]}, follow=True)
self.assertEqual(response.status_code, HTTPStatus.OK) self.assertEqual(response.status_code, HTTPStatus.OK)
self.assertNotContains(response, 'Lulla')
class GroupAdminTestCase(AdminTestCase): class GroupAdminTestCase(AdminTestCase):
@ -1856,6 +2160,16 @@ class GroupAdminTestCase(AdminTestCase):
response = c.get(url) response = c.get(url)
self.assertEqual(response.status_code, HTTPStatus.OK) self.assertEqual(response.status_code, HTTPStatus.OK)
def test_group_overview(self):
url = reverse('admin:members_group_action')
c = self._login('standard')
response = c.post(url, data={'group_overview': ''}, follow=True)
self.assertEqual(response.status_code, HTTPStatus.FORBIDDEN)
c = self._login('superuser')
response = c.post(url, data={'group_overview': ''}, follow=True)
self.assertEqual(response.status_code, HTTPStatus.OK)
class FilteredMemberFieldMixinTestCase(AdminTestCase): class FilteredMemberFieldMixinTestCase(AdminTestCase):
def setUp(self): def setUp(self):
@ -1980,6 +2294,14 @@ class GroupTestCase(BasicMemberTestCase):
self.assertTrue(self.alp.has_time_info()) self.assertTrue(self.alp.has_time_info())
self.assertFalse(self.spiel.has_time_info()) self.assertFalse(self.spiel.has_time_info())
def test_has_age_info(self):
self.assertTrue(self.alp.has_age_info())
self.assertFalse(self.jl.has_age_info())
def test_get_age_info(self):
self.assertGreater(len(self.alp.get_age_info()), 0)
self.assertEqual(self.jl.get_age_info(), "")
def test_get_invitation_text_template(self): def test_get_invitation_text_template(self):
alp_text = self.alp.get_invitation_text_template() alp_text = self.alp.get_invitation_text_template()
spiel_text = self.spiel.get_invitation_text_template() spiel_text = self.spiel.get_invitation_text_template()
@ -1991,6 +2313,9 @@ class GroupTestCase(BasicMemberTestCase):
self.assertIn(str(WEEKDAYS[self.alp.weekday][1]), alp_text) self.assertIn(str(WEEKDAYS[self.alp.weekday][1]), alp_text)
# check that method does not crash if no age info exists
self.assertGreater(len(self.jl.get_invitation_text_template()), 0)
class NewMemberOnListTestCase(BasicMemberTestCase): class NewMemberOnListTestCase(BasicMemberTestCase):
def setUp(self): def setUp(self):
@ -2070,3 +2395,51 @@ class EmergencyContactTestCase(TestCase):
def test_str(self): def test_str(self):
self.assertEqual(str(self.emergency_contact), str(self.member)) self.assertEqual(str(self.emergency_contact), str(self.member))
class InvitationToGroupAdminTestCase(AdminTestCase):
def setUp(self):
super().setUp(model=InvitationToGroup, admin=InvitationToGroupAdmin)
def test_has_add_permission(self):
self.assertFalse(self.admin.has_add_permission(None))
class MemberWaitingListFilterTestCase(AdminTestCase):
def setUp(self):
super().setUp(model=MemberWaitingList, admin=MemberWaitingListAdmin)
self.waiter = MemberWaitingList.objects.create(**WAITER_DATA)
self.waiter.invite_to_group(self.staff)
class AgeFilterTestCase(MemberWaitingListFilterTestCase):
def test_queryset_no_value(self):
fil = AgeFilter(None, {}, MemberWaitingList, self.admin)
qs = MemberWaitingList.objects.all()
self.assertQuerysetEqual(fil.queryset(None, qs), qs, ordered=False)
def test_queryset(self):
fil = AgeFilter(None, {'age': 12}, MemberWaitingList, self.admin)
request = self.factory.get('/')
request.user = User.objects.get(username='superuser')
qs = self.admin.get_queryset(request)
self.assertQuerysetEqual(fil.queryset(request, qs),
qs.filter(birth_date_delta=12),
ordered=False)
class InvitedToGroupFilterTestCase(MemberWaitingListFilterTestCase):
def test_queryset_no_value(self):
fil = InvitedToGroupFilter(None, {}, MemberWaitingList, self.admin)
qs = MemberWaitingList.objects.all()
self.assertQuerysetEqual(fil.queryset(None, qs), qs, ordered=False)
def test_queryset(self):
fil = InvitedToGroupFilter(None, {'pending_group_invitation': self.staff.pk},
MemberWaitingList, self.admin)
request = self.factory.get('/')
request.user = User.objects.get(username='superuser')
qs = self.admin.get_queryset(request)
self.assertQuerysetEqual(fil.queryset(request, qs).distinct(),
[self.waiter],
ordered=False)

@ -0,0 +1,179 @@
from django.test import TestCase
from django.utils import timezone
from django.contrib.auth.models import User
from ..models import Member, Group, Freizeit, DIVERSE, GEMEINSCHAFTS_TOUR, MemberTraining, TrainingCategory, LJPProposal
from ..rules import is_oneself, may_view, may_change, may_delete, is_own_training, is_leader_of_excursion, is_leader, statement_not_submitted, _is_leader
from finance.models import Statement
from mailer.models import EmailAddress
class RulesTestCase(TestCase):
def setUp(self):
# Create email address for groups
self.email_address = EmailAddress.objects.create(name='test@example.com')
# Create test users and members
self.user1 = User.objects.create_user(username='user1', email='user1@example.com')
self.member1 = Member.objects.create(
prename='Test',
lastname='Member1',
birth_date=timezone.now().date(),
email='member1@example.com',
gender=DIVERSE
)
self.user1.member = self.member1
self.user1.save()
self.user2 = User.objects.create_user(username='user2', email='user2@example.com')
self.member2 = Member.objects.create(
prename='Test',
lastname='Member2',
birth_date=timezone.now().date(),
email='member2@example.com',
gender=DIVERSE
)
self.user2.member = self.member2
self.user2.save()
self.user3 = User.objects.create_user(username='user3', email='user3@example.com')
self.member3 = Member.objects.create(
prename='Test',
lastname='Member3',
birth_date=timezone.now().date(),
email='member3@example.com',
gender=DIVERSE
)
self.user3.member = self.member3
self.user3.save()
# Create test group
self.group = Group.objects.create(name='Test Group')
self.group.contact_email = self.email_address
self.group.leiters.add(self.member2)
self.group.save()
# Create test excursion
self.excursion = Freizeit.objects.create(
name='Test Excursion',
tour_type=GEMEINSCHAFTS_TOUR,
kilometers_traveled=10,
difficulty=1
)
self.excursion.jugendleiter.add(self.member1)
self.excursion.groups.add(self.group)
self.excursion.save()
# Create training category and training
self.training_category = TrainingCategory.objects.create(
name='Test Training',
permission_needed=False
)
self.training = MemberTraining.objects.create(
member=self.member1,
title='Test Training',
category=self.training_category,
participated=True,
passed=True
)
# Create LJP proposal
self.ljp_proposal = LJPProposal.objects.create(
title='Test LJP',
excursion=self.excursion
)
# Create statement
self.statement_unsubmitted = Statement.objects.create(
short_description='Unsubmitted Statement',
excursion=self.excursion,
submitted=False
)
self.statement_submitted = Statement.objects.create(
short_description='Submitted Statement',
submitted=True
)
def test_is_oneself(self):
"""Test is_oneself rule - member can identify themselves."""
# Same member
self.assertTrue(is_oneself(self.user1, self.member1))
# Different members
self.assertFalse(is_oneself(self.user1, self.member2))
def test_may(self):
"""Test `may_` rules."""
self.assertTrue(may_view(self.user1, self.member1))
self.assertTrue(may_change(self.user1, self.member1))
self.assertTrue(may_delete(self.user1, self.member1))
def test_is_own_training(self):
"""Test is_own_training rule - member can access their own training."""
# Own training
self.assertTrue(is_own_training(self.user1, self.training))
# Other member's training
self.assertFalse(is_own_training(self.user2, self.training))
def test_is_leader_of_excursion(self):
"""Test is_leader_of_excursion rule for LJP proposals."""
# LJP proposal with excursion - member3 is not a leader
self.assertFalse(is_leader_of_excursion(self.user3, self.ljp_proposal))
# Directly pass an excursion
self.assertTrue(is_leader_of_excursion(self.user1, self.excursion))
def test_is_leader(self):
"""Test is_leader rule for excursions."""
# Direct excursion leader
self.assertTrue(is_leader(self.user1, self.excursion))
# Group leader (member2 is leader of group that is part of excursion)
self.assertTrue(is_leader(self.user2, self.excursion))
# member3 is unrelated
self.assertFalse(is_leader(self.user3, self.excursion))
# Test user without member attribute
user_no_member = User.objects.create_user(username='nomember', email='nomember@example.com')
self.assertFalse(is_leader(user_no_member, self.excursion))
# Test member without pk attribute
class MemberNoPk:
pass
member_no_pk = MemberNoPk()
self.assertFalse(_is_leader(member_no_pk, self.excursion))
# Test member with None pk
class MemberNonePk:
pk = None
member_none_pk = MemberNonePk()
self.assertFalse(_is_leader(member_none_pk, self.excursion))
def test_statement_not_submitted(self):
"""Test statement_not_submitted rule."""
# Unsubmitted statement with excursion
self.assertTrue(statement_not_submitted(self.user1, self.excursion))
# Submitted statement
self.excursion.statement = self.statement_submitted
self.excursion.save()
self.assertFalse(statement_not_submitted(self.user1, self.excursion))
# Excursion without statement
excursion_no_statement = Freizeit.objects.create(
name='No Statement Excursion',
tour_type=GEMEINSCHAFTS_TOUR,
kilometers_traveled=10,
difficulty=1
)
self.assertFalse(statement_not_submitted(self.user1, excursion_no_statement))
# Test the excursion.statement is None case
# Create a special test object to directly trigger
class ExcursionWithNoneStatement:
def __init__(self):
self.statement = None
# if excursion.statement is None: return False
self.assertFalse(statement_not_submitted(self.user1, ExcursionWithNoneStatement()))

@ -0,0 +1,141 @@
from unittest.mock import patch, MagicMock
from django.test import TestCase
from django.utils import timezone
from django.conf import settings
from ..models import MemberWaitingList, Freizeit, Group, DIVERSE, GEMEINSCHAFTS_TOUR
from ..tasks import ask_for_waiting_confirmation, send_crisis_intervention_list, send_notification_crisis_intervention_list
from mailer.models import EmailAddress
class TasksTestCase(TestCase):
def setUp(self):
# Create test email address
self.email_address = EmailAddress.objects.create(name='test@example.com')
# Create test group
self.group = Group.objects.create(name='Test Group')
self.group.contact_email = self.email_address
self.group.save()
# Create test waiters
now = timezone.now()
old_confirmation = now - timezone.timedelta(days=settings.WAITING_CONFIRMATION_FREQUENCY + 1)
old_reminder = now - timezone.timedelta(days=settings.CONFIRMATION_REMINDER_FREQUENCY + 1)
self.waiter1 = MemberWaitingList.objects.create(
prename='Test',
lastname='Waiter1',
birth_date=now.date(),
email='waiter1@example.com',
gender=DIVERSE,
last_wait_confirmation=old_confirmation,
last_reminder=old_reminder,
sent_reminders=0
)
self.waiter2 = MemberWaitingList.objects.create(
prename='Test',
lastname='Waiter2',
birth_date=now.date(),
email='waiter2@example.com',
gender=DIVERSE,
last_wait_confirmation=old_confirmation,
last_reminder=old_reminder,
sent_reminders=settings.MAX_REMINDER_COUNT - 1
)
# Create waiter that shouldn't receive reminder (too recent confirmation)
self.waiter3 = MemberWaitingList.objects.create(
prename='Test',
lastname='Waiter3',
birth_date=now.date(),
email='waiter3@example.com',
gender=DIVERSE,
last_wait_confirmation=now,
last_reminder=old_reminder,
sent_reminders=0
)
# Create waiter that shouldn't receive reminder (max reminders reached)
self.waiter4 = MemberWaitingList.objects.create(
prename='Test',
lastname='Waiter4',
birth_date=now.date(),
email='waiter4@example.com',
gender=DIVERSE,
last_wait_confirmation=old_confirmation,
last_reminder=old_reminder,
sent_reminders=settings.MAX_REMINDER_COUNT
)
# Create test excursions
today = timezone.now().date()
tomorrow = today + timezone.timedelta(days=1)
self.excursion_today_not_sent = Freizeit.objects.create(
name='Today Excursion 1',
date=timezone.now().replace(hour=10, minute=0, second=0, microsecond=0),
tour_type=GEMEINSCHAFTS_TOUR,
kilometers_traveled=10,
difficulty=1,
crisis_intervention_list_sent=False,
notification_crisis_intervention_list_sent=False
)
self.excursion_today_sent = Freizeit.objects.create(
name='Today Excursion 2',
date=timezone.now().replace(hour=14, minute=0, second=0, microsecond=0),
tour_type=GEMEINSCHAFTS_TOUR,
kilometers_traveled=10,
difficulty=1,
crisis_intervention_list_sent=True,
notification_crisis_intervention_list_sent=True
)
self.excursion_tomorrow_not_sent = Freizeit.objects.create(
name='Tomorrow Excursion 1',
date=(timezone.now() + timezone.timedelta(days=1)).replace(hour=10, minute=0, second=0, microsecond=0),
tour_type=GEMEINSCHAFTS_TOUR,
kilometers_traveled=10,
difficulty=1,
crisis_intervention_list_sent=False,
notification_crisis_intervention_list_sent=False
)
self.excursion_tomorrow_sent = Freizeit.objects.create(
name='Tomorrow Excursion 2',
date=(timezone.now() + timezone.timedelta(days=1)).replace(hour=14, minute=0, second=0, microsecond=0),
tour_type=GEMEINSCHAFTS_TOUR,
kilometers_traveled=10,
difficulty=1,
crisis_intervention_list_sent=True,
notification_crisis_intervention_list_sent=True
)
@patch.object(MemberWaitingList, 'ask_for_wait_confirmation')
def test_ask_for_waiting_confirmation(self, mock_ask):
"""Test ask_for_waiting_confirmation task calls correct waiters."""
result = ask_for_waiting_confirmation()
# Should call ask_for_wait_confirmation for waiter1 and waiter2 only
self.assertEqual(result, 2)
self.assertEqual(mock_ask.call_count, 2)
@patch.object(Freizeit, 'send_crisis_intervention_list')
def test_send_crisis_intervention_list(self, mock_send):
"""Test send_crisis_intervention_list task calls correct excursions."""
result = send_crisis_intervention_list()
# Should call send_crisis_intervention_list for today's excursions that haven't been sent
self.assertEqual(result, 1)
self.assertEqual(mock_send.call_count, 1)
@patch.object(Freizeit, 'notify_leaders_crisis_intervention_list')
def test_send_notification_crisis_intervention_list(self, mock_notify):
"""Test send_notification_crisis_intervention_list task calls correct excursions."""
result = send_notification_crisis_intervention_list()
# Should call notify_leaders_crisis_intervention_list for tomorrow's excursions that haven't been sent
self.assertEqual(result, 1)
self.assertEqual(mock_notify.call_count, 1)

@ -82,8 +82,9 @@ class BasicMemberTestCase(TestCase):
It creates a few groups and members with different attributes. It creates a few groups and members with different attributes.
""" """
def setUp(self): def setUp(self):
self.jl = Group.objects.create(name="Jugendleiter") self.jl = Group.objects.create(name="Jugendleiter", year_from=0, year_to=0)
self.alp = Group.objects.create(name="Alpenfuechse") self.alp = Group.objects.create(name="Alpenfuechse", year_from=1900, year_to=2000,
show_website=True)
self.spiel = Group.objects.create(name="Spielkinder") self.spiel = Group.objects.create(name="Spielkinder")
self.fritz = Member.objects.create(prename="Fritz", lastname="Wulter", birth_date=timezone.now().date(), self.fritz = Member.objects.create(prename="Fritz", lastname="Wulter", birth_date=timezone.now().date(),

@ -0,0 +1,110 @@
from unittest import skip
from http import HTTPStatus
from django.test import TestCase, Client
from django.urls import reverse
from django.utils import timezone
from django.utils.translation import gettext as _
from mailer.models import EmailAddress
from ..models import Member, Group, InvitationToGroup, MemberWaitingList, DIVERSE
class ConfirmInvitationViewTestCase(TestCase):
def setUp(self):
self.client = Client()
# Create an email address for the group
self.email_address = EmailAddress.objects.create(name='testmail')
# Create a test group
self.group = Group.objects.create(name='Test Group')
self.group.contact_email = self.email_address
self.group.save()
# Create a waiting list entry
self.waiter = MemberWaitingList.objects.create(
prename='Waiter',
lastname='User',
birth_date=timezone.now().date(),
email='waiter@example.com',
gender=DIVERSE,
wait_confirmation_key='test_wait_key',
wait_confirmation_key_expire=timezone.now() + timezone.timedelta(days=1)
)
# Create an invitation
self.invitation = InvitationToGroup.objects.create(
waiter=self.waiter,
group=self.group,
key='test_invitation_key',
date=timezone.now().date()
)
def test_confirm_invitation_get_valid_key(self):
"""Test GET request with valid key shows invitation confirmation page."""
url = reverse('members:confirm_invitation')
response = self.client.get(url, {'key': 'test_invitation_key'})
self.assertEqual(response.status_code, HTTPStatus.OK)
self.assertContains(response, _('Confirm trial group meeting invitation'))
self.assertContains(response, self.group.name)
def test_confirm_invitation_get_invalid_key(self):
"""Test GET request with invalid key shows invalid confirmation page."""
url = reverse('members:confirm_invitation')
# no key
response = self.client.get(url)
self.assertEqual(response.status_code, HTTPStatus.OK)
self.assertContains(response, _('This invitation is invalid or expired.'))
# invalid key
response = self.client.get(url, {'key': 'invalid_key'})
self.assertEqual(response.status_code, HTTPStatus.OK)
self.assertContains(response, _('This invitation is invalid or expired.'))
def test_confirm_invitation_get_rejected_invitation(self):
"""Test GET request with rejected invitation shows invalid confirmation page."""
self.invitation.rejected = True
self.invitation.save()
url = reverse('members:confirm_invitation')
response = self.client.get(url, {'key': self.invitation.key})
self.assertEqual(response.status_code, HTTPStatus.OK)
self.assertContains(response, _('This invitation is invalid or expired.'))
def test_confirm_invitation_get_expired_invitation(self):
"""Test GET request with expired invitation shows invalid confirmation page."""
# Set invitation date to more than 30 days ago to make it expired
self.invitation.date = timezone.now().date() - timezone.timedelta(days=31)
self.invitation.save()
url = reverse('members:confirm_invitation')
response = self.client.get(url, {'key': self.invitation.key})
self.assertEqual(response.status_code, HTTPStatus.OK)
self.assertContains(response, _('This invitation is invalid or expired.'))
def test_confirm_invitation_post_invalid_key(self):
"""Test POST request with invalid key shows invalid confirmation page."""
url = reverse('members:confirm_invitation')
# no key
response = self.client.post(url)
self.assertEqual(response.status_code, HTTPStatus.OK)
self.assertContains(response, _('This invitation is invalid or expired.'))
# invalid key
response = self.client.post(url, {'key': 'invalid_key'})
self.assertEqual(response.status_code, HTTPStatus.OK)
self.assertContains(response, _('This invitation is invalid or expired.'))
def test_confirm_invitation_post_valid_key(self):
"""Test POST request with valid key confirms invitation and shows success page."""
url = reverse('members:confirm_invitation')
response = self.client.post(url, {'key': self.invitation.key})
self.assertEqual(response.status_code, HTTPStatus.OK)
self.assertContains(response, _('Invitation confirmed'))
self.assertContains(response, self.group.name)
# Verify invitation was not marked as rejected (confirm() sets rejected=False)
self.invitation.refresh_from_db()
self.assertFalse(self.invitation.rejected)

@ -9,6 +9,7 @@ from members.models import Member, RegistrationPassword, MemberUnconfirmedProxy,
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from django.conf import settings from django.conf import settings
from django.views.decorators.cache import never_cache
from .pdf import render_tex, media_path from .pdf import render_tex, media_path
@ -505,6 +506,7 @@ def render_confirm_success(request, invitation):
'timeinfo': invitation.group.get_time_info()}) 'timeinfo': invitation.group.get_time_info()})
@never_cache
def confirm_invitation(request): def confirm_invitation(request):
if request.method == 'GET' and 'key' in request.GET: if request.method == 'GET' and 'key' in request.GET:
key = request.GET['key'] key = request.GET['key']

@ -4,8 +4,11 @@ from django.conf import settings
from django.templatetags.static import static from django.templatetags.static import static
from django.utils import timezone from django.utils import timezone
from django.core.files.uploadedfile import SimpleUploadedFile from django.core.files.uploadedfile import SimpleUploadedFile
from unittest import mock
from importlib import reload
from members.models import Member, Group, DIVERSE from members.models import Member, Group, DIVERSE
from startpage import urls
from .models import Post, Section, Image from .models import Post, Section, Image
@ -139,3 +142,16 @@ class ViewTestCase(BasicTestCase):
url = img.f.url url = img.f.url
response = c.get('/de' + url) response = c.get('/de' + url)
self.assertEqual(response.status_code, 200, 'Images on posts should be visible without login.') self.assertEqual(response.status_code, 200, 'Images on posts should be visible without login.')
def test_urlpatterns_with_redirect_url(self):
"""Test URL patterns when STARTPAGE_REDIRECT_URL is not empty"""
# Mock settings to have a non-empty STARTPAGE_REDIRECT_URL
with mock.patch.object(settings, 'STARTPAGE_REDIRECT_URL', 'https://example.com'):
# Reload the urls module to trigger the conditional urlpatterns creation
reload(urls)
# Check that urlpatterns contains the redirect view
url_names = [pattern.name for pattern in urls.urlpatterns if hasattr(pattern, 'name')]
self.assertIn('index', url_names)
self.assertEqual(len(urls.urlpatterns), 2) # Should have index and impressum only

Loading…
Cancel
Save