diff --git a/aleksis/core/apps.py b/aleksis/core/apps.py index e5e2d4008c132c24341d26f6532c254167b13741..558c2a060899cfb35bf7dd5d3ec7eff3ef3d19ec 100644 --- a/aleksis/core/apps.py +++ b/aleksis/core/apps.py @@ -1,4 +1,4 @@ -from typing import Any, Optional +from typing import TYPE_CHECKING, Any, Optional import django.apps from django.apps import apps @@ -26,6 +26,9 @@ from .util.core_helpers import ( ) from .util.sass_helpers import clean_scss +if TYPE_CHECKING: + from django.contrib.auth.models import User + class CoreConfig(AppConfig): name = "aleksis.core" diff --git a/aleksis/core/data_checks.py b/aleksis/core/data_checks.py index f16ab2e9f0b3b39ea86d9f53112a4619fb960e90..c5c64f821b163ce53fa000e4169c639002cb91c1 100644 --- a/aleksis/core/data_checks.py +++ b/aleksis/core/data_checks.py @@ -1,5 +1,6 @@ import logging from datetime import timedelta +from typing import TYPE_CHECKING from django.apps import apps from django.contrib.contenttypes.models import ContentType @@ -18,6 +19,9 @@ from .util.celery_progress import ProgressRecorder, recorded_task from .util.core_helpers import get_site_preferences from .util.email import send_email +if TYPE_CHECKING: + from aleksis.core.models import DataCheckResult + class SolveOption: """Define a solve option for one or more data checks. @@ -49,7 +53,7 @@ class SolveOption: verbose_name: str = "" @classmethod - def solve(cls, check_result: "DataCheckResult"): + def solve(cls, check_result: DataCheckResult): pass @@ -60,7 +64,7 @@ class IgnoreSolveOption(SolveOption): verbose_name = _("Ignore problem") @classmethod - def solve(cls, check_result: "DataCheckResult"): + def solve(cls, check_result: DataCheckResult): """Mark the object as solved without doing anything more.""" check_result.solved = True check_result.save() @@ -175,7 +179,7 @@ class DataCheck(RegistryObject): cls.delete_old_results() @classmethod - def solve(cls, check_result: "DataCheckResult", solve_option: str): + def solve(cls, check_result: DataCheckResult, solve_option: str): """Execute a solve option for an object detected by this check. :param check_result: The result item from database @@ -192,7 +196,7 @@ class DataCheck(RegistryObject): solve_option_obj.solve(check_result) @classmethod - def register_result(cls, instance) -> "DataCheckResult": + def register_result(cls, instance) -> DataCheckResult: """Register an object with data issues in the result database. :param instance: The affected object @@ -284,7 +288,7 @@ class DeactivateDashboardWidgetSolveOption(SolveOption): verbose_name = _("Deactivate DashboardWidget") @classmethod - def solve(cls, check_result: "DataCheckResult"): + def solve(cls, check_result: DataCheckResult): widget = check_result.related_object widget.active = False widget.save() @@ -316,10 +320,10 @@ def field_validation_data_check_factory(app_name: str, model_name: str, field_na class FieldValidationDataCheck(DataCheck): name = f"field_validation_{slugify(model_name)}_{slugify(field_name)}" - verbose_name = _( - "Validate field %s of model %s." % (field_name, app_name + "." + model_name) + verbose_name = _("Validate field {field} of model {model}.").format( + field=field_name, model=app_name + "." + model_name ) - problem_name = _("The field %s couldn't be validated successfully." % field_name) + problem_name = _("The field {} couldn't be validated successfully.").format(field_name) solve_options = { IgnoreSolveOption.name: IgnoreSolveOption, } @@ -330,7 +334,7 @@ def field_validation_data_check_factory(app_name: str, model_name: str, field_na for obj in model.objects.all(): try: model._meta.get_field(field_name).validate(getattr(obj, field_name), obj) - except ValidationError as e: + except ValidationError: logging.info(f"Check {model_name} {obj}") cls.register_result(obj) diff --git a/aleksis/core/filters.py b/aleksis/core/filters.py index 0bccc2664489f3698fc77bbe7299a9210ed81dac..5b93e37dd008556b940eac6409ef0b94e3f586f2 100644 --- a/aleksis/core/filters.py +++ b/aleksis/core/filters.py @@ -23,10 +23,7 @@ class MultipleCharFilter(CharFilter): def filter(self, qs, value): # noqa q = None for field in self.fields: - if not q: - q = Q(**{field: value}) - else: - q = q | Q(**{field: value}) + q = Q(**{field: value}) if not q else q | Q(**{field: value}) return qs.filter(q) def __init__(self, fields: Sequence[str], *args, **kwargs): diff --git a/aleksis/core/forms.py b/aleksis/core/forms.py index 03d08a968a972df7994c4c784495b8feb91765d9..841ff59abbf6a83dc256549e101e240c39a38135 100644 --- a/aleksis/core/forms.py +++ b/aleksis/core/forms.py @@ -542,7 +542,6 @@ class AssignPermissionForm(forms.Form): all_objects = self.cleaned_data["all_objects"] objects = self.cleaned_data["objects"] permission_name = f"{self.permission.content_type.app_label}.{self.permission.codename}" - created = 0 # Create permissions for users for person in persons: @@ -621,7 +620,7 @@ class AccountRegisterForm(SignupForm, ExtensibleForm): def __init__(self, *args, **kwargs): request = kwargs.pop("request", None) - super(AccountRegisterForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) person = None if request.session.get("account_verified_email"): @@ -629,16 +628,16 @@ class AccountRegisterForm(SignupForm, ExtensibleForm): try: person = Person.objects.get(email=email) - except (Person.DoesNotExist, Person.MultipleObjectsReturned): - raise SuspiciousOperation() + except (Person.DoesNotExist, Person.MultipleObjectsReturned) as exc: + raise SuspiciousOperation from exc elif request.session.get("invitation_code"): try: invitation = PersonInvitation.objects.get( key=request.session.get("invitation_code") ) - except PersonInvitation.DoesNotExist: - raise SuspiciousOperation() + except PersonInvitation.DoesNotExist as exc: + raise SuspiciousOperation from exc person = invitation.person @@ -667,9 +666,8 @@ class AccountRegisterForm(SignupForm, ExtensibleForm): person_qs = Person.objects.filter(pk=self.instance.pk) else: person_qs = Person.objects.filter(email=data["email"]) - if not person_qs.exists(): - if get_site_preferences()["account__auto_create_person"]: - Person.objects.create(user=user, **data) + if not person_qs.exists() and get_site_preferences()["account__auto_create_person"]: + Person.objects.create(user=user, **data) if person_qs.exists(): person = person_qs.first() for field, value in data.items(): @@ -682,8 +680,8 @@ class AccountRegisterForm(SignupForm, ExtensibleForm): try: invitation = PersonInvitation.objects.get(key=invitation_code) - except PersonInvitation.DoesNotExist: - raise SuspiciousOperation() + except PersonInvitation.DoesNotExist as exc: + raise SuspiciousOperation from exc accept_invitation(invitation, request, user) self.custom_signup(request, user) @@ -782,7 +780,7 @@ class ActionForm(forms.Form): if selected_objects.count() < self.cleaned_data["selected_objects"].count(): raise ValidationError( _("You do not have permission to run {} on all selected objects.").format( - getattr(value, "short_description", value.__name__) + getattr(action, "short_description", action.__name__) ) ) return self.cleaned_data["selected_objects"] diff --git a/aleksis/core/management/commands/vite.py b/aleksis/core/management/commands/vite.py index 747f328ea82060b2b0148d9c928a498e018cefc9..fe8b42312f4f26fa4dbb5389dc6382e4536bf0c2 100644 --- a/aleksis/core/management/commands/vite.py +++ b/aleksis/core/management/commands/vite.py @@ -17,7 +17,7 @@ class Command(BaseYarnCommand): parser.add_argument("--no-install", action="store_true", default=False) def handle(self, *args, **options): - super(Command, self).handle(*args, **options) + super().handle(*args, **options) # Inject settings into Vite write_vite_values(os.path.join(settings.NODE_MODULES_ROOT, "django-vite-values.json")) diff --git a/aleksis/core/managers.py b/aleksis/core/managers.py index 1c9266ab7cb65f8a2109ad4863fcc1a2614958c3..0371942562d66f8a161fcea58a2873a435a74350 100644 --- a/aleksis/core/managers.py +++ b/aleksis/core/managers.py @@ -1,5 +1,5 @@ from datetime import date -from typing import Union +from typing import TYPE_CHECKING, Union from django.apps import apps from django.contrib.sites.managers import CurrentSiteManager as _CurrentSiteManager @@ -10,6 +10,9 @@ from calendarweek import CalendarWeek from django_cte import CTEManager, CTEQuerySet from polymorphic.managers import PolymorphicManager, PolymorphicQuerySet +if TYPE_CHECKING: + from .models import SchoolTerm + class AlekSISBaseManager(_CurrentSiteManager): """Base manager for AlekSIS model customisation.""" diff --git a/aleksis/core/mixins.py b/aleksis/core/mixins.py index 2e7aba6784f2012de091b4db4248eef88c741e30..dce75e8bdf36ace34e7ba77f3030b3aa6d36c572 100644 --- a/aleksis/core/mixins.py +++ b/aleksis/core/mixins.py @@ -1,17 +1,14 @@ # flake8: noqa: DJ12 import os -from datetime import datetime from typing import Any, Callable, ClassVar, Iterable, List, Optional, Union from django.conf import settings from django.contrib import messages from django.contrib.auth.views import LoginView, RedirectURLMixin -from django.contrib.contenttypes.models import ContentType -from django.contrib.sites.managers import CurrentSiteManager from django.contrib.sites.models import Site from django.db import models -from django.db.models import JSONField, QuerySet +from django.db.models import JSONField from django.db.models.fields import CharField, TextField from django.forms.forms import BaseForm from django.forms.models import ModelForm, ModelFormMetaclass, fields_for_model @@ -21,7 +18,6 @@ from django.utils.translation import gettext as _ from django.views.generic import CreateView, UpdateView from django.views.generic.edit import DeleteView, ModelFormMixin -import recurring_ical_events import reversion from django_ical.feedgenerator import ITEM_ELEMENT_FIELD_MAP from dynamic_preferences.settings import preferences_settings @@ -138,9 +134,6 @@ class ExtensibleModel(models.Model, metaclass=_ExtensibleModelBase): site = models.ForeignKey( Site, on_delete=models.CASCADE, default=settings.SITE_ID, editable=False, related_name="+" ) - objects = AlekSISBaseManager() - # FIXME this is now broken, remove sites framework - objects_all = models.Manager() managed_by_app_label = models.CharField( max_length=255, @@ -149,8 +142,27 @@ class ExtensibleModel(models.Model, metaclass=_ExtensibleModelBase): blank=True, ) + extended_data = JSONField(default=dict, editable=False) + extra_permissions = [] + # FIXME this is now broken, remove sites framework + objects_all = models.Manager() + objects = AlekSISBaseManager() + + class Meta: + abstract = True + + def save(self, *args, **kwargs): + """Ensure all functionality of our extensions that needs saving gets it.""" + # For auto-created remote syncable fields + if hasattr(self, "_save_reverse"): + for related in self._save_reverse: + related.save() + del self._save_reverse + + super().save(*args, **kwargs) + def get_absolute_url(self) -> str: """Get the URL o a view representing this model instance.""" pass @@ -178,8 +190,6 @@ class ExtensibleModel(models.Model, metaclass=_ExtensibleModelBase): return versions_with_changes - extended_data = JSONField(default=dict, editable=False) - @classmethod def _safe_add(cls, obj: Any, name: Optional[str]) -> None: # Decide the name for the attribute @@ -276,10 +286,7 @@ class ExtensibleModel(models.Model, metaclass=_ExtensibleModelBase): @_virtual_fk.setter def _virtual_fk(self, value: Optional[models.Model] = None) -> None: - if value is None: - id_field_val = None - else: - id_field_val = getattr(value, to_field) + id_field_val = None if value is None else getattr(value, to_field) setattr(self, id_field_name, id_field_val) # Add property to wrap get/set on foreign model instance @@ -305,12 +312,15 @@ class ExtensibleModel(models.Model, metaclass=_ExtensibleModelBase): @classmethod def syncable_fields( - cls, recursive: bool = True, exclude_remotes: list = [] + cls, recursive: bool = True, exclude_remotes: list = None ) -> list[models.Field]: """Collect all fields that can be synced on a model. If recursive is True, it recurses into related models and generates virtual proxy fields to access fields in related models.""" + if not exclude_remotes: + exclude_remotes = [] + fields = [] for field in cls._meta.get_fields(): if field.is_relation and field.one_to_one and recursive: @@ -327,7 +337,10 @@ class ExtensibleModel(models.Model, metaclass=_ExtensibleModelBase): ): # generate virtual field names for proxy access name = f"_{field.name}__{subfield.name}" - verbose_name = f"{field.name} ({field.related_model._meta.verbose_name}) → {subfield.verbose_name}" + verbose_name = ( + f"{field.name} ({field.related_model._meta.verbose_name})" + " → {subfield.verbose_name}" + ) if not hasattr(cls, name): # Add proxy properties to handle access to related model @@ -341,7 +354,7 @@ class ExtensibleModel(models.Model, metaclass=_ExtensibleModelBase): { "name": name, "verbose_name": verbose_name, - "to_python": lambda v: subfield.to_python(v), + "to_python": lambda v: subfield.to_python(v), # noqa: B023 }, ) ) @@ -371,19 +384,6 @@ class ExtensibleModel(models.Model, metaclass=_ExtensibleModelBase): """Annotate a ``ObjectPermissionChecker`` for use with permission system.""" self._permission_checker = checker - def save(self, *args, **kwargs): - """Ensure all functionality of our extensions that needs saving gets it.""" - # For auto-created remote syncable fields - if hasattr(self, "_save_reverse"): - for related in self._save_reverse: - related.save() - del self._save_reverse - - super().save(*args, **kwargs) - - class Meta: - abstract = True - class _ExtensiblePolymorphicModelBase(_ExtensibleModelBase, PolymorphicModelBase): """Base class for extensible, polymorphic models.""" @@ -401,7 +401,7 @@ class ExtensiblePolymorphicModel( abstract = True -class PureDjangoModel(object): +class PureDjangoModel: """No-op mixin to mark a model as deliberately not using ExtensibleModel.""" pass @@ -423,10 +423,7 @@ class _ExtensibleFormMetaclass(ModelFormMetaclass): x = super().__new__(cls, name, bases, dct) # Enforce a default for the base layout for forms that o not specify one - if hasattr(x, "layout"): - base_layout = x.layout.elements - else: - base_layout = [] + base_layout = x.layout.elements if hasattr(x, "layout") else [] x.base_layout = base_layout x.layout = Layout(*base_layout) diff --git a/aleksis/core/models.py b/aleksis/core/models.py index e52bf4928231598268b076ab5582282d095112d9..b193d26340f3a6c1d52f2547340c8d42787aa4a3 100644 --- a/aleksis/core/models.py +++ b/aleksis/core/models.py @@ -1,10 +1,8 @@ # flake8: noqa: DJ01 import base64 -import hmac -import uuid from datetime import date, datetime, timedelta -from typing import Any, Iterable, Iterator, List, Optional, Sequence, Union -from urllib.parse import urljoin, urlparse +from typing import TYPE_CHECKING, Any, Iterable, Iterator, List, Optional, Sequence, Union +from urllib.parse import urljoin from django.conf import settings from django.contrib.auth import get_user_model @@ -16,9 +14,8 @@ from django.contrib.sites.models import Site from django.contrib.sites.shortcuts import get_current_site from django.core.exceptions import ValidationError from django.core.validators import MaxValueValidator -from django.db import models, transaction +from django.db import models from django.db.models import Q, QuerySet -from django.db.models.signals import m2m_changed from django.dispatch import receiver from django.forms.widgets import Media from django.urls import reverse @@ -44,7 +41,6 @@ from guardian.shortcuts import get_objects_for_user from icalendar import vCalAddress, vText from icalendar.prop import vRecur from invitations import signals -from invitations.adapters import get_invitations_adapter from invitations.base_invitation import AbstractBaseInvitation from invitations.models import Invitation from model_utils import FieldTracker @@ -62,9 +58,7 @@ from recurrence.fields import RecurrenceField from timezone_field import TimeZoneField from aleksis.core.data_checks import ( - BrokenDashboardWidgetDataCheck, DataCheck, - field_validation_data_check_factory, ) from .managers import ( @@ -91,6 +85,9 @@ from .util.core_helpers import generate_random_code, get_site_preferences, now_t from .util.email import send_email from .util.model_helpers import ICONS +if TYPE_CHECKING: + from django.contrib.auth.models import User + FIELD_CHOICES = ( ("BooleanField", _("Boolean (Yes/No)")), ("CharField", _("Text (one line)")), @@ -208,8 +205,11 @@ class Person(ExtensibleModel): verbose_name=_("Additional name(s)"), max_length=255, blank=True ) - short_name = models.CharField( - verbose_name=_("Short name"), max_length=255, blank=True, null=True # noqa + short_name = models.CharField( # noqa + verbose_name=_("Short name"), + max_length=255, + blank=True, + null=True, ) street = models.CharField(verbose_name=_("Street"), max_length=255, blank=True) @@ -440,9 +440,8 @@ class Person(ExtensibleModel): pattern = pattern or get_site_preferences()["account__primary_group_pattern"] field = field or get_site_preferences()["account__primary_group_field"] - if pattern: - if force or not self.primary_group: - self.primary_group = self.member_of.filter(**{f"{field}__regex": pattern}).first() + if pattern and (force or not self.primary_group): + self.primary_group = self.member_of.filter(**{f"{field}__regex": pattern}).first() def notify_about_changed_data( self, changed_fields: Iterable[str], recipients: Optional[List[str]] = None @@ -530,8 +529,11 @@ class Group(SchoolTermRelatedExtensibleModel): icon_ = "account-multiple-outline" name = models.CharField(verbose_name=_("Long name"), max_length=255) - short_name = models.CharField( - verbose_name=_("Short name"), max_length=255, blank=True, null=True # noqa + short_name = models.CharField( # noqa + verbose_name=_("Short name"), + max_length=255, + blank=True, + null=True, ) members = models.ManyToManyField( @@ -1406,6 +1408,9 @@ class UserAdditionalAttributes(models.Model, PureDjangoModel): attributes = models.JSONField(verbose_name=_("Additional attributes"), default=dict) + def __str__(self): + return str(self.user) + @classmethod def get_user_attribute( cls, username: str, attribute: str, default: Optional[Any] = None diff --git a/aleksis/core/schema/base.py b/aleksis/core/schema/base.py index 61836a66d69681609e22134d46ed6f0b63b38b20..a5ade224d4855752f2f55bd338176f3c1b71c375 100644 --- a/aleksis/core/schema/base.py +++ b/aleksis/core/schema/base.py @@ -140,11 +140,11 @@ class DjangoFilterMixin: raise NotImplementedError(f"{cls.__name__} must implement class Meta for filtering.") if hasattr(meta, "filterset_class"): - filterset = getattr(meta, "filterset_class") + filterset = meta.filterset_class if filterset is not None: return filterset - model: Model = getattr(meta, "model") + model: Model = meta.model fields = getattr(meta, "filter_fields", None) if not model: diff --git a/aleksis/core/tables.py b/aleksis/core/tables.py index 8963981bb22412b51abca69120b90dc3b0e18d72..5db97c470d7fbbcd24cd7550fd609073e3159214 100644 --- a/aleksis/core/tables.py +++ b/aleksis/core/tables.py @@ -165,7 +165,7 @@ class PermissionDeleteColumn(tables.LinkColumn): text=_("Delete"), attrs={"a": {"class": "btn-flat waves-effect waves-red red-text"}}, verbose_name=_("Actions"), - **kwargs + **kwargs, ) diff --git a/aleksis/core/templatetags/data_helpers.py b/aleksis/core/templatetags/data_helpers.py index ab2309f260dd6b039cc722bae86fa71637967a1f..2c08f61d82846bf3459c8b3a79ccfe63eb63ae18 100644 --- a/aleksis/core/templatetags/data_helpers.py +++ b/aleksis/core/templatetags/data_helpers.py @@ -13,7 +13,7 @@ def get_dict(value: Any, arg: Any) -> Any: """Get an attribute of an object dynamically from a string name.""" if hasattr(value, str(arg)): return getattr(value, arg) - elif hasattr(value, "keys") and arg in value.keys(): + elif hasattr(value, "keys") and arg in value: return value[arg] elif str(arg).isnumeric() and len(value) > int(arg): return value[int(arg)] diff --git a/aleksis/core/templatetags/html_helpers.py b/aleksis/core/templatetags/html_helpers.py index 98608a084574994400bffe46c50f944b52296e24..1005cf053b1354bef598d180c5e8659803fe29b5 100644 --- a/aleksis/core/templatetags/html_helpers.py +++ b/aleksis/core/templatetags/html_helpers.py @@ -23,8 +23,8 @@ def add_class_to_el(value: str, arg: str) -> str: el, cls = arg.split(",") soup = BeautifulSoup(value, "html.parser") - for el in soup.find_all(el): - el["class"] = el.get("class", []) + [cls] + for sub_el in soup.find_all(el): + sub_el["class"] = sub_el.get("class", []) + [cls] return str(soup) @@ -58,7 +58,8 @@ def generate_random_id(prefix: str, length: int = 10) -> str: {% generate_random_id "prefix-" %} """ return prefix + "".join( - random.choice(string.ascii_lowercase) for i in range(length) # noqa: S311 + random.choice(string.ascii_lowercase) # noqa: S311 + for i in range(length) ) diff --git a/aleksis/core/util/apps.py b/aleksis/core/util/apps.py index 26f0752dc352cf0793ad7a9c7d70f95e015dd457..fce211f57ea53d280841d7e74ce99d30f091827e 100644 --- a/aleksis/core/util/apps.py +++ b/aleksis/core/util/apps.py @@ -15,6 +15,8 @@ from .core_helpers import copyright_years from .spdx import LICENSES if TYPE_CHECKING: + from django.contrib.auth.models import User + from oauth2_provider.models import AbstractApplication diff --git a/aleksis/core/util/auth_helpers.py b/aleksis/core/util/auth_helpers.py index ca80aeae4a59ac069023465559d599f929bab6d8..018c528bd75d8942bb5ae1ebdd6ff3b1aae3a17c 100644 --- a/aleksis/core/util/auth_helpers.py +++ b/aleksis/core/util/auth_helpers.py @@ -77,7 +77,7 @@ class AppScopes(BaseScopes): application: Optional[AbstractApplication] = None, request: Optional[HttpRequest] = None, *args, - **kwargs + **kwargs, ) -> list[str]: scopes = [] for app in AppConfig.__subclasses__(): @@ -92,7 +92,7 @@ class AppScopes(BaseScopes): application: Optional[AbstractApplication] = None, request: Optional[HttpRequest] = None, *args, - **kwargs + **kwargs, ) -> list[str]: scopes = [] for app in AppConfig.__subclasses__(): diff --git a/aleksis/core/util/core_helpers.py b/aleksis/core/util/core_helpers.py index 9329dda6192b554efa5e546a0160b5225e09da17..a04aaa2faef16f200e3a0fe175f5c4b1fe410e65 100644 --- a/aleksis/core/util/core_helpers.py +++ b/aleksis/core/util/core_helpers.py @@ -5,7 +5,7 @@ from importlib import import_module, metadata from itertools import groupby from operator import itemgetter from types import ModuleType -from typing import Any, Callable, Dict, Optional, Sequence, Union +from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Sequence, Union from warnings import warn from django.conf import settings @@ -27,6 +27,11 @@ from cachalot.signals import post_invalidation from cache_memoize import cache_memoize from icalendar import Calendar, Event, Todo +if TYPE_CHECKING: + from django.contrib.contenttypes.models import ContentType + + from favicon.models import Favicon + def copyright_years(years: Sequence[int], separator: str = ", ", joiner: str = "–") -> str: """Take a sequence of integers and produces a string with ranges. @@ -172,7 +177,8 @@ def get_or_create_favicon(title: str, default: str, is_favicon: bool = False) -> changed = True if created: - favicon.faviconImage.save(os.path.basename(default), File(open(default, "rb"))) + with open(default, "rb") as f: + favicon.faviconImage.save(os.path.basename(default), File(f)) changed = True if changed: @@ -213,12 +219,10 @@ def has_person(obj: Union[HttpRequest, Model]) -> bool: return False person = getattr(obj, "person", None) - if person is None: + if person is None or getattr(person, "is_dummy", False): return False - elif getattr(person, "is_dummy", False): - return False - else: - return True + + return True def custom_information_processor(request: Union[HttpRequest, None]) -> dict: @@ -533,10 +537,8 @@ class ExtendedICal20Feed(feedgenerator.ICal20Feed): def write_items(self, calendar, with_meta=True): for item in self.items: component_type = item.get("component_type") - if component_type == "todo": - element = Todo() - else: - element = Event() + element = Todo() if component_type == "todo" else Event() + for ifield, efield in feedgenerator.ITEM_ELEMENT_FIELD_MAP: val = item.get(ifield) if val is not None: diff --git a/aleksis/core/util/notifications.py b/aleksis/core/util/notifications.py index 061b8d3d8fae61f68b1c3f2fa9e9f3f421491292..183858f9407ce5afb04c1dd8b60693ec4973cd02 100644 --- a/aleksis/core/util/notifications.py +++ b/aleksis/core/util/notifications.py @@ -1,6 +1,6 @@ """Utility code for notification system.""" -from typing import Sequence, Union +from typing import TYPE_CHECKING, Sequence, Union from django.apps import apps from django.conf import settings @@ -17,6 +17,9 @@ try: except ImportError: TwilioClient = None +if TYPE_CHECKING: + from ..models import Notification + def send_templated_sms( template_name: str, from_number: str, recipient_list: Sequence[str], context: dict @@ -95,7 +98,7 @@ def get_notification_choices() -> list: by the administrator (by selecting a subset of these choices). """ choices = [] - for channel, (name, check, send) in _CHANNELS_MAP.items(): + for channel, (name, check, send) in _CHANNELS_MAP.items(): # noqa: B007 if check(): choices.append((channel, name)) return choices diff --git a/aleksis/core/util/predicates.py b/aleksis/core/util/predicates.py index bdf3a03a3944325ab957e8586fce6fa24a693836..3d91751938c785baf694f379b4126fb879e6a0fc 100644 --- a/aleksis/core/util/predicates.py +++ b/aleksis/core/util/predicates.py @@ -12,9 +12,8 @@ from rules import predicate from ..mixins import ExtensibleModel from ..models import Group, PersonalEvent -from .core_helpers import get_content_type_by_perm, get_site_preferences +from .core_helpers import get_content_type_by_perm, get_site_preferences, queryset_rules_filter from .core_helpers import has_person as has_person_helper -from .core_helpers import queryset_rules_filter def permission_validator(request: HttpRequest, perm: str) -> bool: diff --git a/aleksis/core/util/spdx.py b/aleksis/core/util/spdx.py index 76452ba4a3c5a03b2f065350b13c3adde7a90d57..39a02d95349ae72876090c9d575edbde6b805d92 100644 --- a/aleksis/core/util/spdx.py +++ b/aleksis/core/util/spdx.py @@ -1,5 +1,5 @@ import json import os -with open(os.path.join(os.path.dirname(__file__), "licenses.json"), "r") as f: +with open(os.path.join(os.path.dirname(__file__), "licenses.json")) as f: LICENSES = json.load(f) diff --git a/aleksis/core/views.py b/aleksis/core/views.py index f9e21e5b57472664e96bded0ebb471a11aff014b..f9e87eca9634022de2e86c8c64225ed29b9e8d35 100644 --- a/aleksis/core/views.py +++ b/aleksis/core/views.py @@ -8,7 +8,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.models import Group as DjangoGroup from django.contrib.auth.models import Permission, User from django.contrib.contenttypes.models import ContentType -from django.core.exceptions import PermissionDenied, ValidationError +from django.core.exceptions import BadRequest, PermissionDenied, ValidationError from django.core.paginator import Paginator from django.db.models import QuerySet from django.forms.models import BaseModelForm, modelform_factory @@ -151,11 +151,8 @@ from .util.pdf import render_pdf class LogoView(View): def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: image = request.site.preferences["theme__logo"] - if image: - image = image.url - else: - image = static("img/aleksis-banner.svg") - return redirect(image) + image_url = image.url if image else static("img/aleksis-banner.svg") + return redirect(image_url) class RenderPDFView(TemplateView): @@ -178,9 +175,8 @@ class ServiceWorkerView(View): """ def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: - return HttpResponse( - open(settings.SERVICE_WORKER_PATH, "rt"), content_type="application/javascript" - ) + with open(settings.SERVICE_WORKER_PATH) as f: + return HttpResponse(f, content_type="application/javascript") class ManifestView(View): @@ -232,23 +228,13 @@ def index(request: HttpRequest) -> HttpResponse: if has_person(request.user): person = request.user.person widgets = person.dashboard_widgets - activities = person.activities.all().order_by("-created")[:5] - notifications = person.notifications.filter(send_at__lte=timezone.now()).order_by( - "-created" - )[:5] - unread_notifications = person.notifications.filter( - send_at__lte=timezone.now(), read=False - ).order_by("-created") announcements = Announcement.objects.at_time().for_person(person) activities = person.activities.all().order_by("-created")[:5] - else: person = None - activities = [] - notifications = [] - unread_notifications = [] widgets = [] announcements = [] + activities = [] if len(widgets) == 0: # Use default dashboard if there are no widgets @@ -441,15 +427,14 @@ def edit_group(request: HttpRequest, id_: Optional[int] = None) -> HttpResponse: else: raise PermissionDenied() - if request.method == "POST": - if edit_group_form.is_valid(): - with reversion.create_revision(): - set_user(request.user) - group = edit_group_form.save(commit=True) + if edit_group_form.is_valid(): + with reversion.create_revision(): + set_user(request.user) + group = edit_group_form.save(commit=True) - messages.success(request, _("The group has been saved.")) + messages.success(request, _("The group has been saved.")) - return redirect("group_by_id", group.pk) + return redirect("group_by_id", group.pk) context["edit_group_form"] = edit_group_form @@ -551,12 +536,11 @@ def announcement_form(request: HttpRequest, id_: Optional[int] = None) -> HttpRe form = AnnouncementForm(request.POST or None) context["mode"] = "add" - if request.method == "POST": - if form.is_valid(): - form.save() + if form.is_valid(): + form.save() - messages.success(request, _("The announcement has been saved.")) - return redirect("announcements") + messages.success(request, _("The announcement has been saved.")) + return redirect("announcements") context["form"] = form @@ -725,13 +709,12 @@ def edit_additional_field(request: HttpRequest, id_: Optional[int] = None) -> Ht else: raise PermissionDenied() - if request.method == "POST": - if edit_additional_field_form.is_valid(): - edit_additional_field_form.save(commit=True) + if edit_additional_field_form.is_valid(): + edit_additional_field_form.save(commit=True) - messages.success(request, _("The additional field has been saved.")) + messages.success(request, _("The additional field has been saved.")) - return redirect("additional_fields") + return redirect("additional_fields") context["edit_additional_field_form"] = edit_additional_field_form @@ -785,13 +768,12 @@ def edit_group_type(request: HttpRequest, id_: Optional[int] = None) -> HttpResp # Empty form to create a new group_type edit_group_type_form = EditGroupTypeForm(request.POST or None) - if request.method == "POST": - if edit_group_type_form.is_valid(): - edit_group_type_form.save(commit=True) + if edit_group_type_form.is_valid(): + edit_group_type_form.save(commit=True) - messages.success(request, _("The group type has been saved.")) + messages.success(request, _("The group type has been saved.")) - return redirect("group_types") + return redirect("group_types") context["edit_group_type_form"] = edit_group_type_form @@ -1388,10 +1370,10 @@ class AccountRegisterView(SignupView): and not request.session.get("invitation_code") ): raise PermissionDenied() - return super(AccountRegisterView, self).dispatch(request, *args, **kwargs) + return super().dispatch(request, *args, **kwargs) def get_form_kwargs(self): - kwargs = super(AccountRegisterView, self).get_form_kwargs() + kwargs = super().get_form_kwargs() kwargs["request"] = self.request return kwargs @@ -1564,8 +1546,8 @@ class ObjectRepresentationView(View): """Get the model by app label and model name.""" try: return apps.get_model(app_label, model) - except LookupError: - raise Http404() + except LookupError as exc: + raise Http404 from exc def get_object(self, request: HttpRequest, app_label: str, model: str, pk: int): """Get the object by app label, model name and primary key.""" @@ -1574,8 +1556,8 @@ class ObjectRepresentationView(View): try: return self.model.objects.get(pk=pk) - except self.model.DoesNotExist: - raise Http404() + except self.model.DoesNotExist as exc: + raise Http404 from exc def get( self, diff --git a/conftest.py b/conftest.py index 2aa286cdb37550088652b10cced7bfc3d3b56aa9..e0de0cc90762c0d5230d0aaa7c296d0b7d121b24 100644 --- a/conftest.py +++ b/conftest.py @@ -1 +1 @@ -pytest_plugins = ("celery.contrib.pytest", ) +pytest_plugins = ("celery.contrib.pytest",) diff --git a/docs/conf.py b/docs/conf.py index 4f14591b931a77ed87e0e2cc7af56b48805e820b..8dbdcee464e976d14ea50a117c447c1062e78fe3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -117,9 +117,7 @@ html_static_path = ["_static"] # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', # 'searchbox.html']``. # -html_sidebars = { - "**": ["logo-text.html", "globaltoc.html", "localtoc.html", "searchbox.html"] -} +html_sidebars = {"**": ["logo-text.html", "globaltoc.html", "localtoc.html", "searchbox.html"]} # -- Options for HTMLHelp output ---------------------------------------------