~homeworkprod/byceps

634080d46c6bf42cbb990f42ae7e4b750f88c305 — Jochen Kupperschmidt 2 years ago d0d4197
Format calls according to Black
123 files changed, 1456 insertions(+), 804 deletions(-)

M byceps/application.py
M byceps/blueprints/admin/board/views.py
M byceps/blueprints/admin/consent/views.py
M byceps/blueprints/admin/dashboard/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/party/views.py
M byceps/blueprints/admin/seating/views.py
M byceps/blueprints/admin/shop/article/forms.py
M byceps/blueprints/admin/shop/article/views.py
M byceps/blueprints/admin/shop/email/views.py
M byceps/blueprints/admin/shop/order/service.py
M byceps/blueprints/admin/shop/order/views.py
M byceps/blueprints/admin/shop/shop/views.py
M byceps/blueprints/admin/site/views.py
M byceps/blueprints/admin/snippet/views.py
M byceps/blueprints/admin/terms/views.py
M byceps/blueprints/admin/ticketing/checkin/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/admin/user/service.py
M byceps/blueprints/admin/user/views.py
M byceps/blueprints/admin/user_badge/views.py
M byceps/blueprints/api/tourney/avatar/views.py
M byceps/blueprints/api/tourney/match/views.py
M byceps/blueprints/attendance/views.py
M byceps/blueprints/authentication/forms.py
M byceps/blueprints/authentication/session.py
M byceps/blueprints/authentication/views.py
M byceps/blueprints/authorization/registry.py
M byceps/blueprints/board/_helpers.py
M byceps/blueprints/board/service.py
M byceps/blueprints/board/views_category.py
M byceps/blueprints/board/views_posting.py
M byceps/blueprints/board/views_topic.py
M byceps/blueprints/consent/views.py
M byceps/blueprints/core/views.py
M byceps/blueprints/healthcheck/views.py
M byceps/blueprints/metrics/views.py
M byceps/blueprints/news/views.py
M byceps/blueprints/orga_team/views.py
M byceps/blueprints/seating/views.py
M byceps/blueprints/shop/order/views.py
M byceps/blueprints/shop/orders/views.py
M byceps/blueprints/snippet/init.py
M byceps/blueprints/snippet/templating.py
M byceps/blueprints/snippet/views.py
M byceps/blueprints/ticketing/forms.py
M byceps/blueprints/ticketing/notification_service.py
M byceps/blueprints/ticketing/views.py
M byceps/blueprints/user/avatar/views.py
M byceps/blueprints/user/creation/views.py
M byceps/blueprints/user/current/views.py
M byceps/blueprints/user/email_address/views.py
M byceps/blueprints/user/profile/views.py
M byceps/blueprints/user_badge/views.py
M byceps/blueprints/user_group/views.py
M byceps/blueprints/user_message/views.py
M byceps/config.py
M byceps/email.py
M byceps/services/authentication/password/reset_service.py
M byceps/services/board/category_command_service.py
M byceps/services/board/models/posting.py
M byceps/services/board/models/topic.py
M byceps/services/board/posting_query_service.py
M byceps/services/board/topic_command_service.py
M byceps/services/image/service.py
M byceps/services/metrics/service.py
M byceps/services/news/image_service.py
M byceps/services/news/service.py
M byceps/services/newsletter/command_service.py
M byceps/services/orga/birthday_service.py
M byceps/services/party/service.py
M byceps/services/seating/seat_group_service.py
M byceps/services/shop/article/models/compilation.py
M byceps/services/shop/article/service.py
M byceps/services/shop/order/action_registry_service.py
M byceps/services/shop/order/action_service.py
M byceps/services/shop/order/actions/create_ticket_bundles.py
M byceps/services/shop/order/actions/create_tickets.py
M byceps/services/shop/order/email/service.py
M byceps/services/shop/order/service.py
M byceps/services/shop/sequence/service.py
M byceps/services/shop/shipping/service.py
M byceps/services/site/service.py
M byceps/services/snippet/service.py
M byceps/services/terms/document_service.py
M byceps/services/terms/version_service.py
M byceps/services/text_diff/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/ticket_bundle_service.py
M byceps/services/ticketing/ticket_creation_service.py
M byceps/services/ticketing/ticket_seat_management_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/service.py
M byceps/services/user/command_service.py
M byceps/services/user/creation_service.py
M byceps/services/user/email_address_confirmation_service.py
M byceps/services/user/models/user.py
M byceps/services/user/screen_name_validator.py
M byceps/services/user_avatar/service.py
M byceps/services/user_badge/service.py
M byceps/services/user_message/service.py
M byceps/util/export.py
M byceps/util/framework/blueprint.py
M byceps/util/framework/flash.py
M byceps/util/navigation.py
M byceps/util/templating.py
M scripts/add_archived_attendance.py
M scripts/award_badge_to_user.py
M scripts/copy_snippet.py
M scripts/create_initial_admin_user.py
M scripts/create_terms_version.py
M scripts/grant_board_access.py
M scripts/occupy_seat_group.py
M scripts/search_snippets.py
M scripts/set_current_terms_version.py
M byceps/application.py => byceps/application.py +10 -7
@@ 158,10 158,9 @@ def _add_static_file_url_rules(app):
        (config.STATIC_URL_PREFIX_SITE, 'site_file'),
    ]:
        rule = rule_prefix + '/<path:filename>'
        app.add_url_rule(rule,
                         endpoint=endpoint,
                         methods=['GET'],
                         build_only=True)
        app.add_url_rule(
            rule, endpoint=endpoint, methods=['GET'], build_only=True
        )


def init_app(app):


@@ 177,14 176,18 @@ def init_app(app):
            add_routes_for_snippets(site_id)

            # Incorporate template overrides for the configured site ID.
            app.template_folder = str(Path('..') / 'sites' / site_id / 'template_overrides')
            app.template_folder = str(
                Path('..') / 'sites' / site_id / 'template_overrides'
            )

            # Import site-specific code.
            _load_site_extension(app, site_id)
        elif site_mode.is_admin() and app.config['RQ_DASHBOARD_ENABLED']:
            import rq_dashboard
            app.register_blueprint(rq_dashboard.blueprint,
                                   url_prefix='/admin/rq')

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


def _set_url_root_path(app):

M byceps/blueprints/admin/board/views.py => byceps/blueprints/admin/board/views.py +25 -9
@@ 163,8 163,9 @@ def category_create(board_id):
    title = form.title.data.strip()
    description = form.description.data.strip()

    category = board_category_command_service \
        .create_category(board.id, slug, title, description)
    category = board_category_command_service.create_category(
        board.id, slug, title, description
    )

    flash_success('Die Kategorie "{}" wurde angelegt.', category.title)
    return redirect_to('.board_view', board_id=board.id)


@@ 204,8 205,9 @@ def category_update(category_id):
    title = form.title.data
    description = form.description.data

    category = board_category_command_service \
        .update_category(category.id, slug, title, description)
    category = board_category_command_service.update_category(
        category.id, slug, title, description
    )

    flash_success('Die Kategorie "{}" wurde aktualisiert.', category.title)
    return redirect_to('.board_view', board_id=category.board_id)


@@ 249,9 251,15 @@ def category_move_up(category_id):
    try:
        board_category_command_service.move_category_up(category.id)
    except ValueError:
        flash_error('Die Kategorie "{}" befindet sich bereits ganz oben.', category.title)
        flash_error(
            'Die Kategorie "{}" befindet sich bereits ganz oben.',
            category.title,
        )
    else:
        flash_success('Die Kategorie "{}" wurde eine Position nach oben verschoben.', category.title)
        flash_success(
            'Die Kategorie "{}" wurde eine Position nach oben verschoben.',
            category.title,
        )


@blueprint.route('/categories/<uuid:category_id>/down', methods=['POST'])


@@ 264,9 272,15 @@ def category_move_down(category_id):
    try:
        board_category_command_service.move_category_down(category.id)
    except ValueError:
        flash_error('Die Kategorie "{}" befindet sich bereits ganz unten.', category.title)
        flash_error(
            'Die Kategorie "{}" befindet sich bereits ganz unten.',
            category.title,
        )
    else:
        flash_success('Die Kategorie "{}" wurde eine Position nach unten verschoben.', category.title)
        flash_success(
            'Die Kategorie "{}" wurde eine Position nach unten verschoben.',
            category.title,
        )


@blueprint.route('/categories/<uuid:category_id>', methods=['DELETE'])


@@ 279,7 293,9 @@ def category_delete(category_id):
    try:
        board_category_command_service.delete_category(category.id)
    except:
        flash_error('Die Kategorie "{}" konnte nicht gelöscht werden.', category.title)
        flash_error(
            'Die Kategorie "{}" konnte nicht gelöscht werden.', category.title
        )
    else:
        flash_success('Die Kategorie "{}" wurde gelöscht.', category.title)


M byceps/blueprints/admin/consent/views.py => byceps/blueprints/admin/consent/views.py +3 -2
@@ 27,8 27,9 @@ permission_registry.register_enum(ConsentPermission)
@templated
def index():
    """List consent subjects."""
    subjects_with_consent_counts = subject_service \
        .get_subjects_with_consent_counts()
    subjects_with_consent_counts = (
        subject_service.get_subjects_with_consent_counts()
    )

    subjects_with_consent_counts = list(subjects_with_consent_counts.items())


M byceps/blueprints/admin/dashboard/views.py => byceps/blueprints/admin/dashboard/views.py +11 -6
@@ 60,7 60,9 @@ def view_global():
    user_count = user_stats_service.count_users()

    one_week_ago = timedelta(days=7)
    recent_users_count = user_stats_service.count_users_created_since(one_week_ago)
    recent_users_count = user_stats_service.count_users_created_since(
        one_week_ago
    )

    uninitialized_user_count = user_stats_service.count_uninitialized_users()



@@ 100,13 102,15 @@ def view_brand(brand_id):

    news_item_count = news_service.count_items_for_brand(brand.id)

    newsletter_list_id = brand_settings_service \
        .find_setting_value(brand.id, 'newsletter_list_id')
    newsletter_list_id = brand_settings_service.find_setting_value(
        brand.id, 'newsletter_list_id'
    )
    newsletter_list = None
    if newsletter_list_id:
        newsletter_list = newsletter_service.find_list(newsletter_list_id)
        newsletter_subscriber_count = newsletter_service \
            .count_subscribers_for_list(newsletter_list.id)
        newsletter_subscriber_count = newsletter_service.count_subscribers_for_list(
            newsletter_list.id
        )
    else:
        newsletter_subscriber_count = None



@@ 161,7 165,8 @@ def view_party(party_id):

    tickets_sold = ticket_service.count_tickets_for_party(party.id)
    tickets_checked_in = ticket_service.count_tickets_checked_in_for_party(
        party.id)
        party.id
    )

    return {
        'party': party,

M byceps/blueprints/admin/news/views.py => byceps/blueprints/admin/news/views.py +15 -9
@@ 87,7 87,9 @@ def channel_create(brand_id):
    channel_id = form.channel_id.data.strip().lower()
    url_prefix = form.url_prefix.data.strip()

    channel = news_channel_service.create_channel(brand.id, channel_id, url_prefix)
    channel = news_channel_service.create_channel(
        brand.id, channel_id, url_prefix
    )

    flash_success('Der News-Kanal mit der ID "{}" wurde angelegt.', channel.id)
    return redirect_to('.channel_view', channel_id=channel.id)


@@ 173,8 175,9 @@ def item_compare_versions(from_version_id, to_version_id):

    html_diff_title = _create_html_diff(from_version, to_version, 'title')
    html_diff_body = _create_html_diff(from_version, to_version, 'body')
    html_diff_image_url_path = _create_html_diff(from_version, to_version,
                                                 'image_url_path')
    html_diff_image_url_path = _create_html_diff(
        from_version, to_version, 'image_url_path'
    )

    return {
        'brand': brand,


@@ 219,8 222,9 @@ def item_create(channel_id):
    body = form.body.data.strip()
    image_url_path = form.image_url_path.data.strip()

    item = news_item_service.create_item(channel.id, slug, creator.id, title,
                                         body, image_url_path=image_url_path)
    item = news_item_service.create_item(
        channel.id, slug, creator.id, title, body, image_url_path=image_url_path
    )

    flash_success('Die News "{}" wurde angelegt.', item.title)



@@ 266,8 270,9 @@ def item_update(item_id):
    body = form.body.data.strip()
    image_url_path = form.image_url_path.data.strip()

    news_item_service.update_item(item.id, slug, creator.id, title, body,
                                  image_url_path=image_url_path)
    news_item_service.update_item(
        item.id, slug, creator.id, title, body, image_url_path=image_url_path
    )

    flash_success('Die News "{}" wurde aktualisiert.', item.title)
    return redirect_to('.channel_view', channel_id=item.channel.id)


@@ 336,5 341,6 @@ def _create_html_diff(from_version, to_version, attribute_name):
    from_text = getattr(from_version, attribute_name)
    to_text = getattr(to_version, attribute_name)

    return text_diff_service.create_html_diff(from_text, to_text,
                                              from_description, to_description)
    return text_diff_service.create_html_diff(
        from_text, to_text, from_description, to_description
    )

M byceps/blueprints/admin/orga/views.py => byceps/blueprints/admin/orga/views.py +12 -5
@@ 93,8 93,11 @@ def create_orgaflag(brand_id):

    orga_flag = orga_service.add_orga_flag(brand.id, user.id, initiator.id)

    flash_success('{} wurde das Orga-Flag für die Marke {} gegeben.',
                  orga_flag.user.screen_name, orga_flag.brand.title)
    flash_success(
        '{} wurde das Orga-Flag für die Marke {} gegeben.',
        orga_flag.user.screen_name,
        orga_flag.brand.title,
    )
    return redirect_to('.persons_for_brand', brand_id=orga_flag.brand.id)




@@ 114,8 117,11 @@ def remove_orgaflag(brand_id, user_id):

    orga_service.remove_orga_flag(orga_flag, initiator.id)

    flash_success('{} wurde das Orga-Flag für die Marke {} entzogen.',
                  user.screen_name, brand.title)
    flash_success(
        '{} wurde das Orga-Flag für die Marke {} entzogen.',
        user.screen_name,
        brand.title,
    )


@blueprint.route('/persons/<brand_id>/export')


@@ 168,7 174,8 @@ def export_persons(brand_id):
@templated
def birthdays():
    orgas = list(
        orga_birthday_service.collect_orgas_with_next_birthdays(limit=5))
        orga_birthday_service.collect_orgas_with_next_birthdays(limit=5)
    )

    return {
        'orgas': orgas,

M byceps/blueprints/admin/orga_presence/views.py => byceps/blueprints/admin/orga_presence/views.py +2 -1
@@ 55,7 55,8 @@ def view(party_id):
    hour_ranges = list(orga_presence_service.get_hour_ranges(time_slots))

    days_and_hour_totals = list(
        orga_presence_service.get_days_and_hour_totals(hour_ranges))
        orga_presence_service.get_days_and_hour_totals(hour_ranges)
    )

    return {
        'party': party,

M byceps/blueprints/admin/orga_team/views.py => byceps/blueprints/admin/orga_team/views.py +27 -14
@@ 77,8 77,11 @@ def team_create(party_id):

    team = orga_team_service.create_team(party.id, title)

    flash_success('Das Team "{}" wurde für die Party "{}" erstellt.',
                  team.title, team.party.title)
    flash_success(
        'Das Team "{}" wurde für die Party "{}" erstellt.',
        team.title,
        team.party.title,
    )
    return redirect_to('.teams_for_party', party_id=party.id)




@@ 107,7 110,8 @@ def membership_create_form(team_id, erroneous_form=None):
    team = _get_team_or_404(team_id)

    unassigned_orgas = orga_team_service.get_unassigned_orgas_for_party(
        team.party_id)
        team.party_id
    )

    if not unassigned_orgas:
        return {


@@ 132,7 136,8 @@ def membership_create(team_id):
    team = _get_team_or_404(team_id)

    unassigned_orgas = orga_team_service.get_unassigned_orgas_for_party(
        team.party_id)
        team.party_id
    )

    form = MembershipCreateForm(request.form)
    form.set_user_choices(unassigned_orgas)


@@ 145,10 150,14 @@ def membership_create(team_id):

    membership = orga_team_service.create_membership(team.id, user.id, duties)

    flash_success('{} wurde in das Team "{}" aufgenommen.',
                  membership.user.screen_name, membership.orga_team.title)
    return redirect_to('.teams_for_party',
                       party_id=membership.orga_team.party_id)
    flash_success(
        '{} wurde in das Team "{}" aufgenommen.',
        membership.user.screen_name,
        membership.orga_team.title,
    )
    return redirect_to(
        '.teams_for_party', party_id=membership.orga_team.party_id
    )


@blueprint.route('/memberships/<uuid:membership_id>/update')


@@ 190,10 199,13 @@ def membership_update(membership_id):

    orga_team_service.update_membership(membership, team, duties)

    flash_success('Die Teammitgliedschaft von {} wurde aktualisiert.',
                  membership.user.screen_name)
    return redirect_to('.teams_for_party',
                       party_id=membership.orga_team.party_id)
    flash_success(
        'Die Teammitgliedschaft von {} wurde aktualisiert.',
        membership.user.screen_name,
    )
    return redirect_to(
        '.teams_for_party', party_id=membership.orga_team.party_id
    )


@blueprint.route('/memberships/<uuid:membership_id>', methods=['DELETE'])


@@ 208,8 220,9 @@ def membership_remove(membership_id):

    orga_team_service.delete_membership(membership)

    flash_success('{} wurde aus dem Team "{}" entfernt.',
                  user.screen_name, team.title)
    flash_success(
        '{} wurde aus dem Team "{}" entfernt.', user.screen_name, team.title
    )


def _get_party_or_404(party_id):

M byceps/blueprints/admin/party/views.py => byceps/blueprints/admin/party/views.py +25 -11
@@ 56,8 56,9 @@ def index_for_brand(brand_id, page):
    brand = _get_brand_or_404(brand_id)

    per_page = request.args.get('per_page', type=int, default=15)
    parties = party_service.get_parties_for_brand_paginated(brand.id, page,
                                                            per_page)
    parties = party_service.get_parties_for_brand_paginated(
        brand.id, page, per_page
    )

    shops_by_party_id = _get_shops_by_party_id(parties.items)



@@ 136,10 137,15 @@ def create(brand_id):
    if not shop_id:
        shop_id = None

    party = party_service.create_party(party_id, brand.id, title, starts_at,
                                       ends_at,
                                       max_ticket_quantity=max_ticket_quantity,
                                       shop_id=shop_id)
    party = party_service.create_party(
        party_id,
        brand.id,
        title,
        starts_at,
        ends_at,
        max_ticket_quantity=max_ticket_quantity,
        shop_id=shop_id,
    )

    flash_success('Die Party "{}" wurde angelegt.', party.title)
    return redirect_to('.index_for_brand', brand_id=brand.id)


@@ 153,9 159,11 @@ def update_form(party_id, erroneous_form=None):
    party = _get_party_or_404(party_id)
    brand = brand_service.find_brand(party.brand_id)

    party = attr.evolve(party,
    party = attr.evolve(
        party,
        starts_at=utc_to_local_tz(party.starts_at),
        ends_at=utc_to_local_tz(party.ends_at))
        ends_at=utc_to_local_tz(party.ends_at),
    )

    form = erroneous_form if erroneous_form else UpdateForm(obj=party)



@@ 186,9 194,15 @@ def update(party_id):
    archived = form.archived.data

    try:
        party = party_service.update_party(party.id, title, starts_at, ends_at,
                                           max_ticket_quantity, shop_id,
                                           archived)
        party = party_service.update_party(
            party.id,
            title,
            starts_at,
            ends_at,
            max_ticket_quantity,
            shop_id,
            archived,
        )
    except party_service.UnknownPartyId:
        abort(404, 'Unknown party ID "{}".'.format(party_id))


M byceps/blueprints/admin/seating/views.py => byceps/blueprints/admin/seating/views.py +10 -5
@@ 42,7 42,8 @@ def index_for_party(party_id):
    seat_count = seat_service.count_seats_for_party(party.id)
    area_count = seating_area_service.count_areas_for_party(party.id)
    category_count = ticketing_category_service.count_categories_for_party(
        party.id)
        party.id
    )
    group_count = seat_group_service.count_seat_groups_for_party(party.id)

    return {


@@ 63,8 64,9 @@ def area_index(party_id, page):
    party = _get_party_or_404(party_id)

    per_page = request.args.get('per_page', type=int, default=15)
    areas_with_occupied_seat_counts = seating_area_service \
        .get_areas_for_party_paginated(party.id, page, per_page)
    areas_with_occupied_seat_counts = seating_area_service.get_areas_for_party_paginated(
        party.id, page, per_page
    )

    seat_total_per_area = seat_service.get_seat_total_per_area(party.id)



@@ 82,8 84,11 @@ def seat_category_index(party_id):
    """List seat categories for that party."""
    party = _get_party_or_404(party_id)

    categories_with_ticket_counts = list(ticketing_category_service \
        .get_categories_with_ticket_counts_for_party(party.id).items())
    categories_with_ticket_counts = list(
        ticketing_category_service.get_categories_with_ticket_counts_for_party(
            party.id
        ).items()
    )

    return {
        'party': party,

M byceps/blueprints/admin/shop/article/forms.py => byceps/blueprints/admin/shop/article/forms.py +2 -1
@@ 42,7 42,8 @@ class ArticleUpdateForm(ArticleCreateForm):
        if (begin is not None) and (begin >= end):
            raise ValidationError(
                'Das Ende des Verfügbarkeitszeitraums muss nach dessen Beginn '
                'liegen.')
                'liegen.'
            )


class ArticleAttachmentCreateForm(LocalizedForm):

M byceps/blueprints/admin/shop/article/views.py => byceps/blueprints/admin/shop/article/views.py +40 -20
@@ 51,13 51,15 @@ def index_for_shop(shop_id, page):
    """List articles for that shop."""
    shop = _get_shop_or_404(shop_id)

    article_number_sequence = sequence_service \
        .find_article_number_sequence(shop.id)
    article_number_sequence = sequence_service.find_article_number_sequence(
        shop.id
    )
    article_number_prefix = article_number_sequence.prefix

    per_page = request.args.get('per_page', type=int, default=15)
    articles = article_service.get_articles_for_shop_paginated(shop.id, page,
                                                               per_page)
    articles = article_service.get_articles_for_shop_paginated(
        shop.id, page, per_page
    )

    return {
        'shop': shop,


@@ 77,8 79,9 @@ def view(article_id):

    shop = shop_service.get_shop(article.shop_id)

    totals = ordered_articles_service \
        .count_ordered_articles(article.item_number)
    totals = ordered_articles_service.count_ordered_articles(
        article.item_number
    )

    return {
        'article': article,


@@ 99,8 102,9 @@ def view_ordered(article_id):

    shop = shop_service.get_shop(article.shop_id)

    order_items = ordered_articles_service \
        .get_order_items_for_article(article.item_number)
    order_items = ordered_articles_service.get_order_items_for_article(
        article.item_number
    )

    quantity_total = sum(item.quantity for item in order_items)



@@ 137,8 141,9 @@ def create_form(shop_id, erroneous_form=None):
    """Show form to create an article."""
    shop = _get_shop_or_404(shop_id)

    article_number_sequence = sequence_service \
        .find_article_number_sequence(shop.id)
    article_number_sequence = sequence_service.find_article_number_sequence(
        shop.id
    )
    article_number_prefix = article_number_sequence.prefix

    form = erroneous_form if erroneous_form else ArticleCreateForm(


@@ 173,8 178,9 @@ def create(shop_id):
    tax_rate = form.tax_rate.data
    quantity = form.quantity.data

    article = article_service.create_article(shop.id, item_number, description,
                                             price, tax_rate, quantity)
    article = article_service.create_article(
        shop.id, item_number, description, price, tax_rate, quantity
    )

    flash_success('Der Artikel "{}" wurde angelegt.', article.item_number)
    return redirect_to('.view', article_id=article.id)


@@ 229,11 235,19 @@ def update(article_id):
    if available_until:
        available_until = local_tz_to_utc(available_until)

    article_service.update_article(article, description, price, tax_rate,
                                   available_from, available_until, quantity,
                                   max_quantity_per_order,
                                   not_directly_orderable,
                                   requires_separate_order, shipping_required)
    article_service.update_article(
        article,
        description,
        price,
        tax_rate,
        available_from,
        available_until,
        quantity,
        max_quantity_per_order,
        not_directly_orderable,
        requires_separate_order,
        shipping_required,
    )

    flash_success('Der Artikel "{}" wurde aktualisiert.', article.description)
    return redirect_to('.view', article_id=article.id)


@@ 283,7 297,10 @@ def attachment_create(article_id):

    flash_success(
        'Der Artikel "{}" wurde {:d} mal an den Artikel "{}" angehängt.',
        article_to_attach.item_number, quantity, article.item_number)
        article_to_attach.item_number,
        quantity,
        article.item_number,
    )
    return redirect_to('.view', article_id=article.id)




@@ 302,8 319,11 @@ def attachment_remove(article_id):

    article_service.unattach_article(attached_article)

    flash_success('Artikel "{}" ist nun nicht mehr an Artikel "{}" angehängt.',
                  article.item_number, attached_to_article.item_number)
    flash_success(
        'Artikel "{}" ist nun nicht mehr an Artikel "{}" angehängt.',
        article.item_number,
        attached_to_article.item_number,
    )


def _get_shop_or_404(shop_id):

M byceps/blueprints/admin/shop/email/views.py => byceps/blueprints/admin/shop/email/views.py +16 -9
@@ 61,8 61,9 @@ def view_example_order_placed(shop_id):

    data = _build_email_data(order, shop)

    message = shop_order_email_service \
        ._assemble_email_for_incoming_order_to_orderer(data)
    message = shop_order_email_service._assemble_email_for_incoming_order_to_orderer(
        data
    )

    yield from _render_message(message)



@@ 78,8 79,9 @@ def view_example_order_paid(shop_id):

    data = _build_email_data(order, shop)

    message = shop_order_email_service \
        ._assemble_email_for_paid_order_to_orderer(data)
    message = shop_order_email_service._assemble_email_for_paid_order_to_orderer(
        data
    )

    yield from _render_message(message)



@@ 91,14 93,18 @@ def view_example_order_canceled(shop_id):
    """Show example of order canceled e-mail."""
    shop = _get_shop_or_404(shop_id)

    order = _build_order(shop.id, PaymentState.canceled_before_paid,
    order = _build_order(
        shop.id,
        PaymentState.canceled_before_paid,
        is_canceled=True,
        cancelation_reason='Kein fristgerechter Geldeingang feststellbar')
        cancelation_reason='Kein fristgerechter Geldeingang feststellbar',
    )

    data = _build_email_data(order, shop)

    message = shop_order_email_service \
        ._assemble_email_for_canceled_order_to_orderer(data)
    message = shop_order_email_service._assemble_email_for_canceled_order_to_orderer(
        data
    )

    yield from _render_message(message)



@@ 172,7 178,8 @@ def _build_email_data(order, shop):
def _render_message(message):
    if not message.sender:
        raise ConfigurationError(
            'No e-mail sender address configured for message.')
            'No e-mail sender address configured for message.'
        )

    yield f'From: {message.sender}\n'
    yield f'To: {message.recipients}\n'

M byceps/blueprints/admin/shop/order/service.py => byceps/blueprints/admin/shop/order/service.py +3 -1
@@ 98,7 98,9 @@ def _get_additional_data(
    elif event.event_type == 'ticket-bundle-created':
        return _get_additional_data_for_ticket_bundle_created(event)
    elif event.event_type == 'ticket-bundle-revoked':
        return _get_additional_data_for_ticket_bundle_revoked(event, users_by_id)
        return _get_additional_data_for_ticket_bundle_revoked(
            event, users_by_id
        )
    elif event.event_type == 'ticket-created':
        return _get_additional_data_for_ticket_created(event)
    elif event.event_type == 'ticket-revoked':

M byceps/blueprints/admin/shop/order/views.py => byceps/blueprints/admin/shop/order/views.py +35 -21
@@ 52,8 52,9 @@ def index_for_shop(shop_id, page):

    search_term = request.args.get('search_term', default='').strip()

    only_payment_state = request.args.get('only_payment_state',
                                          type=PaymentState.__members__.get)
    only_payment_state = request.args.get(
        'only_payment_state', type=PaymentState.__members__.get
    )

    def _str_to_bool(value):
        valid_values = {


@@ 66,11 67,14 @@ def index_for_shop(shop_id, page):

    order_state_filter = OrderStateFilter.find(only_payment_state, only_shipped)

    orders = order_service \
        .get_orders_for_shop_paginated(shop.id, page, per_page,
                                       search_term=search_term,
                                       only_payment_state=only_payment_state,
                                       only_shipped=only_shipped)
    orders = order_service.get_orders_for_shop_paginated(
        shop.id,
        page,
        per_page,
        search_term=search_term,
        only_payment_state=only_payment_state,
        only_shipped=only_shipped,
    )

    # Replace order objects in pagination object with order tuples.
    orders.items = [order.to_transfer_object() for order in orders.items]


@@ 130,8 134,9 @@ def export(order_id):
    if xml_export is None:
        abort(404)

    return Response(xml_export['content'],
                    content_type=xml_export['content_type'])
    return Response(
        xml_export['content'], content_type=xml_export['content_type']
    )


@blueprint.route('/<uuid:order_id>/flags/invoiced', methods=['POST'])


@@ 146,7 151,8 @@ def set_invoiced_flag(order_id):

    flash_success(
        'Bestellung {} wurde als in Rechnung gestellt markiert.',
        order.order_number)
        order.order_number,
    )


@blueprint.route('/<uuid:order_id>/flags/invoiced', methods=['DELETE'])


@@ 161,7 167,8 @@ def unset_invoiced_flag(order_id):

    flash_success(
        'Bestellung {} wurde als nicht in Rechnung gestellt markiert.',
        order.order_number)
        order.order_number,
    )


@blueprint.route('/<uuid:order_id>/flags/shipped', methods=['POST'])


@@ 174,8 181,9 @@ def set_shipped_flag(order_id):

    order_service.set_shipped_flag(order, initiator_id)

    flash_success('Bestellung {} wurde als verschickt markiert.',
                  order.order_number)
    flash_success(
        'Bestellung {} wurde als verschickt markiert.', order.order_number
    )


@blueprint.route('/<uuid:order_id>/flags/shipped', methods=['DELETE'])


@@ 188,8 196,9 @@ def unset_shipped_flag(order_id):

    order_service.unset_shipped_flag(order, initiator_id)

    flash_success('Bestellung {} wurde als nicht verschickt markiert.',
                  order.order_number)
    flash_success(
        'Bestellung {} wurde als nicht verschickt markiert.', order.order_number
    )


@blueprint.route('/<uuid:order_id>/cancel')


@@ 202,7 211,8 @@ def cancel_form(order_id, erroneous_form=None):
    if order.is_canceled:
        flash_error(
            'Die Bestellung ist bereits storniert worden; '
            'der Bezahlstatus kann nicht mehr geändert werden.')
            'der Bezahlstatus kann nicht mehr geändert werden.'
        )
        return redirect_to('.view', order_id=order.id)

    shop = shop_service.get_shop(order.shop_id)


@@ 236,19 246,22 @@ def cancel(order_id):
    except order_service.OrderAlreadyCanceled:
        flash_error(
            'Die Bestellung ist bereits storniert worden; '
            'der Bezahlstatus kann nicht mehr geändert werden.')
            'der Bezahlstatus kann nicht mehr geändert werden.'
        )
        return redirect_to('.view', order_id=order.id)

    flash_success(
        'Die Bestellung wurde als storniert markiert und die betroffenen '
        'Artikel in den entsprechenden Stückzahlen wieder zur Bestellung '
        'freigegeben.')
        'freigegeben.'
    )

    if send_email:
        order_email_service.send_email_for_canceled_order_to_orderer(order.id)
    else:
        flash_notice(
            'Es wurde keine E-Mail an den/die Auftraggeber/in versendet.')
            'Es wurde keine E-Mail an den/die Auftraggeber/in versendet.'
        )

    order_canceled.send(None, order_id=order.id)



@@ 291,8 304,9 @@ def mark_as_paid(order_id):
    updated_by_id = g.current_user.id

    try:
        order_service.mark_order_as_paid(order.id, payment_method,
                                         updated_by_id)
        order_service.mark_order_as_paid(
            order.id, payment_method, updated_by_id
        )
    except order_service.OrderAlreadyMarkedAsPaid:
        flash_error('Die Bestellung ist bereits als bezahlt markiert worden.')
        return redirect_to('.view', order_id=order.id)

M byceps/blueprints/admin/shop/shop/views.py => byceps/blueprints/admin/shop/shop/views.py +6 -4
@@ 75,11 75,13 @@ def view_for_shop(shop_id):
    most_recent_order_number = _get_most_recent_order_number(shop.id)

    article_count = article_service.count_articles_for_shop(shop.id)
    order_counts_by_payment_state = order_service \
        .count_orders_per_payment_state(shop.id)
    order_counts_by_payment_state = order_service.count_orders_per_payment_state(
        shop.id
    )

    order_actions_by_article_number = \
        _get_order_actions_by_article_number(shop.id)
    order_actions_by_article_number = _get_order_actions_by_article_number(
        shop.id
    )

    return {
        'shop': shop,

M byceps/blueprints/admin/site/views.py => byceps/blueprints/admin/site/views.py +6 -4
@@ 123,8 123,9 @@ def create():
    email_config_id = form.email_config_id.data.strip()
    party_id = form.party_id.data.strip()

    site = site_service.create_site(site_id, title, server_name,
                                    email_config_id, party_id=party_id)
    site = site_service.create_site(
        site_id, title, server_name, email_config_id, party_id=party_id
    )

    flash_success('Die Site "{}" wurde angelegt.', site.title)
    return redirect_to('.view', site_id=site.id)


@@ 163,8 164,9 @@ def update(site_id):
    party_id = form.party_id.data.strip()

    try:
        site = site_service.update_site(site.id, title, server_name,
                                        email_config_id, party_id=party_id)
        site = site_service.update_site(
            site.id, title, server_name, email_config_id, party_id=party_id
        )
    except site_service.UnknownSiteId:
        abort(404, 'Unknown site ID "{}".'.format(site_id))


M byceps/blueprints/admin/snippet/views.py => byceps/blueprints/admin/snippet/views.py +34 -22
@@ 48,8 48,9 @@ def index_for_scope(scope_type, scope_name):
    """List snippets for that scope."""
    scope = Scope(scope_type, scope_name)

    snippets = snippet_service \
        .get_snippets_for_scope_with_current_versions(scope)
    snippets = snippet_service.get_snippets_for_scope_with_current_versions(
        scope
    )

    brand = _find_brand_for_scope(scope)
    site = _find_site_for_scope(scope)


@@ 170,9 171,15 @@ def create_document(scope_type, scope_name):
    body = form.body.data.strip()
    image_url_path = form.image_url_path.data.strip()

    version = snippet_service.create_document(scope, name, creator.id, title,
                                              body, head=head,
                                              image_url_path=image_url_path)
    version = snippet_service.create_document(
        scope,
        name,
        creator.id,
        title,
        body,
        head=head,
        image_url_path=image_url_path,
    )

    flash_success('Das Dokument "{}" wurde angelegt.', version.snippet.name)
    signals.snippet_created.send(None, snippet_version_id=version.id)


@@ 190,9 197,7 @@ def update_document_form(snippet_id):

    scope = snippet.scope

    form = DocumentUpdateForm(
        obj=current_version,
        name=snippet.name)
    form = DocumentUpdateForm(obj=current_version, name=snippet.name)

    brand = _find_brand_for_scope(scope)
    site = _find_site_for_scope(scope)


@@ 220,9 225,14 @@ def update_document(snippet_id):
    body = form.body.data.strip()
    image_url_path = form.image_url_path.data.strip()

    version = snippet_service.update_document(snippet, creator.id, title, body,
                                              head=head,
                                              image_url_path=image_url_path)
    version = snippet_service.update_document(
        snippet,
        creator.id,
        title,
        body,
        head=head,
        image_url_path=image_url_path,
    )

    flash_success('Das Dokument "{}" wurde aktualisiert.', version.snippet.name)
    signals.snippet_updated.send(None, snippet_version_id=version.id)


@@ 248,8 258,9 @@ def compare_documents(from_version_id, to_version_id):
    html_diff_title = _create_html_diff(from_version, to_version, 'title')
    html_diff_head = _create_html_diff(from_version, to_version, 'head')
    html_diff_body = _create_html_diff(from_version, to_version, 'body')
    html_diff_image_url_path = _create_html_diff(from_version, to_version,
                                                 'image_url_path')
    html_diff_image_url_path = _create_html_diff(
        from_version, to_version, 'image_url_path'
    )

    brand = _find_brand_for_scope(scope)
    site = _find_site_for_scope(scope)


@@ 321,9 332,7 @@ def update_fragment_form(snippet_id):

    scope = snippet.scope

    form = FragmentUpdateForm(
        obj=current_version,
        name=snippet.name)
    form = FragmentUpdateForm(obj=current_version, name=snippet.name)

    brand = _find_brand_for_scope(scope)
    site = _find_site_for_scope(scope)


@@ 443,11 452,13 @@ def create_mountpoint(snippet_id):
    endpoint_suffix = form.endpoint_suffix.data.strip()
    url_path = form.url_path.data.strip()

    mountpoint = mountpoint_service \
        .create_mountpoint(site_id, endpoint_suffix, url_path, snippet.id)
    mountpoint = mountpoint_service.create_mountpoint(
        site_id, endpoint_suffix, url_path, snippet.id
    )

    flash_success('Der Mountpoint für "{}" wurde angelegt.',
                  mountpoint.url_path)
    flash_success(
        'Der Mountpoint für "{}" wurde angelegt.', mountpoint.url_path
    )
    return redirect_to('.index_mountpoints', site_id=site_id)




@@ 500,8 511,9 @@ def _create_html_diff(from_version, to_version, attribute_name):
    from_text = getattr(from_version, attribute_name)
    to_text = getattr(to_version, attribute_name)

    return text_diff_service.create_html_diff(from_text, to_text,
                                              from_description, to_description)
    return text_diff_service.create_html_diff(
        from_text, to_text, from_description, to_description
    )


def _find_brand_for_scope(scope):

M byceps/blueprints/admin/terms/views.py => byceps/blueprints/admin/terms/views.py +3 -2
@@ 40,8 40,9 @@ def view_document(document_id):

    _add_version_creators(versions)

    consent_counts_by_version_id = terms_consent_service \
        .count_consents_for_document_versions(document.id)
    consent_counts_by_version_id = terms_consent_service.count_consents_for_document_versions(
        document.id
    )

    for version in versions:
        version.consent_count = consent_counts_by_version_id[version.id]

M byceps/blueprints/admin/ticketing/checkin/views.py => byceps/blueprints/admin/ticketing/checkin/views.py +11 -6
@@ 76,7 76,8 @@ def _search_tickets(party_id, search_term, limit):
    per_page = limit

    tickets_pagination = ticket_service.get_tickets_with_details_for_party_paginated(
        party_id, page, per_page, search_term=search_term)
        party_id, page, per_page, search_term=search_term
    )

    return tickets_pagination.items



@@ 91,13 92,15 @@ def _search_orders(shop_id, search_term, limit):
    per_page = limit

    orders_pagination = order_service.get_orders_for_shop_paginated(
        shop.id, page, per_page, search_term=search_term)
        shop.id, page, per_page, search_term=search_term
    )

    # Replace order objects with order tuples.
    orders = [order.to_transfer_object() for order in orders_pagination.items]

    orders = list(order_blueprint_service.extend_order_tuples_with_orderer(
        orders))
    orders = list(
        order_blueprint_service.extend_order_tuples_with_orderer(orders)
    )

    return orders



@@ 107,7 110,8 @@ def _search_users(party_id, search_term, limit):
    per_page = limit

    users_pagination = user_blueprint_service.get_users_paginated(
        page, per_page, search_term=search_term)
        page, per_page, search_term=search_term
    )

    # Exclude deleted users.
    users_pagination.items = [user for user in users_pagination.items


@@ 119,4 123,5 @@ def _search_users(party_id, search_term, limit):
def _get_tickets_for_users(party_id, users):
    for user in users:
        yield from ticket_service.find_tickets_used_by_user_simplified(
            user.id, party_id)
            user.id, party_id
        )

M byceps/blueprints/admin/ticketing/service.py => byceps/blueprints/admin/ticketing/service.py +21 -10
@@ 82,12 82,17 @@ def _get_additional_data(
            'user-manager-withdrawn',
            'user-withdrawn',
    }:
        yield from _get_additional_data_for_user_initiated_event(event,
                                                                 users_by_id)
        yield from _get_additional_data_for_user_initiated_event(
            event, users_by_id
        )

    if event.event_type == 'seat-manager-appointed':
        yield _look_up_user_for_id(event, users_by_id,
            'appointed_seat_manager_id', 'appointed_seat_manager')
        yield _look_up_user_for_id(
            event,
            users_by_id,
            'appointed_seat_manager_id',
            'appointed_seat_manager',
        )

    if event.event_type == 'seat-occupied':
        yield from _get_additional_data_for_seat_occupied_event(event)


@@ 99,16 104,22 @@ def _get_additional_data(
        yield from _get_additional_data_for_ticket_revoked_event(event)

    if event.event_type == 'user-appointed':
        yield _look_up_user_for_id(event, users_by_id,
            'appointed_user_id', 'appointed_user')
        yield _look_up_user_for_id(
            event, users_by_id, 'appointed_user_id', 'appointed_user'
        )

    if event.event_type in {'user-checked-in', 'user-check-in-reverted'}:
        yield _look_up_user_for_id(event, users_by_id,
            'checked_in_user_id', 'checked_in_user')
        yield _look_up_user_for_id(
            event, users_by_id, 'checked_in_user_id', 'checked_in_user'
        )

    if event.event_type == 'user-manager-appointed':
        yield _look_up_user_for_id(event, users_by_id,
            'appointed_user_manager_id', 'appointed_user_manager')
        yield _look_up_user_for_id(
            event,
            users_by_id,
            'appointed_user_manager_id',
            'appointed_user_manager',
        )


def _get_additional_data_for_user_initiated_event(

M byceps/blueprints/admin/ticketing/views.py => byceps/blueprints/admin/ticketing/views.py +14 -6
@@ 51,7 51,8 @@ def index_for_party(party_id, page):
    search_term = request.args.get('search_term', default='').strip()

    tickets = ticket_service.get_tickets_with_details_for_party_paginated(
        party.id, page, per_page, search_term=search_term)
        party.id, page, per_page, search_term=search_term
    )

    return {
        'party': party,


@@ 115,8 116,11 @@ def appoint_user(ticket_id):

    ticket_user_management_service.appoint_user(ticket.id, user.id, manager.id)

    flash_success('{} wurde als Nutzer/in von Ticket {} eingetragen.',
        user.screen_name, ticket.code)
    flash_success(
        '{} wurde als Nutzer/in von Ticket {} eingetragen.',
        user.screen_name,
        ticket.code,
    )

    return redirect(url_for('.view_ticket', ticket_id=ticket.id))



@@ 137,15 141,19 @@ def set_user_checked_in_flag(ticket_id):
    except ticket_exceptions.UserAccountDeleted:
        flash_error(
            'Das dem Ticket zugewiesene Benutzerkonto ist gelöscht worden. '
            'Der Check-In ist nicht erlaubt.')
            'Der Check-In ist nicht erlaubt.'
        )
        return
    except ticket_exceptions.UserAccountSuspended:
        flash_error(
            'Das dem Ticket zugewiesene Benutzerkonto ist gesperrt. '
            'Der Check-In ist nicht erlaubt.')
            'Der Check-In ist nicht erlaubt.'
        )
        return

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

    occupies_seat = (ticket.occupied_seat_id is not None)
    if not occupies_seat:

M byceps/blueprints/admin/tourney/views.py => byceps/blueprints/admin/tourney/views.py +16 -4
@@ 118,9 118,15 @@ def category_move_up(category_id):
    try:
        category_service.move_category_up(category)
    except ValueError:
        flash_error('Die Kategorie "{}" befindet sich bereits ganz oben.', category.title)
        flash_error(
            'Die Kategorie "{}" befindet sich bereits ganz oben.',
            category.title,
        )
    else:
        flash_success('Die Kategorie "{}" wurde eine Position nach oben verschoben.', category.title)
        flash_success(
            'Die Kategorie "{}" wurde eine Position nach oben verschoben.',
            category.title,
        )


@blueprint.route('/categories/<uuid:category_id>/down', methods=['POST'])


@@ 133,9 139,15 @@ def category_move_down(category_id):
    try:
        category_service.move_category_down(category)
    except ValueError:
        flash_error('Die Kategorie "{}" befindet sich bereits ganz unten.', category.title)
        flash_error(
            'Die Kategorie "{}" befindet sich bereits ganz unten.',
            category.title,
        )
    else:
        flash_success('Die Kategorie "{}" wurde eine Position nach unten verschoben.', category.title)
        flash_success(
            'Die Kategorie "{}" wurde eine Position nach unten verschoben.',
            category.title,
        )


def _get_party_or_404(party_id):

M byceps/blueprints/admin/user/service.py => byceps/blueprints/admin/user/service.py +3 -2
@@ 185,8 185,9 @@ def _fake_consent_events(user_id: UserID) -> Iterator[UserEvent]:
            'subject_title': consent.subject.title,
        }

        yield UserEvent(consent.expressed_at, 'consent-expressed', user_id,
                        data)
        yield UserEvent(
            consent.expressed_at, 'consent-expressed', user_id, data
        )


def _fake_newsletter_subscription_update_events(

M byceps/blueprints/admin/user/views.py => byceps/blueprints/admin/user/views.py +83 -48
@@ 63,12 63,13 @@ def index(page):
    search_term = request.args.get('search_term', default='').strip()
    only = request.args.get('only')

    user_state_filter = UserStateFilter.__members__.get(only,
                                                        UserStateFilter.none)
    user_state_filter = UserStateFilter.__members__.get(
        only, UserStateFilter.none
    )

    users = service.get_users_paginated(page, per_page,
                                        search_term=search_term,
                                        state_filter=user_state_filter)
    users = service.get_users_paginated(
        page, per_page, search_term=search_term, state_filter=user_state_filter
    )

    total_active = user_stats_service.count_active_users()
    total_uninitialized = user_stats_service.count_uninitialized_users()


@@ 154,21 155,30 @@ def create_account():

    if user_service.is_screen_name_already_assigned(screen_name):
        flash_error(
            'Dieser Benutzername ist bereits einem Benutzerkonto zugeordnet.')
            'Dieser Benutzername ist bereits einem Benutzerkonto zugeordnet.'
        )
        return create_account_form(form)

    if user_service.is_email_address_already_assigned(email_address):
        flash_error(
            'Diese E-Mail-Adresse ist bereits einem Benutzerkonto zugeordnet.')
            'Diese E-Mail-Adresse ist bereits einem Benutzerkonto zugeordnet.'
        )
        return create_account_form(form)

    try:
        user = user_creation_service.create_basic_user(
            screen_name, email_address, password, first_names=first_names,
            last_name=last_name, creator_id=g.current_user.id)
            screen_name,
            email_address,
            password,
            first_names=first_names,
            last_name=last_name,
            creator_id=g.current_user.id,
        )
    except user_creation_service.UserCreationFailed:
        flash_error('Das Benutzerkonto für "{}" konnte nicht angelegt werden.',
                    screen_name)
        flash_error(
            'Das Benutzerkonto für "{}" konnte nicht angelegt werden.',
            screen_name,
        )
        return create_account_form(form)

    flash_success('Das Benutzerkonto "{}" wurde angelegt.', user.screen_name)


@@ 207,8 217,10 @@ def set_password(user_id):

    password_service.update_password_hash(user.id, new_password, initiator_id)

    flash_success("Für Benutzerkonto '{}' wurde ein neues Passwort gesetzt.",
                  user.screen_name)
    flash_success(
        "Für Benutzerkonto '{}' wurde ein neues Passwort gesetzt.",
        user.screen_name,
    )

    return redirect(url_for('.view', user_id=user.id))



@@ 224,7 236,9 @@ def initialize_account(user_id):

    user_command_service.initialize_account(user.id, initiator_id)

    flash_success("Das Benutzerkonto '{}' wurde initialisiert.", user.screen_name)
    flash_success(
        "Das Benutzerkonto '{}' wurde initialisiert.", user.screen_name
    )


@blueprint.route('/<uuid:user_id>/suspend')


@@ 235,8 249,9 @@ def suspend_account_form(user_id, erroneous_form=None):
    user = _get_user_or_404(user_id)

    if user.suspended:
        flash_error("Das Benutzerkonto '{}' ist bereits gesperrt.",
                    user.screen_name)
        flash_error(
            "Das Benutzerkonto '{}' ist bereits gesperrt.", user.screen_name
        )
        return redirect_to('.view', user_id=user.id)

    form = erroneous_form if erroneous_form else SuspendAccountForm()


@@ 254,8 269,9 @@ def suspend_account(user_id):
    user = _get_user_or_404(user_id)

    if user.suspended:
        flash_error("Das Benutzerkonto '{}' ist bereits gesperrt.",
                    user.screen_name)
        flash_error(
            "Das Benutzerkonto '{}' ist bereits gesperrt.", user.screen_name
        )
        return redirect_to('.view', user_id=user.id)

    form = SuspendAccountForm(request.form)


@@ 281,8 297,9 @@ def unsuspend_account_form(user_id, erroneous_form=None):
    user = _get_user_or_404(user_id)

    if not user.suspended:
        flash_error("Das Benutzerkonto '{}' ist bereits entsperrt.",
                    user.screen_name)
        flash_error(
            "Das Benutzerkonto '{}' ist bereits entsperrt.", user.screen_name
        )
        return redirect_to('.view', user_id=user.id)

    form = erroneous_form if erroneous_form else SuspendAccountForm()


@@ 300,8 317,9 @@ def unsuspend_account(user_id):
    user = _get_user_or_404(user_id)

    if not user.suspended:
        flash_error("Das Benutzerkonto '{}' ist bereits entsperrt.",
                    user.screen_name)
        flash_error(
            "Das Benutzerkonto '{}' ist bereits entsperrt.", user.screen_name
        )
        return redirect_to('.view', user_id=user.id)

    form = SuspendAccountForm(request.form)


@@ 327,8 345,10 @@ def delete_account_form(user_id, erroneous_form=None):
    user = _get_user_or_404(user_id)

    if user.deleted:
        flash_error("Das Benutzerkonto '{}' ist bereits gelöscht worden.",
                    user.screen_name)
        flash_error(
            "Das Benutzerkonto '{}' ist bereits gelöscht worden.",
            user.screen_name,
        )
        return redirect_to('.view', user_id=user.id)

    form = erroneous_form if erroneous_form else DeleteAccountForm()


@@ 346,8 366,10 @@ def delete_account(user_id):
    user = _get_user_or_404(user_id)

    if user.deleted:
        flash_error("Das Benutzerkonto '{}' ist bereits gelöscht worden.",
                    user.screen_name)
        flash_error(
            "Das Benutzerkonto '{}' ist bereits gelöscht worden.",
            user.screen_name,
        )
        return redirect_to('.view', user_id=user.id)

    form = DeleteAccountForm(request.form)


@@ 395,16 417,23 @@ def change_screen_name(user_id):
    initiator_id = g.current_user.id
    reason = form.reason.data.strip()

    user_command_service.change_screen_name(user.id, new_screen_name,
                                            initiator_id, reason=reason)

    screen_name_changed.send(None, user_id=user.id,
                             old_screen_name=old_screen_name,
                             new_screen_name=new_screen_name,
                             initiator_id=initiator_id)

    flash_success("Das Benutzerkonto '{}' wurde umbenannt in '{}'.",
                  old_screen_name, new_screen_name)
    user_command_service.change_screen_name(
        user.id, new_screen_name, initiator_id, reason=reason
    )

    screen_name_changed.send(
        None,
        user_id=user.id,
        old_screen_name=old_screen_name,
        new_screen_name=new_screen_name,
        initiator_id=initiator_id,
    )

    flash_success(
        "Das Benutzerkonto '{}' wurde umbenannt in '{}'.",
        old_screen_name,
        new_screen_name,
    )
    return redirect_to('.view', user_id=user.id)




@@ 415,8 444,9 @@ def view_permissions(user_id):
    """Show user's permissions."""
    user = _get_user_or_404(user_id)

    permissions_by_role = authorization_service \
        .get_permissions_by_roles_for_user_with_titles(user.id)
    permissions_by_role = authorization_service.get_permissions_by_roles_for_user_with_titles(
        user.id
    )

    return {
        'user': user,


@@ 431,8 461,9 @@ def manage_roles(user_id):
    """Manage what roles are assigned to the user."""
    user = _get_user_or_404(user_id)

    permissions_by_role = authorization_service \
        .get_permissions_by_roles_with_titles()
    permissions_by_role = (
        authorization_service.get_permissions_by_roles_with_titles()
    )

    user_role_ids = authorization_service.find_role_ids_for_user(user.id)



@@ 452,11 483,13 @@ def role_assign(user_id, role_id):
    role = _get_role_or_404(role_id)
    initiator_id = g.current_user.id

    authorization_service.assign_role_to_user(role.id, user.id,
                                              initiator_id=initiator_id)
    authorization_service.assign_role_to_user(
        role.id, user.id, initiator_id=initiator_id
    )

    flash_success('{} wurde die Rolle "{}" zugewiesen.',
                  user.screen_name, role.title)
    flash_success(
        '{} wurde die Rolle "{}" zugewiesen.', user.screen_name, role.title
    )


@blueprint.route('/<uuid:user_id>/roles/<role_id>', methods=['DELETE'])


@@ 468,11 501,13 @@ def role_deassign(user_id, role_id):
    role = _get_role_or_404(role_id)
    initiator_id = g.current_user.id

    authorization_service.deassign_role_from_user(role.id, user.id,
                                                  initiator_id=initiator_id)
    authorization_service.deassign_role_from_user(
        role.id, user.id, initiator_id=initiator_id
    )

    flash_success('{} wurde die Rolle "{}" genommen.',
                  user.screen_name, role.title)
    flash_success(
        '{} wurde die Rolle "{}" genommen.', user.screen_name, role.title
    )


@blueprint.route('/<uuid:user_id>/events')

M byceps/blueprints/admin/user_badge/views.py => byceps/blueprints/admin/user_badge/views.py +8 -4
@@ 115,10 115,14 @@ def create():
    else:
        brand_id = None

    badge = badge_service.create_badge(slug, label, image_filename,
                                       brand_id=brand_id,
                                       description=description,
                                       featured=featured)
    badge = badge_service.create_badge(
        slug,
        label,
        image_filename,
        brand_id=brand_id,
        description=description,
        featured=featured,
    )

    flash_success('Das Abzeichen "{}" wurde angelegt.', badge.label)
    return redirect_to('.index')

M byceps/blueprints/api/tourney/avatar/views.py => byceps/blueprints/api/tourney/avatar/views.py +2 -1
@@ 58,7 58,8 @@ def _create(party_id, creator_id, image):

    try:
        return avatar_service.create_avatar_image(
            party_id, creator_id, image.stream, ALLOWED_IMAGE_TYPES)
            party_id, creator_id, image.stream, ALLOWED_IMAGE_TYPES
        )
    except avatar_service.ImageTypeProhibited as e:
        abort(400, str(e))
    except FileExistsError:

M byceps/blueprints/api/tourney/match/views.py => byceps/blueprints/api/tourney/match/views.py +5 -3
@@ 80,9 80,11 @@ def _comment_to_json(comment):
    }


blueprint.add_url_rule('/<uuid:match_id>/comments/<uuid:comment_id>',
                       endpoint='comment_view',
                       build_only=True)
blueprint.add_url_rule(
    '/<uuid:match_id>/comments/<uuid:comment_id>',
    endpoint='comment_view',
    build_only=True,
)


@blueprint.route('/<uuid:match_id>/comments', methods=['POST'])

M byceps/blueprints/attendance/views.py => byceps/blueprints/attendance/views.py +2 -1
@@ 29,7 29,8 @@ def attendees(page):
        abort(404)

    pagination = ticket_service.get_tickets_in_use_for_party_paginated(
        g.party_id, page, per_page, search_term=search_term)
        g.party_id, page, per_page, search_term=search_term
    )

    tickets = pagination.items


M byceps/blueprints/authentication/forms.py => byceps/blueprints/authentication/forms.py +2 -1
@@ 55,4 55,5 @@ class UpdatePasswordForm(ResetPasswordForm):

        if not password_service.is_password_valid_for_user(user_id, password):
            raise ValidationError(
                'Das eingegebene Passwort stimmt nicht mit dem bisherigen überein.')
                'Das eingegebene Passwort stimmt nicht mit dem bisherigen überein.'
            )

M byceps/blueprints/authentication/session.py => byceps/blueprints/authentication/session.py +3 -2
@@ 71,8 71,9 @@ def _load_user(
    if user_id is None:
        return None

    user = user_service.find_active_user(user_id, include_avatar=True,
                                         include_orga_flag_for_party_id=party_id)
    user = user_service.find_active_user(
        user_id, include_avatar=True, include_orga_flag_for_party_id=party_id
    )

    if user is None:
        return None

M byceps/blueprints/authentication/views.py => byceps/blueprints/authentication/views.py +36 -20
@@ 58,8 58,10 @@ def login_form():
    """Show login form."""
    logged_in = g.current_user.is_active
    if logged_in:
        flash_notice('Du bist bereits als Benutzer "{}" angemeldet.',
                     g.current_user.screen_name)
        flash_notice(
            'Du bist bereits als Benutzer "{}" angemeldet.',
            g.current_user.screen_name,
        )

    in_admin_mode = get_site_mode().is_admin()



@@ 114,11 116,13 @@ def login():
    if not in_admin_mode:
        required_consent_subject_ids = _get_required_consent_subject_ids()
        if _is_consent_required(user.id, required_consent_subject_ids):
            verification_token = verification_token_service \
                .find_or_create_for_terms_consent(user.id)
            verification_token = verification_token_service.find_or_create_for_terms_consent(
                user.id
            )

            consent_form_url = url_for('consent.consent_form',
                                       token=verification_token.token)
            consent_form_url = url_for(
                'consent.consent_form', token=verification_token.token
            )

            return [('Location', consent_form_url)]



@@ 140,13 144,15 @@ def _is_login_allowed():
def _get_required_consent_subject_ids():
    subject_ids = []

    terms_version = terms_version_service \
        .find_current_version_for_brand(g.brand_id)
    terms_version = terms_version_service.find_current_version_for_brand(
        g.brand_id
    )
    if terms_version:
        subject_ids.append(terms_version.consent_subject_id)

    privacy_policy_consent_subject_id \
        = _find_privacy_policy_consent_subject_id()
    privacy_policy_consent_subject_id = (
        _find_privacy_policy_consent_subject_id()
    )

    if privacy_policy_consent_subject_id:
        subject_ids.append(privacy_policy_consent_subject_id)


@@ 156,7 162,9 @@ def _get_required_consent_subject_ids():

def _is_consent_required(user_id, subject_ids):
    for subject_id in subject_ids:
        if not consent_service.has_user_consented_to_subject(user_id, subject_id):
        if not consent_service.has_user_consented_to_subject(
            user_id, subject_id
        ):
            return True

    return False


@@ 231,8 239,11 @@ def request_password_reset():
        return request_password_reset_form(form)

    if not user.email_address_verified:
        flash_error('Die E-Mail-Adresse für das Benutzerkonto "{}" wurde '
                    'noch nicht bestätigt.', screen_name)
        flash_error(
            'Die E-Mail-Adresse für das Benutzerkonto "{}" wurde '
            'noch nicht bestätigt.',
            screen_name,
        )
        return redirect_to('user_email_address.request_confirmation_email')

    sender = None


@@ 246,7 257,8 @@ def request_password_reset():
    flash_success(
        'Ein Link zum Setzen eines neuen Passworts für den Benutzernamen "{}" '
        'wurde an die hinterlegte E-Mail-Adresse versendet.',
        user.screen_name)
        user.screen_name,
    )
    return request_password_reset_form()




@@ 254,8 266,9 @@ def request_password_reset():
@templated
def password_reset_form(token, erroneous_form=None):
    """Show a form to reset the current user's password."""
    verification_token = verification_token_service \
        .find_for_password_reset_by_token(token)
    verification_token = verification_token_service.find_for_password_reset_by_token(
        token
    )

    _verify_password_reset_token(verification_token)



@@ 270,8 283,9 @@ def password_reset_form(token, erroneous_form=None):
@blueprint.route('/password/reset/token/<token>', methods=['POST'])
def password_reset(token):
    """Reset the current user's password."""
    verification_token = verification_token_service \
        .find_for_password_reset_by_token(token)
    verification_token = verification_token_service.find_for_password_reset_by_token(
        token
    )

    _verify_password_reset_token(verification_token)



@@ 289,8 303,10 @@ def password_reset(token):

def _verify_password_reset_token(verification_token):
    if verification_token is None or verification_token.is_expired:
        flash_error('Es wurde kein gültiges Token angegeben. '
                    'Ein Token ist nur 24 Stunden lang gültig.')
        flash_error(
            'Es wurde kein gültiges Token angegeben. '
            'Ein Token ist nur 24 Stunden lang gültig.'
        )
        abort(404)



M byceps/blueprints/authorization/registry.py => byceps/blueprints/authorization/registry.py +4 -1
@@ 41,7 41,10 @@ class PermissionRegistry:
            current_app.logger.warn(
                'Ignoring unknown permission name "%s" configured '
                'in database for "%s" enum (permission ID: "%s").',
                permission_name, enum_key, permission_id)
                permission_name,
                enum_key,
                permission_id,
            )
            return None

    def get_enum_members(self, permission_ids):

M byceps/blueprints/board/_helpers.py => byceps/blueprints/board/_helpers.py +19 -16
@@ 75,8 75,9 @@ def get_posting_or_404(posting_id):


def require_board_access(board_id, user_id):
    has_access = access_control_service.has_user_access_to_board(user_id,
                                                                 board_id)
    has_access = access_control_service.has_user_access_to_board(
        user_id, board_id
    )
    if not has_access:
        abort(404)



@@ 86,15 87,15 @@ def build_external_url_for_topic(topic_id):


def build_url_for_topic(topic_id, *, external=False):
    return url_for('board.topic_view',
                   topic_id=topic_id,
                   _external=external)
    return url_for('board.topic_view', topic_id=topic_id, _external=external)


def build_url_for_topic_in_category_view(topic):
    return url_for('board.category_view',
                   slug=topic.category.slug,
                   _anchor='topic-{}'.format(topic.id))
    return url_for(
        'board.category_view',
        slug=topic.category.slug,
        _anchor='topic-{}'.format(topic.id),
    )


def build_external_url_for_posting(posting_id):


@@ 102,14 103,16 @@ def build_external_url_for_posting(posting_id):


def build_url_for_posting(posting_id, *, external=False):
    return url_for('board.posting_view',
                   posting_id=posting_id,
                   _external=external)
    return url_for(
        'board.posting_view', posting_id=posting_id, _external=external
    )


def build_url_for_posting_in_topic_view(posting, page, **kwargs):
    return url_for('board.topic_view',
                   topic_id=posting.topic.id,
                   page=page,
                   _anchor='posting-{}'.format(posting.id),
                   **kwargs)
    return url_for(
        'board.topic_view',
        topic_id=posting.topic.id,
        page=page,
        _anchor='posting-{}'.format(posting.id),
        **kwargs,
    )

M byceps/blueprints/board/service.py => byceps/blueprints/board/service.py +11 -7
@@ 42,8 42,9 @@ def add_unseen_postings_flag_to_categories(
            and board_last_view_service.contains_category_unseen_postings(
                category, user.id)

        category_with_flag = CategoryWithLastUpdateAndUnseenFlag \
            .from_category_with_last_update(category, contains_unseen_postings)
        category_with_flag = CategoryWithLastUpdateAndUnseenFlag.from_category_with_last_update(
            category, contains_unseen_postings
        )

        categories_with_flag.append(category_with_flag)



@@ 89,7 90,8 @@ def enrich_creators(
    if party_id is not None:
        party = party_service.get_party(party_id)
        ticket_users = ticket_service.select_ticket_users_for_party(
            creator_ids, party.id)
            creator_ids, party.id
        )
    else:
        party = None
        ticket_users = set()


@@ 111,8 113,9 @@ def _get_badges(
    user_ids: Set[UserID], brand_id: BrandID
) -> Dict[UserID, Set[Badge]]:
    """Fetch users' badges that are either global or belong to the brand."""
    badges_by_user_id = badge_service.get_badges_for_users(user_ids,
                                                           featured_only=True)
    badges_by_user_id = badge_service.get_badges_for_users(
        user_ids, featured_only=True
    )

    def generate_items():
        for user_id, badges in badges_by_user_id.items():


@@ 127,8 130,9 @@ def calculate_posting_page_number(posting: DbPosting, user: CurrentUser) -> int:
    """Calculate the number of postings to show per page."""
    postings_per_page = get_postings_per_page_value()

    return board_posting_query_service \
        .calculate_posting_page_number(posting, user, postings_per_page)
    return board_posting_query_service.calculate_posting_page_number(
        posting, user, postings_per_page
    )


def get_topics_per_page_value() -> int:

M byceps/blueprints/board/views_category.py => byceps/blueprints/board/views_category.py +18 -11
@@ 30,11 30,13 @@ def category_index():

    h.require_board_access(board_id, user.id)

    categories = board_category_query_service \
        .get_categories_with_last_updates(board_id)
    categories = board_category_query_service.get_categories_with_last_updates(
        board_id
    )

    categories_with_flag = service.add_unseen_postings_flag_to_categories(
        categories, user)
        categories, user
    )

    return {
        'categories': categories_with_flag,


@@ 51,8 53,9 @@ def category_view(slug, page):

    h.require_board_access(board_id, user.id)

    category = board_category_query_service \
        .find_category_by_slug(board_id, slug)
    category = board_category_query_service.find_category_by_slug(
        board_id, slug
    )

    if category is None:
        abort(404)


@@ 61,13 64,15 @@ def category_view(slug, page):
        abort(404)

    if not user.is_anonymous:
        board_last_view_service.mark_category_as_just_viewed(category.id,
                                                             user.id)
        board_last_view_service.mark_category_as_just_viewed(
            category.id, user.id
        )

    topics_per_page = service.get_topics_per_page_value()

    topics = board_topic_query_service \
        .paginate_topics_of_category(category.id, user, page, topics_per_page)
    topics = board_topic_query_service.paginate_topics_of_category(
        category.id, user, page, topics_per_page
    )

    service.add_topic_creators(topics.items)
    service.add_topic_unseen_flag(topics.items, user)


@@ 86,9 91,11 @@ def mark_all_topics_in_category_as_viewed(category_id):
    category = h.get_category_or_404(category_id)

    board_last_view_service.mark_all_topics_in_category_as_viewed(
        category_id, g.current_user.id)
        category_id, g.current_user.id
    )

    flash_success(
        'Alle Themen in dieser Kategorie wurden als gelesen markiert.')
        'Alle Themen in dieser Kategorie wurden als gelesen markiert.'
    )

    return url_for('.category_view', slug=category.slug)

M byceps/blueprints/board/views_posting.py => byceps/blueprints/board/views_posting.py +39 -21
@@ 37,7 37,8 @@ def posting_view(posting_id):
    page = service.calculate_posting_page_number(posting, g.current_user)

    return redirect(
        h.build_url_for_posting_in_topic_view(posting, page, _external=True))
        h.build_url_for_posting_in_topic_view(posting, page, _external=True)
    )


@blueprint.route('/topics/<uuid:topic_id>/create')


@@ 72,8 73,9 @@ def quote_posting_as_bbcode():

    creator = user_service.find_user(posting.creator_id)

    return '[quote author="{}"]{}[/quote]'.format(creator.screen_name,
                                                  posting.body)
    return '[quote author="{}"]{}[/quote]'.format(
        creator.screen_name, posting.body
    )


@blueprint.route('/topics/<uuid:topic_id>/create', methods=['POST'])


@@ 93,26 95,33 @@ def posting_create(topic_id):
        flash_error(
            'Das Thema ist geschlossen. '
            'Es können keine Beiträge mehr hinzugefügt werden.',
            icon='lock')
            icon='lock',
        )
        return redirect(h.build_url_for_topic(topic.id))

    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')
            icon='announce',
        )
        return redirect(h.build_url_for_topic(topic.id))

    posting = board_posting_command_service \
        .create_posting(topic, creator.id, body)
    posting = board_posting_command_service.create_posting(
        topic, creator.id, body
    )

    if not g.current_user.is_anonymous:
        board_last_view_service.mark_category_as_just_viewed(topic.category.id,
                                                             g.current_user.id)
        board_last_view_service.mark_category_as_just_viewed(
            topic.category.id, g.current_user.id
        )

    flash_success('Deine Antwort wurde hinzugefügt.')
    signals.posting_created.send(None, posting_id=posting.id,
                                 url=h.build_external_url_for_posting(posting.id))
    signals.posting_created.send(
        None,
        posting_id=posting.id,
        url=h.build_external_url_for_posting(posting.id),
    )

    postings_per_page = service.get_postings_per_page_value()
    page_count = topic.count_pages(postings_per_page)


@@ 135,7 144,8 @@ def posting_update_form(posting_id, erroneous_form=None):
    if posting.topic.locked and not user_may_update:
        flash_error(
            'Der Beitrag darf nicht bearbeitet werden weil das Thema, '
            'zu dem dieser Beitrag gehört, gesperrt ist.')
            'zu dem dieser Beitrag gehört, gesperrt ist.'
        )
        return redirect(url)

    if posting.topic.hidden or posting.hidden:


@@ 169,7 179,8 @@ def posting_update(posting_id):
    if posting.topic.locked and not user_may_update:
        flash_error(
            'Der Beitrag darf nicht bearbeitet werden weil das Thema, '
            'zu dem dieser Beitrag gehört, gesperrt ist.')
            'zu dem dieser Beitrag gehört, gesperrt ist.'
        )
        return redirect(url)

    if posting.topic.hidden or posting.hidden:


@@ 184,8 195,9 @@ def posting_update(posting_id):
    if not form.validate():
        return posting_update_form(posting_id, form)

    board_posting_command_service \
        .update_posting(posting, g.current_user.id, form.body.data)
    board_posting_command_service.update_posting(
        posting, g.current_user.id, form.body.data
    )

    flash_success('Der Beitrag wurde aktualisiert.')
    return redirect(url)


@@ 219,9 231,12 @@ def posting_hide(posting_id):

    flash_success('Der Beitrag wurde versteckt.', icon='hidden')

    signals.posting_hidden.send(None, posting_id=posting.id,
                                moderator_id=moderator_id,
                                url=h.build_external_url_for_posting(posting.id))
    signals.posting_hidden.send(
        None,
        posting_id=posting.id,
        moderator_id=moderator_id,
        url=h.build_external_url_for_posting(posting.id),
    )

    return h.build_url_for_posting_in_topic_view(posting, page)



@@ 240,8 255,11 @@ def posting_unhide(posting_id):

    flash_success('Der Beitrag wurde wieder sichtbar gemacht.', icon='view')

    signals.posting_unhidden.send(None, posting_id=posting.id,
                                  moderator_id=moderator_id,
                                  url=h.build_external_url_for_posting(posting.id))
    signals.posting_unhidden.send(
        None,
        posting_id=posting.id,
        moderator_id=moderator_id,
        url=h.build_external_url_for_posting(posting.id),
    )

    return h.build_url_for_posting_in_topic_view(posting, page)

M byceps/blueprints/board/views_topic.py => byceps/blueprints/board/views_topic.py +99 -55
@@ 41,8 41,9 @@ def topic_index(page):

    topics_per_page = service.get_topics_per_page_value()

    topics = board_topic_query_service \
        .paginate_topics(board_id, user, page, topics_per_page)
    topics = board_topic_query_service.paginate_topics(
        board_id, user, page, topics_per_page
    )

    service.add_topic_creators(topics.items)
    service.add_topic_unseen_flag(topics.items, user)


@@ 59,8 60,9 @@ def topic_view(topic_id, page):
    """List postings for the topic."""
    user = g.current_user

    topic = board_topic_query_service \
        .find_topic_visible_for_user(topic_id, user)
    topic = board_topic_query_service.find_topic_visible_for_user(
        topic_id, user
    )

    if topic is None:
        abort(404)


@@ 78,18 80,21 @@ def topic_view(topic_id, page):
    # Copy last view timestamp for later use to compare postings
    # against it.
    last_viewed_at = board_last_view_service.find_topic_last_viewed_at(
        topic.id, user.id)
        topic.id, user.id
    )

    postings_per_page = service.get_postings_per_page_value()
    if page == 0:
        posting = board_topic_query_service \
            .find_default_posting_to_jump_to(topic.id, user, last_viewed_at)
        posting = board_topic_query_service.find_default_posting_to_jump_to(
            topic.id, user, last_viewed_at
        )

        if posting is None:
            page = 1
        else:
            page = service.calculate_posting_page_number(posting,
                                                         g.current_user)
            page = service.calculate_posting_page_number(
                posting, g.current_user
            )
            # Jump to a specific posting. This requires a redirect.
            url = h.build_url_for_posting_in_topic_view(posting, page)
            return redirect(url, code=307)


@@ 99,8 104,9 @@ def topic_view(topic_id, page):
        # 'new' tag from a locked topic.
        board_last_view_service.mark_topic_as_just_viewed(topic.id, user.id)

    postings = board_posting_query_service \
        .paginate_postings(topic.id, user, g.party_id, page, postings_per_page)
    postings = board_posting_query_service.paginate_postings(
        topic.id, user, g.party_id, page, postings_per_page
    )

    service.add_unseen_flag_to_postings(postings.items, user, last_viewed_at)



@@ 153,8 159,9 @@ def topic_create(category_id):
    title = form.title.data.strip()
    body = form.body.data.strip()

    topic = board_topic_command_service \
        .create_topic(category.id, creator.id, title, body)
    topic = board_topic_command_service.create_topic(
        category.id, creator.id, title, body
    )
    topic_url = h.build_external_url_for_topic(topic.id)

    flash_success('Das Thema "{}" wurde hinzugefügt.', topic.title)


@@ 175,7 182,8 @@ def topic_update_form(topic_id, erroneous_form=None):

    if topic.locked and not user_may_update:
        flash_error(
            'Das Thema darf nicht bearbeitet werden weil es gesperrt ist.')
            'Das Thema darf nicht bearbeitet werden weil es gesperrt ist.'
        )
        return redirect(url)

    if topic.hidden:


@@ 207,7 215,8 @@ def topic_update(topic_id):

    if topic.locked and not user_may_update:
        flash_error(
            'Das Thema darf nicht bearbeitet werden weil es gesperrt ist.')
            'Das Thema darf nicht bearbeitet werden weil es gesperrt ist.'
        )
        return redirect(url)

    if topic.hidden:


@@ 222,8 231,9 @@ def topic_update(topic_id):
    if not form.validate():
        return topic_update_form(topic_id, form)

    board_topic_command_service \
        .update_topic(topic, g.current_user.id, form.title.data, form.body.data)
    board_topic_command_service.update_topic(
        topic, g.current_user.id, form.title.data, form.body.data
    )

    flash_success('Das Thema "{}" wurde aktualisiert.', topic.title)
    return redirect(url)


@@ 239,8 249,9 @@ def topic_moderate_form(topic_id):

    topic.creator = user_service.find_user(topic.creator_id)

    categories = board_category_query_service \
        .get_categories_excluding(board_id, topic.category_id)
    categories = board_category_query_service.get_categories_excluding(
        board_id, topic.category_id
    )

    return {
        'topic': topic,


@@ 260,9 271,12 @@ def topic_hide(topic_id):

    flash_success('Das Thema "{}" wurde versteckt.', topic.title, icon='hidden')

    signals.topic_hidden.send(None, topic_id=topic.id,
                              moderator_id=moderator_id,
                              url=h.build_external_url_for_topic(topic.id))
    signals.topic_hidden.send(
        None,
        topic_id=topic.id,
        moderator_id=moderator_id,
        url=h.build_external_url_for_topic(topic.id),
    )

    return h.build_url_for_topic_in_category_view(topic)



@@ 278,11 292,17 @@ def topic_unhide(topic_id):
    board_topic_command_service.unhide_topic(topic, moderator_id)

    flash_success(
        'Das Thema "{}" wurde wieder sichtbar gemacht.', topic.title, icon='view')

    signals.topic_unhidden.send(None, topic_id=topic.id,
                                moderator_id=moderator_id,
                                url=h.build_external_url_for_topic(topic.id))
        'Das Thema "{}" wurde wieder sichtbar gemacht.',
        topic.title,
        icon='view',
    )

    signals.topic_unhidden.send(
        None,
        topic_id=topic.id,
        moderator_id=moderator_id,
        url=h.build_external_url_for_topic(topic.id),
    )

    return h.build_url_for_topic_in_category_view(topic)



@@ 299,9 319,12 @@ def topic_lock(topic_id):

    flash_success('Das Thema "{}" wurde geschlossen.', topic.title, icon='lock')

    signals.topic_locked.send(None, topic_id=topic.id,
                              moderator_id=moderator_id,
                              url=h.build_external_url_for_topic(topic.id))
    signals.topic_locked.send(
        None,
        topic_id=topic.id,
        moderator_id=moderator_id,
        url=h.build_external_url_for_topic(topic.id),
    )

    return h.build_url_for_topic_in_category_view(topic)



@@ 316,12 339,16 @@ def topic_unlock(topic_id):

    board_topic_command_service.unlock_topic(topic, moderator_id)

    flash_success('Das Thema "{}" wurde wieder geöffnet.', topic.title,
                  icon='unlock')
    flash_success(
        'Das Thema "{}" wurde wieder geöffnet.', topic.title, icon='unlock'
    )

    signals.topic_unlocked.send(None, topic_id=topic.id,
                                moderator_id=moderator_id,
                                url=h.build_external_url_for_topic(topic.id))
    signals.topic_unlocked.send(
        None,
        topic_id=topic.id,
        moderator_id=moderator_id,
        url=h.build_external_url_for_topic(topic.id),
    )

    return h.build_url_for_topic_in_category_view(topic)



@@ 338,9 365,12 @@ def topic_pin(topic_id):

    flash_success('Das Thema "{}" wurde angepinnt.', topic.title, icon='pin')

    signals.topic_pinned.send(None, topic_id=topic.id,
                              moderator_id=moderator_id,
                              url=h.build_external_url_for_topic(topic.id))
    signals.topic_pinned.send(
        None,
        topic_id=topic.id,
        moderator_id=moderator_id,
        url=h.build_external_url_for_topic(topic.id),
    )

    return h.build_url_for_topic_in_category_view(topic)



@@ 357,9 387,12 @@ def topic_unpin(topic_id):

    flash_success('Das Thema "{}" wurde wieder gelöst.', topic.title)

    signals.topic_unpinned.send(None, topic_id=topic.id,
                                moderator_id=moderator_id,
                                url=h.build_external_url_for_topic(topic.id))
    signals.topic_unpinned.send(
        None,
        topic_id=topic.id,
        moderator_id=moderator_id,
        url=h.build_external_url_for_topic(topic.id),
    )

    return h.build_url_for_topic_in_category_view(topic)



@@ 381,16 414,23 @@ def topic_move(topic_id):

    board_topic_command_service.move_topic(topic, new_category.id)

    flash_success('Das Thema "{}" wurde aus der Kategorie "{}" '
                  'in die Kategorie "{}" verschoben.',
                  topic.title, old_category.title, new_category.title,
                  icon='move')

    signals.topic_moved.send(None, topic_id=topic.id,
                             old_category_id=old_category.id,
                             new_category_id=new_category.id,
                             moderator_id=moderator_id,
                             url=h.build_external_url_for_topic(topic.id))
    flash_success(
        'Das Thema "{}" wurde aus der Kategorie "{}" '
        'in die Kategorie "{}" verschoben.',
        topic.title,
        old_category.title,
        new_category.title,
        icon='move',
    )

    signals.topic_moved.send(
        None,
        topic_id=topic.id,
        old_category_id=old_category.id,
        new_category_id=new_category.id,
        moderator_id=moderator_id,
        url=h.build_external_url_for_topic(topic.id),
    )

    return redirect(h.build_url_for_topic_in_category_view(topic))



@@ 406,8 446,11 @@ def topic_limit_to_announcements(topic_id):

    board_topic_command_service.limit_topic_to_announcements(topic)

    flash_success('Das Thema "{}" wurde auf Ankündigungen beschränkt.',
                  topic.title, icon='announce')
    flash_success(
        'Das Thema "{}" wurde auf Ankündigungen beschränkt.',
        topic.title,
        icon='announce',
    )

    return h.build_url_for_topic_in_category_view(topic)



@@ 423,7 466,8 @@ def topic_remove_limit_to_announcements(topic_id):

    board_topic_command_service.remove_limit_of_topic_to_announcements(topic)

    flash_success('Das Thema "{}" wurde für normale Beiträge geöffnet.',
                  topic.title)
    flash_success(
        'Das Thema "{}" wurde für normale Beiträge geöffnet.', topic.title
    )

    return h.build_url_for_topic_in_category_view(topic)

M byceps/blueprints/consent/views.py => byceps/blueprints/consent/views.py +14 -7
@@ 80,10 80,13 @@ def consent(token):
            return consent_form(token, erroneous_form=form)

    expressed_at = datetime.utcnow()
    consent_service.consent_to_subjects(subject_ids_from_form, expressed_at,
                                        verification_token)
    consent_service.consent_to_subjects(
        subject_ids_from_form, expressed_at, verification_token
    )

    flash_success('Vielen Dank für deine Zustimmung. Bitte melde dich erneut an.')
    flash_success(
        'Vielen Dank für deine Zustimmung. Bitte melde dich erneut an.'
    )
    return redirect_to('authentication.login_form')




@@ 91,7 94,8 @@ def _get_unconsented_subjects_for_user(user_id):
    required_consent_subject_ids = _get_required_consent_subject_ids()

    unconsented_subject_ids = _get_unconsented_subject_ids(
        user_id, required_consent_subject_ids)
        user_id, required_consent_subject_ids
    )

    return _get_subjects(unconsented_subject_ids)



@@ 100,7 104,9 @@ def _get_unconsented_subject_ids(user_id, required_subject_ids):
    subject_ids = []

    for subject_id in required_subject_ids:
        if not consent_service.has_user_consented_to_subject(user_id, subject_id):
        if not consent_service.has_user_consented_to_subject(
            user_id, subject_id
        ):
            subject_ids.append(subject_id)

    return subject_ids


@@ 112,8 118,9 @@ def _get_subjects(subject_ids):


def _get_verification_token_or_404(token_value):
    verification_token = verification_token_service \
        .find_for_terms_consent_by_token(token_value)
    verification_token = verification_token_service.find_for_terms_consent_by_token(
        token_value
    )

    if verification_token is None:
        flash_error('Unbekannter Bestätigungscode.')

M byceps/blueprints/core/views.py => byceps/blueprints/core/views.py +3 -2
@@ 90,5 90,6 @@ def provide_site_mode():

    # current user
    is_admin_mode = site_mode.is_admin()
    g.current_user = authentication_blueprint_service \
        .get_current_user(is_admin_mode, party_id=party_id)
    g.current_user = authentication_blueprint_service.get_current_user(
        is_admin_mode, party_id=party_id
    )

M byceps/blueprints/healthcheck/views.py => byceps/blueprints/healthcheck/views.py +2 -1
@@ 44,7 44,8 @@ def health():
    status_code = 503 if not rdbms_ok else 200

    return current_app.response_class(
        json, status=status_code, content_type='application/health+json')
        json, status=status_code, content_type='application/health+json'
    )


def _is_rdbms_ok():

M byceps/blueprints/metrics/views.py => byceps/blueprints/metrics/views.py +1 -3
@@ 23,6 23,4 @@ def metrics():
    metrics = metrics_service.collect_metrics()
    lines = list(metrics_service.serialize(metrics))

    return Response(lines,
                    status=200,
                    mimetype='text/plain; version=0.0.4')
    return Response(lines, status=200, mimetype='text/plain; version=0.0.4')

M byceps/blueprints/news/views.py => byceps/blueprints/news/views.py +7 -4
@@ 33,7 33,8 @@ def index(page):
    published_only = not _may_view_drafts(g.current_user)

    items = news_service.get_aggregated_items_paginated(
        channel_id, page, items_per_page, published_only=published_only)
        channel_id, page, items_per_page, published_only=published_only
    )

    return {
        'items': items,


@@ 49,7 50,8 @@ def view(slug):
    published_only = not _may_view_drafts(g.current_user)

    item = news_service.find_aggregated_item_by_slug(
        channel_id, slug, published_only=published_only)
        channel_id, slug, published_only=published_only
    )

    if item is None:
        abort(404)


@@ 60,8 62,9 @@ def view(slug):


def _get_channel_id():
    channel_id = site_settings_service \
        .find_setting_value(g.site_id, 'news_channel_id')
    channel_id = site_settings_service.find_setting_value(
        g.site_id, 'news_channel_id'
    )

    if channel_id is None:
        abort(404)

M byceps/blueprints/orga_team/views.py => byceps/blueprints/orga_team/views.py +2 -1
@@ 42,7 42,8 @@ def index():
            user,
            membership.user.detail.full_name,
            membership.orga_team.title,
            membership.duties)
            membership.duties,
        )

    orgas = list(map(_to_orga, memberships))


M byceps/blueprints/seating/views.py => byceps/blueprints/seating/views.py +20 -8
@@ 69,7 69,8 @@ def view_area(slug):

    if seat_management_enabled:
        tickets = ticket_service.find_tickets_for_seat_manager(
            g.current_user.id, g.party_id)
            g.current_user.id, g.party_id
        )
    else:
        tickets = None



@@ 118,7 119,9 @@ def occupy_seat(ticket_id, seat_id):
    if not ticket.is_seat_managed_by(manager.id):
        flash_error(
            'Du bist nicht berechtigt, den Sitzplatz für Ticket {} '
            'zu verwalten.', ticket.code)
            'zu verwalten.',
            ticket.code,
        )
        return

    seat = _get_seat_or_404(seat_id)


@@ 128,17 131,22 @@ def occupy_seat(ticket_id, seat_id):
        return

    try:
        ticket_seat_management_service \
            .occupy_seat(ticket.id, seat.id, manager.id)
        ticket_seat_management_service.occupy_seat(
            ticket.id, seat.id, manager.id
        )
    except ticket_exceptions.SeatChangeDeniedForBundledTicket:
        flash_error(
            'Ticket {} gehört zu einem Paket und kann nicht einzeln '
            'verwaltet werden.', ticket.code)
            'verwaltet werden.',
            ticket.code,
        )
        return
    except ticket_exceptions.TicketCategoryMismatch:
        flash_error(
            'Ticket {} und {} haben unterschiedliche Kategorien.',
            ticket.code, seat.label)
            ticket.code,
            seat.label,
        )
        return
    except ValueError:
        abort(404)


@@ 164,7 172,9 @@ def release_seat(ticket_id):
    if not ticket.is_seat_managed_by(manager.id):
        flash_error(
            'Du bist nicht berechtigt, den Sitzplatz für Ticket {} '
            'zu verwalten.', ticket.code)
            'zu verwalten.',
            ticket.code,
        )
        return

    seat = ticket.occupied_seat


@@ 174,7 184,9 @@ def release_seat(ticket_id):
    except ticket_exceptions.SeatChangeDeniedForBundledTicket:
        flash_error(
            'Ticket {} gehört zu einem Paket und kann nicht einzeln '
            'verwaltet werden.', ticket.code)
            'verwaltet werden.',
            ticket.code,
        )
        return

    flash_success('{} wurde freigegeben.', seat.label)

M byceps/blueprints/shop/order/views.py => byceps/blueprints/shop/order/views.py +18 -12
@@ 41,8 41,9 @@ def order_form(erroneous_form=None):
        flash_notice('Der Shop ist derzeit geschlossen.')
        return {'article_compilation': None}

    article_compilation = article_service \
        .get_article_compilation_for_orderable_articles(shop.id)
    article_compilation = article_service.get_article_compilation_for_orderable_articles(
        shop.id
    )

    if article_compilation.is_empty():
        flash_error('Es sind keine Artikel verfügbar.')


@@ 88,8 89,9 @@ def order():
        flash_notice('Der Shop ist derzeit geschlossen.')
        return order_form()

    article_compilation = article_service \
        .get_article_compilation_for_orderable_articles(shop.id)
    article_compilation = article_service.get_article_compilation_for_orderable_articles(
        shop.id
    )

    if article_compilation.is_empty():
        flash_error('Es sind keine Artikel verfügbar.')


@@ 136,8 138,9 @@ def order_single_form(article_id, erroneous_form=None):
            'article': None,
        }

    article_compilation = article_service \
        .get_article_compilation_for_single_article(article, fixed_quantity=1)
    article_compilation = article_service.get_article_compilation_for_single_article(
        article, fixed_quantity=1
    )

    user = user_service.find_user_with_details(g.current_user.id)



@@ 190,9 193,9 @@ def order_single(article_id):
        flash_error('Der Artikel kann nicht direkt bestellt werden.')
        return order_single_form(article.id)

    article_compilation = article_service \
        .get_article_compilation_for_single_article(article,
                                                    fixed_quantity=quantity)
    article_compilation = article_service.get_article_compilation_for_single_article(
        article, fixed_quantity=quantity
    )

    user = g.current_user



@@ 260,6 263,9 @@ def _place_order(shop_id, orderer, cart):


def _flash_order_success(order):
    flash_success('Deine Bestellung mit der Bestellnummer <strong>{}</strong> '
                  'wurde entgegen genommen. Vielen Dank!', order.order_number,
                  text_is_safe=True)
    flash_success(
        'Deine Bestellung mit der Bestellnummer <strong>{}</strong> '
        'wurde entgegen genommen. Vielen Dank!',
        order.order_number,
        text_is_safe=True,
    )

M byceps/blueprints/shop/orders/views.py => byceps/blueprints/shop/orders/views.py +8 -5
@@ 40,7 40,8 @@ def index():
    if party.shop_id is not None:
        shop = shop_service.get_shop(party.shop_id)
        orders = order_service.get_orders_placed_by_user_for_shop(
            current_user.id, shop.id)
            current_user.id, shop.id
        )
    else:
        orders = []



@@ 82,8 83,9 @@ def view(order_id):
    }

    if order.is_open:
        template_context['payment_instructions'] \
            = _get_payment_instructions(order)
        template_context['payment_instructions'] = _get_payment_instructions(
            order
        )

    return template_context



@@ 92,5 94,6 @@ def _get_payment_instructions(order):
    scope = Scope('shop', str(order.shop_id))
    context = {'order_number': order.order_number}

    return render_snippet_as_partial('payment_instructions', scope=scope,
                                     context=context)
    return render_snippet_as_partial(
        'payment_instructions', scope=scope, context=context
    )

M byceps/blueprints/snippet/init.py => byceps/blueprints/snippet/init.py +5 -3
@@ 23,12 23,14 @@ def add_routes_for_snippets(site_id):

def add_route_for_snippet(mountpoint):
    """Register a route for the snippet."""
    endpoint = '{}.{}'.format(snippet_blueprint.name,
                              mountpoint.endpoint_suffix)
    endpoint = '{}.{}'.format(
        snippet_blueprint.name, mountpoint.endpoint_suffix
    )
    defaults = {'name': mountpoint.endpoint_suffix}

    current_app.add_url_rule(
        mountpoint.url_path,
        endpoint,
        view_func=view_current_version_by_name,
        defaults=defaults)
        defaults=defaults,
    )

M byceps/blueprints/snippet/templating.py => byceps/blueprints/snippet/templating.py +3 -2
@@ 62,8 62,9 @@ def render_snippet_as_partial(
    if scope is None:
        scope = Scope.for_site(g.site_id)

    current_version = snippet_service \
        .find_current_version_of_snippet_with_name(scope, name)
    current_version = snippet_service.find_current_version_of_snippet_with_name(
        scope, name
    )

    if current_version is None:
        if ignore_if_unknown:

M byceps/blueprints/snippet/views.py => byceps/blueprints/snippet/views.py +6 -4
@@ 30,8 30,9 @@ def view_current_version_by_name(name):
    name.
    """
    # Note: endpoint suffix != snippet name
    version = mountpoint_service \
        .find_current_snippet_version_for_mountpoint(g.site_id, name)
    version = mountpoint_service.find_current_snippet_version_for_mountpoint(
        g.site_id, name
    )

    if version is None:
        abort(404)


@@ 71,5 72,6 @@ def _find_current_snippet_version(name):
    """
    scope = Scope.for_site(g.site_id)

    return snippet_service \
        .find_current_version_of_snippet_with_name(scope, name)
    return snippet_service.find_current_version_of_snippet_with_name(
        scope, name
    )

M byceps/blueprints/ticketing/forms.py => byceps/blueprints/ticketing/forms.py +9 -5
@@ 27,13 27,17 @@ def validate_user(form, field):

    user = user.to_dto()

    terms_version = terms_version_service \
        .find_current_version_for_brand(g.brand_id)
    terms_version = terms_version_service.find_current_version_for_brand(
        g.brand_id
    )

    if terms_version and not consent_service.has_user_consented_to_subject(
                user.id, terms_version.consent_subject_id):
        raise ValidationError(f'Der Benutzer "{user.screen_name}" '
                               'hat die aktuellen AGB noch nicht akzeptiert.')
        user.id, terms_version.consent_subject_id
    ):
        raise ValidationError(
            f'Der Benutzer "{user.screen_name}" '
            'hat die aktuellen AGB noch nicht akzeptiert.'
        )

    field.data = user


M byceps/blueprints/ticketing/notification_service.py => byceps/blueprints/ticketing/notification_service.py +12 -8
@@ 19,8 19,9 @@ from ...services.user.transfer.models import User
def notify_appointed_user(ticket: Ticket, user: User, manager: User) -> None:
    party_title = _get_party_title()

    subject = '{} hat dir Ticket {} zugewiesen.' \
        .format(manager.screen_name, ticket.code)
    subject = '{} hat dir Ticket {} zugewiesen.'.format(
        manager.screen_name, ticket.code
    )

    body = '{} hat dir Ticket {} zugewiesen, was dich zur Teilnahme ' \
        'an der {} berechtigt.' \


@@ 32,8 33,9 @@ def notify_appointed_user(ticket: Ticket, user: User, manager: User) -> None:
def notify_withdrawn_user(ticket: Ticket, user: User, manager: User) -> None:
    party_title = _get_party_title()

    subject = '{} hat Ticket {} zurückgezogen.' \
        .format(manager.screen_name, ticket.code)
    subject = '{} hat Ticket {} zurückgezogen.'.format(
        manager.screen_name, ticket.code
    )

    body = '{} hat das dir bisher zugewiesene Ticket {} für die {} ' \
        'zurückgezogen.' \


@@ 47,8 49,9 @@ def notify_appointed_user_manager(
) -> None:
    party_title = _get_party_title()

    subject = '{} hat dir die Nutzerverwaltung für Ticket {} übertragen.' \
        .format(manager.screen_name, ticket.code)
    subject = '{} hat dir die Nutzerverwaltung für Ticket {} übertragen.'.format(
        manager.screen_name, ticket.code
    )

    body = '{} hat dir die Verwaltung des Nutzers von Ticket {} für die {} ' \
        'übertragen.' \


@@ 78,8 81,9 @@ def notify_appointed_seat_manager(
) -> None:
    party_title = _get_party_title()

    subject = '{} hat dir die Sitzplatzverwaltung für Ticket {} übertragen.' \
        .format(manager.screen_name, ticket.code)
    subject = '{} hat dir die Sitzplatzverwaltung für Ticket {} übertragen.'.format(
        manager.screen_name, ticket.code
    )

    body = '{} hat dir die Verwaltung des Sitzplatzes von Ticket {} ' \
        'für die {} übertragen.' \

M byceps/blueprints/ticketing/views.py => byceps/blueprints/ticketing/views.py +36 -21
@@ 46,12 46,14 @@ def index_mine():
    current_user = g.current_user

    tickets = ticket_service.find_tickets_related_to_user_for_party(
        current_user.id, party.id)
        current_user.id, party.id
    )

    tickets = [ticket for ticket in tickets if not ticket.revoked]

    current_user_uses_any_ticket = find(
        lambda t: t.used_by_id == current_user.id, tickets)
        lambda t: t.used_by_id == current_user.id, tickets
    )

    return {
        'party_title': party.title,


@@ 140,11 142,13 @@ def appoint_user(ticket_id):

    user = form.user.data

    ticket_user_management_service \
        .appoint_user(ticket.id, user.id, manager.id)
    ticket_user_management_service.appoint_user(ticket.id, user.id, manager.id)

    flash_success('{} wurde als Nutzer/in von Ticket {} eingetragen.',
        user.screen_name, ticket.code)
    flash_success(
        '{} wurde als Nutzer/in von Ticket {} eingetragen.',
        user.screen_name,
        ticket.code,
    )

    notification_service.notify_appointed_user(ticket, user, manager)



@@ 164,11 168,13 @@ def withdraw_user(ticket_id):
    if not ticket.is_user_managed_by(manager.id):
        abort(403)

    ticket_user_management_service \
        .appoint_user(ticket.id, manager.id, manager.id)
    ticket_user_management_service.appoint_user(
        ticket.id, manager.id, manager.id
    )

    flash_success('Du wurdest als Nutzer/in von Ticket {} eingetragen.',
        ticket.code)
    flash_success(
        'Du wurdest als Nutzer/in von Ticket {} eingetragen.', ticket.code
    )


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


@@ 215,11 221,15 @@ def appoint_user_manager(ticket_id):

    user = form.user.data

    ticket_user_management_service \
        .appoint_user_manager(ticket.id, user.id, manager.id)
    ticket_user_management_service.appoint_user_manager(
        ticket.id, user.id, manager.id
    )

    flash_success('{} wurde als Nutzer-Verwalter/in von Ticket {} eingetragen.',
        user.screen_name, ticket.code)
    flash_success(
        '{} wurde als Nutzer-Verwalter/in von Ticket {} eingetragen.',
        user.screen_name,
        ticket.code,
    )

    notification_service.notify_appointed_user_manager(ticket, user, manager)



@@ 243,8 253,9 @@ def withdraw_user_manager(ticket_id):

    ticket_user_management_service.withdraw_user_manager(ticket.id, manager.id)

    flash_success('Der Nutzer-Verwalter von Ticket {} wurde entfernt.',
                  ticket.code)
    flash_success(
        'Der Nutzer-Verwalter von Ticket {} wurde entfernt.', ticket.code
    )

    notification_service.notify_withdrawn_user_manager(ticket, user, manager)



@@ 293,12 304,15 @@ def appoint_seat_manager(ticket_id):

    user = form.user.data

    ticket_seat_management_service \
        .appoint_seat_manager(ticket.id, user.id, manager.id)
    ticket_seat_management_service.appoint_seat_manager(
        ticket.id, user.id, manager.id
    )

    flash_success(
        '{} wurde als Sitzplatz-Verwalter/in von Ticket {} eingetragen.',
        user.screen_name, ticket.code)
        user.screen_name,
        ticket.code,
    )

    notification_service.notify_appointed_seat_manager(ticket, user, manager)



@@ 322,8 336,9 @@ def withdraw_seat_manager(ticket_id):

    ticket_seat_management_service.withdraw_seat_manager(ticket.id, manager.id)

    flash_success('Der Sitzplatz-Verwalter von Ticket {} wurde entfernt.',
                  ticket.code)
    flash_success(
        'Der Sitzplatz-Verwalter von Ticket {} wurde entfernt.', ticket.code
    )

    notification_service.notify_withdrawn_seat_manager(ticket, user, manager)


M byceps/blueprints/user/avatar/views.py => byceps/blueprints/user/avatar/views.py +5 -3
@@ 76,8 76,9 @@ def _update(user_id, image):
        abort(400, 'No file to upload has been specified.')

    try:
        avatar_service.update_avatar_image(user_id, image.stream,
                                           ALLOWED_IMAGE_TYPES)
        avatar_service.update_avatar_image(
            user_id, image.stream, ALLOWED_IMAGE_TYPES
        )
    except avatar_service.UnknownUserId as e:
        abort(404)
    except avatar_service.ImageTypeProhibited as e:


@@ 98,7 99,8 @@ def delete():
        # No avatar selected.
        # But that's ok, deletions should be idempotent.
        flash_notice(
            'Es ist kein Avatarbild gesetzt, das entfernt werden könnte.')
            'Es ist kein Avatarbild gesetzt, das entfernt werden könnte.'
        )
    else:
        flash_success('Dein Avatarbild wurde entfernt.')


M byceps/blueprints/user/creation/views.py => byceps/blueprints/user/creation/views.py +51 -23
@@ 43,11 43,13 @@ def create_form(erroneous_form=None):
        flash_error('Das Erstellen von Benutzerkonten ist deaktiviert.')
        abort(403)

    terms_version = terms_version_service \
        .find_current_version_for_brand(g.brand_id)
    terms_version = terms_version_service.find_current_version_for_brand(
        g.brand_id
    )

    privacy_policy_consent_subject_id \
        = _find_privacy_policy_consent_subject_id()
    privacy_policy_consent_subject_id = (
        _find_privacy_policy_consent_subject_id()
    )

    newsletter_list_id = _find_newsletter_list_for_brand()



@@ 65,8 67,13 @@ def create_form(erroneous_form=None):
    form = erroneous_form if erroneous_form \
        else UserCreateForm(terms_version_id=terms_version_id)

    _adjust_create_form(form, real_name_required, terms_consent_required,
                        privacy_policy_consent_required, newsletter_offered)
    _adjust_create_form(
        form,
        real_name_required,
        terms_consent_required,
        privacy_policy_consent_required,
        newsletter_offered,
    )

    return {'form': form}



@@ 78,11 85,13 @@ def create():
        flash_error('Das Erstellen von Benutzerkonten ist deaktiviert.')
        abort(403)

    terms_document_id = terms_document_service \
        .find_document_id_for_brand(g.brand_id)
    terms_document_id = terms_document_service.find_document_id_for_brand(
        g.brand_id
    )

    privacy_policy_consent_subject_id \
        = _find_privacy_policy_consent_subject_id()
    privacy_policy_consent_subject_id = (
        _find_privacy_policy_consent_subject_id()
    )

    newsletter_list_id = _find_newsletter_list_for_brand()



@@ 94,8 103,13 @@ def create():

    form = UserCreateForm(request.form)

    _adjust_create_form(form, real_name_required, terms_consent_required,
                        privacy_policy_consent_required, newsletter_offered)
    _adjust_create_form(
        form,
        real_name_required,
        terms_consent_required,
        privacy_policy_consent_required,
        newsletter_offered,
    )

    if not form.validate():
        return create_form(form)


@@ 108,12 122,14 @@ def create():

    if user_service.is_screen_name_already_assigned(screen_name):
        flash_error(
            'Dieser Benutzername ist bereits einem Benutzerkonto zugeordnet.')
            'Dieser Benutzername ist bereits einem Benutzerkonto zugeordnet.'
        )
        return create_form(form)

    if user_service.is_email_address_already_assigned(email_address):
        flash_error(
            'Diese E-Mail-Adresse ist bereits einem Benutzerkonto zugeordnet.')
            'Diese E-Mail-Adresse ist bereits einem Benutzerkonto zugeordnet.'
        )
        return create_form(form)

    if real_name_required:


@@ 135,14 151,16 @@ def create():
        terms_consent = Consent(
            user_id=None,  # not available at this point
            subject_id=terms_version.consent_subject_id,
            expressed_at=now_utc)
            expressed_at=now_utc,
        )

    privacy_policy_consent = None
    if privacy_policy_consent_required:
        privacy_policy_consent = Consent(
            user_id=None,  # not available at this point
            subject_id=privacy_policy_consent_subject_id,
            expressed_at=now_utc)
            expressed_at=now_utc,
        )

    newsletter_subscription = None
    if newsletter_offered:


@@ 151,24 169,34 @@ def create():
            newsletter_subscription = NewsletterSubscription(
                user_id=None,  # not available at this point
                list_id=newsletter_list_id,
                expressed_at=now_utc)
                expressed_at=now_utc,
            )

    try:
        user = user_creation_service.create_user(
            screen_name, email_address, password, first_names, last_name,
            g.site_id, terms_consent=terms_consent,
            screen_name,
            email_address,
            password,
            first_names,
            last_name,
            g.site_id,
            terms_consent=terms_consent,
            privacy_policy_consent=privacy_policy_consent,
            newsletter_subscription=newsletter_subscription)
            newsletter_subscription=newsletter_subscription,
        )
    except user_creation_service.UserCreationFailed:
        flash_error('Das Benutzerkonto für "{}" konnte nicht angelegt werden.',
                    screen_name)
        flash_error(
            'Das Benutzerkonto für "{}" konnte nicht angelegt werden.',
            screen_name,
        )
        return create_form(form)

    flash_success(
        'Das Benutzerkonto für "{}" wurde angelegt. '
        'Bevor du dich damit anmelden kannst, muss zunächst der Link in '
        'der an die angegebene Adresse verschickten E-Mail besucht werden.',
        user.screen_name)
        user.screen_name,
    )
    signals.account_created.send(None, user_id=user.id)

    return redirect_to('authentication.login_form')

M byceps/blueprints/user/current/views.py => byceps/blueprints/user/current/views.py +13 -5
@@ 44,7 44,8 @@ def view():
        newsletter_offered = (newsletter_list_id is not None)

        subscribed_to_newsletter = newsletter_service.is_subscribed(
            user.id, newsletter_list_id)
            user.id, newsletter_list_id
        )
    else:
        newsletter_list_id = None
        newsletter_offered = False


@@ 112,10 113,17 @@ def details_update():
    street = form.street.data.strip()
    phone_number = form.phone_number.data.strip()

    user_command_service.update_user_details(current_user.id, first_names,
                                             last_name, date_of_birth, country,
                                             zip_code, city, street,
                                             phone_number)
    user_command_service.update_user_details(
        current_user.id,
        first_names,
        last_name,
        date_of_birth,
        country,
        zip_code,
        city,
        street,
        phone_number,
    )

    flash_success('Deine Daten wurden gespeichert.')
    return redirect_to('.view')

M byceps/blueprints/user/email_address/views.py => byceps/blueprints/user/email_address/views.py +14 -8
@@ 57,19 57,23 @@ def request_confirmation_email():
        flash_notice(
            'Die E-Mail-Adresse für den Benutzernamen "{}" wurde bereits '
            'bestätigt.',
            user.screen_name)
            user.screen_name,
        )
        return request_confirmation_email_form()

    verification_token = verification_token_service \
        .find_or_create_for_email_address_confirmation(user.id)
    verification_token = verification_token_service.find_or_create_for_email_address_confirmation(
        user.id
    )

    email_address_confirmation_service.send_email_address_confirmation_email(
        user.email_address, user.screen_name, verification_token, g.site_id)
        user.email_address, user.screen_name, verification_token, g.site_id
    )

    flash_success(
        'Der Link zur Bestätigung der für den Benutzernamen "{}" '
        'hinterlegten E-Mail-Adresse wurde erneut versendet.',
        user.screen_name)
        user.screen_name,
    )

    return redirect_to('.request_confirmation_email_form')



@@ 79,8 83,9 @@ def confirm(token):
    """Confirm e-mail address of the user account assigned with the
    verification token.
    """
    verification_token = verification_token_service \
        .find_for_email_address_confirmation_by_token(token)
    verification_token = verification_token_service.find_for_email_address_confirmation_by_token(
        token
    )

    if verification_token is None:
        abort(404)


@@ 98,7 103,8 @@ def confirm(token):

    flash_success(
        'Die E-Mail-Adresse wurde bestätigt. Das Benutzerkonto "{}" ist nun aktiviert.',
        user.screen_name)
        user.screen_name,
    )
    signals.email_address_confirmed.send(None, user_id=user.id)

    return redirect_to('authentication.login_form')

M byceps/blueprints/user/profile/views.py => byceps/blueprints/user/profile/views.py +6 -4
@@ 31,11 31,13 @@ def view(user_id):

    badges_with_awarding_quantity = badge_service.get_badges_for_user(user.id)

    orga_team_membership = orga_team_service.find_membership_for_party(user.id,
        g.party_id)
    orga_team_membership = orga_team_service.find_membership_for_party(
        user.id, g.party_id
    )

    _current_party_tickets = ticket_service.find_tickets_used_by_user(user.id,
        g.party_id)
    _current_party_tickets = ticket_service.find_tickets_used_by_user(
        user.id, g.party_id
    )
    current_party_tickets = [t for t in _current_party_tickets if not t.revoked]

    attended_parties = attendance_service.get_attended_parties(user.id)

M byceps/blueprints/user_badge/views.py => byceps/blueprints/user_badge/views.py +4 -2
@@ 40,8 40,10 @@ def view(slug):
    awardings = badge_service.get_awardings_of_badge(badge.id)
    recipient_ids = [awarding.user_id for awarding in awardings]
    recipients = user_service.find_users(
        recipient_ids, include_avatars=True,
        include_orga_flags_for_party_id=g.party_id)
        recipient_ids,
        include_avatars=True,
        include_orga_flags_for_party_id=g.party_id,
    )

    return {
        'badge': badge,

M byceps/blueprints/user_group/views.py => byceps/blueprints/user_group/views.py +4 -2
@@ 37,7 37,8 @@ def create_form(erroneous_form=None):
    """Show a form to create a group."""
    if not g.current_user.is_active:
        flash_error(
            'Du musst angemeldet sein, um eine Benutzergruppe erstellen zu können.')
            'Du musst angemeldet sein, um eine Benutzergruppe erstellen zu können.'
        )
        return redirect_to('.index')

    form = erroneous_form if erroneous_form else CreateForm()


@@ 52,7 53,8 @@ def create():
    """Create a group."""
    if not g.current_user.is_active:
        flash_error(
            'Du musst angemeldet sein, um eine Benutzergruppe erstellen zu können.')
            'Du musst angemeldet sein, um eine Benutzergruppe erstellen zu können.'
        )
        return redirect_to('.index')

    form = CreateForm(request.form)

M byceps/blueprints/user_message/views.py => byceps/blueprints/user_message/views.py +8 -5
@@ 52,14 52,17 @@ def create(recipient_id):

    sender = g.current_user
    body = form.body.data.strip()
    sender_contact_url = url_for('.create_form', recipient_id=sender.id,
                                 _external=True)
    sender_contact_url = url_for(
        '.create_form', recipient_id=sender.id, _external=True
    )

    user_message_service.send_message(sender.id, recipient.id, body,
                                      sender_contact_url, g.site_id)
    user_message_service.send_message(
        sender.id, recipient.id, body, sender_contact_url, g.site_id
    )

    flash_success(
        'Deine Nachricht an {} wurde versendet.'.format(recipient.screen_name))
        'Deine Nachricht an {} wurde versendet.'.format(recipient.screen_name)
    )

    return redirect_to('user_profile.view', user_id=recipient.id)


M byceps/config.py => byceps/config.py +16 -11
@@ 43,19 43,24 @@ def init_app(app):
        site_id = determine_site_id(app)
        update_extension_value(app, KEY_SITE_ID, site_id)

    user_registration_enabled = determine_user_registration_enabled(app,
                                                                    site_mode)
    update_extension_value(app, KEY_USER_REGISTRATION_ENABLED,
                           user_registration_enabled)

    ticket_management_enabled = determine_ticket_management_enabled(app,
                                                                    site_mode)
    update_extension_value(app, KEY_TICKET_MANAGEMENT_ENABLED,
                           ticket_management_enabled)
    user_registration_enabled = determine_user_registration_enabled(
        app, site_mode
    )
    update_extension_value(
        app, KEY_USER_REGISTRATION_ENABLED, user_registration_enabled
    )

    ticket_management_enabled = determine_ticket_management_enabled(
        app, site_mode
    )
    update_extension_value(
        app, KEY_TICKET_MANAGEMENT_ENABLED, ticket_management_enabled
    )

    seat_management_enabled = determine_seat_management_enabled(app, site_mode)
    update_extension_value(app, KEY_SEAT_MANAGEMENT_ENABLED,
                           seat_management_enabled)
    update_extension_value(
        app, KEY_SEAT_MANAGEMENT_ENABLED, seat_management_enabled
    )


def update_extension_value(app, key, value):

M byceps/email.py => byceps/email.py +2 -5
@@ 63,11 63,8 @@ def send(
    mailer = current_app.marrowmailer

    message = mailer.new(
        author=sender,
        to=recipients,
        subject=subject,
        plain=body,
        brand=False)
        author=sender, to=recipients, subject=subject, plain=body, brand=False
    )

    mailer.start()
    mailer.send(message)

M byceps/services/authentication/password/reset_service.py => byceps/services/authentication/password/reset_service.py +11 -7
@@ 27,16 27,20 @@ def prepare_password_reset(
    """Create a verification token for password reset and email it to
    the user's address.
    """
    verification_token = verification_token_service \
        .create_for_password_reset(user.id)
    verification_token = verification_token_service.create_for_password_reset(
        user.id
    )

    confirmation_url = url_for('authentication.password_reset_form',
                               token=verification_token.token,
                               _external=True)
    confirmation_url = url_for(
        'authentication.password_reset_form',
        token=verification_token.token,
        _external=True,
    )

    recipients = [user.email_address]
    subject = '{0.screen_name}, so kannst du ein neues Passwort festlegen' \
        .format(user)
    subject = '{0.screen_name}, so kannst du ein neues Passwort festlegen'.format(
        user
    )
    body = (
        'Hallo {0.screen_name},\n\n'
        'du kannst ein neues Passwort festlegen, indem du diese URL abrufst: {1}'

M byceps/services/board/category_command_service.py => byceps/services/board/category_command_service.py +2 -1
@@ 99,7 99,8 @@ def delete_category(category_id: CategoryID) -> None:
    if topic_ids:
        raise ValueError(
            f'Category "{category.title}" in board "{category.board_id}" '
            'contains topics. It will not be deleted because of that.')
            'contains topics. It will not be deleted because of that.'
        )

    db.session.delete(category)
    db.session.commit()

M byceps/services/board/models/posting.py => byceps/services/board/models/posting.py +3 -1
@@ 105,7 105,9 @@ class Posting(db.Model):
            .add('topic', self.topic.title)

        if self.hidden:
            builder.add_custom('hidden by {}'.format(self.hidden_by.screen_name))
            builder.add_custom(
                'hidden by {}'.format(self.hidden_by.screen_name)
            )

        return builder.build()


M byceps/services/board/models/topic.py => byceps/services/board/models/topic.py +12 -5
@@ 96,8 96,9 @@ class Topic(db.Model):

    def count_pages(self, postings_per_page: int) -> int:
        """Return the number of pages this topic spans."""
        full_page_count, remaining_postings = divmod(self.posting_count,
                                                     postings_per_page)
        full_page_count, remaining_postings = divmod(
            self.posting_count, postings_per_page
        )
        if remaining_postings > 0:
            return full_page_count + 1
        else:


@@ 114,12 115,18 @@ class Topic(db.Model):
            .add_with_lookup('title')

        if self.hidden:
            builder.add_custom('hidden by {}'.format(self.hidden_by.screen_name))
            builder.add_custom(
                'hidden by {}'.format(self.hidden_by.screen_name)
            )

        if self.locked:
            builder.add_custom('locked by {}'.format(self.locked_by.screen_name))
            builder.add_custom(
                'locked by {}'.format(self.locked_by.screen_name)
            )

        if self.pinned:
            builder.add_custom('pinned by {}'.format(self.pinned_by.screen_name))
            builder.add_custom(
                'pinned by {}'.format(self.pinned_by.screen_name)
            )

        return builder.build()

M byceps/services/board/posting_query_service.py => byceps/services/board/posting_query_service.py +3 -2
@@ 65,8 65,9 @@ def paginate_postings(
def _get_users_by_id(
    user_ids: Set[UserID], party_id: Optional[PartyID]
) -> Dict[UserID, User]:
    users = user_service.find_users(user_ids, include_avatars=True,
                                    include_orga_flags_for_party_id=party_id)
    users = user_service.find_users(
        user_ids, include_avatars=True, include_orga_flags_for_party_id=party_id
    )
    return user_service.index_users_by_id(users)



M byceps/services/board/topic_command_service.py => byceps/services/board/topic_command_service.py +3 -2
@@ 25,8 25,9 @@ def create_topic(
    """Create a topic with an initial posting in that category."""
    topic = DbTopic(category_id, creator_id, title)
    posting = DbPosting(topic, creator_id, body)
    initial_topic_posting_association = InitialTopicPostingAssociation(topic,
                                                                       posting)
    initial_topic_posting_association = InitialTopicPostingAssociation(
        topic, posting
    )

    db.session.add(topic)
    db.session.add(posting)

M byceps/services/image/service.py => byceps/services/image/service.py +4 -2
@@ 33,8 33,10 @@ def determine_image_type(
        allowed_type_names_string = ', '.join(sorted(allowed_type_names))

        raise ImageTypeProhibited(
            'Image is not one of the allowed types ({}).'
            .format(allowed_type_names_string))
            'Image is not one of the allowed types ({}).'.format(
                allowed_type_names_string
            )
        )

    stream.seek(0)
    return image_type

M byceps/services/metrics/service.py => byceps/services/metrics/service.py +55 -35
@@ 60,20 60,25 @@ def _collect_board_metrics(brand_ids: List[BrandID]) -> Iterator[Metric]:
        for board_id in board_ids:
            labels = [Label('board', board_id)]

            topic_count = board_topic_query_service \
                .count_topics_for_board(board_id)
            topic_count = board_topic_query_service.count_topics_for_board(
                board_id
            )
            yield Metric('board_topic_count', topic_count, labels=labels)

            posting_count = board_posting_query_service \
                .count_postings_for_board(board_id)
            posting_count = board_posting_query_service.count_postings_for_board(
                board_id
            )
            yield Metric('board_posting_count', posting_count, labels=labels)


def _collect_consent_metrics() -> Iterator[Metric]:
    consents_per_subject = consent_service.count_consents_by_subject()
    for subject_name, consent_count in consents_per_subject.items():
        yield Metric('consent_count', consent_count,
                     labels=[Label('subject_name', subject_name)])
        yield Metric(
            'consent_count',
            consent_count,
            labels=[Label('subject_name', subject_name)],
        )


def _collect_shop_ordered_article_metrics(


@@ 83,27 88,34 @@ def _collect_shop_ordered_article_metrics(
    stats = shop_article_service.sum_ordered_articles_by_payment_state(shop_ids)

    for shop_id, article_number, description, payment_state, quantity in stats:
        yield Metric('shop_ordered_article_quantity', quantity,
                     labels=[
                         Label('shop', shop_id),
                         Label('article_number', article_number),
                         Label('description', description),
                         Label('payment_state', payment_state.name),
                     ])
        yield Metric(
            'shop_ordered_article_quantity',
            quantity,
            labels=[
                Label('shop', shop_id),
                Label('article_number', article_number),
                Label('description', description),
                Label('payment_state', payment_state.name),
            ],
        )


def _collect_shop_order_metrics(shops: List[Shop]) -> Iterator[Metric]:
    """Provide order counts grouped by payment state for shops."""
    for shop in shops:
        order_counts_per_payment_state = order_service \
            .count_orders_per_payment_state(shop.id)
        order_counts_per_payment_state = order_service.count_orders_per_payment_state(
            shop.id
        )

        for payment_state, quantity in order_counts_per_payment_state.items():
            yield Metric('shop_order_quantity', quantity,
                         labels=[
                             Label('shop', shop.id),
                             Label('payment_state', payment_state.name),
                         ])
            yield Metric(
                'shop_order_quantity',
                quantity,
                labels=[
                    Label('shop', shop.id),
                    Label('payment_state', payment_state.name),
                ],
            )


def _collect_seating_metrics(


@@ 111,15 123,19 @@ def _collect_seating_metrics(
) -> Iterator[Metric]:
    """Provide seat occupation counts per party and category."""
    for party_id in active_party_ids:
        occupied_seat_counts_by_category = seat_service \
            .count_occupied_seats_by_category(party_id)
        occupied_seat_counts_by_category = seat_service.count_occupied_seats_by_category(
            party_id
        )

        for category, count in occupied_seat_counts_by_category:
            yield Metric('occupied_seat_count', count,
                         labels=[
                             Label('party', party_id),
                             Label('category_title', category.title),
                         ])
            yield Metric(
                'occupied_seat_count',
                count,
                labels=[
                    Label('party', party_id),
                    Label('category_title', category.title),
                ],
            )


def _collect_ticket_metrics(active_parties: List[Party]) -> Iterator[Metric]:


@@ 132,18 148,22 @@ def _collect_ticket_metrics(active_parties: List[Party]) -> Iterator[Metric]:
        if max_ticket_quantity is not None:
            yield Metric('tickets_max', max_ticket_quantity, labels=labels)

        tickets_revoked_count = ticket_service \
            .count_revoked_tickets_for_party(party_id)
        yield Metric('tickets_revoked_count', tickets_revoked_count,
                     labels=labels)
        tickets_revoked_count = ticket_service.count_revoked_tickets_for_party(
            party_id
        )
        yield Metric(
            'tickets_revoked_count', tickets_revoked_count, labels=labels
        )

        tickets_sold_count = ticket_service.count_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(party_id)
        yield Metric('tickets_checked_in_count', tickets_checked_in_count,
                     labels=labels)
        tickets_checked_in_count = ticket_service.count_tickets_checked_in_for_party(
            party_id
        )
        yield Metric(
            'tickets_checked_in_count', tickets_checked_in_count, labels=labels
        )


def _collect_user_metrics() -> Iterator[Metric]:

M byceps/services/news/image_service.py => byceps/services/news/image_service.py +3 -2
@@ 24,8 24,9 @@ def create_image(
    caption: Optional[str] = None,
) -> Image:
    """Create an image for a news item."""
    image = DbImage(creator_id, item_id, filename, alt_text=alt_text,
                    caption=caption)
    image = DbImage(
        creator_id, item_id, filename, alt_text=alt_text, caption=caption
    )

    db.session.add(image)
    db.session.commit()

M byceps/services/news/service.py => byceps/services/news/service.py +9 -8
@@ 42,8 42,9 @@ def create_item(
    item = DbItem(brand_id, slug)
    db.session.add(item)

    version = _create_version(item, creator_id, title, body,
                              image_url_path=image_url_path)
    version = _create_version(
        item, creator_id, title, body, image_url_path=image_url_path
    )
    db.session.add(version)

    current_version_association = DbCurrentVersionAssociation(item, version)


@@ 71,8 72,9 @@ def update_item(
    item.slug = slug
    db.session.add(item)

    version = _create_version(item, creator_id, title, body,
                              image_url_path=image_url_path)
    version = _create_version(
        item, creator_id, title, body, image_url_path=image_url_path
    )
    db.session.add(version)

    item.current_version = version


@@ 271,7 273,6 @@ def _assemble_image_url(item: DbItem) -> Optional[str]:
        return None

    filename = 'news/{}'.format(url_path)
    return url_for('brand_file',
                   filename=filename,
                   _method='GET',
                   _external=True)
    return url_for(
        'brand_file', filename=filename, _method='GET', _external=True
    )

M byceps/services/newsletter/command_service.py => byceps/services/newsletter/command_service.py +9 -6
@@ 33,16 33,18 @@ def create_list(list_id: ListID, title: str) -> List:

def subscribe(user_id: UserID, list_id: ListID, expressed_at: datetime) -> None:
    """Subscribe the user to that list."""
    _update_subscription_state(user_id, list_id, expressed_at,
                               SubscriptionState.requested)
    _update_subscription_state(
        user_id, list_id, expressed_at, SubscriptionState.requested
    )


def unsubscribe(
    user_id: UserID, list_id: ListID, expressed_at: datetime
) -> None:
    """Unsubscribe the user from that list."""
    _update_subscription_state(user_id, list_id, expressed_at,
                               SubscriptionState.declined)
    _update_subscription_state(
        user_id, list_id, expressed_at, SubscriptionState.declined
    )


def _update_subscription_state(


@@ 56,8 58,9 @@ def _update_subscription_state(
    if list_ is None:
        raise UnknownListId(list_id)

    subscription_update = DbSubscriptionUpdate(user_id, list_.id, expressed_at,
                                               state)
    subscription_update = DbSubscriptionUpdate(
        user_id, list_.id, expressed_at, state
    )

    db.session.add(subscription_update)
    db.session.commit()

M byceps/services/orga/birthday_service.py => byceps/services/orga/birthday_service.py +10 -6
@@ 34,8 34,9 @@ def collect_orgas_with_next_birthdays(

    user_ids = {user.id for user in orgas}

    avatar_urls_by_user_id = user_avatar_service \
        .get_avatar_urls_for_users(user_ids)
    avatar_urls_by_user_id = user_avatar_service.get_avatar_urls_for_users(
        user_ids
    )

    for user in orgas:
        avatar_url = avatar_urls_by_user_id.get(user.id)


@@ 63,7 64,10 @@ def _collect_orgas_with_birthdays() -> Sequence[DbUser]:


def sort_users_by_next_birthday(users: Sequence[DbUser]) -> Sequence[DbUser]:
    return sorted(users,
                  key=lambda user: (
                    user.detail.days_until_next_birthday,
                    -user.detail.age))
    return sorted(
        users,
        key=lambda user: (
            user.detail.days_until_next_birthday,
            -user.detail.age,
        ),
    )

M byceps/services/party/service.py => byceps/services/party/service.py +9 -2
@@ 34,8 34,15 @@ def create_party(
    shop_id: Optional[ShopID] = None,
) -> Party:
    """Create a party."""
    party = DbParty(party_id, brand_id, title, starts_at, ends_at,
                    max_ticket_quantity=max_ticket_quantity, shop_id=shop_id)
    party = DbParty(
        party_id,
        brand_id,
        title,
        starts_at,
        ends_at,
        max_ticket_quantity=max_ticket_quantity,
        shop_id=shop_id,
    )

    db.session.add(party)
    db.session.commit()

M byceps/services/seating/seat_group_service.py => byceps/services/seating/seat_group_service.py +3 -2
@@ 125,8 125,9 @@ def _ensure_actual_quantities_match(
) -> None:
    """Raise an error if the totals of seats and tickets don't match."""
    if len(seats) != len(tickets):
        raise ValueError('The actual quantities of seats and tickets '
                         'do not match.')
        raise ValueError(
            'The actual quantities of seats and tickets ' 'do not match.'
        )


def _occupy_seats(seats: Sequence[DbSeat], tickets: Sequence[DbTicket]) -> None:

M byceps/services/shop/article/models/compilation.py => byceps/services/shop/article/models/compilation.py +2 -1
@@ 17,7 17,8 @@ class ArticleCompilationItem:
    ) -> None:
        if (fixed_quantity is not None) and fixed_quantity < 1:
            raise ValueError(
                'Fixed quantity, if given, must be a positive number.')
                'Fixed quantity, if given, must be a positive number.'
            )

        self.article = article
        self.fixed_quantity = fixed_quantity

M byceps/services/shop/article/service.py => byceps/services/shop/article/service.py +8 -5
@@ 196,7 196,8 @@ def get_article_compilation_for_single_article(
    compilation = ArticleCompilation()

    compilation.append(
        ArticleCompilationItem(article, fixed_quantity=fixed_quantity))
        ArticleCompilationItem(article, fixed_quantity=fixed_quantity)
    )

    _add_attached_articles(compilation, article.attached_articles)



@@ 210,8 211,11 @@ def _add_attached_articles(
    """Add the attached articles to the compilation."""
    for attached_article in attached_articles:
        compilation.append(
            ArticleCompilationItem(attached_article.article,
                                   fixed_quantity=attached_article.quantity))
            ArticleCompilationItem(
                attached_article.article,
                fixed_quantity=attached_article.quantity,
            )
        )


def get_attachable_articles(article: DbArticle) -> Sequence[DbArticle]:


@@ 279,7 283,6 @@ def sum_ordered_articles_by_payment_state(
                key = (shop_id, article_number, description, payment_state)
                quantity = quantities.get(key, 0)

                yield shop_id, article_number, description, payment_state, \
                    quantity
                yield shop_id, article_number, description, payment_state, quantity

    return list(generate())

M byceps/services/shop/order/action_registry_service.py => byceps/services/shop/order/action_registry_service.py +18 -6
@@ 38,14 38,22 @@ def register_ticket_bundles_creation(
        'category_id': str(ticket_category_id),
        'ticket_quantity': ticket_quantity,
    }
    action_service.create_action(article_number, PaymentState.paid,
                                 'create_ticket_bundles', params_create)
    action_service.create_action(
        article_number,
        PaymentState.paid,
        'create_ticket_bundles',
        params_create,
    )

    # Revoke ticket bundles that have been created for the order when it
    # is canceled after being marked as paid.
    params_revoke: Parameters = {}
    action_service.create_action(article_number, PaymentState.canceled_after_paid,
                                 'revoke_ticket_bundles', params_revoke)
    action_service.create_action(
        article_number,
        PaymentState.canceled_after_paid,
        'revoke_ticket_bundles',
        params_revoke,
    )


def register_tickets_creation(


@@ 61,5 69,9 @@ def register_tickets_creation(
    # Revoke tickets that have been created for the order when it is
    # canceled after being marked as paid.
    params_revoke: Parameters = {}
    action_service.create_action(article_number, PaymentState.canceled_after_paid,
                                 'revoke_tickets', params_revoke)
    action_service.create_action(
        article_number,
        PaymentState.canceled_after_paid,
        'revoke_tickets',
        params_revoke,
    )

M byceps/services/shop/order/action_service.py => byceps/services/shop/order/action_service.py +4 -2
@@ 123,7 123,9 @@ def _get_procedure(name: str, article_number: ArticleNumber) -> OrderActionType:

    if procedure is None:
        raise Exception(
            "Unknown procedure '{}' configured for article number '{}'."
                .format(name, article_number))
            "Unknown procedure '{}' configured for article number '{}'.".format(
                name, article_number
            )
        )

    return procedure

M byceps/services/shop/order/actions/create_ticket_bundles.py => byceps/services/shop/order/actions/create_ticket_bundles.py +2 -2
@@ 33,8 33,8 @@ def create_ticket_bundles(

    for _ in range(bundle_quantity):
        bundle = ticket_bundle_service.create_bundle(
            category_id, ticket_quantity, owned_by_id,
            order_number=order_number)
            category_id, ticket_quantity, owned_by_id, order_number=order_number
        )

        for ticket in bundle.tickets:
            ticket.used_by_id = owned_by_id

M byceps/services/shop/order/actions/create_tickets.py => byceps/services/shop/order/actions/create_tickets.py +3 -3
@@ 32,9 32,9 @@ def create_tickets(
    owned_by_id = order.placed_by_id
    order_number = order.order_number

    tickets = ticket_creation_service \
        .create_tickets(category_id, owned_by_id, quantity,
                        order_number=order_number)
    tickets = ticket_creation_service.create_tickets(
        category_id, owned_by_id, quantity, order_number=order_number
    )

    for ticket in tickets:
        ticket.used_by_id = owned_by_id

M byceps/services/shop/order/email/service.py => byceps/services/shop/order/email/service.py +37 -17
@@ 68,15 68,21 @@ def _assemble_email_for_incoming_order_to_orderer(
) -> Message:
    order = data.order

    subject = 'Deine Bestellung ({}) ist eingegangen.' \
        .format(order.order_number)
    subject = 'Deine Bestellung ({}) ist eingegangen.'.format(
        order.order_number
    )
    template_name = 'order_placed.txt'
    template_context = _get_template_context(data)
    template_context['payment_instructions'] = _get_payment_instructions(order)
    recipient_address = data.orderer_email_address

    return _assemble_email_to_orderer(subject, template_name, template_context,
                                      data.email_config_id, recipient_address)
    return _assemble_email_to_orderer(
        subject,
        template_name,
        template_context,
        data.email_config_id,
        recipient_address,
    )


def _get_payment_instructions(order: Order) -> str:


@@ 89,25 95,37 @@ def _get_payment_instructions(order: Order) -> str:
def _assemble_email_for_canceled_order_to_orderer(
    data: OrderEmailData
) -> Message:
    subject = '\u274c Deine Bestellung ({}) wurde storniert.' \
        .format(data.order.order_number)
    subject = '\u274c Deine Bestellung ({}) wurde storniert.'.format(
        data.order.order_number
    )
    template_name = 'order_canceled.txt'
    template_context = _get_template_context(data)
    recipient_address = data.orderer_email_address

    return _assemble_email_to_orderer(subject, template_name, template_context,
                                      data.email_config_id, recipient_address)
    return _assemble_email_to_orderer(
        subject,
        template_name,
        template_context,
        data.email_config_id,
        recipient_address,
    )


def _assemble_email_for_paid_order_to_orderer(data: OrderEmailData) -> Message:
    subject = '\u2705 Deine Bestellung ({}) ist bezahlt worden.' \
        .format(data.order.order_number)
    subject = '\u2705 Deine Bestellung ({}) ist bezahlt worden.'.format(
        data.order.order_number
    )
    template_name = 'order_paid.txt'
    template_context = _get_template_context(data)
    recipient_address = data.orderer_email_address

    return _assemble_email_to_orderer(subject, template_name, template_context,
                                      data.email_config_id, recipient_address)
    return _assemble_email_to_orderer(
        subject,
        template_name,
        template_context,
        data.email_config_id,
        recipient_address,
    )


def _get_order_email_data(order_id: OrderID) -> OrderEmailData:


@@ 164,7 182,8 @@ def _get_sender_address(email_config_id: str) -> Optional[Sender]:

    if not config:
        current_app.logger.warning(
            'No e-mail sender configured for ID "%s".', email_config_id)
            'No e-mail sender configured for ID "%s".', email_config_id
        )

    return config.sender



@@ 172,8 191,9 @@ def _get_sender_address(email_config_id: str) -> Optional[Sender]:
def _get_snippet_body(shop_id: ShopID, name: str) -> str:
    scope = Scope('shop', str(shop_id))

    version = snippet_service \
        .find_current_version_of_snippet_with_name(scope, name)
    version = snippet_service.find_current_version_of_snippet_with_name(
        scope, name
    )

    if not version:
        raise SnippetNotFound(scope, name)


@@ 183,8 203,8 @@ def _get_snippet_body(shop_id: ShopID, name: str) -> str:

def _render_template(name: str, **context: Dict[str, Any]) -> str:
    templates_path = os.path.join(
        current_app.root_path,
        'services/shop/order/email/templates')
        current_app.root_path, 'services/shop/order/email/templates'
    )

    loader = FileSystemLoader(templates_path)


M byceps/services/shop/order/service.py => byceps/services/shop/order/service.py +12 -7
@@ 53,13 53,16 @@ def place_order(

    order_number = sequence_service.generate_order_number(shop.id)

    order = _build_order(shop.id, order_number, orderer, payment_method,
                         created_at)
    order = _build_order(
        shop.id, order_number, orderer, payment_method, created_at
    )

    order_items = list(_add_items_from_cart_to_order(cart, order))
    order.total_amount = cart.calculate_total_amount()

    order.shipping_required = any(item.shipping_required for item in order_items)
    order.shipping_required = any(
        item.shipping_required for item in order_items
    )

    db.session.add(order)
    db.session.add_all(order_items)


@@ 247,8 250,9 @@ def cancel_order(order_id: OrderID, initiator_id: UserID, reason: str) -> None:

    db.session.commit()

    action_service.execute_actions(order.to_transfer_object(), payment_state_to,
                                   initiator_id)
    action_service.execute_actions(
        order.to_transfer_object(), payment_state_to, initiator_id
    )


def mark_order_as_paid(


@@ 284,8 288,9 @@ def mark_order_as_paid(

    db.session.commit()

    action_service.execute_actions(order.to_transfer_object(), payment_state_to,
                                   initiator_id)
    action_service.execute_actions(
        order.to_transfer_object(), payment_state_to, initiator_id
    )


def _update_payment_state(

M byceps/services/shop/sequence/service.py => byceps/services/shop/sequence/service.py +4 -2
@@ 62,8 62,10 @@ def _get_next_sequence_step(

    if sequence is None:
        raise NumberGenerationFailed(
            'No sequence configured for shop "{}" and purpose "{}".'
            .format(shop_id, purpose.name))
            'No sequence configured for shop "{}" and purpose "{}".'.format(
                shop_id, purpose.name
            )
        )

    sequence.value = DbNumberSequence.value + 1
    db.session.commit()

M byceps/services/shop/shipping/service.py => byceps/services/shop/shipping/service.py +8 -4
@@ 32,13 32,17 @@ def get_articles_to_ship(shop_id: ShopID) -> Sequence[ArticleToShip]:
    }

    order_item_quantities = list(
        _find_order_items(shop_id, relevant_order_payment_states))
        _find_order_items(shop_id, relevant_order_payment_states)
    )

    article_numbers = {item.article_number for item in order_item_quantities}
    article_descriptions = _get_article_descriptions(article_numbers)

    articles_to_ship = list(_aggregate_ordered_article_quantites(
        order_item_quantities, article_descriptions))
    articles_to_ship = list(
        _aggregate_ordered_article_quantites(
            order_item_quantities, article_descriptions
        )
    )

    articles_to_ship.sort(key=lambda a: a.article_number)



@@ 103,7 107,7 @@ def _aggregate_ordered_article_quantites(
            description,
            quantity_paid,
            quantity_open,
            quantity_total = quantity_paid + quantity_open
            quantity_total=quantity_paid + quantity_open,
        )



M byceps/services/site/service.py => byceps/services/site/service.py +3 -1
@@ 28,7 28,9 @@ def create_site(
    party_id: Optional[PartyID] = None,
) -> Site:
    """Create a site for that party."""
    site = DbSite(site_id, title, server_name, email_config_id, party_id=party_id)
    site = DbSite(
        site_id, title, server_name, email_config_id, party_id=party_id
    )

    db.session.add(site)
    db.session.commit()

M byceps/services/snippet/service.py => byceps/services/snippet/service.py +22 -11
@@ 30,9 30,16 @@ def create_document(
    image_url_path: Optional[str] = None,
) -> SnippetVersion:
    """Create a document and its initial version, and return that version."""
    return _create_snippet(scope, name, SnippetType.document, creator_id, body,
                           title=title, head=head,
                           image_url_path=image_url_path)
    return _create_snippet(
        scope,
        name,
        SnippetType.document,
        creator_id,
        body,
        title=title,
        head=head,
        image_url_path=image_url_path,
    )


def update_document(


@@ 45,8 52,9 @@ def update_document(
    image_url_path: Optional[str] = None,
) -> SnippetVersion:
    """Update document with a new version, and return that version."""
    return _update_snippet(document, creator_id, title, head, body,
                           image_url_path)
    return _update_snippet(
        document, creator_id, title, head, body, image_url_path
    )


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


@@ 68,8 76,9 @@ def update_fragment(
    head = None
    image_url_path = None

    return _update_snippet(fragment, creator_id, title, head, body,
                           image_url_path)
    return _update_snippet(
        fragment, creator_id, title, head, body, image_url_path
    )


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


@@ 91,8 100,9 @@ def _create_snippet(
    snippet = Snippet(scope, name, type_)
    db.session.add(snippet)

    version = SnippetVersion(snippet, creator_id, title, head, body,
                             image_url_path)
    version = SnippetVersion(
        snippet, creator_id, title, head, body, image_url_path
    )
    db.session.add(version)

    current_version_association = CurrentVersionAssociation(snippet, version)


@@ 112,8 122,9 @@ def _update_snippet(
    image_url_path: Optional[str],
) -> SnippetVersion:
    """Update snippet with a new version, and return that version."""
    version = SnippetVersion(snippet, creator_id, title, head, body,
                             image_url_path)
    version = SnippetVersion(
        snippet, creator_id, title, head, body, image_url_path
    )
    db.session.add(version)

    snippet.current_version = version

M byceps/services/terms/document_service.py => byceps/services/terms/document_service.py +3 -1
@@ 42,7 42,9 @@ def set_current_version(document_id: DocumentID, version_id: VersionID) -> None:
    """Specify the current version of the document."""
    document = DbDocument.query.get(document_id)
    if document is None:
        raise ValueError(f'Unknown terms of service document ID "{document_id}"')
        raise ValueError(
            f'Unknown terms of service document ID "{document_id}"'
        )

    version = DbVersion.query.get(version_id)
    if version is None:

M byceps/services/terms/version_service.py => byceps/services/terms/version_service.py +7 -4
@@ 27,8 27,9 @@ def create_version(
    consent_subject_id: ConsentSubjectID,
) -> DbVersion:
    """Create a new version of that document."""
    version = DbVersion(document_id, title, snippet_version_id,
                        consent_subject_id)
    version = DbVersion(
        document_id, title, snippet_version_id, consent_subject_id
    )

    db.session.add(version)
    db.session.commit()


@@ 76,11 77,13 @@ def find_current_version_for_brand(brand_id: BrandID) -> Optional[DbVersion]:
    if not document:
        raise ValueError(
            f'Unknown document ID "{document_id}" configured '
            f'for brand ID "{brand_id}".')
            f'for brand ID "{brand_id}".'
        )

    if document.current_version_id is None:
        raise ValueError(
            f'No current version specified for document ID "{document_id}".')
            f'No current version specified for document ID "{document_id}".'
        )

    return find_version(document.current_version_id)


M byceps/services/text_diff/service.py => byceps/services/text_diff/service.py +8 -3
@@ 31,9 31,14 @@ def create_html_diff(
    from_lines = from_text.split('\n')
    to_lines = to_text.split('\n')

    return HtmlDiff().make_table(from_lines, to_lines,
                                 from_description, to_description,
                                 context=True, numlines=numlines)
    return HtmlDiff().make_table(
        from_lines,
        to_lines,
        from_description,
        to_description,
        context=True,
        numlines=numlines,
    )


def _fallback_if_none(value: Optional[str], *, fallback: str = '') -> str:

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

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

M byceps/services/ticketing/attendance_service.py => byceps/services/ticketing/attendance_service.py +8 -5
@@ 44,8 44,9 @@ def get_attended_parties(user_id: UserID) -> List[Party]:
    ticket_attendance_party_ids = _get_attended_party_ids(user_id)
    archived_attendance_party_ids = _get_archived_attendance_party_ids(user_id)

    party_ids = set(chain(ticket_attendance_party_ids,
                          archived_attendance_party_ids))
    party_ids = set(
        chain(ticket_attendance_party_ids, archived_attendance_party_ids)
    )

    return party_service.get_parties(party_ids)



@@ 81,9 82,11 @@ def get_attendees_by_party(party_ids: Set[PartyID]) -> Dict[PartyID, Set[User]]:
    attendee_ids_by_party_id = get_attendee_ids_for_parties(party_ids)

    all_attendee_ids = set(
        chain.from_iterable(attendee_ids_by_party_id.values()))
    all_attendees = user_service.find_users(all_attendee_ids,
                                            include_avatars=True)
        chain.from_iterable(attendee_ids_by_party_id.values())
    )
    all_attendees = user_service.find_users(
        all_attendee_ids, include_avatars=True
    )
    all_attendees_by_id = user_service.index_users_by_id(all_attendees)

    attendees_by_party_id = {}

M byceps/services/ticketing/barcode_service.py => byceps/services/ticketing/barcode_service.py +8 -5
@@ 161,7 161,8 @@ def _generate_values(text):

    # check digit symbol
    check_digit_value = _calculate_check_digit_value(
        check_digit_calculation_values)
        check_digit_calculation_values
    )
    yield check_digit_value

    # stop symbol


@@ 177,8 178,8 @@ def _calculate_check_digit_value(values):
    # Important: *Both* the start code *and* the
    # first encoded symbol are in position 1.
    symbol_products_sum = sum(
        max(1, position) * value
        for position, value in enumerate(values))
        max(1, position) * value for position, value in enumerate(values)
    )

    return symbol_products_sum % 103



@@ 197,13 198,15 @@ def _generate_svg(bar_widths, *, image_height=100):
    # Calculate whe