~homeworkprod/byceps

6ba16b966e0fa3a401dd991213f07c97b332d72c — Jochen Kupperschmidt 1 year, 2 months ago 4a04126
Format code according to Black
68 files changed, 373 insertions(+), 250 deletions(-)

M byceps/application.py
M byceps/blueprints/admin/board/views.py
M byceps/blueprints/admin/news/views.py
M byceps/blueprints/admin/orga/views.py
M byceps/blueprints/admin/orga_presence/views.py
M byceps/blueprints/admin/orga_team/views.py
M byceps/blueprints/admin/shop/article/views.py
M byceps/blueprints/admin/shop/order/models.py
M byceps/blueprints/admin/shop/order/service.py
M byceps/blueprints/admin/shop/shop/views.py
M byceps/blueprints/admin/ticketing/service.py
M byceps/blueprints/admin/ticketing/views.py
M byceps/blueprints/admin/tourney/views.py
M byceps/blueprints/api/v1/snippet/views.py
M byceps/blueprints/api/v1/tourney/match/comments/views.py
M byceps/blueprints/api/v1/user/views.py
M byceps/blueprints/authentication/forms.py
M byceps/blueprints/authentication/views.py
M byceps/blueprints/board/service.py
M byceps/blueprints/board/views_posting.py
M byceps/blueprints/board/views_topic.py
M byceps/blueprints/consent/forms.py
M byceps/blueprints/core/views.py
M byceps/blueprints/ticketing/views.py
M byceps/blueprints/user/current/views.py
M byceps/config.py
M byceps/services/attendance/service.py
M byceps/services/authentication/password/service.py
M byceps/services/authentication/service.py
M byceps/services/board/category_query_service.py
M byceps/services/board/last_view_service.py
M byceps/services/board/models/posting.py
M byceps/services/consent/subject_service.py
M byceps/services/email/service.py
M byceps/services/metrics/models.py
M byceps/services/metrics/service.py
M byceps/services/newsletter/service.py
M byceps/services/orga/service.py
M byceps/services/orga_presence/service.py
M byceps/services/orga_team/service.py
M byceps/services/seating/models/seat.py
M byceps/services/seating/seat_group_service.py
M byceps/services/seating/seat_service.py
M byceps/services/shop/article/service.py
M byceps/services/shop/order/action_registry_service.py
M byceps/services/shop/order/email/service.py
M byceps/services/shop/order/export/service.py
M byceps/services/shop/order/ordered_articles_service.py
M byceps/services/shop/order/service.py
M byceps/services/shop/shipping/service.py
M byceps/services/snippet/service.py
M byceps/services/terms/consent_service.py
M byceps/services/terms/version_service.py
M byceps/services/text_markup/service.py
M byceps/services/ticketing/attendance_service.py
M byceps/services/ticketing/barcode_service.py
M byceps/services/ticketing/category_service.py
M byceps/services/ticketing/models/ticket.py
M byceps/services/ticketing/ticket_seat_management_service.py
M byceps/services/ticketing/ticket_service.py
M byceps/services/ticketing/ticket_user_checkin_service.py
M byceps/services/ticketing/ticket_user_management_service.py
M byceps/services/tourney/avatar/models.py
M byceps/services/tourney/category_service.py
M byceps/services/user/command_service.py
M byceps/services/user/screen_name_validator.py
M byceps/util/system.py
M byceps/util/views.py
M byceps/application.py => byceps/application.py +3 -2
@@ 225,6 225,7 @@ def init_app(app: Flask) -> None:

        if app_mode.is_admin() and app.config['RQ_DASHBOARD_ENABLED']:
            import rq_dashboard

            app.register_blueprint(
                rq_dashboard.blueprint, url_prefix='/admin/rq'
            )


@@ 261,7 262,7 @@ def _get_site_template_context() -> Dict[str, Any]:


def _find_site_template_context_processor_cached(
    site_id: str
    site_id: str,
) -> Optional[Callable[[], Dict[str, Any]]]:
    """Return the template context processor for the site.



@@ 280,7 281,7 @@ def _find_site_template_context_processor_cached(


def _find_site_template_context_processor(
    site_id: str
    site_id: str,
) -> Optional[Callable[[], Dict[str, Any]]]:
    """Import a template context processor from the site's package.


M byceps/blueprints/admin/board/views.py => byceps/blueprints/admin/board/views.py +3 -2
@@ 182,8 182,9 @@ def category_update_form(category_id, erroneous_form=None):
    board = board_service.find_board(category.board_id)
    brand = brand_service.find_brand(board.brand_id)

    form = erroneous_form if erroneous_form \
           else CategoryUpdateForm(obj=category)
    form = (
        erroneous_form if erroneous_form else CategoryUpdateForm(obj=category)
    )

    return {
        'category': category,

M byceps/blueprints/admin/news/views.py => byceps/blueprints/admin/news/views.py +6 -6
@@ 228,10 228,7 @@ def image_update(image_id):
    attribution = form.attribution.data.strip()

    news_image_service.update_image(
        image.id,
        alt_text=alt_text,
        caption=caption,
        attribution=attribution,
        image.id, alt_text=alt_text, caption=caption, attribution=attribution,
    )

    flash_success(f'Das Newsbild #{image.number} wurde aktualisiert.')


@@ 399,8 396,11 @@ def item_update_form(item_id, erroneous_form=None):

    current_version = news_item_service.get_current_item_version(item.id)

    form = erroneous_form if erroneous_form \
            else ItemUpdateForm(obj=current_version, slug=item.slug)
    form = (
        erroneous_form
        if erroneous_form
        else ItemUpdateForm(obj=current_version, slug=item.slug)
    )

    return {
        'item': item,

M byceps/blueprints/admin/orga/views.py => byceps/blueprints/admin/orga/views.py +5 -2
@@ 147,8 147,11 @@ def export_persons(brand_id):
    ]

    def to_dict(user):
        date_of_birth = user.detail.date_of_birth.strftime('%d.%m.%Y') \
                        if user.detail.date_of_birth else None
        date_of_birth = (
            user.detail.date_of_birth.strftime('%d.%m.%Y')
            if user.detail.date_of_birth
            else None
        )

        return {
            'Benutzername': user.screen_name,

M byceps/blueprints/admin/orga_presence/views.py => byceps/blueprints/admin/orga_presence/views.py +1 -1
@@ 70,7 70,7 @@ def view(party_id):


def _group_presences_by_orga(
    presences: Iterable[PresenceTimeSlot]
    presences: Iterable[PresenceTimeSlot],
) -> Dict[User, PresenceTimeSlot]:
    d = defaultdict(set)


M byceps/blueprints/admin/orga_team/views.py => byceps/blueprints/admin/orga_team/views.py +5 -2
@@ 177,8 177,11 @@ def membership_update_form(membership_id, erroneous_form=None):

    teams = orga_team_service.get_teams_for_party(membership.orga_team.party_id)

    form = erroneous_form if erroneous_form \
           else MembershipUpdateForm(obj=membership)
    form = (
        erroneous_form
        if erroneous_form
        else MembershipUpdateForm(obj=membership)
    )
    form.set_orga_team_choices(teams)

    return {

M byceps/blueprints/admin/shop/article/views.py => byceps/blueprints/admin/shop/article/views.py +12 -6
@@ 140,10 140,13 @@ def create_form(shop_id, erroneous_form=None):
    )
    article_number_sequence_available = bool(article_number_sequences)

    form = erroneous_form if erroneous_form else ArticleCreateForm(
        price=Decimal('0.00'),
        tax_rate=Decimal('0.19'),
        quantity=0)
    form = (
        erroneous_form
        if erroneous_form
        else ArticleCreateForm(
            price=Decimal('0.00'), tax_rate=Decimal('0.19'), quantity=0
        )
    )
    form.set_article_number_sequence_choices(article_number_sequences)

    return {


@@ 286,8 289,11 @@ def attachment_create_form(article_id, erroneous_form=None):

    attachable_articles = article_service.get_attachable_articles(article.id)

    form = erroneous_form if erroneous_form else ArticleAttachmentCreateForm(
        quantity=0)
    form = (
        erroneous_form
        if erroneous_form
        else ArticleAttachmentCreateForm(quantity=0)
    )
    form.set_article_to_attach_choices(attachable_articles)

    return {

M byceps/blueprints/admin/shop/order/models.py => byceps/blueprints/admin/shop/order/models.py +4 -2
@@ 42,7 42,9 @@ class OrderStateFilter(Enum):
    @classmethod
    def find_for_payment_state(cls, payment_state):
        def match(order_state_filter):
            return order_state_filter.payment_state == payment_state and \
                order_state_filter.shipped is None
            return (
                order_state_filter.payment_state == payment_state
                and order_state_filter.shipped is None
            )

        return iterables.find(match, cls)

M byceps/blueprints/admin/shop/order/service.py => byceps/blueprints/admin/shop/order/service.py +3 -3
@@ 33,7 33,7 @@ class OrderWithOrderer(Order):


def extend_order_tuples_with_orderer(
    orders: Sequence[Order]
    orders: Sequence[Order],
) -> Iterator[OrderWithOrderer]:
    orderer_ids = {order.placed_by_id for order in orders}
    orderers = user_service.find_users(orderer_ids, include_avatars=True)


@@ 134,7 134,7 @@ def _get_additional_data_for_badge_awarded(event: OrderEvent) -> OrderEventData:


def _get_additional_data_for_ticket_bundle_created(
    event: OrderEvent
    event: OrderEvent,
) -> OrderEventData:
    bundle_id = event.data['ticket_bundle_id']
    category_id = event.data['ticket_bundle_category_id']


@@ 168,7 168,7 @@ def _get_additional_data_for_ticket_bundle_revoked(


def _get_additional_data_for_ticket_created(
    event: OrderEvent
    event: OrderEvent,
) -> OrderEventData:
    ticket_id = event.data['ticket_id']
    ticket_code = event.data['ticket_code']

M byceps/blueprints/admin/shop/shop/views.py => byceps/blueprints/admin/shop/shop/views.py +1 -5
@@ 114,11 114,7 @@ def create():
    title = form.title.data.strip()
    email_config_id = form.email_config_id.data

    shop = shop_service.create_shop(
        shop_id,
        title,
        email_config_id,
    )
    shop = shop_service.create_shop(shop_id, title, email_config_id)

    flash_success(f'Der Shop "{shop.title}" wurde angelegt.')
    return redirect_to('.index')

M byceps/blueprints/admin/ticketing/service.py => byceps/blueprints/admin/ticketing/service.py +26 -21
@@ 24,13 24,18 @@ def get_events(ticket_id: TicketID) -> Iterator[TicketEventData]:
    events = event_service.get_events_for_ticket(ticket_id)
    events.insert(0, _fake_ticket_creation_event(ticket_id))

    user_ids = set(_find_values_for_keys(events, {
        'initiator_id',
        'appointed_seat_manager_id',
        'appointed_user_manager_id',
        'appointed_user_id',
        'checked_in_user_id',
        }))
    user_ids = set(
        _find_values_for_keys(
            events,
            {
                'initiator_id',
                'appointed_seat_manager_id',
                'appointed_user_manager_id',
                'appointed_user_id',
                'checked_in_user_id',
            },
        )
    )
    users = user_service.find_users(user_ids, include_avatars=True)
    users_by_id = {str(user.id): user for user in users}



@@ 70,17 75,17 @@ def _get_additional_data(
    event: TicketEvent, users_by_id: Dict[str, User]
) -> Iterator[Tuple[str, Any]]:
    if event.event_type in {
            'seat-manager-appointed',
            'seat-manager-withdrawn',
            'seat-occupied',
            'seat-released',
            'ticket-revoked',
            'user-appointed',
            'user-checked-in',
            'user-check-in-reverted',
            'user-manager-appointed',
            'user-manager-withdrawn',
            'user-withdrawn',
        'seat-manager-appointed',
        'seat-manager-withdrawn',
        'seat-occupied',
        'seat-released',
        'ticket-revoked',
        'user-appointed',
        'user-checked-in',
        'user-check-in-reverted',
        'user-manager-appointed',
        'user-manager-withdrawn',
        'user-withdrawn',
    }:
        yield from _get_additional_data_for_user_initiated_event(
            event, users_by_id


@@ 131,7 136,7 @@ def _get_additional_data_for_user_initiated_event(


def _get_additional_data_for_seat_occupied_event(
    event: TicketEvent
    event: TicketEvent,
) -> Iterator[Tuple[str, Any]]:
    seat_id = event.data['seat_id']
    seat = seat_service.find_seat(seat_id)


@@ 144,7 149,7 @@ def _get_additional_data_for_seat_occupied_event(


def _get_additional_data_for_seat_released_event(
    event: TicketEvent
    event: TicketEvent,
) -> Iterator[Tuple[str, Any]]:
    seat_id = event.data.get('seat_id')
    if seat_id:


@@ 153,7 158,7 @@ def _get_additional_data_for_seat_released_event(


def _get_additional_data_for_ticket_revoked_event(
    event: TicketEvent
    event: TicketEvent,
) -> Iterator[Tuple[str, Any]]:
    reason = event.data.get('reason')
    if reason:

M byceps/blueprints/admin/ticketing/views.py => byceps/blueprints/admin/ticketing/views.py +4 -2
@@ 137,7 137,9 @@ def set_user_checked_in_flag(ticket_id):
    initiator_id = g.current_user.id

    try:
        event = ticket_user_checkin_service.check_in_user(ticket.id, initiator_id)
        event = ticket_user_checkin_service.check_in_user(
            ticket.id, initiator_id
        )
    except ticket_exceptions.UserAccountDeleted:
        flash_error(
            'Das dem Ticket zugewiesene Benutzerkonto ist gelöscht worden. '


@@ 155,7 157,7 @@ def set_user_checked_in_flag(ticket_id):

    flash_success(f"Benutzer '{ticket.used_by.screen_name}' wurde eingecheckt.")

    occupies_seat = (ticket.occupied_seat_id is not None)
    occupies_seat = ticket.occupied_seat_id is not None
    if not occupies_seat:
        flash_notice('Das Ticket belegt noch keinen Sitzplatz.', icon='warning')


M byceps/blueprints/admin/tourney/views.py => byceps/blueprints/admin/tourney/views.py +5 -2
@@ 83,8 83,11 @@ def category_update_form(category_id, erroneous_form=None):
    """Show form to update a category."""
    category = _get_category_or_404(category_id)

    form = erroneous_form if erroneous_form \
           else TourneyCategoryUpdateForm(obj=category)
    form = (
        erroneous_form
        if erroneous_form
        else TourneyCategoryUpdateForm(obj=category)
    )

    return {
        'category': category,

M byceps/blueprints/api/v1/snippet/views.py => byceps/blueprints/api/v1/snippet/views.py +7 -5
@@ 36,11 36,13 @@ def get_snippet_by_name(scope_type, scope_name, snippet_name):

    content = _get_content(version)

    return jsonify({
        'type': version.snippet.type_.name,
        'version': version.id,
        'content': content,
    })
    return jsonify(
        {
            'type': version.snippet.type_.name,
            'version': version.id,
            'content': content,
        }
    )


def _get_content(version):

M byceps/blueprints/api/v1/tourney/match/comments/views.py => byceps/blueprints/api/v1/tourney/match/comments/views.py +2 -5
@@ 112,9 112,7 @@ def _user_to_json(user: User) -> Dict[str, Any]:


blueprint.add_url_rule(
    '/match_comments/<uuid:comment_id>',
    endpoint='view',
    build_only=True,
    '/match_comments/<uuid:comment_id>', endpoint='view', build_only=True,
)




@@ 179,8 177,7 @@ def hide(comment_id):


@blueprint.route(
    '/match_comments/<uuid:comment_id>/flags/hidden',
    methods=['DELETE'],
    '/match_comments/<uuid:comment_id>/flags/hidden', methods=['DELETE'],
)
@api_token_required
@respond_no_content

M byceps/blueprints/api/v1/user/views.py => byceps/blueprints/api/v1/user/views.py +7 -5
@@ 34,11 34,13 @@ def get_profile(user_id):
    if user is None:
        return create_empty_json_response(404)

    return jsonify({
        'id': user.id,
        'screen_name': user.screen_name,
        'avatar_url': user.avatar_url,
    })
    return jsonify(
        {
            'id': user.id,
            'screen_name': user.screen_name,
            'avatar_url': user.avatar_url,
        }
    )


@blueprint.route('/invalidate_email_address', methods=['POST'])

M byceps/blueprints/authentication/forms.py => byceps/blueprints/authentication/forms.py +8 -4
@@ 31,8 31,10 @@ class RequestPasswordResetForm(LocalizedForm):
def _get_new_password_validators(companion_field_name):
    return [
        InputRequired(),
        EqualTo(companion_field_name,
                message='Das neue Passwort muss mit der Wiederholung übereinstimmen.'),
        EqualTo(
            companion_field_name,
            message='Das neue Passwort muss mit der Wiederholung übereinstimmen.',
        ),
        Length(min=MINIMUM_PASSWORD_LENGTH, max=MAXIMUM_PASSWORD_LENGTH),
    ]



@@ 40,10 42,12 @@ def _get_new_password_validators(companion_field_name):
class ResetPasswordForm(LocalizedForm):
    new_password = PasswordField(
        'Neues Passwort',
        _get_new_password_validators('new_password_confirmation'))
        _get_new_password_validators('new_password_confirmation'),
    )
    new_password_confirmation = PasswordField(
        'Neues Passwort (Wiederholung)',
        _get_new_password_validators('new_password'))
        _get_new_password_validators('new_password'),
    )


class UpdatePasswordForm(ResetPasswordForm):

M byceps/blueprints/authentication/views.py => byceps/blueprints/authentication/views.py +3 -1
@@ 70,7 70,9 @@ def login_form():
        }

    form = LoginForm()
    user_account_creation_enabled = _is_user_account_creation_enabled(in_admin_mode)
    user_account_creation_enabled = _is_user_account_creation_enabled(
        in_admin_mode
    )

    return {
        'login_enabled': True,

M byceps/blueprints/board/service.py => byceps/blueprints/board/service.py +10 -4
@@ 43,9 43,12 @@ def add_unseen_postings_flag_to_categories(
    categories_with_flag = []

    for category in categories:
        contains_unseen_postings = not user.is_anonymous \
        contains_unseen_postings = (
            not user.is_anonymous
            and board_last_view_service.contains_category_unseen_postings(
                category, user.id)
                category, user.id
            )
        )

        category_with_flag = CategoryWithLastUpdateAndUnseenFlag.from_category_with_last_update(
            category, contains_unseen_postings


@@ 69,9 72,12 @@ def add_topic_creators(topics: Sequence[DbTopic]) -> None:
def add_topic_unseen_flag(topics: Sequence[DbTopic], user: CurrentUser) -> None:
    """Add `unseen` flag to topics."""
    for topic in topics:
        topic.contains_unseen_postings = not user.is_anonymous \
        topic.contains_unseen_postings = (
            not user.is_anonymous
            and board_last_view_service.contains_topic_unseen_postings(
                topic, user.id)
                topic, user.id
            )
        )


def add_unseen_flag_to_postings(

M byceps/blueprints/board/views_posting.py => byceps/blueprints/board/views_posting.py +4 -2
@@ 99,8 99,10 @@ def posting_create(topic_id):
        )
        return redirect(h.build_url_for_topic(topic.id))

    if topic.posting_limited_to_moderators \
            and not g.current_user.has_permission(BoardPermission.announce):
    if (
        topic.posting_limited_to_moderators
        and not g.current_user.has_permission(BoardPermission.announce)
    ):
        flash_error(
            'In diesem Thema dürfen nur Moderatoren Beiträge hinzufügen.',
            icon='announce',

M byceps/blueprints/board/views_topic.py => byceps/blueprints/board/views_topic.py +5 -2
@@ 195,8 195,11 @@ def topic_update_form(topic_id, erroneous_form=None):
        flash_error('Du darfst dieses Thema nicht bearbeiten.')
        return redirect(url)

    form = erroneous_form if erroneous_form \
            else TopicUpdateForm(obj=topic, body=topic.initial_posting.body)
    form = (
        erroneous_form
        if erroneous_form
        else TopicUpdateForm(obj=topic, body=topic.initial_posting.body)
    )

    return {
        'form': form,

M byceps/blueprints/consent/forms.py => byceps/blueprints/consent/forms.py +3 -2
@@ 16,8 16,9 @@ def create_consent_form(subjects):
    subject_ids_str = ','.join(subject.id.hex for subject in subjects)

    class ConsentForm(LocalizedForm):
        subject_ids = HiddenField(None, [InputRequired()],
                                  default=subject_ids_str)
        subject_ids = HiddenField(
            None, [InputRequired()], default=subject_ids_str
        )

    for subject in subjects:
        field_name = get_subject_field_name(subject)

M byceps/blueprints/core/views.py => byceps/blueprints/core/views.py +1 -2
@@ 69,8 69,7 @@ def add_page_arg(args, page):

@blueprint.app_template_test()
def is_current_page(nav_item_path, current_page=None):
    return (current_page is not None) \
            and (nav_item_path == current_page)
    return (current_page is not None) and (nav_item_path == current_page)


@blueprint.before_app_request

M byceps/blueprints/ticketing/views.py => byceps/blueprints/ticketing/views.py +4 -2
@@ 370,6 370,8 @@ def _get_ticket_or_404(ticket_id):

def _is_user_allowed_to_print_ticket(ticket, user_id):
    """Return `True` only if the user is allowed to print the ticket."""
    return ticket.is_owned_by(user_id) \
        or ticket.is_managed_by(user_id) \
    return (
        ticket.is_owned_by(user_id)
        or ticket.is_managed_by(user_id)
        or ticket.used_by_id == user_id
    )

M byceps/blueprints/user/current/views.py => byceps/blueprints/user/current/views.py +8 -6
@@ 42,7 42,7 @@ def view():

    if get_app_mode().is_site():
        newsletter_list_id = _find_newsletter_list_for_brand()
        newsletter_offered = (newsletter_list_id is not None)
        newsletter_offered = newsletter_list_id is not None

        subscribed_to_newsletter = newsletter_service.is_subscribed(
            user.id, newsletter_list_id


@@ 72,11 72,13 @@ def view_as_json():
        # Return empty response.
        return Response(status=403)

    return jsonify({
        'id': user.id,
        'screen_name': user.screen_name,
        'avatar_url': user.avatar_url,
    })
    return jsonify(
        {
            'id': user.id,
            'screen_name': user.screen_name,
            'avatar_url': user.avatar_url,
        }
    )


@blueprint.route('/me/screen_name')

M byceps/config.py => byceps/config.py +3 -3
@@ 39,7 39,7 @@ def init_app(app: Flask) -> None:
        set_extension_value(KEY_SITE_ID, site_id, app)


def get_extension_value(key: str, app: Optional[Flask]=None) -> Any:
def get_extension_value(key: str, app: Optional[Flask] = None) -> Any:
    """Return the value for the key in this application's own extension
    namespace.



@@ 76,7 76,7 @@ def _determine_app_mode(app: Flask) -> AppMode:
        raise ConfigurationError(f'Invalid app mode "{value}" configured.')


def get_app_mode(app: Optional[Flask]=None) -> AppMode:
def get_app_mode(app: Optional[Flask] = None) -> AppMode:
    """Return the mode the site should run in."""
    return get_extension_value(KEY_APP_MODE, app)



@@ 93,6 93,6 @@ def _determine_site_id(app: Flask) -> SiteID:
    return site_id


def get_current_site_id(app: Optional[Flask]=None) -> SiteID:
def get_current_site_id(app: Optional[Flask] = None) -> SiteID:
    """Return the id of the current site."""
    return get_extension_value(KEY_SITE_ID, app)

M byceps/services/attendance/service.py => byceps/services/attendance/service.py +2 -2
@@ 85,7 85,7 @@ def _get_tickets_for_users(


def _index_tickets_by_user_id(
    tickets: Iterable[DbTicket]
    tickets: Iterable[DbTicket],
) -> Dict[UserID, Set[DbTicket]]:
    tickets_by_user_id = defaultdict(set)
    for ticket in tickets:


@@ 111,7 111,7 @@ def _to_attendee_tickets(tickets: Iterable[DbTicket]) -> List[AttendeeTicket]:


def _get_attendee_ticket_sort_key(
    attendee_ticket: AttendeeTicket
    attendee_ticket: AttendeeTicket,
) -> Tuple[bool, str, bool]:
    return (
        # List tickets with occupied seat first.

M byceps/services/authentication/password/service.py => byceps/services/authentication/password/service.py +6 -5
@@ 58,9 58,9 @@ def update_password_hash(
    credential.password_hash = password_hash
    credential.updated_at = now

    event = user_event_service.build_event('password-updated', user_id, {
        'initiator_id': str(initiator_id),
    })
    event = user_event_service.build_event(
        'password-updated', user_id, {'initiator_id': str(initiator_id),}
    )
    db.session.add(event)

    db.session.commit()


@@ 85,8 85,9 @@ def check_password_hash(password_hash: str, password: str) -> bool:
    """Hash the password and return `True` if the result matches the
    given hash, `False` otherwise.
    """
    return (password_hash is not None) \
        and _check_password_hash(password_hash, password)
    return (password_hash is not None) and _check_password_hash(
        password_hash, password
    )


def delete_password_hash(user_id: UserID) -> None:

M byceps/services/authentication/service.py => byceps/services/authentication/service.py +1 -1
@@ 39,7 39,7 @@ def authenticate(screen_name_or_email_address: str, password: str) -> User:


def _find_user_by_screen_name_or_email_address(
    screen_name_or_email_address: str
    screen_name_or_email_address: str,
) -> Optional[User]:
    if '@' in screen_name_or_email_address:
        return user_service.find_user_by_email_address(

M byceps/services/board/category_query_service.py => byceps/services/board/category_query_service.py +2 -2
@@ 73,7 73,7 @@ def get_categories_excluding(


def get_categories_with_last_updates(
    board_id: BoardID
    board_id: BoardID,
) -> Sequence[CategoryWithLastUpdate]:
    """Return the categories for that board.



@@ 108,7 108,7 @@ def _db_entity_to_category(category: DbCategory) -> Category:


def _db_entity_to_category_with_last_update(
    category: DbCategory
    category: DbCategory,
) -> CategoryWithLastUpdate:
    return CategoryWithLastUpdate(
        category.id,

M byceps/services/board/last_view_service.py => byceps/services/board/last_view_service.py +4 -6
@@ 77,8 77,7 @@ def contains_topic_unseen_postings(topic: DbTopic, user_id: UserID) -> bool:
    """
    last_viewed_at = find_topic_last_viewed_at(topic.id, user_id)

    return last_viewed_at is None \
        or topic.last_updated_at > last_viewed_at
    return last_viewed_at is None or topic.last_updated_at > last_viewed_at


def find_last_topic_view(


@@ 130,9 129,8 @@ def mark_all_topics_in_category_as_viewed(
        'occurred_at': datetime.utcnow(),
    }

    identifiers = [{
        'user_id': user_id,
        'topic_id': topic_id,
    } for topic_id in topic_ids]
    identifiers = [
        {'user_id': user_id, 'topic_id': topic_id} for topic_id in topic_ids
    ]

    upsert_many(table, identifiers, replacement)

M byceps/services/board/models/posting.py => byceps/services/board/models/posting.py +4 -7
@@ 78,13 78,10 @@ class Posting(db.Model):

    def may_be_updated_by_user(self, user: User) -> bool:
        return (
            (
                not self.topic.locked
                    and user.id == self.creator_id
                    and user.has_permission(BoardPostingPermission.update)
            )
            or user.has_permission(BoardPermission.update_of_others)
        )
            not self.topic.locked
            and user.id == self.creator_id
            and user.has_permission(BoardPostingPermission.update)
        ) or user.has_permission(BoardPermission.update_of_others)

    def is_unseen(self, user: CurrentUser, last_viewed_at: datetime) -> bool:
        # Don't display any posting as new to a guest.

M byceps/services/consent/subject_service.py => byceps/services/consent/subject_service.py +4 -2
@@ 46,8 46,10 @@ def get_subjects_with_consent_counts() -> Dict[Subject, int]:
        .group_by(DbSubject.id) \
        .all()

    return {_db_entity_to_subject(subject): consent_count
            for subject, consent_count in rows}
    return {
        _db_entity_to_subject(subject): consent_count
        for subject, consent_count in rows
    }


def _db_entity_to_subject(subject: DbSubject) -> Subject:

M byceps/services/email/service.py => byceps/services/email/service.py +2 -4
@@ 128,10 128,8 @@ def get_all_configs() -> List[EmailConfig]:
def enqueue_message(message: Message) -> None:
    """Enqueue e-mail to be sent asynchronously."""
    enqueue_email(
        message.sender,
        message.recipients,
        message.subject,
        message.body)
        message.sender, message.recipients, message.subject, message.body
    )


def enqueue_email(

M byceps/services/metrics/models.py => byceps/services/metrics/models.py +4 -2
@@ 43,8 43,10 @@ class Metric:
    def serialize(self) -> str:
        labels_str = ''
        if self.labels:
            labels_str = '{' \
                + ', '.join(label.serialize() for label in self.labels) \
            labels_str = (
                '{'
                + ', '.join(label.serialize() for label in self.labels)
                + '}'
            )

        return f'{self.name}{labels_str} {self.value}'

M byceps/services/metrics/service.py => byceps/services/metrics/service.py +5 -3
@@ 82,7 82,7 @@ def _collect_consent_metrics() -> Iterator[Metric]:


def _collect_shop_ordered_article_metrics(
    shop_ids: Set[ShopID]
    shop_ids: Set[ShopID],
) -> Iterator[Metric]:
    """Provide ordered article quantities for shops."""
    stats = shop_article_service.sum_ordered_articles_by_payment_state(shop_ids)


@@ 119,7 119,7 @@ def _collect_shop_order_metrics(shops: List[Shop]) -> Iterator[Metric]:


def _collect_seating_metrics(
    active_party_ids: List[PartyID]
    active_party_ids: List[PartyID],
) -> Iterator[Metric]:
    """Provide seat occupation counts per party and category."""
    for party_id in active_party_ids:


@@ 155,7 155,9 @@ def _collect_ticket_metrics(active_parties: List[Party]) -> Iterator[Metric]:
            'tickets_revoked_count', tickets_revoked_count, labels=labels
        )

        tickets_sold_count = ticket_service.count_sold_tickets_for_party(party_id)
        tickets_sold_count = ticket_service.count_sold_tickets_for_party(
            party_id
        )
        yield Metric('tickets_sold_count', tickets_sold_count, labels=labels)

        tickets_checked_in_count = ticket_service.count_tickets_checked_in_for_party(

M byceps/services/newsletter/service.py => byceps/services/newsletter/service.py +2 -2
@@ 128,7 128,7 @@ def _get_subscriber_details(user_ids: Set[UserID]) -> Iterator[Subscriber]:


def count_subscriptions_by_state(
    list_id: ListID
    list_id: ListID,
) -> Dict[Union[SubscriptionState, str], int]:
    """Return the totals for each state as well as an overall total."""
    rows = _build_query_for_current_state(list_id) \


@@ 232,7 232,7 @@ def get_subscription_state(


def get_subscription_updates_for_user(
    user_id: UserID
    user_id: UserID,
) -> Sequence[DbSubscriptionUpdate]:
    """Return subscription updates made by the user, for any list."""
    return DbSubscriptionUpdate.query \

M byceps/services/orga/service.py => byceps/services/orga/service.py +16 -8
@@ 76,10 76,14 @@ def add_orga_flag(
    orga_flag = OrgaFlag(brand_id, user_id)
    db.session.add(orga_flag)

    event = user_event_service.build_event('orgaflag-added', user_id, {
        'brand_id': str(brand_id),
        'initiator_id': str(initiator_id),
    })
    event = user_event_service.build_event(
        'orgaflag-added',
        user_id,
        {
            'brand_id': str(brand_id),
            'initiator_id': str(initiator_id),
        },
    )
    db.session.add(event)

    db.session.commit()


@@ 92,10 96,14 @@ def remove_orga_flag(orga_flag: OrgaFlag, initiator_id: UserID) -> None:
    db.session.delete(orga_flag)

    user_id = orga_flag.user_id
    event = user_event_service.build_event('orgaflag-removed', user_id, {
        'brand_id': str(orga_flag.brand_id),
        'initiator_id': str(initiator_id),
    })
    event = user_event_service.build_event(
        'orgaflag-removed',
        user_id,
        {
            'brand_id': str(orga_flag.brand_id),
            'initiator_id': str(initiator_id),
        },
    )
    db.session.add(event)

    db.session.commit()

M byceps/services/orga_presence/service.py => byceps/services/orga_presence/service.py +3 -9
@@ 42,18 42,12 @@ def get_tasks(party_id: PartyID) -> List[TaskTimeSlot]:

def _presence_to_time_slot(presence: Presence) -> PresenceTimeSlot:
    return PresenceTimeSlot.from_(
        presence.orga,
        presence.starts_at,
        presence.ends_at,
        presence.orga, presence.starts_at, presence.ends_at,
    )


def _task_to_time_slot(task: Task) -> TaskTimeSlot:
    return TaskTimeSlot.from_(
        task.title,
        task.starts_at,
        task.ends_at,
    )
    return TaskTimeSlot.from_(task.title, task.starts_at, task.ends_at)


# -------------------------------------------------------------------- #


@@ 90,7 84,7 @@ def _to_datetimes_without_tzinfo(dts: Sequence[DateTime]) -> Iterator[datetime]:


def get_days_and_hour_totals(
    hour_ranges: Sequence[DateTimeRange]
    hour_ranges: Sequence[DateTimeRange],
) -> Iterator[Tuple[date, int]]:
    """Yield (day, relevant hours total) pairs."""


M byceps/services/orga_team/service.py => byceps/services/orga_team/service.py +4 -2
@@ 83,7 83,7 @@ def _find_db_team(team_id: OrgaTeamID) -> Optional[DbOrgaTeam]:


def get_teams_and_memberships_for_party(
    party_id: PartyID
    party_id: PartyID,
) -> Sequence[Tuple[OrgaTeam, DbMembership]]:
    """Return all orga teams and their corresponding memberships for
    that party.


@@ 221,7 221,9 @@ def get_public_orgas_for_party(party_id: PartyID) -> Set[PublicOrga]:
    return orgas


def _get_public_orga_users_by_id(memberships: DbMembership) -> Dict[UserID, User]:
def _get_public_orga_users_by_id(
    memberships: DbMembership,
) -> Dict[UserID, User]:
    user_ids = {ms.user_id for ms in memberships}

    users = user_service.find_users(user_ids, include_avatars=True)

M byceps/services/seating/models/seat.py => byceps/services/seating/models/seat.py +3 -2
@@ 73,8 73,9 @@ class Seat(db.Model):
        """Return `True` if the seat is occupied by a ticket, and that
        ticket is assigned to a user.
        """
        return self.is_occupied and \
            (self.occupied_by_ticket.used_by_id is not None)
        return self.is_occupied and (
            self.occupied_by_ticket.used_by_id is not None
        )

    @property
    def user(self) -> Optional[User]:

M byceps/services/seating/seat_group_service.py => byceps/services/seating/seat_group_service.py +4 -3
@@ 30,7 30,7 @@ def create_seat_group(
    title: str,
    seats: Sequence[DbSeat],
    *,
    commit: bool=True,
    commit: bool = True,
) -> DbSeatGroup:
    """Create a seat group and assign the given seats."""
    seat_quantity = len(seats)


@@ 38,8 38,9 @@ def create_seat_group(
        raise ValueError("No seats specified.")

    ticket_category_ids = {seat.category_id for seat in seats}
    if len(ticket_category_ids) != 1 \
            or (ticket_category_id not in ticket_category_ids):
    if len(ticket_category_ids) != 1 or (
        ticket_category_id not in ticket_category_ids
    ):
        raise ValueError("Seats' ticket category IDs do not match the group's.")

    group = DbSeatGroup(party_id, ticket_category_id, seat_quantity, title)

M byceps/services/seating/seat_service.py => byceps/services/seating/seat_service.py +1 -1
@@ 41,7 41,7 @@ def delete_seat(seat_id: SeatID) -> None:


def count_occupied_seats_by_category(
    party_id: PartyID
    party_id: PartyID,
) -> List[Tuple[TicketCategory, int]]:
    """Count occupied seats for the party, grouped by ticket category."""
    subquery = db.session \

M byceps/services/shop/article/service.py => byceps/services/shop/article/service.py +1 -1
@@ 201,7 201,7 @@ def find_attached_article(


def get_articles_by_numbers(
    article_numbers: Set[ArticleNumber]
    article_numbers: Set[ArticleNumber],
) -> Set[Article]:
    """Return the articles with those numbers."""
    if not article_numbers:

M byceps/services/shop/order/action_registry_service.py => byceps/services/shop/order/action_registry_service.py +6 -4
@@ 24,8 24,9 @@ def register_badge_awarding(
    params_create = {
        'badge_id': str(badge_id),
    }
    action_service.create_action(article_number, PaymentState.paid,
                                 'create_tickets', params_create)
    action_service.create_action(
        article_number, PaymentState.paid, 'create_tickets', params_create
    )


def register_ticket_bundles_creation(


@@ 63,8 64,9 @@ def register_tickets_creation(
    params_create = {
        'category_id': str(ticket_category_id),
    }
    action_service.create_action(article_number, PaymentState.paid,
                                 'create_tickets', params_create)
    action_service.create_action(
        article_number, PaymentState.paid, 'create_tickets', params_create
    )

    # Revoke tickets that have been created for the order when it is
    # canceled after being marked as paid.

M byceps/services/shop/order/email/service.py => byceps/services/shop/order/email/service.py +2 -2
@@ 64,7 64,7 @@ def send_email_for_paid_order_to_orderer(order_id: OrderID) -> None:


def _assemble_email_for_incoming_order_to_orderer(
    data: OrderEmailData
    data: OrderEmailData,
) -> Message:
    order = data.order



@@ 91,7 91,7 @@ def _get_payment_instructions(order: Order) -> str:


def _assemble_email_for_canceled_order_to_orderer(
    data: OrderEmailData
    data: OrderEmailData,
) -> Message:
    subject = (
        f'\u274c Deine Bestellung ({data.order.order_number}) '

M byceps/services/shop/order/export/service.py => byceps/services/shop/order/export/service.py +3 -2
@@ 67,8 67,9 @@ def _format_export_datetime(dt: datetime) -> str:
    tz_str = current_app.config['SHOP_ORDER_EXPORT_TIMEZONE']
    localized_dt = pendulum.instance(dt).in_tz(tz_str)

    date_time, utc_offset = localized_dt.strftime('%Y-%m-%dT%H:%M:%S|%z') \
                                        .split('|', 1)
    date_time, utc_offset = localized_dt.strftime('%Y-%m-%dT%H:%M:%S|%z').split(
        '|', 1
    )

    if len(utc_offset) == 5:
        # Insert colon between hours and minutes.

M byceps/services/shop/order/ordered_articles_service.py => byceps/services/shop/order/ordered_articles_service.py +2 -2
@@ 18,7 18,7 @@ from .transfer.models import OrderItem, PaymentState


def count_ordered_articles(
    article_number: ArticleNumber
    article_number: ArticleNumber,
) -> Dict[PaymentState, int]:
    """Count how often the article has been ordered, grouped by the
    order's payment state.


@@ 43,7 43,7 @@ def count_ordered_articles(


def get_order_items_for_article(
    article_number: ArticleNumber
    article_number: ArticleNumber,
) -> Sequence[OrderItem]:
    """Return all order items for that article."""
    order_items = DbOrderItem.query \

M byceps/services/shop/order/service.py => byceps/services/shop/order/service.py +19 -13
@@ 112,9 112,7 @@ def _build_order(
    )


def _build_order_items(
    cart: Cart, order: DbOrder
) -> Iterator[DbOrderItem]:
def _build_order_items(cart: Cart, order: DbOrder) -> Iterator[DbOrderItem]:
    """Build order items from the cart's content."""
    for cart_item in cart.get_items():
        article = cart_item.article


@@ 250,14 248,20 @@ def cancel_order(

    updated_at = now
    payment_state_from = order.payment_state
    payment_state_to = PaymentState.canceled_after_paid if has_order_been_paid \
                  else PaymentState.canceled_before_paid
    payment_state_to = (
        PaymentState.canceled_after_paid
        if has_order_been_paid
        else PaymentState.canceled_before_paid
    )

    _update_payment_state(order, payment_state_to, updated_at, initiator_id)
    order.cancelation_reason = reason

    event_type = 'order-canceled-after-paid' if has_order_been_paid \
            else 'order-canceled-before-paid'
    event_type = (
        'order-canceled-after-paid'
        if has_order_been_paid
        else 'order-canceled-before-paid'
    )
    data = {
        'initiator_id': str(initiator_id),
        'former_payment_state': payment_state_from.name,


@@ 315,11 319,13 @@ def mark_order_as_paid(
    event_data = {}
    if additional_event_data is not None:
        event_data.update(additional_event_data)
    event_data.update({
        'initiator_id': str(initiator_id),
        'former_payment_state': payment_state_from.name,
        'payment_method': payment_method.name,
    })
    event_data.update(
        {
            'initiator_id': str(initiator_id),
            'former_payment_state': payment_state_from.name,
            'payment_method': payment_method.name,
        }
    )

    event = DbOrderEvent(now, event_type, order.id, event_data)
    db.session.add(event)


@@ 439,7 445,7 @@ def find_order_by_order_number(order_number: OrderNumber) -> Optional[Order]:


def find_orders_by_order_numbers(
    order_numbers: Set[OrderNumber]
    order_numbers: Set[OrderNumber],
) -> Sequence[Order]:
    """Return the orders with those order numbers."""
    if not order_numbers:

M byceps/services/shop/shipping/service.py => byceps/services/shop/shipping/service.py +1 -1
@@ 111,7 111,7 @@ def _aggregate_ordered_article_quantites(


def _get_article_descriptions(
    article_numbers: Set[ArticleNumber]
    article_numbers: Set[ArticleNumber],
) -> Dict[ArticleNumber, str]:
    """Look up description texts of the specified articles."""
    if not article_numbers:

M byceps/services/snippet/service.py => byceps/services/snippet/service.py +2 -2
@@ 192,7 192,7 @@ def get_snippets(snippet_ids: Set[SnippetID]) -> Sequence[DbSnippet]:


def get_snippets_for_scope_with_current_versions(
    scope: Scope
    scope: Scope,
) -> Sequence[DbSnippet]:
    """Return all snippets with their current versions for that scope."""
    return DbSnippet.query \


@@ 205,7 205,7 @@ def get_snippets_for_scope_with_current_versions(


def find_snippet_version(
    version_id: SnippetVersionID
    version_id: SnippetVersionID,
) -> Optional[DbSnippetVersion]:
    """Return the snippet version with that id, or `None` if not found."""
    return DbSnippetVersion.query.get(version_id)

M byceps/services/terms/consent_service.py => byceps/services/terms/consent_service.py +1 -1
@@ 18,7 18,7 @@ from .transfer.models import DocumentID, VersionID


def count_consents_for_document_versions(
    document_id: DocumentID
    document_id: DocumentID,
) -> Dict[VersionID, int]:
    """Return the number of consents for each version of the document."""
    rows = db.session \

M byceps/services/terms/version_service.py => byceps/services/terms/version_service.py +1 -1
@@ 43,7 43,7 @@ def find_version(version_id: VersionID) -> Optional[DbVersion]:


def find_version_for_consent_subject_id(
    consent_subject_id: ConsentSubjectID
    consent_subject_id: ConsentSubjectID,
) -> Optional[DbVersion]:
    """Return the version with that consent subject ID, or `None` if
    not found.

M byceps/services/text_markup/service.py => byceps/services/text_markup/service.py +3 -1
@@ 64,7 64,9 @@ def _add_quote_formatter(parser: Parser) -> None:
        intro = ''
        if 'author' in options:
            author = escape(options['author'])
            intro = f'<p class="quote-intro"><cite>{author}</cite> schrieb:</p>\n'
            intro = (
                f'<p class="quote-intro"><cite>{author}</cite> schrieb:</p>\n'
            )
        return f'{intro}<blockquote>{value}</blockquote>'

    parser.add_formatter('quote', render_quote, strip=True)

M byceps/services/ticketing/attendance_service.py => byceps/services/ticketing/attendance_service.py +3 -3
@@ 116,7 116,7 @@ def get_attendees_by_party(party_ids: Set[PartyID]) -> Dict[PartyID, Set[User]]:


def get_attendee_ids_for_parties(
    party_ids: Set[PartyID]
    party_ids: Set[PartyID],
) -> Dict[PartyID, Set[UserID]]:
    """Return the partys' attendee IDs, indexed by party ID."""
    if not party_ids:


@@ 178,7 178,7 @@ def get_top_attendees_for_brand(brand_id: BrandID) -> List[Tuple[UserID, int]]:


def _get_top_ticket_attendees_for_parties(
    brand_id: BrandID
    brand_id: BrandID,
) -> List[Tuple[UserID, int]]:
    user_id_column = db.aliased(DbTicket).used_by_id



@@ 206,7 206,7 @@ def _get_top_ticket_attendees_for_parties(


def _get_top_archived_attendees_for_parties(
    brand_id: BrandID
    brand_id: BrandID,
) -> List[Tuple[UserID, int]]:
    attendance_count_column = db.func \
        .count(DbArchivedAttendance.user_id) \

M byceps/services/ticketing/barcode_service.py => byceps/services/ticketing/barcode_service.py +4 -2
@@ 131,14 131,16 @@ VALUES_TO_WIDTHS = {value: width for value, _, width in VALUES_CHARS_WIDTHS}
CHARS_TO_VALUES = {char: value for value, char, _ in VALUES_CHARS_WIDTHS}


SVG_TEMPLATE = Template('''
SVG_TEMPLATE = Template(
    '''
<svg xmlns="http://www.w3.org/2000/svg" width="{{ image_width }}" height="{{ image_height }}" viewBox="0 0 {{ image_width }} {{ image_height }}">
  <rect width="{{ image_width }}" height="{{ image_height }}" fill="white"/>
  {%- for bar_x, bar_width in bars %}
  <rect x="{{ bar_x }}" width="{{ bar_width }}" height="{{ image_height }}"/>
  {%- endfor %}
</svg>
'''.strip())
'''.strip()
)


def render_svg(text, *, thickness=3):

M byceps/services/ticketing/category_service.py => byceps/services/ticketing/category_service.py +1 -1
@@ 58,7 58,7 @@ def get_categories_for_party(party_id: PartyID) -> Sequence[TicketCategory]:


def get_categories_with_ticket_counts_for_party(
    party_id: PartyID
    party_id: PartyID,
) -> Dict[TicketCategory, int]:
    """Return all categories with ticket counts for that party."""
    category = db.aliased(DbCategory)

M byceps/services/ticketing/models/ticket.py => byceps/services/ticketing/models/ticket.py +9 -6
@@ 101,18 101,21 @@ class Ticket(db.Model):
        """Return `True` if the user may choose the seat for or the
        user of this ticket.
        """
        return self.is_seat_managed_by(user_id) \
            or self.is_user_managed_by(user_id)
        return self.is_seat_managed_by(user_id) or self.is_user_managed_by(
            user_id
        )

    def is_seat_managed_by(self, user_id: UserID) -> bool:
        """Return `True` if the user may choose the seat for this ticket."""
        return ((self.seat_managed_by_id is None) and self.is_owned_by(user_id)) or \
            (self.seat_managed_by_id == user_id)
        return (
            (self.seat_managed_by_id is None) and self.is_owned_by(user_id)
        ) or (self.seat_managed_by_id == user_id)

    def is_user_managed_by(self, user_id: UserID) -> bool:
        """Return `True` if the user may choose the user of this ticket."""
        return ((self.user_managed_by_id is None) and self.is_owned_by(user_id)) or \
            (self.user_managed_by_id == user_id)
        return (
            (self.user_managed_by_id is None) and self.is_owned_by(user_id)
        ) or (self.user_managed_by_id == user_id)

    def __repr__(self) -> str:
        def user(user: User) -> Optional[str]:

M byceps/services/ticketing/ticket_seat_management_service.py => byceps/services/ticketing/ticket_seat_management_service.py +23 -11
@@ 36,10 36,14 @@ def appoint_seat_manager(

    ticket.seat_managed_by_id = manager_id

    event = event_service.build_event('seat-manager-appointed', ticket.id, {
        'appointed_seat_manager_id': str(manager_id),
        'initiator_id': str(initiator_id),
    })
    event = event_service.build_event(
        'seat-manager-appointed',
        ticket.id,
        {
            'appointed_seat_manager_id': str(manager_id),
            'initiator_id': str(initiator_id),
        },
    )
    db.session.add(event)

    db.session.commit()


@@ 54,9 58,13 @@ def withdraw_seat_manager(ticket_id: TicketID, initiator_id: UserID) -> None:

    ticket.seat_managed_by_id = None

    event = event_service.build_event('seat-manager-withdrawn', ticket.id, {
        'initiator_id': str(initiator_id),
    })
    event = event_service.build_event(
        'seat-manager-withdrawn',
        ticket.id,
        {
            'initiator_id': str(initiator_id),
        },
    )
    db.session.add(event)

    db.session.commit()


@@ 118,10 126,14 @@ def release_seat(ticket_id: TicketID, initiator_id: UserID) -> None:

    ticket.occupied_seat_id = None

    event = event_service.build_event('seat-released', ticket.id, {
        'seat_id': str(seat.id),
        'initiator_id': str(initiator_id),
    })
    event = event_service.build_event(
        'seat-released',
        ticket.id,
        {
            'seat_id': str(seat.id),
            'initiator_id': str(initiator_id),
        },
    )
    db.session.add(event)

    db.session.commit()

M byceps/services/ticketing/ticket_service.py => byceps/services/ticketing/ticket_service.py +1 -1
@@ 59,7 59,7 @@ def find_tickets(ticket_ids: Set[TicketID]) -> Sequence[DbTicket]:


def find_tickets_created_by_order(
    order_number: OrderNumber
    order_number: OrderNumber,
) -> Sequence[DbTicket]:
    """Return the tickets created by this order (as it was marked as paid)."""
    return DbTicket.query \

M byceps/services/ticketing/ticket_user_checkin_service.py => byceps/services/ticketing/ticket_user_checkin_service.py +8 -4
@@ 97,10 97,14 @@ def revert_user_check_in(ticket_id: TicketID, initiator_id: UserID) -> None:

    ticket.user_checked_in = False

    event = event_service.build_event('user-check-in-reverted', ticket.id, {
        'checked_in_user_id': str(ticket.used_by_id),
        'initiator_id': str(initiator_id),
    })
    event = event_service.build_event(
        'user-check-in-reverted',
        ticket.id,
        {
            'checked_in_user_id': str(ticket.used_by_id),
            'initiator_id': str(initiator_id),
        },
    )
    db.session.add(event)

    db.session.commit()

M byceps/services/ticketing/ticket_user_management_service.py => byceps/services/ticketing/ticket_user_management_service.py +30 -14
@@ 33,10 33,14 @@ def appoint_user_manager(

    ticket.user_managed_by_id = manager_id

    event = event_service.build_event('user-manager-appointed', ticket.id, {
        'appointed_user_manager_id': str(manager_id),
        'initiator_id': str(initiator_id),
    })
    event = event_service.build_event(
        'user-manager-appointed',
        ticket.id,
        {
            'appointed_user_manager_id': str(manager_id),
            'initiator_id': str(initiator_id),
        },
    )
    db.session.add(event)

    db.session.commit()


@@ 51,9 55,13 @@ def withdraw_user_manager(ticket_id: TicketID, initiator_id: UserID) -> None:

    ticket.user_managed_by_id = None

    event = event_service.build_event('user-manager-withdrawn', ticket.id, {
        'initiator_id': str(initiator_id),
    })
    event = event_service.build_event(
        'user-manager-withdrawn',
        ticket.id,
        {
            'initiator_id': str(initiator_id),
        },
    )
    db.session.add(event)

    db.session.commit()


@@ 82,10 90,14 @@ def appoint_user(

    ticket.used_by_id = user_id

    event = event_service.build_event('user-appointed', ticket.id, {
        'appointed_user_id': str(user_id),
        'initiator_id': str(initiator_id),
    })
    event = event_service.build_event(
        'user-appointed',
        ticket.id,
        {
            'appointed_user_id': str(user_id),
            'initiator_id': str(initiator_id),
        },
    )
    db.session.add(event)

    db.session.commit()


@@ 103,9 115,13 @@ def withdraw_user(ticket_id: TicketID, initiator_id: UserID) -> None:

    ticket.used_by_id = None

    event = event_service.build_event('user-withdrawn', ticket.id, {
        'initiator_id': str(initiator_id),
    })
    event = event_service.build_event(
        'user-withdrawn',
        ticket.id,
        {
            'initiator_id': str(initiator_id),
        },
    )
    db.session.add(event)

    db.session.commit()

M byceps/services/tourney/avatar/models.py => byceps/services/tourney/avatar/models.py +7 -1
@@ 59,7 59,13 @@ class Avatar(db.Model):

    @property
    def path(self) -> Path:
        path = current_app.config['PATH_DATA'] / 'parties' / self.party_id / 'tourney' / 'avatars'
        path = (
            current_app.config['PATH_DATA']
            / 'parties'
            / self.party_id
            / 'tourney'
            / 'avatars'
        )
        return path / self.filename

    @property

M byceps/services/tourney/category_service.py => byceps/services/tourney/category_service.py +1 -1
@@ 64,7 64,7 @@ def move_category_down(category: DbTourneyCategory) -> None:


def find_category(
    category_id: TourneyCategoryID
    category_id: TourneyCategoryID,
) -> Optional[DbTourneyCategory]:
    """Return the category with that id, or `None` if not found."""
    return DbTourneyCategory.query.get(category_id)

M byceps/services/user/command_service.py => byceps/services/user/command_service.py +24 -12
@@ 77,10 77,14 @@ def suspend_account(

    user.suspended = True

    event = event_service.build_event('user-suspended', user.id, {
        'initiator_id': str(initiator_id),
        'reason': reason,
    })
    event = event_service.build_event(
        'user-suspended',
        user.id,
        {
            'initiator_id': str(initiator_id),
            'reason': reason,
        },
    )
    db.session.add(event)

    db.session.commit()


@@ 100,10 104,14 @@ def unsuspend_account(

    user.suspended = False

    event = event_service.build_event('user-unsuspended', user.id, {
        'initiator_id': str(initiator_id),
        'reason': reason,
    })
    event = event_service.build_event(
        'user-unsuspended',
        user.id,
        {
            'initiator_id': str(initiator_id),
            'reason': reason,
        },
    )
    db.session.add(event)

    db.session.commit()


@@ 124,10 132,14 @@ def delete_account(
    user.deleted = True
    _anonymize_account(user)

    event = event_service.build_event('user-deleted', user.id, {
        'initiator_id': str(initiator_id),
        'reason': reason,
    })
    event = event_service.build_event(
        'user-deleted',
        user.id,
        {
            'initiator_id': str(initiator_id),
            'reason': reason,
        },
    )
    db.session.add(event)

    # Deassign authorization roles.

M byceps/services/user/screen_name_validator.py => byceps/services/user/screen_name_validator.py +3 -2
@@ 29,8 29,9 @@ def is_screen_name_valid(screen_name: str) -> bool:
    """Return `True` if the screen name has a valid length and contains
    only permitted characters.
    """
    return is_length_valid(screen_name) \
        and contains_only_valid_chars(screen_name)
    return is_length_valid(screen_name) and contains_only_valid_chars(
        screen_name
    )


def is_length_valid(screen_name: str) -> bool:

M byceps/util/system.py => byceps/util/system.py +4 -2
@@ 20,8 20,10 @@ def get_config_filename_from_env() -> str:

    Raise an exception if it isn't set.
    """
    error_message = "No configuration file was specified via the " \
                    f"'{CONFIG_VAR_NAME}' environment variable."
    error_message = (
        "No configuration file was specified via the "
        f"'{CONFIG_VAR_NAME}' environment variable."
    )

    return get_env_value(CONFIG_VAR_NAME, error_message)


M byceps/util/views.py => byceps/util/views.py +4 -0
@@ 22,19 22,23 @@ def create_empty_json_response(status):

def jsonified(f):
    """Send the data returned by the decorated function as JSON."""

    @wraps(f)
    def wrapper(*args, **kwargs):
        data = f(*args, **kwargs)
        return jsonify(data)

    return wrapper


def textified(f):
    """Send the data returned by the decorated function as plaintext."""

    @wraps(f)
    def wrapper(*args, **kwargs):
        data = f(*args, **kwargs)
        return Response(stream_with_context(data), mimetype='text/plain')

    return wrapper