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
|
||||
|
||||
from . import app
|
||||
|
||||
db = Walrus(host='localhost', port=6379, db=0)
|
||||
from .countdown import Cache
|
||||
|
||||
@app.route('/api/v1/<uuid:id>', methods=['GET'])
|
||||
def get_countdown(id):
|
||||
ct = db.Hash(str(id))
|
||||
cache = Cache.getInstance()
|
||||
countdown = cache.get_countdown(id)
|
||||
|
||||
resp = ct.as_dict(decode=True)
|
||||
resp['left'] = float(ct['total']) - (time() - float(ct['start']))
|
||||
response = countdown
|
||||
|
||||
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'])
|
||||
def create_countdown():
|
||||
cache = Cache.getInstance()
|
||||
|
||||
countdown = request.json
|
||||
ct_id = str(uuid.uuid4())
|
||||
ct = db.Hash(ct_id)
|
||||
ct.update(start=time(), total=countdown['total'])
|
||||
total = float(countdown['total'])
|
||||
response = cache.add_countdown(total)
|
||||
return response
|
||||
|
||||
resp = ct.as_dict(decode=True)
|
||||
resp['id'] = ct_id
|
||||
@app.route('/api/v1/start/<uuid:id>', methods=['PATCH'])
|
||||
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 $ from 'jquery';
|
||||
|
||||
import {sleep} from '../js/netclock.js';
|
||||
|
||||
import './countdown.scss';
|
||||
|
||||
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) :
|
||||
"";
|
||||
|
||||
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!");
|
||||
let end = new Date(Math.floor(countdown.start*1000 + countdown.total*1000));
|
||||
$("#subtext").text("This countdown ended on "+ end.toLocaleDateString()+" "+end.toLocaleTimeString());
|
||||
return;
|
||||
}
|
||||
return `${leftPad+t_s}`;
|
||||
};
|
||||
|
||||
// total seconds left
|
||||
let sec = Math.floor(countdown.left);
|
||||
// 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 formatTime = (h, m, s) => {
|
||||
let htext = padTime(h);
|
||||
let mtext = padTime(m);
|
||||
let stext = padTime(s);
|
||||
|
||||
// let milliseconds pass, then set element to
|
||||
// amount of full seconds left
|
||||
let htext = padTime(h);
|
||||
let mtext = padTime(m);
|
||||
htext = htext !== "00" ? htext+":": "";
|
||||
mtext = mtext !== "00" ? mtext+":": "";
|
||||
setTimeout(() => $("#countdown").text(htext+mtext+padTime(s)), milli);
|
||||
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 scheduledUpdater = setInterval(updateCountdown, 1000);
|
||||
let padTime = t => `${"0".repeat(2-t.toString().length)+t}`;
|
||||
let unsyncedLeft = countdown.left - unsyncedSeconds;
|
||||
|
||||
// Countdown already over
|
||||
if (unsyncedLeft <= 0) {
|
||||
if(countdownTimer) clearInterval(countdownTimer);
|
||||
if(syncTimer) clearInterval(syncTimer);
|
||||
|
||||
$("#countdown").text("Time is up!");
|
||||
let end = new Date(Math.floor(countdown.start * 1000 + countdown.total * 1000));
|
||||
$("#subtext").text("This countdown ended on " + end.toLocaleDateString() + " " + end.toLocaleTimeString());
|
||||
return;
|
||||
}
|
||||
|
||||
let time = splitTimestamp(unsyncedLeft);
|
||||
let text = formatTime(time.hours, time.minutes, time.seconds);
|
||||
|
||||
sleep(time.milliseconds)
|
||||
.then(() => $("#countdown").text(text));
|
||||
};
|
||||
|
||||
let unsyncTimer = undefined;
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
// 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 wtforms import TimeField
|
||||
from wtforms import fields
|
||||
from wtforms.fields import html5
|
||||
from wtforms.validators import Optional
|
||||
|
||||
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 forms
|
||||
from .countdown import Cache
|
||||
|
||||
@app.route('/<uuid:countdown_id>', methods=['GET'])
|
||||
def countdown(countdown_id):
|
||||
return render_template('countdown/countdown.html', countdown_id=countdown_id)
|
||||
@app.route('/', methods=['GET', 'POST'])
|
||||
def create():
|
||||
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)
|
||||
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,
|
||||
# form.password.data)
|
||||
# db_session.add(user)
|
||||
flash('Thanks for registering')
|
||||
return redirect(url_for('login'))
|
||||
session['created_countdowns'].append(str(countdown_id))
|
||||
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) {
|
||||
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
|
||||
|
||||
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
|
||||
app.register_blueprint(countdown, url_prefix="/countdown")
|
||||
|
||||
import views
|
||||
|
|
|
@ -4,3 +4,15 @@ POST http://localhost:5000/countdown/api/v1
|
|||
Content-Type: application/json
|
||||
|
||||
{"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