Skip to content

Commit

Permalink
first working version
Browse files Browse the repository at this point in the history
  • Loading branch information
loelkes committed Jan 28, 2021
1 parent 33a786a commit b854e68
Show file tree
Hide file tree
Showing 16 changed files with 832 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .flaskenv
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
199 changes: 199 additions & 0 deletions jire/Conferences.py
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
12 changes: 12 additions & 0 deletions jire/CustomExceptions.py
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
45 changes: 45 additions & 0 deletions jire/__init__.py
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
20 changes: 20 additions & 0 deletions jire/forms.py
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')
73 changes: 73 additions & 0 deletions jire/routes.py
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
Loading

0 comments on commit b854e68

Please sign in to comment.