diff --git a/aleksis/core/__init__.py b/aleksis/core/__init__.py index 587b829c611913af41c69f2734a48235956ff787..8025958440c84991c6d28b463bde735bae2efcbc 100644 --- a/aleksis/core/__init__.py +++ b/aleksis/core/__init__.py @@ -1,7 +1,7 @@ -import pkg_resources - from django.utils.translation import gettext_lazy as _ +import pkg_resources + try: from .celery import app as celery_app except ModuleNotFoundError: diff --git a/aleksis/core/admin.py b/aleksis/core/admin.py index 85fc6756b86e72c20d277326ca1f95d35ca5ad4c..383b35ad2c5e244a766b7abfe35287b16dd60cc3 100644 --- a/aleksis/core/admin.py +++ b/aleksis/core/admin.py @@ -4,16 +4,15 @@ from reversion.admin import VersionAdmin from .mixins import BaseModelAdmin from .models import ( - Group, - Person, Activity, - Notification, Announcement, AnnouncementRecipient, CustomMenuItem, + Group, + Notification, + Person, ) - admin.site.register(Person, VersionAdmin) admin.site.register(Group, VersionAdmin) admin.site.register(Activity, VersionAdmin) diff --git a/aleksis/core/apps.py b/aleksis/core/apps.py index 492da4a3e808583bd8ca820a42812a1d33d6f240..921d12ff785c39c76ac0af63cdb73d9732faaf80 100644 --- a/aleksis/core/apps.py +++ b/aleksis/core/apps.py @@ -7,7 +7,11 @@ from django.utils.translation import gettext_lazy as _ from dynamic_preferences.registries import preference_models -from .registries import group_preferences_registry, person_preferences_registry, site_preferences_registry +from .registries import ( + group_preferences_registry, + person_preferences_registry, + site_preferences_registry, +) from .util.apps import AppConfig from .util.core_helpers import has_person from .util.sass_helpers import clean_scss @@ -34,9 +38,9 @@ class CoreConfig(AppConfig): def ready(self): super().ready() - SitePreferenceModel = self.get_model('SitePreferenceModel') - PersonPreferenceModel = self.get_model('PersonPreferenceModel') - GroupPreferenceModel = self.get_model('GroupPreferenceModel') + SitePreferenceModel = self.get_model("SitePreferenceModel") + PersonPreferenceModel = self.get_model("PersonPreferenceModel") + GroupPreferenceModel = self.get_model("GroupPreferenceModel") preference_models.register(SitePreferenceModel, site_preferences_registry) preference_models.register(PersonPreferenceModel, person_preferences_registry) @@ -52,16 +56,15 @@ class CoreConfig(AppConfig): **kwargs, ) -> None: if section == "theme": - if name in ("primary", "secondary"): + if name in ("primary", "secondary"): clean_scss() elif name in ("favicon", "pwa_icon"): from favicon.models import Favicon # noqa - Favicon.on_site.update_or_create(title=name, - defaults={ - "isFavicon": name == "favicon", - "faviconImage": new_value, - }) + Favicon.on_site.update_or_create( + title=name, + defaults={"isFavicon": name == "favicon", "faviconImage": new_value,}, + ) def post_migrate( self, diff --git a/aleksis/core/celery.py b/aleksis/core/celery.py index 2f4dce954576a5d688311c466bc2b9fb4ad4e151..27a67c539babfc61701cc9521b42187ebebc5787 100644 --- a/aleksis/core/celery.py +++ b/aleksis/core/celery.py @@ -1,8 +1,9 @@ import os + from celery import Celery os.environ.setdefault("DJANGO_SETTINGS_MODULE", "aleksis.core.settings") app = Celery("aleksis") # noqa -app.config_from_object('django.conf:settings', namespace='CELERY') +app.config_from_object("django.conf:settings", namespace="CELERY") app.autodiscover_tasks() diff --git a/aleksis/core/filters.py b/aleksis/core/filters.py index 6e813e888c548254fc7d153bd641b8c857523293..12265c33c6b9e0d94a9dfb5e87d9111c721fc4ad 100644 --- a/aleksis/core/filters.py +++ b/aleksis/core/filters.py @@ -1,4 +1,4 @@ -from django_filters import FilterSet, CharFilter +from django_filters import CharFilter, FilterSet from material import Layout, Row diff --git a/aleksis/core/forms.py b/aleksis/core/forms.py index 92faded47a9da5912dbf2219ecb764737cd3b745..a7989eb4842e4e90291a7b92ab8bdee4805e71ba 100644 --- a/aleksis/core/forms.py +++ b/aleksis/core/forms.py @@ -1,4 +1,4 @@ -from datetime import time, datetime +from datetime import datetime, time from typing import Optional from django import forms @@ -10,11 +10,15 @@ from django.utils.translation import gettext_lazy as _ from django_select2.forms import ModelSelect2MultipleWidget, Select2Widget from dynamic_preferences.forms import PreferenceForm -from material import Layout, Fieldset, Row +from material import Fieldset, Layout, Row from .mixins import ExtensibleForm -from .models import Group, Person, Announcement, AnnouncementRecipient -from .registries import site_preferences_registry, person_preferences_registry, group_preferences_registry +from .models import Announcement, AnnouncementRecipient, Group, Person +from .registries import ( + group_preferences_registry, + person_preferences_registry, + site_preferences_registry, +) class PersonAccountForm(forms.ModelForm): diff --git a/aleksis/core/menus.py b/aleksis/core/menus.py index 985273c9621f88ba4ccd4bec1a8cd58b1ae57d3d..c91287d016f24487163a332376b2cc95a9a78cf0 100644 --- a/aleksis/core/menus.py +++ b/aleksis/core/menus.py @@ -41,9 +41,7 @@ MENUS = { "name": _("2FA"), "url": "two_factor:profile", "icon": "phonelink_lock", - "validators": [ - "menu_generator.validators.is_authenticated", - ], + "validators": ["menu_generator.validators.is_authenticated",], }, { "name": _("Me"), @@ -78,7 +76,10 @@ MENUS = { "url": "announcements", "icon": "announcement", "validators": [ - ("aleksis.core.util.predicates.permission_validator", "core.view_announcements"), + ( + "aleksis.core.util.predicates.permission_validator", + "core.view_announcements", + ), ], }, { @@ -94,7 +95,10 @@ MENUS = { "url": "system_status", "icon": "power_settings_new", "validators": [ - ("aleksis.core.util.predicates.permission_validator", "core.view_system_status"), + ( + "aleksis.core.util.predicates.permission_validator", + "core.view_system_status", + ), ], }, { @@ -110,16 +114,17 @@ MENUS = { "url": "preferences_site", "icon": "settings", "validators": [ - ("aleksis.core.util.predicates.permission_validator", "core.change_site_preferences"), + ( + "aleksis.core.util.predicates.permission_validator", + "core.change_site_preferences", + ), ], }, { "name": _("Backend Admin"), "url": "admin:index", "icon": "settings", - "validators": [ - "menu_generator.validators.is_superuser", - ], + "validators": ["menu_generator.validators.is_superuser",], }, ], }, @@ -128,7 +133,9 @@ MENUS = { "url": "#", "icon": "people", "root": True, - "validators": [("aleksis.core.util.predicates.permission_validator", "core.view_people_menu")], + "validators": [ + ("aleksis.core.util.predicates.permission_validator", "core.view_people_menu") + ], "submenu": [ { "name": _("Persons"), @@ -151,7 +158,10 @@ MENUS = { "url": "persons_accounts", "icon": "person_add", "validators": [ - ("aleksis.core.util.predicates.permission_validator", "core.link_persons_accounts") + ( + "aleksis.core.util.predicates.permission_validator", + "core.link_persons_accounts", + ) ], }, { @@ -159,7 +169,10 @@ MENUS = { "url": "groups_child_groups", "icon": "group_add", "validators": [ - ("aleksis.core.util.predicates.permission_validator", "core.assign_child_groups_to_groups") + ( + "aleksis.core.util.predicates.permission_validator", + "core.assign_child_groups_to_groups", + ) ], }, ], @@ -170,7 +183,10 @@ MENUS = { "name": _("Assign child groups to groups"), "url": "groups_child_groups", "validators": [ - ("aleksis.core.util.predicates.permission_validator", "core.assign_child_groups_to_groups") + ( + "aleksis.core.util.predicates.permission_validator", + "core.assign_child_groups_to_groups", + ) ], }, ], diff --git a/aleksis/core/migrations/0001_initial.py b/aleksis/core/migrations/0001_initial.py index 15eb9d297b01a571c60120dd9ce64fb8bbeca446..7a31953baef6db56de5898e53b0ac1da9ddacb5f 100644 --- a/aleksis/core/migrations/0001_initial.py +++ b/aleksis/core/migrations/0001_initial.py @@ -1,13 +1,15 @@ # Generated by Django 3.0.2 on 2020-01-03 19:18 -import aleksis.core.mixins -from django.conf import settings import django.contrib.postgres.fields.jsonb -from django.db import migrations, models import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + import image_cropping.fields import phonenumber_field.modelfields +import aleksis.core.mixins + class Migration(migrations.Migration): initial = True diff --git a/aleksis/core/migrations/0002_activity_notification.py b/aleksis/core/migrations/0002_activity_notification.py index 0b05696e31ab754c009dd64743b06cd2d73deaa1..8df1be8adbc034dd0c69f7e378697da39b0161c5 100644 --- a/aleksis/core/migrations/0002_activity_notification.py +++ b/aleksis/core/migrations/0002_activity_notification.py @@ -1,9 +1,9 @@ # Generated by Django 3.0.2 on 2020-01-03 19:19 -from django.conf import settings -from django.db import migrations, models import django.db.models.deletion import django.utils.timezone +from django.conf import settings +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/aleksis/core/migrations/0003_add_verbose_names.py b/aleksis/core/migrations/0003_add_verbose_names.py index 68e2e00302fdfc5d8e4be9c891e76ca2aea28225..776919bc99e4af2a20479183a924189a150db499 100644 --- a/aleksis/core/migrations/0003_add_verbose_names.py +++ b/aleksis/core/migrations/0003_add_verbose_names.py @@ -1,7 +1,7 @@ # Generated by Django 3.0.2 on 2020-01-05 16:50 -from django.db import migrations, models import django.utils.timezone +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/aleksis/core/migrations/0004_replace_user_by_person.py b/aleksis/core/migrations/0004_replace_user_by_person.py index 32222f8782866903eaa138880c2477291993cb75..48952ca885da4f7c9081ebd37c6bb2d66e371772 100644 --- a/aleksis/core/migrations/0004_replace_user_by_person.py +++ b/aleksis/core/migrations/0004_replace_user_by_person.py @@ -1,7 +1,7 @@ # Generated by Django 3.0.2 on 2020-01-05 18:32 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/aleksis/core/migrations/0009_dashboard_widget.py b/aleksis/core/migrations/0009_dashboard_widget.py index b57c272b4cc8501e41b5ffaf9f2ec3d796795021..672d516d6c9dda37ff672a2e747e80ba0cf81b2b 100644 --- a/aleksis/core/migrations/0009_dashboard_widget.py +++ b/aleksis/core/migrations/0009_dashboard_widget.py @@ -1,7 +1,7 @@ # Generated by Django 3.0.2 on 2020-01-29 16:45 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/aleksis/core/migrations/0011_make_primary_group_optional.py b/aleksis/core/migrations/0011_make_primary_group_optional.py index 0d28af2f4c01013a139328851aba9f5881e1d883..d63c65e45d57124684d5bec69135a5ca1590bb2f 100644 --- a/aleksis/core/migrations/0011_make_primary_group_optional.py +++ b/aleksis/core/migrations/0011_make_primary_group_optional.py @@ -1,7 +1,7 @@ # Generated by Django 3.0.2 on 2020-02-03 22:41 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/aleksis/core/migrations/0012_announcement.py b/aleksis/core/migrations/0012_announcement.py index 7cfd66158d4565fed228194e753c56f516fd4b6b..5d73e965829370c42357d45e82dcd5f0f897ddd9 100644 --- a/aleksis/core/migrations/0012_announcement.py +++ b/aleksis/core/migrations/0012_announcement.py @@ -1,8 +1,9 @@ # Generated by Django 3.0.3 on 2020-02-10 14:22 import datetime -from django.db import migrations, models + import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/aleksis/core/migrations/0013_extensible_model_as_default.py b/aleksis/core/migrations/0013_extensible_model_as_default.py index 4e23d86f49c0fcc53d92813b794c4083e475a6e3..00abbb392cb8e76441168a3934c5763fb318d828 100644 --- a/aleksis/core/migrations/0013_extensible_model_as_default.py +++ b/aleksis/core/migrations/0013_extensible_model_as_default.py @@ -1,9 +1,10 @@ # Generated by Django 3.0.3 on 2020-02-20 12:24 -import aleksis.core.util.core_helpers import django.contrib.postgres.fields.jsonb from django.db import migrations, models +import aleksis.core.util.core_helpers + class Migration(migrations.Migration): diff --git a/aleksis/core/migrations/0014_accouncement_recipients.py b/aleksis/core/migrations/0014_accouncement_recipients.py index 50e78da8fbb67c81fade2cd3e8923ada4653cc08..aac4f16b5f3cb607d4580eed3748ee1ae4e66800 100644 --- a/aleksis/core/migrations/0014_accouncement_recipients.py +++ b/aleksis/core/migrations/0014_accouncement_recipients.py @@ -1,9 +1,10 @@ # Generated by Django 3.0.3 on 2020-03-11 18:43 -import aleksis.core.models import django.contrib.postgres.fields.jsonb -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models + +import aleksis.core.models class Migration(migrations.Migration): diff --git a/aleksis/core/migrations/0016_custom_menus.py b/aleksis/core/migrations/0016_custom_menus.py index 4961d51c6b2e3e34da2245c288e06864bbef3090..816bbee4472db71d677f4b51ba045390af16f04d 100644 --- a/aleksis/core/migrations/0016_custom_menus.py +++ b/aleksis/core/migrations/0016_custom_menus.py @@ -1,8 +1,8 @@ # Generated by Django 3.0.4 on 2020-03-21 16:39 import django.contrib.postgres.fields.jsonb -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/aleksis/core/migrations/0022_group_types.py b/aleksis/core/migrations/0022_group_types.py index c521da4a6f5a5d6f6c314ee0b34e57cb374c9e7c..8abdd146705aed72807ccadae24fc1290cc912e3 100644 --- a/aleksis/core/migrations/0022_group_types.py +++ b/aleksis/core/migrations/0022_group_types.py @@ -1,8 +1,8 @@ # Generated by Django 3.0.5 on 2020-04-13 13:44 import django.contrib.postgres.fields.jsonb -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/aleksis/core/migrations/0025_dynamic_preferences.py b/aleksis/core/migrations/0025_dynamic_preferences.py index 35486e8d1b045733b7623ba972aa76e0f94dcaca..08d85f3877c145950091ab33a70f6f62d3961757 100644 --- a/aleksis/core/migrations/0025_dynamic_preferences.py +++ b/aleksis/core/migrations/0025_dynamic_preferences.py @@ -1,7 +1,7 @@ # Generated by Django 3.0.5 on 2020-04-30 19:41 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/aleksis/core/mixins.py b/aleksis/core/mixins.py index 0063750f80162ecd310253f9b26ebf221c5218fa..35fb855c6062c72192a33049ae8bb6ae861e6176 100644 --- a/aleksis/core/mixins.py +++ b/aleksis/core/mixins.py @@ -6,13 +6,13 @@ from django.contrib.sites.managers import CurrentSiteManager from django.contrib.sites.models import Site from django.db import models from django.db.models import QuerySet -from django.forms.models import ModelFormMetaclass, ModelForm +from django.forms.models import ModelForm, ModelFormMetaclass +import reversion from easyaudit.models import CRUDEvent from guardian.admin import GuardedModelAdmin from jsonstore.fields import JSONField, JSONFieldMixin -from material.base import LayoutNode, Layout -import reversion +from material.base import Layout, LayoutNode from rules.contrib.admin import ObjectPermissionsModelAdmin @@ -187,8 +187,10 @@ class ExtensibleModel(CRUDMixin): class Meta: abstract = True + class PureDjangoModel(object): """ No-op mixin to mark a model as deliberately not using ExtensibleModel """ + pass diff --git a/aleksis/core/models.py b/aleksis/core/models.py index 03e38d52a8a7f7b3c9a76533dc7e18a59010469d..542a4025512ad37f6a45ea048819f9599abc413d 100644 --- a/aleksis/core/models.py +++ b/aleksis/core/models.py @@ -1,7 +1,6 @@ from datetime import date, datetime -from typing import Optional, Iterable, Union, Sequence, List +from typing import Iterable, List, Optional, Sequence, Union -import jsonstore from django.contrib.auth import get_user_model from django.contrib.auth.models import Group as DjangoGroup from django.contrib.contenttypes.fields import GenericForeignKey @@ -14,6 +13,8 @@ from django.urls import reverse from django.utils import timezone from django.utils.text import slugify from django.utils.translation import gettext_lazy as _ + +import jsonstore from dynamic_preferences.models import PerInstancePreferenceModel from image_cropping import ImageCropField, ImageRatioField from phonenumber_field.modelfields import PhoneNumberField @@ -24,7 +25,6 @@ from .tasks import send_notification from .util.core_helpers import get_site_preferences, now_tomorrow from .util.model_helpers import ICONS - FIELD_CHOICES = ( ("BooleanField", _("Boolean (Yes/No)")), ("CharField", _("Text (one line)")), @@ -63,7 +63,12 @@ class Person(ExtensibleModel): SEX_CHOICES = [("f", _("female")), ("m", _("male"))] user = models.OneToOneField( - get_user_model(), on_delete=models.SET_NULL, blank=True, null=True, related_name="person", verbose_name=_("Linked user") + get_user_model(), + on_delete=models.SET_NULL, + blank=True, + null=True, + related_name="person", + verbose_name=_("Linked user"), ) is_active = models.BooleanField(verbose_name=_("Is person active?"), default=True) @@ -94,14 +99,19 @@ class Person(ExtensibleModel): photo_cropping = ImageRatioField("photo", "600x800", size_warning=True) guardians = models.ManyToManyField( - "self", verbose_name=_("Guardians / Parents"), symmetrical=False, related_name="children", blank=True + "self", + verbose_name=_("Guardians / Parents"), + symmetrical=False, + related_name="children", + blank=True, ) - primary_group = models.ForeignKey("Group", models.SET_NULL, null=True, blank=True, verbose_name=_("Primary group")) + primary_group = models.ForeignKey( + "Group", models.SET_NULL, null=True, blank=True, verbose_name=_("Primary group") + ) description = models.TextField(verbose_name=_("Description"), blank=True, null=True) - def get_absolute_url(self) -> str: return reverse("person_by_id", args=[self.id]) @@ -150,9 +160,9 @@ class Person(ExtensibleModel): """ Age of the person at a given date and time """ years = today.year - self.date_of_birth.year - if (self.date_of_birth.month > today.month - or (self.date_of_birth.month == today.month - and self.date_of_birth.day > today.day)): + if self.date_of_birth.month > today.month or ( + self.date_of_birth.month == today.month and self.date_of_birth.day > today.day + ): years -= 1 return years @@ -182,9 +192,7 @@ class Person(ExtensibleModel): User = get_user_model() if not User.objects.filter(is_superuser=True).exists(): admin = User.objects.create_superuser( - username='admin', - email='root@example.com', - password='admin' + username="admin", email="root@example.com", password="admin" ) admin.save() @@ -225,7 +233,9 @@ class AdditionalField(ExtensibleModel): """ An additional field that can be linked to a group """ title = models.CharField(verbose_name=_("Title of field"), max_length=255) - field_type = models.CharField(verbose_name=_("Type of field"), choices=FIELD_CHOICES, max_length=50) + field_type = models.CharField( + verbose_name=_("Type of field"), choices=FIELD_CHOICES, max_length=50 + ) class Meta: verbose_name = _("Addtitional field for groups") @@ -241,17 +251,25 @@ class Group(ExtensibleModel): ordering = ["short_name", "name"] verbose_name = _("Group") verbose_name_plural = _("Groups") - permissions = ( - ("assign_child_groups_to_groups", _("Can assign child groups to groups")), - ) + permissions = (("assign_child_groups_to_groups", _("Can assign child groups to groups")),) icon_ = "group" name = models.CharField(verbose_name=_("Long name"), max_length=255, unique=True) - short_name = models.CharField(verbose_name=_("Short name"), max_length=255, unique=True, blank=True, null=True) + short_name = models.CharField( + verbose_name=_("Short name"), max_length=255, unique=True, blank=True, null=True + ) - members = models.ManyToManyField("Person", related_name="member_of", blank=True, through="PersonGroupThrough", verbose_name=_("Members")) - owners = models.ManyToManyField("Person", related_name="owner_of", blank=True, verbose_name=_("Owners")) + members = models.ManyToManyField( + "Person", + related_name="member_of", + blank=True, + through="PersonGroupThrough", + verbose_name=_("Members"), + ) + owners = models.ManyToManyField( + "Person", related_name="owner_of", blank=True, verbose_name=_("Owners") + ) parent_groups = models.ManyToManyField( "self", @@ -261,10 +279,16 @@ class Group(ExtensibleModel): blank=True, ) - type = models.ForeignKey("GroupType", on_delete=models.SET_NULL, related_name="type", verbose_name=_("Type of group"), null=True, blank=True) + type = models.ForeignKey( + "GroupType", + on_delete=models.SET_NULL, + related_name="type", + verbose_name=_("Type of group"), + null=True, + blank=True, + ) additional_fields = models.ManyToManyField(AdditionalField, verbose_name=_("Additional fields")) - def get_absolute_url(self) -> str: return reverse("group_by_id", args=[self.id]) @@ -284,9 +308,9 @@ class Group(ExtensibleModel): dj_group, _ = DjangoGroup.objects.get_or_create(name=self.name) dj_group.user_set.set( list( - self.members.filter(user__isnull=False).values_list("user", flat=True).union( - self.owners.filter(user__isnull=False).values_list("user", flat=True) - ) + self.members.filter(user__isnull=False) + .values_list("user", flat=True) + .union(self.owners.filter(user__isnull=False).values_list("user", flat=True)) ) ) dj_group.save() @@ -311,10 +335,13 @@ class PersonGroupThrough(ExtensibleModel): field_instance = field_class(verbose_name=field.title) setattr(self, field_name, field_instance) + class Activity(ExtensibleModel): """ Activity of a user to trace some actions done in AlekSIS in displayable form """ - user = models.ForeignKey("Person", on_delete=models.CASCADE, related_name="activities", verbose_name=_("User")) + user = models.ForeignKey( + "Person", on_delete=models.CASCADE, related_name="activities", verbose_name=_("User") + ) title = models.CharField(max_length=150, verbose_name=_("Title")) description = models.TextField(max_length=500, verbose_name=_("Description")) @@ -333,7 +360,12 @@ class Notification(ExtensibleModel): """ Notification to submit to a user """ sender = models.CharField(max_length=100, verbose_name=_("Sender")) - recipient = models.ForeignKey("Person", on_delete=models.CASCADE, related_name="notifications", verbose_name=_("Recipient")) + recipient = models.ForeignKey( + "Person", + on_delete=models.CASCADE, + related_name="notifications", + verbose_name=_("Recipient"), + ) title = models.CharField(max_length=150, verbose_name=_("Title")) description = models.TextField(max_length=500, verbose_name=_("Description")) @@ -367,14 +399,14 @@ class AnnouncementQuerySet(models.QuerySet): if isinstance(obj, models.QuerySet): ct = ContentType.objects.get_for_model(obj.model) - pks = list(obj.values_list('pk', flat=True)) + pks = list(obj.values_list("pk", flat=True)) else: ct = ContentType.objects.get_for_model(obj) pks = [obj.pk] return self.filter(recipients__content_type=ct, recipients__recipient_id__in=pks) - def at_time(self,when: Optional[datetime] = None ) -> models.QuerySet: + def at_time(self, when: Optional[datetime] = None) -> models.QuerySet: """ Get all announcements at a certain time """ when = when or timezone.datetime.now() @@ -429,8 +461,7 @@ class Announcement(ExtensibleModel): verbose_name=_("Date and time from when to show"), default=timezone.datetime.now ) valid_until = models.DateTimeField( - verbose_name=_("Date and time until when to show"), - default=now_tomorrow, + verbose_name=_("Date and time until when to show"), default=now_tomorrow, ) @property @@ -464,7 +495,9 @@ class AnnouncementRecipient(ExtensibleModel): returning a flat list of Person objects. """ - announcement = models.ForeignKey(Announcement, on_delete=models.CASCADE, related_name="recipients") + announcement = models.ForeignKey( + Announcement, on_delete=models.CASCADE, related_name="recipients" + ) content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) recipient_id = models.PositiveIntegerField() diff --git a/aleksis/core/preferences.py b/aleksis/core/preferences.py index a5538eb96420d49b995bcaddc077fa56d84ad99d..c363875824b5ebba86637c5c557953233278982f 100644 --- a/aleksis/core/preferences.py +++ b/aleksis/core/preferences.py @@ -1,16 +1,24 @@ from django.conf import settings -from django.forms import EmailField, URLField, ImageField +from django.forms import EmailField, ImageField, URLField from django.utils.translation import gettext_lazy as _ from colorfield.widgets import ColorWidget -from dynamic_preferences.types import BooleanPreference, ChoicePreference, StringPreference, FilePreference from dynamic_preferences.preferences import Section from dynamic_preferences.registries import global_preferences_registry - -from .registries import group_preferences_registry, person_preferences_registry, site_preferences_registry +from dynamic_preferences.types import ( + BooleanPreference, + ChoicePreference, + FilePreference, + StringPreference, +) + +from .registries import ( + group_preferences_registry, + person_preferences_registry, + site_preferences_registry, +) from .util.notifications import get_notification_choices_lazy - general = Section("general") school = Section("school") theme = Section("theme") @@ -127,11 +135,11 @@ class AdressingNameFormat(ChoicePreference): required = False verbose_name = _("Name format for addressing") choices = ( - (None, "-----"), - ("german", "John Doe"), - ("english", "Doe, John"), - ("dutch", "Doe John"), - ) + (None, "-----"), + ("german", "John Doe"), + ("english", "Doe, John"), + ("dutch", "Doe John"), + ) @person_preferences_registry.register diff --git a/aleksis/core/rules.py b/aleksis/core/rules.py index 4ebbaee0bc302de092b4f0676eb107254602a547..5753bbc5374e03dccb97b935b1eba6ae65f04239 100644 --- a/aleksis/core/rules.py +++ b/aleksis/core/rules.py @@ -1,17 +1,16 @@ from rules import add_perm, always_allow -from .models import Person, Group, Announcement +from .models import Announcement, Group, Person from .util.predicates import ( - has_person, - has_global_perm, has_any_object, - is_current_person, + has_global_perm, has_object_perm, + has_person, + is_current_person, is_group_owner, is_notification_recipient, ) - add_perm("core", always_allow) # View dashboard @@ -41,7 +40,9 @@ add_perm("core.view_address", view_address_predicate) # View person contact details view_contact_details_predicate = has_person & ( - has_global_perm("core.view_contact_details") | has_object_perm("core.view_contact_details") | is_current_person + has_global_perm("core.view_contact_details") + | has_object_perm("core.view_contact_details") + | is_current_person ) add_perm("core.view_contact_details", view_contact_details_predicate) @@ -53,7 +54,9 @@ add_perm("core.view_photo", view_photo_predicate) # View persons groups view_groups_predicate = has_person & ( - has_global_perm("core.view_person_groups") | has_object_perm("core.view_person_groups") | is_current_person + has_global_perm("core.view_person_groups") + | has_object_perm("core.view_person_groups") + | is_current_person ) add_perm("core.view_person_groups", view_groups_predicate) @@ -86,7 +89,9 @@ edit_group_predicate = has_person & ( add_perm("core.edit_group", edit_group_predicate) # Assign child groups to groups -assign_child_groups_to_groups_predicate = has_person & has_global_perm("core.assign_child_groups_to_groups") +assign_child_groups_to_groups_predicate = has_person & has_global_perm( + "core.assign_child_groups_to_groups" +) add_perm("core.assign_child_groups_to_groups", assign_child_groups_to_groups_predicate) # Edit school information @@ -111,13 +116,15 @@ add_perm("core.mark_notification_as_read", mark_notification_as_read_predicate) # View announcements view_announcements_predicate = has_person & ( - has_global_perm("core.view_announcement") | has_any_object("core.view_announcement", Announcement) + has_global_perm("core.view_announcement") + | has_any_object("core.view_announcement", Announcement) ) add_perm("core.view_announcements", view_announcements_predicate) # Create or edit announcement create_or_edit_announcement_predicate = has_person & ( - has_global_perm("core.add_announcement") & (has_global_perm("core.change_announcement") | has_object_perm("core.change_announcement")) + has_global_perm("core.add_announcement") + & (has_global_perm("core.change_announcement") | has_object_perm("core.change_announcement")) ) add_perm("core.create_or_edit_announcement", create_or_edit_announcement_predicate) @@ -136,32 +143,54 @@ view_system_status_predicate = has_person & has_global_perm("core.view_system_st add_perm("core.view_system_status", view_system_status_predicate) # View people menu (persons + objects) -add_perm("core.view_people_menu", has_person & (view_persons_predicate | view_groups_predicate | link_persons_accounts_predicate | assign_child_groups_to_groups_predicate)) +add_perm( + "core.view_people_menu", + has_person + & ( + view_persons_predicate + | view_groups_predicate + | link_persons_accounts_predicate + | assign_child_groups_to_groups_predicate + ), +) # View admin menu -view_admin_menu_predicate = has_person & (manage_data_predicate | manage_school_predicate | impersonate_predicate | view_system_status_predicate | view_announcements_predicate) +view_admin_menu_predicate = has_person & ( + manage_data_predicate + | manage_school_predicate + | impersonate_predicate + | view_system_status_predicate + | view_announcements_predicate +) add_perm("core.view_admin_menu", view_admin_menu_predicate) # View person personal details view_personal_details_predicate = has_person & ( - has_global_perm("core.view_personal_details") | has_object_perm("core.view_personal_details") | is_current_person + has_global_perm("core.view_personal_details") + | has_object_perm("core.view_personal_details") + | is_current_person ) add_perm("core.view_personal_details", view_personal_details_predicate) # Change site preferences change_site_preferences = has_person & ( - has_global_perm("core.change_site_preferences") | has_object_perm("core.change_site_preferences") + has_global_perm("core.change_site_preferences") + | has_object_perm("core.change_site_preferences") ) add_perm("core.change_site_preferences", change_site_preferences) # Change person preferences change_person_preferences = has_person & ( - has_global_perm("core.change_person_preferences") | has_object_perm("core.change_person_preferences") | is_current_person + has_global_perm("core.change_person_preferences") + | has_object_perm("core.change_person_preferences") + | is_current_person ) add_perm("core.change_person_preferences", change_person_preferences) # Change group preferences change_group_preferences = has_person & ( - has_global_perm("core.change_group_preferences") | has_object_perm("core.change_group_preferences") | is_group_owner + has_global_perm("core.change_group_preferences") + | has_object_perm("core.change_group_preferences") + | is_group_owner ) add_perm("core.change_group_preferences", change_group_preferences) diff --git a/aleksis/core/search_indexes.py b/aleksis/core/search_indexes.py index 39d6be9906aa0ebe6afb9ca707a5ce355d7445ee..7c7beca9e1d691a5548113c37d13a0109c85fe61 100644 --- a/aleksis/core/search_indexes.py +++ b/aleksis/core/search_indexes.py @@ -1,4 +1,4 @@ -from .models import Person, Group +from .models import Group, Person from .util.search import Indexable, SearchIndex diff --git a/aleksis/core/settings.py b/aleksis/core/settings.py index 58ff7e2d6b17fec8e3f49fc2f2611cab366ed38f..3153f9d8dd83b128ba8c35852c6addcb4c3fe599 100644 --- a/aleksis/core/settings.py +++ b/aleksis/core/settings.py @@ -9,7 +9,12 @@ from django.utils.translation import gettext_lazy as _ from dynaconf import LazySettings from easy_thumbnails.conf import Settings as thumbnail_settings -from .util.core_helpers import get_app_packages, lazy_preference, merge_app_settings, lazy_get_favicon_url +from .util.core_helpers import ( + get_app_packages, + lazy_get_favicon_url, + lazy_preference, + merge_app_settings, +) ENVVAR_PREFIX_FOR_DYNACONF = "ALEKSIS" DIRS_FOR_DYNACONF = ["/etc/aleksis"] @@ -200,7 +205,12 @@ AUTHENTICATION_BACKENDS = [] if _settings.get("ldap.uri", None): # LDAP dependencies are not necessarily installed, so import them here import ldap # noqa - from django_auth_ldap.config import LDAPSearch, NestedGroupOfNamesType, NestedGroupOfUniqueNamesType, PosixGroupType # noqa + from django_auth_ldap.config import ( + LDAPSearch, + NestedGroupOfNamesType, + NestedGroupOfUniqueNamesType, + PosixGroupType, + ) # noqa # Enable Django's integration to LDAP AUTHENTICATION_BACKENDS.append("django_auth_ldap.backend.LDAPBackend") @@ -243,16 +253,20 @@ if _settings.get("ldap.uri", None): elif _group_type == "posixgroup": AUTH_LDAP_GROUP_TYPE = PosixGroupType() - AUTH_LDAP_USER_FLAGS_BY_GROUP = { - } + AUTH_LDAP_USER_FLAGS_BY_GROUP = {} for _flag in ["is_active", "is_staff", "is_superuser"]: _dn = _settings.get(f"ldap.groups.flags.{_flag}", None) if _dn: AUTH_LDAP_USER_FLAGS_BY_GROUP[_flag] = _dn # Backend admin requires superusers to also be staff members - if "is_superuser" in AUTH_LDAP_USER_FLAGS_BY_GROUP and "is_staff" not in AUTH_LDAP_USER_FLAGS_BY_GROUP: - AUTH_LDAP_USER_FLAGS_BY_GROUP["is_staff"] = AUTH_LDAP_USER_FLAGS_BY_GROUP["is_superuser"] + if ( + "is_superuser" in AUTH_LDAP_USER_FLAGS_BY_GROUP + and "is_staff" not in AUTH_LDAP_USER_FLAGS_BY_GROUP + ): + AUTH_LDAP_USER_FLAGS_BY_GROUP["is_staff"] = AUTH_LDAP_USER_FLAGS_BY_GROUP[ + "is_superuser" + ] # Add ModelBckend last so all other backends get a chance # to verify passwords first @@ -313,7 +327,10 @@ ANY_JS = { "css_url": JS_URL + "/material-design-icons-iconfont/dist/material-design-icons.css" }, "paper-css": {"css_url": JS_URL + "/paper-css/paper.min.css"}, - "select2-materialize": {"css_url": JS_URL + "/select2-materialize/select2-materialize.css", "js_url": JS_URL + "/select2-materialize/index.js"}, + "select2-materialize": { + "css_url": JS_URL + "/select2-materialize/select2-materialize.css", + "js_url": JS_URL + "/select2-materialize/index.js", + }, } merge_app_settings("ANY_JS", ANY_JS, True) @@ -344,7 +361,7 @@ if _settings.get("mail.server.host", None): EMAIL_HOST_USER = _settings.get("mail.server.user") EMAIL_HOST_PASSWORD = _settings.get("mail.server.password") -TEMPLATED_EMAIL_BACKEND = 'templated_email.backends.vanilla_django' +TEMPLATED_EMAIL_BACKEND = "templated_email.backends.vanilla_django" TEMPLATED_EMAIL_AUTO_PLAIN = True @@ -421,24 +438,50 @@ PWA_APP_BACKGROUND_COLOR = "#ffffff" PWA_APP_DISPLAY = "standalone" PWA_APP_ORIENTATION = "any" PWA_APP_ICONS = [ # three icons to upload dbsettings - {"src": lazy_get_favicon_url(title="pwa_icon", size=192, rel="android", - default=STATIC_URL + "icons/android_192.png"), "sizes": "192x192"}, - {"src": lazy_get_favicon_url(title="pwa_icon", size=512, rel="android", - default=STATIC_URL + "icons/android_512.png"), "sizes": "512x512"}, + { + "src": lazy_get_favicon_url( + title="pwa_icon", size=192, rel="android", default=STATIC_URL + "icons/android_192.png" + ), + "sizes": "192x192", + }, + { + "src": lazy_get_favicon_url( + title="pwa_icon", size=512, rel="android", default=STATIC_URL + "icons/android_512.png" + ), + "sizes": "512x512", + }, ] PWA_APP_ICONS_APPLE = [ - {"src": lazy_get_favicon_url(title="pwa_icon", size=192, rel="apple", - default=STATIC_URL + "icons/apple_76.png"), "sizes": "76x76"}, - {"src": lazy_get_favicon_url(title="pwa_icon", size=192, rel="apple", - default=STATIC_URL + "icons/apple_114.png"), "sizes": "114x114"}, - {"src": lazy_get_favicon_url(title="pwa_icon", size=192, rel="apple", - default=STATIC_URL + "icons/apple_152.png"), "sizes": "152x152"}, - {"src": lazy_get_favicon_url(title="pwa_icon", size=192, rel="apple", - default=STATIC_URL + "icons/apple_180.png"), "sizes": "180x180"}, + { + "src": lazy_get_favicon_url( + title="pwa_icon", size=192, rel="apple", default=STATIC_URL + "icons/apple_76.png" + ), + "sizes": "76x76", + }, + { + "src": lazy_get_favicon_url( + title="pwa_icon", size=192, rel="apple", default=STATIC_URL + "icons/apple_114.png" + ), + "sizes": "114x114", + }, + { + "src": lazy_get_favicon_url( + title="pwa_icon", size=192, rel="apple", default=STATIC_URL + "icons/apple_152.png" + ), + "sizes": "152x152", + }, + { + "src": lazy_get_favicon_url( + title="pwa_icon", size=192, rel="apple", default=STATIC_URL + "icons/apple_180.png" + ), + "sizes": "180x180", + }, ] PWA_APP_SPLASH_SCREEN = [ { - "src": lazy_get_favicon_url(title="pwa_icon", size=192, rel="apple", default=STATIC_URL + "icons/apple_180.png"), + "src": lazy_get_favicon_url( + title="pwa_icon", size=192, rel="apple", default=STATIC_URL + "icons/apple_180.png" + ), "media": "(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2)", } ] @@ -449,65 +492,114 @@ PWA_SERVICE_WORKER_PATH = os.path.join(STATIC_ROOT, "js", "serviceworker.js") SITE_ID = 1 CKEDITOR_CONFIGS = { - 'default': { - 'toolbar_Basic': [ - ['Source', '-', 'Bold', 'Italic'] - ], - 'toolbar_Full': [ - {'name': 'document', 'items': ['Source', '-', 'Save', 'NewPage', 'Preview', 'Print', '-', 'Templates']}, - {'name': 'clipboard', 'items': ['Cut', 'Copy', 'Paste', 'PasteText', 'PasteFromWord', '-', 'Undo', 'Redo']}, - {'name': 'editing', 'items': ['Find', 'Replace', '-', 'SelectAll']}, - {'name': 'insert', - 'items': ['Image', 'Table', 'HorizontalRule', 'Smiley', 'SpecialChar', 'PageBreak', 'Iframe']}, - '/', - {'name': 'basicstyles', - 'items': ['Bold', 'Italic', 'Underline', 'Strike', 'Subscript', 'Superscript', '-', 'RemoveFormat']}, - {'name': 'paragraph', - 'items': ['NumberedList', 'BulletedList', '-', 'Outdent', 'Indent', '-', 'Blockquote', 'CreateDiv', '-', - 'JustifyLeft', 'JustifyCenter', 'JustifyRight', 'JustifyBlock', '-', 'BidiLtr', 'BidiRtl', - 'Language']}, - {'name': 'links', 'items': ['Link', 'Unlink', 'Anchor']}, - '/', - {'name': 'styles', 'items': ['Styles', 'Format', 'Font', 'FontSize']}, - {'name': 'colors', 'items': ['TextColor', 'BGColor']}, - {'name': 'tools', 'items': ['Maximize', 'ShowBlocks']}, - {'name': 'about', 'items': ['About']}, - {'name': 'customtools', 'items': [ - 'Preview', - 'Maximize', - ]}, + "default": { + "toolbar_Basic": [["Source", "-", "Bold", "Italic"]], + "toolbar_Full": [ + { + "name": "document", + "items": ["Source", "-", "Save", "NewPage", "Preview", "Print", "-", "Templates"], + }, + { + "name": "clipboard", + "items": [ + "Cut", + "Copy", + "Paste", + "PasteText", + "PasteFromWord", + "-", + "Undo", + "Redo", + ], + }, + {"name": "editing", "items": ["Find", "Replace", "-", "SelectAll"]}, + { + "name": "insert", + "items": [ + "Image", + "Table", + "HorizontalRule", + "Smiley", + "SpecialChar", + "PageBreak", + "Iframe", + ], + }, + "/", + { + "name": "basicstyles", + "items": [ + "Bold", + "Italic", + "Underline", + "Strike", + "Subscript", + "Superscript", + "-", + "RemoveFormat", + ], + }, + { + "name": "paragraph", + "items": [ + "NumberedList", + "BulletedList", + "-", + "Outdent", + "Indent", + "-", + "Blockquote", + "CreateDiv", + "-", + "JustifyLeft", + "JustifyCenter", + "JustifyRight", + "JustifyBlock", + "-", + "BidiLtr", + "BidiRtl", + "Language", + ], + }, + {"name": "links", "items": ["Link", "Unlink", "Anchor"]}, + "/", + {"name": "styles", "items": ["Styles", "Format", "Font", "FontSize"]}, + {"name": "colors", "items": ["TextColor", "BGColor"]}, + {"name": "tools", "items": ["Maximize", "ShowBlocks"]}, + {"name": "about", "items": ["About"]}, + {"name": "customtools", "items": ["Preview", "Maximize",]}, ], - 'toolbar': 'Full', - 'tabSpaces': 4, - 'extraPlugins': ','.join([ - 'uploadimage', - 'div', - 'autolink', - 'autoembed', - 'embedsemantic', - 'autogrow', - # 'devtools', - 'widget', - 'lineutils', - 'clipboard', - 'dialog', - 'dialogui', - 'elementspath' - ]), + "toolbar": "Full", + "tabSpaces": 4, + "extraPlugins": ",".join( + [ + "uploadimage", + "div", + "autolink", + "autoembed", + "embedsemantic", + "autogrow", + # 'devtools', + "widget", + "lineutils", + "clipboard", + "dialog", + "dialogui", + "elementspath", + ] + ), } } # Which HTML tags are allowed -BLEACH_ALLOWED_TAGS = ['p', 'b', 'i', 'u', 'em', 'strong', 'a', 'div'] +BLEACH_ALLOWED_TAGS = ["p", "b", "i", "u", "em", "strong", "a", "div"] # Which HTML attributes are allowed -BLEACH_ALLOWED_ATTRIBUTES = ['href', 'title', 'style'] +BLEACH_ALLOWED_ATTRIBUTES = ["href", "title", "style"] # Which CSS properties are allowed in 'style' attributes (assuming # style is an allowed attribute) -BLEACH_ALLOWED_STYLES = [ - 'font-family', 'font-weight', 'text-decoration', 'font-variant' -] +BLEACH_ALLOWED_STYLES = ["font-family", "font-weight", "text-decoration", "font-variant"] # Strip unknown tags if True, replace with HTML escaped characters if # False @@ -517,23 +609,11 @@ BLEACH_STRIP_TAGS = True BLEACH_STRIP_COMMENTS = True LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'handlers': { - 'console': { - 'class': 'logging.StreamHandler', - 'formatter': "verbose" - }, - }, - 'formatters': { - 'verbose': { - 'format': '%(levelname)s %(asctime)s %(module)s: %(message)s' - } - }, - 'root': { - 'handlers': ['console'], - 'level': _settings.get("logging.level", "WARNING"), - }, + "version": 1, + "disable_existing_loggers": False, + "handlers": {"console": {"class": "logging.StreamHandler", "formatter": "verbose"},}, + "formatters": {"verbose": {"format": "%(levelname)s %(asctime)s %(module)s: %(message)s"}}, + "root": {"handlers": ["console"], "level": _settings.get("logging.level", "WARNING"),}, } # Rules and permissions @@ -550,29 +630,27 @@ HAYSTACK_BACKEND_SHORT = _settings.get("search.backend", "simple") if HAYSTACK_BACKEND_SHORT == "simple": HAYSTACK_CONNECTIONS = { - 'default': { - 'ENGINE': 'haystack.backends.simple_backend.SimpleEngine', - }, + "default": {"ENGINE": "haystack.backends.simple_backend.SimpleEngine",}, } elif HAYSTACK_BACKEND_SHORT == "xapian": HAYSTACK_CONNECTIONS = { - 'default': { - 'ENGINE': 'xapian_backend.XapianEngine', - 'PATH': _settings.get("search.index", os.path.join(BASE_DIR, "xapian_index")), + "default": { + "ENGINE": "xapian_backend.XapianEngine", + "PATH": _settings.get("search.index", os.path.join(BASE_DIR, "xapian_index")), }, } elif HAYSTACK_BACKEND_SHORT == "whoosh": HAYSTACK_CONNECTIONS = { - 'default': { - 'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine', - 'PATH': _settings.get("search.index", os.path.join(BASE_DIR, "whoosh_index")), + "default": { + "ENGINE": "haystack.backends.whoosh_backend.WhooshEngine", + "PATH": _settings.get("search.index", os.path.join(BASE_DIR, "whoosh_index")), }, } if _settings.get("celery.enabled", False) and _settings.get("search.celery", True): INSTALLED_APPS.append("celery_haystack") - HAYSTACK_SIGNAL_PROCESSOR = 'celery_haystack.signals.CelerySignalProcessor' + HAYSTACK_SIGNAL_PROCESSOR = "celery_haystack.signals.CelerySignalProcessor" else: - HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor' + HAYSTACK_SIGNAL_PROCESSOR = "haystack.signals.RealtimeSignalProcessor" HAYSTACK_SEARCH_RESULTS_PER_PAGE = 10 diff --git a/aleksis/core/tasks.py b/aleksis/core/tasks.py index 6fea93569610b2fbe17ba44833073f239e469979..9a9f1ea39ac3b95aade5d6fffa21e766ab042f0f 100644 --- a/aleksis/core/tasks.py +++ b/aleksis/core/tasks.py @@ -21,7 +21,9 @@ def backup_data() -> None: # Assemble command-line options for dbbackup management command db_options = "-z " * settings.DBBACKUP_COMPRESS_DB + "-e" * settings.DBBACKUP_ENCRYPT_DB - media_options = "-z " * settings.DBBACKUP_COMPRESS_MEDIA + "-e" * settings.DBBACKUP_ENCRYPT_MEDIA + media_options = ( + "-z " * settings.DBBACKUP_COMPRESS_MEDIA + "-e" * settings.DBBACKUP_ENCRYPT_MEDIA + ) # Hand off to dbbackup's management commands management.call_command("dbbackup", db_options) diff --git a/aleksis/core/templatetags/apps.py b/aleksis/core/templatetags/apps.py index 6f48c994917130290f2c4400fca753c4df11b06f..c25203daf2e256a436b42ef223e7976db0de8de6 100644 --- a/aleksis/core/templatetags/apps.py +++ b/aleksis/core/templatetags/apps.py @@ -2,4 +2,4 @@ from django.apps import AppConfig class TemplatetagsConfig(AppConfig): - name = 'templatetags' + name = "templatetags" diff --git a/aleksis/core/tests/views/test_account.py b/aleksis/core/tests/views/test_account.py index ecc15f59920ddb493ae64323a7aa4fbeebdafec4..da1c2b90ef0f867b7239817b7ce60fc1cc2478b0 100644 --- a/aleksis/core/tests/views/test_account.py +++ b/aleksis/core/tests/views/test_account.py @@ -10,7 +10,7 @@ def test_index_not_logged_in(client): response = client.get("/") assert response.status_code == 302 - assert response['Location'].startswith(reverse(settings.LOGIN_URL)) + assert response["Location"].startswith(reverse(settings.LOGIN_URL)) def test_login_without_person(client, django_user_model): diff --git a/aleksis/core/urls.py b/aleksis/core/urls.py index 91e32af87edfbaa21335394c342ab69ee75395e7..6f58fc46e909db034f6a61d210a5e6bddba3868f 100644 --- a/aleksis/core/urls.py +++ b/aleksis/core/urls.py @@ -34,7 +34,11 @@ urlpatterns = [ path("group/<int:id_>", views.group, name="group_by_id"), path("group/<int:id_>/edit", views.edit_group, name="edit_group_by_id"), path("", views.index, name="index"), - path("notifications/mark-read/<int:id_>", views.notification_mark_read, name="notification_mark_read"), + path( + "notifications/mark-read/<int:id_>", + views.notification_mark_read, + name="notification_mark_read", + ), path("announcements/", views.announcements, name="announcements"), path("announcement/create/", views.announcement_form, name="add_announcement"), path("announcement/edit/<int:pk>/", views.announcement_form, name="edit_announcement"), @@ -45,21 +49,78 @@ urlpatterns = [ path("impersonate/", include("impersonate.urls")), path("__i18n__/", include("django.conf.urls.i18n")), path("select2/", include("django_select2.urls")), - path("jsreverse.js", urls_js, name='js_reverse'), + path("jsreverse.js", urls_js, name="js_reverse"), path("calendarweek_i18n.js", calendarweek.django.i18n_js, name="calendarweek_i18n_js"), - path('gettext.js', JavaScriptCatalog.as_view(), name='javascript-catalog'), - path("preferences/site/", views.preferences, {"registry_name": "site"}, name="preferences_site"), - path("preferences/person/", views.preferences, {"registry_name": "person"}, name="preferences_person"), - path("preferences/group/", views.preferences, {"registry_name": "group"}, name="preferences_group"), - path("preferences/site/<int:pk>/", views.preferences, {"registry_name": "site"}, name="preferences_site"), - path("preferences/person/<int:pk>/", views.preferences, {"registry_name": "person"}, name="preferences_person"), - path("preferences/group/<int:pk>/", views.preferences, {"registry_name": "group"}, name="preferences_group"), - path("preferences/site/<int:pk>/<str:section>/", views.preferences, {"registry_name": "site"}, name="preferences_site" ), - path("preferences/person/<int:pk>/<str:section>/", views.preferences, {"registry_name": "person"}, name="preferences_person"), - path("preferences/group/<int:pk>/<str:section>/", views.preferences, {"registry_name": "group"}, name="preferences_group"), - path("preferences/site/<str:section>/", views.preferences, {"registry_name": "site"}, name="preferences_site"), - path("preferences/person/<str:section>/", views.preferences, {"registry_name": "person"}, name="preferences_person"), - path("preferences/group/<str:section>/", views.preferences, {"registry_name": "group"}, name="preferences_group"), + path("gettext.js", JavaScriptCatalog.as_view(), name="javascript-catalog"), + path( + "preferences/site/", views.preferences, {"registry_name": "site"}, name="preferences_site" + ), + path( + "preferences/person/", + views.preferences, + {"registry_name": "person"}, + name="preferences_person", + ), + path( + "preferences/group/", + views.preferences, + {"registry_name": "group"}, + name="preferences_group", + ), + path( + "preferences/site/<int:pk>/", + views.preferences, + {"registry_name": "site"}, + name="preferences_site", + ), + path( + "preferences/person/<int:pk>/", + views.preferences, + {"registry_name": "person"}, + name="preferences_person", + ), + path( + "preferences/group/<int:pk>/", + views.preferences, + {"registry_name": "group"}, + name="preferences_group", + ), + path( + "preferences/site/<int:pk>/<str:section>/", + views.preferences, + {"registry_name": "site"}, + name="preferences_site", + ), + path( + "preferences/person/<int:pk>/<str:section>/", + views.preferences, + {"registry_name": "person"}, + name="preferences_person", + ), + path( + "preferences/group/<int:pk>/<str:section>/", + views.preferences, + {"registry_name": "group"}, + name="preferences_group", + ), + path( + "preferences/site/<str:section>/", + views.preferences, + {"registry_name": "site"}, + name="preferences_site", + ), + path( + "preferences/person/<str:section>/", + views.preferences, + {"registry_name": "person"}, + name="preferences_person", + ), + path( + "preferences/group/<str:section>/", + views.preferences, + {"registry_name": "group"}, + name="preferences_group", + ), ] # Serve static files from STATIC_ROOT to make it work with runserver diff --git a/aleksis/core/util/apps.py b/aleksis/core/util/apps.py index 7063e378908d5092efd9bea259d64759483ccd1c..c1162d4ea8e49d3fbbcde7777ce7f851569c512b 100644 --- a/aleksis/core/util/apps.py +++ b/aleksis/core/util/apps.py @@ -1,5 +1,5 @@ from importlib import import_module -from typing import Any, List, Optional, Tuple, Sequence +from typing import Any, List, Optional, Sequence, Tuple import django.apps from django.contrib.auth.signals import user_logged_in, user_logged_out @@ -7,7 +7,7 @@ from django.db.models.signals import post_migrate, pre_migrate from django.http import HttpRequest from dynamic_preferences.signals import preference_updated -from license_expression import Licensing, LicenseSymbol +from license_expression import LicenseSymbol, Licensing from spdx_license_list import LICENSES from .core_helpers import copyright_years @@ -70,13 +70,13 @@ class AppConfig(django.apps.AppConfig): licence = getattr(cls, "licence", None) default_dict = { - 'isDeprecatedLicenseId': False, - 'isFsfLibre': False, - 'isOsiApproved': False, - 'licenseId': 'unknown', - 'name': 'Unknown Licence', - 'referenceNumber': -1, - 'url': '', + "isDeprecatedLicenseId": False, + "isFsfLibre": False, + "isOsiApproved": False, + "licenseId": "unknown", + "name": "Unknown Licence", + "referenceNumber": -1, + "url": "", } if licence: # Parse licence string into object format @@ -133,7 +133,9 @@ class AppConfig(django.apps.AppConfig): copyrights_processed.append( ( # Sort copyright years and combine year ranges for display - copyright[0] if isinstance(copyright[0], str) else copyright_years(copyright[0]), + copyright[0] + if isinstance(copyright[0], str) + else copyright_years(copyright[0]), copyright[1], copyright[2], ) diff --git a/aleksis/core/util/core_helpers.py b/aleksis/core/util/core_helpers.py index c9c28deaf57fcdec72fb73470619ba40d5404002..aa47102931de63a881e2e76e26cc7d484bdff933 100644 --- a/aleksis/core/util/core_helpers.py +++ b/aleksis/core/util/core_helpers.py @@ -1,12 +1,10 @@ -from datetime import datetime, timedelta -from itertools import groupby -from operator import itemgetter import os import pkgutil +from datetime import datetime, timedelta from importlib import import_module - +from itertools import groupby +from operator import itemgetter from typing import Any, Callable, List, Optional, Sequence, Union - from uuid import uuid4 from django.conf import settings @@ -15,6 +13,7 @@ from django.http import HttpRequest from django.utils import timezone from django.utils.functional import lazy + def copyright_years(years: Sequence[int], seperator: str = ", ", joiner: str = "–") -> str: """ Takes a sequence of integegers and produces a string with ranges @@ -22,11 +21,18 @@ def copyright_years(years: Sequence[int], seperator: str = ", ", joiner: str = " '1999–2001, 2005, 2007–2009' """ - ranges = [list(map(itemgetter(1), group)) for _, group in groupby(enumerate(years), lambda e: e[1]-e[0])] - years_strs = [str(range_[0]) if len(range_) == 1 else joiner.join([str(range_[0]), str(range_[-1])]) for range_ in ranges] + ranges = [ + list(map(itemgetter(1), group)) + for _, group in groupby(enumerate(years), lambda e: e[1] - e[0]) + ] + years_strs = [ + str(range_[0]) if len(range_) == 1 else joiner.join([str(range_[0]), str(range_[-1])]) + for range_ in ranges + ] return seperator.join(years_strs) + def dt_show_toolbar(request: HttpRequest) -> bool: """ Helper to determin if Django debug toolbar should be displayed @@ -59,7 +65,9 @@ def get_app_packages() -> Sequence[str]: return [f"aleksis.apps.{pkg[1]}" for pkg in pkgutil.iter_modules(aleksis.apps.__path__)] -def merge_app_settings(setting: str, original: Union[dict, list], deduplicate: bool = False) -> Union[dict, list]: +def merge_app_settings( + setting: str, original: Union[dict, list], deduplicate: bool = False +) -> Union[dict, list]: """ Get a named settings constant from all apps and merge it into the original. To use this, add a settings.py file to the app, in the same format as Django's main settings.py. @@ -97,6 +105,7 @@ def get_site_preferences(): """ Get the preferences manager of the current site """ from django.contrib.sites.models import Site # noqa + return Site.objects.get_current().preferences @@ -114,7 +123,9 @@ def lazy_preference(section: str, name: str) -> Callable[[str, str], Any]: return lazy(_get_preference, str)(section, name) -def lazy_get_favicon_url(title: str, size: int, rel: str, default: Optional[str] = None) -> Callable[[str, str], Any]: +def lazy_get_favicon_url( + title: str, size: int, rel: str, default: Optional[str] = None +) -> Callable[[str, str], Any]: """ Lazily get the URL to a favicon image """ def _get_favicon_url(size: int, rel: str) -> Any: @@ -170,6 +181,7 @@ def celery_optional(orig: Callable) -> Callable: if hasattr(settings, "CELERY_RESULT_BACKEND"): from ..celery import app # noqa + task = app.task(orig) def wrapped(*args, **kwargs): @@ -200,6 +212,7 @@ def custom_information_processor(request: HttpRequest) -> dict: """ Provides custom information in all templates """ from ..models import CustomMenu + return { "FOOTER_MENU": CustomMenu.get_default("footer"), } @@ -210,7 +223,9 @@ def now_tomorrow() -> datetime: return timezone.now() + timedelta(days=1) -def objectgetter_optional(model: Model, default: Optional[Any] = None, default_eval: bool = False) -> Callable[[HttpRequest, Optional[int]], Model]: +def objectgetter_optional( + model: Model, default: Optional[Any] = None, default_eval: bool = False +) -> Callable[[HttpRequest, Optional[int]], Model]: """ Get an object by pk, defaulting to None """ def get_object(request: HttpRequest, id_: Optional[int] = None) -> Model: diff --git a/aleksis/core/util/middlewares.py b/aleksis/core/util/middlewares.py index ce2915a28f846f77b578386e2f2aa851f4ab576a..79f15a1393dc93d1571e70d843d81ec6e31993d4 100644 --- a/aleksis/core/util/middlewares.py +++ b/aleksis/core/util/middlewares.py @@ -4,8 +4,8 @@ from django.core.exceptions import PermissionDenied from django.http import HttpRequest, HttpResponse from django.utils.translation import gettext_lazy as _ -from .core_helpers import has_person from ..models import DummyPerson +from .core_helpers import has_person class EnsurePersonMiddleware: @@ -23,7 +23,9 @@ class EnsurePersonMiddleware: if not has_person(request): if request.user.is_superuser: # Super-users get a dummy person linked - dummy_person = DummyPerson(first_name=request.user.first_name, last_name=request.user.last_name) + dummy_person = DummyPerson( + first_name=request.user.first_name, last_name=request.user.last_name + ) request.user.person = dummy_person response = self.get_response(request) diff --git a/aleksis/core/util/notifications.py b/aleksis/core/util/notifications.py index b9b146a2826ffde45f9924bfcd717ececaa39fcd..7ab63a64e93ab595b16d6c92b50940b8946d0c00 100644 --- a/aleksis/core/util/notifications.py +++ b/aleksis/core/util/notifications.py @@ -10,13 +10,13 @@ from django.utils.translation import gettext_lazy as _ from templated_email import send_templated_mail +from .core_helpers import celery_optional, lazy_preference + try: from twilio.rest import Client as TwilioClient except ImportError: TwilioClient = None -from .core_helpers import celery_optional, lazy_preference - def send_templated_sms( template_name: str, from_number: str, recipient_list: Sequence[str], context: dict diff --git a/aleksis/core/util/predicates.py b/aleksis/core/util/predicates.py index 1ff0a91f5dfd160da8c28311d8fc43b2f5e6c1e2..6c8deb1322b1b5407e04fe0de3a2f37322b182b9 100644 --- a/aleksis/core/util/predicates.py +++ b/aleksis/core/util/predicates.py @@ -2,13 +2,13 @@ from django.contrib.auth.backends import ModelBackend from django.contrib.auth.models import User from django.db.models import Model from django.http import HttpRequest + from guardian.backends import ObjectPermissionBackend from guardian.shortcuts import get_objects_for_user from rules import predicate -from .core_helpers import has_person as has_person_helper - from ..models import Group +from .core_helpers import has_person as has_person_helper def permission_validator(request: HttpRequest, perm: str) -> bool: diff --git a/aleksis/core/util/sass_helpers.py b/aleksis/core/util/sass_helpers.py index 2dcbf0aa0314354c91b417b40a6e5606ab841197..d624944c9b3a30fab843682f897c100e4dd4edc7 100644 --- a/aleksis/core/util/sass_helpers.py +++ b/aleksis/core/util/sass_helpers.py @@ -10,6 +10,7 @@ from sass import SassColor from .core_helpers import get_site_preferences + def get_colour(html_colour: str) -> SassColor: """ Get a SASS colour object from an HTML colour string """ diff --git a/aleksis/core/util/search.py b/aleksis/core/util/search.py index 6720fb6b4236f10d3bfb1932969483d4d4db6d8f..dd8fe793162c9fdde33cdb0803754e47e415dc35 100644 --- a/aleksis/core/util/search.py +++ b/aleksis/core/util/search.py @@ -5,11 +5,12 @@ from haystack import indexes # Not used here, but simplifies imports for apps Indexable = indexes.Indexable # noqa -if settings.HAYSTACK_SIGNAL_PROCESSOR == 'celery_haystack.signals.CelerySignalProcessor': +if settings.HAYSTACK_SIGNAL_PROCESSOR == "celery_haystack.signals.CelerySignalProcessor": from haystack.indexes import SearchIndex as BaseSearchIndex else: from celery_haystack.indexes import CelerySearchIndex as BaseSearchIndex + class SearchIndex(BaseSearchIndex): """ Base class for search indexes on AlekSIS models diff --git a/aleksis/core/views.py b/aleksis/core/views.py index bc82720fbb7c157113c7e8a53f7638a0b6e364c7..fab71fd0c306f0a471c4f6a31e54517648c7b016 100644 --- a/aleksis/core/views.py +++ b/aleksis/core/views.py @@ -19,17 +19,21 @@ from rules.contrib.views import permission_required from .filters import GroupFilter from .forms import ( + AnnouncementForm, + ChildGroupsForm, EditGroupForm, EditPersonForm, + GroupPreferenceForm, + PersonPreferenceForm, PersonsAccountsFormSet, - AnnouncementForm, - ChildGroupsForm, SitePreferenceForm, - PersonPreferenceForm, - GroupPreferenceForm, ) -from .models import Activity, Group, Notification, Person, DashboardWidget, Announcement -from .registries import site_preferences_registry, group_preferences_registry, person_preferences_registry +from .models import Activity, Announcement, DashboardWidget, Group, Notification, Person +from .registries import ( + group_preferences_registry, + person_preferences_registry, + site_preferences_registry, +) from .tables import GroupsTable, PersonsTable from .util import messages from .util.apps import AppConfig @@ -73,7 +77,9 @@ def about(request: HttpRequest) -> HttpResponse: context = {} - context["app_configs"] = list(filter(lambda a: isinstance(a, AppConfig), apps.get_app_configs())) + context["app_configs"] = list( + filter(lambda a: isinstance(a, AppConfig), apps.get_app_configs()) + ) return render(request, "core/about.html", context) @@ -97,7 +103,9 @@ def persons(request: HttpRequest) -> HttpResponse: return render(request, "core/persons.html", context) -@permission_required("core.view_person", fn=objectgetter_optional(Person, "request.user.person", True)) +@permission_required( + "core.view_person", fn=objectgetter_optional(Person, "request.user.person", True) +) def person(request: HttpRequest, id_: Optional[int] = None) -> HttpResponse: """ Detail view for one person; defaulting to logged-in person """ @@ -224,7 +232,9 @@ def groups_child_groups(request: HttpRequest) -> HttpResponse: return render(request, "core/groups_child_groups.html", context) -@permission_required("core.edit_person", fn=objectgetter_optional(Person, "request.user.person", True)) +@permission_required( + "core.edit_person", fn=objectgetter_optional(Person, "request.user.person", True) +) def edit_person(request: HttpRequest, id_: Optional[int] = None) -> HttpResponse: """ Edit view for a single person, defaulting to logged-in person """ @@ -301,7 +311,9 @@ def system_status(request: HttpRequest) -> HttpResponse: return render(request, "core/system_status.html", context) -@permission_required("core.mark_notification_as_read", fn=objectgetter_optional(Notification, None, False)) +@permission_required( + "core.mark_notification_as_read", fn=objectgetter_optional(Notification, None, False) +) def notification_mark_read(request: HttpRequest, id_: int) -> HttpResponse: """ Mark a notification read """ @@ -329,7 +341,9 @@ def announcements(request: HttpRequest) -> HttpResponse: return render(request, "core/announcement/list.html", context) -@permission_required("core.create_or_edit_announcement", fn=objectgetter_optional(Announcement, None, False)) +@permission_required( + "core.create_or_edit_announcement", fn=objectgetter_optional(Announcement, None, False) +) def announcement_form(request: HttpRequest, id_: Optional[int] = None) -> HttpResponse: """ View to create or edit an announcement """ @@ -339,10 +353,7 @@ def announcement_form(request: HttpRequest, id_: Optional[int] = None) -> HttpRe if announcement: # Edit form for existing announcement - form = AnnouncementForm( - request.POST or None, - instance=announcement - ) + form = AnnouncementForm(request.POST or None, instance=announcement) context["mode"] = "edit" else: # Empty form to create new announcement @@ -361,7 +372,9 @@ def announcement_form(request: HttpRequest, id_: Optional[int] = None) -> HttpRe return render(request, "core/announcement/form.html", context) -@permission_required("core.delete_announcement", fn=objectgetter_optional(Announcement, None, False)) +@permission_required( + "core.delete_announcement", fn=objectgetter_optional(Announcement, None, False) +) def delete_announcement(request: HttpRequest, id_: int) -> HttpResponse: """ View to delete an announcement """ @@ -377,8 +390,8 @@ def delete_announcement(request: HttpRequest, id_: int) -> HttpResponse: def searchbar_snippets(request: HttpRequest) -> HttpResponse: """ View to return HTML snippet with searchbar autocompletion results """ - query = request.GET.get('q', '') - limit = int(request.GET.get('limit', '5')) + query = request.GET.get("q", "") + limit = int(request.GET.get("limit", "5")) results = SearchQuerySet().filter(text=AutoQuery(query))[:limit] context = {"results": results} @@ -398,7 +411,12 @@ class PermissionSearchView(PermissionRequiredMixin, SearchView): return render(self.request, self.template, context) -def preferences(request: HttpRequest, registry_name: str = "person", pk: Optional[int] = None, section: Optional[str] = None) -> HttpResponse: +def preferences( + request: HttpRequest, + registry_name: str = "person", + pk: Optional[int] = None, + section: Optional[str] = None, +) -> HttpResponse: """ View for changing preferences """ context = {}