-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
16 changed files
with
832 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
FLASK_APP=main.py | ||
FLASK_DEBUG=1 | ||
FLASK_RUN_PORT=5000 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,199 @@ | ||
from datetime import datetime, timedelta | ||
from dateutil import parser as dp | ||
import logging | ||
import random | ||
import pytz | ||
from typing import Union | ||
from .CustomExceptions import ConferenceNotAllowed, ConferenceExists | ||
|
||
|
||
class Reservation: | ||
"""The Reservation class holds room reservations and running conferences.""" | ||
|
||
@staticmethod | ||
def format_event(input: dict) -> dict: | ||
"""Format the event for frontend template. | ||
Formatting could be done in the teamplte or in the brower as well but it seemed easier and | ||
faster to just do it here. | ||
""" | ||
|
||
item = input.copy() | ||
item['start_time'] = dp.isoparse(item['start_time']).strftime('%c') | ||
item['duration'] = str(timedelta(seconds=item['duration'])) if item['duration'] > 0 else '' | ||
return item | ||
|
||
def __init__(self, data: dict = None): | ||
|
||
self.id = int(data.get('id', random.random()*10e9)) | ||
self.name = data.get('name') | ||
self.mail_owner = data.get('mail_owner') | ||
self.timezone = pytz.timezone(data.get('timezone', 'UTC')) | ||
self.__duration = timedelta(seconds=int(data.get('duration', -1))) | ||
|
||
# Make it possible to pass datetime instances. Maybe for the future... | ||
if isinstance(data.get('start_time'), datetime): | ||
self.__start_time = data.get('start_time') | ||
else: | ||
self.__start_time = dp.isoparse(data.get('start_time')) | ||
# Only set timezone if datetime is naive | ||
if (self.__start_time.tzinfo is None or | ||
self.__start_time.tzinfo.utcoffset(self.__start_time) is None): | ||
self.__start_time = self.timezone.localize(self.__start_time) | ||
|
||
@property | ||
def start_time(self) -> str: | ||
"""Get a Java SimpleDateFormat compatible date string.""" | ||
|
||
return self.__start_time.isoformat().replace('000+', '+') | ||
# Disgusting hack to make isoformat() print the precision time in milliseconds instead | ||
# of microseconds, becasue Java can't handle that. -.- | ||
|
||
@property | ||
def duration(self) -> int: | ||
"""Get the conference duration in seconds. | ||
Duration is set to -1 if disabled. | ||
""" | ||
|
||
return int(self.__duration.total_seconds()) | ||
|
||
def to_dict(self) -> dict: | ||
"""Return the information about the event as dict""" | ||
|
||
output = { | ||
'id': self.id, | ||
'name': self.name, | ||
'start_time': self.start_time, | ||
'duration': self.duration | ||
} | ||
if self.mail_owner is not None: | ||
output['mail_owner'] = self.mail_owner | ||
return output | ||
|
||
def check_allowed(self, owner: str = None, start_time: str = None) -> bool: | ||
"""Check if the conference is allowed to start. | ||
The conference is check for owner and/or starting time.""" | ||
if start_time is None: | ||
start_time = datetime.now(datetime.timezone.utc).isoformat() | ||
if self.mail_owner != owner: | ||
raise ConferenceNotAllowed('This user is not allowed to start this conference!') | ||
if self.__start_time > dp.isoparse(start_time): | ||
raise ConferenceNotAllowed('The conference has not started yet.') | ||
return True | ||
|
||
|
||
class Manager: | ||
def __init__(self): | ||
self.__logger = logging.getLogger() | ||
self.__conferences = {} | ||
self.__reservations = {} | ||
|
||
@property | ||
def reservations(self) -> dict: | ||
"""Get all reservations as dict""" | ||
|
||
return self.__reservations | ||
|
||
@property | ||
def conferences_formatted(self) -> dict: | ||
"""Get all conferences formatted for the frontend""" | ||
|
||
return {key: Reservation.format_event(val) for key, val in self.__conferences.items()} | ||
|
||
@property | ||
def reservations_formatted(self) -> dict: | ||
"""Get all reservations formatted for the frontend""" | ||
|
||
return {key: Reservation.format_event(val) for key, val in self.__reservations.items()} | ||
|
||
@property | ||
def conferences(self) -> dict: | ||
"""Get all conferences as dict""" | ||
|
||
return self.__conferences | ||
|
||
def search_conference_by_name(self, name: str) -> Union[None, str]: | ||
"""Return the confernce ID for a given name""" | ||
|
||
for id, conference in self.conferences.items(): | ||
if conference.get('name') == name: | ||
return id | ||
return None | ||
|
||
def allocate(self, data: dict) -> dict: | ||
"""Check if the conference request matches a reservation.""" | ||
|
||
# Check for conflicting conference | ||
name = data.get('name') | ||
id = self.search_conference_by_name(name) | ||
if id is not None: | ||
self.__logger.info(f'Conference {id} already exists') | ||
raise ConferenceExists(id) | ||
# Check for existing reservation | ||
if name in self.reservations: | ||
# Raise ConferenceNotAllowed if necessary | ||
reservation = Reservation(self.reservations.get(name)) | ||
reservation.check_allowed(owner=data.get('mail_owner'), | ||
start_time=data.get('start_time')) | ||
self.__logger.debug('Reservation checked, conference can start') | ||
self.__conferences[reservation.id] = self.__reservations.pop(name) | ||
return reservation.to_dict() | ||
|
||
self.__logger.debug(f'No reservation found for room name {name}') | ||
id = self.add_conference(data) | ||
return self.__conferences[id] | ||
|
||
def delete_conference(self, id: int = None) -> bool: | ||
"""Delete a conference in the database""" | ||
|
||
try: | ||
self.__conferences.pop(int(id)) | ||
except KeyError: | ||
self.__logger.error(f'Could not remove conference {id} from the database') | ||
return False | ||
else: | ||
self.__logger.debug(f'Remove conference {id} from the database') | ||
return True | ||
|
||
def add_conference(self, data: dict) -> str: | ||
"""Add a conference to the database""" | ||
|
||
conference = Reservation(data) | ||
self.__conferences[conference.id] = conference.to_dict() | ||
self.__logger.debug(f'Add conference {conference.id} - {conference.name} to the database') | ||
return conference.id | ||
|
||
def get_conference(self, id: int = None) -> dict: | ||
"""Get the conference information""" | ||
|
||
if id in self.__conferences: | ||
return self.__conferences.get(id) | ||
return {} | ||
|
||
def delete_reservation(self, id: int = None, name: str = None) -> bool: | ||
"""Delete a reservation in the database""" | ||
|
||
if id is not None: | ||
for rname, reservation in self.__reservations.items(): | ||
if reservation.get('id') == int(id): | ||
name = rname | ||
break | ||
try: | ||
self.__reservations.pop(name) | ||
except KeyError: | ||
self.__logger.error(f'Could not remove reservation {name} from the database') | ||
return False | ||
else: | ||
self.__logger.debug(f'Remove reservation {name} from the database') | ||
return True | ||
|
||
def add_reservation(self, data: dict) -> int: | ||
"""Add a reservation to the database.""" | ||
|
||
reservation = Reservation(data) | ||
print(reservation.to_dict()) | ||
self.__reservations[reservation.name] = reservation.to_dict() | ||
self.__logger.debug(f'Add reservation for room {reservation.name} to the database') | ||
return reservation.id |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
class ConferenceExists(Exception): | ||
"""Raised if the conference already exists.""" | ||
|
||
def __init__(self, id=None): | ||
self.id = id | ||
|
||
|
||
class ConferenceNotAllowed(Exception): | ||
"""Raised if the user is not allowed to create the conference.""" | ||
|
||
def __init__(self, message=None): | ||
self.message = message |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import os | ||
import secrets | ||
from flask import Flask | ||
from flask_wtf.csrf import CSRFProtect | ||
from flask_bootstrap import Bootstrap | ||
from .Conferences import Manager | ||
from logging.config import dictConfig | ||
|
||
dictConfig({ | ||
'version': 1, | ||
'formatters': {'default': { | ||
'format': '[%(asctime)s] %(levelname)s in %(module)s: %(message)s', | ||
}}, | ||
'handlers': { | ||
'wsgi': { | ||
'class': 'logging.StreamHandler', | ||
'stream': 'ext://flask.logging.wsgi_errors_stream', | ||
'formatter': 'default' | ||
}, | ||
'file': { | ||
'class': 'logging.handlers.RotatingFileHandler', | ||
'formatter': 'default', | ||
'filename': 'log/flask.log' | ||
} | ||
}, | ||
'root': { | ||
'level': 'DEBUG', | ||
'handlers': ['wsgi', 'file'] | ||
} | ||
}) | ||
|
||
|
||
class Config(object): | ||
SECRET_KEY = os.environ.get('SECRET_KEY') or secrets.token_urlsafe(16) | ||
# See https://pythonhosted.org/Flask-Bootstrap/configuration.html | ||
BOOTSTRAP_SERVE_LOCAL = True | ||
|
||
|
||
app = Flask(__name__) | ||
app.config.from_object(Config) | ||
csrf = CSRFProtect(app) # Cross-Site Request Forgery protection for forms. | ||
Bootstrap(app) # Bootstrap system | ||
manager = Manager() | ||
|
||
from jire import routes |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
from flask_wtf import FlaskForm | ||
from wtforms import StringField, SubmitField, DateTimeLocalField, SelectField | ||
import wtforms.validators as formValidators | ||
import pytz | ||
|
||
|
||
class ReservationForm(FlaskForm): | ||
|
||
start_time = DateTimeLocalField(label='Day and time', | ||
validators=[formValidators.InputRequired()], | ||
format='%Y-%m-%dT%H:%M') | ||
|
||
timezone = SelectField(label='Timezone', | ||
choices=[(tz, tz.replace('_', ' ')) for tz in pytz.common_timezones], | ||
default='Europe/Berlin') | ||
|
||
name = StringField(label='Room name', | ||
validators=[formValidators.InputRequired()]) | ||
|
||
submit = SubmitField(label='Submit') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
from jire import app, manager, csrf | ||
from flask import request, render_template, redirect, url_for, jsonify | ||
from flask_api import status | ||
from .CustomExceptions import ConferenceExists, ConferenceNotAllowed | ||
from .forms import ReservationForm | ||
|
||
|
||
@app.route('/conferences', methods=['GET']) | ||
def show_conferences(): | ||
return render_template('conferences.html', | ||
active_conferences=manager.conferences_formatted) | ||
|
||
|
||
@app.route('/') | ||
@app.route('/reservations') | ||
def home(): | ||
form = ReservationForm() | ||
return render_template('reservations.html', | ||
form=form, | ||
active_reservations=manager.reservations_formatted) | ||
|
||
|
||
@app.route('/reservation/create', methods=['POST']) | ||
def reservation(): | ||
form = ReservationForm() | ||
if form.validate_on_submit(): | ||
manager.add_reservation(form.data) | ||
app.logger.info('New reservation validation successfull') | ||
else: | ||
app.logger.info('New reservation validation failed') | ||
print(form.errors.items()) | ||
return redirect(url_for('home')) | ||
|
||
|
||
@app.route('/reservation/delete/<id>', methods=['GET']) | ||
def delete_reservation(id): | ||
manager.delete_reservation(id=id) | ||
return redirect(url_for('home')) | ||
|
||
|
||
@app.route('/conference', methods=['POST']) | ||
@csrf.exempt | ||
def conference(): | ||
|
||
try: | ||
# If a user enters the conference, check for reservations | ||
output = manager.allocate(request.form) | ||
except ConferenceExists as e: | ||
# Conference already exists | ||
return jsonify({'conflict_id': e.id}), status.HTTP_409_CONFLICT | ||
except ConferenceNotAllowed as e: | ||
# Confernce cannot be created: user not allowed or conference has not started | ||
return jsonify({'message': e.message}), status.HTTP_403_FORBIDDEN | ||
else: | ||
return jsonify(output), status.HTTP_200_OK | ||
|
||
|
||
@app.route('/conference/<id>', methods=['GET', 'DELETE']) | ||
@csrf.exempt | ||
def conference_id(id): | ||
|
||
if request.method == 'GET': | ||
# In case of 409 CONFLICT Jitsi will request information about the conference | ||
return jsonify(manager.get_conference(id)), status.HTTP_200_OK | ||
elif request.method == 'DELETE': | ||
# Delete the conference after it's over | ||
if manager.delete_conference(id=id): | ||
return jsonify({'status': 'OK'}), status.HTTP_200_OK | ||
else: | ||
return jsonify({ | ||
'status': 'Failed', | ||
'message': f'Could not remove {id} from database.' | ||
}), status.HTTP_403_FORBIDDEN |
Oops, something went wrong.