Backend as credit fades

This commit is contained in:
Ruben van de Ven 2020-01-22 14:36:52 +01:00
parent 08f2e4fd3e
commit 54bb2787bc
7 changed files with 367 additions and 50 deletions

View file

@ -17,6 +17,7 @@ Pillow = "*"
tqdm = "*"
serial = "*"
pyserial = "*"
country-converter = "*"
[dev-packages]

97
Pipfile.lock generated
View file

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "3c0954c7917f567561faffcf18031aa0718c5144698d0de7022dd9ad9d49b1c4"
"sha256": "0bb22632889d5e728609ae67e478ff7bdeb7e56b7bb0b2be0f3b5db36ca129c5"
},
"pipfile-spec": 6,
"requires": {
@ -21,18 +21,18 @@
},
"boto3": {
"hashes": [
"sha256:98fdd6fa754e17c0d9e87fbb464c43c7e72aaa6b4f78b418eba47b7d15deffee",
"sha256:fdaae8edd63ae114107d375862069d17de23e00489a65169b8141ddee6bdf78b"
"sha256:5222edc5b20d5c6ab7440fc4f89f987ead05be37ff5cc5359a3b9148d9b5a51e",
"sha256:bd3337cfc15613b0091fa567dc3065d94df88e5837ba1adbb1e35b91db728a66"
],
"index": "pypi",
"version": "==1.10.48"
"version": "==1.11.7"
},
"botocore": {
"hashes": [
"sha256:29370f50af7870661609fbfbc4ed01ef2fd531b87b98729700526d1a4b3a2f89",
"sha256:7f60edf33c6f5b7c1c9b9377267bdc56495f52704607f713d4c3bd1d82a08334"
"sha256:9a17d36ee43f1398c7db3cb29aa2216de94bcb60f058b1c645d71e72a330ddf8",
"sha256:e4b82b1a7389f3d16732eb839240c9d3e42470100d5a71415ea2a0a35b911b23"
],
"version": "==1.13.48"
"version": "==1.14.7"
},
"certifi": {
"hashes": [
@ -56,6 +56,13 @@
"index": "pypi",
"version": "==10.0"
},
"country-converter": {
"hashes": [
"sha256:bc01ba2592b77a78b4f3e6f76600ca27852d71d1512cf1f320fecbcaaea3c6f9"
],
"index": "pypi",
"version": "==0.6.7"
},
"docutils": {
"hashes": [
"sha256:6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0",
@ -159,6 +166,56 @@
],
"version": "==1.5.2"
},
"numpy": {
"hashes": [
"sha256:1786a08236f2c92ae0e70423c45e1e62788ed33028f94ca99c4df03f5be6b3c6",
"sha256:17aa7a81fe7599a10f2b7d95856dc5cf84a4eefa45bc96123cbbc3ebc568994e",
"sha256:20b26aaa5b3da029942cdcce719b363dbe58696ad182aff0e5dcb1687ec946dc",
"sha256:2d75908ab3ced4223ccba595b48e538afa5ecc37405923d1fea6906d7c3a50bc",
"sha256:39d2c685af15d3ce682c99ce5925cc66efc824652e10990d2462dfe9b8918c6a",
"sha256:56bc8ded6fcd9adea90f65377438f9fea8c05fcf7c5ba766bef258d0da1554aa",
"sha256:590355aeade1a2eaba17617c19edccb7db8d78760175256e3cf94590a1a964f3",
"sha256:70a840a26f4e61defa7bdf811d7498a284ced303dfbc35acb7be12a39b2aa121",
"sha256:77c3bfe65d8560487052ad55c6998a04b654c2fbc36d546aef2b2e511e760971",
"sha256:9537eecf179f566fd1c160a2e912ca0b8e02d773af0a7a1120ad4f7507cd0d26",
"sha256:9acdf933c1fd263c513a2df3dceecea6f3ff4419d80bf238510976bf9bcb26cd",
"sha256:ae0975f42ab1f28364dcda3dde3cf6c1ddab3e1d4b2909da0cb0191fa9ca0480",
"sha256:b3af02ecc999c8003e538e60c89a2b37646b39b688d4e44d7373e11c2debabec",
"sha256:b6ff59cee96b454516e47e7721098e6ceebef435e3e21ac2d6c3b8b02628eb77",
"sha256:b765ed3930b92812aa698a455847141869ef755a87e099fddd4ccf9d81fffb57",
"sha256:c98c5ffd7d41611407a1103ae11c8b634ad6a43606eca3e2a5a269e5d6e8eb07",
"sha256:cf7eb6b1025d3e169989416b1adcd676624c2dbed9e3bcb7137f51bfc8cc2572",
"sha256:d92350c22b150c1cae7ebb0ee8b5670cc84848f6359cf6b5d8f86617098a9b73",
"sha256:e422c3152921cece8b6a2fb6b0b4d73b6579bd20ae075e7d15143e711f3ca2ca",
"sha256:e840f552a509e3380b0f0ec977e8124d0dc34dc0e68289ca28f4d7c1d0d79474",
"sha256:f3d0a94ad151870978fb93538e95411c83899c9dc63e6fb65542f769568ecfa5"
],
"version": "==1.18.1"
},
"pandas": {
"hashes": [
"sha256:00dff3a8e337f5ed7ad295d98a31821d3d0fe7792da82d78d7fd79b89c03ea9d",
"sha256:22361b1597c8c2ffd697aa9bf85423afa9e1fcfa6b1ea821054a244d5f24d75e",
"sha256:255920e63850dc512ce356233081098554d641ba99c3767dde9e9f35630f994b",
"sha256:26382aab9c119735908d94d2c5c08020a4a0a82969b7e5eefb92f902b3b30ad7",
"sha256:33970f4cacdd9a0ddb8f21e151bfb9f178afb7c36eb7c25b9094c02876f385c2",
"sha256:4545467a637e0e1393f7d05d61dace89689ad6d6f66f267f86fff737b702cce9",
"sha256:52da74df8a9c9a103af0a72c9d5fdc8e0183a90884278db7f386b5692a2220a4",
"sha256:61741f5aeb252f39c3031d11405305b6d10ce663c53bc3112705d7ad66c013d0",
"sha256:6a3ac2c87e4e32a969921d1428525f09462770c349147aa8e9ab95f88c71ec71",
"sha256:7458c48e3d15b8aaa7d575be60e1e4dd70348efcd9376656b72fecd55c59a4c3",
"sha256:78bf638993219311377ce9836b3dc05f627a666d0dbc8cec37c0ff3c9ada673b",
"sha256:8153705d6545fd9eb6dd2bc79301bff08825d2e2f716d5dced48daafc2d0b81f",
"sha256:975c461accd14e89d71772e89108a050fa824c0b87a67d34cedf245f6681fc17",
"sha256:9962957a27bfb70ab64103d0a7b42fa59c642fb4ed4cb75d0227b7bb9228535d",
"sha256:adc3d3a3f9e59a38d923e90e20c4922fc62d1e5a03d083440468c6d8f3f1ae0a",
"sha256:bbe3eb765a0b1e578833d243e2814b60c825b7fdbf4cdfe8e8aae8a08ed56ecf",
"sha256:df8864824b1fe488cf778c3650ee59c3a0d8f42e53707de167ba6b4f7d35f133",
"sha256:e45055c30a608076e31a9fcd780a956ed3b1fa20db61561b8d88b79259f526f7",
"sha256:ee50c2142cdcf41995655d499a157d0a812fce55c97d9aad13bc1eef837ed36c"
],
"version": "==0.25.3"
},
"pillow": {
"hashes": [
"sha256:0a628977ac2e01ca96aaae247ec2bd38e729631ddf2221b4b715446fd45505be",
@ -200,7 +257,6 @@
"sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c",
"sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"
],
"markers": "python_version >= '2.7'",
"version": "==2.8.1"
},
"python-magic": {
@ -211,6 +267,13 @@
"index": "pypi",
"version": "==0.4.15"
},
"pytz": {
"hashes": [
"sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d",
"sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"
],
"version": "==2019.3"
},
"pyyaml": {
"hashes": [
"sha256:059b2ee3194d718896c0ad077dd8c043e5e909d9180f387ce42012662a4946d6",
@ -237,10 +300,10 @@
},
"s3transfer": {
"hashes": [
"sha256:6efc926738a3cd576c2a79725fed9afde92378aa5c6a957e3af010cb019fac9d",
"sha256:b780f2411b824cb541dbcd2c713d0cb61c7d1bcadae204cdddda2b35cef493ba"
"sha256:248dffd2de2dfb870c507b412fc22ed37cd3255293e293c395158e7c55fbe5f9",
"sha256:80ed96731b3bd77395cd6197246069092015e1124164b2c152c8f741a823dd04"
],
"version": "==0.2.1"
"version": "==0.3.1"
},
"serial": {
"hashes": [
@ -252,10 +315,10 @@
},
"six": {
"hashes": [
"sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd",
"sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66"
"sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a",
"sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"
],
"version": "==1.13.0"
"version": "==1.14.0"
},
"sqlalchemy": {
"hashes": [
@ -287,10 +350,10 @@
},
"urllib3": {
"hashes": [
"sha256:a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293",
"sha256:f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745"
"sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc",
"sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc"
],
"version": "==1.25.7"
"version": "==1.25.8"
}
},
"develop": {}

View file

@ -4,16 +4,18 @@ from sqlalchemy import Column, Integer, String, DateTime, Float
from sqlalchemy.orm import relationship
from sqlalchemy.sql.schema import ForeignKey, Sequence
from sqlalchemy.engine import create_engine
from sqlalchemy.orm.session import sessionmaker
from sqlalchemy.orm.session import sessionmaker, object_session
import datetime
from contextlib import contextmanager
import uuid
import os
import country_converter
mainLogger = logging.getLogger("sorteerhoed")
logger = mainLogger.getChild("store")
Base = declarative_base()
cc = country_converter.CountryConverter()
"""
HIT lifetime:
@ -92,6 +94,12 @@ class HIT(Base):
values['state'] = self.getStatus()
values['scan_image'] = self.getImageUrl() if self.scanned_at else None
values['svg_image'] = self.getSvgImageUrl() if self.isSubmitted() else None
values['preceding_assignments'] = [a.toShortDict() for a in self.getBasedOnAssignments()]
values['preceding_assignments'].append({
'worker_id': 'Ruben van de Ven & Merijn van Moll',
'turk_country': 'the Netherlands',
'turk_country_code': 'NL'
})
return values
def delete(self):
@ -108,6 +116,20 @@ class HIT(Base):
if not a:
return False
return bool(a.confirmed_at)
def getBasedOnAssignments(self):
"""
Get preceding assignments, one per worker, excluding the one who did this HIT
"""
assignment = self.getLastAssignment()
session = object_session(self)
q = session.query(Assignment).\
filter(Assignment.submit_page_at < self.created_at).\
group_by(Assignment.worker_id).\
order_by(Assignment.created_at.desc())
if assignment and assignment.worker_id:
q = q.filter(Assignment.worker_id != assignment.worker_id)
return q
class Assignment(Base):
__tablename__ = 'assignments'
@ -148,8 +170,22 @@ class Assignment(Base):
def toDict(self) -> dict:
values = {c.name: getattr(self, c.name) for c in self.__table__.columns}
if self.turk_country:
values['turk_country_code'] = cc.convert([self.turk_country], to='ISO2')
else:
values['turk_country_code'] = None
return values
def toShortDict(self) -> dict:
values = {
'worker_id': self.worker_id,
'turk_country': self.turk_country
}
if self.turk_country:
values['turk_country_code'] = cc.convert([self.turk_country], to='ISO2')
else:
values['turk_country_code'] = None
return values
class Store:
def __init__(self, db_filename, logLevel=0):
@ -157,6 +193,7 @@ class Store:
if logLevel <= logging.DEBUG:
logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO)
needsInitialization = not os.path.exists(path)
self.engine = create_engine('sqlite:///'+path, echo=False, connect_args={'check_same_thread': False})
Base.metadata.create_all(self.engine)
self.Session = sessionmaker(bind=self.engine)
@ -164,6 +201,15 @@ class Store:
self.currentHit = None # mirrors Centralmanagmenet, stored here so we can quickly access it from webserver classes
self.updateHooks = []
# if needsInitialization:
# self.insertInitialContent()
#
# def insertInitialContent(self):
# hit = self.createHIT()
# assignment = self.newAssignment(hit, 'initial')
#
def registerUpdateHook(self, hook):
if hook not in self.updateHooks:
@ -206,11 +252,12 @@ class Store:
order_by(HIT.created_at.desc()).first()
def getNewestHits(self, n = 2) -> list:
hits = list(
self.session.query(HIT).\
q = self.session.query(HIT).\
filter(HIT.deleted_at==None).\
order_by(HIT.created_at.desc()).limit(2)
)
order_by(HIT.created_at.desc())
if n is not None:
q = q.limit(n)
hits = list(q)
# select DESC, because we want latest, then reverse list to get in right order
hits.reverse()
return hits
@ -285,3 +332,4 @@ class Store:
return self.session.query(HIT).\
filter(HIT.submit_hit_at != None).\
order_by(HIT.submit_hit_at.desc()).limit(n)

View file

@ -224,8 +224,10 @@ class StatusWebSocketHandler(tornado.websocket.WebSocketHandler):
# the client connected
def open(self):
self.__class__.connections.add(self)
self.write_message(json.dumps(self.statusPage.fetch(), cls=DateTimeEncoder))
limit = 2
if 'all' in self.request.query_arguments:
limit = None
self.write_message(json.dumps(self.statusPage.fetch(limit), cls=DateTimeEncoder))
# client disconnected
def on_close(self):
@ -369,30 +371,30 @@ class BackendHandler(tornado.web.RequestHandler):
def get(self):
rows = []
for hit in self.store.getHITs(100):
if hit.submit_hit_at and hit.accept_time:
seconds = (hit.submit_hit_at - hit.accept_time).total_seconds()
duration_m = int(seconds/60)
duration_s = max(int(seconds%60), 0)
duration = (f"{duration_m}m" if duration_m else "") + f"{duration_s:02d}s"
else:
duration = "-"
# for hit in self.store.getHITs(100):
# if hit.submit_hit_at and hit.accept_time:
# seconds = (hit.submit_hit_at - hit.accept_time).total_seconds()
# duration_m = int(seconds/60)
# duration_s = max(int(seconds%60), 0)
# duration = (f"{duration_m}m" if duration_m else "") + f"{duration_s:02d}s"
# else:
# duration = "-"
#
# fee = f"${hit.fee:.2}" if hit.fee else "-"
#
# rows.append(
# f"""
# <tr><td></td><td>{hit.worker_id}</td>
# <td>{hit.turk_ip}</td>
# <td>{hit.turk_country}</td>
# <td>{fee}</td>
# <td>{hit.accept_time}</td>
# <td>{duration}</td><td></td>
# """
# )
fee = f"${hit.fee:.2}" if hit.fee else "-"
rows.append(
f"""
<tr><td></td><td>{hit.worker_id}</td>
<td>{hit.turk_ip}</td>
<td>{hit.turk_country}</td>
<td>{fee}</td>
<td>{hit.accept_time}</td>
<td>{duration}</td><td></td>
"""
)
contents = open(os.path.join(self.path, 'backend.html'), 'r').read()
contents = contents.replace("{{TBODY}}", "".join(rows))
contents = open(os.path.join(self.path, 'backend/backend.html'), 'r').read()
# contents = contents.replace("{{TBODY}}", "".join(rows))
self.write(contents)
class StatusPage():
@ -422,11 +424,11 @@ class StatusPage():
else:
logger.warn("Status: no server loop to call update command")
def fetch(self):
def fetch(self, limit = 2):
"""
Fetch latest, used on connection of status page
"""
hits = self.store.getNewestHits(2)
hits = self.store.getNewestHits(limit)
return [hit.toDict() for hit in hits]

40
www/backend/backend.html Normal file
View file

@ -0,0 +1,40 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="stylesheet" type="text/css" href="/backend/style.css" />
<script src='../worker_specs/dateformat.js'></script>
<script src='../worker_specs/reconnecting-websocket.min.js'></script>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<div id="wrapper">
<div v-if="hit && hit.assignment" :class="[{'invisible': !hit.visible}, 'hit']">
<img :src="hit.scan_image">
<div class='credits'>
<span class='worker_id'>{{hit.assignment.worker_id}}</span>
<span class='country'>{{hit.assignment.turk_country}}</span>
<template v-if="hit.preceding_assignments.length">
<div id='collaborators'>
<div id='collab_items'>
<span v-for="a of hit.preceding_assignments" class='credit'>
<span class='worker_id'>{{a.worker_id}}</span>
<span class='country'>{{a.turk_country_code}}</span>
</span>
</div>
</div>
</template>
</div>
</div>
</div>
<script src="/backend/script.js"></script>
</body>
</html>

60
www/backend/script.js Normal file
View file

@ -0,0 +1,60 @@
var app = new Vue({
el: '#wrapper',
data: {
hit: {}
},
})
var hits = {};
var hitIds = [];
// fetch ?all in database
let ws = new ReconnectingWebSocket('ws://localhost:8888/status/ws?all')
ws.addEventListener('open', () => {
// ws.send('hi server')
})
ws.addEventListener('message', (event) => {
console.log('message: ')
let load_hits = JSON.parse(event.data)
for(let hit of load_hits) {
console.log(hit);
hits[hit.id] = hit;
if(hit.scanned_at && hitIds.indexOf(hit.id) < 0){
// only add if indeed scanned
hitIds.push(hit.id);
}
}
})
var currentI = 0;
setInterval(function(e){
if(hitIds.length < 1) {
return;
}
currentI = (currentI + 1) % hitIds.length;
let currentHitId = hitIds[currentI];
console.log(currentHitId);
let hit = hits[currentHitId];
hit['visible'] = false;
app.hit['visible'] = false;
setTimeout(() => {
app.hit = hits[currentHitId]
}, 1000);
setTimeout(() => {
app.hit['visible'] = true;
let wrapperEl = document.getElementById("collaborators");
let innerEl = document.getElementById("collab_items");
let size = 100;
do {
innerEl.style.fontSize = size + "%";
size --;
} while(innerEl.clientHeight > wrapperEl.clientHeight && size > 10);
}, 1100);
}, 4000);

103
www/backend/style.css Normal file
View file

@ -0,0 +1,103 @@
@font-face {
font-family: 'bebas';
src: url('/font/BebasNeue-Regular.ttf');
}
:root{
--base-font-size: 30px;
--spec_name-font-size: 120%;
--spec_value-font-size: 250%;
--base-color: #271601; /* tekst */
--alt-color: #FFF5DF; /* achtergrond */
--amazon-color: #F0C14C;
/* ////// GAT ACHTERKANT PLEK & POSITIE /////// */
/* */ /* */
/* */ --pos-x: 0px; /* */
/* */ --pos-y: 50px; /* */
/* */ --width: 100%; /* 285mm */
/* */ --height: 100%; /* 500mm */
/* */ /* */
/* //////////////////////////////////////////// */
}
body{
background: black;
margin:0;
color:white;
font-family: bebas;
font-size: var(--base-font-size);
}
.hit{
position:absolute;
top:var(--pos-y);
left:0;
right:0;
bottom:0;
text-align: center;
transition: opacity 1s;
opacity: 1;
/*animation: fadeIn 1s, fadeOut 1s 2s;*/
}
.hit.invisible{
opacity: 0;
}
@keyframes fadeIn{
from{opacity:0;}
to{opacity:1;}
}
@keyframes fadeOut{
from{opacity:1;}
to{opacity:0;}
}
.hit img{
display: block;
margin: 10vh auto 7vh;
width: 1000px;
height: 500px;
border: solid 1px white;
}
.credits{
font-size: 60px;
}
.credits::before{
content:'created by';
display:block;
font-size: 60%;
}
.country{color: gray;}
.country::before{content:'(';}
.country::after{content:')';}
#collaborators{
height: 30vh;
width: 1000px;
margin: 0 auto;
}
#collab_items{
text-align: justify;
}
#collaborators::before{
margin-top: 7vh;
content:'in collaboration with';
display:block;
font-size: 60%;
}
#collaborators .credit{
margin-right:10px;
}