Beispiel #1
0
class Waiting(DB.Model):
    """Model for entries on the waiting list."""
    __tablename__ = 'waiting'
    object_id = DB.Column(DB.Integer, primary_key=True)

    waiting_since = DB.Column(DB.DateTime(), nullable=False)
    waiting_for = DB.Column(DB.Integer(), nullable=False)

    user_id = DB.Column(DB.Integer,
                        DB.ForeignKey('user.object_id'),
                        nullable=False)
    user = DB.relationship('User',
                           backref=DB.backref('waiting', lazy='dynamic'),
                           foreign_keys=[user_id])

    def __init__(self, user, waiting_for):
        self.user = user
        self.waiting_for = waiting_for

        self.waiting_since = datetime.datetime.utcnow()

    def __repr__(self):
        return '<Waiting: {0} for {1} ticket{2}>'.format(
            self.user.full_name, self.waiting_for,
            '' if self.waiting_for == 1 else 's')
class Transaction(DB.Model):
    """Model for representing a monetary exchange transaction."""
    __tablename__ = 'transaction'
    object_id = DB.Column(DB.Integer, primary_key=True)

    payment_method = DB.Column(DB.Enum('Battels', 'Card', 'Free', 'Dummy'),
                               nullable=False)

    paid = DB.Column(DB.Boolean, default=False, nullable=False)
    created = DB.Column(DB.DateTime(), nullable=False)

    user_id = DB.Column(DB.Integer,
                        DB.ForeignKey('user.object_id'),
                        nullable=False)
    user = DB.relationship('User',
                           backref=DB.backref('transactions', lazy='dynamic'))

    __mapper_args__ = {'polymorphic_on': payment_method}

    def __init__(self, user, payment_method):
        self.user = user
        self.payment_method = payment_method

        self.created = datetime.datetime.utcnow()

    def __repr__(self):
        return '<Transaction {0}: {1} item(s)>'.format(self.object_id,
                                                       self.items.count())

    @property
    def value(self):
        """Get the total value of the transaction."""
        return sum(item.value for item in self.items)

    @property
    def value_pounds(self):
        """Get the total value of the transaction."""
        value_str = "{0:03d}".format(self.value)

        return value_str[:-2] + '.' + value_str[-2:]

    @property
    def tickets(self):
        """Get the tickets paid for in this transaction.

        Returns a list of Ticket objects.
        """
        return list(item.ticket for item in self.items
                    if item.item_type == 'Ticket')

    @property
    def postage(self):
        """Get the postage paid for in this transaction.

        Returns a single Postage object, or None.
        """
        try:
            return list(item.postage for item in self.items
                        if item.item_type == 'Postage')[0]
        except IndexError:
            return None

    @property
    def admin_fee(self):
        """Get the admin_fee paid for in this transaction.

        Returns a single AdminFee object, or None.
        """
        try:
            return list(item.admin_fee for item in self.items
                        if item.item_type == 'AdminFee')[0]
        except IndexError:
            return None

    def mark_as_paid(self):
        """Mark the transaction as paid for.

        Marks all tickets in the transaction as paid for.
        """
        self.paid = True

        for ticket in self.tickets:
            ticket.mark_as_paid()

        postage = self.postage
        if postage:
            postage.paid = True

        admin_fee = self.admin_fee
        if admin_fee:
            admin_fee.mark_as_paid()
Beispiel #3
0
class EwayTransaction(DB.Model):
    """Model for representing an eWay Transaction."""
    __tablename__ = 'eway_transaction'
    object_id = DB.Column(DB.Integer, primary_key=True)

    # This holds the order_id
    access_code = DB.Column(
        DB.Unicode(200),
        nullable=False
    )
    charged = DB.Column(
        DB.Integer(),
        nullable=False
    )

    completed = DB.Column(
        DB.DateTime(),
        nullable=True
    )
    result_code = DB.Column(
        DB.Unicode(2),
        nullable=True
    )
    # This holds the PASREF field for realex
    eway_id = DB.Column(
        DB.String(50),
        nullable=True
    )
    refunded = DB.Column(
        DB.Integer(),
        nullable=False,
        default=0
    )

    def __init__(self, access_code, charged):
        self.access_code = access_code
        self.charged = charged

    @property
    def status(self):
        """Get a better representation of the status of this transaction.

        The eWay API returns statuses as 2 digit codes; this function provides
        a mapping from these codes to a boolean success value and associated
        explanation.

        Returns:
            (bool, str) pair of success value and explanation
        """
        try:
            return REALEX_RESULT_CODES[self.result_code[0]]
        except KeyError as err:
            return (False, 'Unknown response: {0}'.format(err.args[0]))

    @property
    def success(self):
        """Get whether the transaction was completed successfully."""
        success = self.status[0]
        if success is None:
            return 'Uncompleted'
        elif success:
            return 'Successful'
        else:
            return 'Unsuccessful'
Beispiel #4
0
class User(DB.Model):
    """Model for users."""
    __tablename__ = 'user'

    object_id = DB.Column(DB.Integer, primary_key=True)

    # Class level properties for Flask-Login
    #
    # Sessions don't expire, and no users are anonymous, so these can be hard
    # coded
    is_authenticated = True
    is_anonymous = False

    email = DB.Column(DB.Unicode(120), unique=True, nullable=False)
    new_email = DB.Column(DB.Unicode(120), unique=True, nullable=True)
    password_hash = DB.Column(DB.BINARY(60), nullable=False)
    forenames = DB.Column(DB.Unicode(120), nullable=False)
    surname = DB.Column(DB.Unicode(120), nullable=False)
    full_name = DB.column_property(forenames + ' ' + surname)
    phone = DB.Column(DB.Unicode(20), nullable=False)
    phone_verification_code = DB.Column(DB.Unicode(6), nullable=True)
    phone_verified = DB.Column(DB.Boolean, nullable=False, default=False)
    secret_key = DB.Column(DB.Unicode(64), nullable=True)
    secret_key_expiry = DB.Column(DB.DateTime(), nullable=True)
    verified = DB.Column(DB.Boolean, default=False, nullable=False)
    deleted = DB.Column(DB.Boolean, default=False, nullable=False)
    note = DB.Column(DB.UnicodeText, nullable=True)
    role = DB.Column(DB.Enum('User', 'Admin'), nullable=False)

    college_id = DB.Column(DB.Integer,
                           DB.ForeignKey('college.object_id'),
                           nullable=False)
    college = DB.relationship('College',
                              backref=DB.backref('users', lazy='dynamic'))

    affiliation_id = DB.Column(DB.Integer,
                               DB.ForeignKey('affiliation.object_id'),
                               nullable=False)
    affiliation = DB.relationship('Affiliation',
                                  backref=DB.backref('users', lazy='dynamic'))

    battels_id = DB.Column(DB.Integer,
                           DB.ForeignKey('battels.object_id'),
                           nullable=True)
    battels = DB.relationship('Battels',
                              backref=DB.backref('user', uselist=False))

    affiliation_verified = DB.Column(DB.Boolean, default=False, nullable=True)

    photo_id = DB.Column(DB.Integer,
                         DB.ForeignKey('photo.object_id'),
                         nullable=True)
    photo = DB.relationship('Photo', backref=DB.backref('user', uselist=False))

    def has_tickets(self):
        if ticket.Ticket.query.filter_by(owner_id=self.object_id).count() > 0:
            return True
        return False

    def has_unpaid_tickets(self):
        if ticket.Ticket.query.filter_by(owner_id=self.object_id,
                                         paid=0).count() > 0:
            return True
        return False

    def has_paid_tickets(self):
        if ticket.Ticket.query.filter_by(owner_id=self.object_id,
                                         paid=1).count() > 0:
            return True
        return False

    def can_claim_ticket(self):
        return True

#todo: check logic

    def has_collected_tickets(self):
        return False

    def has_uncollected_tickets(self):
        return False

    def has_held_ticket(self):
        # if ticket.Ticket.query.filter_by(holder_id=self.object_id):
        #     return True
        return False

    def can_update_details(self):
        return APP.config['ENABLE_CHANGING_DETAILS']

    def __init__(self, email, password, forenames, surname, phone, college,
                 affiliation, photo):
        self.email = email
        self.forenames = forenames
        self.surname = surname
        self.phone = phone
        self.college = college
        self.affiliation = affiliation
        self.photo = photo

        self.set_password(password)

        self.secret_key = util.generate_key(64)
        self.verified = False
        self.deleted = False
        self.role = 'User'
        if affiliation.name == 'None':
            self.affiliation_verified = True
        else:
            self.affiliation_verified = False
            #todo add logic for checking if they are on the member list

        self.battels = battels.Battels.query.filter(
            battels.Battels.email == email).first()

    def __repr__(self):
        return '<User {0}: {1} {2}>'.format(self.object_id, self.forenames,
                                            self.surname)

    def can_pay_by_battels(self):
        return False

    @property
    def group_purchase_requests(self):
        """Get this user's group purchase requests.

        Not just a database backref so that old requests can hang around when
        the user leaves a group.
        """
        if self.purchase_group:
            for request in self.purchase_group.requests:
                if request.requester == self:
                    yield request

    def group_purchase_requested(self, ticket_type_slug):
        """Get how many of a given ticket type the user has requested."""
        for request in self.group_purchase_requests:
            if request.ticket_type_slug == ticket_type_slug:
                return request.number_requested

        return 0

    @property
    def total_group_purchase_requested(self):
        """Get the total number of tickets requested by this user."""
        return sum(request.number_requested
                   for request in self.group_purchase_requests)

    @property
    def total_group_purchase_value(self):
        """Get the total number of tickets requested by this user."""
        value = '{0:03d}'.format(
            sum(request.value for request in self.group_purchase_requests))

        return value[:-2] + '.' + value[-2:]

    def check_password(self, candidate):
        """Check if a password matches the hash stored for the user.

        Runs the bcrypt.Bcrypt checking routine to validate the password.

        Args:
            candidate: (str) the candidate password

        Returns:
            (bool) whether the candidate password matches the stored hash
        """
        return BCRYPT.check_password_hash(self.password_hash, candidate)

    def set_password(self, password):
        """Set the password for the user.

        Hashes the password using bcrypt and stores the resulting hash.

        Args:
            password: (str) new password for the user.
        """
        self.password_hash = BCRYPT.generate_password_hash(password)

    def promote(self):
        """Make the user an admin."""
        self.role = 'Admin'

    def demote(self):
        """Make the user an ordinary user (no admin privileges)"""
        self.role = 'User'

    @property
    def is_admin(self):
        """Check if the user is an admin, or is currently being impersonated.

        For future-proofing purposes, the role of the impersonating user is also
        checked.
        """
        return self.role == 'Admin' or (
            'actor_id' in flask.session
            and User.get_by_id(flask.session['actor_id']).role == 'Admin')

    @property
    def is_waiting(self):
        """Is the user on the waiting list?"""
        return self.waiting.count() > 0

    @property
    def active_tickets(self):
        """Get the active tickets owned by the user."""
        return self.tickets.filter(ticket.Ticket.cancelled == False  # pylint: disable=singleton-comparison
                                   )

    @property
    def active_ticket_count(self):
        """How many active tickets does the user own?"""
        return self.active_tickets.count()

    @property
    def waiting_for(self):
        """How many tickets is the user waiting for?"""
        return sum([x.waiting_for for x in self.waiting])

    @property
    def is_verified(self):
        """Has the user's email address been verified?"""
        return self.verified

    @property
    def is_deleted(self):
        """Has the user been deleted?

        In order to maintain referential integrity, when a user is deleted we
        scrub their personal details, but maintain the user object referenced by
        log entries, tickets, transactions etc.
        """
        return self.deleted

    @property
    def is_active(self):
        """Is the user active?

        This method is specifically for the use of the Flask-Login extension,
        and refers to whether the user can log in.
        """
        return self.is_verified and not self.is_deleted

    def get_id(self):
        """What is this user's ID?

        This method is specifically for the use of the Flask-Login extension,
        and is a defined class method which returns a unique identifier for the
        user, in this case their database ID.
        """
        return unicode(self.object_id)

    @staticmethod
    def get_by_email(email):
        """Get a user object by the user's email address."""
        user = User.query.filter(User.email == email).first()

        if not user:
            return None

        return user

    def add_manual_battels(self):
        """Manually add a battels account for the user

        If we don't have a battels account automatically matched to the user,
        the admin can manually create one for them.
        """
        self.battels = battels.Battels.query.filter(
            battels.Battels.email == self.email).first()

        if not self.battels:
            self.battels = battels.Battels(None, self.email, None,
                                           self.surname, self.forenames, True)
            DB.session.add(self.battels)

        DB.session.commit()

    @staticmethod
    def write_csv_header(csv_writer):
        """Write the header of a CSV export file."""
        csv_writer.writerow([
            'User ID',
            'Email',
            'Forenames',
            'Surname',
            'Phone Number',
            'Notes',
            'Role',
            'College',
            'Affiliation',
            'Battels ID',
        ])

    def write_csv_row(self, csv_writer):
        """Write this object as a row in a CSV export file."""
        csv_writer.writerow([
            self.object_id,
            self.email,
            self.forenames,
            self.surname,
            self.phone,
            self.note,
            self.role,
            self.college.name,
            self.affiliation.name,
            self.battels.battels_id if self.battels is not None else 'N/A',
        ])
Beispiel #5
0
class Ticket(DB.Model):
    """Model for tickets."""
    __tablename__ = 'ticket'
    object_id = DB.Column(DB.Integer, primary_key=True)

    ticket_type = DB.Column(DB.Unicode(50), nullable=False)

    paid = DB.Column(DB.Boolean(), default=False, nullable=False)
    entered = DB.Column(DB.Boolean(), default=False, nullable=False)
    cancelled = DB.Column(DB.Boolean(), default=False, nullable=False)

    price_ = DB.Column(DB.Integer(), nullable=False)
    note = DB.Column(DB.UnicodeText(), nullable=True)
    expires = DB.Column(DB.DateTime(), nullable=True)
    barcode = DB.Column(DB.Unicode(20), unique=True, nullable=True)
    claim_code = DB.Column(
        DB.Unicode(17),  # 3 groups of 5 digits separated by dashes
        nullable=True)
    claims_made = DB.Column(DB.Integer, nullable=False, default=0)
    owner_id = DB.Column(DB.Integer,
                         DB.ForeignKey('user.object_id'),
                         nullable=False)
    owner = DB.relationship('User',
                            backref=DB.backref('tickets',
                                               lazy='dynamic',
                                               order_by=b'Ticket.cancelled'),
                            foreign_keys=[owner_id])

    holder_id = DB.Column(DB.Integer,
                          DB.ForeignKey('user.object_id'),
                          nullable=True)
    holder = DB.relationship('User',
                             backref=DB.backref('held_ticket', uselist=False),
                             foreign_keys=[holder_id])
    holder_name = DB.Column(DB.String(60), nullable=True, default="Unassigned")

    def __init__(self, owner, ticket_type, price):
        self.owner = owner
        self.ticket_type = ticket_type
        self.price = price

        self.expires = (datetime.datetime.utcnow() +
                        APP.config['TICKET_EXPIRY_TIME'])

        self.claim_code = '-'.join(
            util.generate_key(5, string.digits)
            for _ in xrange(3)).decode('utf-8')

    def __repr__(self):
        return '<Ticket {0} owned by {1} ({2})>'.format(
            self.object_id, self.owner.full_name, self.owner.object_id)

    def can_be_cancelled(self):
        if datetime.datetime.utcnow() < datetime.datetime(
                2018, 5, 11) and not self.cancelled and not self.paid:
            return True
        return False

    def can_be_resold(self):
        return False

    def can_be_claimed(self):
        if self.status == 'Awaiting ticket holder.':
            return True
        return False

    def can_be_reclaimed(self):
        # if self.paid and datetime.datetime.utcnow()<datetime.datetime(2018,5,11) and not self.cancelled and self.holder_id==self.owner_id:
        #     return True
        return False

    def has_holder(self):
        if not self.holder_id == None:
            return True
        return False

    def can_be_paid_for(self):
        if self.paid == 0:
            return True
        return False

    def can_be_collected(self):
        if self.paid:
            return True
        return False

    def is_assigned(self):
        if self.holder_name == 'Unassigned':
            return False
        return True

    @staticmethod
    def get_by_claim_code(code):
        return Ticket.query.filter_by(claim_code=code).first()

    @property
    def price_pounds(self):
        """Get the price of this ticket as a string of pounds and pence."""
        price = '{0:03d}'.format(self.price)
        return price[:-2] + '.' + price[-2:]

    @property
    def transaction(self):
        """Get the transaction this ticket was paid for in."""
        for transaction_item in self.transaction_items:
            if transaction_item.transaction.paid:
                return transaction_item.transaction

        return None

    @property
    def payment_method(self):
        """Get the payment method for this ticket."""
        transaction = self.transaction

        if transaction:
            return transaction.payment_method
        else:
            return 'Unknown Payment Method'

    @property
    def price(self):
        """Get the price of the ticket."""
        return self.price_

    @property
    def status(self):
        """Get the status of this ticket."""
        if self.cancelled:
            return 'Cancelled.'
        elif not self.paid:
            return 'Awaiting payment. Expires {0}.'.format(
                self.expires.strftime('%H:%M %d/%m/%Y'))
        elif self.entered:
            return 'Used for entry.'
        elif self.collected and self.holder:
            return 'Ticket Sent to {0}.'.format(self.holder.full_name)
        elif self.collected:
            return 'Collected as {0}.'.format(self.barcode)
        # elif self.holder is None:
        #     return 'Awaiting ticket holder.'
        elif self.holder_name == 'Unassigned':
            return 'Awaiting ticket holder.'
        elif APP.config['REQUIRE_USER_PHOTO']:
            if not self.holder.photo.verified:
                return 'Awaiting verification of holder photo.'
        else:
            # return 'Held by {0}.'.format(self.holder.full_name)
            return 'Assigned to {0}.'.format(self.holder_name)

    @price.setter
    def price(self, value):
        """Set the price of the ticket."""
        self.price_ = max(value, 0)

        if self.price_ == 0:
            self.mark_as_paid()

    @hybrid.hybrid_property
    def collected(self):
        """Has this ticket been assigned a barcode."""
        return self.barcode != None  # pylint: disable=singleton-comparison

    def mark_as_paid(self):
        """Mark the ticket as paid, and clear any expiry."""
        self.paid = True
        self.expires = None

    def add_note(self, note):
        """Add a note to the ticket."""
        if not note.endswith('\n'):
            note = note + '\n'

        if self.note is None:
            self.note = note
        else:
            self.note = self.note + note

    @staticmethod
    def count():
        """How many tickets have been sold."""
        # TODO
        return Ticket.query.filter(Ticket.cancelled == False).count()  # pylint: disable=singleton-comparison

    @staticmethod
    def write_csv_header(csv_writer):
        """Write the header of a CSV export file."""
        csv_writer.writerow([
            'Ticket ID',
            'Ticket Type',
            'Paid',
            'Collected',
            'Entered',
            'Cancelled',
            'Price (Pounds)',
            'Holder\'s Name',
            'Notes',
            'Expires',
            'Barcode',
            'Owner\' User ID',
            'Owner\'s Name',
        ])

    def write_csv_row(self, csv_writer):
        """Write this object as a row in a CSV export file."""
        csv_writer.writerow([
            self.object_id,
            self.ticket_type,
            'Yes' if self.paid else 'No',
            'Yes' if self.collected else 'No',
            'Yes' if self.entered else 'No',
            'Yes' if self.cancelled else 'No',
            self.price_pounds,
            self.holder.full_name.encode('utf-8')
            if self.holder is not None else 'N/A',
            self.note,
            self.expires.strftime('%Y-%m-%d %H:%M:%S')
            if self.expires is not None else 'N/A',
            self.barcode if self.barcode is not None else 'N/A',
            self.owner_id,
            self.owner.full_name.encode('utf-8'),
        ])
class Announcement(DB.Model):
    """Model for an announcement sent to registered users."""
    __tablename__ = 'announcement'
    object_id = DB.Column(DB.Integer, primary_key=True)

    timestamp = DB.Column(DB.DateTime(), nullable=False)
    content = DB.Column(DB.UnicodeText(65536), nullable=False)
    subject = DB.Column(DB.UnicodeText(256), nullable=False)
    send_email = DB.Column(DB.Boolean, default=True, nullable=False)
    use_noreply = DB.Column(DB.Boolean, default=False, nullable=False)
    email_sent = DB.Column(DB.Boolean, default=False, nullable=False)

    sender_id = DB.Column(DB.Integer,
                          DB.ForeignKey('user.object_id'),
                          nullable=False)
    sender = DB.relationship('User',
                             backref=DB.backref('announcements-sent',
                                                lazy='dynamic'))

    college_id = DB.Column(DB.Integer,
                           DB.ForeignKey('college.object_id'),
                           nullable=True)
    college = DB.relationship('College',
                              backref=DB.backref('announcements',
                                                 lazy='dynamic'))

    affiliation_id = DB.Column(DB.Integer,
                               DB.ForeignKey('affiliation.object_id'),
                               nullable=True)
    affiliation = DB.relationship('Affiliation',
                                  backref=DB.backref('announcements-received',
                                                     lazy='dynamic'))

    is_waiting = DB.Column(DB.Boolean, nullable=True)
    has_tickets = DB.Column(DB.Boolean, nullable=True)
    holds_ticket = DB.Column(DB.Boolean, nullable=True)
    has_collected = DB.Column(DB.Boolean, nullable=True)
    has_uncollected = DB.Column(DB.Boolean, nullable=True)

    users = DB.relationship('User',
                            secondary=USER_ANNOUNCE_LINK,
                            backref='announcements')

    emails = DB.relationship('User',
                             secondary=EMAIL_ANNOUNCE_LINK,
                             lazy='dynamic')

    def __init__(self,
                 subject,
                 content,
                 sender,
                 send_email,
                 college=None,
                 affiliation=None,
                 has_tickets=None,
                 holds_ticket=None,
                 is_waiting=None,
                 has_collected=None,
                 has_uncollected=None,
                 use_noreply=False):
        self.timestamp = datetime.datetime.utcnow()
        self.subject = subject
        self.content = content
        self.sender = sender
        self.send_email = send_email
        self.use_noreply = use_noreply
        self.college = college
        self.affiliation = affiliation
        self.has_tickets = has_tickets
        self.holds_ticket = holds_ticket
        self.is_waiting = is_waiting
        self.has_collected = has_collected
        self.has_uncollected = has_uncollected

        recipient_query = user.User.query

        if self.college is not None:
            recipient_query = recipient_query.filter(
                user.User.college == self.college)

        if self.affiliation is not None:
            recipient_query = recipient_query.filter(
                user.User.affiliation == self.affiliation)

        for recipient in recipient_query.all():
            if ((  # pylint: disable=too-many-boolean-expressions
                    self.has_tickets is None
                    or recipient.has_tickets() == self.has_tickets)
                    and (self.holds_ticket is None
                         or recipient.has_held_ticket() == self.holds_ticket)
                    and (self.is_waiting is None
                         or recipient.is_waiting == self.is_waiting) and
                (self.has_collected is None
                 or recipient.has_collected_tickets() == self.has_collected)
                    and (self.has_uncollected is None or
                         (recipient.has_uncollected_tickets()
                          == self.has_uncollected))):
                self.users.append(recipient)
                if send_email:
                    self.emails.append(recipient)

    def __repr__(self):
        return '<Announcement {0}: {1}>'.format(self.object_id, self.subject)

    def send_emails(self, count):
        """Send the announcement as an email to a limited number of recipients.

        Used for batch sending, renders the text of the announcement into an
        email and sends it to users who match the criteria.

        Args:
            count: (int) Maximum number of emails to send

        Returns:
            (int) How much of the original limit is remaining (i.e. |count|
            minus the nuber of emails sent)
        """
        if self.use_noreply:
            sender = APP.config['EMAIL_FROM']
        else:
            sender = self.sender.email

        try:
            for recipient in self.emails:
                if count <= 0:
                    break

                APP.email_manager.send_text(recipient.email, self.subject,
                                            self.content, sender)

                self.emails.remove(recipient)
                count = count - 1
        finally:
            self.email_sent = (self.emails.count() == 0)
            DB.session.commit()

        return count
Beispiel #7
0
class Voucher(DB.Model):
    """Model for a discount voucher."""
    __tablename__ = 'voucher'
    object_id = DB.Column(DB.Integer, primary_key=True)

    code = DB.Column(DB.Unicode(30), nullable=False)
    expires = DB.Column(DB.DateTime(), nullable=True)
    discount_type = DB.Column(DB.Enum('Fixed Price', 'Fixed Discount',
                                      'Percentage Discount'),
                              nullable=False)
    discount_value = DB.Column(DB.Integer(), nullable=False)
    applies_to = DB.Column(DB.Enum('Ticket', 'Transaction'), nullable=False)
    single_use = DB.Column(DB.Boolean(), nullable=False)
    used = DB.Column(DB.Boolean(), default=False, nullable=True)

    used_by_id = DB.Column(DB.Integer,
                           DB.ForeignKey('user.object_id'),
                           nullable=True)
    used_by = DB.relationship('User',
                              backref=DB.backref('vouchers_used',
                                                 lazy='dynamic'))

    def __init__(self, code, expires, discount_type, discount_value,
                 applies_to, single_use):
        if discount_type not in [
                'Fixed Price', 'Fixed Discount', 'Percentage Discount'
        ]:
            raise ValueError(
                '{0} is not a valid discount type'.format(discount_type))

        if applies_to not in ['Ticket', 'Transaction']:
            raise ValueError(
                '{0} is not a valid application'.format(applies_to))

        self.code = code
        self.discount_type = discount_type
        self.discount_value = discount_value
        self.applies_to = applies_to
        self.single_use = single_use

        if isinstance(expires, datetime.timedelta):
            self.expires = datetime.datetime.utcnow() + expires
        else:
            self.expires = expires

    def __repr__(self):
        return '<Voucher: {0}/{1}>'.format(self.object_id, self.code)

    @staticmethod
    def get_by_code(code):
        """Get an Announcement object by a voucher code."""
        return Voucher.query.filter(Voucher.code == code).first()

    def apply(self, tickets, user):
        """Apply the voucher to a set of tickets.

        Checks if the voucher can be used, and applies its discount to the
        tickets.

        Args:
            tickets: (list(Ticket)) list of tickets to apply the voucher to
            user: (User) user who is using the voucher

        Returns:
            (bool, list(tickets), str/None) whether the voucher was applied, the
            mutated tickets, and an error message
        """
        if self.single_use and self.used:
            return (False, tickets, 'Voucher has already been used.')

        if (self.expires is not None
                and self.expires < datetime.datetime.utcnow()):
            return (False, tickets, 'Voucher has expired.')

        self.used = True
        if self.single_use:
            self.used_by = user

        if self.applies_to == 'Ticket':
            tickets[0] = self.apply_to_ticket(tickets[0])
            return (True, tickets, None)
        else:
            return (True, [self.apply_to_ticket(t) for t in tickets], None)

    def apply_to_ticket(self, ticket):
        """Apply the voucher to a single ticket.

        Recalculates the price of the ticket, and notes on the ticket that a
        voucher was used

        Args:
            ticket: (Ticket) the ticket to apply the voucher to

        Returns:
            (ticket) the mutated ticket
        """
        if self.discount_type == 'Fixed Price':
            ticket.price = self.discount_value
        elif self.discount_type == 'Fixed Discount':
            ticket.price = ticket.price - self.discount_value
        else:
            ticket.price = ticket.price * (100 - self.discount_value) / 100

        ticket.add_note('Used voucher {0}/{1}'.format(self.object_id,
                                                      self.code))

        return ticket