Skip to content

Commit

Permalink
Merge branch 'whatsapp-integration' of https://github.com/hasgeek/funnel
Browse files Browse the repository at this point in the history
 into whatsapp-integration
  • Loading branch information
djamg committed Nov 20, 2023
2 parents 9da920f + 37516e8 commit 0400176
Show file tree
Hide file tree
Showing 32 changed files with 416 additions and 279 deletions.
4 changes: 2 additions & 2 deletions funnel/models/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ class Account(UuidMixin, BaseMixin, Model):
sa.orm.mapped_column(sa.Integer, nullable=False), read={'all'}
)

search_vector: Mapped[TSVectorType] = sa.orm.mapped_column(
search_vector: Mapped[str] = sa.orm.mapped_column(
TSVectorType(
'title',
'name',
Expand Down Expand Up @@ -1424,7 +1424,7 @@ class Team(UuidMixin, BaseMixin, Model):
account_id: Mapped[int] = sa.orm.mapped_column(
sa.ForeignKey('account.id'), nullable=False, index=True
)
account = with_roles(
account: Mapped[Account] = with_roles(
relationship(
Account,
foreign_keys=[account_id],
Expand Down
6 changes: 4 additions & 2 deletions funnel/models/account_membership.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,10 +151,12 @@ class __Account:
)

owner_users = with_roles(
DynamicAssociationProxy('active_owner_memberships', 'member'), read={'all'}
DynamicAssociationProxy[Account]('active_owner_memberships', 'member'),
read={'all'},
)
admin_users = with_roles(
DynamicAssociationProxy('active_admin_memberships', 'member'), read={'all'}
DynamicAssociationProxy[Account]('active_admin_memberships', 'member'),
read={'all'},
)

# pylint: disable=invalid-unary-operand-type
Expand Down
70 changes: 38 additions & 32 deletions funnel/models/auth_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,24 +105,24 @@ class AuthClient(ScopeMixin, UuidMixin, BaseMixin, Model):
grants_via={None: {'owner': 'owner', 'admin': 'admin'}},
)
#: Human-readable title
title = with_roles(
title: Mapped[str] = with_roles(
sa.orm.mapped_column(sa.Unicode(250), nullable=False),
read={'all'},
write={'owner'},
)
#: Long description
description = with_roles(
description: Mapped[str] = with_roles(
sa.orm.mapped_column(sa.UnicodeText, nullable=False, default=''),
read={'all'},
write={'owner'},
)
#: Confidential or public client? Public has no secret key
confidential = with_roles(
confidential: Mapped[bool] = with_roles(
sa.orm.mapped_column(sa.Boolean, nullable=False), read={'all'}, write={'owner'}
)
#: Website
website = with_roles(
sa.orm.mapped_column(sa.UnicodeText, nullable=False),
website: Mapped[str] = with_roles(
sa.orm.mapped_column(sa.UnicodeText, nullable=False), # FIXME: Use UrlType
read={'all'},
write={'owner'},
)
Expand All @@ -131,15 +131,15 @@ class AuthClient(ScopeMixin, UuidMixin, BaseMixin, Model):
'redirect_uri', sa.UnicodeText, nullable=True, default=''
)
#: Back-end notification URI (TODO: deprecated, needs better architecture)
notification_uri = with_roles(
notification_uri: Mapped[str | None] = with_roles( # FIXME: Use UrlType
sa.orm.mapped_column(sa.UnicodeText, nullable=True, default=''), rw={'owner'}
)
#: Active flag
active: Mapped[bool] = sa.orm.mapped_column(
sa.Boolean, nullable=False, default=True
)
#: Allow anyone to login to this app?
allow_any_login = with_roles(
allow_any_login: Mapped[bool] = with_roles(
sa.orm.mapped_column(sa.Boolean, nullable=False, default=True),
read={'all'},
write={'owner'},
Expand All @@ -150,7 +150,7 @@ class AuthClient(ScopeMixin, UuidMixin, BaseMixin, Model):
#: as a trusted client to provide single sign-in across the services.
#: However, resources in the scope column (via ScopeMixin) are granted for
#: any arbitrary user without explicit user authorization.
trusted = with_roles(
trusted: Mapped[bool] = with_roles(
sa.orm.mapped_column(sa.Boolean, nullable=False, default=False), read={'all'}
)

Expand Down Expand Up @@ -426,15 +426,15 @@ class AuthToken(ScopeMixin, BaseMixin, Model):
backref=backref('authtokens', lazy='dynamic', cascade='all'),
)
#: The session in which this token was issued, null for confidential clients
login_session_id = sa.orm.mapped_column(
login_session_id: Mapped[int | None] = sa.orm.mapped_column(
sa.Integer, sa.ForeignKey('login_session.id'), nullable=True
)
login_session: Mapped[LoginSession | None] = with_roles(
relationship(LoginSession, backref=backref('authtokens', lazy='dynamic')),
read={'owner'},
)
#: The client this authtoken is for
auth_client_id = sa.orm.mapped_column(
auth_client_id: Mapped[int] = sa.orm.mapped_column(
sa.Integer, sa.ForeignKey('auth_client.id'), nullable=False, index=True
)
auth_client: Mapped[AuthClient] = with_roles(
Expand All @@ -445,19 +445,23 @@ class AuthToken(ScopeMixin, BaseMixin, Model):
read={'owner'},
)
#: The token
token = sa.orm.mapped_column(
token: Mapped[str] = sa.orm.mapped_column(
sa.String(22), default=make_buid, nullable=False, unique=True
)
#: The token's type, 'bearer', 'mac' or a URL
token_type = sa.orm.mapped_column(sa.String(250), default='bearer', nullable=False)
token_type: Mapped[str] = sa.orm.mapped_column(
sa.String(250), default='bearer', nullable=False
)
#: Token secret for 'mac' type
secret = sa.orm.mapped_column(sa.String(44), nullable=True)
secret: Mapped[str | None] = sa.orm.mapped_column(sa.String(44), nullable=True)
#: Secret's algorithm (for 'mac' type)
algorithm = sa.orm.mapped_column(sa.String(20), nullable=True)
algorithm: Mapped[str | None] = sa.orm.mapped_column(sa.String(20), nullable=True)
#: Token's validity period in seconds, 0 = unlimited
validity = sa.orm.mapped_column(sa.Integer, nullable=False, default=0)
validity: Mapped[int] = sa.orm.mapped_column(sa.Integer, nullable=False, default=0)
#: Refresh token, to obtain a new token
refresh_token = sa.orm.mapped_column(sa.String(22), nullable=True, unique=True)
refresh_token: Mapped[str | None] = sa.orm.mapped_column(
sa.String(22), nullable=True, unique=True
)

# Only one authtoken per user and client. Add to scope as needed
__table_args__ = (
Expand All @@ -472,13 +476,6 @@ class AuthToken(ScopeMixin, BaseMixin, Model):
}
}

@property
def effective_user(self) -> Account:
"""Return subject user of this auth token."""
if self.login_session:
return self.login_session.account
return self.account

def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
self.token = make_buid()
Expand All @@ -491,13 +488,20 @@ def __repr__(self) -> str:
return f'<AuthToken {self.token} of {self.auth_client!r} {self.account!r}>'

@property
def effective_scope(self) -> list:
def effective_user(self) -> Account:
"""Return subject user of this auth token."""
if self.login_session:
return self.login_session.account
return cast(Account, self.account)

@property
def effective_scope(self) -> list[str]:
"""Return effective scope of this token, combining granted and client scopes."""
return sorted(set(self.scope) | set(self.auth_client.scope))

@with_roles(read={'owner'})
@cached_property
def last_used(self) -> datetime:
def last_used(self) -> datetime | None:
"""Return last used timestamp for this auth token."""
return (
db.session.query(sa.func.max(auth_client_login_session.c.accessed_at))
Expand Down Expand Up @@ -645,7 +649,7 @@ class AuthClientPermissions(BaseMixin, Model):
backref=backref('client_permissions', cascade='all'),
)
#: AuthClient app they are assigned on
auth_client_id = sa.orm.mapped_column(
auth_client_id: Mapped[int] = sa.orm.mapped_column(
sa.Integer, sa.ForeignKey('auth_client.id'), nullable=False, index=True
)
auth_client: Mapped[AuthClient] = with_roles(
Expand All @@ -657,7 +661,7 @@ class AuthClientPermissions(BaseMixin, Model):
grants_via={None: {'owner'}},
)
#: The permissions as a string of tokens
access_permissions = sa.orm.mapped_column(
access_permissions: Mapped[str] = sa.orm.mapped_column(
'permissions', sa.UnicodeText, default='', nullable=False
)

Expand Down Expand Up @@ -715,14 +719,16 @@ class AuthClientTeamPermissions(BaseMixin, Model):

__tablename__ = 'auth_client_team_permissions'
#: Team which has these permissions
team_id = sa.orm.mapped_column(sa.Integer, sa.ForeignKey('team.id'), nullable=False)
team = relationship(
team_id: Mapped[int] = sa.orm.mapped_column(
sa.Integer, sa.ForeignKey('team.id'), nullable=False
)
team: Mapped[Team] = relationship(
Team,
foreign_keys=[team_id],
backref=backref('client_permissions', cascade='all'),
)
#: AuthClient app they are assigned on
auth_client_id = sa.orm.mapped_column(
auth_client_id: Mapped[int] = sa.orm.mapped_column(
sa.Integer, sa.ForeignKey('auth_client.id'), nullable=False, index=True
)
auth_client: Mapped[AuthClient] = with_roles(
Expand All @@ -734,7 +740,7 @@ class AuthClientTeamPermissions(BaseMixin, Model):
grants_via={None: {'owner'}},
)
#: The permissions as a string of tokens
access_permissions = sa.orm.mapped_column(
access_permissions: Mapped[str] = sa.orm.mapped_column(
'permissions', sa.UnicodeText, default='', nullable=False
)

Expand All @@ -759,7 +765,7 @@ def get(
@classmethod
def all_for(
cls, auth_client: AuthClient, account: Account
) -> Query[AuthClientPermissions]:
) -> Query[AuthClientTeamPermissions]:
"""Get all permissions for the specified account via their teams."""
return cls.query.filter(
cls.auth_client == auth_client,
Expand Down
16 changes: 8 additions & 8 deletions funnel/models/comment.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ class SET_TYPE: # noqa: N801
class Commentset(UuidMixin, BaseMixin, Model):
__tablename__ = 'commentset'
#: Commentset state code
_state = sa.orm.mapped_column(
_state: Mapped[int] = sa.orm.mapped_column(
'state',
sa.SmallInteger,
StateManager.check_constraint('state', COMMENTSET_STATE),
Expand All @@ -105,7 +105,7 @@ class Commentset(UuidMixin, BaseMixin, Model):
datasets={'primary'},
)
#: Count of comments, stored to avoid count(*) queries
count = with_roles(
count: Mapped[int] = with_roles(
sa.orm.mapped_column(sa.Integer, default=0, nullable=False),
read={'all'},
datasets={'primary'},
Expand Down Expand Up @@ -214,7 +214,7 @@ class Comment(UuidMixin, BaseMixin, Model):
),
grants={'author'},
)
commentset_id = sa.orm.mapped_column(
commentset_id: Mapped[int] = sa.orm.mapped_column(
sa.Integer, sa.ForeignKey('commentset.id'), nullable=False
)
commentset: Mapped[Commentset] = with_roles(
Expand All @@ -225,7 +225,7 @@ class Comment(UuidMixin, BaseMixin, Model):
grants_via={None: {'document_subscriber'}},
)

in_reply_to_id = sa.orm.mapped_column(
in_reply_to_id: Mapped[int | None] = sa.orm.mapped_column(
sa.Integer, sa.ForeignKey('comment.id'), nullable=True
)
replies: Mapped[list[Comment]] = relationship(
Expand All @@ -236,7 +236,7 @@ class Comment(UuidMixin, BaseMixin, Model):
'message', nullable=False
)

_state = sa.orm.mapped_column(
_state: Mapped[int] = sa.orm.mapped_column(
'state',
sa.Integer,
StateManager.check_constraint('state', COMMENT_STATE),
Expand All @@ -245,18 +245,18 @@ class Comment(UuidMixin, BaseMixin, Model):
)
state = StateManager('_state', COMMENT_STATE, doc="Current state of the comment")

edited_at = with_roles(
edited_at: Mapped[datetime | None] = with_roles(
sa.orm.mapped_column(sa.TIMESTAMP(timezone=True), nullable=True),
read={'all'},
datasets={'primary', 'related', 'json'},
)

#: Revision number maintained by SQLAlchemy, starting at 1
revisionid = with_roles(
revisionid: Mapped[int] = with_roles(
sa.orm.mapped_column(sa.Integer, nullable=False), read={'all'}
)

search_vector: Mapped[TSVectorType] = sa.orm.mapped_column(
search_vector: Mapped[str] = sa.orm.mapped_column(
TSVectorType(
'message_text',
weights={'message_text': 'A'},
Expand Down
8 changes: 6 additions & 2 deletions funnel/models/commentset_membership.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

from __future__ import annotations

from datetime import datetime

from werkzeug.utils import cached_property

from coaster.sqlalchemy import DynamicAssociationProxy, with_roles
Expand Down Expand Up @@ -58,9 +60,11 @@ class CommentsetMembership(ImmutableUserMembershipMixin, Model):
parent: Mapped[Commentset] = sa.orm.synonym('commentset')

#: Flag to indicate notifications are muted
is_muted = sa.orm.mapped_column(sa.Boolean, nullable=False, default=False)
is_muted: Mapped[bool] = sa.orm.mapped_column(
sa.Boolean, nullable=False, default=False
)
#: When the user visited this commentset last
last_seen_at = sa.orm.mapped_column(
last_seen_at: Mapped[datetime] = sa.orm.mapped_column(
sa.TIMESTAMP(timezone=True), nullable=False, default=sa.func.utcnow()
)

Expand Down
12 changes: 8 additions & 4 deletions funnel/models/contact_exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ class ContactExchange(TimestampMixin, RoleMixin, Model):
),
)
#: Participant whose contact was scanned
ticket_participant_id = sa.orm.mapped_column(
ticket_participant_id: Mapped[int] = sa.orm.mapped_column(
sa.Integer,
sa.ForeignKey('ticket_participant.id', ondelete='CASCADE'),
primary_key=True,
Expand All @@ -82,13 +82,17 @@ class ContactExchange(TimestampMixin, RoleMixin, Model):
backref=backref('scanned_contacts', passive_deletes=True),
)
#: Datetime at which the scan happened
scanned_at = sa.orm.mapped_column(
scanned_at: Mapped[datetime] = sa.orm.mapped_column(
sa.TIMESTAMP(timezone=True), nullable=False, default=sa.func.utcnow()
)
#: Note recorded by the user (plain text)
description = sa.orm.mapped_column(sa.UnicodeText, nullable=False, default='')
description: Mapped[str] = sa.orm.mapped_column(
sa.UnicodeText, nullable=False, default=''
)
#: Archived flag
archived = sa.orm.mapped_column(sa.Boolean, nullable=False, default=False)
archived: Mapped[bool] = sa.orm.mapped_column(
sa.Boolean, nullable=False, default=False
)

__roles__ = {
'owner': {
Expand Down
Loading

0 comments on commit 0400176

Please sign in to comment.