diff --git a/app/models.py b/app/models.py deleted file mode 100644 index 587cd6d..0000000 --- a/app/models.py +++ /dev/null @@ -1,268 +0,0 @@ -from enum import Enum -from secrets import token_hex -from datetime import datetime -from json import JSONEncoder -from flask_login import UserMixin -from werkzeug.security import generate_password_hash, check_password_hash -from sqlalchemy.ext.associationproxy import association_proxy -from sqlalchemy.sql import func -from flask_moment import Moment -from app import db, login - -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] - - def bunnies(self): - # pylint: disable=not-an-iterable - return [gameplayer.player for gameplayer in self.game_players if gameplayer.role == Role.bunny] - - def last_locations(self, players): - locations = [] - for player in players: - locations.append(player.last_location(self)) - return locations - - def owned_by(self, player): - # pylint: disable=not-an-iterable - '''given player is an owner of game''' - return player in [gameplayer.player for gameplayer in self.game_players if gameplayer.role == Role.owner] - -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_during_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_during_game(game), key=lambda location: location.timestamp) - - def role_in_game(self, game): - '''returns the role as Role enum of player in given game. Returns None if player does not participate in game''' - # pylint: disable=not-an-iterable - gameplayers = [gameplayer for gameplayer in self.player_games if gameplayer.game == game] - if not gameplayers: - return None - return gameplayers[0].role - - def owns_game_played_by(self, player): - '''self is an owner of a game the player participates in''' - return self in [gameplayer.player for gameplayers in - [game.game_players for game in player.games] - for gameplayer in gameplayers if gameplayer.role == Role.owner] - - @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) - - def owned_by(self, player): - '''given player is an owner of a game object is part of''' - return player in [gameplayer.player for gameplayer in self.game.game_players if gameplayer.role == Role.owner] - -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()) diff --git a/app/models/__init__.py b/app/models/__init__.py new file mode 100644 index 0000000..ccb5cd3 --- /dev/null +++ b/app/models/__init__.py @@ -0,0 +1,16 @@ +from app import login + +from app.models.game import Game +from app.models.game_player import GamePlayer +from app.models.game_state import GameState +from app.models.location import Location, LocationEncoder +from app.models.notification_player import NotificationPlayer +from app.models.objective import Objective, ObjectiveMinimalEncoder +from app.models.player_caught_player import PlayerCaughtPlayer +from app.models.player_found_objective import PlayerFoundObjective +from app.models.role import Role +from app.models.user import User + +@login.user_loader +def load_user(id): + return User.query.get(int(id)) diff --git a/app/models/game.py b/app/models/game.py new file mode 100644 index 0000000..560822d --- /dev/null +++ b/app/models/game.py @@ -0,0 +1,43 @@ +from sqlalchemy.ext.associationproxy import association_proxy +from app import db +from app.models import GameState, GamePlayer, Role + +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") + users = association_proxy('game_players', 'user', + creator=lambda user: GamePlayer(user=user)) # 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.users if player.locations] + + def bunnies(self): + # pylint: disable=not-an-iterable + return [gameplayer.player for gameplayer in self.game_players if gameplayer.role == Role.bunny] + + def last_locations(self, players): + locations = [] + for player in players: + locations.append(player.last_location(self)) + return locations + + def owned_by(self, player): + # pylint: disable=not-an-iterable + '''given player is an owner of game''' + return player in [gameplayer.player for gameplayer in self.game_players if gameplayer.role == Role.owner] diff --git a/app/models/game_player.py b/app/models/game_player.py new file mode 100644 index 0000000..1b24aa9 --- /dev/null +++ b/app/models/game_player.py @@ -0,0 +1,10 @@ +from app import db +from app.models import Role + +class GamePlayer(db.Model): + __tablename__ = 'game_player' + game_id = db.Column(db.Integer, db.ForeignKey('game.id'), primary_key=True, nullable=False) + user_id = db.Column(db.Integer, db.ForeignKey('user.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') + user = db.relationship('User', back_populates='user_games') diff --git a/app/models/game_state.py b/app/models/game_state.py new file mode 100644 index 0000000..7c50865 --- /dev/null +++ b/app/models/game_state.py @@ -0,0 +1,9 @@ +from enum import Enum + +class GameState(Enum): + initiated = 1 + published = 2 + started = 3 + interrupted = 4 + finished = 5 + \ No newline at end of file diff --git a/app/models/location.py b/app/models/location.py new file mode 100644 index 0000000..999321f --- /dev/null +++ b/app/models/location.py @@ -0,0 +1,24 @@ +from json import JSONEncoder +from sqlalchemy.sql import func + +from app import db + +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) + } diff --git a/app/models/notification.py b/app/models/notification.py new file mode 100644 index 0000000..749e6b5 --- /dev/null +++ b/app/models/notification.py @@ -0,0 +1,20 @@ +from sqlalchemy.ext.associationproxy import association_proxy +from sqlalchemy.sql import func + +from app import db +from app.models import NotificationPlayer + +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)) diff --git a/app/models/notification_player.py b/app/models/notification_player.py new file mode 100644 index 0000000..00b6418 --- /dev/null +++ b/app/models/notification_player.py @@ -0,0 +1,10 @@ +from app import db + +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') + \ No newline at end of file diff --git a/app/models/objective.py b/app/models/objective.py new file mode 100644 index 0000000..783c835 --- /dev/null +++ b/app/models/objective.py @@ -0,0 +1,38 @@ +from secrets import token_hex +from json import JSONEncoder +from sqlalchemy.ext.associationproxy import association_proxy + +from app import db +from app.models import PlayerFoundObjective, Role + +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) + + def owned_by(self, player): + '''given player is an owner of a game object is part of''' + return player in [gameplayer.player for gameplayer in self.game.game_players if gameplayer.role == Role.owner] + +class ObjectiveMinimalEncoder(JSONEncoder): + def default(self, objective): + return { + 'name' : objective.name, + 'hash' : objective.hash, + 'longitude' : objective.longitude, + 'latitude' : objective.latitude + } diff --git a/app/models/player_caught_player.py b/app/models/player_caught_player.py new file mode 100644 index 0000000..f6eb5ec --- /dev/null +++ b/app/models/player_caught_player.py @@ -0,0 +1,14 @@ +from sqlalchemy.sql import func + +from app import db + +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]) + \ No newline at end of file diff --git a/app/models/player_found_objective.py b/app/models/player_found_objective.py new file mode 100644 index 0000000..a7e254c --- /dev/null +++ b/app/models/player_found_objective.py @@ -0,0 +1,11 @@ +from sqlalchemy.sql import func + +from app import db + +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') diff --git a/app/models/role.py b/app/models/role.py new file mode 100644 index 0000000..5a4090e --- /dev/null +++ b/app/models/role.py @@ -0,0 +1,8 @@ +from enum import Enum + +class Role(Enum): + none = 0 + owner = 1 + hunter = 2 + bunny = 3 + \ No newline at end of file diff --git a/app/models/user.py b/app/models/user.py new file mode 100644 index 0000000..8de7f99 --- /dev/null +++ b/app/models/user.py @@ -0,0 +1,105 @@ +from secrets import token_hex +from datetime import datetime + +from flask_login import UserMixin +from sqlalchemy.ext.associationproxy import association_proxy +from werkzeug.security import generate_password_hash, check_password_hash + +from app import db +from app.models import GamePlayer, PlayerFoundObjective, PlayerCaughtPlayer, NotificationPlayer, Role + +class User(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)) + + user_games = db.relationship( + 'GamePlayer', + back_populates='user', + cascade='save-update, merge, delete, delete-orphan') + games = association_proxy('user_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_during_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_during_game(game), key=lambda location: location.timestamp) + + def role_in_game(self, game): + '''returns the role as Role enum of player in given game. Returns None if player does not participate in game''' + # pylint: disable=not-an-iterable + gameplayers = [gameplayer for gameplayer in self.user_games if gameplayer.game == game] + if not gameplayers: + return None + return gameplayers[0].role + + def owns_game_played_by(self, player): + '''self is an owner of a game the player participates in''' + return self in [gameplayer.player for gameplayers in + [game.game_players for game in player.games] + for gameplayer in gameplayers if gameplayer.role == Role.owner] + + @staticmethod + def delete_orphans(): + User.query.filter(~User.user_games.any()).delete() + db.session.commit()