Skip to content

Commit

Permalink
Merge branch 'main' into shortlink-form
Browse files Browse the repository at this point in the history
  • Loading branch information
jace committed Oct 26, 2023
2 parents 086ad1f + 1d9106b commit a091210
Show file tree
Hide file tree
Showing 49 changed files with 1,147 additions and 991 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/pytest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest] # TODO: Figure out macos-latest and Docker
python-version: ['3.11']
python-version: ['3.11', '3.12']

services:
redis:
Expand Down Expand Up @@ -81,7 +81,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v3
with:
node-version: latest
node-version: 20
cache: npm
- name: Cache node modules
uses: actions/cache@v3
Expand Down
10 changes: 5 additions & 5 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,12 @@ repos:
]
files: ^requirements/.*\.txt$
- repo: https://github.com/asottile/pyupgrade
rev: v3.13.0
rev: v3.15.0
hooks:
- id: pyupgrade
args: ['--keep-runtime-typing', '--py310-plus']
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.0.291
rev: v0.1.1
hooks:
- id: ruff
args: ['--fix', '--exit-non-zero-on-fix']
Expand Down Expand Up @@ -98,7 +98,7 @@ repos:
additional_dependencies:
- tomli
- repo: https://github.com/psf/black
rev: 23.9.1
rev: 23.10.0
hooks:
- id: black
# Mypy is temporarily disabled until the SQLAlchemy 2.0 migration is complete
Expand Down Expand Up @@ -127,7 +127,7 @@ repos:
- id: flake8
additional_dependencies: *flake8deps
- repo: https://github.com/PyCQA/pylint
rev: v3.0.0a7
rev: v3.0.1
hooks:
- id: pylint
args: [
Expand All @@ -147,7 +147,7 @@ repos:
additional_dependencies:
- 'bandit[toml]'
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
rev: v4.5.0
hooks:
- id: check-added-large-files
- id: check-ast
Expand Down
2 changes: 1 addition & 1 deletion funnel/assets/js/utils/ractive_util.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { USER_AVATAR_IMG_SIZE } from '../constants';
Ractive.DEBUG = false;

export const useravatar = Ractive.extend({
template: `{{#if user.profile_url && addprofilelink }}<a href="{{user.profile_url}}" class="nounderline">{{#if user.logo_url }}<img class="user__box__gravatar" src="{{ imgurl() }}" />{{else}}<div class="user__box__gravatar user__box__gravatar--initials" data-avatar-colour="{{ getAvatarColour(user.fullname) }}">{{ getInitials(user.fullname) }}</div>{{/if}}</a>{{else}}<span>{{#if user.logo_url }}<img class="user__box__gravatar" src="{{ imgurl() }}" />{{else}}<div class="user__box__gravatar user__box__gravatar--initials" data-avatar-colour="{{ getAvatarColour(user.fullname) }}">{{ getInitials(user.fullname) }}</div>{{/if}}</span>{{/if}}`,
template: `{{#if addprofilelink }}<a href="{{user.absolute_url}}" class="nounderline">{{#if user.logo_url }}<img class="user__box__gravatar" src="{{ imgurl() }}" />{{else}}<div class="user__box__gravatar user__box__gravatar--initials" data-avatar-colour="{{ getAvatarColour(user.fullname) }}">{{ getInitials(user.fullname) }}</div>{{/if}}</a>{{else}}<span>{{#if user.logo_url }}<img class="user__box__gravatar" src="{{ imgurl() }}" />{{else}}<div class="user__box__gravatar user__box__gravatar--initials" data-avatar-colour="{{ getAvatarColour(user.fullname) }}">{{ getInitials(user.fullname) }}</div>{{/if}}</span>{{/if}}`,
data: {
addprofilelink: true,
size: 'medium',
Expand Down
2 changes: 1 addition & 1 deletion funnel/assets/js/utils/vue_util.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { USER_AVATAR_IMG_SIZE } from '../constants';

export const userAvatarUI = Vue.component('useravatar', {
template:
'<a :href="user.profile_url" v-if="user.profile_url && addprofilelink" class="nounderline"><span class="user__box__wrapper" v-if="user.logo_url"><img class="user__box__gravatar" :src="imgurl"></span><div class="user__box__gravatar user__box__gravatar--initials" :data-avatar-colour="getAvatarColour(user.fullname)" v-else>{{ getInitials(user.fullname) }}</div></a v-if="user.profile_url && addprofilelink"></a><span v-else><img class="user__box__gravatar" :src="imgurl" v-if="user.logo_url"/><div class="user__box__gravatar user__box__gravatar--initials" :data-avatar-colour="getAvatarColour(user.fullname)" v-else>{{ getInitials(user.fullname) }}</span v-else>',
'<a :href="user.absolute_url" v-if="addprofilelink" class="nounderline"><span class="user__box__wrapper" v-if="user.logo_url"><img class="user__box__gravatar" :src="imgurl"></span><div class="user__box__gravatar user__box__gravatar--initials" :data-avatar-colour="getAvatarColour(user.fullname)" v-else>{{ getInitials(user.fullname) }}</div></a v-if="user.absolute_url && addprofilelink"></a><span v-else><img class="user__box__gravatar" :src="imgurl" v-if="user.logo_url"/><div class="user__box__gravatar user__box__gravatar--initials" :data-avatar-colour="getAvatarColour(user.fullname)" v-else>{{ getInitials(user.fullname) }}</span v-else>',
props: {
user: Object,
addprofilelink: {
Expand Down
9 changes: 6 additions & 3 deletions funnel/forms/organization.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,12 @@ def validate_name(self, field: forms.Field) -> None:
)
if reason == 'reserved':
raise forms.validators.ValidationError(_("This name is reserved"))
if self.edit_obj and field.data.lower() == self.edit_obj.name.lower():
# Name is not reserved or invalid under current rules. It's also not changed
# from existing name, or has only changed case. This is a validation pass.
if (
self.edit_obj
and self.edit_obj.name
and field.data.lower() == self.edit_obj.name.lower()
):
# Name has only changed case from previous name. This is a validation pass
return
if reason == 'user':
if (
Expand Down
7 changes: 5 additions & 2 deletions funnel/forms/sync_ticket.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,8 @@ class TicketParticipantForm(forms.Form):
)
email = forms.EmailField(
__("Email"),
validators=[forms.validators.DataRequired(), forms.validators.ValidEmail()],
filters=[forms.filters.strip()],
validators=[forms.validators.Optional(), forms.validators.ValidEmail()],
filters=[forms.filters.none_if_empty()],
)
phone = forms.StringField(
__("Phone number"),
Expand Down Expand Up @@ -219,6 +219,9 @@ def set_queries(self) -> None:
def validate(self, *args, **kwargs) -> bool:
"""Validate form."""
result = super().validate(*args, **kwargs)
if self.email.data is None:
self.user = None
return True
with db.session.no_autoflush:
accountemail = AccountEmail.get(email=self.email.data)
if accountemail is not None:
Expand Down
28 changes: 13 additions & 15 deletions funnel/models/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,14 @@ class PROFILE_STATE(LabeledEnum): # noqa: N801
class ZBase32Comparator(Comparator[str]): # pylint: disable=abstract-method
"""Comparator to allow lookup by Account.uuid_zbase32."""

def __eq__(self, other: str) -> sa.ColumnElement[bool]: # type: ignore[override]
def __eq__(self, other: object) -> sa.ColumnElement[bool]: # type: ignore[override]
"""Return an expression for column == other."""
return self.__clause_element__() == UUID(bytes=zbase32_decode(other))
try:
return self.__clause_element__() == UUID( # type: ignore[return-value]
bytes=zbase32_decode(str(other))
)
except ValueError: # zbase32 call failed, so it's not a valid string
return sa.false()


class Account(UuidMixin, BaseMixin, Model):
Expand Down Expand Up @@ -332,7 +337,7 @@ class Account(UuidMixin, BaseMixin, Model):
'logo_url',
'banner_image_url',
'joined_at',
'profile_url',
'absolute_url',
'urls',
'is_user_profile',
'is_organization_profile',
Expand All @@ -357,7 +362,7 @@ class Account(UuidMixin, BaseMixin, Model):
'logo_url',
'website',
'joined_at',
'profile_url',
'absolute_url',
'is_verified',
},
'related': {
Expand All @@ -373,7 +378,7 @@ class Account(UuidMixin, BaseMixin, Model):
'description',
'logo_url',
'joined_at',
'profile_url',
'absolute_url',
'is_verified',
},
}
Expand Down Expand Up @@ -618,13 +623,6 @@ def has_public_profile(self) -> bool:

with_roles(has_public_profile, read={'all'}, write={'owner'})

@property
def profile_url(self) -> str | None:
"""Return optional URL to account profile page."""
return self.url_for(_external=True)

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

def is_profile_complete(self) -> bool:
"""Verify if profile is complete (fullname, username and contacts present)."""
return bool(self.title and self.name and self.has_verified_contact_info)
Expand Down Expand Up @@ -1292,7 +1290,7 @@ class DuckTypeAccount(RoleMixin):
uuid_b58: None = None
username: None = None
name: None = None
profile_url: None = None
absolute_url: None = None
email: None = None
phone: None = None

Expand All @@ -1313,7 +1311,7 @@ class DuckTypeAccount(RoleMixin):
'username',
'fullname',
'pickername',
'profile_url',
'absolute_url',
},
'call': {'views', 'forms', 'features', 'url_for'},
}
Expand All @@ -1324,7 +1322,7 @@ class DuckTypeAccount(RoleMixin):
'username',
'fullname',
'pickername',
'profile_url',
'absolute_url',
}
}

Expand Down
14 changes: 4 additions & 10 deletions funnel/models/comment.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,16 +278,16 @@ class Comment(UuidMixin, BaseMixin, Model):

__roles__ = {
'all': {
'read': {'created_at', 'urls', 'uuid_b58', 'has_replies'},
'read': {'created_at', 'urls', 'uuid_b58', 'has_replies', 'absolute_url'},
'call': {'state', 'commentset', 'view_for', 'url_for'},
},
'replied_to_commenter': {'granted_via': {'in_reply_to': '_posted_by'}},
}

__datasets__ = {
'primary': {'created_at', 'urls', 'uuid_b58'},
'related': {'created_at', 'urls', 'uuid_b58'},
'json': {'created_at', 'urls', 'uuid_b58'},
'primary': {'created_at', 'urls', 'uuid_b58', 'absolute_url'},
'related': {'created_at', 'urls', 'uuid_b58', 'absolute_url'},
'json': {'created_at', 'urls', 'uuid_b58', 'absolute_url'},
'minimal': {'created_at', 'uuid_b58'},
}

Expand Down Expand Up @@ -361,12 +361,6 @@ def _message_expression(cls):
message, read={'all'}, datasets={'primary', 'related', 'json', 'minimal'}
)

@property
def absolute_url(self) -> str:
return self.url_for()

with_roles(absolute_url, read={'all'}, datasets={'primary', 'related', 'json'})

@property
def title(self) -> str:
obj = self.commentset.parent
Expand Down
5 changes: 4 additions & 1 deletion funnel/models/project_membership.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from .membership_mixin import ImmutableUserMembershipMixin
from .project import Project

__all__ = ['ProjectMembership', 'project_child_role_map']
__all__ = ['ProjectMembership', 'project_child_role_map', 'project_child_role_set']

#: Roles in a project and their remapped names in objects attached to a project
project_child_role_map: dict[str, str] = {
Expand All @@ -24,6 +24,9 @@
'reader': 'reader',
}

#: A model that is indirectly under a project needs the role names without remapping
project_child_role_set: set[str] = set(project_child_role_map.values())

#: ProjectMembership maps project's `account_admin` role to membership's `editor`
#: role in addition to the recurring role grant map
project_membership_role_map: dict[str, str | set[str]] = {
Expand Down
4 changes: 2 additions & 2 deletions funnel/models/rsvp.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class Rsvp(UuidMixin, NoIdMixin, Model):
project_id = sa.orm.mapped_column(
sa.Integer, sa.ForeignKey('project.id'), nullable=False, primary_key=True
)
project = with_roles(
project: Mapped[Project] = with_roles(
relationship(Project, backref=backref('rsvps', cascade='all', lazy='dynamic')),
read={'owner', 'project_promoter'},
grants_via={None: project_child_role_map},
Expand All @@ -54,7 +54,7 @@ class Rsvp(UuidMixin, NoIdMixin, Model):
participant_id: Mapped[int] = sa.orm.mapped_column(
sa.ForeignKey('account.id'), nullable=False, primary_key=True
)
participant = with_roles(
participant: Mapped[Account] = with_roles(
relationship(Account, backref=backref('rsvps', cascade='all', lazy='dynamic')),
read={'owner', 'project_promoter'},
grants={'owner'},
Expand Down
12 changes: 7 additions & 5 deletions funnel/models/sync_ticket.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ class TicketParticipant(EmailAddressMixin, UuidMixin, BaseMixin, Model):
"""A participant in one or more events, synced from an external ticket source."""

__tablename__ = 'ticket_participant'
__email_optional__ = False
__email_optional__ = True
__email_for__ = 'participant'

fullname = with_roles(
Expand Down Expand Up @@ -297,10 +297,10 @@ def has_public_profile(self) -> bool:
with_roles(has_public_profile, read={'all'})

@property
def profile_url(self) -> str | None:
return self.participant.profile_url if self.participant else None
def absolute_url(self) -> str | None:
return self.participant.absolute_url if self.participant else None

with_roles(profile_url, read={'all'})
with_roles(absolute_url, read={'all'})

@classmethod
def get(
Expand Down Expand Up @@ -376,7 +376,9 @@ def checkin_list(cls, ticket_event: TicketEvent) -> list: # TODO: List type?
TicketEventParticipant,
TicketParticipant.id == TicketEventParticipant.ticket_participant_id,
)
.join(EmailAddress, EmailAddress.id == TicketParticipant.email_address_id)
.outerjoin(
EmailAddress, EmailAddress.id == TicketParticipant.email_address_id
)
.outerjoin(
SyncTicket, TicketParticipant.id == SyncTicket.ticket_participant_id
)
Expand Down
6 changes: 2 additions & 4 deletions funnel/models/venue.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

from __future__ import annotations

import itertools

from sqlalchemy.ext.orderinglist import ordering_list

from coaster.sqlalchemy import add_primary_relationship, with_roles
Expand All @@ -19,7 +17,7 @@
)
from .helpers import MarkdownCompositeBasic, reopen
from .project import Project
from .project_membership import project_child_role_map
from .project_membership import project_child_role_map, project_child_role_set

__all__ = ['Venue', 'VenueRoom']

Expand Down Expand Up @@ -113,7 +111,7 @@ class VenueRoom(UuidMixin, BaseScopedNameMixin, Model):
venue: Mapped[Venue] = with_roles(
relationship(Venue, back_populates='rooms'),
# Since Venue already remaps Project roles, we just want the remapped role names
grants_via={None: set(itertools.chain(*project_child_role_map.values()))},
grants_via={None: project_child_role_set},
)
parent: Mapped[Venue] = sa.orm.synonym('venue')
description, description_text, description_html = MarkdownCompositeBasic.create(
Expand Down
1 change: 1 addition & 0 deletions funnel/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
org_data_changed = app_signals.signal('org-data-changed')
team_data_changed = app_signals.signal('team-data-changed')
session_revoked = app_signals.signal('session-revoked')
project_data_change = app_signals.signal('project-data-change')

# Commentset role change signals (sends user, document)
project_role_change = app_signals.signal('project_role_change')
Expand Down
2 changes: 1 addition & 1 deletion funnel/templates/account.html.jinja2
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
{{ faicon(icon='info-circle', icon_size='body2', baseline=true) }} <a href="{{ url_for('account_edit') }}" data-cy="add-username">{% trans %}Add username{% endtrans %} <span class="circle-icon">{{ faicon(icon='plus', icon_size='caption', baseline=false) }}</span></a>
</p>
{%- endif %}
<a href="{{ current_auth.user.profile_url }}"
<a href="{{ current_auth.user.absolute_url }}"
data-cy="my-profile"
class="nounderline">
{%- trans %}Go to account{% endtrans %}
Expand Down
26 changes: 9 additions & 17 deletions funnel/templates/account_menu.html.jinja2
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,17 @@
<div class="user">
<div class="user__box">
{{ useravatar(current_auth.user, add_profile_link=true, size='big') }}
{% if current_auth.user.profile_url %}
<a href="{{ current_auth.user.profile_url }}"
class="user__box__header nounderline"
data-cy="profile">
{%- else %}
<div class="user__box__header">
<a href="{{ current_auth.user.absolute_url }}"
class="user__box__header nounderline"
data-cy="profile">
<div class="user__box__header__details">
<p class="mui--text-title text-bold mui--text-dark zero-bottom-margin">{{ current_auth.user.fullname }}</p>
{% if current_auth.user.username %}
<p class="mui--text-caption mui--text-light zero-bottom-margin">@{{ current_auth.user.username }}</p>
{% endif %}
<div class="user__box__header__details">
<p class="mui--text-title text-bold mui--text-dark zero-bottom-margin">{{ current_auth.user.fullname }}</p>
{% if current_auth.user.username %}
<p class="mui--text-caption mui--text-light zero-bottom-margin">@{{ current_auth.user.username }}</p>
{% endif %}
</div>
{% if current_auth.user.profile_url %}
{{ faicon(icon='chevron-right', icon_size='subhead', baseline=false, css_class="user__box__header__icon mui--text-dark") }}
</a>
{%- else %}
</div>
{% endif %}
{{ faicon(icon='chevron-right', icon_size='subhead', baseline=false, css_class="user__box__header__icon mui--text-dark") }}
</a>
</div>
</div>
</div>
Expand Down
Loading

0 comments on commit a091210

Please sign in to comment.