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 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 PayPalTransaction(transaction.Transaction): """Model for representing a paypal transaction.""" __tablename__ = 'paypal_transaction' __mapper_args__ = {'polymorphic_identity': 'PayPal'} object_id = DB.Column(DB.Integer, primary_key=True) object_id = DB.Column(DB.Integer(), DB.ForeignKey('transaction.object_id'), primary_key=True) # This holds the order_id access_code = DB.Column(DB.Unicode(200), nullable=True) charged = DB.Column(DB.Integer(), nullable=False, default=0) completed = DB.Column(DB.DateTime(), nullable=True) result_code = DB.Column(DB.Unicode(2), nullable=True) # This holds the PASREF field for realex paypal_id = DB.Column(DB.String(50), nullable=True) refunded = DB.Column(DB.Integer(), nullable=False, default=0) order_id = DB.Column(DB.Integer(), nullable=True) # eway_transaction_id = DB.Column( # DB.Integer(), # DB.ForeignKey('eway_transaction.object_id'), # nullable=True # ) # eway_transaction = DB.relationship( # 'EwayTransaction', # backref=DB.backref( # 'transactions', # lazy='dynamic' # ) # ) def __init__(self, user, eway_trans=None): super(PayPalTransaction, self).__init__(user, 'PayPal') # if eway_transaction is not None: # self.eway_transaction = eway_trans def __repr__(self): return '<PayPalTransaction({0}): {1} item(s)>'.format( self.object_id, self.items.count())
class Category(DB.Model): """Model for a products.""" __tablename__ = 'category' 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 '<Category {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 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 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) prepaid_classes = DB.Column(DB.Integer, nullable=False, default=0) 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_membership(self): if membership.Membership.query.filter_by( owner_id=self.object_id).count() > 0: return True return False def memberships(self): if self.has_membership(): return membership.Membership.query.filter_by( owner_id=self.object_id).all() return None def has_unpaid_memberships(self): if membership.Membership.query.filter_by(owner_id=self.object_id, paid=0).count() > 0: return True return False def has_paid_memberships(self): if membership.Membership.query.filter_by(owner_id=self.object_id, paid=1).count() > 0: return True return False def can_claim_membership(self): return False #todo: check logic # def has_collected_memberships(self): # return False # def has_uncollected_memberships(self): # return False def has_held_membership(self): # if membership.Membership.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, membership_type_slug): return 0 @property def total_group_purchase_requested(self): return 0 @property def total_group_purchase_value(self): return 0 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) # return 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) # self.password_hash = 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 memberships(self): """Get the active memberships owned by the user.""" return self.memberships.filter(membership.Membership.cancelled == False # pylint: disable=singleton-comparison ) @property def active_membership_count(self): """How many active memberships does the user own?""" return self.active_memberships.count() @property def waiting_for(self): return 0 @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, memberships, 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 Product(DB.Model): """Model for a products.""" __tablename__ = 'product' object_id = DB.Column(DB.Integer, primary_key=True) name = DB.Column(DB.Unicode(50), unique=True, nullable=False) price_ = DB.Column(DB.Integer(), nullable=False) lot_of = DB.Column(DB.Integer(), nullable=False, default=1) description = DB.Column(DB.Text(), nullable=False) photo_id = DB.Column(DB.Integer, DB.ForeignKey('photo.object_id'), nullable=True) photo = DB.relationship('Photo', backref=DB.backref('product', uselist=False)) category_id = DB.Column(DB.Integer, DB.ForeignKey('category.object_id'), nullable=True) category = DB.relationship('Category', backref=DB.backref('product_cat', uselist=False)) stock = 1000 def __init__(self, name, category_id, description="a thing", price=1, lot=1): self.name = name self.price = price self.category_id = category_id self.photo = None self.description = description self.lot_of = lot def __repr__(self): return '<Product {0}: {1}>'.format(self.object_id, self.name) @property def price_pounds(self): """Get the price of this membership as a string of pounds and pence.""" price = '{0:03d}'.format(self.price) return price[:-2] + '.' + price[-2:] @property def price(self): """Get the price of the membership.""" return self.price_ @price.setter def price(self, value): """Set the price of the membership.""" self.price_ = max(value, 0) if self.price_ == 0: self.mark_as_paid()
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]) memberships = DB.relationship('Membership', secondary=LOG_MEMBERSHIP_LINK, backref=DB.backref('events', lazy='dynamic'), lazy='dynamic') products = DB.relationship('Product', secondary=LOG_PRODUCT_LINK, backref=DB.backref('events2', lazy='dynamic'), lazy='dynamic') transaction_id = DB.Column(DB.Integer(), DB.ForeignKey('transaction.object_id'), nullable=True) transaction = DB.relationship('Transaction', backref=DB.backref('events3', 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, memberships=None, products=None, transaction=None, purchase_group=None, admin_fee=None): if memberships is None: memberships = [] if products is None: products = [] self.timestamp = datetime.datetime.utcnow() self.ip_address = ip_address self.action = action self.actor = actor self.user = user self.memberships = memberships self.products = products 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 Membership 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(membership.object_id) for membership in self.memberships), # ( # 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 Membership(DB.Model): """Model for membership.""" __tablename__ = 'membership' object_id = DB.Column(DB.Integer, primary_key=True) membership_type = DB.Column(DB.Unicode(50), nullable=False) owner_id = DB.Column(DB.Integer, DB.ForeignKey('user.object_id'), nullable=False) owner = DB.relationship('User', backref=DB.backref( 'memberships', lazy='dynamic', order_by=b'Membership.cancelled'), foreign_keys=[owner_id]) paid = 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) def __init__(self, owner, membership_type, price): self.owner = owner self.membership_type = membership_type self.price = price y = datetime.datetime.utcnow().year m = datetime.datetime.utcnow().month if m >= 10: y = y + 1 self.expires = datetime.datetime(y, 10, 1) #(datetime.datetime.utcnow()) + #APP.config['MEMBERSHIP_EXPIRY_TIME']) def generate_barcode(self): # generate barcode key = util.generate_key(20).decode('utf-8') self.barcode = key def generate_qrcode(self): # generate QR qrcode_img = pyqrcode.create( '{0}admin/membership/validate-ticket/{1}/{2}'.format( APP.config['EISITIRIO_URL'], self.object_id, self.barcode)) buffer = io.BytesIO() qrcode_img.png(buffer, scale=20) f = open( '/Users/Gwyneth/Documents/repositories/oussshop/flaskshop/tmp.png', 'wb') f.write(buffer) f.close() return buffer.getvalue() def __repr__(self): return '<Membership {0} owned by {1} ({2})>'.format( self.object_id, self.owner.full_name, self.owner.object_id) def can_be_cancelled(self): return False def can_be_resold(self): return False def can_be_claimed(self): return False def can_be_reclaimed(self): return False def has_holder(self): return False def can_be_paid_for(self): if self.paid == 0: return True return False def can_be_collected(self): return False def is_assigned(self): return True @property def price_pounds(self): """Get the price of this membership 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 membership 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 membership.""" transaction = self.transaction if transaction: return transaction.payment_method else: return 'Unknown Payment Method' @property def price(self): """Get the price of the membership.""" return self.price_ @property def status(self): """Get the status of this membership.""" if not self.paid: return 'Awaiting payment. Expires {0}.'.format( self.expires.strftime('%H:%M %d/%m/%Y')) elif APP.config['REQUIRE_USER_PHOTO']: if not self.holder.photo.verified: return 'Awaiting verification of holder photo.' else: return 'Valid membership' @price.setter def price(self, value): """Set the price of the membership.""" self.price_ = max(value, 0) if self.price_ == 0: self.mark_as_paid() @hybrid.hybrid_property def collected(self): """Has this membership been assigned a barcode.""" return self.barcode != None # pylint: disable=singleton-comparison def mark_as_paid(self): """Mark the membership as paid, and clear any expiry.""" self.paid = True self.expires = None def add_note(self, note): """Add a note to the membership.""" 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 memberships have been sold.""" # TODO return Membership.query.filter(Membership.paid == True).count() # pylint: disable=singleton-comparison @staticmethod def write_csv_header(csv_writer): """Write the header of a CSV export file.""" csv_writer.writerow([ 'Membership ID', 'Membership Type', 'Paid', 'Cancelled', 'Price (Pounds)', '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.membership_type, 'Yes' if self.paid else 'No', 'Yes' if self.cancelled else 'No', self.price_pounds, 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'), ])