Create countdown
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Armin Friedl 2020-09-13 15:07:11 +02:00
parent f7264e26d8
commit d45d2fa982
Signed by: armin
GPG key ID: 48C726EEE7FBCBC8
11 changed files with 257 additions and 67 deletions

View file

@ -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)

View file

@ -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
View 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)

View file

@ -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"})

View file

@ -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 %}

View 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 %}

View 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 %}

View file

@ -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)

View file

@ -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};

View file

@ -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

View file

@ -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