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.

396 lines
17 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
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, flash_errors
from app.models import User, Game, Role, GamePlayer, GameState, 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():
4 years ago
game = Game(name=form.game_name.data,
start_time=form.start_time.data,
end_time=form.end_time.data)
game.update_state()
4 years ago
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('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 = Game.query.filter_by(name=game_name).first_or_404()
if not game.owned_by(current_user):
abort(403)
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
game.update_state()
db.session.commit()
flash(f"'{game.name}' had been updated!")
return redirect(url_for('main.game_dashboard', game_name=game.name))
else:
flash_errors(form)
return render_template('create_game.html', title='Chage Game Settings', 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.users:
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:
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:
return render_template('game_hunter_dashboard.html', title='Game Dashboard', game=game,
json=json, location_encoder=LocationEncoder)
if role is None:
abort(403)
4 years ago
@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()
4 years ago
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()
4 years ago
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)
4 years ago
db.session.add(pcp)
db.session.commit()
flash(f"You caught {bunny.user.name}! The submitted photo will be reviewed by a game owner.")
4 years ago
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()
4 years ago
game.users.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:
if not objective.game.is_active():
flash("Its not 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',
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(minutes=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
@bp.route('/user/<username>')
@login_required
def user_profile(username):
user = User.query.filter_by(name=username).first_or_404()
if current_user != user:
abort(403)
return render_template('user_profile.html', user=user)