Skip to content
Snippets Groups Projects
Unverified Commit 9f659c6e authored by Sandro Lutz's avatar Sandro Lutz
Browse files

Add attendance tracking

parent 0954e5b9
No related branches found
No related tags found
No related merge requests found
Showing
with 226 additions and 55 deletions
......@@ -2,11 +2,13 @@ from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_bootstrap import Bootstrap
from flask_wtf.csrf import CSRFProtect
from flask_mail import Mail
# global variable initialization
db = SQLAlchemy()
mail = Mail()
csrf = CSRFProtect()
def create_app():
# initialize flask app and configure
......@@ -16,10 +18,12 @@ def create_app():
# initialize Mail
mail.init_app(app)
# initialize CSRF protection
csrf.init_app(app)
# initialize ORM
db.init_app(app)
# add database migration from flask-migrate
Migrate(app, db)
......@@ -39,9 +43,4 @@ def create_app():
from .bouncer import bouncer_bp
app.register_blueprint(bouncer_bp)
# @app.route('/')
# def hello_world():
# print(app.config.get('TOTAL_SPOTS'))
# return 'Hello, World!'
return app
......@@ -8,13 +8,10 @@ from .models import User
def login_required(f):
"""
Requires that the user is logged in.
This is a wrapper for the @login_required decorator.
Error 403: shwon if trying to access with an api key (Authorization header).
"""
@wraps(f)
def wrapped(*args, **kwargs):
print('test')
if not is_authenticated():
abort(403)
......@@ -23,15 +20,19 @@ def login_required(f):
def is_authenticated():
return 'userID' in session and User.query.get(session['userID']) is not None
return 'userID' in session and session['userID'] is not None and User.query.get(session['userID']) is not None
def get_authenticated_user():
return User.query.get(session['userID']) if 'userID' in session else None
return User.query.get(session['userID']) if 'userID' in session and session['userID'] is not None else None
def login(email, password):
if email is None or password is None:
return False
user = User.query.filter(User.email == email).first()
if user.check_password(password):
session['userID'] = user._id
return True
......
......@@ -3,8 +3,9 @@ from flask import flash, redirect, render_template, url_for, request, abort, mak
from . import bouncer_bp
from ..auth import login_required, is_authenticated, get_authenticated_user
from ..controllers import FreeSpotController, UserController, ReservationController, RecordController
from ..exceptions import NoFreeSpotError
@bouncer_bp.route('/')
@bouncer_bp.route('/', methods=['GET', 'POST'])
def home():
free_spots = FreeSpotController.get_free_spots()
......@@ -12,6 +13,95 @@ def home():
user = get_authenticated_user()
reservation = ReservationController.get_active_reservation(user)
record = RecordController.get_active_record(user)
return make_response(render_template('bouncer/home_authenticated.html', title='Home', free_spots=free_spots, name=user.name, email=user.email))
if request.method == 'POST':
if request.form['action'] == 'enter':
try:
UserController.start_record(user)
flash('You are allowed to enter.')
return redirect(url_for('bouncer.home'))
except NoFreeSpotError:
flash('No free spots available! Try again later.', 'error')
if request.form['action'] == 'reserve':
try:
UserController.reserve(user)
flash('Your reservation has been recorded. Your request for entry will be granted immediately as long as your reservation is valid.')
return redirect(url_for('bouncer.home'))
except NoFreeSpotError:
flash('No free spots available! Try again later.', 'error')
return make_response(render_template('bouncer/home_authenticated.html', title='Home', free_spots=free_spots, user=user, reservation=reservation, record=record))
return make_response(render_template('bouncer/home_anonymous.html', title='Home', free_spots=free_spots))
@bouncer_bp.route('/confirm/leave', methods=['GET', 'POST'])
@login_required
def confirm_leave():
free_spots = FreeSpotController.get_free_spots()
user = get_authenticated_user()
record = RecordController.get_active_record(user)
if not user.is_confirmed:
flash('Please confirm your email address first!', 'warning')
return redirect(url_for('bouncer.home'))
if record is None:
flash('You didn\'t report that you were there.', 'warning')
return redirect(url_for('bouncer.home'))
if request.method == 'POST':
if request.form['action'] == 'leave':
RecordController.terminate(record)
flash('You have been checked out successfully.')
return redirect(url_for('bouncer.home'))
return make_response(render_template('bouncer/confirm_leave.html', title='Confirm: leave', user=user, free_spots=free_spots))
@bouncer_bp.route('/confirm/enter', methods=['GET', 'POST'])
@login_required
def confirm_enter():
free_spots = FreeSpotController.get_free_spots()
user = get_authenticated_user()
if not user.is_confirmed:
flash('Please confirm your email address first!', 'warning')
return redirect(url_for('bouncer.home'))
if RecordController.has_active_record(user):
flash('You already reported to be there!', 'warning')
return redirect(url_for('bouncer.home'))
if request.method == 'POST':
if request.form['action'] == 'enter':
try:
UserController.start_record(user)
flash('You are cleared for entry.')
return redirect(url_for('bouncer.home'))
except NoFreeSpotError:
flash ('No free spots available! Try again later.', 'error')
return redirect(url_for('bouncer.home'))
return make_response(render_template('bouncer/confirm_enter.html', title='Confirm: enter', user=user, free_spots=free_spots))
@bouncer_bp.route('/confirm/cancel', methods=['GET', 'POST'])
@login_required
def confirm_cancel():
free_spots = FreeSpotController.get_free_spots()
user = get_authenticated_user()
reservation = ReservationController.get_active_reservation(user)
if not user.is_confirmed:
flash('Please confirm your email address first!', 'warning')
return redirect(url_for('bouncer.home'))
if not reservation:
flash('You do not have any reservations!', 'warning')
return redirect(url_for('bouncer.home'))
if request.method == 'POST':
if request.form['action'] == 'cancel':
ReservationController.cancel(reservation)
flash('Your reservation has been cancelled.')
return redirect(url_for('bouncer.home'))
return make_response(render_template('bouncer/confirm_cancel.html', title='Confirm: cancel reservation', user=user, free_spots=free_spots))
import threading
from datetime import datetime
from sqlalchemy import DateTime, cast, func
from sqlalchemy import DateTime, cast, func, or_
from flask import current_app
from app import models
from app.models import Record, Reservation
from app import db
from .lock import Lock
from ..exceptions import NoFreeSpotError
......@@ -36,14 +36,14 @@ class FreeSpotController():
total_spots = current_app.config.get('TOTAL_SPOTS')
now = datetime.now()
active_records_count = db.session.query(func.count(models.Record._id)) \
.filter(cast(models.Record.time_start,DateTime) <= now) \
.filter(cast(models.Record.time_end,DateTime) >= now) \
active_records_count = db.session.query(func.count(Record._id)) \
.filter(cast(Record.time_start,DateTime) <= now) \
.filter(or_(cast(Record.time_end,DateTime) >= now, Record.time_end == None)) \
.scalar()
valid_reservations_count = db.session.query(func.count(models.Reservation._id)) \
.filter(cast(models.Reservation.time_start,DateTime) <= now) \
.filter(cast(models.Reservation.time_end,DateTime) >= now) \
valid_reservations_count = db.session.query(func.count(Reservation._id)) \
.filter(cast(Reservation.time_start,DateTime) <= now) \
.filter(cast(Reservation.time_end,DateTime) >= now) \
.scalar()
......
import threading
from datetime import datetime
from sqlalchemy import DateTime, cast, func
from sqlalchemy import DateTime, cast, func, or_
from app import db
from app.models import Record
from .lock import Lock
......@@ -22,7 +22,6 @@ class RecordController():
record.user = user
record.time_start = datetime.now()
record.time_end = None
record.name = reservation.name
db.session.add(record)
db.session.commit()
......@@ -31,18 +30,17 @@ class RecordController():
@classmethod
def create_from_reservation(cls, reservation):
with cls.spot_lock:
if not user.is_confirmed:
if not reservation.user.is_confirmed:
raise UserNotConfirmedError
if reservation.is_valid:
time_start = datetime.now()
reservation.time_end = time_start
record = models.Record()
record = Record()
record.user = reservation.user
record.time_start = time_start
record.time_end = None
record.name = reservation.name
db.session.add(record)
db.session.commit()
......@@ -60,7 +58,8 @@ class RecordController():
now = datetime.now()
return Record.query \
.filter(cast(Record.time_start,DateTime) <= now) \
.filter(cast(Record.time_end,DateTime) > now)
.filter(or_(cast(Record.time_end,DateTime) > now, Record.time_end == None)) \
.first()
@classmethod
......
......@@ -5,6 +5,7 @@ from flask import current_app
from app import db
from app.models import Reservation
from .lock import Lock
from .free_spot import FreeSpotController
from ..exceptions import ReservationExpiredError, NoFreeSpotError, UserNotConfirmedError
class ReservationController():
......@@ -14,16 +15,15 @@ class ReservationController():
@classmethod
def create(cls, user):
with cls.spot_lock:
cls.check_free_spots()
FreeSpotController.check_free_spots()
if not user.is_confirmed:
raise UserNotConfirmedError
record = Reservation()
record.user = user
record.name = name
record.time_start = datetime.now()
record.time_end = datetime.now() + current_app.config('RESERVATION_DURATION', timedelta(hours=1))
record.time_end = datetime.now() + current_app.config.get('RESERVATION_DURATION', timedelta(hours=1))
db.session.add(record)
db.session.commit()
......@@ -39,7 +39,8 @@ class ReservationController():
now = datetime.now()
return Reservation.query \
.filter(cast(Reservation.time_start,DateTime) <= now) \
.filter(cast(Reservation.time_end,DateTime) > now)
.filter(cast(Reservation.time_end,DateTime) > now) \
.first()
@classmethod
......
......@@ -2,11 +2,14 @@ from flask import flash, redirect, render_template, url_for, request, abort, mak
from ..controllers.user import UserController
from ..exceptions import UserRegistrationInvalidDataError
from ..auth import login as auth_login, logout as auth_logout
from ..auth import login as auth_login, logout as auth_logout, is_authenticated
from . import login_bp
@login_bp.route('/register', methods=['GET', 'POST'])
def register():
if is_authenticated():
return redirect(url_for('bouncer.home'))
errors = None
if request.method == 'POST':
try:
......@@ -28,7 +31,7 @@ def register():
return make_response(render_template('login/register.html', title='Registration', errors=errors))
@login_bp.route('/confirm_email')
@login_bp.route('/confirm/email')
def confirm_email():
email = request.args.get('email')
token = request.args.get('token')
......@@ -45,6 +48,9 @@ def confirm_email():
@login_bp.route('/login', methods=['GET', 'POST'])
def login():
if is_authenticated():
return redirect(url_for('bouncer.home'))
if request.method == 'POST':
if auth_login(request.form['email'], request.form['password']):
return redirect(url_for('bouncer.home'))
......
......@@ -19,4 +19,4 @@ class Reservation(db.Model):
@property
def is_valid(self):
return self.time_end - datetime.now() < timedelta(seconds=0)
return self.time_end - datetime.now() > timedelta(seconds=0)
{% import "bootstrap/utils.html" as utils %}
{% extends "base.html" %}
{% block body %}
<div class="content-section">
<br/>
{{ utils.flashed_messages() }}
<br/>
<div class="center-narrow">
<h1>Bastli Bouncer</h1>
<br/>
<p>We currently have <span class="free_spots">{{ free_spots }} free spots</span> in our workshop.</p>
<p>
Welcome, {{ user.name }} ({{ user.email }}).
<a class="btn btn-default" role="button" href="{{ url_for('login.logout') }}">Logout</a>
</p>
<br/>
{% block content %}
{% endblock %}
</div>
</div>
{% endblock %}
{% import "bootstrap/utils.html" as utils %}
{% extends "base_authenticated.html" %}
{% block content %}
<h2>Cancel Reservation</h2>
<p>Do you really want to cancel your reservation?</p>
<form action="" method="post">
<input type="hidden" name="action" value="cancel">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input class="btn btn-primary" type="submit" value="Cancel reservation now"> or
<a class="btn btn-default" role="button" href="{{ url_for('bouncer.home') }}">Go back</a>
</form>
{% endblock %}
{% import "bootstrap/utils.html" as utils %}
{% extends "base_authenticated.html" %}
{% block content %}
<h2>Enter Bastli</h2>
<p>You request entry by clicking below.</p>
<form action="" method="post">
<input type="hidden" name="action" value="enter">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input class="btn btn-primary" type="submit" value="Apply for entry now"> or
<a class="btn btn-default" role="button" href="{{ url_for('bouncer.home') }}">Go back</a>
</form>
{% endblock %}
{% import "bootstrap/utils.html" as utils %}
{% extends "base_authenticated.html" %}
{% block content %}
<h2>Leave Bastli</h2>
<p>You confirm that you have left the workshop by clicking below.</p>
<form action="" method="post">
<input type="hidden" name="action" value="leave">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input class="btn btn-primary" type="submit" value="Leave now"> or
<a class="btn btn-default" role="button" href="{{ url_for('bouncer.home') }}">Go back</a>
</form>
{% endblock %}
{% import "bootstrap/utils.html" as utils %}
{% extends "base.html" %}
{% block body %}
<div class="content-section">
<br/>
{{ utils.flashed_messages() }}
<br/>
<div class="center-narrow">
<h1>Bastli Bouncer</h1>
<br/>
<p>We currently have <span class="free_spots">{{ free_spots }} free spots</span> in our workshop.</p>
<p>
Welcome, {{ name }} ({{ email }}).
<a class="btn btn-default" role="button" href="{{ url_for('login.logout') }}">Logout</a>
</p>
<br/>
</div>
</div>
{% extends "base_authenticated.html" %}
{% block content %}
{% if record %}
<div class="active_record">
You reported to be at Bastli.
<a class="btn btn-default" role="button" href="{{ url_for('bouncer.confirm_leave') }}">Leave now</a>
</div>
{% elif reservation %}
<div class="active_reservation">
You have a valid reservation expiring at {{ reservation.time_end.strftime("%H:%M") }}.
<a class="btn btn-default" role="button" href="{{ url_for('bouncer.confirm_enter') }}">Enter now</a>
or
<a class="btn btn-default" role="button" href="{{ url_for('bouncer.confirm_cancel') }}">Cancel reservation</a>
</div>
{% else %}
<form action="" method="post">
<input type="hidden" name="action" value="reserve">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input class="btn btn-primary" type="submit" value="Reserve a spot">
</form>
or
<form action="" method="post">
<input type="hidden" name="action" value="enter">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input class="btn btn-primary" type="submit" value="Apply for entry">
</form>
{% endif %}
{% endblock %}
......@@ -16,6 +16,7 @@
<input type="hidden" name="email" value="{{ email }}">
<input type="hidden" name="token" value="{{ token }}">
<input type="hidden" name="action" value="confirm">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input class="btn btn-primary" type="submit" value="Confirm">
<a class="btn btn-default" role="button" href="{{ url_for('bouncer.home') }}">Abort</a>
</form>
......
......@@ -15,6 +15,7 @@
<input type="email" id="email" name="email" value="{{ request.form.email }}"><br/>
<label for="name">Password:</label><br>
<input type="password" id="password" name="password" value="{{ request.form.password }}"><br/>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input class="btn btn-primary" type="submit" value="Login"> or
<a class="btn btn-default" role="button" href="{{ url_for('bouncer.home') }}">Go back</a>
</form>
......
......@@ -19,6 +19,7 @@
<input type="password" id="password" name="password" value="{{ request.form.password }}"><br/>
<label for="name">Repeat Password:</label><br>
<input type="password" id="password2" name="password2" value="{{ request.form.password2 }}"><br/>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input class="btn btn-primary" type="submit" value="Register"> or
<a class="btn btn-default" role="button" href="{{ url_for('bouncer.home') }}">Go back</a>
</form>
......
......@@ -13,6 +13,7 @@ services:
- MYSQL_RANDOM_ROOT_PASSWORD=yes
volumes:
- ./.data/:/var/lib/mysql
- /etc/localtime:/etc/localtime
smtp:
build:
dockerfile: Dockerfile.smtp-mock
......@@ -21,6 +22,8 @@ services:
restart: always
networks:
- backend
volumes:
- /etc/localtime:/etc/localtime
networks:
backend:
......@@ -4,7 +4,7 @@ from datetime import timedelta
TOTAL_SPOTS = 3
RESERVATION_DURATION = timedelta(hours=1)
RECORD_TIMEOUT = timedelta(hours=24)
USER_TOKEN_TIMEOUT = timedelta(hours=12)
USER_TOKEN_TIMEOUT = timedelta(hours=4)
SERVER_NAME = 'localhost:5000'
......
......@@ -11,6 +11,7 @@ BASE_DOCKER_RUN_COMMAND="${SUDO_COMMAND} docker run -it \
--network bastli-bouncer_backend \
-v ${PWD}:/bastlibouncer \
-v ${PWD}/instance/config.dev.py:/bastlibouncer/instance/config.py \
-v /etc/localtime:/etc/localtime
bastlibouncer-dev"
case $1 in
......@@ -32,7 +33,7 @@ case $1 in
$SUDO_COMMAND docker-compose stop
;;
*)
echo "Unknown sub-command for command \"db\"."
echo "Unknown sub-command for command \"services\"."
exit 1
;;
esac
......
......@@ -6,6 +6,7 @@ flask-bootstrap
flask-migrate
flask-sqlalchemy
Flask-Mail
flask-wtf
py3-validate-email
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment