Browse Source

implement hunter dashboard, inc catch bunny

testing
Burathar 4 years ago
parent
commit
713d4ddee3
  1. 1
      .gitignore
  2. 3
      app/__init__.py
  3. 6
      app/main/forms.py
  4. 75
      app/main/routes.py
  5. 3
      app/models/player_caught_player.py
  6. 2
      app/templates/game_hunter_dashboard.html
  7. 110
      app/templates/game_player.html
  8. 4
      config.py
  9. 121
      migrations/versions/c2c2f3cf0c14_reset_migrations.py
  10. BIN
      uploads/TestGame/caught_bunny_photos/20200719225209_Rogier_caught_Emma

1
.gitignore vendored

@ -145,3 +145,4 @@ cython_debug/ @@ -145,3 +145,4 @@ cython_debug/
# the-hunt specific:
logs/
app.db
uploads/

3
app/__init__.py

@ -10,6 +10,7 @@ from flask_sqlalchemy import SQLAlchemy @@ -10,6 +10,7 @@ from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_login import LoginManager
from flask_moment import Moment
from flask_wtf import CSRFProtect
from config import Config
@ -20,6 +21,7 @@ login = LoginManager() @@ -20,6 +21,7 @@ login = LoginManager()
login.login_view = 'auth.login'
login.login_message = 'Please log in to access this page.'
moment = Moment()
csrf = CSRFProtect()
def create_app(config_class=Config):
# pylint: disable=no-member
@ -31,6 +33,7 @@ def create_app(config_class=Config): @@ -31,6 +33,7 @@ def create_app(config_class=Config):
migrate.init_app(app, db)
login.init_app(app)
moment.init_app(app)
csrf.init_app(app)
from app.errors import bp as errors_bp
app.register_blueprint(errors_bp)

6
app/main/forms.py

@ -1,8 +1,10 @@ @@ -1,8 +1,10 @@
from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileAllowed, FileRequired
from wtforms import StringField, SubmitField, DateTimeField, BooleanField, HiddenField, FloatField, SelectField
from wtforms.validators import InputRequired, DataRequired, ValidationError, Length, NumberRange
from pytz import timezone
from app.models import Objective
from app import Config
class CreateGameForm(FlaskForm):
game_name = StringField('Game Name', validators=[InputRequired(), Length(min=0, max=64)])
@ -41,7 +43,7 @@ class ObjectiveForm(FlaskForm): @@ -41,7 +43,7 @@ class ObjectiveForm(FlaskForm):
raise ValidationError('Please use a different name.')
class PlayerUpdateForm(FlaskForm):
role = SelectField('Player Role', choices=[('none', 'none'), ('owner', 'owner'), ('hunter', 'hunter'), ('bunny', 'bunny')], validators=[InputRequired()])
role = SelectField('Player Role', coerce=int, validators=[InputRequired()])
submit = SubmitField('Update')
class PlayerAddForm(FlaskForm):
@ -56,5 +58,5 @@ class UserCreateForm(FlaskForm): @@ -56,5 +58,5 @@ class UserCreateForm(FlaskForm):
class CatchBunnyForm(FlaskForm):
bunny = SelectField('Bunny Name', coerce=int, validators=[InputRequired()])
#photo
photo = FileField('Upload Photo', validators=[FileRequired(), FileAllowed(Config.ALLOWED_PHOTO_EXTENSIONS, 'Images only!')])
submit = SubmitField('Send')

75
app/main/routes.py

@ -1,9 +1,14 @@ @@ -1,9 +1,14 @@
import fnmatch
import json
from os import listdir
from pathlib import Path
from datetime import datetime
from io import BytesIO
import qrcode
from flask import render_template, flash, redirect, url_for, request, abort, send_file
from flask import render_template, flash, redirect, url_for, request, abort, send_file, current_app, send_from_directory
from flask_login import current_user, login_required
from werkzeug.utils import secure_filename
from werkzeug.security import safe_join
from sqlalchemy import and_
from app import db
@ -11,6 +16,9 @@ from app.main import bp @@ -11,6 +16,9 @@ from app.main import bp
from app.models import User, Game, Role, GamePlayer, Objective, ObjectiveMinimalEncoder, LocationEncoder, PlayerCaughtPlayer
from app.main.forms import CreateGameForm, ObjectiveForm, PlayerAddForm, UserCreateForm, PlayerUpdateForm, CatchBunnyForm
import os
@bp.before_app_request
def before_request():
if current_user.is_authenticated:
@ -81,11 +89,11 @@ def catch_bunny(game_name): @@ -81,11 +89,11 @@ def catch_bunny(game_name):
flash('Only hunters can catch bunnies!')
abort(403)
game_bunnies = [gameplayer for gameplayer in game.players if gameplayer.role == Role.bunny]
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]
@ -96,13 +104,59 @@ def catch_bunny(game_name): @@ -96,13 +104,59 @@ def catch_bunny(game_name):
if form.validate_on_submit():
bunny = [gameplayer for gameplayer in game_bunnies if gameplayer.user.id == form.bunny.data]
pcp = PlayerCaughtPlayer(catching_player=current_user.player_in(game), caught_player=bunny[0], photo_reference='To be Impemented')
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[0].user.name}'! The submitted photo will be reviewd by a game owner.")
flash(f"You caught {bunny.user.name}! The submitted photo will be reviewed by a game owner.")
return redirect(url_for('main.game_dashboard', game_name=game.name))
return render_template('catch_bunny.html', title='Catch Bunny', form=form, game=game)
def save_player_caught_player_photo(file_storage, game, pcp):
timestamp = pcp.timestamp.strftime('%Y%m%d%H%M%S')
extension = Path(file_storage.filename).suffix
filename = secure_filename(f'{timestamp}_{pcp.catching_player.user.name}_caught_{pcp.caught_player.user.name}{extension}')
path = get_caught_bunny_photo_directory(game)
path.mkdir(parents=True, exist_ok=True)
file_storage.save(path / filename)
@bp.route('/game/<game_name>/caught_bunny_photo', methods=['GET'])
@login_required
def caught_bunny_photo(game_name):
game = Game.query.filter_by(name=game_name).first_or_404()
timestamp = request.args['timestamp']
bunny_name = request.args['bunny_name']
hunter_name = request.args['hunter_name']
hunter = GamePlayer.query.join(User).filter(
(GamePlayer.user_id == User.id) &
(GamePlayer.game_id == game.id) &
(User.name == hunter_name) &
(GamePlayer.role == Role.hunter)).first_or_404()
if not (game.owned_by(current_user) or current_user.player_in(game) == hunter):
abort(403)
directory = get_caught_bunny_photo_directory(game)
filename = get_bunny_photo_filename(directory, timestamp, hunter_name, bunny_name)
photo_path = safe_join(directory, filename)
#TODO: Implement switch between serve self and serve by webserver
return send_file(photo_path, conditional=True, as_attachment=False)
def get_bunny_photo_filename(directory, timestamp, hunter_name, bunny_name):
filename = secure_filename(f'{timestamp}_{hunter_name}_caught_{bunny_name}') + '.*'
matches = fnmatch.filter(listdir(directory), filename)
return matches[0] if matches else ''
def get_caught_bunny_photo_directory(game):
return Path(current_app.root_path).parent / \
current_app.config['UPLOAD_FOLDER'] / \
secure_filename(game.name) / \
current_app.config['PLAYER_CAUGHT_PLAYER_PHOTO_DIR_NAME']
@bp.route('/game/<game_name>/adduser', methods=['GET', 'POST'])
@bp.route('/game/<game_name>/addplayer', methods=['GET', 'POST'])
@login_required
@ -149,15 +203,20 @@ def game_player(game_name, username): @@ -149,15 +203,20 @@ def game_player(game_name, username):
game = Game.query.filter_by(name=game_name).first_or_404()
if not game.owned_by(current_user):
abort(403)
user = game.users
user = User.query.filter(and_(User.name == username, User.games.contains(game))).first_or_404()
user = User.query.filter((User.name == username) & (User.games.contains(game))).first_or_404()
player = user.player_in(game)
# pylint: disable=no-member
form = PlayerUpdateForm(role=player.role.name)
form.role.choices = [(role.value, role.name) for role in Role]
form.role.default = player.role.value
form.process()
if form.validate_on_submit():
player.role = Role[form.role.data]
db.session.commit()
return redirect(url_for('main.game_dashboard', game_name=game.name))
return render_template('game_player.html', title=f'{user.name} in {game_name}', game=game, user=user, form=form, json=json, location_encoder=LocationEncoder)
return render_template('game_player.html', title=f'{user.name} in {game_name}', player=player, form=form, json=json, location_encoder=LocationEncoder)
@bp.route('/user/<auth_hash>/qrcode.png')
@login_required

3
app/models/player_caught_player.py

@ -8,7 +8,6 @@ class PlayerCaughtPlayer(db.Model): @@ -8,7 +8,6 @@ class PlayerCaughtPlayer(db.Model):
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)
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('GamePlayer', primaryjoin=(catching_player_id == GamePlayer.id),
backref=db.backref('player_caught_players', cascade='save-update, merge, delete, delete-orphan'))
@ -20,7 +19,7 @@ This relation doesn't work as well as the others, and must be used as folowed: @@ -20,7 +19,7 @@ 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, photo_reference='reference')
pc = PlayerCaughtPlayer(caught_player=p2, catching_player=p1)
db.session.add(pc)
db.session.commit()

2
app/templates/game_hunter_dashboard.html

@ -33,7 +33,7 @@ @@ -33,7 +33,7 @@
{{ location }}
{% endwith %}</td>
<td>
<a href="{{ url_for('main.catch_bunny', game_name=game.name, bunny_name=bunny.name) }}">
<a href="{{ url_for('main.catch_bunny', game_name=game.name, bunny_name=bunny.user.name) }}">
<button class="btn btn-success">Catch</button>
</a>
</td>

110
app/templates/game_player.html

@ -8,55 +8,86 @@ @@ -8,55 +8,86 @@
{% endblock %}
{% block app_content %}
<h1>Player: {{ user.name }}</h1>
<h1>Player: {{ player.user.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 user.user_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>
{{ wtf.quick_form(form, button_map={'submit': 'primary'}) }}
</div>
{% if user.auth_hash and not user.last_login %}
{% if player.user.auth_hash and not player.user.last_login %}
<div class="row">
<img src="{{ url_for('main.user_qrcode', auth_hash=user.auth_hash) }}" alt="qr_code_failed", width="100%">
<img src="{{ url_for('main.user_qrcode', auth_hash=player.user.auth_hash) }}" alt="qr_code_failed" , width="80%">
</div>
{% elif not user.last_login %}
{% elif not player.user.last_login %}
<br>
<div class="row">
<a href="#", id="generate_auth_hash">
<a href="#" , id="generate_auth_hash">
<button class="btn btn-success">Generate Login Code</button></a>
</div>
{% endif %}
</div>
<div id="map" style=" height: 600px; border-radius: 10px; " class="col-md-6 col-xs-12"></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">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>{{ 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() }}
{{ moment.include_moment() }}
<script type="text/javascript", crossorigin="anonymous">
<script type="text/javascript" , crossorigin="anonymous">
// Leaflet Map
'{% set last_location = user.last_location(game) %}'
var map = L.map( 'map', {
'{% set last_location = player.last_location() %}'
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]],
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', {
L.tileLayer(
'https://geodata.nationaalgeoregister.nl/tiles/service/wmts/brtachtergrondkaartpastel/EPSG:3857/{z}/{x}/{y}.png', {
attribution: 'Kaartgegevens &copy; <a href="https://kadaster.nl">Kadaster</a>'
}).addTo( map );
}).addTo(map);
var greenIcon = new L.Icon({
iconUrl: "{{ url_for('static', filename='assets/leaflet/images/marker-icon-2x-green.png') }}",
@ -67,42 +98,41 @@ @@ -67,42 +98,41 @@
shadowSize: [41, 41]
});
var locations = JSON.parse('{{ json.dumps(user.locations_during_game(game), cls=location_encoder)|safe }}')
if (locations == null) {locations=[]}
for (var i = 0; i < locations.length; i++){
var locations = JSON.parse('{{ json.dumps(player.locations_during_game(), cls=location_encoder)|safe }}')
if (locations == null) {
locations = []
}
for (var i = 0; i < locations.length; i++) {
var playerMarker = L.marker([
locations[i]['latitude'],
locations[i]['longitude']
], {icon: greenIcon}).addTo(map);
], {
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]['username']}</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]]
];
if (locations.length > 0){
var polyline = L.polyline(locations.map(l => [l.latitude, l.longitude]), {color: 'red'}).addTo(map);
if (locations.length > 0) {
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());
}
//Ajax for Generate Login Code button
$(function() {
$('a#generate_auth_hash').bind('click', function() {
$.ajax({url: "{{ url_for('auth.generate_auth_hash', username=user.name) }}",
success: function(result) {
//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 %}

4
config.py

@ -20,3 +20,7 @@ class Config(object): @@ -20,3 +20,7 @@ class Config(object):
ADMINS = ['your-email@example.com']
BOOTSTRAP_SERVE_LOCAL = True
UPLOAD_FOLDER = os.environ.get('UPLOAD_FOLDER') or 'uploads'
PLAYER_CAUGHT_PLAYER_PHOTO_DIR_NAME = os.environ.get('PLAYER_CAUGHT_PLAYER_PHOTO_DIR_NAME') or 'caught_bunny_photos'
ALLOWED_PHOTO_EXTENSIONS = os.environ.get('ALLOWED_EXTENSIONS') or {'png', 'jpg', 'jpeg', 'gif', 'tiff', 'heif', 'heic'}

121
migrations/versions/c2c2f3cf0c14_reset_migrations.py

@ -1,121 +0,0 @@ @@ -1,121 +0,0 @@
"""reset migrations
Revision ID: c2c2f3cf0c14
Revises:
Create Date: 2020-07-19 04:46:04.972644
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'c2c2f3cf0c14'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('game',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=64), nullable=False),
sa.Column('state', sa.Enum('initiated', 'published', 'started', 'interrupted', 'finished', name='gamestate'), server_default='initiated', nullable=False),
sa.Column('start_time', sa.DateTime(), nullable=True),
sa.Column('end_time', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_game_name'), 'game', ['name'], unique=True)
op.create_table('user',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=64), nullable=False),
sa.Column('auth_hash', sa.String(length=32), nullable=True),
sa.Column('password_hash', sa.String(length=128), nullable=True),
sa.Column('last_login', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('auth_hash'),
sa.UniqueConstraint('name')
)
op.create_table('game_player',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('game_id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('role', sa.Enum('none', 'owner', 'hunter', 'bunny', name='role'), server_default='none', nullable=False),
sa.ForeignKeyConstraint(['game_id'], ['game.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('game_id', 'user_id', name='_game_user_uc')
)
op.create_table('location',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('longitude', sa.Numeric(precision=15, scale=10, asdecimal=False), nullable=False),
sa.Column('latitude', sa.Numeric(precision=15, scale=10, asdecimal=False), nullable=False),
sa.Column('timestamp', sa.DateTime(), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('notification',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('game_id', sa.Integer(), nullable=False),
sa.Column('message', sa.Text(), nullable=False),
sa.Column('type', sa.String(length=64), nullable=False),
sa.Column('timestamp', sa.DateTime(), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.ForeignKeyConstraint(['game_id'], ['game.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('objective',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=64), nullable=True),
sa.Column('game_id', sa.Integer(), nullable=False),
sa.Column('hash', sa.String(length=32), nullable=False),
sa.Column('longitude', sa.Numeric(precision=15, scale=10, asdecimal=False), nullable=True),
sa.Column('latitude', sa.Numeric(precision=15, scale=10, asdecimal=False), nullable=True),
sa.ForeignKeyConstraint(['game_id'], ['game.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('hash')
)
op.create_table('notification_player',
sa.Column('notification_id', sa.Integer(), nullable=False),
sa.Column('game_player_id', sa.Integer(), nullable=False),
sa.Column('been_shown', sa.Boolean(), server_default='True', nullable=False),
sa.ForeignKeyConstraint(['game_player_id'], ['game_player.id'], ),
sa.ForeignKeyConstraint(['notification_id'], ['notification.id'], ),
sa.PrimaryKeyConstraint('notification_id', 'game_player_id')
)
op.create_table('player_caught_player',
sa.Column('id', sa.Integer(), server_default='-1', autoincrement=True, nullable=False),
sa.Column('catching_player_id', sa.Integer(), nullable=False),
sa.Column('caught_player_id', sa.Integer(), nullable=False),
sa.Column('photo_reference', sa.String(length=128), nullable=False),
sa.Column('timestamp', sa.DateTime(), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.ForeignKeyConstraint(['catching_player_id'], ['game_player.id'], ),
sa.ForeignKeyConstraint(['caught_player_id'], ['game_player.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('photo_reference')
)
op.create_table('player_found_objective',
sa.Column('objective_id', sa.Integer(), server_default='-1', nullable=False),
sa.Column('game_player_id', sa.Integer(), server_default='-1', nullable=False),
sa.Column('timestamp', sa.DateTime(), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.ForeignKeyConstraint(['game_player_id'], ['game_player.id'], ),
sa.ForeignKeyConstraint(['objective_id'], ['objective.id'], ),
sa.PrimaryKeyConstraint('objective_id', 'game_player_id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('player_found_objective')
op.drop_table('player_caught_player')
op.drop_table('notification_player')
op.drop_table('objective')
op.drop_table('notification')
op.drop_table('location')
op.drop_table('game_player')
op.drop_table('user')
op.drop_index(op.f('ix_game_name'), table_name='game')
op.drop_table('game')
# ### end Alembic commands ###

BIN
uploads/TestGame/caught_bunny_photos/20200719225209_Rogier_caught_Emma

Binary file not shown.

Before

Width:  |  Height:  |  Size: 582 KiB

Loading…
Cancel
Save