Compare commits
116 Commits
feature_te
...
master
@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
|
||||
from flask import Blueprint |
||||
|
||||
bp = Blueprint('game', __name__) |
||||
|
||||
from app.game_settings import routes |
@ -0,0 +1,45 @@
@@ -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 @@
@@ -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 @@
@@ -1,54 +1,41 @@
|
||||
from flask_wtf import FlaskForm |
||||
from wtforms import StringField, SubmitField, DateTimeField, BooleanField, HiddenField, FloatField, SelectField |
||||
from wtforms.validators import DataRequired, ValidationError, Length, NumberRange |
||||
from pytz import timezone |
||||
from flask_wtf.file import FileField, FileAllowed, FileRequired |
||||
from wtforms import StringField, SubmitField, FloatField, SelectField |
||||
from wtforms.validators import InputRequired, DataRequired, ValidationError, Length, NumberRange |
||||
from app.models import Objective |
||||
|
||||
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 |
||||
from app import Config |
||||
|
||||
class ObjectiveForm(FlaskForm): |
||||
objective_name = StringField('Objective Name', validators=[Length(min=0, max=64)]) |
||||
latitude = FloatField('Latitude', validators=[DataRequired(), NumberRange(min=-90, max=90)]) |
||||
longitude = FloatField('Longitude', validators=[DataRequired(), NumberRange(min=-180, max=180)]) |
||||
submit = SubmitField('Save') |
||||
old_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() |
||||
if objective is not None: |
||||
raise ValidationError('Please use a different name.') |
||||
|
||||
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') |
||||
|
||||
class PlayerAddForm(FlaskForm): |
||||
name = StringField('Player Name', validators=[DataRequired(), Length(min=0, max=64)]) |
||||
role = SelectField('Player Role', choices=[('none', 'none'), ('owner', 'owner'), ('hunter', 'hunter'), ('bunny', 'bunny')], validators=[DataRequired()]) |
||||
name = StringField('Username', validators=[InputRequired(), Length(min=0, max=64)]) |
||||
role = SelectField('Player Role', coerce=int, validators=[InputRequired()]) |
||||
submit_add = SubmitField('Create') |
||||
|
||||
class PlayerCreateForm(FlaskForm): |
||||
name = StringField('Player Name', validators=[DataRequired(), Length(min=0, max=64)]) |
||||
role = SelectField('Player Role', choices=[('none', 'none'), ('owner', 'owner'), ('hunter', 'hunter'), ('bunny', 'bunny')], validators=[DataRequired()]) |
||||
submit_create = SubmitField('Create') |
||||
class UserCreateForm(FlaskForm): |
||||
name = StringField('Username', validators=[InputRequired(), Length(min=0, max=64)]) |
||||
role = SelectField('Player Role', coerce=int, validators=[InputRequired()]) |
||||
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 @@
@@ -1,187 +1,163 @@
|
||||
import json |
||||
import qrcode |
||||
from flask import render_template, flash, redirect, url_for, request, abort, send_file |
||||
from datetime import datetime, timedelta |
||||
from flask import render_template, redirect, url_for, request, abort, send_file, current_app, flash |
||||
from flask_login import current_user, login_required |
||||
from sqlalchemy import and_ |
||||
from io import BytesIO |
||||
from werkzeug.security import safe_join |
||||
from app import db |
||||
from app.main import bp |
||||
from app.models import Player, Game, Role, GamePlayer, Objective, ObjectiveMinimalEncoder, LocationEncoder |
||||
from app.main.forms import CreateGameForm, ObjectiveForm, PlayerAddForm, PlayerCreateForm, PlayerUpdateForm |
||||
from app.utils import get_game_if_owner, get_caught_bunny_photo_directory, get_bunny_photo_filename |
||||
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('/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.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') |
||||
@login_required |
||||
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): |
||||
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) |
||||
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']) |
||||
@bp.route('/game/<game_name>/caught_bunny_photo', methods=['GET']) |
||||
@login_required |
||||
def add_player(game_name): |
||||
def caught_bunny_photo(game_name): |
||||
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) |
||||
form_add = PlayerAddForm() |
||||
form_create = PlayerCreateForm() |
||||
|
||||
if form_add.submit_add.data and form_add.validate_on_submit(): |
||||
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)) |
||||
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) |
||||
|
||||
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>') |
||||
@bp.route('/game/<game_name>/review') |
||||
@login_required |
||||
def remove_player(game_name, player_name): |
||||
game = Game.query.filter_by(name=game_name).first_or_404() |
||||
if not is_game_owner(game): |
||||
abort(403) |
||||
player = Player.query.filter(and_(Player.name == player_name, Player.games.contains(game))).first_or_404() |
||||
game.players.remove(player) |
||||
db.session.commit() |
||||
return redirect(url_for('main.game_dashboard', game_name=game.name)) |
||||
|
||||
@bp.route('/game/<game_name>/player/<player_name>', methods=['GET', 'POST']) |
||||
@login_required |
||||
def game_player(game_name, player_name): |
||||
game = Game.query.filter_by(name=game_name).first_or_404() |
||||
if not is_game_owner(game): |
||||
abort(403) |
||||
player = Player.query.filter(and_(Player.name == player_name, Player.games.contains(game))).first_or_404() |
||||
gameplayer = [gameplayer for gameplayer in player.player_games if gameplayer.game == game][0] |
||||
form = PlayerUpdateForm(role=gameplayer.role.name) |
||||
if form.validate_on_submit(): |
||||
gameplayer.role = Role[form.role.data] |
||||
def review_caught_bunny_photos(game_name): |
||||
game = get_game_if_owner(game_name) |
||||
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 redirect(url_for('main.game_dashboard', game_name=game.name)) |
||||
return render_template('player.html', title=f'{player.name} in {game_name}', game=game, player=player, form=form, json=json, location_encoder=LocationEncoder) |
||||
return render_template('review_caught_bunny_photos.html', game=game) |
||||
|
||||
@bp.route('/player/<auth_hash>/qrcode.png') |
||||
@bp.route('/user/<username>/send_location', methods=['POST']) |
||||
@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>') |
||||
@login_required |
||||
def player(auth_hash): |
||||
player = Player.query.filter_by(auth_hash=auth_hash).first_or_404() |
||||
return render_template('player.html',title=f'Player: {player.name}', player=player) |
||||
|
||||
'''given player is an owner of the given game''' |
||||
def is_game_owner(game, owner=current_user): |
||||
return owner in [gameplayer.player for gameplayer in game.game_players if gameplayer.role == Role.owner] |
||||
|
||||
'''given player is an owner of a game the subject_player participates in''' |
||||
def is_player_game_owner(subject_player, owner=current_user): |
||||
return owner in [gameplayer.player for gameplayers in |
||||
[game.game_players for game in subject_player.games] |
||||
for gameplayer in gameplayers if gameplayer.role == Role.owner] |
||||
|
||||
'''given player is an owner of a game the given object is part of''' |
||||
def is_objective_owner(objective, owner=current_user): |
||||
return owner in [gameplayer.player for gameplayer in objective.game.game_players if gameplayer.role == Role.owner] |
||||
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(seconds=30): |
||||
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('/game/<game_name>/add_objective', methods=['GET', 'POST']) |
||||
@bp.route('/game/<game_name>/get_locations', methods=['POST']) |
||||
@login_required |
||||
def add_objective(game_name): |
||||
def poll_locations(game_name): |
||||
game = Game.query.filter_by(name=game_name).first_or_404() |
||||
if not is_game_owner(game): |
||||
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 is_objective_owner(objective): |
||||
role = current_user.role_in_game(game) |
||||
if role is None or role == Role.none: |
||||
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 |
||||
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/<objective_hash>/qrcode.png') |
||||
|
||||
payload = request.get_json() |
||||
if payload is None: |
||||
abort(400) |
||||
mode = get_value_if_key_exists(payload, 'mode', 'last') |
||||
last_update = get_value_if_key_exists(payload, 'last_update', 'none') |
||||
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') |
||||
#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 |
||||
def objective_qrcode(objective_hash): |
||||
objective = Objective.query.filter_by(hash=objective_hash).first_or_404() |
||||
if not is_objective_owner(objective): |
||||
def user_profile(username): |
||||
user = User.query.filter_by(name=username).first_or_404() |
||||
if current_user != 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() |
||||
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) |
||||
return render_template('user_profile.html', user=user) |
||||
|
@ -0,0 +1,88 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -0,0 +1,8 @@
|
||||
from enum import Enum |
||||
|
||||
class Role(Enum): |
||||
none = 0 |
||||
owner = 1 |
||||
hunter = 2 |
||||
bunny = 3 |
||||
|
@ -0,0 +1,98 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -1,53 +1,66 @@
|
||||
{% extends 'bootstrap/base.html' %} |
||||
|
||||
{% block styles %} |
||||
{{ super() }} |
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/default.css') }}"> |
||||
{% endblock %} |
||||
|
||||
{% block title %} |
||||
{% if title %}{{ title }} - The Hunt{% else %}Welcome to The Hunt{% endif %} |
||||
{% endblock %} |
||||
|
||||
{% block navbar %} |
||||
<nav class="navbar navbar-default"> |
||||
<div class="container"> |
||||
<div class="navbar-header"> |
||||
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false"> |
||||
<span class="sr-only">Toggle navigation</span> |
||||
<span class="icon-bar"></span> |
||||
<span class="icon-bar"></span> |
||||
<span class="icon-bar"></span> |
||||
</button> |
||||
<a class="navbar-brand" href="{{ url_for('main.index') }}">The Hunt</a> |
||||
<div class="container"> |
||||
<div class="navbar-header"> |
||||
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" |
||||
data-target="#bs-example-navbar-collapse-1" aria-expanded="false"> |
||||
<span class="sr-only">Toggle navigation</span> |
||||
<span class="icon-bar"></span> |
||||
<span class="icon-bar"></span> |
||||
<span class="icon-bar"></span> |
||||
</button> |
||||
<a class="navbar-brand" href="{{ url_for('main.index') }}">The Hunt</a> |
||||
</div> |
||||
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1"> |
||||
<ul class="nav navbar-nav"> |
||||
<li><a href="{{ url_for('main.index') }}">Home</a></li> |
||||
{% if current_user.is_authenticated %} |
||||
<li><a href="{{ url_for('game.create_game') }}">Create Game</a></li> |
||||
{% endif %} |
||||
</ul> |
||||
<ul class="nav navbar-nav navbar-right"> |
||||
{% if current_user.is_anonymous %} |
||||
<li><a href="{{ url_for('auth.login') }}">Login</a></li> |
||||
{% else %} |
||||
<li><a href="{{ url_for('main.user_profile', username=current_user.name) }}"> |
||||
<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 class="collapse navbar-collapse" id="bs-example-navbar-collapse-1"> |
||||
<ul class="nav navbar-nav"> |
||||
<li><a href="{{ url_for('main.index') }}">Home</a></li> |
||||
{% if current_user.is_authenticated %} |
||||
<li><a href="{{ url_for('main.create_game') }}">Create Game</a></li> |
||||
{% endif %} |
||||
</ul> |
||||
<ul class="nav navbar-nav navbar-right"> |
||||
{% if current_user.is_anonymous %} |
||||
<li><a href="{{ url_for('auth.login') }}">Login</a></li> |
||||
{% else %} |
||||
<li><a href="{{ url_for('auth.logout') }}">Logout</a></li> |
||||
{% endif %} |
||||
</ul> |
||||
</div> |
||||
</div> |
||||
</nav> |
||||
{% endblock %} |
||||
|
||||
{% block content %} |
||||
<div class="container"> |
||||
{% with messages = get_flashed_messages() %} |
||||
{% if messages %} |
||||
{% for message in messages %} |
||||
<div class="alert alert-info" role="alert">{{ message }}</div> |
||||
{% endfor %} |
||||
{% endif %} |
||||
{% endwith %} |
||||
{% with messages = get_flashed_messages() %} |
||||
{% if messages %} |
||||
{% for message in messages %} |
||||
<div class="alert alert-info" role="alert">{{ message }}</div> |
||||
{% endfor %} |
||||
{% endif %} |
||||
{% endwith %} |
||||
|
||||
{# application content needs to be provided in the app_content block #} |
||||
{% block app_content %}{% endblock %} |
||||
{# application content needs to be provided in the app_content block #} |
||||
{% block app_content %}{% endblock %} |
||||
</div> |
||||
{% endblock %} |
||||
|
||||
|
||||
{% block scripts %} |
||||
{{ super() }} |
||||
{{ moment.include_moment() }} |
||||
{% endblock %} |
@ -0,0 +1,19 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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">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</diagram></mxfile> |
After Width: | Height: | Size: 80 KiB |
@ -0,0 +1 @@
@@ -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 @@
@@ -1,8 +1,8 @@
|
||||
Gamestate |
||||
|
||||
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 |
||||
started = 3 |
||||
interrupted = 4 |
||||
active = 3 |
||||
paused = 4 |
||||
finished = 5 |
||||
|
@ -1,27 +1,40 @@
@@ -1,27 +1,40 @@
|
||||
alembic==1.4.2 |
||||
astroid==2.4.2 |
||||
click==7.1.2 |
||||
coverage==5.2 |
||||
dnspython==2.0.0 |
||||
dominate==2.5.1 |
||||
Flask==1.1.2 |
||||
Flask-Bootstrap==3.3.7.1 |
||||
Flask-Login==0.5.0 |
||||
Flask-Migrate==2.5.3 |
||||
Flask-Moment==0.10.0 |
||||
Flask-SQLAlchemy==2.4.3 |
||||
Flask-WTF==0.14.3 |
||||
is-disposable-email==1.0.0 |
||||
isort==4.3.21 |
||||
itsdangerous==1.1.0 |
||||
Jinja2==2.11.2 |
||||
lazy-object-proxy==1.4.3 |
||||
Mako==1.1.3 |
||||
MarkupSafe==1.1.1 |
||||
mccabe==0.6.1 |
||||
nose2==0.9.2 |
||||
Pillow==7.2.0 |
||||
pylint==2.5.3 |
||||
pylint-flask-sqlalchemy==0.2.0 |
||||
python-dateutil==2.8.1 |
||||
python-dotenv==0.13.0 |
||||
python-editor==1.0.4 |
||||
pytz==2020.1 |
||||
qrcode==6.1 |
||||
rope==0.17.0 |
||||
six==1.15.0 |
||||
SQLAlchemy==1.3.18 |
||||
toml==0.10.1 |
||||
typed-ast==1.4.1 |
||||
visitor==0.1.3 |
||||
Werkzeug==1.0.1 |
||||
wrapt==1.12.1 |
||||
WTForms==2.3.1 |
||||
wtforms-validators==1.0.0 |
||||
|
@ -1,12 +1,51 @@
@@ -1,12 +1,51 @@
|
||||
from app import create_app, db |
||||
from app.models import Game, Player, Objective, Location, Notification, GamePlayer, \ |
||||
PlayerFoundObjective, NotificationPlayer, PlayerCaughtPlayer, Role, GameState |
||||
from app.models import Game, User, Objective, Location, Notification, GamePlayer, \ |
||||
PlayerFoundObjective, NotificationPlayer, PlayerCaughtPlayer, Role, GameState, Review |
||||
|
||||
app = create_app() |
||||
|
||||
@app.shell_context_processor |
||||
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, |
||||
'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() |
||||
|