from enum import Enum from werkzeug.security import generate_password_hash, check_password_hash from . import db, login from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.sql import func from secrets import token_hex from flask_login import UserMixin import json from json import JSONEncoder from datetime import datetime import pytz from flask_moment import Moment moment = Moment() class Role(Enum): none = 0 owner = 1 hunter = 2 bunny = 3 class GameState(Enum): initiated = 1 published = 2 started = 3 interrupted = 4 finished = 5 class GamePlayer(db.Model): __tablename__ = 'game_player' game_id = db.Column(db.Integer, db.ForeignKey('game.id'), primary_key=True, nullable=False) player_id = db.Column(db.Integer, db.ForeignKey('player.id'), primary_key=True, nullable=False) role = db.Column(db.Enum(Role), server_default=Role(0).name, nullable=False) game = db.relationship('Game', back_populates='game_players') player = db.relationship('Player', back_populates='player_games') class PlayerFoundObjective(db.Model): __tablename__ = 'player_found_objective' objective_id = db.Column(db.Integer, db.ForeignKey('objective.id'), primary_key=True, nullable=False, server_default='-1') player_id = db.Column(db.Integer, db.ForeignKey('player.id'), primary_key=True, nullable=False, server_default='-1') timestamp = db.Column(db.DateTime, server_default=func.now(), nullable=False) player = db.relationship('Player', back_populates='player_found_objectives') objective = db.relationship('Objective', back_populates='objective_found_by') class NotificationPlayer(db.Model): __tablename__ = 'notification_player' notification_id = db.Column(db.Integer, db.ForeignKey('notification.id'), primary_key=True, nullable=False) player_id= db.Column(db.Integer, db.ForeignKey('player.id'), primary_key=True, nullable=False) been_shown = db.Column(db.Boolean, server_default='True', nullable=False) notification = db.relationship('Notification', back_populates='notification_recipients') recipient = db.relationship('Player', back_populates='player_notifications') class PlayerCaughtPlayer(db.Model): __tablename__ = 'player_caught_player' id = db.Column(db.Integer, primary_key=True, autoincrement=True, server_default='-1') catching_player_id = db.Column(db.Integer, db.ForeignKey('player.id'), nullable=False) caught_player_id = db.Column(db.Integer, db.ForeignKey('player.id'), nullable=False) photo_reference = db.Column(db.String(128), unique=True, nullable=False) timestamp = db.Column(db.DateTime, server_default=func.now(), nullable=False) catching_player = db.relationship('Player', back_populates='player_caught_by_players', foreign_keys=[catching_player_id]) caught_player = db.relationship('Player', back_populates='player_caught_players', foreign_keys=[caught_player_id]) class Game(db.Model): __tablename__ = 'game' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(64), index=True, unique=True, nullable=False) state = db.Column(db.Enum(GameState), server_default=GameState(1).name, nullable=False) start_time = db.Column(db.DateTime) end_time = db.Column(db.DateTime) game_players = db.relationship( 'GamePlayer', back_populates='game', cascade="save-update, merge, delete, delete-orphan") players = association_proxy('game_players', 'player', creator=lambda player: GamePlayer(player=player)) # to enable game.players.append(player) objectives = db.relationship( 'Objective', lazy='select', backref=db.backref('game', lazy='joined')) notifications = db.relationship( 'Notification', lazy='select', backref=db.backref('game', lazy='joined')) def last_player_locations(self): return [player.last_location(self) for player in self.players if player.locations] class Player(UserMixin, db.Model): """ !Always call set_auth_hash() after creating new instance! """ __tablename__ = 'player' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(64), unique=True, nullable=False) auth_hash = db.Column(db.String(32), unique=True, nullable=True) password_hash = db.Column(db.String(128)) player_games = db.relationship( 'GamePlayer', back_populates='player', cascade='save-update, merge, delete, delete-orphan') games = association_proxy('player_games', 'game', creator=lambda game: GamePlayer(game=game)) player_found_objectives = db.relationship( 'PlayerFoundObjective', back_populates='player', cascade='save-update, merge, delete, delete-orphan') found_objectives = association_proxy('player_found_objectives', 'objective', creator=lambda objective: PlayerFoundObjective(objective=objective)) player_notifications = db.relationship( 'NotificationPlayer', back_populates='recipient', cascade='save-update, merge, delete, delete-orphan') notifications = association_proxy('player_notifications', 'notification', creator=lambda notification: NotificationPlayer(notification=notification)) player_caught_players = db.relationship( 'PlayerCaughtPlayer', back_populates='catching_player', cascade='save-update, merge, delete, delete-orphan', foreign_keys=[PlayerCaughtPlayer.catching_player_id]) caught_players = association_proxy('player_caught_players', 'player', creator=lambda player: PlayerCaughtPlayer(caught_player=player)) player_caught_by_players = db.relationship( 'PlayerCaughtPlayer', back_populates='caught_player', cascade='save-update, merge, delete, delete-orphan', foreign_keys=[PlayerCaughtPlayer.caught_player_id]) caught_by_players = association_proxy('player_caught_by_players', 'player', creator=lambda player: PlayerCaughtPlayer(catching_player=player)) locations = db.relationship( 'Location', lazy='select', backref=db.backref('player', lazy='joined')) def set_password(self, password): self.password_hash = generate_password_hash(password) def set_auth_hash(self): self.auth_hash = token_hex(16) def check_password(self, password): return check_password_hash(self.password_hash, password) def locations_game(self, game): # pylint: disable=not-an-iterable if not self.locations: return None if game is None: return self.locations game_start = game.start_time or datetime.min game_end = game.end_time or datetime.max return (location for location in self.locations if location.timestamp > game_start and location.timestamp < game_end) def last_location(self, game=None): # pylint: disable=not-an-iterable if not self.locations: return None if game is None: return max(self.locations, key=lambda location: location.timestamp) return max(self.locations_game(game), key=lambda location: location.timestamp) @staticmethod def delete_orphans(): Player.query.filter(~Player.player_games.any()).delete() db.session.commit() @login.user_loader def load_user(id): return Player.query.get(int(id)) class Objective(db.Model): """ !Always call set_hash after() creating new instance! """ __tablename__ = 'objective' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(64)) game_id = db.Column(db.Integer, db.ForeignKey('game.id'), nullable=False) hash = db.Column(db.String(32), unique=True, nullable=False) longitude = db.Column(db.Numeric(precision=15, scale=10, asdecimal=False, decimal_return_scale=None)) # maybe check asdecimal and decimal_return_scale later? latitude = db.Column(db.Numeric(precision=15, scale=10, asdecimal=False, decimal_return_scale=None)) objective_found_by = db.relationship( 'PlayerFoundObjective', back_populates='objective', cascade='save-update, merge, delete, delete-orphan') found_by = association_proxy('objective_found_by', 'player', creator=lambda player: PlayerFoundObjective(player=player)) def set_hash(self): self.hash = token_hex(16) class ObjectiveMinimalEncoder(JSONEncoder): def default(self, objective): return { 'name' : objective.name, 'hash' : objective.hash, 'longitude' : objective.longitude, 'latitude' : objective.latitude } class Location(db.Model): __tablename__ = 'location' id = db.Column(db.Integer, primary_key=True) player_id = db.Column(db.Integer, db.ForeignKey('player.id'), nullable=False) longitude = db.Column(db.Numeric(precision=15, scale=10, asdecimal=False, decimal_return_scale=None),nullable=False) # maybe check asdecimal and decimal_return_scale later? latitude = db.Column(db.Numeric(precision=15, scale=10, asdecimal=False, decimal_return_scale=None),nullable=False) timestamp = db.Column(db.DateTime, server_default=func.now(), nullable=False) def __str__(self): return f'{self.longitude}, {self.latitude}' class LocationEncoder(JSONEncoder): def default(self, location): return { 'player_name' : location.player.name, 'longitude' : location.longitude, 'latitude' : location.latitude, 'timestamp_utc' : str(location.timestamp) } class Notification(db.Model): __tablename__ = 'notification' id = db.Column(db.Integer, primary_key=True) game_id = db.Column(db.Integer, db.ForeignKey('game.id'), nullable=False) message = db.Column(db.Text, nullable=False) type = db.Column(db.String(64), nullable=False, default='General') timestamp = db.Column(db.DateTime, server_default=func.now(), nullable=False) notification_recipients = db.relationship( 'NotificationPlayer', back_populates='notification', cascade='save-update, merge, delete, delete-orphan') recipients = association_proxy('notification_recipients', 'player', creator=lambda player: NotificationPlayer(recipient=player)) #start_time = db.Column(db.DateTime, server_default=func.now())