class FinalFeedback(db.Model, CRUDMixin): id = db.Column(db.Integer, primary_key=True, autoincrement=True) motivation = db.Column(db.Enum(MotivationFeedback), nullable=False) user_uuid = db.Column(db.String(255), db.ForeignKey('user.uuid')) user = relationship("User", back_populates="feedback")
class Badge(db.Model, CRUDMixin): """ Badges that can be earned """ __tablename__ = "badge" id = db.Column(db.Integer, primary_key=True, autoincrement=True) name = db.Column(db.String(255), unique=True, nullable=False) src_filename = db.Column(db.String(255), nullable=False) description = db.Column(db.Text, nullable=False) condition = db.Column(db.Enum(BadgeConditions)) # Relations users = relationship("User", secondary=badges_user_association_table, back_populates="badges") def __repr__(self): return self.name def to_dict(self) -> dict: return dict(id=self.id, name=self.name, src_filename=self.src_filename, description=self.description) @classmethod def active_badges(cls) -> typing.Iterable: return cls.query.filter(cls.condition != None).all() @classmethod def json_list(cls): return dict( list( map(lambda badge: (badge.name, badge.to_dict()), cls.query.all()))) @classmethod def earned_through_action(cls, user: User, command: str) -> typing.Set: return { badge for badge in cls.active_badges() if badge.condition.is_solved(user, command) }
class ActionLog(db.Model, BaseModel): __tablename__ = "action_logs" user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) user = db.relationship("User", foreign_keys=[user_id], lazy='selectin') action_date = db.Column(db.DateTime, default=datetime.utcnow, nullable=False) action = db.Column(db.String(), nullable=False) screen = db.Column(db.Enum(Screen), nullable=False) def to_json(self): return { "action_date": str(self.action_date), "user_id": self.user_id, "full_name": str(self.user.full_name), "action": self.action, "screen": self.screen.value }
class User(db.Model, CRUDMixin): __tablename__ = "user" uuid = db.Column(db.String(255), primary_key=True) first_seen = db.Column(db.DateTime, nullable=False, default=datetime.datetime.now) mode = db.Column(db.Enum(GameModes), nullable=False) # demographics age = db.Column(db.String(255)) gender = db.Column(db.String(255)) english_skills = db.Column(db.String(255)) bash_experience = db.Column(db.String(255)) # Relations user_identifiers = relationship("UserIdentifier", secondary=user_identifier_association, back_populates="users") commands = relationship("SubmittedCommand", back_populates="user") badges = relationship("Badge", secondary=badges_user_association_table, back_populates="users") solved_challenges = relationship("SolvedChallenges", back_populates="user") feedback = relationship("FinalFeedback", back_populates="user") def __repr__(self): return f"<User: {self.uuid}>" @property def correct_command_count(self): return SubmittedCommand.query.filter( SubmittedCommand.user_uuid == self.uuid, SubmittedCommand.solved_challenge == True).count() @property def wrong_command_count(self): return SubmittedCommand.query.filter( SubmittedCommand.user_uuid == self.uuid, SubmittedCommand.solved_challenge == False).count() @property def last_seen(self): try: return SubmittedCommand.query.filter( SubmittedCommand.user_uuid == self.uuid).order_by( SubmittedCommand.time_submitted.desc()).first( ).time_submitted except AttributeError: return self.first_seen @classmethod def create_user(cls): return cls.create(uuid=str(uuid4()), mode=choice(list(GameModes))) def set_identifier(self, ip_addr: str, user_agent: str): # does it exist? identifier = UserIdentifier.query.filter( UserIdentifier.ip_addr == ip_addr, UserIdentifier.user_agent == user_agent).first() if identifier: if identifier not in self.user_identifiers: # update list of user agent of user if the current identifier is not associated with user self.user_identifiers.append(identifier) else: # user agent does not exist -> Create it and link to user self.user_identifiers.append( UserIdentifier.create(ip_addr=ip_addr, user_agent=user_agent)) # finally commit changes self.save() def add_new_badges(self, applicable_badges: typing.Set): new_badges = applicable_badges - set(self.badges) if new_badges: [self.badges.append(badge) for badge in new_badges] self.save() logger.debug(f"User earned new badges: {new_badges}") def to_dict(self): return dict(uuid=self.uuid, first_seen=self.first_seen, last_seen=self.last_seen, mode=self.mode.value) def add_solved_challenge(self, challenge): try: a = SolvedChallenges(challenge_id=challenge.identifier) self.solved_challenges.append(a) self.save() except FlushError: pass def add_command(self, command_string: str, challenge_id: str, solved: bool): SubmittedCommand.create(command_string=command_string, challenge_id=challenge_id, solved_challenge=solved, user=self) if solved: challenge = Challenge.query.get(challenge_id) self.add_solved_challenge(challenge)
class User(UserMixin, ResourceMixin, db.Model): ROLE = OrderedDict([('admin', 'Admin'), ('client', 'Client'), ('staff', 'Staff'), ('public', 'Public')]) __tablename__ = 'users' id = db.Column(db.Integer, primary_key=True) # Relationships client = db.relationship(Client, backref=db.backref('user', uselist=False), uselist=False, cascade="all, delete-orphan") staff = db.relationship(Staff, backref=db.backref('user', uselist=False), uselist=False, cascade="all, delete-orphan") comments = db.relationship(Comment, backref=db.backref('author', uselist=False), lazy='dynamic') appointments = db.relationship(Appointment, backref='user', lazy='dynamic') # Properties. role = db.Column(db.Enum(*ROLE, name='role_types', native_enum=False), index=True, nullable=False, server_default='public') active = db.Column('is_active', db.Boolean(), nullable=False, server_default='1') username = db.Column(db.String(24), unique=True, index=True, nullable=False) email = db.Column(db.String(255), unique=True, index=True, nullable=False, server_default='') password = db.Column(db.String(128), nullable=False, server_default='') first_name = db.Column(db.String(256), nullable=False) middle_name = db.Column(db.String(256)) last_name = db.Column(db.String(256), index=True, nullable=False) unit_number = db.Column(db.String(128)) street_address = db.Column(db.String(128)) suburb = db.Column(db.String(128)) postcode = db.Column(db.String(4)) state = db.Column(db.String(128)) country = db.Column(db.String(128)) phone_number = db.Column(db.String(128), nullable=False) photo = db.Column(db.String(256)) # 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)) # Additional settings. locale = db.Column(db.String(5), nullable=False, server_default='en') def __init__(self, **kwargs): # Call Flask-SQLAlchemy's constructor. super(User, self).__init__(**kwargs) self.password = User.encrypt_password(kwargs.get('password', '')) @hybrid_property def full_name(self): if self.middle_name: return self.last_name.upper( ) + ', ' + self.first_name + ' ' + self.middle_name else: return self.last_name.upper() + ', ' + self.first_name @hybrid_property def first_last_name(self): return self.first_name + ' ' + self.last_name @hybrid_property def full_address(self): street_address = self.unit_number + "/" + self.street_address if self.unit_number and self.street_address else self.street_address address_elements = [ street_address, self.suburb, self.postcode, self.state, self.country ] return ", ".join( address_elements) if None not in address_elements else None @classmethod def find_by_identity(cls, identity): """ Find user by 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 password using Bcrypt. :param plaintext_password: Password in plain text :type plaintext_password: str :return: str """ if plaintext_password: return hashpw(plaintext_password, gensalt()) return None @classmethod def deserialize_token(cls, token): """ Obtain user from de-serializing 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 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 server.blueprints.user.tasks import (deliver_password_reset_email) deliver_password_reset_email.delay(u.id, reset_token) return u @classmethod def search(cls, query): """ Search using ILIKE (case-insensitive) expression. :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 is_last_admin(cls, user, new_role, new_active): """ Return whether user is 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 def is_active(self): """ Return whether user account is active (overrides Flask-Login default). :return: bool """ return self.active def get_auth_token(self): """ Return user's auth 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 user authenticated. :param with_password: Optionally check password :type with_password: bool :param password: Password to verify :type password: str :return: bool """ if with_password: return checkpw(password.encode('utf-8'), self.password.encode('utf-8')) return True def serialize_token(self, expiration=3600): """ Serialize token for resetting passwords, etc. :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 user's meta data. :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() def to_json(self): active = "Active" if self.active == True else "Disabled" json_data = { 'id': self.id, 'created': self.created_on, 'updated': self.updated_on, 'active': active, 'firstLastName': self.first_last_name, 'firstName': self.first_name, 'middleName': self.middle_name, 'lastName': self.last_name.upper(), 'fullName': self.full_name, 'username': self.username, 'email': self.email, 'phoneNumber': self.phone_number, 'unitNumber': self.unit_number, 'streetAddress': self.street_address, 'suburb': self.suburb, 'postcode': self.postcode, 'state': self.state, 'country': self.country, 'fullAddress': self.full_address, 'role': self.role, 'photo': self.photo, } return json_data
class Comment(ResourceMixin, db.Model): __tablename__ = 'comments' TOPICS = OrderedDict([('tech', 'Science & Technology'), ('politics', 'Politics'), ('health', 'Health & Food'), ('entertainment', 'Movies, Music & more')]) id = db.Column(db.Integer, primary_key=True) version = db.Column(db.Integer, server_default='1') topic = db.Column(db.Enum(*TOPICS.keys(), name='topic_tags'), index=True, nullable=False, server_default='tech') text = db.Column(db.String(255), index=True, nullable=False, server_default='') user_id = db.Column(db.Integer, db.ForeignKey('users.id', onupdate='CASCADE', ondelete='CASCADE'), index=True, nullable=False) def __init__(self, **kwargs): # Call Flask-SQLAlchemy's constructor. super(Comment, self).__init__(**kwargs) def serialize(self, lite=False): """ Return JSON fields to render the comment. :return: dict """ from server.blueprints.user.models import User user = User.query.get(self.user_id) username = user.username if user else "" params = { 'id': self.id, 'version': self.version, 'topic': self.topic, 'text': self.text, 'created_by': username } if lite: del params['created_by'] return params @classmethod def find(cls, _id=None, username=None, topic=None): """ Find comments. :param id: Comment id to find :type id: str :param username: Username of person who created comment :type username: str :param topic: Comment topic to find :type topic: str :return: Comments """ comments = Comment.query if not topic and not id and not username: return comments.all() if _id: return [comments.get(_id)] if topic: formatted_topic = topic.lower() comments = comments.filter(Comment.topic == formatted_topic) if username: from server.blueprints.user.models import User user = User.query.filter(User.username == username).first() if user: comments = comments.filter(Comment.user_id == user.id) return comments