diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1221e3fd3..604ab19ee 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,6 +8,7 @@ ci: skip: [ 'pip-audit', 'yesqa', + 'creosote', 'no-commit-to-branch', # 'hadolint-docker', 'docker-compose-check', @@ -52,9 +53,9 @@ repos: rev: v3.15.0 hooks: - id: pyupgrade - args: ['--keep-runtime-typing', '--py310-plus'] + args: ['--keep-runtime-typing', '--py311-plus'] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.1 + rev: v0.1.4 hooks: - id: ruff args: ['--fix', '--exit-non-zero-on-fix'] @@ -100,7 +101,7 @@ repos: additional_dependencies: - tomli - repo: https://github.com/psf/black - rev: 23.10.0 + rev: 23.10.1 hooks: - id: black # Mypy is temporarily disabled until the SQLAlchemy 2.0 migration is complete @@ -148,6 +149,26 @@ repos: args: ['-c', 'pyproject.toml'] additional_dependencies: - 'bandit[toml]' + - repo: https://github.com/fredrikaverpil/creosote + rev: v3.0.0 + hooks: + - id: creosote + args: + - --venv=.venv + - --path=funnel + - --path=tests + - --path=migrations/versions + - --deps-file=requirements/base.in + - --exclude-dep=argon2-cffi # Optional dep for passlib + - --exclude-dep=bcrypt # Optional dep for passlib + - --exclude-dep=gunicorn # Not imported, used as server + - --exclude-dep=linkify-it-py # Optional dep for markdown-it-py + - --exclude-dep=psycopg # Optional dep for SQLAlchemy + - --exclude-dep=rq-dashboard # Creosote fails to recognise the import + - --exclude-dep=tzdata # Data-only dep, therefore no import statement + - --exclude-dep=urllib3 # Required to silence a pip-audit warning + - --exclude-dep=wtforms-sqlalchemy # Temp dep on an unreleased git branch + - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.5.0 hooks: diff --git a/funnel/devtest.py b/funnel/devtest.py index 21cdb251f..ae00c228e 100644 --- a/funnel/devtest.py +++ b/funnel/devtest.py @@ -13,8 +13,7 @@ import weakref from collections.abc import Callable, Iterable from secrets import token_urlsafe -from typing import Any, NamedTuple -from typing_extensions import Protocol +from typing import Any, NamedTuple, Protocol from flask import Flask diff --git a/funnel/forms/profile.py b/funnel/forms/profile.py index 8236caf2a..046c656cd 100644 --- a/funnel/forms/profile.py +++ b/funnel/forms/profile.py @@ -91,7 +91,9 @@ class ProfileTransitionForm(forms.Form): def set_queries(self) -> None: """Prepare form for use.""" - self.transition.choices = list(self.edit_obj.state.transitions().items()) + self.transition.choices = list( + self.edit_obj.profile_state.transitions().items() + ) @Account.forms('logo') diff --git a/funnel/models/notification.py b/funnel/models/notification.py index 024d238dc..c8b07c78c 100644 --- a/funnel/models/notification.py +++ b/funnel/models/notification.py @@ -90,18 +90,19 @@ ClassVar, Generic, Optional, + Protocol, TypeVar, Union, cast, get_args, get_origin, ) -from typing_extensions import Protocol, get_original_bases from uuid import UUID, uuid4 from sqlalchemy import event from sqlalchemy.orm import column_keyed_dict from sqlalchemy.orm.exc import NoResultFound +from typing_extensions import get_original_bases from werkzeug.utils import cached_property from baseframe import __ diff --git a/funnel/models/notification_types.py b/funnel/models/notification_types.py index a8fc70102..ca11d5a04 100644 --- a/funnel/models/notification_types.py +++ b/funnel/models/notification_types.py @@ -248,7 +248,7 @@ class RegistrationReceivedNotification( class OrganizationAdminMembershipNotification( - DocumentHasAccount, + DocumentIsAccount, Notification[Account, AccountMembership], type='organization_membership_granted', ): @@ -263,7 +263,7 @@ class OrganizationAdminMembershipNotification( class OrganizationAdminMembershipRevokedNotification( - DocumentHasAccount, + DocumentIsAccount, Notification[Account, AccountMembership], type='organization_membership_revoked', shadows=OrganizationAdminMembershipNotification, diff --git a/funnel/models/project.py b/funnel/models/project.py index 5bf0787bc..04353bcf3 100644 --- a/funnel/models/project.py +++ b/funnel/models/project.py @@ -89,6 +89,7 @@ class Project(UuidMixin, BaseScopedNameMixin, Model): None: { 'admin': 'account_admin', 'follower': 'account_participant', + 'member': 'account_member', } }, # `account` only appears in the 'primary' dataset. It must not be included in @@ -191,6 +192,12 @@ class Project(UuidMixin, BaseScopedNameMixin, Model): read={'all'}, datasets={'primary', 'without_parent', 'related'}, ) + + #: Auto-generated preview image for Open Graph + preview_image: Mapped[bytes | None] = sa.orm.mapped_column( + sa.LargeBinary, nullable=True, deferred=True + ) + allow_rsvp: Mapped[bool] = with_roles( sa.orm.mapped_column(sa.Boolean, default=True, nullable=False), read={'all'}, @@ -840,6 +847,14 @@ def published_project_count(self) -> int: self.listed_projects.filter(Project.state.PUBLISHED).order_by(None).count() ) + @with_roles(grants_via={None: {'participant': 'member'}}) + @cached_property + def membership_project(self) -> Project | None: + """Return a project that has memberships flag enabled (temporary).""" + return self.projects.filter( + Project.boxoffice_data.op('@>')({'has_membership': True}) + ).first() + class ProjectRedirect(TimestampMixin, Model): __tablename__ = 'project_redirect' diff --git a/funnel/models/sync_ticket.py b/funnel/models/sync_ticket.py index cc717b686..924570375 100644 --- a/funnel/models/sync_ticket.py +++ b/funnel/models/sync_ticket.py @@ -157,6 +157,9 @@ class TicketEvent(GetTitleMixin, Model): 'read': {'name', 'title'}, 'write': {'name', 'title'}, }, + 'project_usher': { + 'read': {'name', 'title'}, + }, } diff --git a/funnel/models/update.py b/funnel/models/update.py index 68a28157d..3f637aeb2 100644 --- a/funnel/models/update.py +++ b/funnel/models/update.py @@ -15,7 +15,6 @@ Mapped, Model, Query, - TimestampMixin, TSVectorType, UuidMixin, backref, @@ -47,7 +46,7 @@ class VISIBILITY_STATE(LabeledEnum): # noqa: N801 RESTRICTED = (2, 'restricted', __("Restricted")) -class Update(UuidMixin, BaseScopedIdNameMixin, TimestampMixin, Model): +class Update(UuidMixin, BaseScopedIdNameMixin, Model): __tablename__ = 'update' _visibility_state = sa.orm.mapped_column( diff --git a/funnel/templates/account_menu.html.jinja2 b/funnel/templates/account_menu.html.jinja2 index 382e9e75f..fcaf35c39 100644 --- a/funnel/templates/account_menu.html.jinja2 +++ b/funnel/templates/account_menu.html.jinja2 @@ -36,7 +36,7 @@ {%- for orgmem in orgmemlist.recent %}