Compare commits
116 Commits
feature_te
...
master
@ -0,0 +1,5 @@ |
|||||||
|
from flask import Blueprint |
||||||
|
|
||||||
|
bp = Blueprint('game', __name__) |
||||||
|
|
||||||
|
from app.game_settings import routes |
@ -0,0 +1,45 @@ |
|||||||
|
from flask_wtf import FlaskForm |
||||||
|
from wtforms import StringField, SubmitField, DateTimeField, BooleanField, HiddenField |
||||||
|
from wtforms.validators import InputRequired, DataRequired, ValidationError, Length |
||||||
|
from pytz import timezone |
||||||
|
from app.models import Game |
||||||
|
|
||||||
|
class CreateGameForm(FlaskForm): |
||||||
|
game_name = StringField('Game Name', validators=[InputRequired(), Length(min=0, max=64)]) |
||||||
|
start_time_disabled = BooleanField('No start time') |
||||||
|
start_time = DateTimeField(id='datetimepicker_start', format="%d-%m-%Y %H:%M") |
||||||
|
end_time_disabled = BooleanField('No end time') |
||||||
|
end_time = DateTimeField(id='datetimepicker_end', format="%d-%m-%Y %H:%M") |
||||||
|
timezone = HiddenField(validators=[DataRequired()]) |
||||||
|
submit = SubmitField('Create') |
||||||
|
old_name = '' |
||||||
|
|
||||||
|
def validate_start_time(self, start_time): |
||||||
|
self.date_time_validator(self.start_time_disabled, start_time) |
||||||
|
|
||||||
|
def validate_end_time(self, end_time): |
||||||
|
self.date_time_validator(self.end_time_disabled, end_time) |
||||||
|
if self.start_time.data and end_time.data: |
||||||
|
if self.start_time.data > end_time.data: |
||||||
|
raise ValidationError('Start Time must be before End Time.') |
||||||
|
|
||||||
|
def date_time_validator(self, disabled, date_time): |
||||||
|
print(1) |
||||||
|
if disabled.data: |
||||||
|
date_time.data = None |
||||||
|
return |
||||||
|
clientzone = timezone(self.timezone.data) |
||||||
|
print(clientzone) |
||||||
|
print(date_time.data) |
||||||
|
date_time_utc = clientzone.localize(date_time.data).astimezone(timezone('UTC')) |
||||||
|
date_time.data = date_time_utc |
||||||
|
print(date_time.data) |
||||||
|
|
||||||
|
def validate_game_name(self, game_name): |
||||||
|
if game_name.data == '': |
||||||
|
return |
||||||
|
if game_name.data == self.old_name: |
||||||
|
return |
||||||
|
game = Game.query.filter_by(name=game_name.data).first() |
||||||
|
if game is not None: |
||||||
|
raise ValidationError('Please use a different name.') |
@ -0,0 +1,105 @@ |
|||||||
|
from flask import render_template, flash, redirect, url_for, request |
||||||
|
from flask_login import login_required, current_user |
||||||
|
from app import db |
||||||
|
from app.models import Game, GamePlayer, Role |
||||||
|
from app.utils import flash_errors, get_game_if_owner |
||||||
|
from app.game_settings import bp |
||||||
|
from app.game_settings.forms import CreateGameForm |
||||||
|
|
||||||
|
@bp.route('/create_game', methods=['GET', 'POST']) |
||||||
|
@login_required |
||||||
|
def create_game(): |
||||||
|
form = CreateGameForm() |
||||||
|
|
||||||
|
if form.validate_on_submit(): |
||||||
|
game = Game(name=form.game_name.data, |
||||||
|
start_time=form.start_time.data, |
||||||
|
end_time=form.end_time.data) |
||||||
|
game.players.append(GamePlayer(user=current_user, role=Role['owner'])) |
||||||
|
db.session.add(game) |
||||||
|
db.session.commit() |
||||||
|
flash(f"'{game.name}' had been created!") |
||||||
|
return redirect(url_for('main.game_dashboard', game_name=game.name)) |
||||||
|
else: |
||||||
|
flash_errors(form) |
||||||
|
return render_template('game_settings/create_game.html', title='Create Game', form=form) |
||||||
|
|
||||||
|
@bp.route('/game/<game_name>/change_settings', methods=['GET', 'POST']) |
||||||
|
@login_required |
||||||
|
def change_game_settings(game_name): |
||||||
|
game = get_game_if_owner(game_name) |
||||||
|
|
||||||
|
form = CreateGameForm() |
||||||
|
form.old_name = game.name |
||||||
|
|
||||||
|
if request.method == 'GET': |
||||||
|
# pylint: disable=no-member |
||||||
|
form.process() |
||||||
|
form.game_name.data = game.name |
||||||
|
if game.start_time: |
||||||
|
form.start_time.data = game.start_time |
||||||
|
else: |
||||||
|
form.start_time_disabled.data = True |
||||||
|
form.start_time.data = None |
||||||
|
if game.end_time: |
||||||
|
form.end_time.data = game.end_time |
||||||
|
else: |
||||||
|
form.end_time_disabled.data = True |
||||||
|
form.end_time.data = None |
||||||
|
|
||||||
|
if form.validate_on_submit(): |
||||||
|
game.name = form.game_name.data |
||||||
|
game.start_time = form.start_time.data |
||||||
|
game.end_time = form.end_time.data |
||||||
|
db.session.commit() |
||||||
|
flash(f"'{game.name}' had been updated!") |
||||||
|
return redirect(url_for('main.game_dashboard', game_name=game.name)) |
||||||
|
|
||||||
|
flash_errors(form) |
||||||
|
return render_template('game_settings/edit_game.html', title='Chage Game Settings', form=form, game=game) |
||||||
|
|
||||||
|
@bp.route('/game/<game_name>/delete') |
||||||
|
@login_required |
||||||
|
def delete_game(game_name): |
||||||
|
game = get_game_if_owner(game_name) |
||||||
|
game.delete() |
||||||
|
flash(f"Game '{game.name}' has been deleted!") |
||||||
|
return redirect(url_for('main.index')) |
||||||
|
|
||||||
|
@bp.route('/game/<game_name>/unhide') |
||||||
|
@bp.route('/game/<game_name>/publish') |
||||||
|
@login_required |
||||||
|
def publish_game(game_name): |
||||||
|
game = get_game_if_owner(game_name) |
||||||
|
game.hidden = False |
||||||
|
db.session.commit() |
||||||
|
flash(f"Game '{game.name}' has been published!") |
||||||
|
return redirect(url_for('main.game_dashboard', game_name=game.name)) |
||||||
|
|
||||||
|
@bp.route('/game/<game_name>/hide') |
||||||
|
@login_required |
||||||
|
def hide_game(game_name): |
||||||
|
game = get_game_if_owner(game_name) |
||||||
|
game.hidden = True |
||||||
|
db.session.commit() |
||||||
|
flash(f"Game '{game.name}' has been hidden!") |
||||||
|
return redirect(url_for('main.game_dashboard', game_name=game.name)) |
||||||
|
|
||||||
|
@bp.route('/game/<game_name>/pause') |
||||||
|
@login_required |
||||||
|
def pause_game(game_name): |
||||||
|
game = get_game_if_owner(game_name) |
||||||
|
game.paused = True |
||||||
|
db.session.commit() |
||||||
|
flash(f"Game '{game.name}' has been paused!") |
||||||
|
return redirect(url_for('main.game_dashboard', game_name=game.name)) |
||||||
|
|
||||||
|
@bp.route('/game/<game_name>/unpause') |
||||||
|
@bp.route('/game/<game_name>/resume') |
||||||
|
@login_required |
||||||
|
def resume_game(game_name): |
||||||
|
game = get_game_if_owner(game_name) |
||||||
|
game.paused = False |
||||||
|
db.session.commit() |
||||||
|
flash(f"Game '{game.name}' has been resumed!") |
||||||
|
return redirect(url_for('main.game_dashboard', game_name=game.name)) |
@ -1,54 +1,41 @@ |
|||||||
from flask_wtf import FlaskForm |
from flask_wtf import FlaskForm |
||||||
from wtforms import StringField, SubmitField, DateTimeField, BooleanField, HiddenField, FloatField, SelectField |
from flask_wtf.file import FileField, FileAllowed, FileRequired |
||||||
from wtforms.validators import DataRequired, ValidationError, Length, NumberRange |
from wtforms import StringField, SubmitField, FloatField, SelectField |
||||||
from pytz import timezone |
from wtforms.validators import InputRequired, DataRequired, ValidationError, Length, NumberRange |
||||||
from app.models import Objective |
from app.models import Objective |
||||||
|
from app import Config |
||||||
class CreateGameForm(FlaskForm): |
|
||||||
game_name = StringField('Game Name', validators=[DataRequired(), Length(min=0, max=64)]) |
|
||||||
start_time_disabled = BooleanField('No start time') |
|
||||||
start_time = DateTimeField(id='datetimepicker_start', format="%d-%m-%Y %H:%M") |
|
||||||
end_time_disabled = BooleanField('No end time') |
|
||||||
end_time = DateTimeField(id='datetimepicker_end', format="%d-%m-%Y %H:%M") |
|
||||||
timezone = HiddenField(validators=[DataRequired()]) |
|
||||||
submit = SubmitField('Create') |
|
||||||
|
|
||||||
def validate_start_time(self, start_time): |
|
||||||
self.date_time_validator(self.start_time_disabled, start_time) |
|
||||||
|
|
||||||
def validate_end_time(self, end_time): |
|
||||||
self.date_time_validator(self.end_time_disabled, end_time) |
|
||||||
|
|
||||||
def date_time_validator(self, disabled, date_time): |
|
||||||
if disabled.data: |
|
||||||
date_time.data = None |
|
||||||
return |
|
||||||
clientzone = timezone(self.timezone.data) |
|
||||||
date_time_utc = clientzone.localize(date_time.data).astimezone(timezone('UTC')) |
|
||||||
date_time.data = date_time_utc |
|
||||||
|
|
||||||
class ObjectiveForm(FlaskForm): |
class ObjectiveForm(FlaskForm): |
||||||
objective_name = StringField('Objective Name', validators=[Length(min=0, max=64)]) |
objective_name = StringField('Objective Name', validators=[Length(min=0, max=64)]) |
||||||
latitude = FloatField('Latitude', validators=[DataRequired(), NumberRange(min=-90, max=90)]) |
latitude = FloatField('Latitude', validators=[DataRequired(), NumberRange(min=-90, max=90)]) |
||||||
longitude = FloatField('Longitude', validators=[DataRequired(), NumberRange(min=-180, max=180)]) |
longitude = FloatField('Longitude', validators=[DataRequired(), NumberRange(min=-180, max=180)]) |
||||||
submit = SubmitField('Save') |
submit = SubmitField('Save') |
||||||
|
old_name = '' |
||||||
|
|
||||||
def validate_objective_name(self, objective_name): |
def validate_objective_name(self, objective_name): |
||||||
if objective_name.data == '': return |
if objective_name.data == '': |
||||||
|
return |
||||||
|
if objective_name.data == self.old_name: |
||||||
|
return |
||||||
objective = Objective.query.filter_by(name=objective_name.data).first() |
objective = Objective.query.filter_by(name=objective_name.data).first() |
||||||
if objective is not None: |
if objective is not None: |
||||||
raise ValidationError('Please use a different name.') |
raise ValidationError('Please use a different name.') |
||||||
|
|
||||||
class PlayerUpdateForm(FlaskForm): |
class PlayerUpdateForm(FlaskForm): |
||||||
role = SelectField('Player Role', choices=[('none', 'none'), ('owner', 'owner'), ('hunter', 'hunter'), ('bunny', 'bunny')], validators=[DataRequired()]) |
role = SelectField('Player Role', coerce=int, validators=[InputRequired()]) |
||||||
submit = SubmitField('Update') |
submit = SubmitField('Update') |
||||||
|
|
||||||
class PlayerAddForm(FlaskForm): |
class PlayerAddForm(FlaskForm): |
||||||
name = StringField('Player Name', validators=[DataRequired(), Length(min=0, max=64)]) |
name = StringField('Username', validators=[InputRequired(), Length(min=0, max=64)]) |
||||||
role = SelectField('Player Role', choices=[('none', 'none'), ('owner', 'owner'), ('hunter', 'hunter'), ('bunny', 'bunny')], validators=[DataRequired()]) |
role = SelectField('Player Role', coerce=int, validators=[InputRequired()]) |
||||||
submit_add = SubmitField('Create') |
submit_add = SubmitField('Create') |
||||||
|
|
||||||
class PlayerCreateForm(FlaskForm): |
class UserCreateForm(FlaskForm): |
||||||
name = StringField('Player Name', validators=[DataRequired(), Length(min=0, max=64)]) |
name = StringField('Username', validators=[InputRequired(), Length(min=0, max=64)]) |
||||||
role = SelectField('Player Role', choices=[('none', 'none'), ('owner', 'owner'), ('hunter', 'hunter'), ('bunny', 'bunny')], validators=[DataRequired()]) |
role = SelectField('Player Role', coerce=int, validators=[InputRequired()]) |
||||||
submit_create = SubmitField('Create') |
submit_create = SubmitField('Create') |
||||||
|
|
||||||
|
class CatchBunnyForm(FlaskForm): |
||||||
|
bunny = SelectField('Bunny Name', coerce=int, validators=[InputRequired()]) |
||||||
|
photo = FileField('Upload Photo', validators=[FileRequired(), FileAllowed(Config.ALLOWED_PHOTO_EXTENSIONS, 'Images only!')]) |
||||||
|
submit = SubmitField('Send') |
||||||
|
@ -1,187 +1,163 @@ |
|||||||
import json |
import json |
||||||
import qrcode |
from datetime import datetime, timedelta |
||||||
from flask import render_template, flash, redirect, url_for, request, abort, send_file |
from flask import render_template, redirect, url_for, request, abort, send_file, current_app, flash |
||||||
from flask_login import current_user, login_required |
from flask_login import current_user, login_required |
||||||
from sqlalchemy import and_ |
from werkzeug.security import safe_join |
||||||
from io import BytesIO |
|
||||||
from app import db |
from app import db |
||||||
from app.main import bp |
from app.main import bp |
||||||
from app.models import Player, Game, Role, GamePlayer, Objective, ObjectiveMinimalEncoder, LocationEncoder |
from app.utils import get_game_if_owner, get_caught_bunny_photo_directory, get_bunny_photo_filename |
||||||
from app.main.forms import CreateGameForm, ObjectiveForm, PlayerAddForm, PlayerCreateForm, PlayerUpdateForm |
from app.models import User, Game, Role, GamePlayer, ObjectiveEncoder, LocationEncoder, \ |
||||||
|
PlayerCaughtPlayer, Review, Location |
||||||
|
|
||||||
|
@bp.before_app_request |
||||||
|
def before_request(): |
||||||
|
if current_user.is_authenticated: |
||||||
|
current_user.last_login = datetime.utcnow() |
||||||
|
db.session.commit() |
||||||
|
|
||||||
@bp.route('/') |
@bp.route('/') |
||||||
@bp.route('/index') |
@bp.route('/index') |
||||||
@login_required |
@login_required |
||||||
def index(): |
def index(): |
||||||
|
if len(current_user.games) == 1: |
||||||
|
return redirect(url_for('main.game_dashboard', game_name=current_user.games[0].name)) |
||||||
return render_template("index.html", title='Home') |
return render_template("index.html", title='Home') |
||||||
|
|
||||||
@bp.route('/create_game', methods=['GET', 'POST']) |
|
||||||
@login_required |
|
||||||
def create_game(): |
|
||||||
print(current_user.is_authenticated) |
|
||||||
form = CreateGameForm() |
|
||||||
if form.validate_on_submit(): |
|
||||||
game = Game(name=form.game_name.data, start_time=form.start_time.data, end_time=form.end_time.data) |
|
||||||
game.game_players.append(GamePlayer(player=current_user, role=Role['owner'])) |
|
||||||
db.session.add(game) |
|
||||||
db.session.commit() |
|
||||||
flash(f"'{game.name}' had been created!") |
|
||||||
return redirect(url_for('main.game_dashboard', game_name=game.name)) |
|
||||||
return render_template('create_game.html', title='Create Game', form=form) |
|
||||||
|
|
||||||
@bp.route('/game/<game_name>/dashboard') |
@bp.route('/game/<game_name>/dashboard') |
||||||
@login_required |
@login_required |
||||||
def game_dashboard(game_name): |
def game_dashboard(game_name): |
||||||
#game = Game.query.filter(Game.game_players.any(and_(GamePlayer.player.has(Player.name == current_user.name), GamePlayer.role == 'owner'))).first_or_404() |
|
||||||
game = Game.query.filter_by(name=game_name).first_or_404() |
|
||||||
if not is_game_owner(game): |
|
||||||
abort(403) |
|
||||||
return render_template('game_dashboard.html', title='Game Dashboard', game=game, json=json, objective_encoder=ObjectiveMinimalEncoder, location_encoder=LocationEncoder) |
|
||||||
|
|
||||||
@bp.route('/game/<game_name>/addplayer', methods=['GET', 'POST']) |
|
||||||
@login_required |
|
||||||
def add_player(game_name): |
|
||||||
game = Game.query.filter_by(name=game_name).first_or_404() |
game = Game.query.filter_by(name=game_name).first_or_404() |
||||||
if not is_game_owner(game): |
role = current_user.role_in_game(game) |
||||||
|
if role == Role.owner: |
||||||
|
return render_template('game_owner_dashboard.html', title='Game Dashboard', game=game, |
||||||
|
json=json, objective_encoder=ObjectiveEncoder, |
||||||
|
location_encoder=LocationEncoder) |
||||||
|
if role == Role.bunny: |
||||||
|
return render_template('game_bunny_dashboard.html', title='Game Dashboard', game=game, |
||||||
|
json=json, location_encoder=LocationEncoder) |
||||||
|
if role == Role.hunter: |
||||||
|
hunter_delay = current_app.config['HUNTER_LOCATION_DELAY'] |
||||||
|
return render_template('game_hunter_dashboard.html', title='Game Dashboard', game=game, |
||||||
|
hunter_delay=hunter_delay, json=json, |
||||||
|
location_encoder=LocationEncoder) |
||||||
|
if role == Role.none: |
||||||
|
flash('Please ask your game owner for a role to join this game') |
||||||
|
return abort(403) |
||||||
|
if role is None: |
||||||
abort(403) |
abort(403) |
||||||
form_add = PlayerAddForm() |
|
||||||
form_create = PlayerCreateForm() |
|
||||||
|
|
||||||
if form_add.submit_add.data and form_add.validate_on_submit(): |
@bp.route('/game/<game_name>/caught_bunny_photo', methods=['GET']) |
||||||
player = Player.query.filter_by(form_add.name.data).first_or_404() |
|
||||||
game.game_players.append(GamePlayer(player=player, role=Role[form_create.role.data])) |
|
||||||
return redirect(url_for('main.game_dashboard', game_name=game.name)) |
|
||||||
|
|
||||||
if form_create.submit_create.data and form_create.validate_on_submit(): |
|
||||||
player = Player(name=form_create.name.data) |
|
||||||
player.set_auth_hash() |
|
||||||
game.game_players.append(GamePlayer(player=player, role=Role[form_create.role.data])) |
|
||||||
db.session.commit() |
|
||||||
return redirect(url_for('main.game_dashboard', game_name=game.name)) |
|
||||||
|
|
||||||
return render_template('add_player.html', title=f'Add Player for {game_name}', form_add=form_add, form_create=form_create, game=game) |
|
||||||
|
|
||||||
@bp.route('/game/<game_name>/removeplayer/<player_name>') |
|
||||||
@login_required |
@login_required |
||||||
def remove_player(game_name, player_name): |
def caught_bunny_photo(game_name): |
||||||
game = Game.query.filter_by(name=game_name).first_or_404() |
game = Game.query.filter_by(name=game_name).first_or_404() |
||||||
if not is_game_owner(game): |
timestamp = request.args['timestamp'] |
||||||
|
bunny_name = request.args['bunny_name'] |
||||||
|
hunter_name = request.args['hunter_name'] |
||||||
|
hunter = GamePlayer.query.join(User).filter( |
||||||
|
(GamePlayer.user_id == User.id) & |
||||||
|
(GamePlayer.game_id == game.id) & |
||||||
|
(User.name == hunter_name) & |
||||||
|
(GamePlayer.role == Role.hunter)).first_or_404() |
||||||
|
if not (game.owned_by(current_user) or current_user.player_in(game) == hunter): |
||||||
abort(403) |
abort(403) |
||||||
player = Player.query.filter(and_(Player.name == player_name, Player.games.contains(game))).first_or_404() |
directory = get_caught_bunny_photo_directory(game) |
||||||
game.players.remove(player) |
filename = get_bunny_photo_filename(directory, timestamp, hunter_name, bunny_name) |
||||||
db.session.commit() |
photo_path = safe_join(directory, filename) |
||||||
return redirect(url_for('main.game_dashboard', game_name=game.name)) |
#TODO: Implement switch between serve self and serve by webserver |
||||||
|
return send_file(photo_path, conditional=True, as_attachment=False) |
||||||
|
|
||||||
@bp.route('/game/<game_name>/player/<player_name>', methods=['GET', 'POST']) |
@bp.route('/game/<game_name>/review') |
||||||
@login_required |
@login_required |
||||||
def game_player(game_name, player_name): |
def review_caught_bunny_photos(game_name): |
||||||
game = Game.query.filter_by(name=game_name).first_or_404() |
game = get_game_if_owner(game_name) |
||||||
if not is_game_owner(game): |
pcp_id = request.args.get('pcp_id', default=-1, type=int) |
||||||
abort(403) |
action = request.args.get('action', default='none', type=str).lower() |
||||||
player = Player.query.filter(and_(Player.name == player_name, Player.games.contains(game))).first_or_404() |
if pcp_id != -1: |
||||||
gameplayer = [gameplayer for gameplayer in player.player_games if gameplayer.game == game][0] |
pcp = PlayerCaughtPlayer.query.filter_by(id=pcp_id).first_or_404() |
||||||
form = PlayerUpdateForm(role=gameplayer.role.name) |
review = Review.parse_string(action) |
||||||
if form.validate_on_submit(): |
pcp.review = review |
||||||
gameplayer.role = Role[form.role.data] |
|
||||||
db.session.commit() |
db.session.commit() |
||||||
return redirect(url_for('main.game_dashboard', game_name=game.name)) |
return render_template('review_caught_bunny_photos.html', game=game) |
||||||
return render_template('player.html', title=f'{player.name} in {game_name}', game=game, player=player, form=form, json=json, location_encoder=LocationEncoder) |
|
||||||
|
|
||||||
@bp.route('/player/<auth_hash>/qrcode.png') |
|
||||||
@login_required |
|
||||||
def player_qrcode(auth_hash): |
|
||||||
player = Player.query.filter_by(auth_hash=auth_hash).first_or_404() |
|
||||||
if not is_player_game_owner(player): |
|
||||||
abort(403) |
|
||||||
img = generate_qr_code(url_for('main.player', auth_hash=auth_hash, _external=True)) |
|
||||||
return serve_pil_image(img) |
|
||||||
|
|
||||||
@bp.route('/player/<auth_hash>') |
@bp.route('/user/<username>/send_location', methods=['POST']) |
||||||
@login_required |
@login_required |
||||||
def player(auth_hash): |
def send_location(username): |
||||||
player = Player.query.filter_by(auth_hash=auth_hash).first_or_404() |
user = User.query.filter_by(name=username).first_or_404() |
||||||
return render_template('player.html',title=f'Player: {player.name}', player=player) |
last_location = user.last_location() |
||||||
|
|
||||||
'''given player is an owner of the given game''' |
latitude = request.form.get('lat', default=None, type=float) |
||||||
def is_game_owner(game, owner=current_user): |
longitude = request.form.get('long', default=None, type=float) |
||||||
return owner in [gameplayer.player for gameplayer in game.game_players if gameplayer.role == Role.owner] |
if latitude is None or longitude is None: |
||||||
|
return '', 400 |
||||||
'''given player is an owner of a game the subject_player participates in''' |
|
||||||
def is_player_game_owner(subject_player, owner=current_user): |
# Check if previous two locations are exactly the same, |
||||||
return owner in [gameplayer.player for gameplayers in |
# if so, only update timestamp of last location |
||||||
[game.game_players for game in subject_player.games] |
if last_location: |
||||||
for gameplayer in gameplayers if gameplayer.role == Role.owner] |
if datetime.utcnow() - last_location.timestamp < timedelta(seconds=30): |
||||||
|
return '', 204 |
||||||
'''given player is an owner of a game the given object is part of''' |
if (latitude == last_location.latitude and |
||||||
def is_objective_owner(objective, owner=current_user): |
longitude == last_location.longitude and |
||||||
return owner in [gameplayer.player for gameplayer in objective.game.game_players if gameplayer.role == Role.owner] |
len(user.locations) >= 2): |
||||||
|
before_last_location = user.locations[-2] |
||||||
|
if before_last_location: |
||||||
|
if (latitude == before_last_location.latitude and |
||||||
|
longitude == before_last_location.longitude): |
||||||
|
last_location.timestamp = datetime.utcnow() |
||||||
|
db.session.commit() |
||||||
|
return '', 204 |
||||||
|
|
||||||
|
user.locations.append(Location(longitude=longitude, latitude=latitude)) |
||||||
|
db.session.commit() |
||||||
|
return '', 204 |
||||||
|
|
||||||
@bp.route('/game/<game_name>/add_objective', methods=['GET', 'POST']) |
@bp.route('/game/<game_name>/get_locations', methods=['POST']) |
||||||
@login_required |
@login_required |
||||||
def add_objective(game_name): |
def poll_locations(game_name): |
||||||
game = Game.query.filter_by(name=game_name).first_or_404() |
game = Game.query.filter_by(name=game_name).first_or_404() |
||||||
if not is_game_owner(game): |
role = current_user.role_in_game(game) |
||||||
abort(403) |
if role is None or role == Role.none: |
||||||
form = ObjectiveForm() |
|
||||||
objective = Objective(name='', latitude=52.0932, longitude=5.12405) |
|
||||||
if form.validate_on_submit(): |
|
||||||
objective = Objective(name=form.objective_name.data, longitude=form.longitude.data, latitude=form.latitude.data) |
|
||||||
objective.set_hash() |
|
||||||
game.objectives.append(objective) |
|
||||||
db.session.commit() |
|
||||||
flash(f"Objective has been added!") |
|
||||||
return redirect(url_for('main.game_dashboard', game_name=game.name)) |
|
||||||
return render_template('objective.html', title=f'Add Objective for {game_name}', form=form, objective=objective, owner=True) |
|
||||||
|
|
||||||
@bp.route('/objective/<objective_hash>/delete', methods=['GET']) |
|
||||||
@login_required |
|
||||||
def delete_objective(objective_hash): |
|
||||||
objective = Objective.query.filter_by(hash=objective_hash).first_or_404() |
|
||||||
if not is_objective_owner(objective): |
|
||||||
abort(403) |
abort(403) |
||||||
if is_objective_owner(objective): |
|
||||||
db.session.delete(objective) |
|
||||||
db.session.commit() |
|
||||||
return redirect(url_for('main.game_dashboard', game_name=objective.game.name)) |
|
||||||
|
|
||||||
def generate_qr_code(url): |
|
||||||
qr = qrcode.QRCode( |
|
||||||
version=None, |
|
||||||
error_correction=qrcode.constants.ERROR_CORRECT_M, |
|
||||||
box_size=30, |
|
||||||
border=4, |
|
||||||
) |
|
||||||
qr.add_data(url) |
|
||||||
qr.make(fit=True) |
|
||||||
return qr.make_image(fill_color='black', back_color='white') |
|
||||||
|
|
||||||
# Source: https://stackoverflow.com/questions/7877282/how-to-send-image-generated-by-pil-to-browser |
payload = request.get_json() |
||||||
def serve_pil_image(pil_img): |
if payload is None: |
||||||
img_io = BytesIO() |
abort(400) |
||||||
pil_img.save(img_io, 'PNG', quality=70) |
mode = get_value_if_key_exists(payload, 'mode', 'last') |
||||||
img_io.seek(0) |
last_update = get_value_if_key_exists(payload, 'last_update', 'none') |
||||||
return send_file(img_io, mimetype='image/png') |
last_update = datetime.strptime(last_update, '%Y-%m-%d %H:%M:%S:%f') if last_update != 'none' else datetime.min |
||||||
|
requested_users = get_value_if_key_exists(payload, 'requested_users', 'none') |
||||||
@bp.route('/objective/<objective_hash>/qrcode.png') |
#print(f'mode: {mode}\nlast_request: {last_update}\nrequested_users: {requested_users}') |
||||||
|
response_objects = [] |
||||||
|
if role in (Role.owner, Role.hunter): |
||||||
|
for username in requested_users: |
||||||
|
locations = get_user_locations(game, username, mode, last_update, role == Role.hunter) |
||||||
|
#print(locations) |
||||||
|
if locations: |
||||||
|
response_objects.append(locations) |
||||||
|
response_objects = [obj for obj_list in response_objects for obj in obj_list] |
||||||
|
return json.dumps(response_objects, cls=LocationEncoder) |
||||||
|
|
||||||
|
def get_value_if_key_exists(dictionary, key, default=None): |
||||||
|
return dictionary[key] if key in dictionary else default |
||||||
|
|
||||||
|
def get_user_locations(game, username, mode, last_update, hunter=False): |
||||||
|
user = User.query.filter_by(name=username).first() |
||||||
|
if user is None: |
||||||
|
return None |
||||||
|
if hunter and user.role_in_game(game) != Role.bunny: |
||||||
|
return None |
||||||
|
if mode == 'accumulative': |
||||||
|
if game.end_time or datetime.max < last_update: # Don't return locations when the game is finished |
||||||
|
return [] |
||||||
|
offset = current_app.config['HUNTER_LOCATION_DELAY'] if hunter else 0 |
||||||
|
locations = user.locations_during_game(game, offset) |
||||||
|
if not locations: |
||||||
|
return None |
||||||
|
return [location for location in locations if location.timestamp - last_update > timedelta(milliseconds=1)] |
||||||
|
|
||||||
|
@bp.route('/user/<username>') |
||||||
@login_required |
@login_required |
||||||
def objective_qrcode(objective_hash): |
def user_profile(username): |
||||||
objective = Objective.query.filter_by(hash=objective_hash).first_or_404() |
user = User.query.filter_by(name=username).first_or_404() |
||||||
if not is_objective_owner(objective): |
if current_user != user: |
||||||
abort(403) |
abort(403) |
||||||
img = generate_qr_code(url_for('main.objective', objective_hash=objective.hash, _external=True)) |
return render_template('user_profile.html', user=user) |
||||||
return serve_pil_image(img) |
|
||||||
|
|
||||||
@bp.route('/objective/<objective_hash>', methods=['GET', 'POST']) |
|
||||||
@login_required |
|
||||||
def objective(objective_hash): |
|
||||||
objective = Objective.query.filter_by(hash=objective_hash).first_or_404() |
|
||||||
owner = is_objective_owner(objective) |
|
||||||
qrcode = generate_qr_code(objective) if owner else None |
|
||||||
form = ObjectiveForm() |
|
||||||
if form.submit.data and form.validate() and owner: |
|
||||||
objective.name = form.objective_name.data |
|
||||||
objective.longitude = form.longitude.data |
|
||||||
objective.latitude = form.latitude.data |
|
||||||
db.session.commit() |
|
||||||
return redirect(url_for('main.game_dashboard', game_name=objective.game.name)) |
|
||||||
return render_template('objective.html', title='Objective view', objective=objective, owner=owner, form=form, qrcode=qrcode) |
|
||||||
|
@ -0,0 +1,88 @@ |
|||||||
|
import json |
||||||
|
|
||||||
|
from flask import render_template, flash, redirect, url_for, abort |
||||||
|
from flask_login import current_user, login_required |
||||||
|
|
||||||
|
from app import db |
||||||
|
from app.main import bp |
||||||
|
from app.utils import generate_qr_code, serve_pil_image, get_game_if_owner |
||||||
|
from app.models import Objective, Role, ObjectiveEncoder |
||||||
|
from app.main.forms import ObjectiveForm |
||||||
|
|
||||||
|
@bp.route('/game/<game_name>/add_objective', methods=['GET', 'POST']) |
||||||
|
@login_required |
||||||
|
def add_objective(game_name): |
||||||
|
game = get_game_if_owner(game_name) |
||||||
|
form = ObjectiveForm() |
||||||
|
_objective = Objective(name='', latitude=52.0932, longitude=5.12405) |
||||||
|
if form.validate_on_submit(): |
||||||
|
_objective = Objective(name=form.objective_name.data, longitude=form.longitude.data, |
||||||
|
latitude=form.latitude.data) |
||||||
|
_objective.set_hash() |
||||||
|
game.objectives.append(_objective) |
||||||
|
db.session.commit() |
||||||
|
flash("Objective has been added!") |
||||||
|
return redirect(url_for('main.game_dashboard', game_name=game.name)) |
||||||
|
return render_template('objective.html', title=f'Add Objective for {game_name}', |
||||||
|
form=form, game=game, objective=_objective, owner=True, json=json, |
||||||
|
objective_encoder=ObjectiveEncoder) |
||||||
|
|
||||||
|
@bp.route('/objective/<objective_hash>/delete', methods=['GET']) |
||||||
|
@login_required |
||||||
|
def delete_objective(objective_hash): |
||||||
|
_objective = Objective.query.filter_by(hash=objective_hash).first_or_404() |
||||||
|
if not _objective.owned_by(current_user): |
||||||
|
abort(403) |
||||||
|
else: |
||||||
|
db.session.delete(_objective) |
||||||
|
db.session.commit() |
||||||
|
return redirect(url_for('main.game_dashboard', game_name=_objective.game.name)) |
||||||
|
|
||||||
|
@bp.route('/objective/<objective_hash>/qrcode.png') |
||||||
|
@login_required |
||||||
|
def objective_qrcode(objective_hash): |
||||||
|
_objective = Objective.query.filter_by(hash=objective_hash).first_or_404() |
||||||
|
if not _objective.owned_by(current_user): |
||||||
|
abort(403) |
||||||
|
img = generate_qr_code(url_for('main.objective', objective_hash=_objective.hash, |
||||||
|
_external=True)) |
||||||
|
return serve_pil_image(img) |
||||||
|
|
||||||
|
@bp.route('/objective/<objective_hash>', methods=['GET', 'POST']) |
||||||
|
@login_required |
||||||
|
def objective(objective_hash): |
||||||
|
_objective = Objective.query.filter_by(hash=objective_hash).first_or_404() |
||||||
|
if current_user.role_in_game(_objective.game) == Role.bunny: |
||||||
|
if not _objective.game.is_active(): |
||||||
|
flash("It's not possible to find an objective before or after a game, " |
||||||
|
"or if the game is not in 'active' mode.") |
||||||
|
return redirect(url_for('main.game_dashboard', game_name=_objective.game.name)) |
||||||
|
player = current_user.player_in(_objective.game) |
||||||
|
if not _objective in player.found_objectives: |
||||||
|
player.found_objectives.append(_objective) |
||||||
|
db.session.commit() |
||||||
|
if _objective.name: |
||||||
|
flash(f'You found objective: {_objective.name}!') |
||||||
|
else: |
||||||
|
flash('You found an objective!') |
||||||
|
elif _objective.name: |
||||||
|
flash(f"You have already found objective '{_objective.name}'") |
||||||
|
else: |
||||||
|
flash('You have already found this objective') |
||||||
|
return redirect(url_for('main.game_dashboard', game_name=_objective.game.name)) |
||||||
|
if not _objective.owned_by(current_user): |
||||||
|
flash("Only bunnies in an objective's game can find objectives!") |
||||||
|
abort(403) |
||||||
|
|
||||||
|
qrcode = generate_qr_code(_objective) |
||||||
|
form = ObjectiveForm() |
||||||
|
form.old_name = _objective.name |
||||||
|
if form.submit.data and form.validate(): |
||||||
|
_objective.name = form.objective_name.data |
||||||
|
_objective.longitude = form.longitude.data |
||||||
|
_objective.latitude = form.latitude.data |
||||||
|
db.session.commit() |
||||||
|
return redirect(url_for('main.game_dashboard', game_name=_objective.game.name)) |
||||||
|
return render_template('objective.html', title='Objective view', |
||||||
|
form=form, game=_objective.game, objective=_objective, owner=True, |
||||||
|
qrcode=qrcode, json=json, objective_encoder=ObjectiveEncoder) |
@ -0,0 +1,118 @@ |
|||||||
|
import json |
||||||
|
from datetime import datetime |
||||||
|
from flask import render_template, flash, redirect, url_for, request, abort |
||||||
|
from flask_login import current_user, login_required |
||||||
|
from sqlalchemy import and_ |
||||||
|
from app import db |
||||||
|
from app.main import bp |
||||||
|
from app.utils import get_game_if_owner, save_player_caught_player_photo |
||||||
|
from app.models import User, Game, Role, GamePlayer, PlayerCaughtPlayer, LocationEncoder |
||||||
|
from app.main.forms import PlayerAddForm, UserCreateForm, CatchBunnyForm, PlayerUpdateForm |
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/game/<game_name>/adduser', methods=['GET', 'POST']) |
||||||
|
@bp.route('/game/<game_name>/addplayer', methods=['GET', 'POST']) |
||||||
|
@login_required |
||||||
|
def add_player(game_name): |
||||||
|
game = get_game_if_owner(game_name) |
||||||
|
|
||||||
|
form_add = PlayerAddForm() |
||||||
|
form_add.role.choices = [(role.value, role.name) for role in Role] |
||||||
|
form_create = UserCreateForm() |
||||||
|
form_create.role.choices = [(role.value, role.name) for role in Role] |
||||||
|
|
||||||
|
if form_add.submit_add.data and form_add.validate_on_submit(): |
||||||
|
user = User.query.filter_by(name=form_add.name.data).first_or_404() |
||||||
|
if user.player_in(game): |
||||||
|
flash(f'{user.name} is already a player in this game!') |
||||||
|
return redirect(url_for('main.game_dashboard', game_name=game.name)) |
||||||
|
game.players.append(GamePlayer(user=user, role=Role(form_add.role.data))) |
||||||
|
db.session.commit() |
||||||
|
return redirect(url_for('main.game_dashboard', game_name=game.name)) |
||||||
|
|
||||||
|
if form_create.submit_create.data and form_create.validate_on_submit(): |
||||||
|
user = User(name=form_create.name.data) |
||||||
|
user.set_auth_hash() |
||||||
|
game.players.append(GamePlayer(user=user, role=Role(form_create.role.data))) |
||||||
|
db.session.commit() |
||||||
|
return redirect(url_for('main.game_dashboard', game_name=game.name)) |
||||||
|
|
||||||
|
return render_template('add_player.html', title=f'Add User for {game_name}', |
||||||
|
form_add=form_add, form_create=form_create, game=game) |
||||||
|
|
||||||
|
@bp.route('/game/<game_name>/removeuser/<username>') |
||||||
|
@bp.route('/game/<game_name>/removeplayer/<username>') |
||||||
|
@login_required |
||||||
|
def remove_player(game_name, username): |
||||||
|
game = get_game_if_owner(game_name) |
||||||
|
user = User.query.filter(and_(User.name == username, User.games.contains(game))).first_or_404() |
||||||
|
warning = game.remove_player(user) |
||||||
|
if warning: |
||||||
|
flash(warning) |
||||||
|
return redirect(url_for('main.game_dashboard', game_name=game.name)) |
||||||
|
|
||||||
|
@bp.route('/game/<game_name>/user/<username>', methods=['GET', 'POST']) |
||||||
|
@bp.route('/game/<game_name>/player/<username>', methods=['GET', 'POST']) |
||||||
|
@login_required |
||||||
|
def game_player(game_name, username): |
||||||
|
game = get_game_if_owner(game_name) |
||||||
|
user = User.query.filter((User.name == username) & (User.games.contains(game))).first_or_404() |
||||||
|
player = user.player_in(game) |
||||||
|
|
||||||
|
# pylint: disable=no-member |
||||||
|
form = PlayerUpdateForm(role=player.role.name) |
||||||
|
form.role.choices = [(role.value, role.name) for role in Role] |
||||||
|
if request.method == 'GET': |
||||||
|
form.role.default = player.role.value |
||||||
|
form.process() |
||||||
|
|
||||||
|
if form.validate_on_submit(): |
||||||
|
player.role = Role(form.role.data) |
||||||
|
print(form.role.data) |
||||||
|
db.session.commit() |
||||||
|
return redirect(url_for('main.game_dashboard', game_name=game.name)) |
||||||
|
return render_template('game_player.html', title=f'{user.name} in {game_name}', |
||||||
|
player=player, form=form, json=json, location_encoder=LocationEncoder) |
||||||
|
|
||||||
|
@bp.route('/game/<game_name>/catch_bunny', methods=['GET', 'POST']) |
||||||
|
@login_required |
||||||
|
def catch_bunny(game_name): |
||||||
|
game = Game.query.filter_by(name=game_name).first_or_404() |
||||||
|
if current_user.role_in_game(game) is not Role.hunter: |
||||||
|
flash('Only hunters can catch bunnies!') |
||||||
|
abort(403) |
||||||
|
if not game.is_active(): |
||||||
|
flash("Its not possible to catch a bunny before or after a game, " |
||||||
|
"or if the game is not in 'active' mode.") |
||||||
|
return redirect(url_for('main.game_dashboard', game_name=game.name)) |
||||||
|
|
||||||
|
game_bunnies = game.bunnies() |
||||||
|
form = CatchBunnyForm() |
||||||
|
form.bunny.choices = [(player.user.id, player.user.name) for player in game_bunnies] |
||||||
|
|
||||||
|
if request.method == 'GET': |
||||||
|
parsed_bunny_name = request.args.get('bunny_name', default='', type=str) |
||||||
|
if parsed_bunny_name: |
||||||
|
bunny = [gameplayer for gameplayer in game_bunnies |
||||||
|
if gameplayer.user.name == parsed_bunny_name] |
||||||
|
if bunny: |
||||||
|
# pylint: disable=no-member |
||||||
|
form.bunny.default = bunny[0].user.id |
||||||
|
form.process() |
||||||
|
|
||||||
|
if form.validate_on_submit(): |
||||||
|
bunny = [gameplayer for gameplayer in game_bunnies if gameplayer.user.id == form.bunny.data] |
||||||
|
if not bunny: |
||||||
|
flash('Please choose a bunny') |
||||||
|
return request.url |
||||||
|
bunny = bunny[0] |
||||||
|
|
||||||
|
pcp = PlayerCaughtPlayer(catching_player=current_user.player_in(game), |
||||||
|
caught_player=bunny, timestamp=datetime.utcnow()) |
||||||
|
save_player_caught_player_photo(form.photo.data, game, pcp) |
||||||
|
db.session.add(pcp) |
||||||
|
db.session.commit() |
||||||
|
flash(f"You caught {bunny.user.name}! " |
||||||
|
"The submitted photo will be reviewed by a game owner.") |
||||||
|
return redirect(url_for('main.game_dashboard', game_name=game.name)) |
||||||
|
return render_template('catch_bunny.html', title='Catch Bunny', form=form, game=game) |
@ -1,237 +0,0 @@ |
|||||||
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()) |
|
@ -0,0 +1,12 @@ |
|||||||
|
from .game import Game |
||||||
|
from .game_player import GamePlayer |
||||||
|
from .game_state import GameState |
||||||
|
from .location import Location, LocationEncoder |
||||||
|
from .notification import Notification |
||||||
|
from .notification_player import NotificationPlayer |
||||||
|
from .objective import Objective, ObjectiveEncoder |
||||||
|
from .player_caught_player import PlayerCaughtPlayer |
||||||
|
from .player_found_objective import PlayerFoundObjective |
||||||
|
from .review import Review |
||||||
|
from .role import Role |
||||||
|
from .user import User |
@ -0,0 +1,115 @@ |
|||||||
|
from datetime import datetime |
||||||
|
from sqlalchemy.ext.associationproxy import association_proxy |
||||||
|
from app import db |
||||||
|
from .game_state import GameState |
||||||
|
from .game_player import GamePlayer |
||||||
|
from .role import Role |
||||||
|
from .review import Review |
||||||
|
|
||||||
|
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) |
||||||
|
hidden = db.Column(db.Boolean, server_default='1', nullable=False) |
||||||
|
paused = db.Column(db.Boolean, server_default='0', nullable=False) |
||||||
|
start_time = db.Column(db.DateTime) |
||||||
|
end_time = db.Column(db.DateTime) |
||||||
|
players = db.relationship( |
||||||
|
'GamePlayer', |
||||||
|
back_populates='game', |
||||||
|
cascade="save-update, merge, delete, delete-orphan") |
||||||
|
users = association_proxy('players', 'user', |
||||||
|
creator=lambda user: GamePlayer(user=user)) |
||||||
|
objectives = db.relationship( |
||||||
|
'Objective', |
||||||
|
lazy='select', |
||||||
|
backref=db.backref('game', lazy='joined'), |
||||||
|
cascade="save-update, merge, delete, delete-orphan") |
||||||
|
notifications = db.relationship( |
||||||
|
'Notification', |
||||||
|
lazy='select', |
||||||
|
backref=db.backref('game', lazy='joined'), |
||||||
|
cascade="save-update, merge, delete, delete-orphan") |
||||||
|
|
||||||
|
def usernames(self): |
||||||
|
return [user.name for user in self.users] |
||||||
|
|
||||||
|
def last_player_locations(self, offset=None): |
||||||
|
# pylint: disable=not-an-iterable |
||||||
|
return [location for location in |
||||||
|
[player.last_location(offset=offset) for player in self.players if player.user.locations] |
||||||
|
if location is not None] |
||||||
|
|
||||||
|
def last_locations(self, players, offset=None): |
||||||
|
''' |
||||||
|
Returns last locations for given players within time boundaries of game |
||||||
|
|
||||||
|
Parameters: |
||||||
|
players (Player or User list): players for whom the last location is returned |
||||||
|
offset (int): Offset in minutes. Only locations older than this amount of minutes will be returned. |
||||||
|
''' |
||||||
|
locations = [] |
||||||
|
for player in players: |
||||||
|
if isinstance(player, GamePlayer): |
||||||
|
player = player.user |
||||||
|
location = player.last_location(self, offset=offset) |
||||||
|
if location: |
||||||
|
locations.append(location) |
||||||
|
return locations |
||||||
|
|
||||||
|
def bunnies(self): |
||||||
|
# pylint: disable=not-an-iterable |
||||||
|
return [gameplayer for gameplayer in self.players if gameplayer.role == Role.bunny] |
||||||
|
|
||||||
|
def owned_by(self, user): |
||||||
|
'''given user is an owner of this game''' |
||||||
|
# pylint: disable=not-an-iterable |
||||||
|
return user in [gameplayer.user for gameplayer in self.players if gameplayer.role == Role.owner] |
||||||
|
|
||||||
|
def unreviewed_bunny_photos(self): |
||||||
|
# pylint: disable=not-an-iterable |
||||||
|
return [pcp for pcps in |
||||||
|
[player.player_caught_players for player in self.players] |
||||||
|
for pcp in pcps if pcp.review == Review.none] |
||||||
|
|
||||||
|
def is_active(self): |
||||||
|
return self.get_state() == GameState.active |
||||||
|
|
||||||
|
def get_state(self): |
||||||
|
now = datetime.utcnow() |
||||||
|
start = (self.start_time or datetime.min).replace(tzinfo=None) |
||||||
|
|
||||||
|
if now < start: # Before Game |
||||||
|
if self.hidden: |
||||||
|
return GameState.hidden |
||||||
|
return GameState.published |
||||||
|
|
||||||
|
end = (self.end_time or datetime.max).replace(tzinfo=None) |
||||||
|
if start < now < end: # During Game |
||||||
|
if self.paused: |
||||||
|
return GameState.paused |
||||||
|
return GameState.active |
||||||
|
|
||||||
|
if now > end: # After Game |
||||||
|
if self.hidden: |
||||||
|
return GameState.hidden |
||||||
|
return GameState.finished |
||||||
|
|
||||||
|
def delete(self): |
||||||
|
db.session.delete(self) |
||||||
|
for user in self.users: |
||||||
|
if not user.last_login: |
||||||
|
db.session.delete(user) |
||||||
|
db.session.commit() |
||||||
|
|
||||||
|
def remove_player(self, user): |
||||||
|
# pylint: disable=not-an-iterable |
||||||
|
if user.role_in_game(self) == Role.owner: |
||||||
|
if len([player for player in self.players if player.role == Role.owner]) < 2: |
||||||
|
return "Can't remove only owner from game" |
||||||
|
self.users.remove(user) |
||||||
|
if not user.last_login: |
||||||
|
db.session.delete(user) |
||||||
|
db.session.commit() |
||||||
|
return False |
@ -0,0 +1,75 @@ |
|||||||
|
import json |
||||||
|
|
||||||
|
from sqlalchemy.ext.associationproxy import association_proxy |
||||||
|
from sqlalchemy.schema import UniqueConstraint |
||||||
|
|
||||||
|
from app import db |
||||||
|
from .role import Role |
||||||
|
from .notification_player import NotificationPlayer |
||||||
|
from .player_found_objective import PlayerFoundObjective |
||||||
|
from .review import Review |
||||||
|
|
||||||
|
class GamePlayer(db.Model): |
||||||
|
__tablename__ = 'game_player' |
||||||
|
id = db.Column(db.Integer, primary_key=True) |
||||||
|
game_id = db.Column(db.Integer, db.ForeignKey('game.id'), nullable=False) |
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) |
||||||
|
role = db.Column(db.Enum(Role), server_default=Role(0).name, nullable=False) |
||||||
|
game = db.relationship('Game', back_populates='players') |
||||||
|
user = db.relationship('User', back_populates='user_games') |
||||||
|
__table_args__ = (UniqueConstraint('game_id', 'user_id', name='_game_user_uc'), |
||||||
|
) |
||||||
|
|
||||||
|
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_found_objectives = db.relationship( |
||||||
|
'PlayerFoundObjective', |
||||||
|
back_populates='game_player', |
||||||
|
cascade='save-update, merge, delete, delete-orphan') |
||||||
|
found_objectives = association_proxy('player_found_objectives', 'objective', |
||||||
|
creator=lambda objective: PlayerFoundObjective(objective=objective)) |
||||||
|
|
||||||
|
caught_by_players = association_proxy('player_caught_by_players', 'catching_player') |
||||||
|
caught_players = association_proxy('player_caught_players', 'caught_player') |
||||||
|
|
||||||
|
def last_location(self, offset=None): |
||||||
|
''' |
||||||
|
Returns game_player's last recorded location within game start- and end time. |
||||||
|
|
||||||
|
Parameters: |
||||||
|
offset (int): Offset in minutes. Only locations older than this amount of minutes will be returned. |
||||||
|
''' |
||||||
|
# pylint: disable=no-member |
||||||
|
return self.user.last_location(self.game, offset) |
||||||
|
|
||||||
|
def locations_during_game(self): |
||||||
|
# pylint: disable=no-member |
||||||
|
return self.user.locations_during_game(self.game) |
||||||
|
|
||||||
|
def encode_objectives(self): |
||||||
|
# pylint: disable=no-member |
||||||
|
objectives = ['['] |
||||||
|
for objective in self.game.objectives: |
||||||
|
obj = { |
||||||
|
'name' : objective.name, |
||||||
|
'longitude' : objective.longitude, |
||||||
|
'latitude' : objective.latitude, |
||||||
|
'found' : objective in self.found_objectives} |
||||||
|
objectives.append(json.dumps(obj)) |
||||||
|
objectives.append(',') |
||||||
|
return ''.join(objectives)[:-1] + ']' |
||||||
|
|
||||||
|
def accepted_caught_players(self): |
||||||
|
return [pcp.caught_player |
||||||
|
for pcp in self.player_caught_players |
||||||
|
if pcp.review == Review.accepted] |
||||||
|
|
||||||
|
def accepted_caught_by_players(self): |
||||||
|
return [pcp.catching_player |
||||||
|
for pcp in self.player_caught_by_players |
||||||
|
if pcp.review == Review.accepted] |
@ -0,0 +1,9 @@ |
|||||||
|
from enum import Enum |
||||||
|
|
||||||
|
class GameState(Enum): |
||||||
|
hidden = 1 |
||||||
|
published = 2 |
||||||
|
active = 3 |
||||||
|
paused = 4 |
||||||
|
finished = 5 |
||||||
|
|
@ -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) |
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey('user.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 { |
||||||
|
'username' : location.user.name, |
||||||
|
'longitude' : location.longitude, |
||||||
|
'latitude' : location.latitude, |
||||||
|
'timestamp_utc' : str(location.timestamp) |
||||||
|
} |
@ -0,0 +1,20 @@ |
|||||||
|
from sqlalchemy.ext.associationproxy import association_proxy |
||||||
|
from sqlalchemy.sql import func |
||||||
|
|
||||||
|
from app import db |
||||||
|
from .notification_player 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', 'game_player', |
||||||
|
creator=lambda game_player: NotificationPlayer(recipient=game_player)) |
@ -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) |
||||||
|
game_player_id = db.Column(db.Integer, db.ForeignKey('game_player.id'), primary_key=True, nullable=False) |
||||||
|
been_shown = db.Column(db.Boolean, server_default='0', nullable=False) |
||||||
|
notification = db.relationship('Notification', back_populates='notification_recipients') |
||||||
|
recipient = db.relationship('GamePlayer', back_populates='player_notifications') |
||||||
|
|
@ -0,0 +1,39 @@ |
|||||||
|
from secrets import token_hex |
||||||
|
from json import JSONEncoder |
||||||
|
from sqlalchemy.ext.associationproxy import association_proxy |
||||||
|
|
||||||
|
from app import db |
||||||
|
from .player_found_objective import PlayerFoundObjective |
||||||
|
from .role import 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', 'game_player', |
||||||
|
creator=lambda game_player: PlayerFoundObjective(game_player=game_player)) |
||||||
|
|
||||||
|
def set_hash(self): |
||||||
|
self.hash = token_hex(16) |
||||||
|
|
||||||
|
def owned_by(self, user): |
||||||
|
'''Returns True if given user is an owner of a game object is part of''' |
||||||
|
return user in [gameplayer.user for gameplayer in self.game.players if gameplayer.role == Role.owner] |
||||||
|
|
||||||
|
class ObjectiveEncoder(JSONEncoder): |
||||||
|
def default(self, objective): |
||||||
|
return { |
||||||
|
'name' : objective.name, |
||||||
|
'hash' : objective.hash, |
||||||
|
'longitude' : objective.longitude, |
||||||
|
'latitude' : objective.latitude |
||||||
|
} |
@ -0,0 +1,28 @@ |
|||||||
|
from sqlalchemy.sql import func |
||||||
|
|
||||||
|
from app import db |
||||||
|
from .game_player import GamePlayer |
||||||
|
from .review import Review |
||||||
|
|
||||||
|
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('game_player.id'), nullable=False) |
||||||
|
caught_player_id = db.Column(db.Integer, db.ForeignKey('game_player.id'), nullable=False) |
||||||
|
timestamp = db.Column(db.DateTime, server_default=func.now(), nullable=False) |
||||||
|
review = db.Column(db.Enum(Review), server_default=Review(0).name, nullable=False) |
||||||
|
catching_player = db.relationship('GamePlayer', primaryjoin=(catching_player_id == GamePlayer.id), |
||||||
|
backref=db.backref('player_caught_players', cascade='save-update, merge, delete, delete-orphan')) |
||||||
|
caught_player = db.relationship('GamePlayer', primaryjoin=(caught_player_id == GamePlayer.id), |
||||||
|
backref=db.backref('player_caught_by_players', cascade='save-update, merge, delete, delete-orphan')) |
||||||
|
|
||||||
|
''' |
||||||
|
This relation doesn't work as well as the others, and must be used as folowed: |
||||||
|
g = Game.query.first() |
||||||
|
p1 = User.query[2].player_in(g) |
||||||
|
p2 = User.query[3].player_in(g) |
||||||
|
pc = PlayerCaughtPlayer(caught_player=p2, catching_player=p1) |
||||||
|
db.session.add(pc) |
||||||
|
db.session.commit() |
||||||
|
|
||||||
|
''' |
@ -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') |
||||||
|
game_player_id = db.Column(db.Integer, db.ForeignKey('game_player.id'), primary_key=True, nullable=False, server_default='-1') |
||||||
|
timestamp = db.Column(db.DateTime, server_default=func.now(), nullable=False) |
||||||
|
game_player = db.relationship('GamePlayer', back_populates='player_found_objectives') |
||||||
|
objective = db.relationship('Objective', back_populates='objective_found_by') |
@ -0,0 +1,14 @@ |
|||||||
|
from enum import Enum |
||||||
|
|
||||||
|
class Review(Enum): |
||||||
|
none = 0 |
||||||
|
denied = 1 |
||||||
|
accepted = 2 |
||||||
|
|
||||||
|
@classmethod |
||||||
|
def parse_string(cls, string): |
||||||
|
if string == 'accept' or string == 'accepted': |
||||||
|
return cls.accepted |
||||||
|
if string == 'deny' or string == 'denied': |
||||||
|
return cls.denied |
||||||
|
return cls.none |
@ -0,0 +1,8 @@ |
|||||||
|
from enum import Enum |
||||||
|
|
||||||
|
class Role(Enum): |
||||||
|
none = 0 |
||||||
|
owner = 1 |
||||||
|
hunter = 2 |
||||||
|
bunny = 3 |
||||||
|
|
@ -0,0 +1,98 @@ |
|||||||
|
import unittest |
||||||
|
from datetime import datetime |
||||||
|
from app import create_app, db |
||||||
|
from app.models import User, Game, Role, GamePlayer |
||||||
|
from config import Config |
||||||
|
|
||||||
|
class TestConfig(Config): |
||||||
|
TESTING = True |
||||||
|
WTF_CSRF_ENABLED = False |
||||||
|
DEBUG = False |
||||||
|
SQLALCHEMY_DATABASE_URI = 'sqlite://' |
||||||
|
|
||||||
|
class GameCase(unittest.TestCase): |
||||||
|
# implement this: https://stackoverflow.com/questions/47294304/how-to-mock-current-user-in-flask-templates |
||||||
|
def setUp(self): |
||||||
|
self.app = create_app(TestConfig) |
||||||
|
self.app_context = self.app.app_context() |
||||||
|
self.app_context.push() |
||||||
|
db.create_all() |
||||||
|
|
||||||
|
def tearDown(self): |
||||||
|
db.session.remove() |
||||||
|
db.drop_all() |
||||||
|
self.app_context.pop() |
||||||
|
|
||||||
|
def test_is_game_owner(self): |
||||||
|
g1 = Game(name='TestGame') |
||||||
|
u1 = User(name='Henk') |
||||||
|
u2 = User(name='Alfred') |
||||||
|
|
||||||
|
g1.players.append(GamePlayer(user=u1, role=Role.owner)) |
||||||
|
g1.players.append(GamePlayer(user=u2, role=Role.bunny)) |
||||||
|
|
||||||
|
db.session.add(g1) |
||||||
|
db.session.commit() |
||||||
|
|
||||||
|
self.assertTrue(g1.owned_by(u1)) |
||||||
|
self.assertFalse(g1.owned_by(u2)) |
||||||
|
|
||||||
|
def test_delete(self): |
||||||
|
g1 = Game(name='TestGame') |
||||||
|
u1 = User(name='Henk') |
||||||
|
u2 = User(name='Karel') |
||||||
|
p1 = GamePlayer(user=u1, role=Role.bunny) |
||||||
|
p2 = GamePlayer(user=u2, role=Role.bunny) |
||||||
|
|
||||||
|
g1.players.append(p1) |
||||||
|
g1.players.append(p2) |
||||||
|
|
||||||
|
db.session.add(g1) |
||||||
|
db.session.commit() |
||||||
|
|
||||||
|
u2.last_login = datetime.now() |
||||||
|
db.session.commit() |
||||||
|
|
||||||
|
self.assertNotEqual(User.query.filter_by(name='Henk').first(), None) |
||||||
|
self.assertNotEqual(User.query.filter_by(name='Karel').first(), None) |
||||||
|
|
||||||
|
g1.delete() |
||||||
|
|
||||||
|
self.assertEqual(User.query.filter_by(name='Henk').first(), None) |
||||||
|
self.assertNotEqual(User.query.filter_by(name='Karel').first(), None) |
||||||
|
self.assertEqual(Game.query.filter_by(name='TestGame').first(), None) |
||||||
|
|
||||||
|
def test_remove_player(self): |
||||||
|
g1 = Game(name='TestGame') |
||||||
|
g2 = Game(name='TestGame2') |
||||||
|
u1 = User(name='Henk') |
||||||
|
u2 = User(name='Karel') |
||||||
|
p1_1 = GamePlayer(user=u1, role=Role.owner) |
||||||
|
p1_2 = GamePlayer(user=u2, role=Role.bunny) |
||||||
|
p2_1 = GamePlayer(user=u1, role=Role.owner) |
||||||
|
p2_2 = GamePlayer(user=u2, role=Role.owner) |
||||||
|
|
||||||
|
g1.players.append(p1_1) |
||||||
|
g1.players.append(p1_2) |
||||||
|
|
||||||
|
g2.players.append(p2_1) |
||||||
|
g2.players.append(p2_2) |
||||||
|
|
||||||
|
db.session.add(g1) |
||||||
|
db.session.add(g2) |
||||||
|
db.session.commit() |
||||||
|
|
||||||
|
self.assertTrue(g1.remove_player(u1)) |
||||||
|
self.assertFalse(g1.remove_player(u2)) |
||||||
|
self.assertFalse(g2.remove_player(u1)) |
||||||
|
self.assertTrue(g2.remove_player(u2)) |
||||||
|
|
||||||
|
db.session.commit() |
||||||
|
|
||||||
|
self.assertTrue(p1_1 in g1.players) |
||||||
|
self.assertTrue(p1_2 not in g1.players) |
||||||
|
self.assertTrue(p2_1 not in g2.players) |
||||||
|
self.assertTrue(p2_2 in g2.players) |
||||||
|
|
||||||
|
if __name__ == '__main__': |
||||||
|
unittest.main(verbosity=2) |
@ -0,0 +1,61 @@ |
|||||||
|
import unittest |
||||||
|
import json |
||||||
|
from app import create_app, db |
||||||
|
from app.models import User, Game, Role, GamePlayer, Objective |
||||||
|
from config import Config |
||||||
|
|
||||||
|
class TestConfig(Config): |
||||||
|
TESTING = True |
||||||
|
WTF_CSRF_ENABLED = False |
||||||
|
DEBUG = False |
||||||
|
SQLALCHEMY_DATABASE_URI = 'sqlite://' |
||||||
|
|
||||||
|
class GamePlayerCase(unittest.TestCase): |
||||||
|
# implement this: https://stackoverflow.com/questions/47294304/how-to-mock-current-user-in-flask-templates |
||||||
|
def setUp(self): |
||||||
|
self.app = create_app(TestConfig) |
||||||
|
self.app_context = self.app.app_context() |
||||||
|
self.app_context.push() |
||||||
|
db.create_all() |
||||||
|
|
||||||
|
def tearDown(self): |
||||||
|
db.session.remove() |
||||||
|
db.drop_all() |
||||||
|
self.app_context.pop() |
||||||
|
|
||||||
|
def test_default(self): |
||||||
|
g1 = Game(name='TestGame') |
||||||
|
u1 = User(name='Henk') |
||||||
|
p1 = GamePlayer(user=u1, role=Role.bunny) |
||||||
|
|
||||||
|
o1 = Objective(name='Objective 1') |
||||||
|
o2 = Objective(name='Objective 2') |
||||||
|
o3 = Objective(name='Objective 3') |
||||||
|
|
||||||
|
o1.set_hash() |
||||||
|
o2.set_hash() |
||||||
|
o3.set_hash() |
||||||
|
|
||||||
|
g1.players.append(p1) |
||||||
|
g1.objectives.append(o1) |
||||||
|
g1.objectives.append(o2) |
||||||
|
g1.objectives.append(o3) |
||||||
|
|
||||||
|
p1.found_objectives.append(o1) |
||||||
|
|
||||||
|
db.session.add(g1) |
||||||
|
db.session.commit() |
||||||
|
|
||||||
|
# Check if test initiaion succeeded |
||||||
|
self.assertEqual(len(Game.query.first().objectives), 3) |
||||||
|
self.assertEqual(User.query.first().user_games[0].found_objectives[0], o1) |
||||||
|
|
||||||
|
# The actual Test |
||||||
|
player_objectives = ('[{"name": "Objective 1", "longitude": null, "latitude": null, "found": true},' |
||||||
|
'{"name": "Objective 2", "longitude": null, "latitude": null, "found": false},' |
||||||
|
'{"name": "Objective 3", "longitude": null, "latitude": null, "found": false}]') |
||||||
|
self.assertEqual(p1.encode_objectives(), player_objectives) |
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__': |
||||||
|
unittest.main(verbosity=2) |
@ -0,0 +1,76 @@ |
|||||||
|
import unittest |
||||||
|
from app import create_app, db |
||||||
|
from app.models import User, Game, Role, GamePlayer, Objective |
||||||
|
from config import Config |
||||||
|
|
||||||
|
class TestConfig(Config): |
||||||
|
TESTING = True |
||||||
|
WTF_CSRF_ENABLED = False |
||||||
|
DEBUG = False |
||||||
|
SQLALCHEMY_DATABASE_URI = 'sqlite://' |
||||||
|
|
||||||
|
class ObjectiveCase(unittest.TestCase): |
||||||
|
# implement this: https://stackoverflow.com/questions/47294304/how-to-mock-current-user-in-flask-templates |
||||||
|
def setUp(self): |
||||||
|
self.app = create_app(TestConfig) |
||||||
|
self.app_context = self.app.app_context() |
||||||
|
self.app_context.push() |
||||||
|
db.create_all() |
||||||
|
|
||||||
|
def tearDown(self): |
||||||
|
db.session.remove() |
||||||
|
db.drop_all() |
||||||
|
self.app_context.pop() |
||||||
|
|
||||||
|
def test_is_objective_owner(self): |
||||||
|
g1 = Game(name='TestGame') |
||||||
|
g2 = Game(name='AnotherGame') |
||||||
|
u1 = User(name='Henk') |
||||||
|
|
||||||
|
g1.players.append(GamePlayer(user=u1, role=Role.owner)) |
||||||
|
g2.players.append(GamePlayer(user=u1, role=Role.bunny)) |
||||||
|
|
||||||
|
o1 = Objective(name='o1') |
||||||
|
o1.set_hash() |
||||||
|
o2 = Objective(name='o2') |
||||||
|
o2.set_hash() |
||||||
|
|
||||||
|
g1.objectives.append(o1) |
||||||
|
g2.objectives.append(o2) |
||||||
|
|
||||||
|
db.session.add(g1) |
||||||
|
db.session.add(g2) |
||||||
|
db.session.commit() |
||||||
|
|
||||||
|
self.assertTrue(o1.owned_by(u1)) |
||||||
|
self.assertFalse(o2.owned_by(u1)) |
||||||
|
|
||||||
|
def test_delete_objectives_recursively(self): |
||||||
|
g1 = Game(name='TestGame') |
||||||
|
u1 = User(name = 'Henk') |
||||||
|
p1 = GamePlayer(user=u1, role=Role.bunny) |
||||||
|
|
||||||
|
o1 = Objective(name='o1') |
||||||
|
o1.set_hash() |
||||||
|
o2 = Objective(name='o2') |
||||||
|
o2.set_hash() |
||||||
|
|
||||||
|
g1.players.append(p1) |
||||||
|
g1.objectives.append(o1) |
||||||
|
g1.objectives.append(o2) |
||||||
|
p1.found_objectives.append(o2) |
||||||
|
|
||||||
|
db.session.add(g1) |
||||||
|
db.session.commit() |
||||||
|
|
||||||
|
self.assertNotEqual(Objective.query.filter_by(name='o1').first(), None) |
||||||
|
self.assertNotEqual(Objective.query.filter_by(name='o2').first(), None) |
||||||
|
|
||||||
|
db.session.delete(g1) |
||||||
|
db.session.commit() |
||||||
|
|
||||||
|
self.assertEqual(Objective.query.filter_by(name='o1').first(), None) |
||||||
|
self.assertEqual(Objective.query.filter_by(name='o2').first(), None) |
||||||
|
|
||||||
|
if __name__ == '__main__': |
||||||
|
unittest.main(verbosity=2) |
@ -0,0 +1,76 @@ |
|||||||
|
import unittest |
||||||
|
from datetime import datetime |
||||||
|
from app import create_app, db |
||||||
|
from app.models import User, Game, GamePlayer, Role |
||||||
|
from config import Config |
||||||
|
|
||||||
|
class TestConfig(Config): |
||||||
|
TESTING = True |
||||||
|
WTF_CSRF_ENABLED = False |
||||||
|
DEBUG = False |
||||||
|
SQLALCHEMY_DATABASE_URI = 'sqlite://' |
||||||
|
|
||||||
|
class UserCase(unittest.TestCase): |
||||||
|
# implement this: https://stackoverflow.com/questions/47294304/how-to-mock-current-user-in-flask-templates |
||||||
|
def setUp(self): |
||||||
|
self.app = create_app(TestConfig) |
||||||
|
self.app_context = self.app.app_context() |
||||||
|
self.app_context.push() |
||||||
|
db.create_all() |
||||||
|
|
||||||
|
def tearDown(self): |
||||||
|
db.session.remove() |
||||||
|
db.drop_all() |
||||||
|
self.app_context.pop() |
||||||
|
|
||||||
|
def test_is_player_game_owner(self): |
||||||
|
g1 = Game(name='TestGame') |
||||||
|
g2 = Game(name='AnotherGame') |
||||||
|
|
||||||
|
u1 = User(name='Henk') |
||||||
|
u2 = User(name='Alfred') |
||||||
|
u3 = User(name='Sasha') |
||||||
|
|
||||||
|
g1.players.append(GamePlayer(user=u1, role=Role.owner)) |
||||||
|
g1.players.append(GamePlayer(user=u2, role=Role.bunny)) |
||||||
|
|
||||||
|
g2.players.append(GamePlayer(user=u1, role=Role.hunter)) |
||||||
|
g2.players.append(GamePlayer(user=u3, role=Role.bunny)) |
||||||
|
|
||||||
|
|
||||||
|
db.session.add(g1) |
||||||
|
db.session.add(g2) |
||||||
|
db.session.commit() |
||||||
|
|
||||||
|
self.assertTrue(u1.owns_game_played_by(user=u2), "owner owns subject_player's game") |
||||||
|
self.assertFalse(u1.owns_game_played_by(user=u3), "owner doesn't own subject_player's game") |
||||||
|
self.assertTrue(u1.owns_game_played_by(user=u1), "owner owns it own's game") |
||||||
|
|
||||||
|
def test_role_in_game(self): |
||||||
|
g1 = Game(name='TestGame') |
||||||
|
|
||||||
|
u1 = User(name='Henk') |
||||||
|
u2 = User(name='Alfred') |
||||||
|
u3 = User(name='Sasha') |
||||||
|
u4 = User(name='Demian') |
||||||
|
u5 = User(name='Karl') |
||||||
|
|
||||||
|
g1.players.append(GamePlayer(user=u1, role=Role.owner)) |
||||||
|
g1.players.append(GamePlayer(user=u2, role=Role.bunny)) |
||||||
|
g1.players.append(GamePlayer(user=u3, role=Role.hunter)) |
||||||
|
g1.players.append(GamePlayer(user=u4, role=Role.none)) |
||||||
|
|
||||||
|
db.session.add(g1) |
||||||
|
db.session.add(u5) |
||||||
|
db.session.commit() |
||||||
|
|
||||||
|
self.assertEqual(u1.role_in_game(g1), Role.owner) |
||||||
|
self.assertEqual(u2.role_in_game(g1), Role.bunny) |
||||||
|
self.assertEqual(u3.role_in_game(g1), Role.hunter) |
||||||
|
self.assertEqual(u4.role_in_game(g1), Role.none) |
||||||
|
self.assertEqual(u5.role_in_game(g1), None) |
||||||
|
with self.assertRaises(AttributeError): |
||||||
|
g1.get_role_for_game(None) |
||||||
|
|
||||||
|
if __name__ == '__main__': |
||||||
|
unittest.main(verbosity=2) |
@ -0,0 +1,128 @@ |
|||||||
|
from secrets import token_hex |
||||||
|
from datetime import datetime, timedelta |
||||||
|
|
||||||
|
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, login |
||||||
|
from app.models import GamePlayer, Role |
||||||
|
|
||||||
|
class User(UserMixin, db.Model): |
||||||
|
""" !Always call set_auth_hash() after creating new instance! """ |
||||||
|
__tablename__ = 'user' |
||||||
|
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)) |
||||||
|
last_login = db.Column(db.DateTime) |
||||||
|
|
||||||
|
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)) |
||||||
|
|
||||||
|
locations = db.relationship( |
||||||
|
'Location', |
||||||
|
lazy='select', |
||||||
|
backref=db.backref('user', lazy='joined'), |
||||||
|
cascade="save-update, merge, delete, delete-orphan") |
||||||
|
|
||||||
|
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): |
||||||
|
if not password or not self.password_hash: |
||||||
|
return False |
||||||
|
return check_password_hash(self.password_hash, password) |
||||||
|
|
||||||
|
def locations_during_game(self, game, offset=None): |
||||||
|
''' |
||||||
|
Returns users locations during game. |
||||||
|
|
||||||
|
Parameters: |
||||||
|
game (Game): If specified, only locations within start- and endtime of game wil be returned |
||||||
|
offset (int): Offset in minutes. Only locations older than this amount of minutes will be returned. |
||||||
|
''' |
||||||
|
# pylint: disable=not-an-iterable |
||||||
|
if offset is None or offset == '': |
||||||
|
offset = 0 |
||||||
|
if not self.locations: |
||||||
|
return None |
||||||
|
if game is None: |
||||||
|
if offset == 0: |
||||||
|
return self.locations |
||||||
|
return [location for location in self.locations |
||||||
|
if datetime.utcnow() - location.timestamp > timedelta(minutes=offset)] |
||||||
|
game_start = game.start_time or datetime.min |
||||||
|
game_end = game.end_time or datetime.max |
||||||
|
if offset == 0: |
||||||
|
return [location for location in self.locations |
||||||
|
if location.timestamp > game_start and location.timestamp < game_end] |
||||||
|
return [location for location in self.locations |
||||||
|
if location.timestamp > game_start and location.timestamp < game_end |
||||||
|
and datetime.utcnow() - location.timestamp > timedelta(minutes=offset)] |
||||||
|
|
||||||
|
def last_location(self, game=None, offset=None): |
||||||
|
''' |
||||||
|
Returns users last recorded location. |
||||||
|
|
||||||
|
Parameters: |
||||||
|
game (Game): If specified, only locations within start- and endtime of game wil be returned |
||||||
|
offset (int): Offset in minutes. Only locations older than this amount of minutes will be returned. |
||||||
|
''' |
||||||
|
# pylint: disable=[not-an-iterable, unsubscriptable-object] |
||||||
|
if offset is None or offset == '': |
||||||
|
offset = 0 |
||||||
|
if not self.locations: |
||||||
|
return None |
||||||
|
if game is None: |
||||||
|
locations = self.locations_during_game(game=None, offset=offset) |
||||||
|
return locations[-1] if locations else None |
||||||
|
|
||||||
|
locations_during_game = self.locations_during_game(game, offset=offset) |
||||||
|
return locations_during_game[-1] if locations_during_game else None |
||||||
|
|
||||||
|
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''' |
||||||
|
gameplayer = self.player_in(game) |
||||||
|
if not gameplayer: |
||||||
|
return None |
||||||
|
return gameplayer.role |
||||||
|
|
||||||
|
def owns_game_played_by(self, user): |
||||||
|
'''Self is an owner of a game the user participates in''' |
||||||
|
return self in [gameplayer.user for gameplayers in |
||||||
|
[game.players for game in user.games] |
||||||
|
for gameplayer in gameplayers if gameplayer.role == Role.owner] |
||||||
|
|
||||||
|
def owned_game_played_by(self, user): |
||||||
|
'''Return first game owned by self that the user participates in''' |
||||||
|
return next(iter([gameplayer.game for gameplayers in |
||||||
|
[game.players for game in user.games] |
||||||
|
for gameplayer in gameplayers |
||||||
|
if gameplayer.role == Role.owner and gameplayer.user == self]), |
||||||
|
None) |
||||||
|
|
||||||
|
def player_in(self, game): |
||||||
|
# pylint: disable=not-an-iterable |
||||||
|
'''Returns GamePlayer object for given game, or None if user does not participate in given game''' |
||||||
|
gameplayers = [gameplayer for gameplayer in self.user_games if gameplayer.game == game] |
||||||
|
if not gameplayers: |
||||||
|
return None |
||||||
|
return gameplayers[0] |
||||||
|
|
||||||
|
@staticmethod |
||||||
|
def delete_orphans(): |
||||||
|
User.query.filter(~User.user_games.any()).delete() |
||||||
|
db.session.commit() |
||||||
|
|
||||||
|
|
||||||
|
@login.user_loader |
||||||
|
def load_user(id): |
||||||
|
return User.query.get(int(id)) |
@ -0,0 +1,26 @@ |
|||||||
|
function getPosition(locationHandler){ |
||||||
|
if (navigator.geolocation) { |
||||||
|
navigator.geolocation.getCurrentPosition(locationHandler, showError); |
||||||
|
|
||||||
|
} else { |
||||||
|
console.log('Geolocation is not supported by this browser.') |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function showError(error) { |
||||||
|
switch(error.code) { |
||||||
|
case error.PERMISSION_DENIED: |
||||||
|
console.log("User denied the request for Geolocation.") |
||||||
|
alert("Please refresh page and allow location sharing, otherwise the game won't work :'(") |
||||||
|
break; |
||||||
|
case error.POSITION_UNAVAILABLE: |
||||||
|
console.log("Location information is unavailable.") |
||||||
|
break; |
||||||
|
case error.TIMEOUT: |
||||||
|
console.log("The request to get user location timed out.") |
||||||
|
break; |
||||||
|
case error.UNKNOWN_ERROR: |
||||||
|
console.log("An unknown error occurred.") |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 21 KiB |
@ -0,0 +1,174 @@ |
|||||||
|
var greyIcon = new L.Icon({ |
||||||
|
iconUrl: '/static/assets/leaflet/images/marker-icon-2x-grey.png', |
||||||
|
shadowUrl: '/static/assets/leaflet/images/marker-shadow.png', |
||||||
|
iconSize: [25, 41], |
||||||
|
iconAnchor: [12, 41], |
||||||
|
popupAnchor: [1, -34], |
||||||
|
shadowSize: [41, 41] |
||||||
|
}); |
||||||
|
|
||||||
|
var greenIcon = new L.Icon({ |
||||||
|
iconUrl: '/static/assets/leaflet/images/marker-icon-2x-green.png', |
||||||
|
shadowUrl: '/static/assets/leaflet/images/marker-shadow.png', |
||||||
|
iconSize: [25, 41], |
||||||
|
iconAnchor: [12, 41], |
||||||
|
popupAnchor: [1, -34], |
||||||
|
shadowSize: [41, 41] |
||||||
|
}); |
||||||
|
|
||||||
|
var goldIcon = new L.Icon({ |
||||||
|
iconUrl: '/static/assets/leaflet/images/marker-icon-2x-gold.png', |
||||||
|
shadowUrl: '/static/assets/leaflet/images/marker-shadow.png', |
||||||
|
iconSize: [25, 41], |
||||||
|
iconAnchor: [12, 41], |
||||||
|
popupAnchor: [1, -34], |
||||||
|
shadowSize: [41, 41] |
||||||
|
}); |
||||||
|
|
||||||
|
var bluePlayerIcon = new L.Icon({ |
||||||
|
iconUrl: '/static/assets/leaflet/images/person-marker-icon-2x-blue.png', |
||||||
|
shadowUrl: '/static/assets/leaflet/images/marker-shadow.png', |
||||||
|
iconSize: [25, 41], |
||||||
|
iconAnchor: [12, 41], |
||||||
|
popupAnchor: [1, -34], |
||||||
|
shadowSize: [41, 41] |
||||||
|
}); |
||||||
|
|
||||||
|
var greenPlayerIcon = new L.Icon({ |
||||||
|
iconUrl: '/static/assets/leaflet/images/person-marker-icon-2x-green.png', |
||||||
|
shadowUrl: '/static/assets/leaflet/images/marker-shadow.png', |
||||||
|
iconSize: [25, 41], |
||||||
|
iconAnchor: [12, 41], |
||||||
|
popupAnchor: [1, -34], |
||||||
|
shadowSize: [41, 41] |
||||||
|
}); |
||||||
|
|
||||||
|
var goldPlayerIcon = new L.Icon({ |
||||||
|
iconUrl: '/static/assets/leaflet/images/person-marker-icon-2x-gold.png', |
||||||
|
shadowUrl: '/static/assets/leaflet/images/marker-shadow.png', |
||||||
|
iconSize: [25, 41], |
||||||
|
iconAnchor: [12, 41], |
||||||
|
popupAnchor: [1, -34], |
||||||
|
shadowSize: [41, 41] |
||||||
|
}); |
||||||
|
|
||||||
|
var bluePlayerIconMini = new L.Icon({ |
||||||
|
iconUrl: '/static/assets/leaflet/images/person-marker-icon-2x-blue.png', |
||||||
|
shadowUrl: '/static/assets/leaflet/images/marker-shadow.png', |
||||||
|
iconSize: [10, 16.4], |
||||||
|
iconAnchor: [5, 16.4], |
||||||
|
popupAnchor: [1, -34], |
||||||
|
shadowSize: [16.4, 16.4] |
||||||
|
}); |
||||||
|
|
||||||
|
function addObjectiveMarker(map, objective, icon=greenIcon){ |
||||||
|
var objectiveMarker = L.marker([ |
||||||
|
objective['latitude'], |
||||||
|
objective['longitude'] |
||||||
|
], {icon: icon}) |
||||||
|
if(objective['found']){ |
||||||
|
objectiveMarker.setIcon(goldIcon) |
||||||
|
} |
||||||
|
objectiveMarker.addTo(map); |
||||||
|
|
||||||
|
if(objective['hash']){ |
||||||
|
objectiveMarker.bindTooltip(`<b>${objective['name']}</b><br>
|
||||||
|
${objective['hash']}`).openPopup();
|
||||||
|
objectiveMarker.hash = objective['hash']; |
||||||
|
} else { |
||||||
|
objectiveMarker.bindTooltip(`<b>${objective['name']}</b>`).openPopup(); |
||||||
|
} |
||||||
|
return objectiveMarker; |
||||||
|
} |
||||||
|
|
||||||
|
function addPlayerMarker(map, player, icon=bluePlayerIcon){ |
||||||
|
var playerMarker = L.marker([ |
||||||
|
player['latitude'], |
||||||
|
player['longitude'] |
||||||
|
], {icon: icon}).addTo(map); |
||||||
|
var timestamp_local = toMomentLocal(player['timestamp_utc']).format('YYYY-MM-DD HH:mm') |
||||||
|
playerMarker.bindTooltip(`<b>${player['username']}</b><br>
|
||||||
|
${timestamp_local}`).openPopup();
|
||||||
|
playerMarker.username = player['username']; |
||||||
|
return playerMarker; |
||||||
|
} |
||||||
|
|
||||||
|
function toMomentLocal(timestamp_utc_string){ |
||||||
|
var timestamp_utc = moment.utc(timestamp_utc_string).toDate(); |
||||||
|
return moment(timestamp_utc).local(); |
||||||
|
} |
||||||
|
|
||||||
|
var myLocationMarker |
||||||
|
function updateMyLocation(position){ |
||||||
|
if(myLocationMarker == undefined){ |
||||||
|
myLocationMarker = L.marker([ |
||||||
|
position.coords.latitude,
|
||||||
|
position.coords.longitude |
||||||
|
], {icon: bluePlayerIcon}).addTo(map) |
||||||
|
myLocationMarker.bindTooltip('Your current location').openPopup(); |
||||||
|
} |
||||||
|
else{ |
||||||
|
var newLocation = new L.LatLng( |
||||||
|
position.coords.latitude,
|
||||||
|
position.coords.longitude |
||||||
|
); |
||||||
|
myLocationMarker.setLatLng(newLocation);
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function getMap(){ |
||||||
|
var map = L.map( 'map', { |
||||||
|
center: [52.2, 5.3], |
||||||
|
minZoom: 6, |
||||||
|
maxZoom: 18, |
||||||
|
bounds: [[50.5, 3.25], [54, 7.6]], |
||||||
|
zoom: 8 |
||||||
|
}); |
||||||
|
L.control.scale().addTo(map); |
||||||
|
|
||||||
|
//pioneer,atlas,neighbourhood,transport
|
||||||
|
L.tileLayer( 'https://tile.thunderforest.com/pioneer/{z}/{x}/{y}.png?apikey=df457ee6c2dd4b6e99b24c853421a1db', { |
||||||
|
attribution: 'Kaartgegevens © <a href="https://www.thunderforest.com">Thunderforest</a>' |
||||||
|
}).addTo( map ); |
||||||
|
return map |
||||||
|
} |
||||||
|
|
||||||
|
var csrftoken = $('meta[name=csrf-token]').attr('content') |
||||||
|
$.ajaxSetup({ |
||||||
|
beforeSend: function(xhr, settings) { |
||||||
|
if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) { |
||||||
|
xhr.setRequestHeader("X-CSRFToken", csrftoken) |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
function pollLocations(url, requestedUsers, mode, playerLocations, responseHandler) { |
||||||
|
$.ajax({ |
||||||
|
type: "POST", |
||||||
|
url: url, |
||||||
|
data: JSON.stringify({ |
||||||
|
requested_users: requestedUsers, |
||||||
|
mode: mode, |
||||||
|
last_update: moment(get_latest_date(playerLocations)).format("YYYY-MM-DD HH:mm:ss:SSSS") |
||||||
|
}), |
||||||
|
contentType: "application/json; charset=utf-8", |
||||||
|
dataType: 'json', |
||||||
|
success: function(data){ |
||||||
|
responseHandler( |
||||||
|
data.filter(item => item.latitude && item.longitude && item.timestamp_utc && item.username) |
||||||
|
) |
||||||
|
}, |
||||||
|
error: function(error) { |
||||||
|
console.log(error); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
function get_latest_date(playerLocations){ |
||||||
|
if (playerLocations.length == 0){ |
||||||
|
return new Date('0001-01-01T00:00:00Z'); |
||||||
|
} |
||||||
|
return new Date(Math.max.apply(null, playerLocations.map(function(e) { |
||||||
|
return new Date(e.timestamp_utc); |
||||||
|
}))); |
||||||
|
} |
@ -0,0 +1,37 @@ |
|||||||
|
function createDateTimePicker(datePicker, checkbox, date){ |
||||||
|
$(datePicker).datetimepicker({ |
||||||
|
//useCurrent: false, //Important! See issue #1075
|
||||||
|
locale: 'en-gb', |
||||||
|
format: 'DD-MM-YYYY HH:mm', |
||||||
|
keepInvalid: true, |
||||||
|
sideBySide: true, |
||||||
|
defaultDate: null, |
||||||
|
timeZone: moment.tz.guess() |
||||||
|
}); |
||||||
|
|
||||||
|
$(checkbox).change(function() { |
||||||
|
updateDateTimePicker(datePicker, checkbox) |
||||||
|
}); |
||||||
|
|
||||||
|
if (!$(checkbox)[0].checked){ |
||||||
|
if ($(datePicker)[0].value == ''){ |
||||||
|
$(datePicker)[0].value = date.format('DD-MM-YYYY HH:mm'); |
||||||
|
} else if (!$('.alert')[0]) { //Don't convert datetime again after error
|
||||||
|
$(datePicker)[0].value = moment.utc($(datePicker)[0].value, 'DD-MM-YYYY HH:mm').local().format('DD-MM-YYYY HH:mm'); |
||||||
|
} |
||||||
|
} else { |
||||||
|
$(datePicker).data("DateTimePicker").disable(); |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
function updateDateTimePicker(picker, checkbox){ |
||||||
|
if ($(checkbox).prop("checked")) { |
||||||
|
$(picker).data("DateTimePicker").disable(); |
||||||
|
} |
||||||
|
else { |
||||||
|
$(picker).data("DateTimePicker").enable(); |
||||||
|
if ($(picker)[0].value == ''){ |
||||||
|
$(picker)[0].value = moment().format('DD-MM-YYYY HH:mm'); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,42 @@ |
|||||||
|
<h2>Game Info</h2> |
||||||
|
<div class="table"> |
||||||
|
<table class="table"> |
||||||
|
<tr> |
||||||
|
<th>My Game</th> |
||||||
|
<td>{{ game.name.title() }}</td> |
||||||
|
</tr> |
||||||
|
<tr> |
||||||
|
<th>My Name</th> |
||||||
|
<td>{{ current_user.name.title() }}</td> |
||||||
|
</tr> |
||||||
|
<tr> |
||||||
|
<th>My Role</th> |
||||||
|
<td>{{ current_user.role_in_game(game).name.title() }}</td> |
||||||
|
</tr> |
||||||
|
<tr> |
||||||
|
<th>Game State</th> |
||||||
|
<td>{{ game.get_state().name.title() }}</td> |
||||||
|
</tr> |
||||||
|
<tr> |
||||||
|
<th>Start Time</th> |
||||||
|
<td>{% if game.start_time %}{{ moment(game.start_time).format('DD-MM-YYYY, HH:mm')}}{% else %}-{% endif %}</td> |
||||||
|
</tr> |
||||||
|
<tr> |
||||||
|
<th>End Time</th> |
||||||
|
<td>{% if game.end_time %}{{ moment(game.end_time).format('DD-MM-YYYY, HH:mm')}}{% else %}-{% endif %}</td> |
||||||
|
</tr> |
||||||
|
{% if current_user.role_in_game(game).name == 'bunny' %} |
||||||
|
<tr> |
||||||
|
<th>Been Found</th> |
||||||
|
<td> |
||||||
|
<span style="color:green;">{{ current_user.player_caught_by_players | selectattr('catching_player', '==', player) | selectattr('review.name', '==', 'accepted') |list|length}}</span> / |
||||||
|
<span style="color:red;">{{ current_user.player_caught_by_players | selectattr('catching_player', '==', player) | selectattr('review.name', '==', 'denied') |list|length}}</span> / |
||||||
|
<span style="color:gray;">{{ current_user.player_caught_by_players | selectattr('catching_player', '==', player) | selectattr('review.name', '==', 'none') |list|length}}</span> |
||||||
|
<span style="font-size: smaller;"> |
||||||
|
(<span style="color:green;">Accepted</span>/<span style="color:red;">Denied</span>/<span style="color:gray;">Not reviewed</span>) |
||||||
|
</span> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
{% endif %} |
||||||
|
</table> |
||||||
|
</div> |
@ -0,0 +1,37 @@ |
|||||||
|
<div class="row"> |
||||||
|
<div class="col-md-4"> |
||||||
|
<div class="row"> |
||||||
|
<div class="table-responsive"> |
||||||
|
<table class="table"> |
||||||
|
<tr> |
||||||
|
<th>Hunter</th> |
||||||
|
<td>{{ pcp.catching_player.user.name }}</td> |
||||||
|
</tr> |
||||||
|
<tr> |
||||||
|
<th>Bunny</th> |
||||||
|
<td>{{ pcp.caught_player.user.name }}</td> |
||||||
|
</tr> |
||||||
|
<tr> |
||||||
|
<th>Time</th> |
||||||
|
<td>{{ pcp.timestamp.strftime('%Y-%m-%d %H:%M') }}</td> |
||||||
|
</tr> |
||||||
|
</table> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="row"> |
||||||
|
<a href="{{ url_for('main.review_caught_bunny_photos', game_name=game.name, pcp_id=pcp.id, action='accept') }}"> |
||||||
|
<button class="btn btn-success">Accept</button> |
||||||
|
</a> |
||||||
|
<a href="{{ url_for('main.review_caught_bunny_photos', game_name=game.name, pcp_id=pcp.id, action='deny') }}"> |
||||||
|
<button class="btn btn-danger">Reject</button> |
||||||
|
</a> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="col-md-8"> |
||||||
|
<img src="{{ url_for('main.caught_bunny_photo', game_name=game.name, |
||||||
|
timestamp=pcp.timestamp.strftime('%Y%m%d%H%M%S'), |
||||||
|
bunny_name=pcp.caught_player.user.name, |
||||||
|
hunter_name=pcp.catching_player.user.name) }}" |
||||||
|
alt="could not load photo", width="100%"> |
||||||
|
</div> |
||||||
|
</div> |
@ -0,0 +1,18 @@ |
|||||||
|
{% extends "base.html" %} |
||||||
|
{% import 'bootstrap/wtf.html' as wtf %} |
||||||
|
|
||||||
|
{% block app_content %} |
||||||
|
<div class="row"> |
||||||
|
<div class="col-xs-0 col-md-1"></div> |
||||||
|
<div class="col-xs-8 col-md-4"> |
||||||
|
{% if current_user.password_hash %} |
||||||
|
<h1>Change Password</h1> |
||||||
|
{% else %} |
||||||
|
<h1>Set Password</h1> |
||||||
|
{% endif %} |
||||||
|
{{ wtf.quick_form(form, button_map={'submit': 'primary'}) }} |
||||||
|
<br> |
||||||
|
</div> |
||||||
|
<div class="col-xs-0 col-md-7"></div> |
||||||
|
</div> |
||||||
|
{% endblock %} |
@ -0,0 +1,20 @@ |
|||||||
|
{% extends "base.html" %} |
||||||
|
{% import 'bootstrap/wtf.html' as wtf %} |
||||||
|
|
||||||
|
{% block app_content %} |
||||||
|
<div class="row"> |
||||||
|
<div class="col-xs-0 col-md-1"></div> |
||||||
|
<div class="col-xs-12 col-md-10"> |
||||||
|
<h1>Welcome, {{ user.name }}!</h1> |
||||||
|
<p> |
||||||
|
If you found this page, it probably means someone who is organising a hunt invited you by |
||||||
|
sending a link or QR-code to this page. You can start playing right away, if and if you get |
||||||
|
logged out just visit this page again. However, if you want to be sure other people can't |
||||||
|
steal this account, please set a password. |
||||||
|
</p> |
||||||
|
<a href="{{ url_for('auth.change_password', auth_hash=user.auth_hash) }}"><button class="btn btn-primary">Set Password</button></a> |
||||||
|
<a href="{{ url_for('auth.user_hash_login', auth_hash=user.auth_hash, login='true') }}"><button class="btn btn-primary">Start Playing!</button></a> |
||||||
|
</div> |
||||||
|
<div class="col-xs-0 col-md-1"></div> |
||||||
|
</div> |
||||||
|
{% endblock %} |
@ -1,53 +1,66 @@ |
|||||||
{% extends 'bootstrap/base.html' %} |
{% extends 'bootstrap/base.html' %} |
||||||
|
|
||||||
|
{% block styles %} |
||||||
|
{{ super() }} |
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/default.css') }}"> |
||||||
|
{% endblock %} |
||||||
|
|
||||||
{% block title %} |
{% block title %} |
||||||
{% if title %}{{ title }} - The Hunt{% else %}Welcome to The Hunt{% endif %} |
{% if title %}{{ title }} - The Hunt{% else %}Welcome to The Hunt{% endif %} |
||||||
{% endblock %} |
{% endblock %} |
||||||
|
|
||||||
{% block navbar %} |
{% block navbar %} |
||||||
<nav class="navbar navbar-default"> |
<nav class="navbar navbar-default"> |
||||||
<div class="container"> |
<div class="container"> |
||||||
<div class="navbar-header"> |
<div class="navbar-header"> |
||||||
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false"> |
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" |
||||||
<span class="sr-only">Toggle navigation</span> |
data-target="#bs-example-navbar-collapse-1" aria-expanded="false"> |
||||||
<span class="icon-bar"></span> |
<span class="sr-only">Toggle navigation</span> |
||||||
<span class="icon-bar"></span> |
<span class="icon-bar"></span> |
||||||
<span class="icon-bar"></span> |
<span class="icon-bar"></span> |
||||||
</button> |
<span class="icon-bar"></span> |
||||||
<a class="navbar-brand" href="{{ url_for('main.index') }}">The Hunt</a> |
</button> |
||||||
</div> |
<a class="navbar-brand" href="{{ url_for('main.index') }}">The Hunt</a> |
||||||
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1"> |
</div> |
||||||
<ul class="nav navbar-nav"> |
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1"> |
||||||
<li><a href="{{ url_for('main.index') }}">Home</a></li> |
<ul class="nav navbar-nav"> |
||||||
{% if current_user.is_authenticated %} |
<li><a href="{{ url_for('main.index') }}">Home</a></li> |
||||||
<li><a href="{{ url_for('main.create_game') }}">Create Game</a></li> |
{% if current_user.is_authenticated %} |
||||||
{% endif %} |
<li><a href="{{ url_for('game.create_game') }}">Create Game</a></li> |
||||||
</ul> |
{% endif %} |
||||||
<ul class="nav navbar-nav navbar-right"> |
</ul> |
||||||
{% if current_user.is_anonymous %} |
<ul class="nav navbar-nav navbar-right"> |
||||||
<li><a href="{{ url_for('auth.login') }}">Login</a></li> |
{% if current_user.is_anonymous %} |
||||||
{% else %} |
<li><a href="{{ url_for('auth.login') }}">Login</a></li> |
||||||
<li><a href="{{ url_for('auth.logout') }}">Logout</a></li> |
{% else %} |
||||||
{% endif %} |
<li><a href="{{ url_for('main.user_profile', username=current_user.name) }}"> |
||||||
</ul> |
<div class="hidden-xs hidden-sm"> |
||||||
|
{{ current_user.name }}{% if game is defined %}/{{ game.name }}{% endif %}</div> |
||||||
|
</a></li> |
||||||
|
<li><a href="{{ url_for('auth.logout') }}">Logout</a></li> |
||||||
|
{% endif %} |
||||||
|
</ul> |
||||||
|
</div> |
||||||
</div> |
</div> |
||||||
</div> |
|
||||||
</nav> |
</nav> |
||||||
{% endblock %} |
{% endblock %} |
||||||
|
|
||||||
{% block content %} |
{% block content %} |
||||||
<div class="container"> |
<div class="container"> |
||||||
{% with messages = get_flashed_messages() %} |
{% with messages = get_flashed_messages() %} |
||||||
{% if messages %} |
{% if messages %} |
||||||
{% for message in messages %} |
{% for message in messages %} |
||||||
<div class="alert alert-info" role="alert">{{ message }}</div> |
<div class="alert alert-info" role="alert">{{ message }}</div> |
||||||
{% endfor %} |
{% endfor %} |
||||||
{% endif %} |
{% endif %} |
||||||
{% endwith %} |
{% endwith %} |
||||||
|
|
||||||
{# application content needs to be provided in the app_content block #} |
{# application content needs to be provided in the app_content block #} |
||||||
{% block app_content %}{% endblock %} |
{% block app_content %}{% endblock %} |
||||||
</div> |
</div> |
||||||
{% endblock %} |
{% endblock %} |
||||||
|
|
||||||
|
{% block scripts %} |
||||||
|
{{ super() }} |
||||||
|
{{ moment.include_moment() }} |
||||||
|
{% endblock %} |
@ -0,0 +1,19 @@ |
|||||||
|
{% extends 'player_base.html' %} |
||||||
|
{% import 'bootstrap/wtf.html' as wtf %} |
||||||
|
|
||||||
|
{% block player_app_content %} |
||||||
|
<div class="row"> |
||||||
|
<div class="col-xs-0 col-md-1"></div> |
||||||
|
<div class="col-xs-8 col-md-4"> |
||||||
|
<h1>Catch Bunny</h1> |
||||||
|
<form action="" method="post" class="form" role="form" enctype="multipart/form-data"> |
||||||
|
{{ form.hidden_tag() }} |
||||||
|
{{ wtf.form_field(form.bunny) }} |
||||||
|
{{ wtf.form_field(form.photo) }} |
||||||
|
{{ wtf.form_field(form.submit, class='btn btn-primary') }} |
||||||
|
<a href="{{ url_for('main.game_dashboard', game_name=game.name) }}"><button type="button" class="btn btn-warning">Back</button></a> |
||||||
|
</form> |
||||||
|
</div> |
||||||
|
<div class="col-xs-0 col-md-7"></div> |
||||||
|
</div> |
||||||
|
{% endblock %} |
@ -0,0 +1,82 @@ |
|||||||
|
{% extends 'player_base.html' %} |
||||||
|
|
||||||
|
{% block head %} |
||||||
|
{{ super() }} |
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='assets/leaflet/leaflet.css') }}" /> |
||||||
|
<script src="{{ url_for('static', filename='assets/leaflet/leaflet.js') }}"></script> |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block player_app_content %} |
||||||
|
<div class="row"> |
||||||
|
<div class="col-xs-0 col-md-1"></div> |
||||||
|
|
||||||
|
<div class="col-xs-12 col-md-7"> |
||||||
|
|
||||||
|
<h2>Objective Locations:</h2> |
||||||
|
{% if game.objectives %} |
||||||
|
<div class="table"> |
||||||
|
<table class="table"> |
||||||
|
<thead> |
||||||
|
<tr> |
||||||
|
<th scope="col">Objective Name</th> |
||||||
|
<th scope="col">Latitude</th> |
||||||
|
<th scope="col">Longitude</th> |
||||||
|
<th scope="col">Found</th> |
||||||
|
</tr> |
||||||
|
</thead> |
||||||
|
<tbody> |
||||||
|
{% for objective in game.objectives %} |
||||||
|
<tr> |
||||||
|
<td>{{ objective.name }}</td> |
||||||
|
<td>{{ objective.latitude }}</td> |
||||||
|
<td>{{ objective.longitude }}</td> |
||||||
|
<td>{{ 'Yes' if objective in current_user.player_in(game).found_objectives else 'No' }}</td> |
||||||
|
</tr> |
||||||
|
{% endfor %} |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
</div> |
||||||
|
{% endif %} |
||||||
|
|
||||||
|
</div> |
||||||
|
<div class="col-xs-12 col-md-3"> |
||||||
|
{% include '_game_player_info.html' %} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
{% if game.objectives %} |
||||||
|
<div class="row"> |
||||||
|
<div class="col-xs-1 col-md-1"></div> |
||||||
|
<div id="map" style=" height: 500px; border-radius: 10px; " class="col-xs-10 col-md-10"></div> |
||||||
|
<div class="col-xs-1 col-md-1"></div> |
||||||
|
</div> |
||||||
|
{% endif %} |
||||||
|
|
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block scripts %} |
||||||
|
{{ super() }} |
||||||
|
<script src="{{ url_for('static', filename='assets/leaflet/utils.js') }}"></script> |
||||||
|
<script type="text/javascript", crossorigin="anonymous"> |
||||||
|
// Leaflet Map |
||||||
|
map = getMap() |
||||||
|
markers = [] |
||||||
|
var objectives = JSON.parse('{{ current_user.player_in(game).encode_objectives() |safe }}') |
||||||
|
for (var i = 0; i < objectives.length; i++){ |
||||||
|
markers.push(addObjectiveMarker(map, objectives[i])) |
||||||
|
} |
||||||
|
|
||||||
|
function updateSelf() { |
||||||
|
getPosition(updateMyLocation) |
||||||
|
} |
||||||
|
setInterval(updateSelf, 10 * 1000); |
||||||
|
updateSelf() |
||||||
|
|
||||||
|
if (markers.length > 1) { |
||||||
|
map.fitBounds(markers.map(m => m.getLatLng())); |
||||||
|
} else if (markers.length == 1){ |
||||||
|
map.setView(markers[0].getLatLng(), 10); |
||||||
|
} |
||||||
|
|
||||||
|
</script> |
||||||
|
{% endblock %} |
@ -1,137 +0,0 @@ |
|||||||
{% extends 'base.html' %} |
|
||||||
|
|
||||||
{% block head %} |
|
||||||
{{ super() }} |
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='assets/leaflet/leaflet.css') }}" /> |
|
||||||
<script src="{{ url_for('static', filename='assets/leaflet/leaflet.js') }}"></script> |
|
||||||
{% endblock %} |
|
||||||
|
|
||||||
{% block app_content %} |
|
||||||
<h1>{{ game.name }} Dashboard</h1> |
|
||||||
|
|
||||||
<h2>Players:</h2> |
|
||||||
<p><a href="{{ url_for('main.add_player', game_name = game.name) }}">Add player</a></p> |
|
||||||
<div class="table-responsive"> |
|
||||||
<table class="table"> |
|
||||||
<thead> |
|
||||||
<tr> |
|
||||||
<th scope="col">Player Name</th> |
|
||||||
<th scope="col">Role</th> |
|
||||||
<th scope="col">Objectives found</th> |
|
||||||
<th scope="col">Bunnies Caught</th> |
|
||||||
<th scope="col">Been Caught</th> |
|
||||||
<th scope="col">Last location</th> |
|
||||||
<th scope="col"></th> |
|
||||||
</tr> |
|
||||||
</thead> |
|
||||||
<tbody> |
|
||||||
{% for player in game.players %} |
|
||||||
<tr> |
|
||||||
<td><a href="{{ url_for('main.game_player', game_name = game.name, player_name = player.name) }}">{{ player.name }}</a></td> |
|
||||||
{% for gameplayer in player.player_games if gameplayer.game == game %} |
|
||||||
<td>{{ gameplayer.role.name }}</td> |
|
||||||
{% endfor %} |
|
||||||
<td>{{ player.found_objectives | selectattr('game', '==', game)|list|length}}</td> |
|
||||||
<td>{{ player.caught_players | selectattr('game', '==', game)|list|length}}</td> |
|
||||||
<td>{{ player.caught_by_players | selectattr('game', '==', game)|list|length}}</td> |
|
||||||
<td>{% with location = player.last_location(game) %} |
|
||||||
{% if location %}{{ moment(location.timestamp).fromNow()}}: {% endif %} |
|
||||||
{{ location }} |
|
||||||
{% endwith %}</td> |
|
||||||
<td><a href="{{ url_for('main.remove_player', game_name=game.name, player_name=player.name) }}"> |
|
||||||
<button class="btn btn-danger">Delete</button></a> |
|
||||||
</td> |
|
||||||
</tr> |
|
||||||
{% endfor %} |
|
||||||
</tbody> |
|
||||||
</table> |
|
||||||
</div> |
|
||||||
<h2>Objectives:</h2> |
|
||||||
<p><a href="{{ url_for('main.add_objective', game_name = game.name) }}">Add new objective</a></p> |
|
||||||
{% if game.objectives %} |
|
||||||
<div class="table-responsive"> |
|
||||||
<table class="table"> |
|
||||||
<thead> |
|
||||||
<tr> |
|
||||||
<th scope="col">Objective Name</th> |
|
||||||
<th scope="col">Latitude</th> |
|
||||||
<th scope="col">Longitude</th> |
|
||||||
<th scope="col">Times found</th> |
|
||||||
<th scope="col">Hash</th> |
|
||||||
<th scope="col"></th> |
|
||||||
</tr> |
|
||||||
</thead> |
|
||||||
<tbody> |
|
||||||
{% for objective in game.objectives %} |
|
||||||
<tr> |
|
||||||
<td>{{ objective.name }}</td> |
|
||||||
<td>{{ objective.latitude }}</td> |
|
||||||
<td>{{ objective.longitude }}</td> |
|
||||||
<td>{{ objective.found_by|length }}</td> |
|
||||||
<td><a href="{{ url_for('main.objective', objective_hash = objective.hash) }}">{{ objective.hash }}</a></td> |
|
||||||
<td><a href="{{ url_for('main.delete_objective', objective_hash = objective.hash) }}"> |
|
||||||
<button class="btn btn-danger">Delete</button></a> |
|
||||||
</td> |
|
||||||
</tr> |
|
||||||
{% endfor %} |
|
||||||
</tbody> |
|
||||||
</table> |
|
||||||
</div> |
|
||||||
<div id="map" style=" height: 500px; border-radius: 10px; " class="col-md-6 col-xs-12"></div> |
|
||||||
{% endif %} |
|
||||||
|
|
||||||
|
|
||||||
{% endblock %} |
|
||||||
|
|
||||||
{% block scripts %} |
|
||||||
{{ super() }} |
|
||||||
{{ moment.include_moment() }} |
|
||||||
<script type="text/javascript", crossorigin="anonymous"> |
|
||||||
// Leaflet Map |
|
||||||
var map = L.map( 'map', { |
|
||||||
center: [52.2, 5.3], |
|
||||||
minZoom: 6, |
|
||||||
maxZoom: 19, |
|
||||||
bounds: [[50.5, 3.25], [54, 7.6]], |
|
||||||
zoom: 8 |
|
||||||
}); |
|
||||||
L.control.scale().addTo(map); |
|
||||||
|
|
||||||
L.tileLayer( 'https://geodata.nationaalgeoregister.nl/tiles/service/wmts/brtachtergrondkaartpastel/EPSG:3857/{z}/{x}/{y}.png', { |
|
||||||
attribution: 'Kaartgegevens © <a href="kadaster.nl">Kadaster</a>' |
|
||||||
}).addTo( map ); |
|
||||||
|
|
||||||
var objectives = JSON.parse('{{ json.dumps(game.objectives, cls=objective_encoder)|safe }}') |
|
||||||
|
|
||||||
for (var i = 0; i < objectives.length; i++){ |
|
||||||
var objectiveMarker = L.marker([ |
|
||||||
objectives[i]['latitude'], |
|
||||||
objectives[i]['longitude'] |
|
||||||
]).addTo(map); |
|
||||||
objectiveMarker.bindTooltip(`<b>${objectives[i]['name']}</b><br> |
|
||||||
${objectives[i]['hash']}`).openPopup(); |
|
||||||
} |
|
||||||
|
|
||||||
var greenIcon = new L.Icon({ |
|
||||||
iconUrl: "{{ url_for('static', filename='assets/leaflet/images/marker-icon-2x-green.png') }}", |
|
||||||
shadowUrl: "{{ url_for('static', filename='assets/leaflet/images/marker-shadow.png') }}", |
|
||||||
iconSize: [25, 41], |
|
||||||
iconAnchor: [12, 41], |
|
||||||
popupAnchor: [1, -34], |
|
||||||
shadowSize: [41, 41] |
|
||||||
}); |
|
||||||
|
|
||||||
var players = JSON.parse('{{ json.dumps(game.last_player_locations(), cls=location_encoder)|safe }}') |
|
||||||
for (var i = 0; i < players.length; i++){ |
|
||||||
var playerMarker = L.marker([ |
|
||||||
players[i]['latitude'], |
|
||||||
players[i]['longitude'] |
|
||||||
], {icon: greenIcon}).addTo(map); |
|
||||||
var timestamp_utc = moment.utc(players[i]['timestamp_utc']).toDate() |
|
||||||
var timestamp_local = moment(timestamp_utc).local().format('YYYY-MM-DD HH:mm'); |
|
||||||
playerMarker.bindTooltip(`<b>${players[i]['player_name']}</b><br> |
|
||||||
${timestamp_local}`).openPopup(); |
|
||||||
} |
|
||||||
|
|
||||||
</script> |
|
||||||
{% endblock %} |
|
@ -0,0 +1,141 @@ |
|||||||
|
{% extends 'player_base.html' %} |
||||||
|
|
||||||
|
{% block head %} |
||||||
|
{{ super() }} |
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='assets/leaflet/leaflet.css') }}" /> |
||||||
|
<script src="{{ url_for('static', filename='assets/leaflet/leaflet.js') }}"></script> |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block player_app_content %} |
||||||
|
<div class="row"> |
||||||
|
<div class="col-xs-0 col-md-1"></div> |
||||||
|
|
||||||
|
<div class="col-xs-12 col-md-7"> |
||||||
|
<h2>Bunnies</h2> |
||||||
|
<div class="table"> |
||||||
|
<table class="table"> |
||||||
|
<thead> |
||||||
|
<tr> |
||||||
|
<th scope="col">Player Name</th> |
||||||
|
<th scope="col">Times Caught</th> |
||||||
|
<th scope="col">Last location</th> |
||||||
|
<th scope="col"></th> |
||||||
|
</tr> |
||||||
|
</thead> |
||||||
|
<tbody> |
||||||
|
{% set player = current_user.player_in(game) %} |
||||||
|
{% for bunny in game.bunnies() %} |
||||||
|
<tr> |
||||||
|
<td>{{ bunny.user.name }}</td> |
||||||
|
<td><span |
||||||
|
style="color:green;">{{ bunny.player_caught_by_players | selectattr('catching_player', '==', player) | selectattr('review.name', '==', 'accepted') |list|length}}</span> |
||||||
|
/ |
||||||
|
<span |
||||||
|
style="color:red;">{{ bunny.player_caught_by_players | selectattr('catching_player', '==', player) | selectattr('review.name', '==', 'denied') |list|length}}</span> |
||||||
|
/ |
||||||
|
<span |
||||||
|
style="color:gray;">{{ bunny.player_caught_by_players | selectattr('catching_player', '==', player) | selectattr('review.name', '==', 'none') |list|length}}</span> |
||||||
|
</td> |
||||||
|
<td> |
||||||
|
<p id='last_location_{{ bunny.user.name }}'> |
||||||
|
{% with location = bunny.last_location(offset=hunter_delay) %} |
||||||
|
{% if location %}{{ moment(location.timestamp).fromNow()}} |
||||||
|
{% else %} |
||||||
|
{{ location }} |
||||||
|
{% endif %} |
||||||
|
{% endwith %} |
||||||
|
</p> |
||||||
|
</td> |
||||||
|
<td> |
||||||
|
<a |
||||||
|
href="{{ url_for('main.catch_bunny', game_name=game.name, bunny_name=bunny.user.name) }}"> |
||||||
|
<button class="btn btn-success btn-sm">Catch</button> |
||||||
|
</a> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
{% endfor %} |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
<span style="font-size: smaller;"> |
||||||
|
(<span style="color:green;">Accepted</span>/<span style="color:red;">Denied</span>/<span |
||||||
|
style="color:gray;">Not |
||||||
|
reviewed</span>) |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="col-xs-12 col-md-3"> |
||||||
|
{% include '_game_player_info.html' %} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="row"> |
||||||
|
<div class="col-xs-1 col-md-1"></div> |
||||||
|
<div id="map" style=" height: 500px; border-radius: 10px; " class="col-xs-10 col-md-10"></div> |
||||||
|
<div class="col-xs-1 col-md-1"></div> |
||||||
|
</div> |
||||||
|
|
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block scripts %} |
||||||
|
{{ super() }} |
||||||
|
<script src="{{ url_for('static', filename='assets/leaflet/utils.js') }}"></script> |
||||||
|
<script type="text/javascript" , crossorigin="anonymous"> |
||||||
|
// Leaflet Map |
||||||
|
var map = getMap() |
||||||
|
var bunnieMarkers = [] |
||||||
|
var bunnies = JSON.parse( |
||||||
|
'{{ json.dumps(game.last_locations(game.bunnies(), offset=hunter_delay), cls=location_encoder)|safe }}') |
||||||
|
updateBunnieMarkers() |
||||||
|
|
||||||
|
if (bunnieMarkers.length > 1) { |
||||||
|
map.fitBounds(bunnieMarkers.map(m => m.getLatLng())); |
||||||
|
} else if (bunnieMarkers.length == 1){ |
||||||
|
map.setView(bunnieMarkers[0].getLatLng(), 10); |
||||||
|
} |
||||||
|
|
||||||
|
function updateBunnieMarkers(){ |
||||||
|
if(bunnieMarkers != undefined){ |
||||||
|
bunnieMarkers.forEach(function(marker){ |
||||||
|
marker.remove() |
||||||
|
}); |
||||||
|
} |
||||||
|
bunnieMarkers = [] |
||||||
|
for (var i = 0; i < bunnies.length; i++) { |
||||||
|
bunnieMarkers.push(addPlayerMarker(map, bunnies[i], greenPlayerIcon)) |
||||||
|
// Update table lastlocation column |
||||||
|
$('#last_location_' + bunnies[i].username)[0].innerHTML = toMomentLocal(bunnies[i].timestamp_utc).fromNow() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Poll Locations |
||||||
|
usernames = JSON.parse('{{ json.dumps(game.usernames())|safe }}').filter(name => name != '{{ current_user.name }}') |
||||||
|
setInterval(function() { |
||||||
|
pollLocations( |
||||||
|
"{{ url_for('main.poll_locations', game_name=game.name) }}", |
||||||
|
usernames, |
||||||
|
'accumulative', |
||||||
|
bunnies, |
||||||
|
handleResponse |
||||||
|
) |
||||||
|
getPosition(updateMyLocation) |
||||||
|
}, 30 * 1000); |
||||||
|
|
||||||
|
function handleResponse(data){ |
||||||
|
data.forEach(function (location) { |
||||||
|
bunnie = bunnies.filter(function (bunnie) { |
||||||
|
return bunnie.username == location.username; |
||||||
|
})[0]; |
||||||
|
if (new Date(location.timestamp_utc) > new Date(bunnie.timestamp_utc)){ |
||||||
|
//lastLocation = bunnie[bunnie.length-1] Not necesary because there is just one of each bunnie |
||||||
|
if (bunnie.latitude == location.latitude && bunnie.longitude == location.longitude){ |
||||||
|
bunnie.timestamp_utc = location.timestamp_utc |
||||||
|
} else{ |
||||||
|
bunnies = bunnies.filter(p => p !== bunnie) |
||||||
|
bunnies.push(location) |
||||||
|
} |
||||||
|
} |
||||||
|
}); |
||||||
|
updateBunnieMarkers() |
||||||
|
} |
||||||
|
</script> |
||||||
|
{% endblock %} |
@ -0,0 +1,183 @@ |
|||||||
|
{% extends 'base.html' %} |
||||||
|
|
||||||
|
{% block head %} |
||||||
|
{{ super() }} |
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='assets/leaflet/leaflet.css') }}" /> |
||||||
|
<script src="{{ url_for('static', filename='assets/leaflet/leaflet.js') }}"></script> |
||||||
|
<script src="{{ url_for('static', filename='assets/geolocation_utils.js') }}"></script> |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block app_content %} |
||||||
|
<meta name="csrf-token" content="{{ csrf_token() }}"> |
||||||
|
<h1>{{ game.name }} Dashboard</h1> |
||||||
|
<a href="{{ url_for('game.change_game_settings', game_name=game.name) }}"> |
||||||
|
<button class="btn btn-primary">Change Game Settings</button> |
||||||
|
</a> |
||||||
|
{% if game.unreviewed_bunny_photos() %} |
||||||
|
<a href="{{ url_for('main.review_caught_bunny_photos', game_name=game.name) }}"> |
||||||
|
<button class="btn btn-primary">Review Bunny Photos</button> |
||||||
|
</a> |
||||||
|
{% endif %} |
||||||
|
<br><br> |
||||||
|
<p><b>Start Time: </b>{% if game.start_time %}{{ moment(game.start_time).format('DD-MM-YYYY, HH:mm') }}{% else %}None{% endif %}</p> |
||||||
|
<p><b>End Time: </b>{% if game.end_time %}{{ moment(game.end_time).format('DD-MM-YYYY, HH:mm') }}{% else %}None{% endif %}</p> |
||||||
|
<p><b>State: </b>{{ game.get_state().name.title() }}</p> |
||||||
|
<h2>Players:</h2> |
||||||
|
<p><a href="{{ url_for('main.add_player', game_name = game.name) }}">Add player</a></p> |
||||||
|
<div class="table-responsive"> |
||||||
|
<table class="table"> |
||||||
|
<thead> |
||||||
|
<tr> |
||||||
|
<th scope="col">Player Name</th> |
||||||
|
<th scope="col">Role</th> |
||||||
|
<th scope="col">Objectives found</th> |
||||||
|
<th scope="col">Bunnies Caught</th> |
||||||
|
<th scope="col">Been Caught</th> |
||||||
|
<th scope="col">Last location</th> |
||||||
|
<th scope="col"></th> |
||||||
|
</tr> |
||||||
|
</thead> |
||||||
|
<tbody> |
||||||
|
{% for player in game.players %} |
||||||
|
<tr> |
||||||
|
<td><a href="{{ url_for('main.game_player', game_name = game.name, username = player.user.name) }}">{{ player.user.name }}</a></td> |
||||||
|
<td>{{ player.role.name }}</td> |
||||||
|
<td>{{ player.found_objectives | list | length }}</td> |
||||||
|
<td>{{ player.accepted_caught_players() | list | length }}</td> |
||||||
|
<td>{{ player.accepted_caught_by_players() | list | length }}</td> |
||||||
|
<td> |
||||||
|
<p id='last_location_{{ player.user.name }}'> |
||||||
|
{% with location = player.last_location() %} |
||||||
|
{% if location %}{{ moment(location.timestamp).fromNow()}} |
||||||
|
{% else %} |
||||||
|
{{ location }} |
||||||
|
{% endif %} |
||||||
|
{% endwith %} |
||||||
|
</p> |
||||||
|
</td> |
||||||
|
<td><a href="{{ url_for('main.remove_player', game_name=game.name, username=player.user.name) }}"> |
||||||
|
<button class="btn btn-danger">Delete</button></a> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
{% endfor %} |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
</div> |
||||||
|
<h2>Objectives:</h2> |
||||||
|
<p><a href="{{ url_for('main.add_objective', game_name = game.name) }}">Add new objective</a></p> |
||||||
|
{% if game.objectives %} |
||||||
|
<div class="table-responsive"> |
||||||
|
<table class="table"> |
||||||
|
<thead> |
||||||
|
<tr> |
||||||
|
<th scope="col">Objective Name</th> |
||||||
|
<th scope="col">Latitude</th> |
||||||
|
<th scope="col">Longitude</th> |
||||||
|
<th scope="col">Times found</th> |
||||||
|
<th scope="col">Hash</th> |
||||||
|
<th scope="col"></th> |
||||||
|
</tr> |
||||||
|
</thead> |
||||||
|
<tbody> |
||||||
|
{% for objective in game.objectives %} |
||||||
|
<tr> |
||||||
|
<td>{{ objective.name }}</td> |
||||||
|
<td>{{ objective.latitude }}</td> |
||||||
|
<td>{{ objective.longitude }}</td> |
||||||
|
<td>{{ objective.found_by|length }}</td> |
||||||
|
<td><a href="{{ url_for('main.objective', objective_hash = objective.hash) }}">{{ objective.hash }}</a></td> |
||||||
|
<td><a href="{{ url_for('main.delete_objective', objective_hash = objective.hash) }}"> |
||||||
|
<button class="btn btn-danger">Delete</button></a> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
{% endfor %} |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
</div> |
||||||
|
<div id="map" style=" height: 500px; border-radius: 10px; " class="col-md-6 col-xs-12"></div> |
||||||
|
{% endif %} |
||||||
|
|
||||||
|
|
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block scripts %} |
||||||
|
{{ super() }} |
||||||
|
<script src="{{ url_for('static', filename='assets/leaflet/utils.js') }}"></script> |
||||||
|
<script type="text/javascript", crossorigin="anonymous"> |
||||||
|
// Leaflet Map |
||||||
|
var map = getMap() |
||||||
|
var objectiveMarkers = [] |
||||||
|
var playerMarkers = [] |
||||||
|
|
||||||
|
var objectives = JSON.parse('{{ json.dumps(game.objectives, cls=objective_encoder)|safe }}') |
||||||
|
|
||||||
|
for (var i = 0; i < objectives.length; i++){ |
||||||
|
marker = addObjectiveMarker(map, objectives[i]) |
||||||
|
marker.on("click", function (e) { |
||||||
|
var caller = e.target || e.srcElement; |
||||||
|
window.location = "{{ url_for('main.objective', objective_hash = 'ObjectiveHash') }}".replace('ObjectiveHash', caller.hash); |
||||||
|
}); |
||||||
|
objectiveMarkers.push(marker) |
||||||
|
} |
||||||
|
|
||||||
|
var players = JSON.parse('{{ json.dumps(game.last_player_locations(), cls=location_encoder)|safe }}') |
||||||
|
updatePlayerMarkers() |
||||||
|
|
||||||
|
if (objectiveMarkers.length + playerMarkers.length > 1) { |
||||||
|
map.fitBounds(objectiveMarkers.concat(playerMarkers).map(m => m.getLatLng())); |
||||||
|
} |
||||||
|
|
||||||
|
getPosition(updateMyLocation); |
||||||
|
|
||||||
|
function updatePlayerMarkers(){ |
||||||
|
if(playerMarkers != undefined){ |
||||||
|
playerMarkers.forEach(function(marker){ |
||||||
|
marker.remove() |
||||||
|
}); |
||||||
|
} |
||||||
|
playerMarkers = [] |
||||||
|
for (var i = 0; i < players.length; i++) { |
||||||
|
marker = addPlayerMarker(map, players[i], goldPlayerIcon) |
||||||
|
marker.on("click", function (e) { |
||||||
|
var caller = e.target || e.srcElement; |
||||||
|
window.location = "{{ url_for('main.game_player', game_name = game.name, username = 'Username') }}".replace('Username', caller.username); |
||||||
|
}); |
||||||
|
playerMarkers.push(marker); |
||||||
|
// Update table lastlocation column |
||||||
|
$('#last_location_' + players[i].username)[0].innerHTML = toMomentLocal(players[i].timestamp_utc).fromNow(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Poll Locations |
||||||
|
usernames = JSON.parse('{{ json.dumps(game.usernames())|safe }}').filter(name => name != '{{ current_user.name }}') |
||||||
|
setInterval(function() { |
||||||
|
pollLocations( |
||||||
|
"{{ url_for('main.poll_locations', game_name=game.name) }}", |
||||||
|
usernames, |
||||||
|
'accumulative', |
||||||
|
players, |
||||||
|
handleResponse |
||||||
|
) |
||||||
|
getPosition(updateMyLocation) |
||||||
|
}, 30 * 1000); |
||||||
|
|
||||||
|
function handleResponse(data){ |
||||||
|
data.forEach(function (location) { |
||||||
|
player = players.filter(function (player) { |
||||||
|
return player.username == location.username; |
||||||
|
})[0]; |
||||||
|
if (new Date(location.timestamp_utc) > new Date(player.timestamp_utc)){ |
||||||
|
//lastLocation = player[player.length-1] Not necesary because there is just one of each player |
||||||
|
if (player.latitude == location.latitude && player.longitude == location.longitude){ |
||||||
|
player.timestamp_utc = location.timestamp_utc |
||||||
|
} else{ |
||||||
|
players = players.filter(p => p !== player) |
||||||
|
players.push(location) |
||||||
|
} |
||||||
|
} |
||||||
|
}); |
||||||
|
updatePlayerMarkers() |
||||||
|
} |
||||||
|
|
||||||
|
</script> |
||||||
|
{% endblock %} |
@ -0,0 +1,164 @@ |
|||||||
|
{% extends "base.html" %} |
||||||
|
{% import 'bootstrap/wtf.html' as wtf %} |
||||||
|
|
||||||
|
{% block head %} |
||||||
|
{{ super() }} |
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='assets/leaflet/leaflet.css') }}" /> |
||||||
|
<script src="{{ url_for('static', filename='assets/leaflet/leaflet.js') }}"></script> |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block app_content %} |
||||||
|
<meta name="csrf-token" content="{{ csrf_token() }}"> |
||||||
|
<h1>Player: {{ player.user.name }}</h1> |
||||||
|
<hr> |
||||||
|
<div class="row"> |
||||||
|
<div class="col-md-4 col-sm-6 col-xs-8"> |
||||||
|
<div class="row"> |
||||||
|
{{ wtf.quick_form(form, button_map={'submit': 'primary'}) }} |
||||||
|
</div> |
||||||
|
|
||||||
|
{% if player.user.auth_hash and not player.user.password_hash %} |
||||||
|
<div class="row"> |
||||||
|
<a href="{{ url_for('auth.user_hash_login', auth_hash=player.user.auth_hash) }}"> |
||||||
|
<img src="{{ url_for('auth.user_qrcode', auth_hash=player.user.auth_hash) }}" alt="qr_code_failed" width="80%" title="login code for {{ player.user.name }}"> |
||||||
|
</a> |
||||||
|
</div> |
||||||
|
{% elif not player.user.password_hash %} |
||||||
|
<br> |
||||||
|
<div class="row"> |
||||||
|
<a href="#" , id="generate_auth_hash"> |
||||||
|
<button class="btn btn-success">Generate Login Code</button></a> |
||||||
|
</div> |
||||||
|
{% endif %} |
||||||
|
</div> |
||||||
|
<div id="map" style=" height: 400px; border-radius: 10px; " class="col-md-6 col-xs-12"></div> |
||||||
|
</div> |
||||||
|
{% if player.caught_players %} |
||||||
|
<div class="row"> |
||||||
|
<h2>Caught Players:</h2> |
||||||
|
<div class="table-responsive"> |
||||||
|
<table class="table"> |
||||||
|
<thead> |
||||||
|
<tr> |
||||||
|
<th scope="col">Player Name</th> |
||||||
|
<th scope="col">Review</th> |
||||||
|
<th scope="col">Time</th> |
||||||
|
<th scope="col"></th> |
||||||
|
</tr> |
||||||
|
</thead> |
||||||
|
<tbody> |
||||||
|
{% for pcp in player.player_caught_players %} |
||||||
|
<tr> |
||||||
|
<td><a |
||||||
|
href="{{ url_for('main.game_player', game_name=player.game.name, username = pcp.caught_player.user.name) }}">{{ pcp.caught_player.user.name }}</a> |
||||||
|
</td> |
||||||
|
<td>{{ pcp.review.name.title() }}</td> |
||||||
|
<td>{{ moment(pcp.timestamp).fromNow() }}</td> |
||||||
|
<td><a href="{{ url_for('main.caught_bunny_photo', |
||||||
|
game_name=player.game.name, |
||||||
|
timestamp=pcp.timestamp.strftime('%Y%m%d%H%M%S'), |
||||||
|
bunny_name=pcp.caught_player.user.name, |
||||||
|
hunter_name=pcp.catching_player.user.name) }}"> |
||||||
|
<button class="btn btn-primary">Photo</button></a> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
{% endfor %} |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{% endif %} |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
|
||||||
|
{% block scripts %} |
||||||
|
{{ super() }} |
||||||
|
<script src="{{ url_for('static', filename='assets/leaflet/utils.js') }}"></script> |
||||||
|
<script type="text/javascript" , crossorigin="anonymous"> |
||||||
|
// Leaflet Map |
||||||
|
map = getMap() |
||||||
|
|
||||||
|
var locations = JSON.parse('{{ json.dumps(player.locations_during_game(), cls=location_encoder)|safe }}') |
||||||
|
if (locations == null) { |
||||||
|
locations = []; |
||||||
|
} |
||||||
|
|
||||||
|
var markers = []; |
||||||
|
var lastMarker; |
||||||
|
updateMarkers(); |
||||||
|
var polyline; |
||||||
|
updatePolyline(); |
||||||
|
|
||||||
|
// zoom the map to the polyline |
||||||
|
if(polyline != undefined){ |
||||||
|
map.fitBounds(polyline.getBounds(), { |
||||||
|
maxZoom : 13 |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
function updateMarkers(){ |
||||||
|
if(markers != undefined){ |
||||||
|
markers.forEach(function(marker){ |
||||||
|
marker.remove() |
||||||
|
}); |
||||||
|
markers = [] |
||||||
|
} |
||||||
|
if(locations.length == 0){ return } |
||||||
|
for (var i = 0; i < locations.length -1; i++) { |
||||||
|
markers.push(addPlayerMarker(map, locations[i], bluePlayerIconMini)) |
||||||
|
} |
||||||
|
if(lastMarker != undefined){ |
||||||
|
lastMarker.remove() |
||||||
|
} |
||||||
|
lastMarker = addPlayerMarker(map, locations[locations.length-1], bluePlayerIcon) |
||||||
|
} |
||||||
|
|
||||||
|
function updatePolyline(){ |
||||||
|
if(polyline != undefined){ |
||||||
|
map.removeLayer(polyline) |
||||||
|
} |
||||||
|
if (locations.length == 0) { return } |
||||||
|
polyline = L.polyline(locations.map(l => [l.latitude, l.longitude]), { |
||||||
|
color: 'blue', |
||||||
|
opacity: 0.6, |
||||||
|
}).addTo(map); |
||||||
|
} |
||||||
|
|
||||||
|
// Poll Locations |
||||||
|
|
||||||
|
setInterval(function() { |
||||||
|
pollLocations( |
||||||
|
"{{ url_for('main.poll_locations', game_name=player.game.name) }}", |
||||||
|
['{{ player.user.name }}'], |
||||||
|
'accumulative', |
||||||
|
locations, |
||||||
|
handleResponse |
||||||
|
)}, 30 * 1000); |
||||||
|
|
||||||
|
function handleResponse(data){ |
||||||
|
data.forEach(function (location) { |
||||||
|
if (location.username == '{{ player.user.name }}' && new Date(location.timestamp_utc) > get_latest_date(locations) ){ |
||||||
|
lastLocation = locations[locations.length-1] |
||||||
|
if (lastLocation.latitude == location.latitude && lastLocation.longitude == location.longitude){ |
||||||
|
lastLocation.timestamp_utc = location.timestamp_utc |
||||||
|
} else{ |
||||||
|
locations.push(location) |
||||||
|
} |
||||||
|
} |
||||||
|
}); |
||||||
|
updatePolyline() |
||||||
|
updateMarkers() |
||||||
|
} |
||||||
|
//Ajax for Generate Code button |
||||||
|
$(function () { |
||||||
|
$('a#generate_auth_hash').bind('click', function () { |
||||||
|
$.ajax({ |
||||||
|
url: "{{ url_for('auth.generate_auth_hash', username=player.user.name) }}", |
||||||
|
success: function (result) { |
||||||
|
location.reload(); |
||||||
|
} |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
||||||
|
</script> |
||||||
|
{% endblock %} |
@ -0,0 +1,96 @@ |
|||||||
|
{% extends 'base.html' %} |
||||||
|
{% import 'bootstrap/wtf.html' as wtf %} |
||||||
|
|
||||||
|
{% block head %} |
||||||
|
{{ super() }} |
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='assets/bootstrap/bootstrap-datetimepicker.min.css') }}"> |
||||||
|
<script src="{{ url_for('static', filename='assets/utils.js') }}"></script> |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block app_content %} |
||||||
|
<h1>{{ title }}</h1> |
||||||
|
|
||||||
|
<div class="col-md-5 col-sm-6 col-xs-8"> |
||||||
|
<hr> |
||||||
|
<form action="" method="post" class="form" role="form"> |
||||||
|
{{ form.hidden_tag() }} |
||||||
|
{{ form.timezone }} |
||||||
|
{{ wtf.form_field(form.game_name, class='form-control') }} |
||||||
|
|
||||||
|
{{ form.start_time.label }} |
||||||
|
<div class="form-group row"> |
||||||
|
<div class="col-sm-7"> |
||||||
|
{{ form.start_time }} |
||||||
|
</div> |
||||||
|
<div class="col-sm-5 align-self-center"> |
||||||
|
{{ wtf.form_field(form.start_time_disabled, class='form-control') }} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
{{ form.end_time.label }} |
||||||
|
<div class="form-group row"> |
||||||
|
<div class="col-sm-7"> |
||||||
|
{{ form.end_time }} |
||||||
|
</div> |
||||||
|
<div class="col-sm-5 align-self-center"> |
||||||
|
{{ wtf.form_field(form.end_time_disabled, class='form-control') }} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{{ wtf.form_field(form.submit, class='btn btn-primary', value="Update") }} |
||||||
|
</form> |
||||||
|
<hr> |
||||||
|
{% if game.hidden %} |
||||||
|
<a href="{{ url_for('game.publish_game', game_name=game.name) }}"> |
||||||
|
<button class="btn btn-success">Publish Game</button> |
||||||
|
</a> |
||||||
|
{% else %} |
||||||
|
<a href="{{ url_for('game.hide_game', game_name=game.name) }}"> |
||||||
|
<button class="btn btn-success">Hide Game</button> |
||||||
|
</a> |
||||||
|
{% endif %} |
||||||
|
{% if game.paused %} |
||||||
|
<a href="{{ url_for('game.resume_game', game_name=game.name) }}"> |
||||||
|
<button class="btn btn-primary">Resume Game</button> |
||||||
|
</a> |
||||||
|
{% else %} |
||||||
|
<a href="{{ url_for('game.pause_game', game_name=game.name) }}"> |
||||||
|
<button class="btn btn-primary">Pause Game</button> |
||||||
|
</a> |
||||||
|
{% endif %} |
||||||
|
|
||||||
|
<button class="btn btn-danger" onclick="deleteGame()">Delete Game</button> |
||||||
|
</div> |
||||||
|
|
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block scripts %} |
||||||
|
{{ super() }} |
||||||
|
<!-- TODO: Scripts downloaden naar repo? --> |
||||||
|
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.18.1/moment-with-locales.min.js"></script> |
||||||
|
<script type="text/javascript" src="https://momentjs.com/downloads/moment-timezone-with-data.js"></script> |
||||||
|
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-datetimepicker/4.17.47/js/bootstrap-datetimepicker.min.js"></script> |
||||||
|
<script type="text/javascript"> |
||||||
|
// Datetime pickers |
||||||
|
$(function () { |
||||||
|
$('#timezone')[0].value = moment.tz.guess(); |
||||||
|
var date = moment() |
||||||
|
createDateTimePicker('#datetimepicker_start', "#start_time_disabled", date) |
||||||
|
date.add(1, 'hour'); |
||||||
|
createDateTimePicker('#datetimepicker_end', "#end_time_disabled", date) |
||||||
|
|
||||||
|
$("#datetimepicker_start").on("dp.change", function (e) { |
||||||
|
$('#datetimepicker_end').data("DateTimePicker").minDate(e.date); |
||||||
|
}); |
||||||
|
$("#datetimepicker_end").on("dp.change", function (e) { |
||||||
|
$('#datetimepicker_start').data("DateTimePicker").maxDate(e.date); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
// Delete Game button |
||||||
|
function deleteGame() { |
||||||
|
if (confirm("Are you sure you want to delete this game?")) { |
||||||
|
window.location.href = "{{ url_for('game.delete_game', game_name=game.name) }}" |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
{% endblock %} |
@ -1,88 +0,0 @@ |
|||||||
{% extends "base.html" %} |
|
||||||
{% import 'bootstrap/wtf.html' as wtf %} |
|
||||||
|
|
||||||
{% block head %} |
|
||||||
{{ super() }} |
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='assets/leaflet/leaflet.css') }}" /> |
|
||||||
<script src="{{ url_for('static', filename='assets/leaflet/leaflet.js') }}"></script> |
|
||||||
{% endblock %} |
|
||||||
|
|
||||||
{% block app_content %} |
|
||||||
<h1>Player: {{ player.name }}</h1> |
|
||||||
<hr> |
|
||||||
<div class="row"> |
|
||||||
<div class="col-md-4 col-sm-6 col-xs-8"> |
|
||||||
<div class="row"> |
|
||||||
<form action="" method="post" class="form" role="form"> |
|
||||||
{{ form.hidden_tag() }} |
|
||||||
{% for gameplayer in player.player_games if gameplayer.game == game %} |
|
||||||
{{ wtf.form_field(form.role, class='form-control') }} |
|
||||||
{% endfor %} |
|
||||||
{{ wtf.form_field(form.submit, class='btn btn-primary', value='Update') }} |
|
||||||
</form> |
|
||||||
</div> |
|
||||||
{% if player.auth_hash %} |
|
||||||
<div class="row"> |
|
||||||
<img src="{{ url_for('main.player_qrcode', auth_hash=player.auth_hash) }}" alt="qr_code_failed", width="100%"> |
|
||||||
</div> |
|
||||||
{% endif %} |
|
||||||
</div> |
|
||||||
<div id="map" style=" height: 600px; border-radius: 10px; " class="col-md-6 col-xs-12"></div> |
|
||||||
</div> |
|
||||||
{% endblock %} |
|
||||||
|
|
||||||
|
|
||||||
{% block scripts %} |
|
||||||
{{ super() }} |
|
||||||
{{ moment.include_moment() }} |
|
||||||
<script type="text/javascript", crossorigin="anonymous"> |
|
||||||
// Leaflet Map |
|
||||||
'{% set last_location = player.last_location(game) %}' |
|
||||||
var map = L.map( 'map', { |
|
||||||
center: ['{{ last_location.latitude or 52.2 }}', '{{ last_location.longitude or 5.3 }}'], |
|
||||||
minZoom: 6, |
|
||||||
maxZoom: 19, |
|
||||||
bounds: [[50.5, 3.25], [54, 7.6]], |
|
||||||
zoom: 9 |
|
||||||
}); |
|
||||||
L.control.scale().addTo(map); |
|
||||||
|
|
||||||
L.tileLayer( 'https://geodata.nationaalgeoregister.nl/tiles/service/wmts/brtachtergrondkaartpastel/EPSG:3857/{z}/{x}/{y}.png', { |
|
||||||
attribution: 'Kaartgegevens © <a href="kadaster.nl">Kadaster</a>' |
|
||||||
}).addTo( map ); |
|
||||||
|
|
||||||
var greenIcon = new L.Icon({ |
|
||||||
iconUrl: "{{ url_for('static', filename='assets/leaflet/images/marker-icon-2x-green.png') }}", |
|
||||||
shadowUrl: "{{ url_for('static', filename='assets/leaflet/images/marker-shadow.png') }}", |
|
||||||
iconSize: [25, 41], |
|
||||||
iconAnchor: [12, 41], |
|
||||||
popupAnchor: [1, -34], |
|
||||||
shadowSize: [41, 41] |
|
||||||
}); |
|
||||||
|
|
||||||
var locations = JSON.parse('{{ json.dumps(player.locations, cls=location_encoder)|safe }}') |
|
||||||
for (var i = 0; i < locations.length; i++){ |
|
||||||
var playerMarker = L.marker([ |
|
||||||
locations[i]['latitude'], |
|
||||||
locations[i]['longitude'] |
|
||||||
], {icon: greenIcon}).addTo(map); |
|
||||||
var timestamp_utc = moment.utc(locations[i]['timestamp_utc']).toDate() |
|
||||||
var timestamp_local = moment(timestamp_utc).local().format('YYYY-MM-DD HH:mm'); |
|
||||||
playerMarker.bindTooltip(`<b>${locations[i]['player_name']}</b><br> |
|
||||||
${timestamp_local}`).openPopup(); |
|
||||||
} |
|
||||||
|
|
||||||
var latlngs = [ |
|
||||||
[[45.51, -122.68], |
|
||||||
[37.77, -122.43], |
|
||||||
[34.04, -118.2]], |
|
||||||
[[40.78, -73.91], |
|
||||||
[41.83, -87.62], |
|
||||||
[32.76, -96.72]] |
|
||||||
]; |
|
||||||
var polyline = L.polyline(locations.map(l => [l.latitude, l.longitude]), {color: 'red'}).addTo(map); |
|
||||||
// zoom the map to the polyline |
|
||||||
map.fitBounds(polyline.getBounds()); |
|
||||||
|
|
||||||
</script> |
|
||||||
{% endblock %} |
|
@ -0,0 +1,41 @@ |
|||||||
|
{% extends 'base.html' %} |
||||||
|
|
||||||
|
{% block app_content %} |
||||||
|
<meta name="csrf-token" content="{{ csrf_token() }}"> |
||||||
|
{% block player_app_content %}{% endblock %} |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block scripts %} |
||||||
|
{{ super() }} |
||||||
|
<script src="{{ url_for('static', filename='assets/geolocation_utils.js') }}"></script> |
||||||
|
<script> |
||||||
|
$(document).ready(getPosition(postCoordinates)) |
||||||
|
|
||||||
|
setInterval(function() {getPosition(postCoordinates)}, 65 * 1000); |
||||||
|
|
||||||
|
function postCoordinates(position) { |
||||||
|
$.ajax({ |
||||||
|
type: "POST", |
||||||
|
url: "{{ url_for('main.send_location', username=current_user.name) }}", |
||||||
|
data:{ |
||||||
|
lat: position.coords.latitude, |
||||||
|
long: position.coords.longitude |
||||||
|
}, |
||||||
|
|
||||||
|
error: function(error) { |
||||||
|
console.log(error); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
var csrftoken = $('meta[name=csrf-token]').attr('content') |
||||||
|
$.ajaxSetup({ |
||||||
|
beforeSend: function(xhr, settings) { |
||||||
|
if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) { |
||||||
|
xhr.setRequestHeader("X-CSRFToken", csrftoken) |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
</script> |
||||||
|
{% endblock %} |
@ -0,0 +1,14 @@ |
|||||||
|
{% extends "base.html" %} |
||||||
|
|
||||||
|
{% block app_content %} |
||||||
|
<div class="col-md-1"></div> |
||||||
|
<div class="col-md-10"> |
||||||
|
<h1>Bunny Photo Review</h1> |
||||||
|
<hr> |
||||||
|
{% for pcp in game.unreviewed_bunny_photos() %} |
||||||
|
{% include '_review_photo.html' %} |
||||||
|
<hr> |
||||||
|
{% endfor %} |
||||||
|
</div> |
||||||
|
<div class="col.md-1"></div> |
||||||
|
{% endblock %} |
@ -0,0 +1,6 @@ |
|||||||
|
{% extends 'base.html' %} |
||||||
|
|
||||||
|
{% block app_content %} |
||||||
|
<h1>My Profile</h1> |
||||||
|
<a href="{{ url_for('auth.change_password') }}"><button class="btn btn-primary">Change Password</button></a> |
||||||
|
{% endblock %} |
@ -0,0 +1,62 @@ |
|||||||
|
import fnmatch |
||||||
|
from os import listdir |
||||||
|
from io import BytesIO |
||||||
|
from pathlib import Path |
||||||
|
import qrcode |
||||||
|
from flask import send_file, flash, abort, current_app |
||||||
|
from flask_login import current_user |
||||||
|
from werkzeug.utils import secure_filename |
||||||
|
from app.models import Game |
||||||
|
|
||||||
|
def generate_qr_code(url): |
||||||
|
qr = qrcode.QRCode( |
||||||
|
version=None, |
||||||
|
error_correction=qrcode.constants.ERROR_CORRECT_M, |
||||||
|
box_size=30, |
||||||
|
border=4, |
||||||
|
) |
||||||
|
qr.add_data(url) |
||||||
|
qr.make(fit=True) |
||||||
|
return qr.make_image(fill_color='black', back_color='white') |
||||||
|
|
||||||
|
def serve_pil_image(pil_img): |
||||||
|
# Source: https://stackoverflow.com/questions/7877282/how-to-send-image-generated-by-pil-to-browser |
||||||
|
img_io = BytesIO() |
||||||
|
pil_img.save(img_io, 'PNG', quality=70) |
||||||
|
img_io.seek(0) |
||||||
|
return send_file(img_io, mimetype='image/png') |
||||||
|
|
||||||
|
def flash_errors(form): |
||||||
|
"""Flashes form errors""" |
||||||
|
print('a') |
||||||
|
for field, errors in form.errors.items(): |
||||||
|
for error in errors: |
||||||
|
flash(u"Error in the %s field - %s" % ( |
||||||
|
getattr(form, field).label.text, |
||||||
|
error |
||||||
|
), 'error') |
||||||
|
|
||||||
|
def get_game_if_owner(game_name): |
||||||
|
game = Game.query.filter_by(name=game_name).first_or_404() |
||||||
|
if not game.owned_by(current_user): |
||||||
|
abort(403) |
||||||
|
return game |
||||||
|
|
||||||
|
def save_player_caught_player_photo(file_storage, game, pcp): |
||||||
|
timestamp = pcp.timestamp.strftime('%Y%m%d%H%M%S') |
||||||
|
extension = Path(file_storage.filename).suffix |
||||||
|
filename = secure_filename(f'{timestamp}_{pcp.catching_player.user.name}_caught_{pcp.caught_player.user.name}{extension}') |
||||||
|
path = get_caught_bunny_photo_directory(game) |
||||||
|
path.mkdir(parents=True, exist_ok=True) |
||||||
|
file_storage.save(path / filename) |
||||||
|
|
||||||
|
def get_bunny_photo_filename(directory, timestamp, hunter_name, bunny_name): |
||||||
|
filename = secure_filename(f'{timestamp}_{hunter_name}_caught_{bunny_name}') + '.*' |
||||||
|
matches = fnmatch.filter(listdir(directory), filename) |
||||||
|
return matches[0] if matches else '' |
||||||
|
|
||||||
|
def get_caught_bunny_photo_directory(game): |
||||||
|
return Path(current_app.root_path).parent / \ |
||||||
|
current_app.config['UPLOAD_FOLDER'] / \ |
||||||
|
secure_filename(game.name) / \ |
||||||
|
current_app.config['PLAYER_CAUGHT_PLAYER_PHOTO_DIR_NAME'] |
@ -0,0 +1 @@ |
|||||||
|
<mxfile host="office.sciuro.org" modified="2020-07-18T19:01:28.741Z" agent="Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0" etag="I9I46YIfR01vjbe-FLCA" version="12.8.6" type="device"><diagram id="C5RBs43oDa-KdzZeNtuy" name="Page-1">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</diagram></mxfile> |
After Width: | Height: | Size: 51 KiB |
@ -0,0 +1 @@ |
|||||||
|
<mxfile host="office.sciuro.org" modified="2020-07-18T22:23:35.254Z" agent="Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0" etag="b1UJ2q3xYko4xSObHrmu" version="12.8.6" type="device"><diagram id="C5RBs43oDa-KdzZeNtuy" name="Page-1">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</diagram></mxfile> |
After Width: | Height: | Size: 53 KiB |
@ -0,0 +1 @@ |
|||||||
|
<mxfile host="office.sciuro.org" modified="2020-07-21T20:35:49.343Z" agent="Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0" etag="G6lIIClnw-Nzaj8fEytL" version="12.8.6" type="device"><diagram id="C5RBs43oDa-KdzZeNtuy" name="Page-1">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</diagram></mxfile> |
After Width: | Height: | Size: 53 KiB |
@ -0,0 +1 @@ |
|||||||
|
<mxfile host="office.sciuro.org" modified="2020-07-18T18:44:20.958Z" agent="Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0" etag="xqK6JFdlF4PMrt82lYA8" version="12.8.6" type="device"><diagram id="C5RBs43oDa-KdzZeNtuy" name="Page-1">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</diagram></mxfile> |
After Width: | Height: | Size: 80 KiB |
@ -0,0 +1 @@ |
|||||||
|
<mxfile host="office.sciuro.org" modified="2020-07-18T22:25:24.207Z" agent="Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0" etag="wvjCfHbN-iwSZYwNJAo7" version="12.8.6" type="device"><diagram id="C5RBs43oDa-KdzZeNtuy" name="Page-1">7V3dd6K6Fv9rXOveh3ZB+PSxddrpnOn09Ezb2/a+oURlBokDONX5609QopBEBeQjKl19kBgDZO/92zv7I+kovcn8s29Nx9+QDd0OkOx5R/nUAUBWAehE/5K9WLUYqrFqGPmOHXfaNDw5f2DcKMWtM8eGQapjiJAbOtN04wB5HhyEqTbL99FHutsQuem7Tq0RZBqeBpbLtr46djhetZrA2LTfQWc0JneW9e7qm4lFOsdvEowtG30kmpSbjtLzEQpXnybzHnSjySPz8vpl8ere/9Q///VP8Mt6uf76/PC/i9Vgt3l+sn4FH3ph4aHDh7v537/AnfJn/vZg3t45QbC4UFZD/7bcWTxf8buGCzKB0MbzGV8iPxyjEfIs92bTeu2jmWfD6DYSvtr0uUdoihtl3PgDhuEiZg5rFiLcNA4nbvwt9OyriNT40kMeXLXcOq4bDzlEXnhrTRw34sEemvkO9PFDPsCP+Mt4ZFmNfjp3wrfoh5dafPVOboM/f5rHYy4vFvFFxgmOCRHgJxjAHbNK+NzyRzDc0S+e7GiGE8wak+8zRBMY+gvcwYeuFTq/0xxtxYIxWvfbEB9/iOmfgxdklhm+HT03BJgIITXcsi0x4I/ZZEread3FRz9hD7nIX763Ii3/ls+C5zrBX9HlmsGiiw2HLa8WCX57S3x+T/KhSEwpm2VzZfzTR+TgFwFSrFpUKf5JrFiAqqeHWD1p/KsNb2NiWotEt2nUIchxH1OiRGU14kZw1u94gCwxovTZmsBH11pEvErJVFpiPsZOCJ+m1pKYH1gpp6VjiPk2wZe2Bc3hgMex+sCE/WFeecnHdL+hH8L5Ti6Jv9UAnwrSx0Ypy1LcNk4oZFWqCu50hkb3aIA5GnlnSSFZSxFIkbtNEwhwhegsiaNoafGRCag1Rh1VZajzgEJn6JyxCJnCEUli6JDXgOMYR0VsugJG+TZLa2Nb8S2tLCbfPhtzu/1XIaPtN+HUjDac0hVqZaGySP7AMCaWsTDNNJbrjDz8eYBnD0+ych1JIsYX9yr+YuLY9oplYeD8sfrLoSIixTYhHle77mifcItr9aF7bQ1+jpbsnaDscPnHId5OSrOU3SmADIasHR7xU3eSPgUetlxIl0CndHQ8rUUNcdIFDYcBDBkql2AGa5pg+CMn0GeDRfvwJ4U+GzBq8Wcr3VWh8Mc0BWND6dIoogdbPszJh6YhFB/KrM38d/8HVgLRnc/RYNa7olnMrKXCQofrOtMA7ieKFUxXEY2hM48ISVMJmx1gwKWSrfd1TReFShq1+FR01jWg1kmkbgYiNe+nXuP22jWcDbebRNaswFp65ILvu9VVivO0bnqIknzEhkzdx+xSTFqFj1hi2PhFzFXRAdy4U34PXhRJl5JsghTpjMPWRMTflh5UpkIT1a2YWAuhA3Q3YgHb+Y0/jqKP1zPPW5B2fJvEV60ZESkepWnftZFBtEVSUTmXFk2qKK1kFbXxr2iappeCHppUj9pSlRxqq06/D8kREmbB3fp96hA5EsEXZLmtsuuEc3E7E/1Tits57XQmXCOw15kh+91sScpztI3oyHGtthFUv856V/a1qvce8esbbwBcrVGnXkuoRkWwD+RLRmeSsJWEZ+7ES0KhM3nshJgu86Ns3Ha9YGV1jCb9WZBBTvNIHS3UGjRtlSfUJugrul6lnOqUK5QkEyTk1OSIqV5Z+k0W7cmyelFK8DMEdmdIFs3FrEEAd2Y07ZXABM01Ds1J26HLFMrrtc6UJEOs3pxZprAD6fR6hxpoy3qnLIUPOJ60APoeL1sss/f+EByBMkYSg4cjXd1QrCpxRFaoPL4uR9/LtYZU2GTYRysIPpBvnz11uNZYvdQBDHWuZvhRgHRnBeOWQEbjBMpSo7NLDZfkUygaztoodpA95yFt7xfX19Lt8/PjV+k9fJXe++HrP+/9L1+JHbzfg5jVYK5HXcvkedasSbFcVnUtE3dkPJCmVuOeBKaefmC5hqga4AWHmQhKDzO35XhB1iDKiS8+NCrO2m147aGwBh279gjG1jT66DreTxrxhPHE1ul3KA6TOyVp/7Ima7ZfPThJJw2AossaJjup5mWNwhrOHN/ZMaj+HMzfGBOLtTYH3ZKYWFEbZmI2o+FwB3AbHDxAmkuWMJIqk5SwXWa3IO5n8tjJKBG+CZD6EHrRDcOzswJlLb3AUCR27VuvGcgufVszsG4zkOS+7S86E0uDylQ1JkmOyb9c7u4ZqGoNqgumL9sizoP0ZdZsGlmsbBqFTavoIeTbjmeF4oZZVt+QnZ9AldozrTyB1rTjWBGv6q360O4J44ZxpLjB5kI/OxOIiTKZCgsbNYGE0nh0SRVtMd4aFweBRFZ3l1gF2mqWredOM1N3G4QUydSVdN1I4csF2TBC3GTdrmhb1LTewFoAiCyiBUEg8tyMO3AYsRrDoqfuCjToCqamXYGqaIuZFiYOqjPP6lQUy07R2LyEc7FTSLZ+CTXbAAhvlajs5qA9axbB3LlpAo2qMVVBw5pAUwTTBIXcWqlMivN1amkgqx4o3alVKKVRJ3YikYZuKqWRrYygdyZI968mBZJMajLhfoxC9B/427GhN4D/ZSXoxP1umqmm6KDxdjeu1fGmsXkxRxBOmULfwe8P/egJHG+UApc6oiwmtY84d8ewegkp2sqk9aAepJEyh1kEW5qwYZazLODR6aIDzjkDteKDLpqDMw8+CCTnCagCubCqbIAoPaO45INUZDpVuKRyHsVIS1Yd56horFv24RyrehW69qtxVGMjdqJFkvN4aAXCuZLhSq8JhRQqeVimh6g4S1LnnEciGEPmKYMpWnIjECNTidkV8fV+z5FYdjp57uQe4IPBzI9ijQ7n2JwTdy8DSrE1XXmqi5ZsXUacERwRapQNE5mzrStRk7ltbGq3NiBRh7ru69+twSjX2Z27ziQKStChjCio3KVytWLFIG5QVGddTEVMLMouEM7iajPO82KnYCaWecwet9YjX5gNSTK9IGyoswVTT3AJ1ZhPzt3Olw3Wg1WroW+AUwAJuYWIJTHlxjRVITOfrk3WarDayRzt3hjrGwwCC89Ntn2xTt3tLoO0jd64291QTwG12jRoQk7luHBLT2+OSXz81eIWG2l6XuAFJLh9wZPmDditwk8elQSLBRpiZ0BlzMhNgdL5LvqNrPlPgkKSWgcktSXtW9FI1huHIzZn5GmMPjw8WLy313do2WdHJyBRh8c3vvW4KbYx21Zy5NEbZlZTtvy0uMOYkDUvn0KRU/CrwgeV3ii0cSA3RYvFt3blAfhwpIcImqwXn+M2S5h/reMswgQqL6H5eiuTE7w+x0xipoCqeZhn7fXzXU2Z9A5hdQrOh+W580E4u3u6+fUyf1Rfbobdi8PLrRtOmd2b/VbdeY3cCc16/ExF9SO5D5mmT0GQy/Ww7Jqj3Ur3xrMjoGhV7haV2zBwgEaAY5M+Y+TKnykXBI5GuFVa3STJvrd7OtuU7Q70Hd07W46nX/Mz99H2bd9dFgKx0fKnaB22Apwzhxa5RmjZdZJxgjjfkdvSBdRpLHLP+DjcYyP4lmzV2YpZjyrk9mssAZf7NGx9AFsbKkR9QI56gF3sXkY5AFMNINIeabt4kzp0mSF0Wrz3ATGVsGpBczjgYas+MGF/LxkP82nmiK3RB2DzgFiqE4i14wNialuHfa7yvW7wsspPdwFu/WVlB3FFlgKekwBm4rQvZVdtagdEIDows5XwX5bVxTaeM2foYAlfPgUD1CdegqBK9IKyvhIELp1YvzcL2gKf7nnIGd37z97OCpx1HceZ9mQoNFNkPUwMaNT58mQ71bLP3qYeWNXK3XiSy9Ayz217RBxdJKldjJPqs3oT65EWRaKkxTAvu8m/grJD5XqqSjWyo8hphV/2pq22PH98+T775aLuAP61+PQ27P0/W8Tj3gqi57xHo2irkCMMe3AMsmzCsj39VEkzRa2ZBlxK8uqsjggEeYBG4UoxPb+d7RvDKSoXojStrkkVIRP1wJpyEDLhSx+hMNkdw8L4G7Jh1ONf</diagram></mxfile> |
After Width: | Height: | Size: 80 KiB |
@ -0,0 +1 @@ |
|||||||
|
<mxfile host="office.sciuro.org" modified="2020-07-21T20:32:55.492Z" agent="Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0" etag="UGGaq7Di0W6oZH4bH7XM" version="12.8.6" type="device"><diagram id="C5RBs43oDa-KdzZeNtuy" name="Page-1">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</diagram></mxfile> |
After Width: | Height: | Size: 81 KiB |
@ -1,8 +1,8 @@ |
|||||||
Gamestate |
Gamestate |
||||||
|
|
||||||
This is an attribute of the Game class. It will be implemented as an enum containing the following values: |
This is an attribute of the Game class. It will be implemented as an enum containing the following values: |
||||||
initiated = 1 |
hidden = 1 |
||||||
published = 2 |
published = 2 |
||||||
started = 3 |
active = 3 |
||||||
interrupted = 4 |
paused = 4 |
||||||
finished = 5 |
finished = 5 |
||||||
|
@ -1,27 +1,40 @@ |
|||||||
alembic==1.4.2 |
alembic==1.4.2 |
||||||
|
astroid==2.4.2 |
||||||
click==7.1.2 |
click==7.1.2 |
||||||
|
coverage==5.2 |
||||||
|
dnspython==2.0.0 |
||||||
dominate==2.5.1 |
dominate==2.5.1 |
||||||
Flask==1.1.2 |
Flask==1.1.2 |
||||||
Flask-Bootstrap==3.3.7.1 |
Flask-Bootstrap==3.3.7.1 |
||||||
Flask-Login==0.5.0 |
Flask-Login==0.5.0 |
||||||
Flask-Migrate==2.5.3 |
Flask-Migrate==2.5.3 |
||||||
|
Flask-Moment==0.10.0 |
||||||
Flask-SQLAlchemy==2.4.3 |
Flask-SQLAlchemy==2.4.3 |
||||||
Flask-WTF==0.14.3 |
Flask-WTF==0.14.3 |
||||||
|
is-disposable-email==1.0.0 |
||||||
|
isort==4.3.21 |
||||||
itsdangerous==1.1.0 |
itsdangerous==1.1.0 |
||||||
Jinja2==2.11.2 |
Jinja2==2.11.2 |
||||||
lazy-object-proxy==1.4.3 |
lazy-object-proxy==1.4.3 |
||||||
Mako==1.1.3 |
Mako==1.1.3 |
||||||
MarkupSafe==1.1.1 |
MarkupSafe==1.1.1 |
||||||
|
mccabe==0.6.1 |
||||||
|
nose2==0.9.2 |
||||||
Pillow==7.2.0 |
Pillow==7.2.0 |
||||||
|
pylint==2.5.3 |
||||||
|
pylint-flask-sqlalchemy==0.2.0 |
||||||
python-dateutil==2.8.1 |
python-dateutil==2.8.1 |
||||||
python-dotenv==0.13.0 |
python-dotenv==0.13.0 |
||||||
python-editor==1.0.4 |
python-editor==1.0.4 |
||||||
pytz==2020.1 |
pytz==2020.1 |
||||||
qrcode==6.1 |
qrcode==6.1 |
||||||
|
rope==0.17.0 |
||||||
six==1.15.0 |
six==1.15.0 |
||||||
SQLAlchemy==1.3.18 |
SQLAlchemy==1.3.18 |
||||||
|
toml==0.10.1 |
||||||
typed-ast==1.4.1 |
typed-ast==1.4.1 |
||||||
visitor==0.1.3 |
visitor==0.1.3 |
||||||
Werkzeug==1.0.1 |
Werkzeug==1.0.1 |
||||||
wrapt==1.12.1 |
wrapt==1.12.1 |
||||||
WTForms==2.3.1 |
WTForms==2.3.1 |
||||||
|
wtforms-validators==1.0.0 |
||||||
|
@ -1,12 +1,51 @@ |
|||||||
from app import create_app, db |
from app import create_app, db |
||||||
from app.models import Game, Player, Objective, Location, Notification, GamePlayer, \ |
from app.models import Game, User, Objective, Location, Notification, GamePlayer, \ |
||||||
PlayerFoundObjective, NotificationPlayer, PlayerCaughtPlayer, Role, GameState |
PlayerFoundObjective, NotificationPlayer, PlayerCaughtPlayer, Role, GameState, Review |
||||||
|
|
||||||
app = create_app() |
app = create_app() |
||||||
|
|
||||||
@app.shell_context_processor |
@app.shell_context_processor |
||||||
def make_shell_context(): |
def make_shell_context(): |
||||||
return {'db': db, 'Game' : Game, 'Player' : Player, 'Objective' : Objective, |
return {'db': db, 'Game' : Game, 'User' : User, 'Objective' : Objective, |
||||||
'Location' : Location, 'Notification' : Notification, 'GamePlayer' : GamePlayer, |
'Location' : Location, 'Notification' : Notification, 'GamePlayer' : GamePlayer, |
||||||
'PlayerFoundObjective' : PlayerFoundObjective, 'NotificationPlayer' : NotificationPlayer, |
'PlayerFoundObjective' : PlayerFoundObjective, 'NotificationPlayer' : NotificationPlayer, |
||||||
'PlayerCaughtPlayer' : PlayerCaughtPlayer, 'Role' : Role, 'GameState' : GameState} |
'PlayerCaughtPlayer' : PlayerCaughtPlayer, 'Role' : Role, 'GameState' : GameState, |
||||||
|
'Review' : Review, 'create_objects' : create_objects} |
||||||
|
|
||||||
|
def create_objects(): |
||||||
|
g1 = Game(name='TestGame') |
||||||
|
g2 = Game(name='MyGame') |
||||||
|
|
||||||
|
u1 = User(name='Marijn') |
||||||
|
u1.set_password('123') |
||||||
|
u2 = User(name='Rogier') |
||||||
|
u2.set_password('123') |
||||||
|
u3 = User(name='Henk') |
||||||
|
u4 = User(name='Emma') |
||||||
|
u5 = User(name='Demi') |
||||||
|
|
||||||
|
o1 = Objective(name='Florin', latitude=52.0932, longitude=5.12405) |
||||||
|
o2 = Objective(name='Amsterdam', latitude=52.35547, longitude=4.87518) |
||||||
|
o3 = Objective(name='Amersfoort', latitude=52.17056, longitude=5.39154) |
||||||
|
|
||||||
|
o1.set_hash() |
||||||
|
o2.set_hash() |
||||||
|
o3.set_hash() |
||||||
|
|
||||||
|
g1.players.append(GamePlayer(user=u1, role=Role.owner)) |
||||||
|
g1.players.append(GamePlayer(user=u2, role=Role.hunter)) |
||||||
|
g1.players.append(GamePlayer(user=u3, role=Role.hunter)) |
||||||
|
g1.players.append(GamePlayer(user=u4, role=Role.bunny)) |
||||||
|
g1.players.append(GamePlayer(user=u5, role=Role.bunny)) |
||||||
|
|
||||||
|
g1.objectives.append(o1) |
||||||
|
g1.objectives.append(o2) |
||||||
|
g1.objectives.append(o3) |
||||||
|
|
||||||
|
g2.players.append(GamePlayer(user=u1, role=Role.bunny)) |
||||||
|
g2.players.append(GamePlayer(user=u2, role=Role.owner)) |
||||||
|
g2.players.append(GamePlayer(user=u3, role=Role.hunter)) |
||||||
|
|
||||||
|
db.session.add(g1) |
||||||
|
db.session.add(g2) |
||||||
|
db.session.commit() |
||||||
|