class Postage(DB.Model): """Model for representing postage for one or more tickets.""" __tablename__ = 'postage' object_id = DB.Column(DB.Integer, primary_key=True) paid = DB.Column(DB.Boolean(), default=False, nullable=False) postage_type = DB.Column(DB.Unicode(50), nullable=False) price = DB.Column(DB.Integer(), nullable=False) address = DB.Column(DB.Unicode(200), nullable=True) cancelled = DB.Column(DB.Boolean(), default=False, nullable=False) posted = DB.Column(DB.Boolean(), default=False, nullable=False) tickets = DB.relationship('Ticket', secondary=POSTAGE_TICKET_LINK, backref=DB.backref('postage', lazy=False, uselist=False), lazy='dynamic') owner_id = DB.Column(DB.Integer, DB.ForeignKey('user.object_id'), nullable=True) owner = DB.relationship('User', backref=DB.backref('postage_entries', lazy='dynamic')) def __init__(self, owner, postage_option, tickets, address=None): self.owner = owner self.postage_type = postage_option.name self.price = postage_option.price self.address = address self.tickets = tickets if self.price == 0: self.paid = True
class Photo(DB.Model): """Model for representing a photo stored on S3.""" __tablename__ = 'photo' object_id = DB.Column(DB.Integer, primary_key=True) filename = DB.Column(DB.Unicode(50), nullable=False) full_url = DB.Column(DB.Unicode(250), nullable=False) thumb_url = DB.Column(DB.Unicode(250), nullable=False) verified = DB.Column(DB.Boolean, nullable=True) def __init__(self, filename, full_url, thumb_url): self.filename = filename self.full_url = full_url self.thumb_url = thumb_url def __repr__(self): return '<Photo {0}: {1}>'.format(self.object_id, self.filename)
class College(DB.Model): """Model for a user's college.""" __tablename__ = 'college' object_id = DB.Column(DB.Integer, primary_key=True) name = DB.Column(DB.Unicode(50), unique=True, nullable=False) def __init__(self, name): self.name = name def __repr__(self): return '<College {0}: {1}>'.format(self.object_id, self.name)
class Affiliation(DB.Model): """Model for representing a users affiliation to their college.""" __tablename__ = 'affiliation' object_id = DB.Column(DB.Integer, primary_key=True) name = DB.Column( DB.Unicode(25), nullable=False ) def __init__(self, name): self.name = name def __repr__(self): return '<Affiliation {0}: {1}>'.format(self.object_id, self.name)
class GenericTransactionItem(transaction_item.TransactionItem): """Model for representing a generic item in a transaction.""" __tablename__ = 'generic_transaction_item' __mapper_args__ = {'polymorphic_identity': 'Generic'} object_id = DB.Column(DB.Integer, primary_key=True) object_id = DB.Column(DB.Integer(), DB.ForeignKey('transaction_item.object_id'), primary_key=True) value = DB.Column(DB.Integer(), nullable=True) description = DB.Column(DB.Unicode(100), nullable=True) def __init__(self, transaction, value, description): super(GenericTransactionItem, self).__init__(transaction, 'Generic') self.value = value self.description = description
class BattelsTransaction(transaction.Transaction): """Model for representing a battels transaction.""" __tablename__ = 'battels_transaction' __mapper_args__ = {'polymorphic_identity': 'Battels'} object_id = DB.Column(DB.Integer, primary_key=True) object_id = DB.Column( DB.Integer(), DB.ForeignKey('transaction.object_id'), primary_key=True ) battels_term = DB.Column( DB.Unicode(4), nullable=True ) def __init__(self, user): super(BattelsTransaction, self).__init__(user, 'Battels') def __repr__(self): return '<BattelsTransaction {0}: {1} item(s)>'.format( self.object_id, self.items.count() ) def charge(self, term): """Charge this transaction to the user's battels account.""" self.battels_term = term self.user.battels.charge(self.value, term) self.mark_as_paid() APP.log_manager.log_event( 'Completed Battels Payment', tickets=self.tickets, user=self.user, transaction=self, commit=False )
class Statistic(DB.Model): """Model for representing a statistic in a timeseries.""" __tablename__ = 'statistic' object_id = DB.Column(DB.Integer, primary_key=True) timestamp = DB.Column(DB.DateTime, nullable=False) group = DB.Column(DB.Enum(*STATISTIC_GROUPS.keys()), nullable=False) statistic = DB.Column(DB.Unicode(25), nullable=False) value = DB.Column(DB.Integer(), nullable=False) def __init__(self, group, statistic, value): if group not in STATISTIC_GROUPS: raise ValueError( '{0} is not a valid statistic group'.format(group)) self.timestamp = datetime.datetime.utcnow() self.group = group self.statistic = statistic self.value = value def __repr__(self): return '<Statistic {0}/{1}/{2}: {3}>'.format( self.group, self.statistic, self.timestamp.strftime('%Y-%m-%d %H:%m (UTC)'), self.value)
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'
class PurchaseGroup(DB.Model): """Model for a purchase group to allow pooling allowances.""" __tablename__ = 'purchase_group' object_id = DB.Column(DB.Integer, primary_key=True) code = DB.Column(DB.Unicode(10), unique=True, nullable=False) leader_id = DB.Column(DB.Integer, DB.ForeignKey('user.object_id'), nullable=False) leader = DB.relationship('User', foreign_keys=[leader_id]) members = DB.relationship('User', secondary=GROUP_MEMBER_LINK, backref=DB.backref('purchase_group', lazy=False, uselist=False), lazy='dynamic') disbanded = DB.Column(DB.Boolean(), default=False, nullable=False) purchased = DB.Column(DB.Boolean(), default=False, nullable=False) def __init__(self, leader): self.leader = leader self.members = [leader] self.code = util.generate_key(10) def __repr__(self): return '<PurchaseGroup({0}): {1} tickets, £{2}>'.format( self.object_id, self.total_requested, self.total_value_pounds) @property def total_value(self): """Get the total value of tickets requested by this group in pence.""" return sum(request.value for request in self.requests) @property def total_value_pounds(self): """Get the total value of this group in pounds and pence.""" value = '{0:03d}'.format(self.total_value) return value[:-2] + '.' + value[-2:] @property def total_requested(self): """Get the total number of tickets requested by this group.""" return sum(request.number_requested for request in self.requests) @property def total_guest_tickets_requested(self): """Get the total number of guest tickets requested by this group.""" return sum(request.number_requested for request in self.requests if request.ticket_type.counts_towards_guest_limit) @staticmethod def get_by_code(code): """Get a purchase group object by its code.""" group = PurchaseGroup.query.filter(PurchaseGroup.code == code).first() if not group: return None return group
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', ])
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 Log(DB.Model): """Model for log entries persisted to the database.""" __tablename__ = 'log' object_id = DB.Column(DB.Integer, primary_key=True) timestamp = DB.Column(DB.DateTime, nullable=False) ip_address = DB.Column(DB.Unicode(45), nullable=False) action = DB.Column(DB.UnicodeText()) actor_id = DB.Column(DB.Integer(), DB.ForeignKey('user.object_id'), nullable=True) actor = DB.relationship('User', backref=DB.backref('actions', lazy='dynamic'), foreign_keys=[actor_id]) user_id = DB.Column(DB.Integer(), DB.ForeignKey('user.object_id'), nullable=True) user = DB.relationship('User', backref=DB.backref('events', lazy='dynamic'), foreign_keys=[user_id]) tickets = DB.relationship('Ticket', secondary=LOG_TICKET_LINK, backref=DB.backref('events', lazy='dynamic'), lazy='dynamic') transaction_id = DB.Column(DB.Integer(), DB.ForeignKey('transaction.object_id'), nullable=True) transaction = DB.relationship('Transaction', backref=DB.backref('events', lazy='dynamic')) purchase_group_id = DB.Column(DB.Integer(), DB.ForeignKey('purchase_group.object_id'), nullable=True) purchase_group = DB.relationship('PurchaseGroup', backref=DB.backref('events', lazy='dynamic')) admin_fee_id = DB.Column(DB.Integer(), DB.ForeignKey('admin_fee.object_id'), nullable=True) admin_fee = DB.relationship('AdminFee', backref=DB.backref('events', lazy='dynamic')) def __init__(self, ip_address, action, actor, user, tickets=None, transaction=None, purchase_group=None, admin_fee=None): if tickets is None: tickets = [] self.timestamp = datetime.datetime.utcnow() self.ip_address = ip_address self.action = action self.actor = actor self.user = user self.tickets = tickets self.transaction = transaction self.purchase_group = purchase_group self.admin_fee = admin_fee def __repr__(self): return '<Log {0}: {1}>'.format( self.object_id, self.timestamp.strftime('%Y-%m-%d %H:%m (UTC)')) @staticmethod def write_csv_header(csv_writer): """Write the header of a CSV export file.""" csv_writer.writerow([ 'Log Entry ID', 'Timestamp', 'IP Address', 'Action', 'Actor\'s User ID', 'Actor\'s Name', 'Target\'s User ID', 'Target\'s Name', 'Relevant Ticket IDs', 'Relevant Transaction ID', 'Relevant Purchase Group ID', 'Relevant Admin Fee 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.timestamp.strftime('%Y-%m-%d %H:%M:%S'), self.ip_address, self.action, self.actor_id if self.actor_id is not None else 'N/A', self.actor if self.actor is not None else 'N/A', self.user_id if self.user_id is not None else 'N/A', self.user if self.user is not None else 'N/A', ','.join(str(ticket.object_id) for ticket in self.tickets), (self.transaction_id if self.transaction_id is not None else 'N/A'), (self.purchase_group_id if self.purchase_group_id is not None else 'N/A'), (self.admin_fee_id if self.admin_fee_id is not None else 'N/A'), ])
class Battels(DB.Model): """Model for battels charges for current students.""" __tablename__ = 'battels' object_id = DB.Column(DB.Integer, primary_key=True) battels_id = DB.Column(DB.Unicode(10), unique=True, nullable=True) email = DB.Column(DB.Unicode(120), unique=True, nullable=True) title = DB.Column(DB.Unicode(10), nullable=True) surname = DB.Column(DB.Unicode(60), nullable=True) forenames = DB.Column(DB.Unicode(60), nullable=True) michaelmas_charge = DB.Column(DB.Integer(), default=0, nullable=False) hilary_charge = DB.Column(DB.Integer(), default=0, nullable=False) manual = DB.Column(DB.Boolean(), default=False, nullable=False) def __init__(self, battelsid=None, email=None, title=None, surname=None, forenames=None, manual=False): self.battelsid = battelsid self.email = email self.title = title self.surname = surname self.forenames = forenames self.manual = manual def __repr__(self): return '<Battels {0}: {1}>'.format(self.object_id, self.battelsid) @property def michaelmas_charge_pounds(self): """Get the amount charged in Michaelmas as a pounds/pence string.""" michaelmas_charge = '{0:03d}'.format(self.michaelmas_charge) return michaelmas_charge[:-2] + '.' + michaelmas_charge[-2:] @property def hilary_charge_pounds(self): """Get the amount charged in Hilary as a pounds/pence string.""" hilary_charge = '{0:03d}'.format(self.hilary_charge) return hilary_charge[:-2] + '.' + hilary_charge[-2:] def charge(self, amount, term): """Apply a charge to this battels account.""" if term == 'MTHT': # Integer division fails for negative numbers (i.e. refunds), as the # number is rounded the wrong way. Instead, we do floating point # division, and truncate. half = int(amount / 2.0) self.michaelmas_charge += half self.hilary_charge += amount - half elif term == 'MT': self.michaelmas_charge += amount elif term == 'HT': self.hilary_charge += amount else: raise ValueError( 'Term "{0}" cannot be charged to battels'.format(term)) def refund(self, amount, term): """Refund a ticket and mark it as cancelled.""" if APP.config['CURRENT_TERM'] == 'MT': if term == 'MTHT': half = amount // 2 self.michaelmas_charge -= half self.hilary_charge -= amount - half elif term == 'MT': self.michaelmas_charge -= amount elif term == 'HT': self.hilary_charge -= amount elif APP.config['CURRENT_TERM'] == 'HT': self.hilary_charge -= amount else: raise ValueError( 'Can\'t refund battels tickets in the current term')
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