~homeworkprod/byceps

10c3defc9ee1f3a57d356e1f792a5b05fe5722ce — Jochen Kupperschmidt 4 years ago 3bdc507
Split ticketing service into several modules
M byceps/blueprints/admin_dashboard/views.py => byceps/blueprints/admin_dashboard/views.py +2 -2
@@ 26,7 26,7 @@ from ...services.seating import area_service as seating_area_service, \
from ...services.shop.article import service as article_service
from ...services.shop.order import service as order_service
from ...services.terms import service as terms_service
from ...services.ticketing import service as ticketing_service
from ...services.ticketing import ticket_service
from ...services.user import service as user_service
from ...util.framework.blueprint import create_blueprint
from ...util.templating import templated


@@ 146,7 146,7 @@ def view_party(party_id):

    article_count = article_service.count_articles_for_party(party.id)
    open_order_count = order_service.count_open_orders_for_party(party.id)
    tickets_sold = ticketing_service.count_tickets_for_party(party.id)
    tickets_sold = ticket_service.count_tickets_for_party(party.id)

    return {
        'party': party,

M byceps/blueprints/party/views.py => byceps/blueprints/party/views.py +2 -2
@@ 10,7 10,7 @@ from flask import g

from ...config import get_current_party_id
from ...services.party import service as party_service
from ...services.ticketing import service as ticketing_service
from ...services.ticketing import attendance_service
from ...util.framework.blueprint import create_blueprint
from ...util.templating import templated



@@ 44,7 44,7 @@ def archive():
    archived_parties = party_service.get_archived_parties_for_brand(brand_id)

    party_ids = {party.id for party in archived_parties}
    attendees_by_party_id = ticketing_service.get_attendees_by_party(party_ids)
    attendees_by_party_id = attendance_service.get_attendees_by_party(party_ids)

    return {
        'parties': archived_parties,

M byceps/blueprints/party_admin/views.py => byceps/blueprints/party_admin/views.py +2 -2
@@ 12,7 12,7 @@ from ...services.brand import service as brand_service
from ...services.party import service as party_service
from ...services.shop.article import service as article_service
from ...services.shop.order import service as order_service
from ...services.ticketing import service as ticketing_service
from ...services.ticketing import ticket_service
from ...util.framework.blueprint import create_blueprint
from ...util.framework.flash import flash_success
from ...util.templating import templated


@@ 59,7 59,7 @@ def index_for_brand(brand_id, page):

    order_count_by_party_id = order_service.get_order_count_by_party_id()

    ticket_count_by_party_id = ticketing_service.get_ticket_count_by_party_id()
    ticket_count_by_party_id = ticket_service.get_ticket_count_by_party_id()

    return {
        'brand': brand,

M byceps/blueprints/shop_article_admin/views.py => byceps/blueprints/shop_article_admin/views.py +2 -2
@@ 16,7 16,7 @@ from ...services.shop.article import service as article_service
from ...services.shop.order.models.order import PaymentState
from ...services.shop.order import ordered_articles_service
from ...services.shop.sequence import service as sequence_service
from ...services.ticketing import service as ticketing_service
from ...services.ticketing import ticket_service
from ...util.framework.blueprint import create_blueprint
from ...util.framework.flash import flash_success
from ...util.templating import templated


@@ 90,7 90,7 @@ def view_ordered(article_id):

    def transform(order_item):
        user = order_item.order.placed_by
        tickets = ticketing_service.find_tickets_used_by_user(
        tickets = ticket_service.find_tickets_used_by_user(
            user.id, article.party.id)
        quantity = order_item.quantity
        order = order_item.order

M byceps/blueprints/ticketing/views.py => byceps/blueprints/ticketing/views.py +2 -2
@@ 8,7 8,7 @@ byceps.blueprints.ticketing.views

from flask import abort, g

from ...services.ticketing import service as ticketing_service
from ...services.ticketing import ticket_service
from ...util.framework.blueprint import create_blueprint
from ...util.iterables import find
from ...util.templating import templated


@@ 23,7 23,7 @@ def index_mine():
    """List tickets related to the current user."""
    me = get_current_user_or_403()

    tickets = ticketing_service.find_tickets_related_to_user_for_party(
    tickets = ticket_service.find_tickets_related_to_user_for_party(
        me.id, g.party.id)

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

M byceps/blueprints/ticketing_admin/views.py => byceps/blueprints/ticketing_admin/views.py +3 -3
@@ 9,7 9,7 @@ byceps.blueprints.ticketing_admin.views
from flask import abort, request

from ...services.party import service as party_service
from ...services.ticketing import service as ticketing_service
from ...services.ticketing import ticket_service
from ...util.framework.blueprint import create_blueprint
from ...util.templating import templated



@@ 37,7 37,7 @@ def index_for_party(party_id, page):

    per_page = request.args.get('per_page', type=int, default=15)

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

    return {


@@ 51,7 51,7 @@ def index_for_party(party_id, page):
@templated
def view(ticket_id):
    """Show a ticket."""
    ticket = ticketing_service.get_ticket_with_details(ticket_id)
    ticket = ticket_service.get_ticket_with_details(ticket_id)
    if ticket is None:
        abort(404)


M byceps/blueprints/user/views.py => byceps/blueprints/user/views.py +3 -3
@@ 15,7 15,7 @@ from ...services.country import service as country_service
from ...services.newsletter import service as newsletter_service
from ...services.orga_team import service as orga_team_service
from ...services.terms import service as terms_service
from ...services.ticketing import service as ticketing_service
from ...services.ticketing import attendance_service, ticket_service
from ...services.user import service as user_service
from ...services.user_badge import service as badge_service
from ...services.verification_token import service as verification_token_service


@@ 50,10 50,10 @@ def view(user_id):
    orga_team_membership = orga_team_service.find_membership_for_party(user.id,
        g.party.id)

    current_party_tickets = ticketing_service.find_tickets_used_by_user(user.id,
    current_party_tickets = ticket_service.find_tickets_used_by_user(user.id,
        g.party.id)

    attended_parties = ticketing_service.get_attended_parties(user.id)
    attended_parties = attendance_service.get_attended_parties(user.id)
    attended_parties.sort(key=attrgetter('starts_at'), reverse=True)

    return {

M byceps/blueprints/user_admin/views.py => byceps/blueprints/user_admin/views.py +2 -2
@@ 13,7 13,7 @@ from flask import abort, request
from ...services.authorization import service as authorization_service
from ...services.party import service as party_service
from ...services.shop.order import service as order_service
from ...services.ticketing import service as ticketing_service
from ...services.ticketing import ticket_service
from ...services.user import service as user_service
from ...services.user_activity import service as activity_service
from ...services.user_badge import service as badge_service


@@ 92,7 92,7 @@ def view(user_id):


def _get_parties_and_tickets(user_id):
    tickets = ticketing_service.find_tickets_related_to_user(user_id)
    tickets = ticket_service.find_tickets_related_to_user(user_id)

    tickets_by_party_id = _group_tickets_by_party_id(tickets)


A byceps/services/ticketing/attendance_service.py => byceps/services/ticketing/attendance_service.py +120 -0
@@ 0,0 1,120 @@
"""
byceps.services.ticketing.attendance_service
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

:Copyright: 2006-2017 Jochen Kupperschmidt
:License: Modified BSD, see LICENSE for details.
"""

from collections import defaultdict
from datetime import datetime
from itertools import chain
from typing import Dict, Sequence, Set

from ...database import db
from ...typing import PartyID, UserID

from ..party.models import Party, PartyTuple
from ..party import service as party_service
from ..seating.models.category import Category
from ..user.models.user import UserTuple
from ..user import service as user_service

from .models.archived_attendance import ArchivedAttendance
from .models.ticket import Ticket


def create_archived_attendance(user_id: UserID, party_id: PartyID
                              ) -> ArchivedAttendance:
    """Create an archived attendance of the user at the party."""
    attendance = ArchivedAttendance(user_id, party_id)

    db.session.add(attendance)
    db.session.commit()

    return attendance


def get_attended_parties(user_id: UserID) -> Sequence[PartyTuple]:
    """Return the parties the user has attended in the past."""
    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))

    return party_service.get_parties(party_ids)


def _get_attended_party_ids(user_id: UserID) -> Set[PartyID]:
    """Return the IDs of the non-legacy parties the user has attended."""
    # Note: Party dates aren't UTC, yet.
    party_id_rows = db.session \
        .query(Party.id) \
        .filter(Party.ends_at < datetime.now()) \
        .join(Category).join(Ticket).filter(Ticket.used_by_id == user_id) \
        .all()

    return {row[0] for row in party_id_rows}


def _get_archived_attendance_party_ids(user_id: UserID) -> Set[PartyID]:
    """Return the IDs of the legacy parties the user has attended."""
    party_id_rows = db.session \
        .query(ArchivedAttendance.party_id) \
        .filter(ArchivedAttendance.user_id == user_id) \
        .all()

    return {row[0] for row in party_id_rows}


def get_attendees_by_party(party_ids: Set[PartyID]
                          ) -> Dict[PartyID, Set[UserTuple]]:
    """Return the parties' attendees, indexed by party."""
    if not party_ids:
        return {}

    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)
    all_attendees_by_id = user_service.index_users_by_id(all_attendees)

    attendees_by_party_id = {}
    for party_id in party_ids:
        attendee_ids = attendee_ids_by_party_id.get(party_id, set())

        attendees = {all_attendees_by_id[attendee_id]
                     for attendee_id in attendee_ids}

        attendees_by_party_id[party_id] = attendees

    return attendees_by_party_id


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

    ticket_rows = db.session \
        .query(Category.party_id, Ticket.used_by_id) \
        .filter(Category.party_id.in_(party_ids)) \
        .join(Ticket) \
        .filter(Ticket.used_by_id != None) \
        .all()

    archived_attendance_rows = db.session \
        .query(ArchivedAttendance.party_id, ArchivedAttendance.user_id) \
        .filter(ArchivedAttendance.party_id.in_(party_ids)) \
        .all()

    rows = ticket_rows + archived_attendance_rows

    attendee_ids_by_party_id = defaultdict(set)  # type: Dict[PartyID, Set[UserID]]
    for party_id, attendee_id in rows:
        attendee_ids_by_party_id[party_id].add(attendee_id)

    return dict(attendee_ids_by_party_id)

A byceps/services/ticketing/ticket_bundle_service.py => byceps/services/ticketing/ticket_bundle_service.py +42 -0
@@ 0,0 1,42 @@
"""
byceps.services.ticketing.ticket_bundle_service
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

:Copyright: 2006-2017 Jochen Kupperschmidt
:License: Modified BSD, see LICENSE for details.
"""

from ...database import db
from ...typing import UserID

from ..seating.models.category import CategoryID

from .models.ticket_bundle import TicketBundle


def create_ticket_bundle(category_id: CategoryID, ticket_quantity: int,
                         owned_by_id: UserID) -> TicketBundle:
    """Create a ticket bundle and the given quantity of tickets."""
    if ticket_quantity < 1:
        raise ValueError('Ticket quantity must be positive.')

    bundle = TicketBundle(category_id, ticket_quantity, owned_by_id)
    db.session.add(bundle)

    tickets = list(_build_tickets(category_id, owned_by_id, ticket_quantity,
                                  bundle=bundle))
    db.session.add_all(tickets)

    db.session.commit()

    return bundle


def delete_ticket_bundle(bundle: TicketBundle) -> None:
    """Delete the ticket bundle and the tickets associated with it."""
    for ticket in bundle.tickets:
        db.session.delete(ticket)

    db.session.delete(bundle)

    db.session.commit()

R byceps/services/ticketing/service.py => byceps/services/ticketing/ticket_service.py +4 -156
@@ 1,37 1,26 @@
"""
byceps.services.ticketing.service
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
byceps.services.ticketing.ticket_service
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

:Copyright: 2006-2017 Jochen Kupperschmidt
:License: Modified BSD, see LICENSE for details.
"""

from collections import defaultdict
from datetime import datetime
from itertools import chain
from typing import Any, Dict, Iterable, Iterator, Optional, Sequence, Set
from typing import Dict, Iterator, Optional, Sequence

from flask_sqlalchemy import Pagination

from ...database import db
from ...typing import PartyID, UserID

from ..party.models import Party, PartyTuple
from ..party import service as party_service
from ..party.models import Party
from ..seating.models.category import Category, CategoryID
from ..seating.models.seat import Seat
from ..user.models.user import UserTuple
from ..user import service as user_service

from .models.archived_attendance import ArchivedAttendance
from .models.ticket import Ticket, TicketID
from .models.ticket_bundle import TicketBundle


# -------------------------------------------------------------------- #
# tickets


def create_ticket(category_id: CategoryID, owned_by_id: UserID
                 ) -> Sequence[Ticket]:
    """Create a single ticket."""


@@ 129,29 118,6 @@ def uses_any_ticket_for_party(user_id: UserID, party_id: PartyID) -> bool:
    return count > 0


def get_attended_parties(user_id: UserID) -> Sequence[PartyTuple]:
    """Return the parties the user has attended in the past."""
    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))

    return party_service.get_parties(party_ids)


def _get_attended_party_ids(user_id: UserID) -> Set[PartyID]:
    """Return the IDs of the non-legacy parties the user has attended."""
    # Note: Party dates aren't UTC, yet.
    party_id_rows = db.session \
        .query(Party.id) \
        .filter(Party.ends_at < datetime.now()) \
        .join(Category).join(Ticket).filter(Ticket.used_by_id == user_id) \
        .all()

    return _get_first_column_values_as_set(party_id_rows)


def get_ticket_with_details(ticket_id: TicketID) -> Optional[Ticket]:
    """Return the ticket with that id, or `None` if not found."""
    return Ticket.query \


@@ 197,121 163,3 @@ def count_tickets_for_party(party_id: PartyID) -> int:
    return Ticket.query \
        .for_party_id(party_id) \
        .count()


def get_attendees_by_party(party_ids: Set[PartyID]
                          ) -> Dict[PartyID, Set[UserTuple]]:
    """Return the parties' attendees, indexed by party."""
    if not party_ids:
        return {}

    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)
    all_attendees_by_id = user_service.index_users_by_id(all_attendees)

    attendees_by_party_id = {}
    for party_id in party_ids:
        attendee_ids = attendee_ids_by_party_id.get(party_id, set())

        attendees = {all_attendees_by_id[attendee_id]
                     for attendee_id in attendee_ids}

        attendees_by_party_id[party_id] = attendees

    return attendees_by_party_id


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

    ticket_rows = db.session \
        .query(Category.party_id, Ticket.used_by_id) \
        .filter(Category.party_id.in_(party_ids)) \
        .join(Ticket) \
        .filter(Ticket.used_by_id != None) \
        .all()

    archived_attendance_rows = db.session \
        .query(ArchivedAttendance.party_id, ArchivedAttendance.user_id) \
        .filter(ArchivedAttendance.party_id.in_(party_ids)) \
        .all()

    rows = ticket_rows + archived_attendance_rows

    attendee_ids_by_party_id = defaultdict(set)  # type: Dict[PartyID, Set[UserID]]
    for party_id, attendee_id in rows:
        attendee_ids_by_party_id[party_id].add(attendee_id)

    return dict(attendee_ids_by_party_id)


# -------------------------------------------------------------------- #
# ticket bundles


def create_ticket_bundle(category_id: CategoryID, ticket_quantity: int,
                         owned_by_id: UserID) -> TicketBundle:
    """Create a ticket bundle and the given quantity of tickets."""
    if ticket_quantity < 1:
        raise ValueError('Ticket quantity must be positive.')

    bundle = TicketBundle(category_id, ticket_quantity, owned_by_id)
    db.session.add(bundle)

    tickets = list(_build_tickets(category_id, owned_by_id, ticket_quantity,
                                  bundle=bundle))
    db.session.add_all(tickets)

    db.session.commit()

    return bundle


def delete_ticket_bundle(bundle: TicketBundle) -> None:
    """Delete the ticket bundle and the tickets associated with it."""
    for ticket in bundle.tickets:
        db.session.delete(ticket)

    db.session.delete(bundle)

    db.session.commit()


# -------------------------------------------------------------------- #
# archived attendances


def create_archived_attendance(user_id: UserID, party_id: PartyID
                              ) -> ArchivedAttendance:
    """Create an archived attendance of the user at the party."""
    attendance = ArchivedAttendance(user_id, party_id)

    db.session.add(attendance)
    db.session.commit()

    return attendance


def _get_archived_attendance_party_ids(user_id: UserID) -> Set[PartyID]:
    """Return the IDs of the legacy parties the user has attended."""
    party_id_rows = db.session \
        .query(ArchivedAttendance.party_id) \
        .filter(ArchivedAttendance.user_id == user_id) \
        .all()

    return _get_first_column_values_as_set(party_id_rows)


# -------------------------------------------------------------------- #
# helpers


def _get_first_column_values_as_set(rows: Iterable[Sequence[Any]]) -> Set[Any]:
    """Return the first element of each row as a set."""
    return {row[0] for row in rows}

M scripts/add_archived_attendance.py => scripts/add_archived_attendance.py +2 -2
@@ 7,7 7,7 @@

import click

from byceps.services.ticketing import service as ticketing_service
from byceps.services.ticketing import attendance_service
from byceps.services.user import service as user_service
from byceps.util.system import get_config_filename_from_env_or_exit



@@ 22,7 22,7 @@ def execute(user, party):
    click.echo('Adding attendance of user "{}" at party "{}" ... '
        .format(user.screen_name, party.title), nl=False)

    ticketing_service.create_archived_attendance(user.id, party.id)
    attendance_service.create_archived_attendance(user.id, party.id)

    click.secho('done.', fg='green')