From 58b354a24046dd8467ab5a1655b56f77e4e253ec Mon Sep 17 00:00:00 2001 From: Burathar Date: Fri, 17 Jul 2020 19:00:03 +0200 Subject: [PATCH] Split up application structure, implement route owner tests, fix route owner methods --- app/__init__.py | 13 +- app/errors.py | 19 --- app/forms.py | 72 ---------- app/models.py | 2 +- app/routes.py | 216 ---------------------------- app/templates/base.html | 10 +- app/templates/{ => errors}/403.html | 2 +- app/templates/{ => errors}/404.html | 2 +- app/templates/{ => errors}/405.html | 2 +- app/templates/{ => errors}/500.html | 2 +- app/templates/game_dashboard.html | 12 +- app/templates/index.html | 2 +- app/templates/login.html | 13 -- app/templates/objective.html | 4 +- app/templates/player.html | 2 +- app/templates/register.html | 13 -- config.py | 1 + tests/__init__.py | 0 tests/test_routes.py | 34 ----- the_hunt.py | 2 +- 20 files changed, 33 insertions(+), 390 deletions(-) delete mode 100644 app/errors.py delete mode 100644 app/forms.py delete mode 100644 app/routes.py rename app/templates/{ => errors}/403.html (63%) rename app/templates/{ => errors}/404.html (52%) rename app/templates/{ => errors}/405.html (66%) rename app/templates/{ => errors}/500.html (70%) delete mode 100644 app/templates/login.html delete mode 100644 app/templates/register.html delete mode 100644 tests/__init__.py delete mode 100644 tests/test_routes.py mode change 100644 => 100755 the_hunt.py diff --git a/app/__init__.py b/app/__init__.py index b65b6d7..2d0a3ad 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -17,7 +17,7 @@ db = SQLAlchemy() bootstrap = Bootstrap() migrate = Migrate() login = LoginManager() -login.login_view = 'login' +login.login_view = 'auth.login' login.login_message = 'Please log in to access this page.' moment = Moment() @@ -32,6 +32,15 @@ def create_app(config_class=Config): login.init_app(app) moment.init_app(app) + from app.errors import bp as errors_bp + app.register_blueprint(errors_bp) + + from app.auth import bp as auth_bp + app.register_blueprint(auth_bp, url_prefix='/auth') + + from app.main import bp as main_bp + app.register_blueprint(main_bp) + if not app.debug and not app.testing: if app.config['MAIL_SERVER']: auth = None @@ -69,4 +78,4 @@ def create_app(config_class=Config): return app -from app import routes, models, errors +from app import models diff --git a/app/errors.py b/app/errors.py deleted file mode 100644 index 23df63d..0000000 --- a/app/errors.py +++ /dev/null @@ -1,19 +0,0 @@ -from flask import render_template -from app import app, db - -@app.errorhandler(403) -def forbidden_error(error): - return render_template('403.html'), 403 - -@app.errorhandler(404) -def not_found_error(error): - return render_template('404.html'), 404 - -@app.errorhandler(405) -def method_not_allowed_error(error): - return render_template('405.html'), 405 - -@app.errorhandler(500) -def internal_error(error): - db.session.rollback() - return render_template('500.html'), 500 \ No newline at end of file diff --git a/app/forms.py b/app/forms.py deleted file mode 100644 index 4bc20ac..0000000 --- a/app/forms.py +++ /dev/null @@ -1,72 +0,0 @@ -from flask_wtf import FlaskForm -from wtforms import StringField, PasswordField, SubmitField, DateTimeField, BooleanField, HiddenField, FloatField, SelectField -from wtforms.validators import DataRequired, EqualTo, ValidationError, Length, NumberRange -from pytz import timezone -from app.models import Player, Objective, Role - -class LoginForm(FlaskForm): - username = StringField('Username', validators=[DataRequired()]) - password = PasswordField('Password', validators=[DataRequired()]) - remember_me = BooleanField('Remember Me', default=True) - submit = SubmitField('Sign In') - -class RegistrationForm(FlaskForm): - username = StringField('Username', validators=[DataRequired(), Length(min=0, max=64)]) - password = PasswordField('Password', validators=[DataRequired(), Length(min=0, max=128)]) - password2 = PasswordField( - 'Repeat Password', validators=[DataRequired(), EqualTo('password')]) - submit = SubmitField('Register') - - def validate_username(self, username): - player = Player.query.filter_by(name=username.data).first() - if player is not None: - raise ValidationError('Please use a different username.') - -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 - -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') - - def validate_objective_name(self, objective_name): - if objective_name.data == '': 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()]) - 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()]) - 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') \ No newline at end of file diff --git a/app/models.py b/app/models.py index 9e6c3b2..789670d 100644 --- a/app/models.py +++ b/app/models.py @@ -1,6 +1,6 @@ from enum import Enum from werkzeug.security import generate_password_hash, check_password_hash -from app import app, db, login +from . import db, login from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.sql import func from secrets import token_hex diff --git a/app/routes.py b/app/routes.py deleted file mode 100644 index 40948ec..0000000 --- a/app/routes.py +++ /dev/null @@ -1,216 +0,0 @@ -import json -import qrcode -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 io import BytesIO -from app import app, db -from app.models import Player, Game, Role, GamePlayer, Objective, ObjectiveMinimalEncoder, LocationEncoder -from app.forms import LoginForm, RegistrationForm, CreateGameForm, ObjectiveForm, PlayerAddForm, PlayerCreateForm, PlayerUpdateForm - -@app.route('/') -@app.route('/index') -@login_required -def index(): - return render_template("index.html", title='Home') - -@app.route('/login', methods=['GET', 'POST']) -def login(): - if current_user.is_authenticated: - return redirect(url_for('index')) - form = LoginForm() - if form.validate_on_submit(): - player = Player.query.filter_by(name=form.username.data).first() - if player is None or not player.check_password(form.password.data): - flash('Invalid username or password') - return redirect(url_for('login')) - login_user(player, remember=form.remember_me.data) - return redirect(url_for('index')) - return render_template('login.html', title='Sign In', form=form) - -@app.route('/logout') -@login_required -def logout(): - logout_user() - return redirect(url_for('index')) - -@app.route('/register', methods=['GET', 'POST']) -def register(): - if current_user.is_authenticated: - return redirect(url_for('index')) - form = RegistrationForm() - if form.validate_on_submit(): - player = Player(name=form.username.data) - player.set_password(form.password.data) - player.set_auth_hash() - db.session.add(player) - db.session.commit() - flash('Congratulations, you are now a registered user!') - return redirect(url_for('login')) - return render_template('register.html', title='Register', form=form) - -@app.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'])) #check if this works, otherwise use 'owner' - db.session.add(game) - db.session.commit() - flash(f"'{game.name}' had been created!") - return redirect(url_for('game_dashboard', game_name=game.name)) - return render_template('create_game.html', title='Create Game', form=form) - -@app.route('/game//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): - abort(403) - return render_template('game_dashboard.html', title = 'Game Dashboard', game=game, json=json, objective_encoder=ObjectiveMinimalEncoder, location_encoder=LocationEncoder) - -@app.route('/game//addplayer', methods=['GET', 'POST']) -@login_required -def add_player(game_name): - game = Game.query.filter_by(name=game_name).first_or_404() - if not is_game_owner(game): - 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('game_dashboard', game_name=game.name)) - - 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('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) - -@app.route('/game//removeplayer/') -@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('game_dashboard', game_name=game.name)) - -@app.route('/game//player/', 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] - db.session.commit() - return redirect(url_for('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) - -@app.route('/player//qrcode.png') -@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('player', auth_hash=auth_hash, _external=True)) - return serve_pil_image(img) - -@app.route('/player/') -@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) - -def is_game_owner(game): - return current_user in [gameplayer.player for gameplayer in game.game_players if gameplayer.role == Role.owner] - -def is_player_game_owner(player): - return current_user in [gameplayer.player for gameplayer in [game for game in player.games].game_players if game_player.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() - 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('game_dashboard', game_name=game.name)) - return render_template('objective.html', title=f'Add Objective for {game_name}', form=form, objective=objective, owner=True) - -@app.route('/objective//delete', methods=['GET']) -@login_required -def delete_objective(objective_hash): - objective = Objective.query.filter_by(hash=objective_hash).first_or_404() - if not 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)) - -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') - -@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_qr_code(url_for('objective', objective_hash=objective.hash, _external=True)) - 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_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('game_dashboard', game_name=objective.game.name)) - return render_template('objective.html', title='Objective view', objective=objective, owner=owner, form=form, qrcode=qrcode) diff --git a/app/templates/base.html b/app/templates/base.html index ba377b2..9a45961 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -14,20 +14,20 @@ - The Hunt + The Hunt diff --git a/app/templates/403.html b/app/templates/errors/403.html similarity index 63% rename from app/templates/403.html rename to app/templates/errors/403.html index 44cc94b..c65a513 100644 --- a/app/templates/403.html +++ b/app/templates/errors/403.html @@ -3,5 +3,5 @@ {% block app_content %}

Forbidden

You don't have permission to do that

-

Back

+

Back

{% endblock %} \ No newline at end of file diff --git a/app/templates/404.html b/app/templates/errors/404.html similarity index 52% rename from app/templates/404.html rename to app/templates/errors/404.html index b76b06c..3fa3377 100644 --- a/app/templates/404.html +++ b/app/templates/errors/404.html @@ -2,5 +2,5 @@ {% block app_content %}

File Not Found

-

Back

+

Back

{% endblock %} \ No newline at end of file diff --git a/app/templates/405.html b/app/templates/errors/405.html similarity index 66% rename from app/templates/405.html rename to app/templates/errors/405.html index c057c78..ecfa75e 100644 --- a/app/templates/405.html +++ b/app/templates/errors/405.html @@ -3,5 +3,5 @@ {% block app_content %}

Method Not Allowed

The method is not allowed for the requested URL.

-

Back

+

Back

{% endblock %} \ No newline at end of file diff --git a/app/templates/500.html b/app/templates/errors/500.html similarity index 70% rename from app/templates/500.html rename to app/templates/errors/500.html index 88addac..5f56727 100644 --- a/app/templates/500.html +++ b/app/templates/errors/500.html @@ -3,5 +3,5 @@ {% block app_content %}

An unexpected error has occurred

The administrator has been notified. Sorry for the inconvenience!

-

Back

+

Back

{% endblock %} \ No newline at end of file diff --git a/app/templates/game_dashboard.html b/app/templates/game_dashboard.html index f55fe8f..4da5150 100644 --- a/app/templates/game_dashboard.html +++ b/app/templates/game_dashboard.html @@ -10,7 +10,7 @@

{{ game.name }} Dashboard

Players:

-

Add player

+

Add player

@@ -27,7 +27,7 @@ {% for player in game.players %} - + {% for gameplayer in player.player_games if gameplayer.game == game %} {% endfor %} @@ -38,7 +38,7 @@ {% if location %}{{ moment(location.timestamp).fromNow()}}: {% endif %} {{ location }} {% endwith %} - @@ -47,7 +47,7 @@
{{ player.name }}{{ player.name }}{{ gameplayer.role.name }} +

Objectives:

-

Add new objective

+

Add new objective

{% if game.objectives %}
@@ -68,8 +68,8 @@ - - + diff --git a/app/templates/index.html b/app/templates/index.html index f22dd83..a1b0547 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -19,7 +19,7 @@ {% for game in current_user.games %} - + diff --git a/app/templates/login.html b/app/templates/login.html deleted file mode 100644 index b8fe51b..0000000 --- a/app/templates/login.html +++ /dev/null @@ -1,13 +0,0 @@ -{% extends 'base.html' %} -{% import 'bootstrap/wtf.html' as wtf %} - -{% block app_content %} -

Sign In

-
-
- {{ wtf.quick_form(form, button_map={'submit': 'primary'}) }} -
-
-
-

New User? Click to Register!

-{% endblock %} \ No newline at end of file diff --git a/app/templates/objective.html b/app/templates/objective.html index 139ac7c..d452010 100644 --- a/app/templates/objective.html +++ b/app/templates/objective.html @@ -34,13 +34,13 @@ {{ wtf.form_field(form.submit, class='btn btn-primary') }} {% if objective.hash %} - + {% endif %} {% if objective.hash %}
- qr_code_failed + qr_code_failed
{% endif %} {% else %} diff --git a/app/templates/player.html b/app/templates/player.html index f1a5a6a..cdaea91 100644 --- a/app/templates/player.html +++ b/app/templates/player.html @@ -23,7 +23,7 @@ {% if player.auth_hash %}
- qr_code_failed + qr_code_failed
{% endif %} diff --git a/app/templates/register.html b/app/templates/register.html deleted file mode 100644 index 1ecb3bf..0000000 --- a/app/templates/register.html +++ /dev/null @@ -1,13 +0,0 @@ -{% extends "base.html" %} -{% import 'bootstrap/wtf.html' as wtf %} - -{% block app_content %} -

Register

-
-
- {{ wtf.quick_form(form, button_map={'submit': 'primary'}) }} -
-
-
-

Already have an account? Click to Sign In!

-{% endblock %} \ No newline at end of file diff --git a/config.py b/config.py index 8234351..bcbe2ff 100644 --- a/config.py +++ b/config.py @@ -1,6 +1,7 @@ import os from dotenv import load_dotenv from pathlib import Path + basedir = Path(__file__).parent.absolute() load_dotenv(basedir / '.env') diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/test_routes.py b/tests/test_routes.py deleted file mode 100644 index e73f27a..0000000 --- a/tests/test_routes.py +++ /dev/null @@ -1,34 +0,0 @@ -import unittest -from app import create_app, db -from app.models import Player, Game, Role, GamePlayer, Objective, ObjectiveMinimalEncoder, LocationEncoder -from config import Config - -class TestConfig(Config): - TESTING = True - SQLALCHEMY_DATABASE_URI = 'sqlite://' - -class RoutesCase(unittest.TestCase): - 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): - g = Game(name='TestGame') - p1 = Player(name='Henk') - p2 = Player(name='Alfred') - g.players.append(p1) - g.players.append(p2) - db.session.append(g) - db.session.commit() - self.assertEqual(True, True) - self.assertFalse('Foo'.isupper()) - -if __name__ == '__main__': - unittest.main(verbosity=2) \ No newline at end of file diff --git a/the_hunt.py b/the_hunt.py old mode 100644 new mode 100755 index 52ed5d6..fb91633 --- a/the_hunt.py +++ b/the_hunt.py @@ -9,4 +9,4 @@ def make_shell_context(): return {'db': db, 'Game' : Game, 'Player' : Player, 'Objective' : Objective, 'Location' : Location, 'Notification' : Notification, 'GamePlayer' : GamePlayer, 'PlayerFoundObjective' : PlayerFoundObjective, 'NotificationPlayer' : NotificationPlayer, - 'PlayerCaughtPlayer' : PlayerCaughtPlayer, 'Role' : Role, 'GameState' : GameState} + 'PlayerCaughtPlayer' : PlayerCaughtPlayer, 'Role' : Role, 'GameState' : GameState} \ No newline at end of file
{{ objective.latitude }} {{ objective.longitude }} {{ objective.found_by|length }}{{ objective.hash }} + {{ objective.hash }}
{{ game.name }}{{ game.name }} {{ game.state.name}} {{ game.start_time }} {{ game.end_time }}