This commit is contained in:
parent
f7264e26d8
commit
d45d2fa982
11 changed files with 257 additions and 67 deletions
|
@ -6,26 +6,42 @@ import uuid
|
||||||
import struct
|
import struct
|
||||||
|
|
||||||
from . import app
|
from . import app
|
||||||
|
from .countdown import Cache
|
||||||
db = Walrus(host='localhost', port=6379, db=0)
|
|
||||||
|
|
||||||
@app.route('/api/v1/<uuid:id>', methods=['GET'])
|
@app.route('/api/v1/<uuid:id>', methods=['GET'])
|
||||||
def get_countdown(id):
|
def get_countdown(id):
|
||||||
ct = db.Hash(str(id))
|
cache = Cache.getInstance()
|
||||||
|
countdown = cache.get_countdown(id)
|
||||||
|
|
||||||
resp = ct.as_dict(decode=True)
|
response = countdown
|
||||||
resp['left'] = float(ct['total']) - (time() - float(ct['start']))
|
|
||||||
|
|
||||||
return resp
|
time_passed = time() - float(countdown['start'])
|
||||||
|
time_left = float(countdown['total']) - time_passed
|
||||||
|
response['left'] = time_left
|
||||||
|
response['roundtrip_start'] = request.args.get('roundtrip_start')
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
@app.route('/api/v1', methods=['POST'])
|
@app.route('/api/v1', methods=['POST'])
|
||||||
def create_countdown():
|
def create_countdown():
|
||||||
|
cache = Cache.getInstance()
|
||||||
|
|
||||||
countdown = request.json
|
countdown = request.json
|
||||||
ct_id = str(uuid.uuid4())
|
total = float(countdown['total'])
|
||||||
ct = db.Hash(ct_id)
|
response = cache.add_countdown(total)
|
||||||
ct.update(start=time(), total=countdown['total'])
|
return response
|
||||||
|
|
||||||
resp = ct.as_dict(decode=True)
|
@app.route('/api/v1/start/<uuid:id>', methods=['PATCH'])
|
||||||
resp['id'] = ct_id
|
def start_countdown(id):
|
||||||
|
cache = Cache.getInstance()
|
||||||
|
return cache.start_countdown(id)
|
||||||
|
|
||||||
return resp
|
@app.route('/api/v1/reset/<uuid:id>', methods=['PATCH'])
|
||||||
|
def reset_countdown(id):
|
||||||
|
cache = Cache.getInstance()
|
||||||
|
return cache.reset_countdown(id)
|
||||||
|
|
||||||
|
@app.route('/api/v1/stop/<uuid:id>', methods=['PATCH'])
|
||||||
|
def stop_countdown(id):
|
||||||
|
cache = Cache.getInstance()
|
||||||
|
return cache.stop_countdown(id)
|
||||||
|
|
|
@ -4,45 +4,105 @@
|
||||||
import log from 'loglevel';
|
import log from 'loglevel';
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
|
|
||||||
|
import {sleep} from '../js/netclock.js';
|
||||||
|
|
||||||
import './countdown.scss';
|
import './countdown.scss';
|
||||||
|
|
||||||
let api_base = "/countdown/api/v1/";
|
let api_base = "/countdown/api/v1/";
|
||||||
|
|
||||||
|
let padTime = (t) => {
|
||||||
|
let t_s = t.toString();
|
||||||
|
// Pads to at least two digits filling with 0
|
||||||
|
let leftPad = t_s.length < 2 ?
|
||||||
|
"0".repeat(2 - t.toString().length) :
|
||||||
|
"";
|
||||||
|
|
||||||
|
return `${leftPad+t_s}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
let formatTime = (h, m, s) => {
|
||||||
|
let htext = padTime(h);
|
||||||
|
let mtext = padTime(m);
|
||||||
|
let stext = padTime(s);
|
||||||
|
|
||||||
|
htext = htext !== "00" ? htext + ":" : "";
|
||||||
|
mtext = mtext !== "00" || htext !== "00" ? mtext + ":" : "";
|
||||||
|
|
||||||
|
return htext + mtext + stext;
|
||||||
|
};
|
||||||
|
|
||||||
|
let splitTimestamp = (t) => {
|
||||||
|
// Timestamp as full seconds
|
||||||
|
let seconds = Math.floor(t);
|
||||||
|
|
||||||
|
return {
|
||||||
|
"hours": ~~(seconds / 3600),
|
||||||
|
"minutes": ~~((seconds % 3600) / 60),
|
||||||
|
"seconds": ~~(seconds % 60),
|
||||||
|
"milliseconds": ~~((t % 1) * 1000)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// amount of seconds since last sync
|
||||||
|
let unsyncedSeconds = 0;
|
||||||
|
let countdown = undefined;
|
||||||
|
|
||||||
|
let updateCountdown = () => {
|
||||||
|
if(!countdown) return;
|
||||||
|
|
||||||
|
// Not started
|
||||||
|
if (countdown.start === "-1") {
|
||||||
|
let split = splitTimestamp(countdown.total);
|
||||||
|
$("#countdown").text(formatTime(split.hours, split.minutes, split.seconds));
|
||||||
|
$("#subtext").text("Countdown has not started");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let unsyncedLeft = countdown.left - unsyncedSeconds;
|
||||||
|
|
||||||
|
// Countdown already over
|
||||||
|
if (unsyncedLeft <= 0) {
|
||||||
|
if(countdownTimer) clearInterval(countdownTimer);
|
||||||
|
if(syncTimer) clearInterval(syncTimer);
|
||||||
|
|
||||||
const updateCountdown = () => $.getJSON({
|
|
||||||
url: api_base+countdown_id,
|
|
||||||
data: Date.now(),
|
|
||||||
success: function(countdown) {
|
|
||||||
if(countdown.left <= 0) {
|
|
||||||
// clear interval, don't count below 0
|
|
||||||
clearInterval(scheduledUpdater);
|
|
||||||
$("#countdown").text("Time is up!");
|
$("#countdown").text("Time is up!");
|
||||||
let end = new Date(Math.floor(countdown.start * 1000 + countdown.total * 1000));
|
let end = new Date(Math.floor(countdown.start * 1000 + countdown.total * 1000));
|
||||||
$("#subtext").text("This countdown ended on " + end.toLocaleDateString() + " " + end.toLocaleTimeString());
|
$("#subtext").text("This countdown ended on " + end.toLocaleDateString() + " " + end.toLocaleTimeString());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// total seconds left
|
let time = splitTimestamp(unsyncedLeft);
|
||||||
let sec = Math.floor(countdown.left);
|
let text = formatTime(time.hours, time.minutes, time.seconds);
|
||||||
// milliseconds left in seconds resolution
|
|
||||||
let frac = countdown.left - sec;
|
|
||||||
// milliseconds left in millisecond resolution
|
|
||||||
let milli = Math.floor(frac * 1000);
|
|
||||||
// seconds in hrs, minutes, seconds
|
|
||||||
let h = Math.floor(sec/(60*60));
|
|
||||||
let m = Math.floor(sec/60) - (h*60);
|
|
||||||
let s = sec - (m*60) - (h*60*60);
|
|
||||||
|
|
||||||
// let milliseconds pass, then set element to
|
sleep(time.milliseconds)
|
||||||
// amount of full seconds left
|
.then(() => $("#countdown").text(text));
|
||||||
let htext = padTime(h);
|
};
|
||||||
let mtext = padTime(m);
|
|
||||||
htext = htext !== "00" ? htext+":": "";
|
let unsyncTimer = undefined;
|
||||||
mtext = mtext !== "00" ? mtext+":": "";
|
|
||||||
setTimeout(() => $("#countdown").text(htext+mtext+padTime(s)), milli);
|
let sync = () => {
|
||||||
|
$.getJSON({
|
||||||
|
url: api_base + countdown_id,
|
||||||
|
success: async function(resp) {
|
||||||
|
if(unsyncTimer) clearInterval(unsyncTimer);
|
||||||
|
|
||||||
|
let time = splitTimestamp(resp.left);
|
||||||
|
await sleep(time.milliseconds);
|
||||||
|
|
||||||
|
countdown = resp;
|
||||||
|
countdown.left = ~~countdown.left;
|
||||||
|
unsyncedSeconds = 0;
|
||||||
|
unsyncTimer = setInterval(() => unsyncedSeconds++, 1000);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
let scheduledUpdater = setInterval(updateCountdown, 1000);
|
|
||||||
let padTime = t => `${"0".repeat(2-t.toString().length)+t}`;
|
|
||||||
|
|
||||||
|
|
||||||
|
// initialize the countdown
|
||||||
|
sync();
|
||||||
|
|
||||||
|
// update countdown from last sync and unsyncedSeconds in regular intervals
|
||||||
|
let countdownTimer = setInterval(updateCountdown, 100);
|
||||||
|
// sync in regular intervals
|
||||||
|
let syncTimer = setInterval(sync, 5000);
|
||||||
|
|
45
countdown/countdown.py
Normal file
45
countdown/countdown.py
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
from walrus import Walrus
|
||||||
|
from time import time
|
||||||
|
|
||||||
|
from uuid import UUID, uuid4
|
||||||
|
import struct
|
||||||
|
|
||||||
|
class Cache:
|
||||||
|
__instance = None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def getInstance():
|
||||||
|
if Cache.__instance == None:
|
||||||
|
Cache()
|
||||||
|
return Cache.__instance
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
if Cache.__instance != None:
|
||||||
|
raise Exception("Cache is a singleton. Use Cache.getInstance()")
|
||||||
|
Cache.__instance = self
|
||||||
|
self.db = Walrus(host='localhost', port=6379, db=0)
|
||||||
|
|
||||||
|
def add_countdown(self, total: float) -> UUID:
|
||||||
|
countdown_id = uuid4()
|
||||||
|
countdown = self.db.Hash(str(countdown_id))
|
||||||
|
countdown.update(id=str(countdown_id), start=-1, total=total)
|
||||||
|
return self.get_countdown(str(countdown_id))
|
||||||
|
|
||||||
|
def start_countdown(self, id: UUID) -> dict:
|
||||||
|
countdown = self.db.Hash(str(id))
|
||||||
|
countdown.update(start=time())
|
||||||
|
return countdown.as_dict(decode=True)
|
||||||
|
|
||||||
|
def stop_countdown(self, id: UUID) -> dict:
|
||||||
|
countdown = self.db.Hash(str(id))
|
||||||
|
countdown.update(start=-1)
|
||||||
|
return countdown.as_dict(decode=True)
|
||||||
|
|
||||||
|
def reset_countdown(self, id: UUID) -> dict:
|
||||||
|
countdown = self.db.Hash(str(id))
|
||||||
|
countdown.update(start=time())
|
||||||
|
return countdown.as_dict(decode=True)
|
||||||
|
|
||||||
|
def get_countdown(self, id: UUID) -> dict:
|
||||||
|
countdown = self.db.Hash(str(id))
|
||||||
|
return countdown.as_dict(decode=True)
|
|
@ -1,5 +1,12 @@
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import TimeField
|
from wtforms import fields
|
||||||
|
from wtforms.fields import html5
|
||||||
|
from wtforms.validators import Optional
|
||||||
|
|
||||||
class CountdownAdminForm(FlaskForm):
|
class CountdownAdminForm(FlaskForm):
|
||||||
totalTime = TimeField('Time')
|
hours = html5.IntegerField("Hours", [Optional()],
|
||||||
|
render_kw = {"min":0, "max":999, "placeholder": "00"})
|
||||||
|
minutes = html5.IntegerField("Minutes", [Optional()],
|
||||||
|
render_kw = {"min":0, "max":59, "placeholder": "00"})
|
||||||
|
seconds = html5.IntegerField("Seconds", [Optional()],
|
||||||
|
render_kw = {"min":0, "max":59, "placeholder": "00"})
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
{% extends "base.html" %}
|
|
||||||
{% block title %}Countdown{% endblock title %}
|
|
||||||
|
|
||||||
{% block body %}
|
|
||||||
Hello from Countdown
|
|
||||||
<div id="clock"></div>
|
|
||||||
{% endblock body %}
|
|
||||||
|
|
||||||
{% block scripts %}
|
|
||||||
<script src="{{ url_for('static', filename='dist/countdown.bundle.js') }}"></script>
|
|
||||||
{% endblock scripts %}
|
|
19
countdown/templates/countdown/create.html
Normal file
19
countdown/templates/countdown/create.html
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Countdown{% endblock title %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<form method="POST" action="{{ url_for('countdown.create') }}">
|
||||||
|
{{ form.csrf_token }}
|
||||||
|
{{ form.hours.label }}: {{ form.hours }}
|
||||||
|
{{ form.minutes.label }}: {{ form.minutes }}
|
||||||
|
{{ form.seconds.label }}: {{ form.seconds }}
|
||||||
|
<input type="submit" value="Go">
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% if clock %}
|
||||||
|
Clock: <a href="http://localhost:5000/countdown/{{ clock }}">http://localhost:5000/countdown/{{ clock }}</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock body %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
{% endblock scripts %}
|
15
countdown/templates/countdown/created.html
Normal file
15
countdown/templates/countdown/created.html
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Countdown{% endblock title %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
{% for clock in clocks %}
|
||||||
|
<div id="created-countdowns">
|
||||||
|
<div class="created-countdown">
|
||||||
|
Clock: <a href="http://localhost:5000/countdown/{{ clock }}">http://localhost:5000/countdown/{{ clock }}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endblock body %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
{% endblock scripts %}
|
|
@ -1,20 +1,37 @@
|
||||||
from flask import render_template, request, flash, redirect, url_for
|
from flask import render_template, request, flash, redirect, url_for, session
|
||||||
|
|
||||||
from . import app
|
from . import app
|
||||||
from . import forms
|
from . import forms
|
||||||
|
from .countdown import Cache
|
||||||
|
|
||||||
@app.route('/<uuid:countdown_id>', methods=['GET'])
|
@app.route('/', methods=['GET', 'POST'])
|
||||||
def countdown(countdown_id):
|
def create():
|
||||||
return render_template('countdown/countdown.html', countdown_id=countdown_id)
|
session.permanent = True
|
||||||
|
if not session.get('created_countdowns'):
|
||||||
|
session['created_countdowns'] = []
|
||||||
|
|
||||||
@app.route('/', methods=['GET', 'POST', 'PUT'])
|
|
||||||
def countdown_admin():
|
|
||||||
form = forms.CountdownAdminForm(request.form)
|
form = forms.CountdownAdminForm(request.form)
|
||||||
if request.method == 'POST' and form.validate():
|
if request.method == 'POST' and form.validate():
|
||||||
|
cache = Cache.getInstance()
|
||||||
|
total = form.seconds.data or 0
|
||||||
|
total += (form.minutes.data or 0) * 60
|
||||||
|
total += (form.hours.data or 0) * 60 * 60
|
||||||
|
|
||||||
|
countdown_id = cache.add_countdown(total)
|
||||||
# user = User(form.username.data, form.email.data,
|
# user = User(form.username.data, form.email.data,
|
||||||
# form.password.data)
|
# form.password.data)
|
||||||
# db_session.add(user)
|
# db_session.add(user)
|
||||||
flash('Thanks for registering')
|
session['created_countdowns'].append(str(countdown_id))
|
||||||
return redirect(url_for('login'))
|
session.modified = True
|
||||||
|
return redirect(url_for('countdown.created'))
|
||||||
|
|
||||||
|
return render_template('countdown/create.html', form=form, clock=None)
|
||||||
|
|
||||||
|
@app.route('/mine', methods=['GET'])
|
||||||
|
def created():
|
||||||
|
return render_template('countdown/created.html', clocks=session['created_countdowns'])
|
||||||
|
|
||||||
|
@app.route('/<uuid:countdown_id>', methods=['GET'])
|
||||||
|
def view(countdown_id):
|
||||||
|
return render_template('countdown/countdown.html', countdown_id=countdown_id)
|
||||||
|
|
||||||
return render_template('countdown/countdown_admin.html', form=form, clock=None)
|
|
||||||
|
|
|
@ -5,3 +5,9 @@ import $ from 'jquery';
|
||||||
if (process.env.LOG_LEVEL) {
|
if (process.env.LOG_LEVEL) {
|
||||||
log.setDefaultLevel(process.env.LOG_LEVEL);
|
log.setDefaultLevel(process.env.LOG_LEVEL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sleep(ms) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
export {sleep};
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
app.secret_key = b'_5#y2L"F4Q8z\n\xec]/'
|
||||||
|
app.config.update(SESSION_COOKIE_SAMESITE='Strict')
|
||||||
|
|
||||||
|
import views
|
||||||
|
|
||||||
from countdown import app as countdown
|
from countdown import app as countdown
|
||||||
app.register_blueprint(countdown, url_prefix="/countdown")
|
app.register_blueprint(countdown, url_prefix="/countdown")
|
||||||
|
|
||||||
import views
|
|
||||||
|
|
|
@ -4,3 +4,15 @@ POST http://localhost:5000/countdown/api/v1
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{"total": "150"}
|
{"total": "150"}
|
||||||
|
|
||||||
|
# Set id
|
||||||
|
:id = cddabcb5-9da1-4ecb-ad36-2ca468da68e1
|
||||||
|
|
||||||
|
# Start
|
||||||
|
PATCH http://localhost:5000/countdown/api/v1/start/:id
|
||||||
|
|
||||||
|
# Stop
|
||||||
|
PATCH http://localhost:5000/countdown/api/v1/stop/:id
|
||||||
|
|
||||||
|
# Reset
|
||||||
|
PATCH http://localhost:5000/countdown/api/v1/reset/:id
|
||||||
|
|
Loading…
Reference in a new issue