Commit dcbad8b3 authored by Martin Mareš's avatar Martin Mareš
Browse files

Merge remote-tracking branch 'origin/master' into zs-dobrichovice

parents dcf560cf 049e07d2
DEST=/srv/zoom-zs-dobrichovice/app
install:
rsync -av --delete zoom.py templates $(DEST)/
rsync -av --delete zoom.py static templates $(DEST)/
touch $(DEST)/force-reload
body {
background-color: white;
color: black;
}
#intro {
max-width: 50em;
}
#heading {
position: relative;
height: 2.5ex;
}
#schedule {
position: relative;
}
.room {
border: 1px solid green;
}
.slot {
border-bottom: 1px solid green;
}
.slotlabel {
display: flex;
justify-content: center;
align-items: center;
margin: 0;
font-weight: bold;
}
.roomhead p {
text-align: center;
font-weight: bold;
margin: 0;
}
.meeting {
border: 1px solid blue;
background-color: #ccccff;
overflow: hidden;
}
.meeting:hover {
background-color: #aaaaff;
z-index: 1;
}
.collision {
border: 1px solid red;
background-color: #ffcccc;
}
.collision:hover {
background-color: #ffaaaa;
}
.mtime {
margin: 0;
font-size: 11px;
}
.mdesc {
margin: 0;
margin-top: 0.5ex;
font-size: 9px;
}
#now {
height: 1px;
background-color: #cc0000;
opacity: 0.3;
z-index: 10;
}
<!DOCTYPE html>
<html>
<head>
<style>
body {
background-color: white;
color: black;
}
#intro {
max-width: 50em;
}
#heading {
position: relative;
height: 2.5ex;
}
#schedule {
position: relative;
}
.room {
border: 1px solid green;
}
.slot {
border-bottom: 1px solid green;
}
.slotlabel {
display: flex;
justify-content: center;
align-items: center;
margin: 0;
font-weight: bold;
}
.roomhead p {
text-align: center;
font-weight: bold;
margin: 0;
}
.meeting {
border: 1px solid blue;
background-color: #ccccff;
overflow: hidden;
}
.meeting:hover {
background-color: #aaaaff;
}
.collision {
border: 1px solid red;
background-color: #ffcccc;
}
.collision:hover {
background-color: #ffaaaa;
}
.mtime {
margin: 0;
font-size: 11px;
}
.mdesc {
margin: 0;
margin-top: 0.5ex;
font-size: 9px;
}
</style>
<title>MFF Zoom Schedule</title>
<link rel=stylesheet href="{{ url_for('static', filename='mffzoom.css') }}?v=1" type='text/css' media=all>
</head>
<body>
<h1>Zoom ZŠ Dobřichovice</h1>
<p id=intro>This table summarizes meetings scheduled in our Zoom accounts. Use standard Zoom interface
to create and modify reservations. Please keep in mind that there is a slight delay between Zoom
and this table. Please report all bugs to Martin Mareš.</p>
<form method=GET action="?">
<form id=f method=GET action="?">
<label for=date>Date:</label>
<input id=date type=date name=date value="{{ g.date }}">
<input id=date type=date name=date step=1 value="{{ g.date }}">
<button type=button onclick="document.getElementById('date').stepDown(); document.getElementById('f').requestSubmit()"></button>
<button type=button onclick="document.getElementById('date').stepUp(); document.getElementById('f').requestSubmit()"></button>
<select name=hours>
<option value=0{{ " selected" if g.hours==0 else "" }}>Study hours</option>
<option value=1{{ " selected" if g.hours==1 else "" }}>Working hours</option>
......@@ -102,6 +47,9 @@
<p class=mdesc>{{ m.topic }}
</div>
{% endfor %}
{% if g.now %}
<div id=now style='position: absolute; left: {{ g.now.x }}px; top: {{ g.now.y }}px; width: {{ g.now.w }}px;'></div>
{% endif %}
</div>
</body>
</html>
import json
from flask import Flask, render_template, request, g
from flask import Flask, render_template, request, g, request_tearing_down
import psycopg2
import psycopg2.extras
import time
......@@ -12,6 +12,8 @@ import dateutil.tz
app = Flask(__name__)
app.config.from_pyfile('config.py')
app.jinja_env.lstrip_blocks = True
app.jinja_env.trim_blocks = True
### Database connection ###
......@@ -42,6 +44,17 @@ def db_query(query, args=()):
db_connect()
db.execute(query, args)
def db_reset_signal(sender, **extra):
# At the end of every request, we have to close the implicitly opened
# transaction. Otherwise we end up with serving stale data.
if db_connection is not None:
try:
db_connection.rollback()
except:
pass
request_tearing_down.connect(db_reset_signal, app)
### Schedule ###
def get_date():
......@@ -123,6 +136,15 @@ def main_page():
"label": (dt + timedelta(seconds = first_slot + i*slot_size)).strftime("%H:%M"),
} for i in range(num_slots)]
dt_now = datetime.now(tz=dateutil.tz.tzlocal())
rel_now = dt_now.timestamp() - dt.timestamp() - first_slot
if rel_now > 0 and rel_now < num_slots * slot_size:
g.now = {
"x": 0,
"y": int(rel_now / 3600. * room_hour_height),
"w": row_offset + num_rooms * room_box_width + 20,
}
# XXX: No meeting is ever longer than 24 hours
db_query("""
SELECT m.meeting_id, m.topic, s.start_time, s.duration, u.email, u.full_name
......
......@@ -16,7 +16,7 @@ CREATE TABLE zoom_meetings (
CREATE TABLE zoom_schedule (
id serial PRIMARY KEY,
mid bigint NOT NULL REFERENCES zoom_meetings(mid) ON DELETE CASCADE,
mid int NOT NULL REFERENCES zoom_meetings(mid) ON DELETE CASCADE,
occurrence_id bigint DEFAULT 0, -- Occurrence for recurring meetings, 0 otherwise
start_time timestamp NOT NULL,
duration int NOT NULL, -- minutes
......
DEST=/srv/zoom-zs-dobrichovice/hook
install:
cp hook.wsgi $(DEST)/
cp hook.wsgi find-collisions.py $(DEST)/
touch $(DEST)/force-reload
#!/usr/bin/env python3
import configparser
import psycopg2
import psycopg2.extras
import datetime
import dateutil.tz
import time
config = configparser.ConfigParser()
config.read('zoom.ini')
db_conn = psycopg2.connect(dbname=config['db']['name'], user=config['db']['user'], password=config['db']['passwd'], host="127.0.0.1")
db = db_conn.cursor(cursor_factory=psycopg2.extras.NamedTupleCursor)
db.execute("""
SELECT
m.meeting_id, m.topic, u.uid, u.full_name, s.occurrence_id, s.start_time, s.duration
FROM
zoom_meetings m
JOIN zoom_users u ON u.uid = m.host_uid
JOIN zoom_schedule s ON s.mid = m.mid
WHERE
s.start_time >= NOW()
ORDER BY u.uid, s.start_time, m.mid
""")
class Meeting:
def __init__(self, row):
for f in row._fields:
setattr(self, f, getattr(row, f))
self.start_time = self.start_time.replace(tzinfo=dateutil.tz.tz.tzutc()).astimezone(dateutil.tz.gettz())
self.end_time = self.start_time + datetime.timedelta(minutes=self.duration)
self.time_range = self.start_time.strftime("%Y-%m-%d %H:%M") + "-" + self.end_time.strftime("%H:%M")
def __str__(self):
return "Meeting(" + ", ".join([ k + ":" + str(getattr(self, k)) for k in dir(self) if k[0] != '_' ]) + ")"
last_uid = None
running = []
for row in db:
m = Meeting(row)
# print(m)
if m.uid != last_uid:
last_uid = m.uid
running = []
while running and running[0].end_time <= m.start_time:
running.pop(0)
colls = [ r for r in running if r.end_time > m.start_time + datetime.timedelta(minutes=5) ]
if colls:
print(f"Collison for {m.time_range} {m.topic} [{m.meeting_id}:{m.occurrence_id} {m.full_name}]")
for c in colls:
print(f"\t{c.time_range} {c.topic} [{c.meeting_id}:{c.occurrence_id}]")
print()
pos = sum(1 for r in running if r.end_time <= m.end_time)
running.insert(pos, m)
......@@ -38,6 +38,15 @@ def db_query(query, args=()):
db_connect()
db.execute(query, args)
def db_reset():
# At the end of every request, we have to close the implicitly opened
# transaction. Otherwise we end up with serving stale data.
if db_connection is not None:
try:
db_connection.rollback()
except:
pass
### Utilities ###
def parse_time(iso_time):
......@@ -279,3 +288,5 @@ def application(env, start_response):
except Exception as exc:
app.log(traceback.print_exception(etype=None, value=exc, tb=exc.__traceback__))
return app.http_error(500, "Internal server error")
finally:
db_reset()
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment