class Department(UserMixin, ResourceMixin, db.Model): __tablename__ = 'department' id = db.Column(db.Integer, primary_key=True) # Authentication. active = db.Column('is_active', db.Boolean(), nullable=False, server_default='1') departmentname = db.Column(db.String(24), unique=True, index=True) deptowneremail = db.Column(db.String(255), unique=True, index=True, nullable=False, server_default='') parentid = db.Column(db.Integer, unique=True) whizcoin = db.Column(db.Integer, nullable=False, default=1000) # Activity tracking. current_sign_in_on = db.Column(AwareDateTime()) current_sign_in_ip = db.Column(db.String(45)) last_sign_in_on = db.Column(AwareDateTime()) last_sign_in_ip = db.Column(db.String(45)) def __init__(self, **kwargs): # Call Flask-SQLAlchemy's constructor. super(Department, self).__init__(**kwargs) @classmethod def find_by_identity(cls, identity): """ Find a user by their e-mail or username. :param identity: Email or username :type identity: str :return: User instance """ return Department.query.filter( (Department.departmentname == identity)).first()
class ResourceMixin(object): # Keep track when records are created and updated. created_on = db.Column(AwareDateTime(), default=tzware_datetime) updated_on = db.Column(AwareDateTime(), default=tzware_datetime, onupdate=tzware_datetime) def save(self): """ Save a model instance. :return: Model instance """ db.session.add(self) db.session.commit() return self def delete(self): """ Delete a model instance. :return: db.session.commit()'s result """ db.session.delete(self) return db.session.commit() def __str__(self): """ Create a human readable version of a class instance. :return: self """ obj_id = hex(id(self)) columns = self.__table__.c.keys() values = ', '.join("%s=%r" % (n, getattr(self, n)) for n in columns) return '<%s %s(%s)>' % (obj_id, self.__class__.__name__, values)
class ResourceMixin: # Keep track when records are created and updated. created_on = db.Column(AwareDateTime(), default=tzware_datetime) updated_on = db.Column(AwareDateTime(), default=tzware_datetime) def save(self): """ Save a model instance. :return: Model instance """ db.session.add(self) db.session.commit() return self def delete(self): """ Delete a model instance. :return: db.session.commit()'s result """ db.session.delete(self) return db.session.commit() def __str__(self): """ Create a human readable version of a class instance. :return: self """ obj_id = hex(id(self)) columns = self.__table__.c.keys() values = ', '.join(f"{n}={getattr(self, n)}" for n in columns) return f'<{obj_id} {self.__class__.__name__}({values})>'
class CreditCard(db.Model): IS_EXPIRING_THRESHOLD_MONTHS = 60 __tablename__ = 'credit_cards' id = db.Column(db.Integer, primary_key=True) # Foreign key user_id = db.Column(db.Integer, db.ForeignKey('users.id', onupdate='CASCADE', ondelete='CASCADE'), index=True, nullable=False) # Card details brand = db.Column(db.String(32)) last4 = db.Column(db.Integer) exp_date = db.Column(db.Date, index=True) is_expiring = db.Column(db.Boolean(), nullable=False, server_default='0') def __init__(self, **kwargs): super(CreditCard, self).__init__(**kwargs) @classmethod def is_expiring_soon(cls, compare_date=None, exp_date=None): """Check card to expire in 60 days""" return exp_date <= timedelta_days(CreditCard.IS_EXPIRING_THRESHOLD_MONTHS, compare_date=compare_date) @classmethod def mark_old_credit_cards(cls, compare_date=None): """Mark credit card that are going to expire in two months or has expired""" today_with_delta = timedelta_days(CreditCard.IS_EXPIRING_THRESHOLD_MONTHS, compare_date=compare_date) CreditCard.query.filter(CreditCard.exp_date <= today_with_delta).update({CreditCard.is_expiring:True}) return db.session.commit() @classmethod def extract_card_params(cls, customer): """ Extract the credit card info from paying customer """ card_data = customer.sources.data[0] exp_date = datetime.date(card_data.exp_year, card_data.exp_month, 1) card = { 'brand' : card_data.brand, 'last4' : card_data.last4, 'exp_date' : exp_date, 'is_expiring' : CreditCard.is_expiring_soon(exp_date=exp_date) } return card
class ResourceMixin(object): # Keep track when records are created and updated. created_on = db.Column(AwareDateTime(), default=tzware_datetime) updated_on = db.Column(AwareDateTime(), default=tzware_datetime, onupdate=tzware_datetime) def save(self): """ Save a model instance. :return: Model instance """ db.session.add(self) db.session.commit() return self def delete(self): """ Delete a model instance. :return: db.session.commit()'s result """ db.session.delete(self) return db.session.commit() def __str__(self): """ Create a human readable version of a class instance. :return: self """ obj_id = hex(id(self)) columns = self.__table__.c.keys() values = ', '.join("%s=%r" % (n, getattr(self, n)) for n in columns) return '<%s %s(%s)>' % (obj_id, self.__class__.__name__, values) @classmethod def sort_by(cls, field, direction): """ Validate the sort field and direction. :param field: Field name :type field: str :param direction: Direction :type direction: str :return: tuple """ if field not in cls.__table__.columns: field = 'created_on' if direction not in ('asc', 'desc'): direction = 'asc' return field, direction @classmethod def get_bulk_action_ids(cls, scope, ids, omit_ids=[], query=''): """ Determine which IDs are to be modified. :param scope: Affect all or only a subset of items :type scope: str :param ids: List of ids to be modified :type ids: list :param omit_ids: Remove 1 or more IDs from the list :type omit_ids: list :param query: Search query (if applicable) :type query: str :return: list """ omit_ids = map(str, omit_ids) if scope == 'all_search_results': # Change the scope to go from selected ids to all search results. ids = cls.query.with_entities(cls.id).filter(cls.search(query)) # SQLAlchemy returns back a list of tuples, we want a list of strs. ids = [str(item[0]) for item in ids] # Remove 1 or more items from the list, this could be useful in spots # where you may want to protect the current user from deleting themself # when bulk deleting user accounts. if omit_ids: ids = [id for id in ids if id not in omit_ids] return ids @classmethod def bulk_delete(cls, ids): """ Delete 1 or more model instances. :param ids: List of ids to be deleted :type ids: list :return: Number of deleted instances """ delete_count = cls.query.filter( cls.id.in_(ids)).delete(synchronize_session=False) db.session.commit() return delete_count
class Invoice(ResourceMixin, db.Model): __tablename__ = 'invoices' id = db.Column(db.Integer, primary_key=True) # Relationships. user_id = db.Column(db.Integer, db.ForeignKey('users.id', onupdate='CASCADE', ondelete='CASCADE'), index=True, nullable=False) # Invoice details. plan = db.Column(db.String(128), index=True) receipt_number = db.Column(db.String(128), index=True) description = db.Column(db.String(128)) period_start_on = db.Column(db.Date) period_end_on = db.Column(db.Date) currency = db.Column(db.String(8)) tax = db.Column(db.Integer()) tax_percent = db.Column(db.Float()) total = db.Column(db.Integer()) # De-normalize the card details so we can render a user's history properly # even if they have no active subscription or changed cards at some point. brand = db.Column(db.String(32)) last4 = db.Column(db.Integer) exp_date = db.Column(db.Date, index=True) def __init__(self, **kwargs): # Call Flask-SQLAlchemy's constructor. super(Invoice, self).__init__(**kwargs) @classmethod def billing_history(cls, user=None): """ Return the billing history for a specific user. :param user: User whose billing history will be retrieved :type user: User instance :return: Invoices """ invoices = Invoice.query.filter(Invoice.user_id == user.id) \ .order_by(Invoice.created_on.desc()).limit(12) return invoices @classmethod def parse_from_event(cls, payload): """ Parse and return the invoice information that will get saved locally. API Documentation: https://stripe.com/docs/api/invoices/object :return: dict """ data = payload['data']['object'] plan_info = data['lines']['data'][0]['plan'] period_start_on = datetime.datetime.utcfromtimestamp( data['lines']['data'][0]['period']['start']).date() period_end_on = datetime.datetime.utcfromtimestamp( data['lines']['data'][0]['period']['end']).date() invoice = { 'payment_id': data['customer'], 'plan': plan_info['name'], 'receipt_number': data['receipt_number'], 'description': plan_info['statement_descriptor'], 'period_start_on': period_start_on, 'period_end_on': period_end_on, 'currency': data['currency'], 'tax': data['tax'], 'tax_percent': data['tax_percent'], 'total': data['total'] } return invoice @classmethod def parse_from_api(cls, payload): """ Parse and return the invoice information we are interested in. API Documentation: https://stripe.com/docs/api/invoices/object :return: dict """ plan_info = payload['lines']['data'][0]['plan'] date = datetime.datetime.utcfromtimestamp(payload['created']) plan = plan_info['id'].upper() invoice = { 'plan': plan, 'description': '{} MONTHLY'.format(plan), 'next_bill_on': date, 'amount_due': payload['amount_due'], 'interval': plan_info['interval'] } return invoice @classmethod def prepare_and_save(cls, parsed_event): """ Potentially save the invoice after argument the event fields. :param parsed_event: Event params to be saved :type parsed_event: dict :return: User instance """ # Avoid circular imports. from snakeeyes.blueprints.user.models import User # Only save the invoice if the user is valid at this point. id = parsed_event.get('payment_id') user = User.query.filter((User.payment_id == id)).first() if user and user.credit_card: parsed_event['user_id'] = user.id parsed_event['brand'] = user.credit_card.brand parsed_event['last4'] = user.credit_card.last4 parsed_event['exp_date'] = user.credit_card.exp_date del parsed_event['payment_id'] invoice = Invoice(**parsed_event) invoice.save() return user @classmethod def upcoming(cls, customer_id): """ Return the upcoming invoice item. :param customer_id: Stripe customer id :type customer_id: int :return: Stripe invoice object """ invoice = PaymentInvoice.upcoming(customer_id) return Invoice.parse_from_api(invoice)
class User(UserMixin, ResourceMixin, db.Model): ROLE = OrderedDict([ ('member', 'Member'), ('admin', 'Admin') ]) __tablename__ = 'users' id = db.Column(db.Integer, primary_key=True) # Authntication. role = db.Column(db.Enum(*ROLE, name='role_types', native_enum=False), index=True, nullable=False, server_default='member') active = db.Column('is_active', db.Boolean(), nullable=False, server_default='1') username = db.Column(db.String(24), unique=True, index=True) email = db.Column(db.String(255), unique=True, index=True, nullable=False, server_default='') password = db.Column(db.String(128), nullable=False, server_default='') # Activity tracking. sign_in_count = db.Column(db.Integer, nullable=False, default=0) current_sign_in_on = db.Column(AwareDateTime()) current_sign_in_ip = db.Column(db.String(45)) last_sign_in_on = db.Column(AwareDateTime()) last_sign_in_ip = db.Column(db.String(45)) def __init__(self, **kwargs): # Call Flask-SQLAlchemy's constructor super(User, self).__init__(**kwargs) self.password = User.encrypt_password(kwargs.get('password', '')) @classmethod def find_by_identity(cls, identity): """ Find a user by thier e-mail or username. :param identity: Email or username :type identity: str :return: User instance """ return User.query.filter( (User.email == identity) | (User.username == identity)).first() @classmethod def encrypt_password(cls, plaintext_password): """ Hash a plaintext string using PBKDF2. This is good enough according to the NIST (National Institute of Standards and Technology). In other words while bcrypt might be superior in practice, if you use PBKDF2 properly (which we are), then your passwords are safe. :param plaintext_password: Password in plain text :type plaintext_password: str :return: str """ if plaintext_password: return generate_password_hash(plaintext_password) return None @classmethod def deserialize_token(cls, token): """ Obtain a user from de-serializing a signed token. :param token: Signed token. :type token: str :return: User instance or None """ private_key = TimedJSONWebSignatureSerializer( current_app.config['SECRET_KEY']) try: decoded_payload = private_key.loads(token) return User.find_by_identity(decoded_payload.get('user_email')) except Exception: return None @classmethod def initialize_password_reset(cls, identity): """ Generate a token to reset the password for a specific user. :param identity: User e-mail address or username :type identity: str :return: User instance """ u = User.find_by_identity(identity) reset_token = u.serialize_token() # This prevents circular imports from snakeeyes.blueprints.user.tasks import ( deliver_password_reset_email) deliver_password_reset_email.delay(u.id, reset_token) return u def is_active(self): """ Return whether or not the user account is active, this satisfies Flask-Login by overwriting the default value. :return: bool """ return self.active def get_auth_token(self): """ Return the user's auth token. Use their password as part of the token because if the user changes their password we will want to invalidate all of their logins across devices. It is completely fine to use md5 here as nothing leaks. This satisfies Flask-Login by providing a means to create a token. :return: str """ private_key = current_app.config['SECRET_KEY'] serializer = URLSafeTimedSerializer(private_key) data = [str(self.id), md5(self.password.encode('utf-8')).hexdigest()] return serializer.dumps(data) def authenticated(self, with_password=True, password=''): """ Ensure a user is authenticated, and optionally check their password. :param with_password: Optionally check their password :param with_password: bool :param password: Optionally verify this as their password :param password: str :return: bool """ if with_password: return check_password_hash(self.password, password) return True def serialize_token(self, expiration=3600): """ Sign and create a token that can be used for things such as resetting a password or other tasks that involve a one off token. :param expiration: Seconds until it expires, defaults to 1 hour :type expiration: int :return: JSON """ private_key = current_app.config['SECRET_KEY'] serializer = TimedJSONWebSignatureSerializer(private_key, expiration) return serializer.dumps({'user_email': self.email}).decode('utf-8') def update_activity_tracking(self, ip_address): """ Update various fields on the user that's related to meta data on their account, such as the sign in count and ip address, etc.. :param ip_address: IP address :type ip_address: str :return: SQLAlchemy commit results """ self.sign_in_count += 1 self.last_sign_in_on = self.current_sign_in_on self.last_sign_in_ip = self.current_sign_in_ip self.current_sign_in_on = datetime.datetime.now(pytz.utc) self.current_sign_in_ip = ip_address return self.save()
class Coupon(db.Model): DURATION = OrderedDict([('once', 'Once'), ('repeating', 'Repeating'), ('forever', 'Forever')]) __tablename__ = 'coupons' id = db.Column(db.Integer, primary_key=True) # Coupon details. code = db.Column(db.String(128), index=True, unique=True) duration = db.Column(db.Enum(*DURATION, name='duration_types'), index=True, nullable=False, default='forever') amount_off = db.Column(db.Integer()) percent_off = db.Column(db.Integer()) currency = db.Column(db.String(8)) duration_in_months = db.Column(db.Integer()) max_redemptions = db.Column(db.Integer(), index=True) redeem_by = db.Column(db.DateTime(), default=datetime.datetime.utcnow) times_redeemed = db.Column(db.Integer(), index=True, nullable=False, default=0) valid = db.Column(db.Boolean(), nullable=False, server_default='1') created_on = db.Column(db.DateTime(), default=datetime.datetime.utcnow) def __init__(self, **kwargs): if self.code: self.code = code.upper() else: self.code = Coupon.random_coupon_code() super(Coupon, self).__init__(**kwargs) @hybrid_property def redeemable(self): """ Returns coupon code that are still valid. """ is_redeemable = or_(self.redeem_by.is_(None), self.redeem_by >= datetime.datetime.now) return and_(self.valid, is_redeemable) @classmethod def search(cls, query): """ search resources by one or more filed """ if not query: return '' search_query = '%{0}%'.format(query) return or_(cls.code.ilike(search_query)) # search_query = '%{0}%'.format(query) # search_chain = (User.email.ilike(search_query), # User.username.ilike(search_query)) # return or_(*search_chain) @classmethod def random_coupon_code(cls): """ Create a human readable random code. """ charset = string.digits + string.ascii_uppercase charset = charset.replace('B', '').replace('I', '') charset = charset.replace('O', '').replace('S', '') charset = charset.replace('0', '').replace('1', '') random_chars = ''.join(choice(charset) for _ in range(0, 14)) coupon_code = '{}-{}-{}'.format(random_chars[0:4], random_chars[5:9], random_chars[10:14]) return coupon_code @classmethod def expire_old_coupons(cls, compare_date=None): """ Invalidate coupon that has pass thier expire date """ if compare_date is None: compare_date = datetime.datetime.now(pytz.utc) cls.query.filter(cls.redeem_by < +compare_date).update( {cls.valid: not cls.valid}) return db.session.commit() @classmethod def create(cls, params): """ Create a coupon code and return true is successful """ payment_params = params payment_params['code'] = payment_params['code'].upper() if payment_params.get('amount_off'): payment_params['amount_off'] = dollars_to_cents( payment_params['amount_off']) PaymentCoupon.create(**payment_params) # Stripe will save the coupon to id field on stripe while on our database, we want it to save on code field if 'id' in payment_params: payment_params['code'] = payment_params['id'] del payment_params['id'] # Converting th eunix time to day stim stamp that is acceptable by the databse if 'redeem_by' in payment_params: if payment_params.get('redeem_by') is not None: params['redeem_by'] = payment_params.get('redeem_by').replace( datetime.datetime.utcnow) coupon = Coupon(**payment_params) db.session.add(coupon) db.session.commit() return True @classmethod def bulk_delete(cls, ids): """ Override the general bulk delete method to delete coupon from application and stripe """ delete_count = 0 for id in ids: coupon = Coupon.query.get(id) print(coupon) if coupon is None: continue # Delete on stripe stripe_delete = PaymentCoupon.delete(coupon) # If successful, delete it locally if stripe_delete.get('deleted'): db.session.delete(coupon) delete_count += 1 return delete_count @classmethod def find_by_code(cls, code): """ Find a coupon by its code """ formatted_code = code.upper() coupon = Coupon.query.filter(Coupon.redeemable, Coupon.code == formatted_code).first() return coupon def redeem(self): """ Update redeem stats for this coupon """ self.times_redeemed += 1 if self.max_redemptions: if self.times_redeemed >= self.max_redemptions: self.valid = False return db.session.commit() def to_json(self): """ Retun JSON fields to represent a coupon """ params = { 'duration': self.duration, 'duration_in_months': self.duration_in_months } if self.amount_off: params['amount_off'] = cent_to_dollar(self.amount_off) if self.percent_off: params['percent_off'] = self.percent_off return params @classmethod def sort_by(cls, field, direction): """This help to sort the user base on the field column and direction. """ if field not in cls.__table__.columns: field = "created_on" if direction not in ('asc', 'desc'): direction = 'asc' return field, direction @classmethod def get_bulk_action_ids(cls, scope, ids, omit_ids=[], query=''): """Determine which id to be deleted.""" omit_ids = list(map(str, omit_ids)) if scope == 'all_search_result': ids = cls.query.with_entities(cls.id).filter(cls.search(query)) ids = [str(item[0]) for item in ids] if omit_ids: ids = [id for id in ids if id not in omit_ids] return ids
class Subscription(db.Model): __tablename__ = 'subscriptions' id = db.Column(db.Integer, primary_key=True) # Relationships user_id = db.Column(db.Integer, db.ForeignKey('users.id', onupdate='CASCADE', ondelete='CASCADE'), index=True, nullable=False) plan = db.Column(db.String(128)) coupon = db.Column(db.String(128)) def __init__(self, **kwargs): super(Subscription, self).__init__(**kwargs) @classmethod def get_plan_by_id(cls, plan): """Identify plan by id and compare""" for key, value in settings.STRIPE_PLANS.items(): if value.get('id') == plan: return settings.STRIPE_PLANS[key] return None @classmethod def get_new_plan(cls, keys): """Pick plan based on the plan identifier""" for key in keys: split_key = key.split('submit_') if isinstance(split_key, list) and len(split_key) == 2: if Subscription.get_plan_by_id(split_key[1]): return split_key[1] return None def create(self, user=None, plan=None, coupon=None, token=None, name=None): """Create a recurring subscription""" if token is None: return False if coupon: self.coupon = coupon.upper() customer = PaymentSubscription.create(token=token, email=user.email, coupon=None, plan=plan) # Updating user details user.name = name user.payment_id = customer.id user.cancelled_subscription_on = None # Set the subscription details self.user_id = user.id self.plan = plan # Redeem the coupon if coupon: coupon = Coupon.query.filter(Coupon.code == self.coupon).first() coupon.redeem() credit_card = CreditCard(user_id=user.id, **CreditCard.extract_card_params(customer)) db.session.add(user) db.session.add(self) db.session.add(credit_card) db.session.commit() return True def update(self, user=None, plan=None, coupon=None): """Updating an existing subscription plan""" PaymentSubscription.update(customer_id=user.payment_id, coupon=coupon, plan=plan) user.subscription.plan = plan if coupon: user.subscription.coupon = coupon coupon = Coupon.query.filter(Coupon.coupon == coupon).first() if coupon: coupon.redeem() db.session.add(user.subscription) db.session.commit() return True def cancel(self, user=None, discard_credit_card=True): """Cancel an existing subscription""" PaymentSubscription.cancel(user.payment_id) user.payment_id = None user.cancelled_subscription_on = datetime.datetime.now(pytz.utc) db.session.add(user) db.session.delete(user.subscription) if discard_credit_card: db.session.delete(user.credit_card) db.session.commit() return True def update_payment_method(self, user=None, credit_card=None, name=None, token=None): """Updating user existing card""" if token is None: return False customer = PaymentCard.update(customer_id=user.payment_id, stripe_token=token) # Update user details user.name = name # Update the credit card new_card = CreditCard.extract_card_params(customer) credit_card.brand = new_card.get('brand') credit_card.last4 = new_card.get('last4') credit_card.exp_date = new_card.get('exp_date') credit_card.is_expiring = new_card.get('is_expiring') db.session.add(user) db.session.add(credit_card) db.session.commit() return True
class CreditCard(ResourceMixin, db.Model): IS_EXPIRING_THRESHOLD_MONTHS = 2 __tablename__ = 'credit_cards' id = db.Column(db.Integer, primary_key=True) # Relationships. user_id = db.Column(db.Integer, db.ForeignKey('users.id', onupdate='CASCADE', ondelete='CASCADE'), index=True, nullable=False) # Card details. brand = db.Column(db.String(32)) last4 = db.Column(db.Integer) exp_date = db.Column(db.Date, index=True) is_expiring = db.Column(db.Boolean(), nullable=False, server_default='0') def __init__(self, **kwargs): # Call Flask-SQLAlchemy's constructor. super(CreditCard, self).__init__(**kwargs) @classmethod def is_expiring_soon(cls, compare_date=None, exp_date=None): """ Determine whether or not this credit card is expiring soon. :param compare_date: Date to compare at :type compare_date: date :param exp_date: Expiration date :type exp_date: date :return: bool """ return exp_date <= timedelta_months( CreditCard.IS_EXPIRING_THRESHOLD_MONTHS, compare_date=compare_date) @classmethod def mark_old_credit_cards(cls, compare_date=None): """ Mark credit cards that are going to expire soon or have expired. :param compare_date: Date to compare at :type compare_date: date :return: Result of updating the records """ today_with_delta = timedelta_months( CreditCard.IS_EXPIRING_THRESHOLD_MONTHS, compare_date) CreditCard.query.filter(CreditCard.exp_date <= today_with_delta) \ .update({CreditCard.is_expiring: True}) return db.session.commit() @classmethod def extract_card_params(cls, customer): """ Extract the credit card info from a payment customer object. :param customer: Payment customer :type customer: Payment customer :return: dict """ card_data = customer.sources.data[0] exp_date = datetime.date(card_data.exp_year, card_data.exp_month, 1) card = { 'brand': card_data.brand, 'last4': card_data.last4, 'exp_date': exp_date, 'is_expiring': CreditCard.is_expiring_soon(exp_date=exp_date) } return card
class Invoice(ResourceMixin, db.Model): __tablename__ = 'invoices' id = db.Column(db.Integer, primary_key=True) # Relationships. user_id = db.Column(db.Integer, db.ForeignKey('users.id', onupdate='CASCADE', ondelete='CASCADE'), index=True, nullable=False) # Invoice details. plan = db.Column(db.String(128), index=True) receipt_number = db.Column(db.String(128), index=True) description = db.Column(db.String(128)) period_start_on = db.Column(db.Date) period_end_on = db.Column(db.Date) currency = db.Column(db.String(8)) tax = db.Column(db.Integer()) tax_percent = db.Column(db.Float()) total = db.Column(db.Integer()) # De-normalize the card details so we can render a user's history properly # even if they have no active subscription or changed cards at some point. brand = db.Column(db.String(32)) last4 = db.Column(db.Integer) exp_date = db.Column(db.Date, index=True) def __init__(self, **kwargs): # Call Flask-SQLAlchemy's constructor. super(Invoice, self).__init__(**kwargs) @classmethod def search(cls, query): """ Search a resource by 1 or more fields. :param query: Search query :type query: str :return: SQLAlchemy filter """ from snakeeyes.blueprints.user.models import User if not query: return '' search_query = '%{0}%'.format(query) search_chain = (User.email.ilike(search_query), User.username.ilike(search_query)) return or_(*search_chain) @classmethod def billing_history(cls, user=None): """ Return the billing history for a specific user. :param user: User whose billing history will be retrieved :type user: User instance :return: Invoices """ invoices = Invoice.query.filter(Invoice.user_id == user.id) \ .order_by(Invoice.created_on.desc()).limit(12) return invoices @classmethod def parse_from_event(cls, payload): """ Parse and return the invoice information that will get saved locally. :return: dict """ data = payload['data']['object'] plan_info = data['lines']['data'][0]['plan'] period_start_on = datetime.datetime.utcfromtimestamp( data['lines']['data'][0]['period']['start']).date() period_end_on = datetime.datetime.utcfromtimestamp( data['lines']['data'][0]['period']['end']).date() invoice = { 'payment_id': data['customer'], 'plan': plan_info['name'], 'receipt_number': data['receipt_number'], 'description': plan_info['statement_descriptor'], 'period_start_on': period_start_on, 'period_end_on': period_end_on, 'currency': data['currency'], 'tax': data['tax'], 'tax_percent': data['tax_percent'], 'total': data['total'] } return invoice @classmethod def parse_from_api(cls, payload): """ Parse and return the invoice information we are interested in. :return: dict """ plan_info = payload['lines']['data'][0]['plan'] date = datetime.datetime.utcfromtimestamp(payload['date']) invoice = { 'plan': plan_info['name'], 'description': plan_info['statement_descriptor'], 'next_bill_on': date, 'amount_due': payload['amount_due'], 'interval': plan_info['interval'] } return invoice @classmethod def prepare_and_save(cls, parsed_event): """ Potentially save the invoice after argument the event fields. :param parsed_event: Event params to be saved :type parsed_event: dict :return: User instance """ # Avoid circular imports. from snakeeyes.blueprints.user.models import User # Only save the invoice if the user is valid at this point. id = parsed_event.get('payment_id') user = User.query.filter((User.payment_id == id)).first() if user and user.credit_card: parsed_event['user_id'] = user.id parsed_event['brand'] = user.credit_card.brand parsed_event['last4'] = user.credit_card.last4 parsed_event['exp_date'] = user.credit_card.exp_date return user @classmethod def upcoming(cls, customer_id): """ Return the upcoming invoice item. :param customer_id: Stripe customer id :type customer_id: int :return: Stripe invoice object """ invoice = PaymentInvoice.upcoming(customer_id) return Invoice.parse_from_api(invoice) def create(self, user=None, currency=None, amount=None, coins=None, coupon=None, token=None): """ Create an invoice item. :param user: User to apply the subscription to :type user: User instance :param amount: Stripe currency :type amount: str :param amount: Amount in cents :type amount: int :param coins: Amount of coins :type coins: int :param coupon: Coupon code to apply :type coupon: str :param token: Token returned by JavaScript :type token: str :return: bool """ if token is None: return False customer = PaymentCustomer.create(token=token, email=user.email) if coupon: self.coupon = coupon.upper() coupon = Coupon.query.filter(Coupon.code == self.coupon).first() amount = coupon.apply_discount_to(amount) charge = PaymentCharge.create(customer.id, currency, amount) # Redeem the coupon. if coupon: coupon.redeem() # Add the coins to the user. user.coins += coins # Create the invoice item. period_on = datetime.datetime.utcfromtimestamp(charge.get('created')) card_params = CreditCard.extract_card_params(customer) self.user_id = user.id self.plan = '—' self.receipt_number = charge.get('receipt_number') self.description = charge.get('statement_descriptor') self.period_start_on = period_on self.period_end_on = period_on self.currency = charge.get('currency') self.tax = None self.tax_percent = None self.total = charge.get('amount') self.brand = card_params.get('brand') self.last4 = card_params.get('last4') self.exp_date = card_params.get('exp_date') db.session.add(user) db.session.add(self) db.session.commit() return True
class Employee(db.Model, ResourceMixin): __tablename__ = 'employee' employee_id = db.Column(db.Integer, primary_key=True) employee_name = db.Column(db.String(60)) parent_id = db.Column(db.String) is_active = db.Column(db.Boolean) age = db.Column(db.Integer) join_date = db.Column(db.Date) def __init__(self, employee_id, employee_name, parent_id, is_active, age, join_date): # Call Flask-SQLAlchemy's constructor. self.employee_id = employee_id self.employee_name = employee_name self.parent_id = parent_id self.is_active = is_active self.age = age self.join_date = join_date def __init__(self, employee_id, employee_name, parent_id): # Call Flask-SQLAlchemy's constructor. self.employee_id = employee_id self.employee_name = employee_name self.parent_id = parent_id @classmethod def getall(cls): # current_app.logger.debug('get all api called') result = Employee.query.all() return Utils.todict(result) @classmethod def getsubtree(cls, empId, isjoindateflag): sql = text( "WITH RECURSIVE subordinates AS " + "(SELECT employee_id,parent_id,employee_name,join_date FROM employee WHERE employee_id = :id" " UNION SELECT e.employee_id, e.parent_id, e.employee_name,e.join_date FROM employee e " + "INNER JOIN subordinates s ON s.employee_id = e.parent_id) SELECT employee_name,parent_id,employee_id FROM subordinates" + " where employee_id <> :id") if (isjoindateflag == True): sql = text( "WITH RECURSIVE subordinates AS " + "(SELECT employee_id,parent_id,employee_name,join_date FROM employee WHERE employee_id = :id" " UNION SELECT e.employee_id, e.parent_id, e.employee_name,e.join_date FROM employee e " + "INNER JOIN subordinates s ON s.employee_id = e.parent_id) SELECT employee_name,parent_id,employee_id FROM subordinates" + " where employee_id <> :id and join_date > (select join_date from employee where employee_id = :id)" ) result = db.session.query(Employee).from_statement(sql).params( id=empId).all() return Utils.todict(result) @classmethod def gettree(cls, empId): sql = text( "WITH RECURSIVE subordinates AS " + "(SELECT employee_id,parent_id,employee_name,join_date FROM employee WHERE employee_id = :id" " UNION SELECT e.employee_id, e.parent_id, e.employee_name,e.join_date FROM employee e " + "INNER JOIN subordinates s ON s.employee_id = e.parent_id) SELECT employee_name,parent_id,employee_id FROM subordinates" ) result = db.session.query(Employee).from_statement(sql).params( id=empId).all() return Utils.todict(result) @classmethod def getancestorpath(cls, empId): sql = text( "WITH RECURSIVE subordinates AS " + "(SELECT employee_id,parent_id,employee_name,join_date FROM employee WHERE employee_id = :id " + "UNION SELECT e.employee_id, e.parent_id, e.employee_name,e.join_date FROM employee e INNER JOIN " + "subordinates s ON s.parent_id = e.employee_id)SELECT employee_name,parent_id,employee_id,join_date FROM subordinates" ) result = db.session.query(Employee).from_statement(sql).params( id=empId).all() return result @classmethod def setitem(cls, emp): e = Employee.query.get(emp.employee_id) if (e != None): e.employee_name = emp.employee_name e.id = emp.id e.parent_id = emp.parent_id db.session.commit() return json.dumps({"status": "insert successful"}) else: Employee.save(emp) return json.dumps({"status": "update successful"}) @classmethod def delitem(cls, employee_id): e = Employee.query.get(employee_id) if (e != None): try: Employee.delete(e) except Exception: return jsonify({"status": "failed"}) return jsonify({"status": "success"})
class Projects(UserMixin, ResourceMixin, db.Model): # ROLE = OrderedDict([ # ('member', 'Member'), # ('admin', 'Admin') # ]) __tablename__ = 'projects' id = db.Column(db.Integer, primary_key=True) # Authentication. projectid = db.Column(db.String(24), unique=True, index=True) description = db.Column(db.String(4100), nullable=False, server_default='') skills = db.Column(db.String(4100), unique=False, server_default='') email = db.Column(db.String(255), unique=False, index=False, nullable=False, server_default='') resource_email = db.Column(db.String(255), unique=False, index=False, nullable=False, server_default='') whizcoin = db.Column(db.Integer, nullable=False, default=100) department = db.Column(db.String(255), unique=False, index=False, nullable=False, server_default='') protype = db.Column(db.String(255), unique=False, index=False, nullable=False, server_default='') startdate = db.Column(db.String(255), unique=False, index=False, nullable=False, server_default='') enddate = db.Column(db.String(255), unique=False, index=False, nullable=False, server_default='') interesting_participants = db.Column(db.String(4100), unique=False, server_default='') project_status = db.Column(db.String(255), unique=False, index=False, nullable=False, server_default='') # Activity tracking. sign_in_count = db.Column(db.Integer, nullable=False, default=0) current_sign_in_on = db.Column(AwareDateTime()) current_sign_in_ip = db.Column(db.String(45)) last_sign_in_on = db.Column(AwareDateTime()) last_sign_in_ip = db.Column(db.String(45)) def __init__(self, **kwargs): # Call Flask-SQLAlchemy's constructor. super(Projects, self).__init__(**kwargs) @classmethod def find_by_identity(cls, identity): """ Find a user by their e-mail or username. :param identity: Email or username :type identity: str :return: User instance """ return Projects.query.filter((Projects.projectid == identity)).first() def update_activity_tracking(self, ip_address): """ Update various fields on the user that's related to meta data on their account, such as the sign in count and ip address, etc.. :param ip_address: IP address :type ip_address: str :return: SQLAlchemy commit results """ self.sign_in_count += 1 self.last_sign_in_on = self.current_sign_in_on self.last_sign_in_ip = self.current_sign_in_ip self.current_sign_in_on = datetime.datetime.now(pytz.utc) self.current_sign_in_ip = ip_address return self.save()
class User(UserMixin, ResourceMixin, db.Model): ROLE = OrderedDict([ ('member', 'Member'), ('admin', 'Admin') ]) __tablename__ = 'users' id = db.Column(db.Integer, primary_key=True) # Relationships. credit_card = db.relationship(CreditCard, uselist=False, backref='users', passive_deletes=True) subscription = db.relationship(Subscription, uselist=False, backref='users', passive_deletes=True) invoices = db.relationship(Invoice, backref='users', passive_deletes=True) # Authentication. role = db.Column(db.Enum(*ROLE, name='role_types', native_enum=False), index=True, nullable=False, server_default='member') active = db.Column('is_active', db.Boolean(), nullable=False, server_default='1') username = db.Column(db.String(24), unique=True, index=True) email = db.Column(db.String(255), unique=True, index=True, nullable=False, server_default='') password = db.Column(db.String(128), nullable=False, server_default='') # Billing. name = db.Column(db.String(128), index=True) payment_id = db.Column(db.String(128), index=True) cancelled_subscription_on = db.Column(AwareDateTime()) # Activity tracking. sign_in_count = db.Column(db.Integer, nullable=False, default=0) current_sign_in_on = db.Column(AwareDateTime()) current_sign_in_ip = db.Column(db.String(45)) last_sign_in_on = db.Column(AwareDateTime()) last_sign_in_ip = db.Column(db.String(45)) def __init__(self, **kwargs): # Call Flask-SQLAlchemy's constructor. super(User, self).__init__(**kwargs) self.password = User.encrypt_password(kwargs.get('password', '')) def __repr__(self): """ Create a human readable version of a User instance. :return: self """ return f'<User {self.username}>' @classmethod def find_by_identity(cls, identity): """ Find a user by their e-mail or username. :param identity: Email or username :type identity: str :return: User instance """ return User.query.filter( (User.email == identity) | (User.username == identity)).first() @classmethod def encrypt_password(cls, plaintext_password): """ Hash a plaintext string using PBKDF2. This is good enough according to the NIST (National Institute of Standards and Technology). In other words while bcrypt might be superior in practice, if you use PBKDF2 properly (which we are), then your passwords are safe. :param plaintext_password: Password in plain text :type plaintext_password: str :return: str """ if plaintext_password: return generate_password_hash(plaintext_password) return None @classmethod def deserialize_token(cls, token): """ Obtain a user from de-serializing a signed token. :param token: Signed token. :type token: str :return: User instance or None """ private_key = TimedJSONWebSignatureSerializer( current_app.config['SECRET_KEY']) try: decoded_payload = private_key.loads(token) return User.find_by_identity(decoded_payload.get('user_email')) except Exception: return None @classmethod def initialize_password_reset(cls, identity): """ Generate a token to reset the password for a specific user. :param identity: User e-mail address or username :type identity: str :return: User instance """ u = User.find_by_identity(identity) reset_token = u.serialize_token() # This prevents circular imports. from snakeeyes.blueprints.user.tasks import ( deliver_password_reset_email) deliver_password_reset_email.delay(u.id, reset_token) return u def is_active(self): """ Return whether or not the user account is active, this satisfies Flask-Login by overwriting the default value. :return: bool """ return self.active def get_auth_token(self): """ Return the user's auth token. Use their password as part of the token because if the user changes their password we will want to invalidate all of their logins across devices. It is completely fine to use md5 here as nothing leaks. This satisfies Flask-Login by providing a means to create a token. :return: str """ private_key = current_app.config['SECRET_KEY'] serializer = URLSafeTimedSerializer(private_key) data = [str(self.id), md5(self.password.encode('utf-8')).hexdigest()] return serializer.dumps(data) def authenticated(self, with_password=True, password=''): """ Ensure a user is authenticated, and optionally check their password. :param with_password: Optionally check their password :type with_password: bool :param password: Optionally verify this as their password :type password: str :return: bool """ if with_password: return check_password_hash(self.password, password) return True def serialize_token(self, expiration=3600): """ Sign and create a token that can be used for things such as resetting a password or other tasks that involve a one off token. :param expiration: Seconds until it expires, defaults to 1 hour :type expiration: int :return: JSON """ private_key = current_app.config['SECRET_KEY'] serializer = TimedJSONWebSignatureSerializer(private_key, expiration) return serializer.dumps({'user_email': self.email}).decode('utf-8') def update_activity_tracking(self, ip_address): """ Update various fields on the user that's related to meta data on their account, such as the sign in count and ip address, etc.. :param ip_address: IP address :type ip_address: str :return: SQLAlchemy commit results """ self.sign_in_count += 1 self.last_sign_in_on = self.current_sign_in_on self.last_sign_in_ip = self.current_sign_in_ip self.current_sign_in_on = datetime.datetime.now(pytz.utc) self.current_sign_in_ip = ip_address return self.save() @classmethod def search(cls, query): """ Search a resource by 1 or more fields. :param query: Search query :type query: str :return: SQLAlchemy filter """ search_query = '%{0}%'.format(query) search_chain = (User.email.ilike(search_query), User.username.ilike(search_query)) return or_(*search_chain) @classmethod def is_last_admin(cls, user, new_role, new_active): """ Determine whether or not this user is the last admin account. :param user: User being tested :type user: User :param new_role: New role being set :type new_role: str :param new_active: New active status being set :type new_active: bool :return: bool """ is_changing_roles = user.role == 'admin' and new_role != 'admin' is_changing_active = user.active is True and new_active is None if is_changing_roles or is_changing_active: admin_count = User.query.filter(User.role == 'admin').count() active_count = User.query.filter(User.is_active is True).count() if admin_count == 1 or active_count == 1: return True return False @classmethod def bulk_delete(cls, ids): """ Override the general bulk_delete method because we need to delete them one at a time while also deleting them on Stripe. :param ids: List of ids to be deleted :type ids: list :return: int """ delete_count = 0 for id in ids: user = User.query.get(id) if user is None: continue if user.payment_id is None: user.delete() else: subscription = Subscription() cancelled = subscription.cancel(user=user) # If successful, delete it locally. if cancelled: user.delete() delete_count += 1 return delete_count
class Invoice(db.Model): __tablename__ = 'invoices' # Unique ID id = db.Column(db.Integer, primary_key=True) # Relationships user_id = db.Column(db.Integer, db.ForeignKey('users.id', onupdate='CASCADE', ondelete='CASCADE'), index=True, nullable=False) # Invoices details plan = db.Column(db.String(128), index=True) receipt_number = db.Column(db.String(128), index=True) description = db.Column(db.String(128)) period_start_on = db.Column(db.Date()) period_end_on = db.Column(db.Date()) currency = db.Column(db.String(12)) tax = db.Column(db.Integer()) tax_percent = db.Column(db.Float()) total = db.Column(db.Integer()) # De-normalize the card details so we can render a user's history properly even if they have no active subscription or change cards at some point brand = db.Column(db.String(32)) last4 = db.Column(db.Integer) exp_date = db.Column(db.Date, index=True) # Invoice runtime created_on = db.Column(db.DateTime(), default=datetime.datetime.utcnow) def __init__(self, **kwargs): super(Invoice, self).__init__(**kwargs) @classmethod def billing_history(cls, user=None): """ Return a billing history for a particular user """ invoices = cls.query.filter(cls.user_id == user.id).order_by( cls.created_on.desc()).limit(12) return invoices @classmethod def parse_from_event(): """ Parse and return the invoice information that will be saved. :return : dict """ data = payload['data']['object'] plan_info = data['lines']['data'][0]['plan'] period_start_on = datetime.datetime.utcfromtimestamp( data['lines']['data'][0]['period']['start']).date() period_end_on = datetime.datetime.utcfromtimestamp( data['lines']['data'][0]['period']['end']).date() invoice = { 'payment_id': data['customer'], 'plan': plan_info['name'], 'receipt_number': data['receipt_number'], 'description': plan_info['statement_descriptor'], 'period_start_on': period_start_on, 'period_end_on': period_end_on, 'currency': data['currency'], 'tax': data['tax'], 'tax_percent': data['tax_percent'], 'total': data['total'] } return invoice @classmethod def parse_from_api(cls, payload): """ Parse and return invoice information we are interested in. """ plan_info = payload['lines']['data'][0]['plan'] date = datetime.datetime.utcfromtimestamp(payload['date']) invoice = { 'plan': plan_info['name'], 'description': plan_info['statement_descriptor'], 'next_bill_on': date, 'amount_due': payload['amount_due'], 'interval': plan_info['interval'] } return invoice @classmethod def prepare_and_save(cls, parsed_event): """ Potentially save the invoice after argument the event fileds. :param parsed_event: Event params to be saved :type parsed_event: dict :return: User instance """ # Avoid circular import from snakeeyes.blueprints.user.models import User id = parsed_event.get('payment_id') user = User.query.filter((User.payment_id == id)).first() if user and user.credit_card: parsed_event['user_id'] = user.id parsed_event['brand'] = user.credit_card.brand parsed_event['last4'] = user.credit_card.last4 parsed_event['exp_date'] = user.credit_card.exp_date del parsed_event['payment_id'] invoice = Invoice(**parsed_event) db.session.add(invoice) db.session.commit() return user @classmethod def upcoming(cls, customer_id): """ Return the upcoming invoice """ invoice = PaymentInvoice.upcoming(customer_id=customer_id) return Invoice.parse_from_api(invoice)
class Bet(ResourceMixin, db.Model): __tablename__ = 'bets' id = db.Column(db.Integer, primary_key=True) # Relationships. user_id = db.Column(db.Integer, db.ForeignKey('users.id', onupdate='CASCADE', ondelete='CASCADE'), index=True, nullable=False) # Bet details. guess = db.Column(db.Integer()) die_1 = db.Column(db.Integer()) die_2 = db.Column(db.Integer()) roll = db.Column(db.Integer()) wagered = db.Column(db.BigInteger()) payout = db.Column(db.Float()) net = db.Column(db.BigInteger()) def __init__(self, **kwargs): # Call Flask-SQLAlchemy's constructor. super(Bet, self).__init__(**kwargs) @classmethod def is_winner(cls, guess, roll): """ Determine if the result is a win or loss. :param guess: Dice guess :type guess: int :param roll: Dice roll :type roll: int :return: bool """ if guess == roll: return True return False @classmethod def determine_payout(cls, payout, is_winner): """ Determine the payout. :param payout: Dice guess :type payout: float :param is_winner: Was the bet won or lost :type is_winner: bool :return: int """ if is_winner: return payout return 1.0 @classmethod def calculate_net(cls, wagered, payout, is_winner): """ Calculate the net won or lost. :param wagered: Dice guess :type wagered: int :param payout: Dice roll :type payout: float :param is_winner: Was the bet won or lost :type is_winner: bool :return: int """ if is_winner: return int(wagered * payout) return -wagered def save_and_update_user(self, user): """ Commit the bet and update the user's information. :return: SQLAlchemy save result """ self.save() user.coins += self.net user.last_bet_on = tzware_datetime() return user.save() def to_json(self): """ Return JSON fields to represent a bet. :return: dict """ params = { 'guess': self.guess, 'die_1': self.die_1, 'die_2': self.die_2, 'roll': self.roll, 'wagered': self.wagered, 'payout': self.payout, 'net': self.net, 'is_winner': Bet.is_winner(self.guess, self.roll) } return params
class Subscription(ResourceMixin, db.Model): __tablename__ = 'subscriptions' id = db.Column(db.Integer, primary_key=True) # Relationships. user_id = db.Column(db.Integer, db.ForeignKey('users.id', onupdate='CASCADE', ondelete='CASCADE'), index=True, nullable=False) # Subscription details. plan = db.Column(db.String(128)) coupon = db.Column(db.String(128)) def __init__(self, **kwargs): # Call Flask-SQLAlchemy's constructor. super(Subscription, self).__init__(**kwargs) @classmethod def get_plan_by_id(cls, plan): """ Pick the plan based on the plan identifier. :param plan: Plan identifier :type plan: str :return: dict or None """ for key, value in settings.STRIPE_PLANS.items(): if value.get('id') == plan: return settings.STRIPE_PLANS[key] return None @classmethod def get_new_plan(cls, keys): """ Pick the plan based on the plan identifier. :param keys: Keys to look through :type keys: list :return: str or None """ for key in keys: split_key = key.split('submit_') if isinstance(split_key, list) and len(split_key) == 2: if Subscription.get_plan_by_id(split_key[1]): return split_key[1] return None def create(self, user=None, name=None, plan=None, coupon=None, token=None): """ Create a recurring subscription. :param user: User to apply the subscription to :type user: User instance :param name: User's billing name :type name: str :param plan: Plan identifier :type plan: str :param coupon: Coupon code to apply :type coupon: str :param token: Token returned by JavaScript :type token: str :return: bool """ if token is None: return False if coupon: self.coupon = coupon.upper() customer = PaymentCustomer.create(token=token, email=user.email, plan=plan, coupon=self.coupon) # Update the user account. user.payment_id = customer.id user.name = name user.previous_plan = plan user.coins = add_subscription_coins( user.coins, Subscription.get_plan_by_id(user.previous_plan), Subscription.get_plan_by_id(plan), user.cancelled_subscription_on) user.cancelled_subscription_on = None # Set the subscription details. self.user_id = user.id self.plan = plan # Redeem the coupon. if coupon: coupon = Coupon.query.filter(Coupon.code == self.coupon).first() coupon.redeem() # Create the credit card. credit_card = CreditCard(user_id=user.id, **CreditCard.extract_card_params(customer)) db.session.add(user) db.session.add(credit_card) db.session.add(self) db.session.commit() return True def update(self, user=None, coupon=None, plan=None): """ Update an existing subscription. :param user: User to apply the subscription to :type user: User instance :param coupon: Coupon code to apply :type coupon: str :param plan: Plan identifier :type plan: str :return: bool """ PaymentSubscription.update(user.payment_id, coupon, plan) user.previous_plan = user.subscription.plan user.subscription.plan = plan user.coins = add_subscription_coins( user.coins, Subscription.get_plan_by_id(user.previous_plan), Subscription.get_plan_by_id(plan), user.cancelled_subscription_on) if coupon: user.subscription.coupon = coupon coupon = Coupon.query.filter(Coupon.code == coupon).first() if coupon: coupon.redeem() db.session.add(user.subscription) db.session.commit() return True def cancel(self, user=None, discard_credit_card=True): """ Cancel an existing subscription. :param user: User to apply the subscription to :type user: User instance :param discard_credit_card: Delete the user's credit card :type discard_credit_card: bool :return: bool """ PaymentSubscription.cancel(user.payment_id) user.payment_id = None user.cancelled_subscription_on = datetime.datetime.now(pytz.utc) user.previous_plan = user.subscription.plan db.session.add(user) db.session.delete(user.subscription) # Explicitly delete the credit card because the FK is on the # user, not subscription so we can't depend on cascading deletes. # This is for cases where you may want to keep a user's card # on file even if they cancelled. if discard_credit_card: db.session.delete(user.credit_card) db.session.commit() return True def update_payment_method(self, user=None, credit_card=None, name=None, token=None): """ Update the subscription. :param user: User to modify :type user: User instance :param credit_card: Card to modify :type credit_card: Credit Card instance :param name: User's billing name :type name: str :param token: Token returned by JavaScript :type token: str :return: bool """ if token is None: return False customer = PaymentCard.update(user.payment_id, token) user.name = name # Update the credit card. new_card = CreditCard.extract_card_params(customer) credit_card.brand = new_card.get('brand') credit_card.last4 = new_card.get('last4') credit_card.exp_date = new_card.get('exp_date') credit_card.is_expiring = new_card.get('is_expiring') db.session.add(user) db.session.add(credit_card) db.session.commit() return True
class Coupon(ResourceMixin, db.Model): DURATION = OrderedDict([('forever', 'Forever'), ('once', 'Once'), ('repeating', 'Repeating')]) __tablename__ = 'coupons' id = db.Column(db.Integer, primary_key=True) # Coupon details. code = db.Column(db.String(128), index=True, unique=True) duration = db.Column(db.Enum(*DURATION, name='duration_types'), index=True, nullable=False, server_default='forever') amount_off = db.Column(db.Integer()) percent_off = db.Column(db.Integer()) currency = db.Column(db.String(8)) duration_in_months = db.Column(db.Integer()) max_redemptions = db.Column(db.Integer(), index=True) redeem_by = db.Column(AwareDateTime(), index=True) times_redeemed = db.Column(db.Integer(), index=True, nullable=False, default=0) valid = db.Column(db.Boolean(), nullable=False, server_default='1') def __init__(self, **kwargs): if self.code: self.code = self.code.upper() else: self.code = Coupon.random_coupon_code() # Call Flask-SQLAlchemy's constructor. super(Coupon, self).__init__(**kwargs) @hybrid_property def redeemable(self): """ Return coupons that are still redeemable. Coupons will become invalid once they run out on save. We want to explicitly do a date check to avoid having to hit Stripe's API to get back potentially valid codes. :return: SQLAlchemy query object """ is_redeemable = or_(self.redeem_by.is_(None), self.redeem_by >= datetime.datetime.now(pytz.utc)) return and_(self.valid, is_redeemable) @classmethod def search(cls, query): """ Search a resource by 1 or more fields. :param query: Search query :type query: str :return: SQLAlchemy filter """ if query == '': return '' #if not query: # return '' search_query = f'%{query}%' return or_(Coupon.code.ilike(search_query)) @classmethod def random_coupon_code(cls): """ Create a human readable random coupon code. :return: str """ charset = string.digits + string.ascii_uppercase charset = charset.replace('B', '').replace('I', '') charset = charset.replace('O', '').replace('S', '') charset = charset.replace('0', '').replace('1', '') random_chars = ''.join(choice(charset) for _ in range(14)) coupon_code = f'{random_chars[0:4]}-{random_chars[5:9]}-{random_chars[10:14]}' return coupon_code @classmethod def expire_old_coupons(cls, compare_datetime=None): """ Invalidate coupons that are past their redeem date. :param compare_datetime: Time to compare at :type compare_datetime: date :return: The result of updating the records """ if compare_datetime is None: compare_datetime = datetime.datetime.now(pytz.utc) Coupon.query.filter(Coupon.redeem_by <= compare_datetime) \ .update({Coupon.valid: not Coupon.valid}) return db.session.commit() @classmethod def create(cls, params): """ Return whether or not the coupon was created successfully. :return: bool """ payment_params = params payment_params['code'] = payment_params['code'].upper() if payment_params.get('amount_off'): payment_params['amount_off'] = \ dollars_to_cents(payment_params['amount_off']) PaymentCoupon.create(**payment_params) if 'id' in payment_params: payment_params['code'] = payment_params['id'] del payment_params['id'] if 'redeem_by' in payment_params: if payment_params.get('redeem_by') is not None: params['redeem_by'] = payment_params.get('redeem_by').replace( tzinfo=pytz.UTC) coupon = Coupon(**payment_params) db.session.add(coupon) db.session.commit() return True @classmethod def bulk_delete(cls, ids): """ Override the general bulk_delete method because we need to delete them one at a time while also deleting them on Stripe. :param ids: List of ids to be deleted :type ids: list :return: int """ delete_count = 0 for id in ids: coupon = Coupon.query.get(id) if coupon is None: continue stripe_response = PaymentCoupon.delete(coupon.code) if stripe_response.get('deleted'): coupon.delete() delete_count += 1 return delete_count @classmethod def find_by_code(cls, code): """ Find a coupon by its code. :param code: Coupon code to find :type code: str :return: Coupon instance """ formatted_code = code.upper() coupon = Coupon.query.filter(Coupon.redeemable, Coupon.code == formatted_code).first() return coupon def redeem(self): """ Update the redeem stats for this coupon. :return: Result of saving the record """ self.times_redeemed += 1 if self.max_redemptions: if self.times_redeemed >= self.max_redemptions: self.valid = False return db.session.commit() def apply_discount_to(self, amount): """ Apply the discount to an amount. :param amount: Amount in cents :type amount: int :return: int """ if self.amount_off: amount -= self.amount_off elif self.percent_off: amount *= (1 - (self.percent_off * 0.01)) return int(amount) def to_json(self): """ Return JSON fields to represent a coupon. :return: dict """ params = { 'duration': self.duration, 'duration_in_months': self.duration_in_months } if self.amount_off: params['amount_off'] = cents_to_dollars(self.amount_off) if self.percent_off: params['percent_off'] = self.percent_off return params
class User(UserMixin, db.Model): __tablename__ = 'users' ROLE = OrderedDict([ ('member', "Member"), ("admin", "Admin") ]) # Unique idntification number id = db.Column(db.Integer, primary_key = True) # Credit card relationship credit_card = db.relationship(CreditCard, uselist=False, backref='users', passive_deletes=True) # Subscription relationship subscription = db.relationship(Subscription, backref='users', uselist=False, passive_deletes=True) # Invoic Relationships invoice = db.relationship(Invoice, backref='users', passive_deletes=True) # User credentials role = db.Column(db.Enum(*ROLE, name = 'role_type', native_enum = False), nullable = False, default='member') username = db.Column(db.String(128), nullable=True, unique=True) email = db.Column(db.String(128), nullable=False, unique = True) active = db.Column(db.Boolean, default = True, nullable=False) hash_password = db.Column(db.String(240), nullable=False) confirmed = db.Column(db.Boolean, default = False) # User tracking information sign_in_count = db.Column(db.Integer, default=0) current_sign_in_on = db.Column(db.DateTime(), default=datetime.datetime.utcnow) current_sign_in_ip = db.Column(db.String(24)) last_sign_in_on = db.Column(db.DateTime(), default=datetime.datetime.utcnow) last_sign_in_ip = db.Column(db.String(24)) # Billing. name = db.Column(db.String(128), index=True) payment_id = db.Column(db.String(128), index=True) cancelled_subscription_on = db.Column(db.DateTime(), default=datetime.datetime.utcnow) # User run time created_on = db.Column(db.DateTime(), default = datetime.datetime.utcnow) updated_on = db.Column(db.DateTime(), default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow) @property def password(self): raise AttributeError('Password is not a readable attribute') @password.setter def password(self, password): self.hash_password = generate_password_hash(password) def verify_password(self, password): return check_password_hash(self.hash_password, password) def is_active(self): return self.active @classmethod def search(cls, query): """ Search a resource by 1 or more fields. :param query: Search query :type query: str :return: SQLAlchemy filter """ if not query: return '' search_query = '%{0}%'.format(query) search_chain = (User.email.ilike(search_query), User.username.ilike(search_query)) return or_(*search_chain) @classmethod def sort_by(cls, field, direction): """This help to sort the user base on the field column and direction. """ if field not in cls.__table__.columns: field = "created_on" if direction not in ('asc', 'desc'): direction = 'asc' return field, direction @classmethod def is_last_admin(cls, user, new_role, new_active): """This particular method will help to check if this particular user is the last admin.""" is_changing_role = user.role == 'admin' and new_role != 'admin' is_changing_active = user.active is True and new_role is None if is_changing_role or is_changing_active : admin_count = User.query.filter(User.role == 'admin').count() active_count = User.query.filter(User.is_active is True).count() if admin_count == 1 or active_count == 1: return True return False @classmethod def get_bulk_action_id(cls, scope, ids, omit_id=None, query=''): """Determine bulk of id to be deleted.""" omit_id = list(map(str, omit_id)) if scope == 'all_search_result': ids = User.query.with_entities(User.id).filter(User.search(query)) ids = [str(item[0]) for item in ids] if omit_id: ids = [id for id in ids if id not in omit_id] return ids @classmethod def bulk_delete(cls, ids): """Delete selected user id""" delete_count = User.query.filter(User.id.in_(ids)).delete(synchronize_session=False) return delete_count def track_user_activities(self, ip_address): self.sign_in_count = +1 self.last_sign_in_on = self.current_sign_in_on self.last_sign_in_ip = self.current_sign_in_ip self.current_sign_in_ip = ip_address self.current_sign_in_on = datetime.datetime.utcnow() return True def generate_token(self, expiration=3600): s = TimedJSONWebSignatureSerializer(current_app.config["SECRET_KEY"], expires_in=expiration) return s.dumps({"confirm": self.id}).decode('utf-8') def verify_token(self,token): s = TimedJSONWebSignatureSerializer(current_app.config['SECRET_KEY']) try: data = s.loads(token) except: return False if data.get('confirm') != self.id: return False self.confirmed = True db.session.add(self) user = User.query.filter_by(id = data.get('confirm')).first() return user.email def generate_reset_token(self): s = TimedJSONWebSignatureSerializer(current_app.config['SECRET_KEY']) return s.dumps({'user_id': self.id}) @staticmethod def confirm_reset_token(token): s = TimedJSONWebSignatureSerializer(current_app.config['SECRET_KEY']) try: data = s.loads(token) except: return False user = User.query.get(data.get('user_id')) return user.id @classmethod def add_fake(cls): from random import seed, randint, choice from sqlalchemy.exc import IntegrityError from snakeeyes.extensions import fake users = [] seed() while len(users) < 100: u = User( email = fake.email(), username = fake.name()+str(randint(0, 300)), active = bool(choice([True, False])), password = '******', sign_in_count = randint(0,20), current_sign_in_on = fake.date_time_between(start_date='-1y', end_date='now'), last_sign_in_ip = fake.ipv4(), confirmed = bool(choice([True, False])), current_sign_in_ip = fake.ipv4(), last_sign_in_on = fake.date_time_between(start_date='-1y', end_date='now'), created_on = fake.date_time_between(start_date='-15y', end_date='now'), updated_on = fake.date_time_between(start_date='-15y', end_date='now') ) users.append(u) db.session.add(u) try: db.session.commit() except IntegrityError: db.session.rollback() @classmethod def add_admin(cls): admin = User(email = '*****@*****.**', password='******', confirmed=True, role='admin') db.session.add(admin) db.session.commit() return True