You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

339 lines
15 KiB

import fnmatch
import json
from os import listdir
from pathlib import Path
from datetime import datetime, timedelta
from flask import render_template, flash, redirect, url_for, request, abort, send_file, current_app, send_from_directory
from flask_login import current_user, login_required
from werkzeug.utils import secure_filename
from werkzeug.security import safe_join
from sqlalchemy import and_
from app import db
from app.main import bp
from app.utils import generate_qr_code, serve_pil_image
from app.models import User, Game, Role, GamePlayer, Objective, ObjectiveMinimalEncoder, \
LocationEncoder, PlayerCaughtPlayer, Review, Location
from app.main.forms import CreateGameForm, ObjectiveForm, PlayerAddForm, UserCreateForm, \
PlayerUpdateForm, CatchBunnyForm
@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('/index')
@login_required
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')
@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))
return render_template('create_game.html', title='Create Game', form=form)
@bp.route('/game/<game_name>/delete')
@login_required
def delete_game(game_name):
game = Game.query.filter_by(name=game_name).first_or_404()
if not game.owned_by(current_user):
abort(403)
db.session.delete(game)
for user in game.players:
if not user.last_login:
db.session.delete(user)
db.session.commit()
flash(f"Game '{game.name}' has been deleted!")
return redirect(url_for('main.index'))
@bp.route('/game/<game_name>/dashboard')
@login_required
def game_dashboard(game_name):
game = Game.query.filter_by(name=game_name).first_or_404()
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=ObjectiveMinimalEncoder,
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:
return render_template('game_hunter_dashboard.html', title='Game Dashboard', game=game,
json=json, location_encoder=LocationEncoder)
if role == Role.none:
return render_template('game_hunter_dashboard.html', title='Game Dashboard', game=game,
json=json, location_encoder=LocationEncoder)
if role is None:
abort(403)
@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)
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)
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)
@bp.route('/game/<game_name>/caught_bunny_photo', methods=['GET'])
@login_required
def caught_bunny_photo(game_name):
game = Game.query.filter_by(name=game_name).first_or_404()
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)
directory = get_caught_bunny_photo_directory(game)
filename = get_bunny_photo_filename(directory, timestamp, hunter_name, bunny_name)
photo_path = safe_join(directory, filename)
#TODO: Implement switch between serve self and serve by webserver
return send_file(photo_path, conditional=True, as_attachment=False)
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']
@bp.route('/game/<game_name>/review')
@login_required
def review_caught_bunny_photos(game_name):
game = Game.query.filter_by(name=game_name).first_or_404()
if not game.owned_by(current_user):
abort(403)
pcp_id = request.args.get('pcp_id', default=-1, type=int)
action = request.args.get('action', default='none', type=str).lower()
if pcp_id != -1:
pcp = PlayerCaughtPlayer.query.filter_by(id=pcp_id).first_or_404()
review = Review.parse_string(action)
pcp.review = review
db.session.commit()
return render_template('review_caught_bunny_photos.html', game=game)
@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 = Game.query.filter_by(name=game_name).first_or_404()
if not game.owned_by(current_user):
abort(403)
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()
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 = Game.query.filter_by(name=game_name).first_or_404()
if not game.owned_by(current_user):
abort(403)
user = User.query.filter(and_(User.name == username, User.games.contains(game))).first_or_404()
game.players.remove(user)
if not user.last_login:
db.session.delete(user)
db.session.commit()
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 = Game.query.filter_by(name=game_name).first_or_404()
if not game.owned_by(current_user):
abort(403)
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>/add_objective', methods=['GET', 'POST'])
@login_required
def add_objective(game_name):
game = Game.query.filter_by(name=game_name).first_or_404()
if not game.owned_by(current_user):
abort(403)
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 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:
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',
objective=objective, owner=True, form=form, qrcode=qrcode)
@bp.route('/user/<username>/send_location', methods=['POST'])
@login_required
def send_location(username):
user = User.query.filter_by(name=username).first_or_404()
last_location = user.last_location()
latitude = request.form.get('lat', default=None, type=float)
longitude = request.form.get('long', default=None, type=float)
if latitude is None or longitude is None:
return '', 400
# Check if previous two locations are exactly the same, if so, only update timestamp of last location
if last_location:
if datetime.utcnow() - last_location.timestamp < timedelta(milliseconds=1):
return '', 204
if latitude == last_location.latitude and longitude == last_location.longitude and 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