diff --git a/jdav_web/contrib/admin.py b/jdav_web/contrib/admin.py index 96a3838..b7e6ca1 100644 --- a/jdav_web/contrib/admin.py +++ b/jdav_web/contrib/admin.py @@ -1,14 +1,12 @@ import copy -from django.contrib.auth import get_permission_codename +from django.contrib import messages +from django.contrib.auth import get_permission_codename from django.core.exceptions import PermissionDenied -from django.contrib import admin, messages -from django.utils.translation import gettext_lazy as _ -from django.http import HttpResponse, HttpResponseRedirect -from django.urls import path, reverse from django.db import models -from django.contrib.admin import helpers, widgets -import rules.contrib.admin +from django.http import HttpResponseRedirect +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ from rules.permissions import perm_exists @@ -16,19 +14,36 @@ def decorate_admin_view(model, perm=None): """ Decorator for wrapping admin views. """ + def decorator(fun): def aux(self, request, object_id): try: obj = model.objects.get(pk=object_id) except model.DoesNotExist: - messages.error(request, _('%(modelname)s not found.') % {'modelname': self.opts.verbose_name}) - return HttpResponseRedirect(reverse('admin:%s_%s_changelist' % (self.opts.app_label, self.opts.model_name))) - permitted = self.has_change_permission(request, obj) if not perm else request.user.has_perm(perm) + messages.error( + request, _("%(modelname)s not found.") % {"modelname": self.opts.verbose_name} + ) + return HttpResponseRedirect( + reverse( + "admin:{}_{}_changelist".format(self.opts.app_label, self.opts.model_name) + ) + ) + permitted = ( + self.has_change_permission(request, obj) + if not perm + else request.user.has_perm(perm) + ) if not permitted: - messages.error(request, _('Insufficient permissions.')) - return HttpResponseRedirect(reverse('admin:%s_%s_changelist' % (self.opts.app_label, self.opts.model_name))) + messages.error(request, _("Insufficient permissions.")) + return HttpResponseRedirect( + reverse( + "admin:{}_{}_changelist".format(self.opts.app_label, self.opts.model_name) + ) + ) return fun(self, request, obj) + return aux + return decorator @@ -37,7 +52,7 @@ class FieldPermissionsAdminMixin: field_view_permissions = {} def may_view_field(self, field_desc, request, obj=None): - if not type(field_desc) is tuple: + if type(field_desc) is not tuple: field_desc = (field_desc,) for fd in field_desc: if fd not in self.field_view_permissions: @@ -47,37 +62,41 @@ class FieldPermissionsAdminMixin: return True def get_fieldsets(self, request, obj=None): - fieldsets = super(FieldPermissionsAdminMixin, self).get_fieldsets(request, obj) + fieldsets = super().get_fieldsets(request, obj) d = [] for title, attrs in fieldsets: - allowed = [f for f in attrs['fields'] if self.may_view_field(f, request, obj)] + allowed = [f for f in attrs["fields"] if self.may_view_field(f, request, obj)] if len(allowed) == 0: continue - d.append((title, dict(attrs, **{'fields': allowed}))) + d.append((title, dict(attrs, **{"fields": allowed}))) return d def get_fields(self, request, obj=None): - fields = super(FieldPermissionsAdminMixin, self).get_fields(request, obj) + fields = super().get_fields(request, obj) return [fd for fd in fields if self.may_view_field(fd, request, obj)] def get_readonly_fields(self, request, obj=None): - readonly_fields = super(FieldPermissionsAdminMixin, self).get_readonly_fields(request, obj) - return list(readonly_fields) +\ - [fd for fd, perm in self.field_change_permissions.items() if not request.user.has_perm(perm)] + readonly_fields = super().get_readonly_fields(request, obj) + return list(readonly_fields) + [ + fd + for fd, perm in self.field_change_permissions.items() + if not request.user.has_perm(perm) + ] class ChangeViewAdminMixin: def change_view(self, request, object_id, form_url="", extra_context=None): try: - return super(ChangeViewAdminMixin, self).change_view(request, object_id, - form_url=form_url, - extra_context=extra_context) + return super().change_view( + request, object_id, form_url=form_url, extra_context=extra_context + ) except PermissionDenied: opts = self.opts obj = self.model.objects.get(pk=object_id) - messages.error(request, - _("You are not allowed to view %(name)s.") % {'name': str(obj)}) - return HttpResponseRedirect(reverse('admin:%s_%s_changelist' % (opts.app_label, opts.model_name))) + messages.error(request, _("You are not allowed to view %(name)s.") % {"name": str(obj)}) + return HttpResponseRedirect( + reverse("admin:{}_{}_changelist".format(opts.app_label, opts.model_name)) + ) class FilteredQuerysetAdminMixin: @@ -91,28 +110,34 @@ class FilteredQuerysetAdminMixin: if ordering: qs = qs.order_by(*ordering) queryset = qs - list_global_perm = '%s.list_global_%s' % (self.opts.app_label, self.opts.model_name) + list_global_perm = "{}.list_global_{}".format(self.opts.app_label, self.opts.model_name) if request.user.has_perm(list_global_perm): - view_global_perm = '%s.view_global_%s' % (self.opts.app_label, self.opts.model_name) + view_global_perm = "{}.view_global_{}".format(self.opts.app_label, self.opts.model_name) if request.user.has_perm(view_global_perm): - return queryset - if hasattr(request.user, 'member'): + return queryset + if hasattr(request.user, "member"): return request.user.member.annotate_view_permission(queryset, model=self.model) return queryset.annotate(_viewable=models.Value(False)) - if not hasattr(request.user, 'member'): + if not hasattr(request.user, "member"): return self.model.objects.none() - return request.user.member.filter_queryset_by_permissions(queryset, annotate=True, model=self.model) + return request.user.member.filter_queryset_by_permissions( + queryset, annotate=True, model=self.model + ) + + +# class ObjectPermissionsInlineModelAdminMixin(rules.contrib.admin.ObjectPermissionsInlineModelAdminMixin): -#class ObjectPermissionsInlineModelAdminMixin(rules.contrib.admin.ObjectPermissionsInlineModelAdminMixin): -class CommonAdminMixin(FieldPermissionsAdminMixin, ChangeViewAdminMixin, FilteredQuerysetAdminMixin): +class CommonAdminMixin( + FieldPermissionsAdminMixin, ChangeViewAdminMixin, FilteredQuerysetAdminMixin +): def has_add_permission(self, request, obj=None): assert obj is None opts = self.opts codename = get_permission_codename("add_global", opts) - perm = "%s.%s" % (opts.app_label, codename) + perm = "{}.{}".format(opts.app_label, codename) return request.user.has_perm(perm, obj) def has_view_permission(self, request, obj=None): @@ -121,7 +146,7 @@ class CommonAdminMixin(FieldPermissionsAdminMixin, ChangeViewAdminMixin, Filtere codename = get_permission_codename("view", opts) else: codename = get_permission_codename("view_obj", opts) - perm = "%s.%s" % (opts.app_label, codename) + perm = "{}.{}".format(opts.app_label, codename) if perm_exists(perm): return request.user.has_perm(perm, obj) else: @@ -133,7 +158,7 @@ class CommonAdminMixin(FieldPermissionsAdminMixin, ChangeViewAdminMixin, Filtere codename = get_permission_codename("view", opts) else: codename = get_permission_codename("change_obj", opts) - return request.user.has_perm("%s.%s" % (opts.app_label, codename), obj) + return request.user.has_perm("{}.{}".format(opts.app_label, codename), obj) def has_delete_permission(self, request, obj=None): opts = self.opts @@ -141,7 +166,7 @@ class CommonAdminMixin(FieldPermissionsAdminMixin, ChangeViewAdminMixin, Filtere codename = get_permission_codename("delete_global", opts) else: codename = get_permission_codename("delete_obj", opts) - return request.user.has_perm("%s.%s" % (opts.app_label, codename), obj) + return request.user.has_perm("{}.{}".format(opts.app_label, codename), obj) def formfield_for_dbfield(self, db_field, request, **kwargs): """ @@ -176,7 +201,7 @@ class CommonAdminMixin(FieldPermissionsAdminMixin, ChangeViewAdminMixin, Filtere # extra HTML -- the "add other" interface -- to the end of the # rendered output. formfield can be None if it came from a # OneToOneField with parent_link=True or a M2M intermediary. - #if formfield and db_field.name not in self.raw_id_fields: + # if formfield and db_field.name not in self.raw_id_fields: # formfield.widget = widgets.RelatedFieldWidgetWrapper( # formfield.widget, # db_field.remote_field, @@ -198,13 +223,13 @@ class CommonAdminMixin(FieldPermissionsAdminMixin, ChangeViewAdminMixin, Filtere class CommonAdminInlineMixin(CommonAdminMixin): def has_add_permission(self, request, obj): - #assert obj is not None + # assert obj is not None if obj is None: return True if obj.pk is None: return True codename = get_permission_codename("add_obj", self.opts) - return request.user.has_perm('%s.%s' % (self.opts.app_label, codename), obj) + return request.user.has_perm("{}.{}".format(self.opts.app_label, codename), obj) def has_view_permission(self, request, obj=None): # pragma: no cover if obj is None: @@ -216,7 +241,7 @@ class CommonAdminInlineMixin(CommonAdminMixin): codename = get_permission_codename("view", opts) else: codename = get_permission_codename("view_obj", opts) - perm = "%s.%s" % (opts.app_label, codename) + perm = "{}.{}".format(opts.app_label, codename) if perm_exists(perm): return request.user.has_perm(perm, obj) else: @@ -234,7 +259,7 @@ class CommonAdminInlineMixin(CommonAdminMixin): opts = field.rel.to._meta break codename = get_permission_codename("change_obj", opts) - return request.user.has_perm("%s.%s" % (opts.app_label, codename), obj) + return request.user.has_perm("{}.{}".format(opts.app_label, codename), obj) def has_delete_permission(self, request, obj=None): # pragma: no cover if obj is None: diff --git a/jdav_web/contrib/apps.py b/jdav_web/contrib/apps.py index 5e87acc..7b992b7 100644 --- a/jdav_web/contrib/apps.py +++ b/jdav_web/contrib/apps.py @@ -2,5 +2,5 @@ from django.apps import AppConfig class ContribConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'contrib' + default_auto_field = "django.db.models.BigAutoField" + name = "contrib" diff --git a/jdav_web/contrib/media.py b/jdav_web/contrib/media.py index 5fd86e6..a9f0152 100644 --- a/jdav_web/contrib/media.py +++ b/jdav_web/contrib/media.py @@ -1,9 +1,9 @@ import os +from wsgiref.util import FileWrapper + +from django import template from django.conf import settings from django.http import HttpResponse -from django import template -from django.template.loader import get_template -from wsgiref.util import FileWrapper def find_template(template_name): @@ -27,12 +27,15 @@ def serve_media(filename, content_type): """ Serve the media file with the given `filename` as an HTTP response. """ - with open(media_path(filename), 'rb') as f: + with open(media_path(filename), "rb") as f: response = HttpResponse(FileWrapper(f)) - response['Content-Type'] = content_type + response["Content-Type"] = content_type # download other files than pdf, show pdfs in the browser - response['Content-Disposition'] = 'filename='+filename if content_type == 'application/pdf' else 'attachment; filename='+filename - + response["Content-Disposition"] = ( + "filename=" + filename + if content_type == "application/pdf" + else "attachment; filename=" + filename + ) return response diff --git a/jdav_web/contrib/models.py b/jdav_web/contrib/models.py index 58412b0..c8be5ba 100644 --- a/jdav_web/contrib/models.py +++ b/jdav_web/contrib/models.py @@ -1,10 +1,17 @@ from django.db import models -from rules.contrib.models import RulesModelBase, RulesModelMixin +from rules.contrib.models import RulesModelBase +from rules.contrib.models import RulesModelMixin + # Create your models here. class CommonModel(models.Model, RulesModelMixin, metaclass=RulesModelBase): class Meta: abstract = True default_permissions = ( - 'add_global', 'change_global', 'view_global', 'delete_global', 'list_global', 'view', + "add_global", + "change_global", + "view_global", + "delete_global", + "list_global", + "view", ) diff --git a/jdav_web/contrib/rules.py b/jdav_web/contrib/rules.py index b47c0f9..a8d690d 100644 --- a/jdav_web/contrib/rules.py +++ b/jdav_web/contrib/rules.py @@ -1,12 +1,12 @@ -from django.contrib.auth import get_permission_codename import rules.contrib.admin -import rules + def memberize_user(func): def inner(user, other): - if not hasattr(user, 'member'): + if not hasattr(user, "member"): return False return func(user.member, other) + return inner diff --git a/jdav_web/contrib/tests.py b/jdav_web/contrib/tests.py index 45adca4..85af444 100644 --- a/jdav_web/contrib/tests.py +++ b/jdav_web/contrib/tests.py @@ -1,28 +1,38 @@ -from datetime import datetime, timedelta -from decimal import Decimal -from django.test import TestCase, RequestFactory -from django.contrib.auth import get_user_model +from datetime import timedelta +from unittest.mock import Mock +from unittest.mock import patch + +from contrib.admin import CommonAdminMixin +from contrib.models import CommonModel +from contrib.rules import has_global_perm from django.contrib import admin -from django.db import models +from django.contrib.auth import get_user_model from django.core.exceptions import ValidationError -from django.core.files.uploadedfile import SimpleUploadedFile +from django.db import models +from django.test import RequestFactory +from django.test import TestCase from django.utils.translation import gettext_lazy as _ -from unittest.mock import Mock, patch -from rules.contrib.models import RulesModelMixin, RulesModelBase -from contrib.models import CommonModel -from contrib.rules import has_global_perm -from contrib.admin import CommonAdminMixin -from utils import file_size_validator, RestrictedFileField, cvt_to_decimal, get_member, normalize_name, normalize_filename, coming_midnight, mondays_until_nth +from rules.contrib.models import RulesModelBase +from rules.contrib.models import RulesModelMixin +from utils import file_size_validator +from utils import mondays_until_nth +from utils import RestrictedFileField User = get_user_model() + class CommonModelTestCase(TestCase): def test_common_model_abstract_base(self): """Test that CommonModel provides the correct meta attributes""" meta = CommonModel._meta self.assertTrue(meta.abstract) expected_permissions = ( - 'add_global', 'change_global', 'view_global', 'delete_global', 'list_global', 'view', + "add_global", + "change_global", + "view_global", + "delete_global", + "list_global", + "view", ) self.assertEqual(meta.default_permissions, expected_permissions) @@ -38,15 +48,13 @@ class CommonModelTestCase(TestCase): class GlobalPermissionRulesTestCase(TestCase): def setUp(self): self.user = User.objects.create_user( - username='testuser', - email='test@example.com', - password='testpass123' + username="testuser", email="test@example.com", password="testpass123" ) def test_has_global_perm_predicate_creation(self): """Test that has_global_perm creates a predicate function""" # has_global_perm is a decorator factory, not a direct predicate - predicate = has_global_perm('auth.add_user') + predicate = has_global_perm("auth.add_user") self.assertTrue(callable(predicate)) def test_has_global_perm_with_superuser(self): @@ -54,33 +62,32 @@ class GlobalPermissionRulesTestCase(TestCase): self.user.is_superuser = True self.user.save() - predicate = has_global_perm('auth.add_user') + predicate = has_global_perm("auth.add_user") result = predicate(self.user, None) self.assertTrue(result) def test_has_global_perm_with_regular_user(self): """Test that regular users don't automatically have global permissions""" - predicate = has_global_perm('auth.add_user') + predicate = has_global_perm("auth.add_user") result = predicate(self.user, None) self.assertFalse(result) class CommonAdminMixinTestCase(TestCase): def setUp(self): - self.user = User.objects.create_user(username='testuser', password='testpass') + 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()} - } + formfield_overrides = {models.ForeignKey: {"widget": Mock()}} # Create a mock model to use with the admin class TestModel: _meta = Mock() - _meta.app_label = 'test' + _meta.app_label = "test" admin_instance = TestAdmin(TestModel, admin.site) @@ -88,11 +95,11 @@ class CommonAdminMixinTestCase(TestCase): db_field = models.ForeignKey(User, on_delete=models.CASCADE) # Create a test request - request = RequestFactory().get('/') + 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') + result = admin_instance.formfield_for_dbfield(db_field, request, help_text="Test help text") # Verify that the formfield_overrides were used self.assertIsNotNone(result) @@ -101,9 +108,7 @@ class CommonAdminMixinTestCase(TestCase): class UtilsTestCase(TestCase): def setUp(self): self.user = User.objects.create_user( - username='testuser', - email='test@example.com', - password='testpass123' + username="testuser", email="test@example.com", password="testpass123" ) def test_file_size_validator_exceeds_limit(self): @@ -118,12 +123,14 @@ class UtilsTestCase(TestCase): validator(mock_file) # Check for the translated error message - expected_message = str(_('Please keep filesize under {} MiB. Current filesize: {:10.2f} MiB.').format(1, 2.00)) + expected_message = str( + _("Please keep filesize under {} MiB. Current filesize: {:10.2f} MiB.").format(1, 2.00) + ) self.assertIn(expected_message, str(cm.exception)) def test_restricted_file_field_content_type_not_supported(self): """Test RestrictedFileField when content type is not supported""" - field = RestrictedFileField(content_types=['image/jpeg']) + field = RestrictedFileField(content_types=["image/jpeg"]) # Create mock data with unsupported content type mock_data = Mock() @@ -131,12 +138,12 @@ class UtilsTestCase(TestCase): mock_data.file.content_type = "text/plain" # Mock the super().clean() to return our mock data - with patch.object(models.FileField, 'clean', return_value=mock_data): + with patch.object(models.FileField, "clean", return_value=mock_data): with self.assertRaises(ValidationError) as cm: field.clean("dummy") # Check for the translated error message - expected_message = str(_('Filetype not supported.')) + expected_message = str(_("Filetype not supported.")) self.assertIn(expected_message, str(cm.exception)) def test_restricted_file_field_size_exceeds_limit(self): @@ -150,12 +157,14 @@ class UtilsTestCase(TestCase): mock_data.file._size = 2 # 2 bytes, exceeds limit # Mock the super().clean() to return our mock data - with patch.object(models.FileField, 'clean', return_value=mock_data): + with patch.object(models.FileField, "clean", return_value=mock_data): with self.assertRaises(ValidationError) as cm: field.clean("dummy") # Check for the translated error message - expected_message = str(_('Please keep filesize under {}. Current filesize: {}').format(1, 2)) + expected_message = str( + _("Please keep filesize under {}. Current filesize: {}").format(1, 2) + ) self.assertIn(expected_message, str(cm.exception)) def test_mondays_until_nth(self): diff --git a/jdav_web/logindata/admin.py b/jdav_web/logindata/admin.py index 377e974..333df29 100644 --- a/jdav_web/logindata/admin.py +++ b/jdav_web/logindata/admin.py @@ -1,10 +1,16 @@ -from django.utils.translation import gettext_lazy as _ from django.contrib import admin -from django.contrib.auth.admin import UserAdmin as BaseUserAdmin, GroupAdmin as BaseAuthGroupAdmin -from django.contrib.auth.models import User as BaseUser, Group as BaseAuthGroup -from .models import AuthGroup, LoginDatum, RegistrationPassword +from django.contrib.auth.admin import GroupAdmin as BaseAuthGroupAdmin +from django.contrib.auth.admin import UserAdmin as BaseUserAdmin +from django.contrib.auth.models import Group as BaseAuthGroup +from django.contrib.auth.models import User as BaseUser +from django.utils.translation import gettext_lazy as _ from members.models import Member +from .models import AuthGroup +from .models import LoginDatum +from .models import RegistrationPassword + + # Register your models here. class AuthGroupAdmin(BaseAuthGroupAdmin): pass @@ -17,8 +23,8 @@ class UserInline(admin.StackedInline): class LoginDatumAdmin(BaseUserAdmin): - list_display = ('username', 'is_superuser') - #inlines = [UserInline] + list_display = ("username", "is_superuser") + # inlines = [UserInline] fieldsets = ( (None, {"fields": ("username", "password")}), ( @@ -45,6 +51,7 @@ class LoginDatumAdmin(BaseUserAdmin): ), ) + admin.site.unregister(BaseUser) admin.site.unregister(BaseAuthGroup) admin.site.register(LoginDatum, LoginDatumAdmin) diff --git a/jdav_web/logindata/apps.py b/jdav_web/logindata/apps.py index b1a5370..887724a 100644 --- a/jdav_web/logindata/apps.py +++ b/jdav_web/logindata/apps.py @@ -3,6 +3,6 @@ from django.utils.translation import gettext_lazy as _ class LoginDataConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'logindata' - verbose_name = _('Authentication') + default_auto_field = "django.db.models.BigAutoField" + name = "logindata" + verbose_name = _("Authentication") diff --git a/jdav_web/logindata/models.py b/jdav_web/logindata/models.py index c86deaa..fc5070f 100644 --- a/jdav_web/logindata/models.py +++ b/jdav_web/logindata/models.py @@ -1,39 +1,41 @@ -from django.utils.translation import gettext_lazy as _ +from django.contrib.auth.models import Group as BaseAuthGroup +from django.contrib.auth.models import User as BaseUser from django.db import models -from django.contrib.auth.admin import UserAdmin as BaseUserAdmin, GroupAdmin as BaseAuthGroupAdmin -from django.contrib.auth.models import User as BaseUser, Group as BaseAuthGroup +from django.utils.translation import gettext_lazy as _ class AuthGroup(BaseAuthGroup): class Meta: proxy = True - verbose_name = _('Permission group') - verbose_name_plural = _('Permission groups') + verbose_name = _("Permission group") + verbose_name_plural = _("Permission groups") class LoginDatum(BaseUser): class Meta: proxy = True - verbose_name = _('Login Datum') - verbose_name_plural = _('Login Data') + verbose_name = _("Login Datum") + verbose_name_plural = _("Login Data") class RegistrationPassword(models.Model): """ A password that can be used to register after inviting a member. """ - password = models.CharField(max_length=100, verbose_name=_('Password')) + + password = models.CharField(max_length=100, verbose_name=_("Password")) def __str__(self): return self.password class Meta: - verbose_name = _('Active registration password') - verbose_name_plural = _('Active registration passwords') + verbose_name = _("Active registration password") + verbose_name_plural = _("Active registration passwords") + def initial_user_setup(user, member): try: - standard_group = AuthGroup.objects.get(name='Standard') + standard_group = AuthGroup.objects.get(name="Standard") except AuthGroup.DoesNotExist: return False @@ -41,6 +43,6 @@ def initial_user_setup(user, member): user.save() user.groups.add(standard_group) member.user = user - member.invite_as_user_key = '' + member.invite_as_user_key = "" member.save() return True diff --git a/jdav_web/logindata/oauth.py b/jdav_web/logindata/oauth.py index ee0e5f3..da0d6a3 100644 --- a/jdav_web/logindata/oauth.py +++ b/jdav_web/logindata/oauth.py @@ -7,7 +7,7 @@ class CustomOAuth2Validator(OAuth2Validator): def get_additional_claims(self, request): if request.user.member: - context = {'email': request.user.member.email} + context = {"email": request.user.member.email} else: context = {} return dict(context, preferred_username=request.user.username) diff --git a/jdav_web/logindata/urls.py b/jdav_web/logindata/urls.py index 80d08b5..860b953 100644 --- a/jdav_web/logindata/urls.py +++ b/jdav_web/logindata/urls.py @@ -4,5 +4,5 @@ from . import views app_name = "logindata" urlpatterns = [ - re_path(r'^register', views.register , name='register'), + re_path(r"^register", views.register, name="register"), ] diff --git a/jdav_web/logindata/views.py b/jdav_web/logindata/views.py index 3af30b0..dad0aed 100644 --- a/jdav_web/logindata/views.py +++ b/jdav_web/logindata/views.py @@ -1,44 +1,46 @@ -from django import forms -from django.shortcuts import render +from django.contrib.auth.forms import UserCreationForm from django.http import HttpResponseRedirect -from django.utils.translation import gettext_lazy as _ +from django.shortcuts import render from django.urls import reverse -from django.contrib.auth.forms import UserCreationForm +from django.utils.translation import gettext_lazy as _ from members.models import Member -from .models import initial_user_setup, RegistrationPassword + +from .models import initial_user_setup +from .models import RegistrationPassword -def render_register_password(request, key, member, error_message=''): - return render(request, 'logindata/register_password.html', - context={'key': key, - 'member': member, - 'error_message': error_message}) +def render_register_password(request, key, member, error_message=""): + return render( + request, + "logindata/register_password.html", + context={"key": key, "member": member, "error_message": error_message}, + ) def render_register_failed(request): - return render(request, 'logindata/register_failed.html') + return render(request, "logindata/register_failed.html") def render_register_form(request, key, password, member, form): - return render(request, 'logindata/register_form.html', - context={'key': key, - 'password': password, - 'member': member, - 'form': form}) + return render( + request, + "logindata/register_form.html", + context={"key": key, "password": password, "member": member, "form": form}, + ) def render_register_success(request): - return render(request, 'logindata/register_success.html') + return render(request, "logindata/register_success.html") # Create your views here. def register(request): - if request.method == 'GET' and 'key' not in request.GET: - return HttpResponseRedirect(reverse('startpage:index')) - if request.method == 'POST' and 'key' not in request.POST: - return HttpResponseRedirect(reverse('startpage:index')) + if request.method == "GET" and "key" not in request.GET: + return HttpResponseRedirect(reverse("startpage:index")) + if request.method == "POST" and "key" not in request.POST: + return HttpResponseRedirect(reverse("startpage:index")) - key = request.GET['key'] if request.method == 'GET' else request.POST['key'] + key = request.GET["key"] if request.method == "GET" else request.POST["key"] if not key: return render_register_failed(request) try: @@ -46,17 +48,19 @@ def register(request): except (Member.DoesNotExist, Member.MultipleObjectsReturned): return render_register_failed(request) - if request.method == 'GET': - return render_register_password(request, request.GET['key'], member) + if request.method == "GET": + return render_register_password(request, request.GET["key"], member) - if 'password' not in request.POST: + if "password" not in request.POST: return render_register_failed(request) - password = request.POST['password'] + password = request.POST["password"] # check if the entered password is one of the active registration passwords if RegistrationPassword.objects.filter(password=password).count() == 0: - return render_register_password(request, key, member, error_message=_('You entered a wrong password.')) + return render_register_password( + request, key, member, error_message=_("You entered a wrong password.") + ) if "save" in request.POST: form = UserCreationForm(request.POST) @@ -70,6 +74,6 @@ def register(request): else: return render_register_failed(request) else: - prefill = {'username': member.suggested_username()} + prefill = {"username": member.suggested_username()} form = UserCreationForm(initial=prefill) return render_register_form(request, key, password, member, form) diff --git a/jdav_web/ludwigsburgalpin/admin.py b/jdav_web/ludwigsburgalpin/admin.py index 7f1e480..89d5f76 100644 --- a/jdav_web/ludwigsburgalpin/admin.py +++ b/jdav_web/ludwigsburgalpin/admin.py @@ -1,26 +1,23 @@ -import os - +import xlsxwriter +from contrib.media import ensure_media_dir +from contrib.media import media_path +from contrib.media import serve_media from django.contrib import admin -from wsgiref.util import FileWrapper -from django.http import HttpResponse -from django.conf import settings -from .models import Termin -from contrib.media import media_path, serve_media, ensure_media_dir -import xlsxwriter +from .models import Termin class TerminAdmin(admin.ModelAdmin): - list_display = ('title','start_date', 'end_date', 'group', 'category', 'responsible') - list_filter = ('group',) - ordering = ('start_date','end_date') - actions = ['make_overview'] + list_display = ("title", "start_date", "end_date", "group", "category", "responsible") + list_filter = ("group",) + ordering = ("start_date", "end_date") + actions = ["make_overview"] def make_overview(self, request, queryset): ensure_media_dir() - filename = 'termine.xlsx' + filename = "termine.xlsx" workbook = xlsxwriter.Workbook(media_path(filename)) - bold = workbook.add_format({'bold': True}) + bold = workbook.add_format({"bold": True}) worksheet = workbook.add_worksheet() worksheet.write(0, 0, "Titel", bold) worksheet.write(0, 1, "Untertitel", bold) @@ -44,30 +41,32 @@ class TerminAdmin(admin.ModelAdmin): worksheet.write(0, 19, "Telefonnummer", bold) worksheet.write(0, 20, "Emailadresse", bold) for row, termin in enumerate(queryset): - worksheet.write(row+2, 0, termin.title) - worksheet.write(row+2, 1, termin.subtitle) - worksheet.write(row+2, 2, termin.start_date.strftime('%d.%m.%Y')) - worksheet.write(row+2, 3, termin.end_date.strftime('%d.%m.%Y')) - worksheet.write(row+2, 4, termin.group) - worksheet.write(row+2, 5, termin.category) - worksheet.write(row+2, 6, termin.technik) - worksheet.write(row+2, 7, termin.condition) - worksheet.write(row+2, 8, termin.saison) - worksheet.write(row+2, 9, termin.eventart) - worksheet.write(row+2, 10, termin.klassifizierung) - worksheet.write(row+2, 11, termin.anforderung_hoehe) - worksheet.write(row+2, 12, termin.anforderung_strecke) - worksheet.write(row+2, 13, termin.anforderung_dauer) - worksheet.write(row+2, 14, termin.voraussetzungen) - worksheet.write(row+2, 15, termin.description) - worksheet.write(row+2, 16, termin.equipment) - worksheet.write(row+2, 17, termin.max_participants) - worksheet.write(row+2, 18, termin.responsible) - worksheet.write(row+2, 19, termin.phone) - worksheet.write(row+2, 20, termin.email) + worksheet.write(row + 2, 0, termin.title) + worksheet.write(row + 2, 1, termin.subtitle) + worksheet.write(row + 2, 2, termin.start_date.strftime("%d.%m.%Y")) + worksheet.write(row + 2, 3, termin.end_date.strftime("%d.%m.%Y")) + worksheet.write(row + 2, 4, termin.group) + worksheet.write(row + 2, 5, termin.category) + worksheet.write(row + 2, 6, termin.technik) + worksheet.write(row + 2, 7, termin.condition) + worksheet.write(row + 2, 8, termin.saison) + worksheet.write(row + 2, 9, termin.eventart) + worksheet.write(row + 2, 10, termin.klassifizierung) + worksheet.write(row + 2, 11, termin.anforderung_hoehe) + worksheet.write(row + 2, 12, termin.anforderung_strecke) + worksheet.write(row + 2, 13, termin.anforderung_dauer) + worksheet.write(row + 2, 14, termin.voraussetzungen) + worksheet.write(row + 2, 15, termin.description) + worksheet.write(row + 2, 16, termin.equipment) + worksheet.write(row + 2, 17, termin.max_participants) + worksheet.write(row + 2, 18, termin.responsible) + worksheet.write(row + 2, 19, termin.phone) + worksheet.write(row + 2, 20, termin.email) workbook.close() - return serve_media(filename, 'application/xlsx') + return serve_media(filename, "application/xlsx") + make_overview.short_description = "Termine in Excel Liste überführen" + # Register your models here. admin.site.register(Termin, TerminAdmin) diff --git a/jdav_web/ludwigsburgalpin/apps.py b/jdav_web/ludwigsburgalpin/apps.py index 203b4d9..47142b5 100644 --- a/jdav_web/ludwigsburgalpin/apps.py +++ b/jdav_web/ludwigsburgalpin/apps.py @@ -2,4 +2,4 @@ from django.apps import AppConfig class LudwigsburgalpinConfig(AppConfig): - name = 'ludwigsburgalpin' + name = "ludwigsburgalpin" diff --git a/jdav_web/ludwigsburgalpin/models.py b/jdav_web/ludwigsburgalpin/models.py index 9f57218..cef29b2 100644 --- a/jdav_web/ludwigsburgalpin/models.py +++ b/jdav_web/ludwigsburgalpin/models.py @@ -1,121 +1,128 @@ -from django.db import models from django.core.validators import MinValueValidator +from django.db import models GRUPPE = [ - ('ASG', 'Alpinsportgruppe'), - ('OGB', 'Ortsgruppe Bietigheim'), - ('OGV', 'Ortsgruppe Vaihingen'), - ('JUG', 'Jugend'), - ('FAM', 'Familie'), - ('Ü30', 'Ü30'), - ('MTB', 'Mountainbike'), - ('RA', 'RegioAktiv'), - ('SEK', 'Sektion'), + ("ASG", "Alpinsportgruppe"), + ("OGB", "Ortsgruppe Bietigheim"), + ("OGV", "Ortsgruppe Vaihingen"), + ("JUG", "Jugend"), + ("FAM", "Familie"), + ("Ü30", "Ü30"), + ("MTB", "Mountainbike"), + ("RA", "RegioAktiv"), + ("SEK", "Sektion"), ] KATEGORIE = [ - ('WAN', 'Wandern'), - ('BW', 'Bergwandern'), - ('KST', 'Klettersteig'), - ('KL', 'Klettern'), - ('SKI', 'Piste, Loipe'), - ('SCH', 'Schneeschuhgehen'), - ('ST', 'Skitour'), - ('STH', 'Skihochtour'), - ('HT', 'Hochtour'), - ('MTB', 'Montainbike'), - ('AUS', 'Ausbildung'), - ('SON', 'Sonstiges z.B. Treffen') + ("WAN", "Wandern"), + ("BW", "Bergwandern"), + ("KST", "Klettersteig"), + ("KL", "Klettern"), + ("SKI", "Piste, Loipe"), + ("SCH", "Schneeschuhgehen"), + ("ST", "Skitour"), + ("STH", "Skihochtour"), + ("HT", "Hochtour"), + ("MTB", "Montainbike"), + ("AUS", "Ausbildung"), + ("SON", "Sonstiges z.B. Treffen"), ] KONDITION = [ - ('gering', 'gering'), - ('mittel', 'mittel'), - ('groß', 'groß'), - ('sehr groß', 'sehr groß'), + ("gering", "gering"), + ("mittel", "mittel"), + ("groß", "groß"), + ("sehr groß", "sehr groß"), ] TECHNIK = [ - ('leicht', 'leicht'), - ('mittel', 'mittel'), - ('schwer', 'schwer'), - ('sehr schwer', 'sehr schwer'), + ("leicht", "leicht"), + ("mittel", "mittel"), + ("schwer", "schwer"), + ("sehr schwer", "sehr schwer"), ] SAISON = [ - ('ganzjährig','ganzjährig'), - ('Indoor', 'Indoor'), - ('Sommer', 'Sommer'), - ('Winter', 'Winter'), + ("ganzjährig", "ganzjährig"), + ("Indoor", "Indoor"), + ("Sommer", "Sommer"), + ("Winter", "Winter"), ] EVENTART = [ - ('Einzeltermin', 'Einzeltermin',), - ('Mehrtagesevent', 'Mehrtagesevent',), - ('Regelmäßiges Event/Training', 'Regelmäßiges Event/Training',), - ('Tagesevent', 'Tagesevent',), - ('Wochenendevent', 'Wochenendevent',), + ( + "Einzeltermin", + "Einzeltermin", + ), + ( + "Mehrtagesevent", + "Mehrtagesevent", + ), + ( + "Regelmäßiges Event/Training", + "Regelmäßiges Event/Training", + ), + ( + "Tagesevent", + "Tagesevent", + ), + ( + "Wochenendevent", + "Wochenendevent", + ), ] KLASSIFIZIERUNG = [ - ('Gemeinschaftstour', 'Gemeinschaftstour'), - ('Ausbildung', 'Ausbildung'), + ("Gemeinschaftstour", "Gemeinschaftstour"), + ("Ausbildung", "Ausbildung"), ] # Create your models here. class Termin(models.Model): - title = models.CharField('Titel', max_length=100) - subtitle = models.CharField('Untertitel', max_length=100, blank=True) - start_date = models.DateField('Von') - end_date = models.DateField('Bis') - group = models.CharField('Gruppe', - choices=GRUPPE, - max_length=100) - responsible = models.CharField('Organisator', max_length=100, blank=False) - phone = models.CharField(max_length=20, verbose_name='Telefonnumer', blank=True) - email = models.EmailField(max_length=100, verbose_name='Email', blank=False) - category = models.CharField('Kategorie', blank=False, choices=KATEGORIE, max_length=100, - default='SON') - condition = models.CharField('Kondition', blank=False, choices=KONDITION, max_length=100, - default='mittel') - technik = models.CharField('Technik', blank=False, choices=TECHNIK, max_length=100, - default='mittel') - saison = models.CharField('Saison', blank=False, choices=SAISON, max_length=100, - default='ganzjährig') - eventart = models.CharField('Eventart', blank=False, choices=EVENTART, max_length=100, - default='Einzeltermin') - klassifizierung = models.CharField('Klassifizierung', blank=False, choices=KLASSIFIZIERUNG, - max_length=100, - default='Gemeinschaftstour') - equipment = models.TextField('Ausrüstung', - blank=True) - voraussetzungen = models.TextField('Voraussetzungen', - blank=True) - description = models.TextField('Beschreibung', - blank=True) - max_participants = models.IntegerField('Max. Teilnehmerzahl', - blank=False, - validators=[ - MinValueValidator(1) - ], - default=10) - anforderung_hoehe = models.IntegerField('Höhenmeter in Meter', - blank=True, - validators=[ - MinValueValidator(0) - ], - default=0) - anforderung_strecke = models.IntegerField('Strecke in Kilometer', - blank=True, - validators=[ - MinValueValidator(0) - ], - default=0) - anforderung_dauer = models.IntegerField('Etappendauer in Stunden', - blank=True, - validators=[ - MinValueValidator(0) - ], - default=0) + title = models.CharField("Titel", max_length=100) + subtitle = models.CharField("Untertitel", max_length=100, blank=True) + start_date = models.DateField("Von") + end_date = models.DateField("Bis") + group = models.CharField("Gruppe", choices=GRUPPE, max_length=100) + responsible = models.CharField("Organisator", max_length=100, blank=False) + phone = models.CharField(max_length=20, verbose_name="Telefonnumer", blank=True) + email = models.EmailField(max_length=100, verbose_name="Email", blank=False) + category = models.CharField( + "Kategorie", blank=False, choices=KATEGORIE, max_length=100, default="SON" + ) + condition = models.CharField( + "Kondition", blank=False, choices=KONDITION, max_length=100, default="mittel" + ) + technik = models.CharField( + "Technik", blank=False, choices=TECHNIK, max_length=100, default="mittel" + ) + saison = models.CharField( + "Saison", blank=False, choices=SAISON, max_length=100, default="ganzjährig" + ) + eventart = models.CharField( + "Eventart", blank=False, choices=EVENTART, max_length=100, default="Einzeltermin" + ) + klassifizierung = models.CharField( + "Klassifizierung", + blank=False, + choices=KLASSIFIZIERUNG, + max_length=100, + default="Gemeinschaftstour", + ) + equipment = models.TextField("Ausrüstung", blank=True) + voraussetzungen = models.TextField("Voraussetzungen", blank=True) + description = models.TextField("Beschreibung", blank=True) + max_participants = models.IntegerField( + "Max. Teilnehmerzahl", blank=False, validators=[MinValueValidator(1)], default=10 + ) + anforderung_hoehe = models.IntegerField( + "Höhenmeter in Meter", blank=True, validators=[MinValueValidator(0)], default=0 + ) + anforderung_strecke = models.IntegerField( + "Strecke in Kilometer", blank=True, validators=[MinValueValidator(0)], default=0 + ) + anforderung_dauer = models.IntegerField( + "Etappendauer in Stunden", blank=True, validators=[MinValueValidator(0)], default=0 + ) def __str__(self): return "{} {}".format(self.title, str(self.group)) class Meta: - verbose_name = 'Termin' - verbose_name_plural = 'Termine' + verbose_name = "Termin" + verbose_name_plural = "Termine" diff --git a/jdav_web/ludwigsburgalpin/tests.py b/jdav_web/ludwigsburgalpin/tests.py index 38f15a9..bac0bf9 100644 --- a/jdav_web/ludwigsburgalpin/tests.py +++ b/jdav_web/ludwigsburgalpin/tests.py @@ -1,13 +1,21 @@ from http import HTTPStatus -from django.test import TestCase, RequestFactory -from django.utils import timezone +from django.conf import settings from django.contrib.admin.sites import AdminSite +from django.test import RequestFactory +from django.test import TestCase from django.urls import reverse -from django.conf import settings -from .models import Termin, GRUPPE, KATEGORIE, KONDITION, TECHNIK, SAISON,\ - EVENTART, KLASSIFIZIERUNG +from django.utils import timezone + from .admin import TerminAdmin +from .models import EVENTART +from .models import GRUPPE +from .models import KATEGORIE +from .models import KLASSIFIZIERUNG +from .models import KONDITION +from .models import SAISON +from .models import TECHNIK +from .models import Termin class BasicTerminTestCase(TestCase): @@ -15,68 +23,80 @@ class BasicTerminTestCase(TestCase): def setUp(self): for i in range(BasicTerminTestCase.TERMIN_NO): - Termin.objects.create(title='Foo {}'.format(i), - start_date=timezone.now().date(), - end_date=timezone.now().date(), - group=GRUPPE[0][0], - email=settings.TEST_MAIL, - category=KATEGORIE[0][0], - technik=TECHNIK[0][0], - max_participants=42, - anforderung_hoehe=10) + Termin.objects.create( + title="Foo {}".format(i), + start_date=timezone.now().date(), + end_date=timezone.now().date(), + group=GRUPPE[0][0], + email=settings.TEST_MAIL, + category=KATEGORIE[0][0], + technik=TECHNIK[0][0], + max_participants=42, + anforderung_hoehe=10, + ) class TerminAdminTestCase(BasicTerminTestCase): def test_str(self): t = Termin.objects.all()[0] - self.assertEqual(str(t), '{} {}'.format(t.title, str(t.group))) + self.assertEqual(str(t), "{} {}".format(t.title, str(t.group))) def test_make_overview(self): factory = RequestFactory() admin = TerminAdmin(Termin, AdminSite()) - url = reverse('admin:ludwigsburgalpin_termin_changelist') + url = reverse("admin:ludwigsburgalpin_termin_changelist") request = factory.get(url) response = admin.make_overview(request, Termin.objects.all()) - self.assertEqual(response['Content-Type'], 'application/xlsx', - 'The content-type of the generated overview should be an .xlsx file.') + self.assertEqual( + response["Content-Type"], + "application/xlsx", + "The content-type of the generated overview should be an .xlsx file.", + ) + class ViewTestCase(BasicTerminTestCase): def test_get_index(self): - url = reverse('ludwigsburgalpin:index') + url = reverse("ludwigsburgalpin:index") response = self.client.get(url) self.assertEqual(response.status_code, HTTPStatus.OK) def test_submit_termin(self): - url = reverse('ludwigsburgalpin:index') - response = self.client.post(url, data={ - 'title': 'My Title', - 'subtitle': 'My Subtitle', - 'start_date': '2024-01-01', - 'end_date': '2024-02-01', - 'group': GRUPPE[0][0], - 'category': KATEGORIE[0][0], - 'condition': KONDITION[0][0], - 'technik': TECHNIK[0][0], - 'saison': SAISON[0][0], - 'eventart': EVENTART[0][0], - 'klassifizierung': KLASSIFIZIERUNG[0][0], - 'anforderung_hoehe': 10, - 'anforderung_strecke': 10, - 'anforderung_dauer': 10, - 'max_participants': 100, - }) - t = Termin.objects.get(title='My Title') + url = reverse("ludwigsburgalpin:index") + response = self.client.post( + url, + data={ + "title": "My Title", + "subtitle": "My Subtitle", + "start_date": "2024-01-01", + "end_date": "2024-02-01", + "group": GRUPPE[0][0], + "category": KATEGORIE[0][0], + "condition": KONDITION[0][0], + "technik": TECHNIK[0][0], + "saison": SAISON[0][0], + "eventart": EVENTART[0][0], + "klassifizierung": KLASSIFIZIERUNG[0][0], + "anforderung_hoehe": 10, + "anforderung_strecke": 10, + "anforderung_dauer": 10, + "max_participants": 100, + }, + ) + t = Termin.objects.get(title="My Title") self.assertEqual(t.group, GRUPPE[0][0]) self.assertEqual(response.status_code, HTTPStatus.OK) self.assertContains(response, "Termin erfolgreich eingereicht", html=True) def test_submit_termin_invalid(self): - url = reverse('ludwigsburgalpin:index') + url = reverse("ludwigsburgalpin:index") # many required fields are missing - response = self.client.post(url, data={ - 'title': 'My Title', - }) + response = self.client.post( + url, + data={ + "title": "My Title", + }, + ) self.assertEqual(response.status_code, HTTPStatus.OK) self.assertContains(response, "Dieses Feld ist zwingend erforderlich.", html=True) diff --git a/jdav_web/ludwigsburgalpin/urls.py b/jdav_web/ludwigsburgalpin/urls.py index ae54963..bc72e3e 100644 --- a/jdav_web/ludwigsburgalpin/urls.py +++ b/jdav_web/ludwigsburgalpin/urls.py @@ -4,6 +4,6 @@ from . import views app_name = "ludwigsburgalpin" urlpatterns = [ - re_path(r'^$', views.index, name='index') + re_path(r"^$", views.index, name="index") # re_path(r'^subscribe', views.subscribe, name='subscribe'), ] diff --git a/jdav_web/ludwigsburgalpin/views.py b/jdav_web/ludwigsburgalpin/views.py index db90ac9..64e7ea5 100644 --- a/jdav_web/ludwigsburgalpin/views.py +++ b/jdav_web/ludwigsburgalpin/views.py @@ -1,100 +1,89 @@ -from django.shortcuts import render from django import forms -from django.http import HttpResponseRedirect -from django.contrib.admin import widgets from django.core.validators import MinValueValidator -from .models import Termin, GRUPPE, KATEGORIE, KONDITION, TECHNIK, SAISON, EVENTART, KLASSIFIZIERUNG +from django.shortcuts import render -datepicker = forms.TextInput(attrs={'class': 'datepicker'}) +from .models import EVENTART +from .models import GRUPPE +from .models import KATEGORIE +from .models import KLASSIFIZIERUNG +from .models import KONDITION +from .models import SAISON +from .models import TECHNIK +from .models import Termin +datepicker = forms.TextInput(attrs={"class": "datepicker"}) -class TerminForm(forms.Form): - - title = forms.CharField(label='Titel') - subtitle = forms.CharField(label='Untertitel') - start_date = forms.DateField(label='Von', - widget=datepicker) - end_date = forms.DateField(label='Bis', - widget=datepicker) - group = forms.ChoiceField(label='Gruppe', - required=True, - choices=GRUPPE) - category = forms.ChoiceField(label='Kategorie', required=True, choices=KATEGORIE) - condition = forms.ChoiceField(label='Kondition', required=True, choices=KONDITION) - technik = forms.ChoiceField(label='Technik', required=True, choices=TECHNIK) - saison = forms.ChoiceField(label='Saison', required=True, choices=SAISON) - eventart = forms.ChoiceField(label='Eventart', required=True, choices=EVENTART) - klassifizierung = forms.ChoiceField(label='Klassifizierung', required=True, choices=KLASSIFIZIERUNG) - anforderung_hoehe = forms.IntegerField(label='Höhenmeter in Metern', - required=True, - validators=[ - MinValueValidator(0) - ]) - anforderung_strecke = forms.IntegerField(label='Strecke in Kilometern', - required=True, - validators=[ - MinValueValidator(0) - ]) - anforderung_dauer = forms.IntegerField(label='Etappendauer in Stunden', - required=True, - validators=[ - MinValueValidator(0) - ]) - description = forms.CharField(label='Beschreibung', - widget=forms.Textarea, - required=False) - equipment = forms.CharField(label='Ausrüstung', - widget=forms.Textarea, - required=False) - voraussetzungen = forms.CharField(label='Voraussetzungen', - widget=forms.Textarea, - required=False) - max_participants = forms.IntegerField(label='Max. Teilnehmerzahl', - required=True, - validators=[ - MinValueValidator(1) - ]) - responsible = forms.CharField(label='Organisator', max_length=100, - required=False) - phone = forms.CharField(max_length=20, label='Telefonnumer', - required=False) - email = forms.EmailField(max_length=100, label='Email', - required=False) +class TerminForm(forms.Form): + title = forms.CharField(label="Titel") + subtitle = forms.CharField(label="Untertitel") + start_date = forms.DateField(label="Von", widget=datepicker) + end_date = forms.DateField(label="Bis", widget=datepicker) + group = forms.ChoiceField(label="Gruppe", required=True, choices=GRUPPE) + category = forms.ChoiceField(label="Kategorie", required=True, choices=KATEGORIE) + condition = forms.ChoiceField(label="Kondition", required=True, choices=KONDITION) + technik = forms.ChoiceField(label="Technik", required=True, choices=TECHNIK) + saison = forms.ChoiceField(label="Saison", required=True, choices=SAISON) + eventart = forms.ChoiceField(label="Eventart", required=True, choices=EVENTART) + klassifizierung = forms.ChoiceField( + label="Klassifizierung", required=True, choices=KLASSIFIZIERUNG + ) + anforderung_hoehe = forms.IntegerField( + label="Höhenmeter in Metern", required=True, validators=[MinValueValidator(0)] + ) + anforderung_strecke = forms.IntegerField( + label="Strecke in Kilometern", required=True, validators=[MinValueValidator(0)] + ) + anforderung_dauer = forms.IntegerField( + label="Etappendauer in Stunden", required=True, validators=[MinValueValidator(0)] + ) + description = forms.CharField(label="Beschreibung", widget=forms.Textarea, required=False) + equipment = forms.CharField(label="Ausrüstung", widget=forms.Textarea, required=False) + voraussetzungen = forms.CharField( + label="Voraussetzungen", widget=forms.Textarea, required=False + ) + max_participants = forms.IntegerField( + label="Max. Teilnehmerzahl", required=True, validators=[MinValueValidator(1)] + ) + responsible = forms.CharField(label="Organisator", max_length=100, required=False) + phone = forms.CharField(max_length=20, label="Telefonnumer", required=False) + email = forms.EmailField(max_length=100, label="Email", required=False) # Create your views here. def index(request, *args): - if request.method == 'POST': + if request.method == "POST": form = TerminForm(request.POST) if form.is_valid(): - termin = Termin(title=form.cleaned_data["title"], - subtitle=form.cleaned_data["subtitle"], - start_date=form.cleaned_data["start_date"], - end_date=form.cleaned_data["end_date"], - group=form.cleaned_data["group"], - responsible=form.cleaned_data["responsible"], - phone=form.cleaned_data["phone"], - email=form.cleaned_data["email"], - category=form.cleaned_data["category"], - condition=form.cleaned_data["condition"], - technik=form.cleaned_data["technik"], - saison=form.cleaned_data["saison"], - eventart=form.cleaned_data["eventart"], - klassifizierung=form.cleaned_data["klassifizierung"], - equipment=form.cleaned_data["equipment"], - voraussetzungen=form.cleaned_data["voraussetzungen"], - max_participants=form.cleaned_data["max_participants"], - anforderung_hoehe=form.cleaned_data["anforderung_hoehe"], - anforderung_strecke=form.cleaned_data["anforderung_strecke"], - anforderung_dauer=form.cleaned_data["anforderung_dauer"], - description=form.cleaned_data["description"]) + termin = Termin( + title=form.cleaned_data["title"], + subtitle=form.cleaned_data["subtitle"], + start_date=form.cleaned_data["start_date"], + end_date=form.cleaned_data["end_date"], + group=form.cleaned_data["group"], + responsible=form.cleaned_data["responsible"], + phone=form.cleaned_data["phone"], + email=form.cleaned_data["email"], + category=form.cleaned_data["category"], + condition=form.cleaned_data["condition"], + technik=form.cleaned_data["technik"], + saison=form.cleaned_data["saison"], + eventart=form.cleaned_data["eventart"], + klassifizierung=form.cleaned_data["klassifizierung"], + equipment=form.cleaned_data["equipment"], + voraussetzungen=form.cleaned_data["voraussetzungen"], + max_participants=form.cleaned_data["max_participants"], + anforderung_hoehe=form.cleaned_data["anforderung_hoehe"], + anforderung_strecke=form.cleaned_data["anforderung_strecke"], + anforderung_dauer=form.cleaned_data["anforderung_dauer"], + description=form.cleaned_data["description"], + ) termin.save() return published(request) else: form = TerminForm() - return render(request, 'ludwigsburgalpin/termine.html', {'form': form.as_table()}) + return render(request, "ludwigsburgalpin/termine.html", {"form": form.as_table()}) def published(request): - return render(request, 'ludwigsburgalpin/published.html') + return render(request, "ludwigsburgalpin/published.html") diff --git a/jdav_web/utils.py b/jdav_web/utils.py index 669df4e..03a610c 100644 --- a/jdav_web/utils.py +++ b/jdav_web/utils.py @@ -1,11 +1,15 @@ -from datetime import datetime, timedelta +import logging +import unicodedata +from datetime import datetime +from datetime import timedelta +from decimal import Decimal +from decimal import ROUND_HALF_DOWN + +from django.core.exceptions import ValidationError from django.db import models from django.utils import timezone -from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ -from decimal import Decimal, ROUND_HALF_DOWN -import unicodedata -import logging + logger = logging.getLogger(__name__) @@ -14,18 +18,20 @@ def file_size_validator(max_upload_size): Returns a function checking if the supplied file has file size less or equal than `max_upload_size` in MB. """ + def check_file_size(value): limit = max_upload_size * 1024 * 1024 if value.size > limit: - raise ValidationError(_('Please keep filesize under {} MiB. ' - 'Current filesize: ' - '{:10.2f} MiB.').format(max_upload_size, - value.size / 1024 / 1024)) + raise ValidationError( + _("Please keep filesize under {} MiB. Current filesize: {:10.2f} MiB.").format( + max_upload_size, value.size / 1024 / 1024 + ) + ) + return check_file_size class RestrictedFileField(models.FileField): - def __init__(self, *args, **kwargs): if "max_upload_size" in kwargs: self.max_upload_size = kwargs.pop("max_upload_size") @@ -36,32 +42,33 @@ class RestrictedFileField(models.FileField): else: self.content_types = None - super(RestrictedFileField, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.validators = [file_size_validator(self.max_upload_size)] def clean(self, *args, **kwargs): - data = super(RestrictedFileField, self).clean(*args, **kwargs) + data = super().clean(*args, **kwargs) f = data.file try: content_type = f.content_type if self.content_types is not None and content_type not in self.content_types: - raise ValidationError(_('Filetype not supported.')) + raise ValidationError(_("Filetype not supported.")) if self.max_upload_size is not None and f._size > self.max_upload_size: - raise ValidationError(_('Please keep filesize under {}. ' - 'Current filesize: ' - '{}').format(self.max_upload_size, - f._size)) + raise ValidationError( + _("Please keep filesize under {}. Current filesize: {}").format( + self.max_upload_size, f._size + ) + ) except AttributeError as e: logger.warning(e) return data def cvt_to_decimal(f): - return Decimal(f).quantize(Decimal('.01'), rounding=ROUND_HALF_DOWN) + return Decimal(f).quantize(Decimal(".01"), rounding=ROUND_HALF_DOWN) def get_member(request): - if not hasattr(request.user, 'member'): + if not hasattr(request.user, "member"): return None else: return request.user.member @@ -69,10 +76,10 @@ def get_member(request): def normalize_name(raw, nospaces=True, noumlaut=True): if noumlaut: - raw = raw.replace('ö', 'oe').replace('ä', 'ae').replace('ü', 'ue') + raw = raw.replace("ö", "oe").replace("ä", "ae").replace("ü", "ue") if nospaces: - raw = raw.replace(' ', '_') - return unicodedata.normalize('NFKD', raw).encode('ascii', 'ignore').decode('ascii') + raw = raw.replace(" ", "_") + return unicodedata.normalize("NFKD", raw).encode("ascii", "ignore").decode("ascii") def normalize_filename(filename, append_date=True, date=None): @@ -80,20 +87,27 @@ def normalize_filename(filename, append_date=True, date=None): date = datetime.today() if date: filename = filename + "_" + date.strftime("%d_%m_%Y") - filename = filename.replace(' ', '_').replace('&', '').replace('/', '_') + filename = filename.replace(" ", "_").replace("&", "").replace("/", "_") # drop umlauts, accents etc. - return unicodedata.normalize('NFKD', filename).encode('ASCII', 'ignore').decode() + return unicodedata.normalize("NFKD", filename).encode("ASCII", "ignore").decode() def coming_midnight(): base = timezone.now() + timezone.timedelta(days=1) - return timezone.datetime(year=base.year, month=base.month, day=base.day, - hour=0, minute=0, second=0, microsecond=0, - tzinfo=base.tzinfo) + return timezone.datetime( + year=base.year, + month=base.month, + day=base.day, + hour=0, + minute=0, + second=0, + microsecond=0, + tzinfo=base.tzinfo, + ) def mondays_until_nth(n): - """ Returns a list of dates for the next n Mondays, starting from the next Monday. + """Returns a list of dates for the next n Mondays, starting from the next Monday. This functions aids in the generation of weekly schedules or reports.""" today = datetime.today() next_monday = today + timedelta(days=(7 - today.weekday()) % 7 or 7)