Ejemplo n.º 1
0
def test_product_view_accessible_voucher_expiry(db, user, monkeypatch):
    EXPIRED_YESTERDAY = "test1"
    EXPIRES_TOMORROW = "test2"
    product_view = ProductView(name="other", type="ticket", vouchers_only=True)
    db.session.add(product_view)
    db.session.add(
        Voucher(
            view=product_view,
            code=EXPIRED_YESTERDAY,
            expiry=datetime.utcnow() - timedelta(days=1) - VOUCHER_GRACE_PERIOD,
        )
    )
    db.session.add(
        Voucher(
            view=product_view,
            code=EXPIRES_TOMORROW,
            expiry=datetime.utcnow() + timedelta(days=1),
        )
    )
    db.session.commit()

    assert not product_view.is_accessible(
        user, voucher=EXPIRED_YESTERDAY
    ), "View should be inaccessible with expired voucher"

    assert product_view.is_accessible(
        user, voucher=EXPIRES_TOMORROW
    ), "View should be accessible with in-date voucher"
Ejemplo n.º 2
0
def test_product_view_accessible_voucher_expiry(db, user, monkeypatch):
    # Vouchers should work regardless of SALES_START so set it into the future
    def sales_start_mock_future(key):
        return TOMORROW

    # Patch config_date rather than utcnow as apparently you can't patch builtins
    monkeypatch.setattr("models.product.config_date", sales_start_mock_future)

    product_view = ProductView(name="other", type="ticket", vouchers_only=True)
    db.session.add(product_view)
    db.session.add(Voucher(view=product_view, code="test1", expiry=YESTERDAY))
    db.session.add(Voucher(view=product_view, code="test2", expiry=TOMORROW))
    db.session.commit()

    assert not product_view.is_accessible(
        user, voucher="test1"
    ), "View should be inaccessible with expired voucher"

    assert product_view.is_accessible(
        user, voucher="test2"
    ), "View should be accessible with in-date voucher"
Ejemplo n.º 3
0
def tickets_token(token=None):
    view = ProductView.get_by_token(token)
    if view:
        session["ticket_token"] = token
        return redirect(url_for("tickets.main", flow=view.name))

    if "ticket_token" in session:
        del session["ticket_token"]

    invalid_tokens.inc()
    flash("Ticket token was invalid")
    return redirect(url_for("tickets.main"))
Ejemplo n.º 4
0
def tickets_token(token=None):
    view = ProductView.get_by_token(token)
    if view:
        session['ticket_token'] = token
        return redirect(url_for('tickets.main', flow=view.name))

    if 'ticket_token' in session:
        del session['ticket_token']

    invalid_tokens.inc()
    flash('Ticket token was invalid')
    return redirect(url_for('tickets.main'))
Ejemplo n.º 5
0
def tickets_token(token=None):
    view = ProductView.get_by_token(token)
    if view:
        session['ticket_token'] = token
        return redirect(url_for('tickets.main', flow=view.name))

    if 'ticket_token' in session:
        del session['ticket_token']

    invalid_tokens.inc()
    flash('Ticket token was invalid')
    return redirect(url_for('tickets.main'))
Ejemplo n.º 6
0
def product_view_new():
    form = NewProductViewForm()

    if form.validate_on_submit():
        view = ProductView(type=form.type.data,
                           name=form.name.data,
                           token=form.token.data,
                           cfp_accepted_only=form.cfp_accepted_only.data)
        app.logger.info('Adding new ProductView %s', view.name)
        db.session.add(view)
        db.session.commit()
        flash("ProductView created")
        return redirect(url_for('.product_view', view_id=view.id))

    return render_template('admin/products/view-new.html', form=form)
Ejemplo n.º 7
0
def test_product_view_accessible(db, user):
    product_view = ProductView(name="other")
    assert product_view.is_accessible(user)

    product_view = ProductView(name="other", token="testtoken")
    assert product_view.is_accessible(user, "testtoken")
    assert not product_view.is_accessible(user)

    product_view = ProductView(name="cfp", cfp_accepted_only=True)
    assert not product_view.is_accessible(user)

    proposal = TalkProposal()
    proposal.title = "title"
    proposal.description = "description"
    proposal.requirements = "requirements"
    proposal.user = user
    db.session.add(proposal)
    db.session.commit()
    proposal.set_state('accepted')

    assert product_view.is_accessible(user)
Ejemplo n.º 8
0
def test_product_view_accessible_wrt_sales_start(db, user, monkeypatch):
    # force SALES_START into the future
    def sales_start_mock_future(key):
        return TOMORROW

    # Patch config_date rather than utcnow as apparently you can't patch builtins
    monkeypatch.setattr("models.product.config_date", sales_start_mock_future)

    product_view = ProductView(name="other")
    assert not product_view.is_accessible(
        user
    ), "Default view should not be visible before SALES_START"

    # force sales start into the future
    def sales_start_mock_past(key):
        return YESTERDAY

    # Patch config_date rather than utcnow as apparently you can't patch builtins
    monkeypatch.setattr("models.product.config_date", sales_start_mock_past)

    product_view = ProductView(name="other")
    assert product_view.is_accessible(
        user
    ), "Default view should be visible after SALES_START"
Ejemplo n.º 9
0
def main(flow=None):
    if flow is None:
        flow = "main"

    # For now, use the flow name as a view name. This might change.
    view = ProductView.get_by_name(flow)
    if not view:
        abort(404)

    if view.cfp_accepted_only and current_user.is_anonymous:
        return redirect(url_for("users.login", next=url_for(".main", flow=flow)))

    if not view.is_accessible(current_user, session.get("ticket_token")):
        abort(404)

    sales_state = get_sales_state()

    if sales_state in ["unavailable", "sold-out"]:
        # For the main entry point, we assume people want admissions tickets,
        # but we still need to sell people parking tickets, tents or tickets
        # from tokens until the final cutoff (sales-ended).
        if flow != "main":
            sales_state = "available"

    if app.config.get("DEBUG"):
        sales_state = request.args.get("sales_state", sales_state)

    if sales_state in {"available", "unavailable"}:
        pass
    elif not current_user.is_anonymous and current_user.has_permission("admin"):
        pass
    else:
        return render_template("tickets/cutoff.html")

    tiers = OrderedDict()
    products = (
        ProductViewProduct.query.filter_by(view_id=view.id)
        .join(ProductViewProduct.product)
        .with_entities(Product)
        .order_by(ProductViewProduct.order)
        .options(joinedload(Product.price_tiers).joinedload(PriceTier.prices))
    )

    for product in products:
        pts = [tier for tier in product.price_tiers if tier.active]
        if len(pts) > 1:
            app.logger.error(
                "Multiple active PriceTiers found for %s. Excluding product.", product
            )
            continue

        pt = pts[0]

        tiers[pt.id] = pt

    basket = Basket.from_session(current_user, get_user_currency())

    form = TicketAmountsForm()

    """
    For consistency and to avoid surprises, we try to ensure a few things here:
    - if the user successfully submits a form with no errors, their basket is updated
    - if they don't, the basket is left untouched
    - the basket is updated to match what was submitted, even if they added tickets in another tab
    - if they already have tickets in their basket, only reserve the extra tickets as necessary
    - don't unreserve surplus tickets until the payment is created
    - if the user hasn't submitted anything, we use their current reserved ticket counts
    - if the user has reserved tickets from exhausted tiers on this view, we still show them
    - if the user has reserved tickets from other views, don't show and don't mess with them
    - this means the user can combine tickets from multiple views into a single basket
    - show the sold-out/unavailable states only when the user doesn't have reserved tickets

    We currently don't deal with multiple price tiers being available around the same time.
    Reserved tickets from a previous tier should be cancelled before activating a new one.
    """
    if request.method != "POST":
        # Empty form - populate products
        for pt_id, tier in tiers.items():
            form.tiers.append_entry()
            f = form.tiers[-1]
            f.tier_id.data = pt_id

            f.amount.data = basket.get(tier, 0)

    # Whether submitted or not, update the allowed amounts before validating
    capacity_gone = False
    for f in form.tiers:
        pt_id = f.tier_id.data
        tier = tiers[pt_id]
        f._tier = tier

        # If they've already got reserved tickets, let them keep them
        user_limit = max(tier.user_limit(), basket.get(tier, 0))
        if f.amount.data and f.amount.data > user_limit:
            capacity_gone = True
        values = range(user_limit + 1)
        f.amount.values = values
        f._any = any(values)

    available = True
    if sales_state == "unavailable":
        if not any(p.product in products for p in basket.purchases):
            # If they have any reservations, they bypass the unavailable state.
            # This means someone can use another view to get access to this one
            # again. I'm not sure what to do about this. It usually won't matter.
            available = False

    if form.validate_on_submit():
        if form.buy_tickets.data or form.buy_hire.data or form.buy_other.data:
            if form.currency_code.data != get_user_currency():
                set_user_currency(form.currency_code.data)
                # Commit so we don't lose the currency change if an error occurs
                db.session.commit()
                # Reload the basket because set_user_currency has changed it under us
                basket = Basket.from_session(current_user, get_user_currency())

            for f in form.tiers:
                pt = f._tier
                if f.amount.data != basket.get(pt, 0):
                    app.logger.info(
                        "Adding %s %s tickets to basket", f.amount.data, pt.name
                    )
                    basket[pt] = f.amount.data

            if not available:
                app.logger.warn("User has no reservations, enforcing unavailable state")
                basket.save_to_session()
                return redirect(url_for("tickets.main", flow=flow))

            if not any(basket.values()):
                empty_baskets.inc()
                if view.type == "tickets":
                    flash("Please select at least one ticket to buy.")
                elif view.type == "hire":
                    flash("Please select at least one item to hire.")
                else:
                    flash("Please select at least one item to buy.")

                basket.save_to_session()
                return redirect(url_for("tickets.main", flow=flow))

            app.logger.info("Basket %s", basket)

            try:
                basket.create_purchases()
                basket.ensure_purchase_capacity()

                db.session.commit()

            except CapacityException as e:
                # Damn, capacity's gone since we created the purchases
                # Redirect back to show what's currently in the basket
                db.session.rollback()
                no_capacity.inc()
                app.logger.warn("Limit exceeded creating tickets: %s", e)
                flash(
                    "We're very sorry, but there is not enough capacity available to "
                    "allocate these tickets. You may be able to try again with a smaller amount."
                )
                return redirect(url_for("tickets.main", flow=flow))

            basket.save_to_session()

            if basket.total != 0:
                # Send the user off to pay
                return redirect(url_for("tickets.pay", flow=flow))

            # Otherwise, the user is trying to buy free tickets.
            # They must be authenticated for this.
            if not current_user.is_authenticated:
                app.logger.warn("User is not authenticated, sending to login")
                flash("You must be logged in to buy additional free tickets")
                return redirect(
                    url_for("users.login", next=url_for("tickets.main", flow=flow))
                )

            # We sell under-12 tickets to non-CfP users, to enforce capacity.
            # We don't let people order an under-12 ticket on its own.
            # However, CfP users need to be able to buy day and parking tickets.
            admissions_tickets = current_user.get_owned_tickets(type="admission_ticket")
            if not any(admissions_tickets) and not view.cfp_accepted_only:
                app.logger.warn(
                    "User trying to buy free add-ons without an admission ticket"
                )
                flash(
                    "You must have an admissions ticket to buy additional free tickets"
                )
                return redirect(url_for("tickets.main", flow=flow))

            basket.user = current_user
            basket.check_out_free()
            db.session.commit()

            Basket.clear_from_session()

            msg = Message(
                "Your EMF ticket order",
                sender=app.config["TICKETS_EMAIL"],
                recipients=[current_user.email],
            )

            already_emailed = set_tickets_emailed(current_user)
            msg.body = render_template(
                "emails/tickets-ordered-email-free.txt",
                user=current_user,
                basket=basket,
                already_emailed=already_emailed,
            )
            if feature_enabled("ISSUE_TICKETS"):
                attach_tickets(msg, current_user)

            mail.send(msg)

            if len(basket.purchases) == 1:
                flash("Your ticket has been confirmed")
            else:
                flash("Your tickets have been confirmed")

            return redirect(url_for("users.purchases"))

    if request.method == "POST" and form.set_currency.data:
        if form.set_currency.validate(form):
            app.logger.info("Updating currency to %s only", form.set_currency.data)
            set_user_currency(form.set_currency.data)
            db.session.commit()

            for field in form:
                field.errors = []

    if capacity_gone:
        no_capacity.inc()
        flash(
            "We're sorry, but there is not enough capacity available to "
            "allocate these tickets. You may be able to try again with a smaller amount."
        )

    form.currency_code.data = get_user_currency()
    return render_template(
        "tickets/choose.html", form=form, flow=flow, view=view, available=available
    )
Ejemplo n.º 10
0
def create_product_groups():
    top_level_groups = [
        # name, capacity, expires
        ('admissions', datetime(2018, 9,
                                3), app.config.get('MAXIMUM_ADMISSIONS')),
        ('parking', datetime(2018, 9, 3), None),
        ('campervan', datetime(2018, 9, 3), None),
        ('merchandise', datetime(2018, 8, 12), None),
    ]
    for name, expires, capacity in top_level_groups:
        if ProductGroup.get_by_name(name):
            continue
        pg = ProductGroup(name=name,
                          type=name,
                          capacity_max=capacity,
                          expires=expires)
        db.session.add(pg)

    db.session.flush()

    allocations = [
        # name, capacity
        ('vendors', 100),
        ('sponsors', 200),
        ('speakers', 100),
        ('general', 800),
    ]

    admissions = ProductGroup.get_by_name('admissions')
    for name, capacity in allocations:
        if ProductGroup.get_by_name(name):
            continue
        ProductGroup(name=name, capacity_max=capacity, parent=admissions)

    view = ProductView.get_by_name('main')
    if not view:
        view = ProductView(name='main', type='tickets')
        db.session.add(view)

    db.session.flush()

    general = ProductGroup.get_by_name('general')

    products = [
        # name, display name, transferable, badge, capacity, description, (std cap, gbp eur), (early cap, gbp, eur), (late cap, gbp, eur)
        ('full', 'Full Camp Ticket', True, True, None, 'Full ticket',
         ((1500, 115, 135), (250, 105, 125), (None, 125, 145))),
        ('full-s', 'Full Camp Ticket (Supporter)', True, True, None,
         'Support this non-profit event by paying a bit more. All money will go towards making EMF more awesome.',
         ((None, 150, 180), )),
        ('full-sg', 'Full Camp Ticket (Gold Supporter)', True, True, None,
         'Support this non-profit event by paying a bit more. All money will go towards making EMF more awesome.',
         ((None, 200, 240), )),
        ('u18', 'Under-18', True, False, 150,
         'For visitors born after August 30th, 2000. All under-18s must be accompanied by an adult.',
         ((None, 55, 63), )),
        ('u12', 'Under-12', True, False, 50,
         'For children born after August 30th, 2006. All children must be accompanied by an adult.',
         ((None, 0, 0), )),
    ]

    order = 0

    for name, display_name, has_xfer, has_badge, capacity, description, prices in products:
        if Product.get_by_name('general', name):
            continue
        product = Product(name=name,
                          display_name=display_name,
                          capacity_max=capacity,
                          description=description,
                          parent=general,
                          attributes={
                              'is_transferable': has_xfer,
                              'has_badge': has_badge
                          })

        for index, (price_cap, gbp, eur) in enumerate(prices):
            if len(prices) == 1 or index == 0:
                tier_name = name + '-std'
                active = True

            elif index == 1:
                tier_name = name + '-early-bird'
                active = False

            elif index == 2:
                tier_name = name + '-late'
                active = False

            if PriceTier.get_by_name('general', 'name', tier_name):
                continue

            pt = PriceTier(name=tier_name,
                           capacity_max=price_cap,
                           personal_limit=10,
                           parent=product,
                           active=active)
            Price(currency='GBP', price_int=gbp * 100, price_tier=pt)
            Price(currency='EUR', price_int=eur * 100, price_tier=pt)

        ProductViewProduct(view, product, order)
        order += 1

    db.session.flush()

    misc = [
        # name, display_name, cap, personal_limit, gbp, eur, description
        ('parking', 'Parking Ticket', 1700, 4, 15, 21,
         "We're trying to keep cars to a minimum. Please take public transport or car-share if you can."
         ),
        ('campervan', 'Caravan/\u200cCampervan Ticket', 60, 2, 30, 42,
         "If you bring a caravan, you won't need a separate parking ticket for the towing car."
         ),
    ]

    for name, display_name, cap, personal_limit, gbp, eur, description in misc:
        if Product.get_by_name(name, name):
            continue

        group = ProductGroup.get_by_name(name)
        product = Product(name=name,
                          display_name=display_name,
                          description=description,
                          parent=group)
        pt = PriceTier(name=name,
                       personal_limit=personal_limit,
                       parent=product)
        db.session.add(pt)
        db.session.add(
            Price(currency='GBP', price_int=gbp * 100, price_tier=pt))
        db.session.add(
            Price(currency='EUR', price_int=eur * 100, price_tier=pt))

        ProductViewProduct(view, product, order)
        order += 1

    db.session.commit()
Ejemplo n.º 11
0
def create_product_groups():
    top_level_groups = [
        # name, capacity, expires
        ('admissions', None, 2500),
        ('parking', None, None),
        ('campervan', None, None),
        ('merchandise', None, None),
    ]
    for name, expires, capacity in top_level_groups:
        if ProductGroup.get_by_name(name):
            continue
        pg = ProductGroup(name=name, type=name, capacity_max=capacity, expires=expires)
        db.session.add(pg)

    db.session.flush()

    allocations = [
        # name, capacity
        ('vendors', 100),
        ('sponsors', 200),
        ('speakers', 100),
        ('general', 1800),
    ]

    admissions = ProductGroup.get_by_name('admissions')
    for name, capacity in allocations:
        if ProductGroup.get_by_name(name):
            continue
        ProductGroup(name=name, capacity_max=capacity, parent=admissions)

    view = ProductView.get_by_name('main')
    if not view:
        view = ProductView(name='main', type='tickets')
        db.session.add(view)

    db.session.flush()

    general = ProductGroup.get_by_name('general')

    products = [
        # name, display name, transferable, badge, capacity, description, (std cap, gbp eur), (early cap, gbp, eur), (late cap, gbp, eur)
        ('full', 'Full Camp Ticket', True, True, None, 'Full ticket',
            ((1500, 115, 135), (250, 105, 125), (None, 125, 145))
        ),
        ('full-s', 'Full Camp Ticket (Supporter)', True, True, None, 'Support this non-profit event by paying a bit more. All money will go towards making EMF more awesome.',
            ((None, 150, 180),)
        ),
        ('full-sg', 'Full Camp Ticket (Gold Supporter)', True, True, None, 'Support this non-profit event by paying a bit more. All money will go towards making EMF more awesome.',
            ((None, 200, 240),)
        ),
        ('u18', 'Under-18', True, False, 150, 'For visitors born after August 30th, 2000. All under-18s must be accompanied by an adult.',
            ((None, 55, 63),)
        ),
        ('u12', 'Under-12', True, False, 50, 'For children born after August 30th, 2006. All children must be accompanied by an adult.',
            ((None, 0, 0),)
        ),
    ]

    order = 0

    for name, display_name, has_xfer, has_badge, capacity, description, prices in products:
        if Product.get_by_name('general', name):
            continue
        product = Product(name=name, display_name=display_name, capacity_max=capacity,
                     description=description, parent=general,
                     attributes={'is_transferable': has_xfer,
                                 'has_badge': has_badge})

        for index, (price_cap, gbp, eur) in enumerate(prices):
            if len(prices) == 1 or index == 0:
                tier_name = name + '-std'
                active = True

            elif index == 1:
                tier_name = name + '-early-bird'
                active = False

            elif index == 2:
                tier_name = name + '-late'
                active = False

            if PriceTier.get_by_name('general', 'name', tier_name):
                continue

            pt = PriceTier(name=tier_name, capacity_max=price_cap, personal_limit=10, parent=product, active=active)
            Price(currency='GBP', price_int=gbp * 100, price_tier=pt)
            Price(currency='EUR', price_int=eur * 100, price_tier=pt)

        ProductViewProduct(view, product, order)
        order += 1

    db.session.flush()

    misc = [
        # name, display_name, cap, personal_limit, gbp, eur, description
        ('parking', 'Parking Ticket', 1700, 4, 15, 21, "We're trying to keep cars to a minimum. Please take public transport or car-share if you can."),
        ('campervan', 'Caravan/\u200cCampervan Ticket', 60, 2, 30, 42, "If you bring a caravan, you won't need a separate parking ticket for the towing car."),
    ]

    for name, display_name, cap, personal_limit, gbp, eur, description in misc:
        if Product.get_by_name(name, name):
            continue

        group = ProductGroup.get_by_name(name)
        product = Product(name=name, display_name=display_name, description=description, parent=group)
        pt = PriceTier(name=name, personal_limit=personal_limit, parent=product)
        db.session.add(pt)
        db.session.add(Price(currency='GBP', price_int=gbp * 100, price_tier=pt))
        db.session.add(Price(currency='EUR', price_int=eur * 100, price_tier=pt))

        ProductViewProduct(view, product, order)
        order += 1

    db.session.commit()
Ejemplo n.º 12
0
def test_product_view_accessible(db, user, monkeypatch):
    product_view = ProductView(name="other", type="ticket")
    assert product_view.is_accessible(user), "Default view should be visible"

    product_view = ProductView(name="another-other", type="ticket", vouchers_only=True)
    voucher = Voucher(view=product_view)

    db.session.add(product_view)
    db.session.add(voucher)
    db.session.commit()

    assert product_view.is_accessible(
        user, voucher.code
    ), "Product should be visible with voucher"

    assert not product_view.is_accessible(
        user
    ), "Product should be inaccessible without voucher"

    assert not product_view.is_accessible(
        user, "wrong"
    ), "Product should be inaccessible with incorrect voucher"

    product_view = ProductView(name="cfp", cfp_accepted_only=True, type="ticket")
    assert not product_view.is_accessible(
        user
    ), "CfP products should not be visible without accepted proposal"

    proposal = TalkProposal()
    proposal.title = "title"
    proposal.description = "description"
    proposal.requirements = "requirements"
    proposal.user = user
    db.session.add(proposal)
    db.session.commit()
    proposal.set_state("accepted")

    assert product_view.is_accessible(
        user
    ), "CfP products should be visible with accepted proposal"
Ejemplo n.º 13
0
def test_product_view_accessible(db, user):
    product_view = ProductView(name="other")
    assert product_view.is_accessible(user)

    product_view = ProductView(name="other", token="testtoken")
    assert product_view.is_accessible(user, "testtoken")
    assert not product_view.is_accessible(user)

    product_view = ProductView(name="cfp", cfp_accepted_only=True)
    assert not product_view.is_accessible(user)

    proposal = TalkProposal()
    proposal.title = "title"
    proposal.description = "description"
    proposal.requirements = "requirements"
    proposal.user = user
    db.session.add(proposal)
    db.session.commit()
    proposal.set_state("accepted")

    assert product_view.is_accessible(user)
Ejemplo n.º 14
0
def main(flow=None):
    if flow is None:
        flow = 'main'

    # For now, use the flow name as a view name. This might change.
    view = ProductView.get_by_name(flow)
    if not view:
        abort(404)

    if view.cfp_accepted_only and current_user.is_anonymous:
        return redirect(url_for('users.login', next=url_for('.main', flow=flow)))

    if not view.is_accessible(current_user, session.get('ticket_token')):
        abort(404)

    sales_state = get_sales_state()

    if sales_state in ['unavailable', 'sold-out']:
        # For the main entry point, we assume people want admissions tickets,
        # but we still need to sell people parking tickets, tents or tickets
        # from tokens until the final cutoff (sales-ended).
        if flow != 'main':
            sales_state = 'available'

    if app.config.get('DEBUG'):
        sales_state = request.args.get("sales_state", sales_state)

    if sales_state in {'available', 'unavailable'}:
        pass
    elif not current_user.is_anonymous and current_user.has_permission('admin'):
        pass
    else:
        return render_template("tickets-cutoff.html")

    tiers = OrderedDict()
    products = ProductViewProduct.query.filter_by(view_id=view.id) \
                                 .join(ProductViewProduct.product) \
                                 .with_entities(Product) \
                                 .order_by(ProductViewProduct.order) \
                                 .options(joinedload(Product.price_tiers)
                                          .joinedload(PriceTier.prices)
                                 )

    for product in products:
        pts = [tier for tier in product.price_tiers if tier.active]
        if len(pts) > 1:
            app.logger.error("Multiple active PriceTiers found for %s. Excluding product.", product)
            continue

        pt = pts[0]

        tiers[pt.id] = pt

    basket = Basket.from_session(current_user, get_user_currency())

    form = TicketAmountsForm()

    """
    For consistency and to avoid surprises, we try to ensure a few things here:
    - if the user successfully submits a form with no errors, their basket is updated
    - if they don't, the basket is left untouched
    - the basket is updated to match what was submitted, even if they added tickets in another tab
    - if they already have tickets in their basket, only reserve the extra tickets as necessary
    - don't unreserve surplus tickets until the payment is created
    - if the user hasn't submitted anything, we use their current reserved ticket counts
    - if the user has reserved tickets from exhausted tiers on this view, we still show them
    - if the user has reserved tickets from other views, don't show and don't mess with them
    - this means the user can combine tickets from multiple views into a single basket
    - show the sold-out/unavailable states only when the user doesn't have reserved tickets

    We currently don't deal with multiple price tiers being available around the same time.
    Reserved tickets from a previous tier should be cancelled before activating a new one.
    """
    if request.method != 'POST':
        # Empty form - populate products
        for pt_id, tier in tiers.items():
            form.tiers.append_entry()
            f = form.tiers[-1]
            f.tier_id.data = pt_id

            f.amount.data = basket.get(tier, 0)


    # Whether submitted or not, update the allowed amounts before validating
    capacity_gone = False
    for f in form.tiers:
        pt_id = f.tier_id.data
        tier = tiers[pt_id]
        f._tier = tier

        # If they've already got reserved tickets, let them keep them
        user_limit = max(tier.user_limit(), basket.get(tier, 0))
        if f.amount.data and f.amount.data > user_limit:
            capacity_gone = True
        values = range(user_limit + 1)
        f.amount.values = values
        f._any = any(values)

    available = True
    if sales_state == 'unavailable':
        if not any(p.product in products for p in basket.purchases):
            # If they have any reservations, they bypass the unavailable state.
            # This means someone can use another view to get access to this one
            # again. I'm not sure what to do about this. It usually won't matter.
            available = False

    if form.validate_on_submit():
        if form.buy_tickets.data or form.buy_hire.data or form.buy_other.data:
            if form.currency_code.data != get_user_currency():
                set_user_currency(form.currency_code.data)
                # Commit so we don't lose the currency change if an error occurs
                db.session.commit()
                # Reload the basket because set_user_currency has changed it under us
                basket = Basket.from_session(current_user, get_user_currency())

            for f in form.tiers:
                pt = f._tier
                if f.amount.data != basket.get(pt, 0):
                    app.logger.info('Adding %s %s tickets to basket', f.amount.data, pt.name)
                    basket[pt] = f.amount.data

            if not available:
                app.logger.warn('User has no reservations, enforcing unavailable state')
                basket.save_to_session()
                return redirect(url_for('tickets.main', flow=flow))

            if not any(basket.values()):
                empty_baskets.inc()
                if view.type == 'tickets':
                    flash("Please select at least one ticket to buy.")
                elif view.type == 'hire':
                    flash("Please select at least one item to hire.")
                else:
                    flash("Please select at least one item to buy.")

                basket.save_to_session()
                return redirect(url_for('tickets.main', flow=flow))

            app.logger.info('Basket %s', basket)

            try:
                basket.create_purchases()
                basket.ensure_purchase_capacity()

                db.session.commit()

            except CapacityException as e:
                # Damn, capacity's gone since we created the purchases
                # Redirect back to show what's currently in the basket
                db.session.rollback()
                no_capacity.inc()
                app.logger.warn('Limit exceeded creating tickets: %s', e)
                flash("We're very sorry, but there is not enough capacity available to "
                      "allocate these tickets. You may be able to try again with a smaller amount.")
                return redirect(url_for("tickets.main", flow=flow))

            basket.save_to_session()

            if basket.total != 0:
                # Send the user off to pay
                return redirect(url_for('tickets.pay', flow=flow))

            # Otherwise, the user is trying to buy free tickets.
            # They must be authenticated for this.
            if not current_user.is_authenticated:
                app.logger.warn("User is not authenticated, sending to login")
                flash("You must be logged in to buy additional free tickets")
                return redirect(url_for('users.login', next=url_for('tickets.main', flow=flow)))

            # We sell under-12 tickets to non-CfP users, to enforce capacity.
            # We don't let people order an under-12 ticket on its own.
            # However, CfP users need to be able to buy day and parking tickets.
            admissions_tickets = current_user.get_owned_tickets(type='admission_ticket')
            if not any(admissions_tickets) and not view.cfp_accepted_only:
                app.logger.warn("User trying to buy free add-ons without an admission ticket")
                flash("You must have an admissions ticket to buy additional free tickets")
                return redirect(url_for("tickets.main", flow=flow))

            basket.user = current_user
            basket.check_out_free()
            db.session.commit()

            Basket.clear_from_session()

            msg = Message("Your EMF ticket order",
                          sender=app.config['TICKETS_EMAIL'],
                          recipients=[current_user.email])

            already_emailed = set_tickets_emailed(current_user)
            msg.body = render_template("emails/tickets-ordered-email-free.txt",
                                       user=current_user, basket=basket,
                                       already_emailed=already_emailed)
            if feature_enabled('ISSUE_TICKETS'):
                attach_tickets(msg, current_user)

            mail.send(msg)

            if len(basket.purchases) == 1:
                flash("Your ticket has been confirmed")
            else:
                flash("Your tickets have been confirmed")

            return redirect(url_for('users.purchases'))


    if request.method == 'POST' and form.set_currency.data:
        if form.set_currency.validate(form):
            app.logger.info("Updating currency to %s only", form.set_currency.data)
            set_user_currency(form.set_currency.data)
            db.session.commit()

            for field in form:
                field.errors = []

    if capacity_gone:
        no_capacity.inc()
        flash("We're sorry, but there is not enough capacity available to "
              "allocate these tickets. You may be able to try again with a smaller amount.")

    form.currency_code.data = get_user_currency()
    return render_template("tickets-choose.html", form=form, flow=flow, view=view, available=available)
Ejemplo n.º 15
0
def pay(flow=None):
    if flow is None:
        flow = 'main'

    view = ProductView.get_by_name(flow)
    if not view:
        abort(404)

    if view.token and session.get('ticket_token') != view.token:
        if not current_user.is_anonymous and current_user.has_permission('admin'):
            abort(404)

    if request.form.get("change_currency") in ('GBP', 'EUR'):
        set_user_currency(request.form.get("change_currency"))
        db.session.commit()

        return redirect(url_for('.pay', flow=flow))

    form = TicketPaymentForm()
    form.flow = flow

    if not current_user.is_anonymous:
        del form.email
        if current_user.name != current_user.email:
            del form.name

    basket = Basket.from_session(current_user, get_user_currency())
    if not any(basket.values()):
        empty_baskets.inc()
        if flow == 'main':
            flash("Please select at least one ticket to buy.")
        else:
            flash("Please select at least one item to buy.")
        return redirect(url_for('tickets.main'))

    if form.validate_on_submit():
        if Decimal(form.basket_total.data) != Decimal(basket.total):
            # Check that the user's basket approximately matches what we told them they were paying.
            price_changed.inc()
            app.logger.warn("User's basket has changed value %s -> %s", form.basket_total.data, basket.total)
            flash("""The tickets you selected have changed, possibly because you had two windows open.
                  Please verify that you've selected the correct tickets.""")
            return redirect(url_for('tickets.pay', flow=flow))

        user = current_user
        if user.is_anonymous:
            try:
                new_user = create_current_user(form.email.data, form.name.data)
            except IntegrityError as e:
                app.logger.warn('Adding user raised %r, possible double-click', e)
                return None

            user = new_user

        elif user.name == user.email:
            user.name = form.name.data

        if form.allow_promo.data:
            user.promo_opt_in = True

        if form.gocardless.data:
            payment_type = GoCardlessPayment
        elif form.banktransfer.data:
            payment_type = BankPayment
        elif form.stripe.data:
            payment_type = StripePayment

        basket.user = user
        payment = basket.create_payment(payment_type)
        basket.cancel_surplus_purchases()
        db.session.commit()

        Basket.clear_from_session()

        if not payment:
            empty_baskets.inc()
            app.logger.warn('User tried to pay for empty basket')
            flash("We're sorry, your session information has been lost. Please try ordering again.")
            return redirect(url_for('tickets.main', flow=flow))

        if payment_type == GoCardlessPayment:
            return gocardless_start(payment)
        elif payment_type == BankPayment:
            return transfer_start(payment)
        elif payment_type == StripePayment:
            return stripe_start(payment)

    form.basket_total.data = basket.total

    return render_template('payment-choose.html', form=form,
                           basket=basket, total=basket.total,
                           flow=flow)
Ejemplo n.º 16
0
def pay(flow=None):
    if flow is None:
        flow = 'main'

    view = ProductView.get_by_name(flow)
    if not view:
        abort(404)

    if view.token and session.get('ticket_token') != view.token:
        if not current_user.is_anonymous and current_user.has_permission(
                'admin'):
            abort(404)

    if request.form.get("change_currency") in ('GBP', 'EUR'):
        set_user_currency(request.form.get("change_currency"))
        db.session.commit()

        return redirect(url_for('.pay', flow=flow))

    form = TicketPaymentForm()
    form.flow = flow

    if not current_user.is_anonymous:
        del form.email
        if current_user.name != current_user.email:
            del form.name

    basket = Basket.from_session(current_user, get_user_currency())
    if not any(basket.values()):
        empty_baskets.inc()
        if flow == 'main':
            flash("Please select at least one ticket to buy.")
        else:
            flash("Please select at least one item to buy.")
        return redirect(url_for('tickets.main'))

    if form.validate_on_submit():
        if Decimal(form.basket_total.data) != Decimal(basket.total):
            # Check that the user's basket approximately matches what we told them they were paying.
            price_changed.inc()
            app.logger.warn("User's basket has changed value %s -> %s",
                            form.basket_total.data, basket.total)
            flash(
                """The tickets you selected have changed, possibly because you had two windows open.
                  Please verify that you've selected the correct tickets.""")
            return redirect(url_for('tickets.pay', flow=flow))

        user = current_user
        if user.is_anonymous:
            try:
                new_user = create_current_user(form.email.data, form.name.data)
            except IntegrityError as e:
                app.logger.warn('Adding user raised %r, possible double-click',
                                e)
                return None

            user = new_user

        elif user.name == user.email:
            user.name = form.name.data

        if form.allow_promo.data:
            user.promo_opt_in = True

        if form.gocardless.data:
            payment_type = GoCardlessPayment
        elif form.banktransfer.data:
            payment_type = BankPayment
        elif form.stripe.data:
            payment_type = StripePayment

        basket.user = user
        payment = basket.create_payment(payment_type)
        basket.cancel_surplus_purchases()
        db.session.commit()

        Basket.clear_from_session()

        if not payment:
            empty_baskets.inc()
            app.logger.warn('User tried to pay for empty basket')
            flash(
                "We're sorry, your session information has been lost. Please try ordering again."
            )
            return redirect(url_for('tickets.main', flow=flow))

        if payment_type == GoCardlessPayment:
            return gocardless_start(payment)
        elif payment_type == BankPayment:
            return transfer_start(payment)
        elif payment_type == StripePayment:
            return stripe_start(payment)

    form.basket_total.data = basket.total

    return render_template('payment-choose.html',
                           form=form,
                           basket=basket,
                           total=basket.total,
                           flow=flow)
Ejemplo n.º 17
0
def pay(flow=None):
    if flow is None:
        flow = 'main'

    view = ProductView.get_by_name(flow)
    if not view:
        abort(404)

    if view.token and session.get('ticket_token') != view.token:
        if not current_user.is_anonymous and current_user.has_permission('admin'):
            abort(404)

    if request.form.get("change_currency") in ('GBP', 'EUR'):
        currency = request.form.get("change_currency")
        app.logger.info("Updating currency to %s", currency)
        set_user_currency(currency)
        db.session.commit()

        return redirect(url_for('.pay', flow=flow))

    basket = Basket.from_session(current_user, get_user_currency())
    if not any(basket.values()):
        empty_baskets.inc()

        if current_user.is_authenticated:
            basket.load_purchases_from_db()

        if any(basket.values()):
            # We've lost the user's state, but we can still show them all
            # tickets they've reserved and let them empty their basket.
            flash("Your browser doesn't seem to be storing cookies. This may break some parts of the site.")
            app.logger.warn("Basket is empty, so showing reserved tickets (%s)", request.headers.get('User-Agent'))

        else:
            app.logger.info("Basket is empty, redirecting back to choose tickets")
            if view.type == 'tickets':
                flash("Please select at least one ticket to buy.")
            elif view.type == 'hire':
                flash("Please select at least one item to hire.")
            else:
                flash("Please select at least one item to buy.")
            return redirect(url_for('tickets.main', flow=flow))

    if basket.requires_shipping:
        if current_user.is_authenticated:
            shipping = current_user.shipping
        else:
            shipping = None

        form = TicketPaymentShippingForm(obj=shipping)

    else:
        form = TicketPaymentForm()

    form.flow = flow

    if current_user.is_authenticated:
        form.name.data = current_user.name
        del form.email
        if current_user.name != current_user.email and not basket.requires_shipping:
            # FIXME: is this helpful?
            del form.name

    if form.validate_on_submit():
        if Decimal(form.basket_total.data) != Decimal(basket.total):
            # Check that the user's basket approximately matches what we told them they were paying.
            price_changed.inc()
            app.logger.warn("User's basket has changed value %s -> %s", form.basket_total.data, basket.total)
            flash("""The items you selected have changed, possibly because you had two windows open.
                  Please verify that you've selected the correct items.""")
            return redirect(url_for('tickets.pay', flow=flow))

        user = current_user
        if user.is_anonymous:
            try:
                new_user = create_current_user(form.email.data, form.name.data)
            except IntegrityError as e:
                app.logger.warn('Adding user raised %r, possible double-click', e)
                return None

            user = new_user

        elif user.name == user.email:
            user.name = form.name.data

        if form.allow_promo.data:
            user.promo_opt_in = True

        if basket.requires_shipping:
            if not user.shipping:
                user.shipping = UserShipping()

            user.shipping.address_1 = form.address_1.data
            user.shipping.address_2 = form.address_2.data
            user.shipping.town = form.town.data
            user.shipping.postcode = form.postcode.data
            user.shipping.country = form.country.data

        if form.gocardless.data:
            payment_type = GoCardlessPayment
        elif form.banktransfer.data:
            payment_type = BankPayment
        elif form.stripe.data:
            payment_type = StripePayment

        basket.user = user
        payment = basket.create_payment(payment_type)
        basket.cancel_surplus_purchases()
        db.session.commit()

        Basket.clear_from_session()

        if not payment:
            empty_baskets.inc()
            app.logger.warn('User tried to pay for empty basket')
            flash("We're sorry, your session information has been lost. Please try ordering again.")
            return redirect(url_for('tickets.main', flow=flow))

        if payment_type == GoCardlessPayment:
            return gocardless_start(payment)
        elif payment_type == BankPayment:
            return transfer_start(payment)
        elif payment_type == StripePayment:
            return stripe_start(payment)

    form.basket_total.data = basket.total

    return render_template('payment-choose.html', form=form,
                           basket=basket, total=basket.total,
                           flow=flow, view=view)
Ejemplo n.º 18
0
def pay(flow=None):
    if flow is None:
        flow = "main"

    view = ProductView.get_by_name(flow)
    if not view:
        abort(404)

    if view.token and session.get("ticket_token") != view.token:
        if not current_user.is_anonymous and current_user.has_permission("admin"):
            abort(404)

    if request.form.get("change_currency") in ("GBP", "EUR"):
        currency = request.form.get("change_currency")
        app.logger.info("Updating currency to %s", currency)
        set_user_currency(currency)
        db.session.commit()

        return redirect(url_for(".pay", flow=flow))

    basket = Basket.from_session(current_user, get_user_currency())
    if not any(basket.values()):
        empty_baskets.inc()

        if current_user.is_authenticated:
            basket.load_purchases_from_db()

        if any(basket.values()):
            # We've lost the user's state, but we can still show them all
            # tickets they've reserved and let them empty their basket.
            flash(
                "Your browser doesn't seem to be storing cookies. This may break some parts of the site."
            )
            app.logger.warn(
                "Basket is empty, so showing reserved tickets (%s)",
                request.headers.get("User-Agent"),
            )

        else:
            app.logger.info("Basket is empty, redirecting back to choose tickets")
            if view.type == "tickets":
                flash("Please select at least one ticket to buy.")
            elif view.type == "hire":
                flash("Please select at least one item to hire.")
            else:
                flash("Please select at least one item to buy.")
            return redirect(url_for("tickets.main", flow=flow))

    if basket.requires_shipping:
        if current_user.is_authenticated:
            shipping = current_user.shipping
        else:
            shipping = None

        form = TicketPaymentShippingForm(obj=shipping)

    else:
        form = TicketPaymentForm()

    form.flow = flow

    if current_user.is_authenticated:
        form.name.data = current_user.name
        del form.email
        if current_user.name != current_user.email and not basket.requires_shipping:
            # FIXME: is this helpful?
            del form.name

    if form.validate_on_submit():
        if Decimal(form.basket_total.data) != Decimal(basket.total):
            # Check that the user's basket approximately matches what we told them they were paying.
            price_changed.inc()
            app.logger.warn(
                "User's basket has changed value %s -> %s",
                form.basket_total.data,
                basket.total,
            )
            flash(
                """The items you selected have changed, possibly because you had two windows open.
                  Please verify that you've selected the correct items."""
            )
            return redirect(url_for("tickets.pay", flow=flow))

        user = current_user
        if user.is_anonymous:
            try:
                new_user = create_current_user(form.email.data, form.name.data)
            except IntegrityError as e:
                app.logger.warn("Adding user raised %r, possible double-click", e)
                return None

            user = new_user

        elif user.name == user.email:
            user.name = form.name.data

        if form.allow_promo.data:
            user.promo_opt_in = True

        if basket.requires_shipping:
            if not user.shipping:
                user.shipping = UserShipping()

            user.shipping.address_1 = form.address_1.data
            user.shipping.address_2 = form.address_2.data
            user.shipping.town = form.town.data
            user.shipping.postcode = form.postcode.data
            user.shipping.country = form.country.data

        if form.gocardless.data:
            payment_type = GoCardlessPayment
        elif form.banktransfer.data:
            payment_type = BankPayment
        elif form.stripe.data:
            payment_type = StripePayment

        basket.user = user
        payment = basket.create_payment(payment_type)
        basket.cancel_surplus_purchases()
        db.session.commit()

        Basket.clear_from_session()

        if not payment:
            empty_baskets.inc()
            app.logger.warn("User tried to pay for empty basket")
            flash(
                "We're sorry, your session information has been lost. Please try ordering again."
            )
            return redirect(url_for("tickets.main", flow=flow))

        if payment_type == GoCardlessPayment:
            return gocardless_start(payment)
        elif payment_type == BankPayment:
            return transfer_start(payment)
        elif payment_type == StripePayment:
            return stripe_start(payment)

    form.basket_total.data = basket.total

    return render_template(
        "tickets/payment-choose.html",
        form=form,
        basket=basket,
        total=basket.total,
        flow=flow,
        view=view,
    )
Ejemplo n.º 19
0
def get_product_view(flow):
    view = ProductView.get_by_name(flow)
    if not view:
        abort(404)
    return view
Ejemplo n.º 20
0
def create_product_groups():
    top_level_groups = [
        # name, capacity, expires
        ("admissions", None, 2500),
        ("parking", None, None),
        ("campervan", None, None),
        ("merchandise", None, None),
    ]
    for name, expires, capacity in top_level_groups:
        if ProductGroup.get_by_name(name):
            continue
        pg = ProductGroup(name=name,
                          type=name,
                          capacity_max=capacity,
                          expires=expires)
        db.session.add(pg)

    db.session.flush()

    allocations = [
        # name, capacity
        ("vendors", 100),
        ("sponsors", 200),
        ("speakers", 100),
        ("general", 1800),
    ]

    admissions = ProductGroup.get_by_name("admissions")
    for name, capacity in allocations:
        if ProductGroup.get_by_name(name):
            continue
        ProductGroup(name=name, capacity_max=capacity, parent=admissions)

    view = ProductView.get_by_name("main")
    if not view:
        view = ProductView(name="main", type="tickets")
        db.session.add(view)

    db.session.flush()

    general = ProductGroup.get_by_name("general")

    products = [
        # name, display name, transferable, badge, capacity, description, (std cap, gbp eur), (early cap, gbp, eur), (late cap, gbp, eur)
        (
            "full",
            "Full Camp Ticket",
            True,
            True,
            None,
            "Full ticket",
            ((1500, 115, 135), (250, 105, 125), (None, 125, 145)),
        ),
        (
            "full-s",
            "Full Camp Ticket (Supporter)",
            True,
            True,
            None,
            "Support this non-profit event by paying a bit more. All money will go towards making EMF more awesome.",
            ((None, 150, 180), ),
        ),
        (
            "full-sg",
            "Full Camp Ticket (Gold Supporter)",
            True,
            True,
            None,
            "Support this non-profit event by paying a bit more. All money will go towards making EMF more awesome.",
            ((None, 200, 240), ),
        ),
        (
            "u18",
            "Under-18",
            True,
            False,
            150,
            "For visitors born after August 30th, 2000. All under-18s must be accompanied by an adult.",
            ((None, 55, 63), ),
        ),
        (
            "u12",
            "Under-12",
            True,
            False,
            50,
            "For children born after August 30th, 2006. All children must be accompanied by an adult.",
            ((None, 0, 0), ),
        ),
    ]

    order = 0

    for (
            name,
            display_name,
            has_xfer,
            has_badge,
            capacity,
            description,
            prices,
    ) in products:
        if Product.get_by_name("general", name):
            continue
        product = Product(
            name=name,
            display_name=display_name,
            capacity_max=capacity,
            description=description,
            parent=general,
            attributes={
                "is_transferable": has_xfer,
                "has_badge": has_badge
            },
        )

        for index, (price_cap, gbp, eur) in enumerate(prices):
            if len(prices) == 1 or index == 0:
                tier_name = name + "-std"
                active = True

            elif index == 1:
                tier_name = name + "-early-bird"
                active = False

            elif index == 2:
                tier_name = name + "-late"
                active = False

            if PriceTier.get_by_name("general", "name", tier_name):
                continue

            pt = PriceTier(
                name=tier_name,
                capacity_max=price_cap,
                personal_limit=10,
                parent=product,
                active=active,
            )
            Price(currency="GBP", price_int=gbp * 100, price_tier=pt)
            Price(currency="EUR", price_int=eur * 100, price_tier=pt)

        ProductViewProduct(view, product, order)
        order += 1

    db.session.flush()

    misc = [
        # name, display_name, cap, personal_limit, gbp, eur, description
        (
            "parking",
            "Parking Ticket",
            1700,
            4,
            15,
            21,
            "We're trying to keep cars to a minimum. Please take public transport or car-share if you can.",
        ),
        (
            "campervan",
            "Caravan/\u200cCampervan Ticket",
            60,
            2,
            30,
            42,
            "If you bring a caravan, you won't need a separate parking ticket for the towing car.",
        ),
    ]

    for name, display_name, cap, personal_limit, gbp, eur, description in misc:
        if Product.get_by_name(name, name):
            continue

        group = ProductGroup.get_by_name(name)
        product = Product(name=name,
                          display_name=display_name,
                          description=description,
                          parent=group)
        pt = PriceTier(name=name,
                       personal_limit=personal_limit,
                       parent=product)
        db.session.add(pt)
        db.session.add(
            Price(currency="GBP", price_int=gbp * 100, price_tier=pt))
        db.session.add(
            Price(currency="EUR", price_int=eur * 100, price_tier=pt))

        ProductViewProduct(view, product, order)
        order += 1

    db.session.commit()