import fnmatch import json from os import listdir from pathlib import Path from datetime import datetime from io import BytesIO import qrcode 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.models import User, Game, Role, GamePlayer, Objective, ObjectiveMinimalEncoder, LocationEncoder, PlayerCaughtPlayer from app.main.forms import CreateGameForm, ObjectiveForm, PlayerAddForm, UserCreateForm, PlayerUpdateForm, CatchBunnyForm import os @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(): 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.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//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//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, objective_encoder=ObjectiveMinimalEncoder, 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//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//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//adduser', methods=['GET', 'POST']) @bp.route('/game//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_create = UserCreateForm() 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_create.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//removeuser/') @bp.route('/game//removeplayer/') @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//user/', methods=['GET', 'POST']) @bp.route('/game//player/', 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] form.role.default = player.role.value form.process() if form.validate_on_submit(): player.role = Role[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('/user//qrcode.png') @login_required def user_qrcode(auth_hash): user = User.query.filter_by(auth_hash=auth_hash).first_or_404() if not current_user.owns_game_played_by(user): abort(403) if user.last_login: flash('After a player has logged in, it is no longer possible to request their QR code.') abort(403) img = generate_qr_code(url_for('main.user', auth_hash=auth_hash, _external=True)) return serve_pil_image(img) @bp.route('/user/') @login_required def user(auth_hash): user = User.query.filter_by(auth_hash=auth_hash).first_or_404() if user.password_hash: flash('Please login with your username and password!') abort(404) return render_template('auth/user.html', title=f'User: {user.name}', user=user) @bp.route('/game//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//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)) 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 def serve_pil_image(pil_img): img_io = BytesIO() pil_img.save(img_io, 'PNG', quality=70) img_io.seek(0) return send_file(img_io, mimetype='image/png') @bp.route('/objective//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/', methods=['GET', 'POST']) @login_required def objective(objective_hash): objective = Objective.query.filter_by(hash=objective_hash).first_or_404() owner = objective.owned_by(current_user) 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)