diff --git a/app/errors.py b/app/errors.py index 1fa9df9..cd2ed05 100644 --- a/app/errors.py +++ b/app/errors.py @@ -1,6 +1,10 @@ from flask import render_template from app import app, db +@app.errorhandler(403) +def not_found_error(error): + return render_template('403.html'), 403 + @app.errorhandler(404) def not_found_error(error): return render_template('404.html'), 404 diff --git a/app/forms.py b/app/forms.py index 313e7eb..cd9928d 100644 --- a/app/forms.py +++ b/app/forms.py @@ -45,11 +45,11 @@ class CreateGameForm(FlaskForm): date_time_utc = clientzone.localize(date_time.data).astimezone(timezone('UTC')) date_time.data = date_time_utc -class AddObjectiveForm(FlaskForm): +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('Create') + submit = SubmitField('Save') def validate_objective_name(self, objective_name): if objective_name.data == '': return diff --git a/app/models.py b/app/models.py index d63c732..dd539b7 100644 --- a/app/models.py +++ b/app/models.py @@ -5,6 +5,9 @@ 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 + class Role(Enum): none = 0 @@ -157,6 +160,15 @@ class Objective(db.Model): 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) diff --git a/app/routes.py b/app/routes.py index a31049f..4709a14 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1,9 +1,12 @@ -from flask import render_template, flash, redirect, url_for, request +from flask import render_template, flash, redirect, url_for, request, abort, send_file from flask_login import login_user, logout_user, current_user, login_required from sqlalchemy import and_ from app import app, db -from app.models import Player, Game, Role, GamePlayer, Objective -from app.forms import LoginForm, RegistrationForm, CreateGameForm, AddObjectiveForm +from app.models import Player, Game, Role, GamePlayer, Objective, ObjectiveMinimalEncoder +from app.forms import LoginForm, RegistrationForm, CreateGameForm, ObjectiveForm +import json +import qrcode +from io import BytesIO @app.route('/') @app.route('/index') @@ -64,18 +67,26 @@ def create_game(): @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() - return render_template('game_dashboard.html', title = 'Game Dashboard', game=game) + return render_template('game_dashboard.html', title = 'Game Dashboard', game=game, json=json, encoder=ObjectiveMinimalEncoder) @app.route('/game//player/') @login_required def game_player(game_name, player_name): return redirect(url_for('indsex')) +def is_game_owner(game): + return current_user in [gameplayer.player for gameplayer in game.game_players if gameplayer.role == Role.owner] + +def is_objective_owner(objective): + return current_user in [gameplayer.player for gameplayer in objective.game.game_players if gameplayer.role == Role.owner] + @app.route('/game//add_objective', methods=['GET', 'POST']) @login_required def add_objective(game_name): game = Game.query.filter_by(name = game_name).first_or_404() - form = AddObjectiveForm() + 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() @@ -83,4 +94,54 @@ def add_objective(game_name): db.session.commit() flash(f"Objective has been added!") return redirect(url_for('game_dashboard', game_name=game.name)) - return render_template('add_objective.html', title = f'Add Objective for {game_name}', form=form) \ No newline at end of file + return render_template('objective.html', title=f'Add Objective for {game_name}', form=form, objective=objective, owner=True) + +def generate_objective_qr_code(objective): + qr = qrcode.QRCode( + version=None, + error_correction=qrcode.constants.ERROR_CORRECT_M, + box_size=30, + border=4, + ) + qr.add_data(url_for('objective', objective_hash=objective.hash, _external=True)) + 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') + +@app.route('/objective//qrcode.png') +@login_required +def objective_qrcode(objective_hash): + objective = Objective.query.filter_by(hash = objective_hash).first_or_404() + if not is_objective_owner(objective): abort(403) + img = generate_objective_qr_code(objective) + return serve_pil_image(img) + +@app.route('/objective/', 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_objective_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 render_template('objective.html', title='Objective view', objective=objective, owner=owner, form=form, qrcode=qrcode) + +@app.route('/objective//delete', methods=['GET']) +@login_required +def objective_delete(objective_hash): + objective = Objective.query.filter_by(hash = objective_hash).first_or_404() + if not is_objective_owner(objective): abort(403) + if is_objective_owner(objective): + db.session.delete(objective) + db.session.commit() + return redirect(url_for('game_dashboard', game_name=objective.game.name)) \ No newline at end of file diff --git a/app/templates/403.html b/app/templates/403.html new file mode 100644 index 0000000..44cc94b --- /dev/null +++ b/app/templates/403.html @@ -0,0 +1,7 @@ +{% extends "base.html" %} + +{% block app_content %} +

Forbidden

+

You don't have permission to do that

+

Back

+{% endblock %} \ No newline at end of file diff --git a/app/templates/game_dashboard.html b/app/templates/game_dashboard.html index 5ecc547..8f5e5e6 100644 --- a/app/templates/game_dashboard.html +++ b/app/templates/game_dashboard.html @@ -1,5 +1,15 @@ {% extends 'base.html' %} +{% block head %} +{{ super() }} + + +{% endblock %} + {% block app_content %}

{{ game.name }} Dashboard

@@ -37,27 +47,65 @@ - + + {% for objective in game.objectives %} - - + + {% endfor %}
Hash Objective Name Latitude Longitude Amount of players that found itHash
{{ objective.hash }} {{ objective.name }} {{ objective.latitude }} {{ objective.longitude }} Placeholder + Placeholder{{ objective.hash }} + +
+
{% endif %} +{% endblock %} + +{% block scripts %} +{{ super() }} + {% endblock %} \ No newline at end of file diff --git a/app/templates/add_objective.html b/app/templates/objective.html similarity index 52% rename from app/templates/add_objective.html rename to app/templates/objective.html index e9b3cb9..ca173ae 100644 --- a/app/templates/add_objective.html +++ b/app/templates/objective.html @@ -12,19 +12,41 @@ {% endblock %} {% block app_content %} + +{% if objective.hash is none %}

Add Objective

+{% elif objective.name == '' %} +

Objective

+{% else %} +

Objective: {{ objective.name }}

+{% endif %} +
-
- {{ form.hidden_tag() }} - {{ wtf.form_field(form.objective_name, class='form-control') }} - {{ wtf.form_field(form.latitude, type='number', value='52.2', min='-90', max='90', step='0.00001') }} - {{ wtf.form_field(form.longitude, class='form-control', type='number', value='5.3', min='-180', max='181', step='0.00001') }} - {{ wtf.form_field(form.submit, class='btn btn-primary') }} -
+ {% if owner %} +
+
+ {{ form.hidden_tag() }} + {{ wtf.form_field(form.objective_name, value=objective.name, class='form-control') }} + {{ wtf.form_field(form.latitude, class='form-control', type='number', value=objective.latitude, min='-90', max='90', step='0.00001') }} + {{ wtf.form_field(form.longitude, class='form-control', type='number', value=objective.longitude, min='-180', max='181', step='0.00001') }} + {{ wtf.form_field(form.submit, class='btn btn-primary') }} +
+ {% if objective.hash %} + + + {% endif %} +
+
+ qr_code_failed +
+ {% else %} +

latitude: {{ objective.latitude }}

+

longitude: {{ objective.longitude }}

+ {% endif %}
-
+

@@ -36,11 +58,11 @@