Skip to content

Commit

Permalink
Merge branch 'main' into auto-thumbnails
Browse files Browse the repository at this point in the history
  • Loading branch information
jace committed Nov 15, 2023
2 parents b5c7b62 + 28cfbe3 commit 699fe31
Show file tree
Hide file tree
Showing 40 changed files with 467 additions and 435 deletions.
25 changes: 23 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ ci:
skip: [
'pip-audit',
'yesqa',
'creosote',
'no-commit-to-branch',
# 'hadolint-docker',
'docker-compose-check',
Expand Down Expand Up @@ -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.3
rev: v0.1.4
hooks:
- id: ruff
args: ['--fix', '--exit-non-zero-on-fix']
Expand Down Expand Up @@ -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:
Expand Down
3 changes: 1 addition & 2 deletions funnel/devtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
8 changes: 5 additions & 3 deletions funnel/forms/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -496,7 +496,7 @@ def validate_username(self, field: forms.Field) -> None:
raise_username_error(reason)


class EnableNotificationsDescriptionMixin:
class EnableNotificationsDescriptionProtoMixin:
"""Mixin to add a link in the description for enabling notifications."""

enable_notifications: forms.Field
Expand All @@ -513,7 +513,9 @@ def set_queries(self) -> None:


@Account.forms('email_add')
class NewEmailAddressForm(EnableNotificationsDescriptionMixin, forms.RecaptchaForm):
class NewEmailAddressForm(
EnableNotificationsDescriptionProtoMixin, forms.RecaptchaForm
):
"""Form to add a new email address to an account."""

__expects__ = ('edit_user',)
Expand Down Expand Up @@ -560,7 +562,7 @@ class EmailPrimaryForm(forms.Form):


@Account.forms('phone_add')
class NewPhoneForm(EnableNotificationsDescriptionMixin, forms.RecaptchaForm):
class NewPhoneForm(EnableNotificationsDescriptionProtoMixin, forms.RecaptchaForm):
"""Form to add a new mobile number (SMS-capable) to an account."""

__expects__ = ('edit_user',)
Expand Down
4 changes: 3 additions & 1 deletion funnel/forms/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
9 changes: 4 additions & 5 deletions funnel/models/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import itertools
from collections.abc import Iterable, Iterator
from datetime import datetime, timedelta
from typing import ClassVar, Literal, Union, cast, overload
from typing import ClassVar, Literal, cast, overload
from uuid import UUID

import phonenumbers
Expand Down Expand Up @@ -259,7 +259,7 @@ class Account(UuidMixin, BaseMixin, Model):
sa.orm.mapped_column(sa.Integer, nullable=False), read={'all'}
)

search_vector: Mapped[str] = sa.orm.mapped_column(
search_vector: Mapped[TSVectorType] = sa.orm.mapped_column(
TSVectorType(
'title',
'name',
Expand Down Expand Up @@ -1227,11 +1227,10 @@ def organization_links(self) -> list:
add_search_trigger(Account, 'name_vector')


class AccountOldId(UuidMixin, BaseMixin, Model):
class AccountOldId(UuidMixin, BaseMixin[UUID], Model):
"""Record of an older UUID for an account, after account merger."""

__tablename__ = 'account_oldid'
__uuid_primary_key__ = True

#: Old account, if still present
old_account: Mapped[Account] = relationship(
Expand Down Expand Up @@ -2178,7 +2177,7 @@ def get(
)

#: Anchor type
Anchor = Union[AccountEmail, AccountEmailClaim, AccountPhone, EmailAddress, PhoneNumber]
Anchor = AccountEmail | AccountEmailClaim | AccountPhone | EmailAddress | PhoneNumber

# Tail imports
# pylint: disable=wrong-import-position
Expand Down
19 changes: 12 additions & 7 deletions funnel/models/comment.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from collections.abc import Sequence
from datetime import datetime
from typing import Any
from typing import TYPE_CHECKING, Any

from werkzeug.utils import cached_property

Expand Down Expand Up @@ -134,7 +134,7 @@ def __init__(self, **kwargs) -> None:
self.count = 0

@cached_property
def parent(self) -> BaseMixin:
def parent(self) -> Project | Proposal | Update:
# FIXME: Move this to a CommentMixin that uses a registry, like EmailAddress
if self.project is not None:
return self.project
Expand All @@ -147,11 +147,8 @@ def parent(self) -> BaseMixin:
with_roles(parent, read={'all'}, datasets={'primary'})

@cached_property
def parent_type(self) -> str | None:
parent = self.parent
if parent is not None:
return parent.__tablename__
return None
def parent_type(self) -> str:
return self.parent.__tablename__

with_roles(parent_type, read={'all'})

Expand Down Expand Up @@ -363,6 +360,7 @@ def _message_expression(cls):

@property
def title(self) -> str:
"""A made-up title referring to the context for the comment."""
obj = self.commentset.parent
if obj is not None:
return _("{user} commented on {obj}").format(
Expand Down Expand Up @@ -439,3 +437,10 @@ class __Commentset:
),
viewonly=True,
)


# Tail imports for type checking
if TYPE_CHECKING:
from .project import Project
from .proposal import Proposal
from .update import Update
18 changes: 10 additions & 8 deletions funnel/models/membership_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from collections.abc import Callable, Iterable
from datetime import datetime as datetime_type
from typing import TYPE_CHECKING, Any, ClassVar, Generic, TypeVar
from uuid import UUID

from sqlalchemy import event
from sqlalchemy.sql.expression import ColumnElement
Expand All @@ -27,7 +28,7 @@
sa,
)
from .account import Account
from .reorder_mixin import ReorderMixin
from .reorder_mixin import ReorderProtoMixin

# Export only symbols needed in views.
__all__ = [
Expand All @@ -40,7 +41,9 @@
# --- Typing ---------------------------------------------------------------------------

MembershipType = TypeVar('MembershipType', bound='ImmutableMembershipMixin')
FrozenAttributionType = TypeVar('FrozenAttributionType', bound='FrozenAttributionMixin')
FrozenAttributionType = TypeVar(
'FrozenAttributionType', bound='FrozenAttributionProtoMixin'
)

# --- Enum -----------------------------------------------------------------------------

Expand Down Expand Up @@ -82,10 +85,9 @@ class MembershipRecordTypeError(MembershipError):


@declarative_mixin
class ImmutableMembershipMixin(UuidMixin, BaseMixin):
class ImmutableMembershipMixin(UuidMixin, BaseMixin[UUID]):
"""Support class for immutable memberships."""

__uuid_primary_key__ = True
#: Can granted_by be null? Only in memberships based on legacy data
__null_granted_by__: ClassVar[bool] = False
#: List of columns that will be copied into a new row when a membership is amended
Expand Down Expand Up @@ -383,7 +385,7 @@ def member_id(cls) -> Mapped[int]:
@with_roles(read={'member', 'editor'}, grants_via={None: {'admin': 'member'}})
@declared_attr
@classmethod
def member(cls) -> Mapped[Account]:
def member(cls) -> Mapped[Account]: # type: ignore[override]
"""Member in this membership record."""
return relationship(Account, foreign_keys=[cls.member_id])

Expand Down Expand Up @@ -490,7 +492,7 @@ def migrate_account(cls, old_account: Account, new_account: Account) -> None:


@declarative_mixin
class ReorderMembershipMixin(ReorderMixin):
class ReorderMembershipProtoMixin(ReorderProtoMixin):
"""Customizes ReorderMixin for membership models."""

if TYPE_CHECKING:
Expand Down Expand Up @@ -558,7 +560,7 @@ def parent_scoped_reorder_query_filter(self) -> ColumnElement:


@declarative_mixin
class FrozenAttributionMixin:
class FrozenAttributionProtoMixin:
"""Provides a `title` data column and support method to freeze it."""

if TYPE_CHECKING:
Expand All @@ -580,7 +582,7 @@ def _title(cls) -> Mapped[str | None]:
def title(self) -> str:
"""Attribution title for this record."""
if self._local_data_only:
return self._title # This may be None
return self._title # This may be None # type: ignore[return-value]
return self._title or self.member.title

@title.setter
Expand Down
5 changes: 3 additions & 2 deletions funnel/models/moderation.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

from __future__ import annotations

from uuid import UUID

from baseframe import __
from coaster.sqlalchemy import StateManager, with_roles
from coaster.utils import LabeledEnum
Expand All @@ -20,9 +22,8 @@ class MODERATOR_REPORT_TYPE(LabeledEnum): # noqa: N801
SPAM = (2, 'spam', __("Spam"))


class CommentModeratorReport(UuidMixin, BaseMixin, Model):
class CommentModeratorReport(UuidMixin, BaseMixin[UUID], Model):
__tablename__ = 'comment_moderator_report'
__uuid_primary_key__ = True

comment_id = sa.orm.mapped_column(
sa.Integer, sa.ForeignKey('comment.id'), nullable=False, index=True
Expand Down
11 changes: 6 additions & 5 deletions funnel/models/notification.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 __
Expand Down Expand Up @@ -635,7 +636,7 @@ def allow_transport(cls, transport: str) -> bool:
@property
def role_provider_obj(self) -> _F | _D:
"""Return fragment if exists, document otherwise, indicating role provider."""
return cast(Union[_F, _D], self.fragment or self.document)
return cast(_F | _D, self.fragment or self.document)

def dispatch(self) -> Generator[NotificationRecipient, None, None]:
"""
Expand Down Expand Up @@ -732,7 +733,7 @@ def __getattr__(self, attr: str) -> Any:
return getattr(self.cls, attr)


class NotificationRecipientMixin:
class NotificationRecipientProtoMixin:
"""Shared mixin for :class:`NotificationRecipient` and :class:`NotificationFor`."""

notification: Mapped[Notification] | Notification | PreviewNotification
Expand Down Expand Up @@ -787,7 +788,7 @@ def is_not_deleted(self, revoke: bool = False) -> bool:
return False


class NotificationRecipient(NotificationRecipientMixin, NoIdMixin, Model):
class NotificationRecipient(NoIdMixin, NotificationRecipientProtoMixin, Model):
"""
The recipient of a notification.
Expand Down Expand Up @@ -1200,7 +1201,7 @@ def migrate_account(cls, old_account: Account, new_account: Account) -> None:
)


class NotificationFor(NotificationRecipientMixin):
class NotificationFor(NotificationRecipientProtoMixin):
"""View-only wrapper to mimic :class:`UserNotification`."""

notification: Notification | PreviewNotification
Expand Down
4 changes: 2 additions & 2 deletions funnel/models/proposal.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
)
from .project import Project
from .project_membership import project_child_role_map
from .reorder_mixin import ReorderMixin
from .reorder_mixin import ReorderProtoMixin
from .video_mixin import VideoMixin

__all__ = ['PROPOSAL_STATE', 'Proposal', 'ProposalSuuidRedirect']
Expand Down Expand Up @@ -119,7 +119,7 @@ class PROPOSAL_STATE(LabeledEnum): # noqa: N801


class Proposal( # type: ignore[misc]
UuidMixin, BaseScopedIdNameMixin, VideoMixin, ReorderMixin, Model
UuidMixin, BaseScopedIdNameMixin, VideoMixin, ReorderProtoMixin, Model
):
__tablename__ = 'proposal'

Expand Down
9 changes: 6 additions & 3 deletions funnel/models/proposal_membership.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
from .account import Account
from .helpers import reopen
from .membership_mixin import (
FrozenAttributionMixin,
FrozenAttributionProtoMixin,
ImmutableUserMembershipMixin,
ReorderMembershipMixin,
ReorderMembershipProtoMixin,
)
from .project import Project
from .proposal import Proposal
Expand All @@ -21,7 +21,10 @@


class ProposalMembership( # type: ignore[misc]
FrozenAttributionMixin, ReorderMembershipMixin, ImmutableUserMembershipMixin, Model
ImmutableUserMembershipMixin,
FrozenAttributionProtoMixin,
ReorderMembershipProtoMixin,
Model,
):
"""Users can be presenters or reviewers on proposals."""

Expand Down
Loading

0 comments on commit 699fe31

Please sign in to comment.