From 54bb2787bc0ab57a9d699a1130adcd31de9c574a Mon Sep 17 00:00:00 2001 From: Ruben van de Ven Date: Wed, 22 Jan 2020 14:36:52 +0100 Subject: [PATCH] Backend as credit fades --- Pipfile | 1 + Pipfile.lock | 97 +++++++++++++++++++++++++++++------- sorteerhoed/HITStore.py | 60 ++++++++++++++++++++--- sorteerhoed/webserver.py | 56 +++++++++++---------- www/backend/backend.html | 40 +++++++++++++++ www/backend/script.js | 60 +++++++++++++++++++++++ www/backend/style.css | 103 +++++++++++++++++++++++++++++++++++++++ 7 files changed, 367 insertions(+), 50 deletions(-) create mode 100644 www/backend/backend.html create mode 100644 www/backend/script.js create mode 100644 www/backend/style.css diff --git a/Pipfile b/Pipfile index c020eba..0677825 100644 --- a/Pipfile +++ b/Pipfile @@ -17,6 +17,7 @@ Pillow = "*" tqdm = "*" serial = "*" pyserial = "*" +country-converter = "*" [dev-packages] diff --git a/Pipfile.lock b/Pipfile.lock index 6c3a1a3..de0fbb1 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -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": {} diff --git a/sorteerhoed/HITStore.py b/sorteerhoed/HITStore.py index 23a08c2..6d53470 100644 --- a/sorteerhoed/HITStore.py +++ b/sorteerhoed/HITStore.py @@ -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) + diff --git a/sorteerhoed/webserver.py b/sorteerhoed/webserver.py index de1f9cc..20de705 100644 --- a/sorteerhoed/webserver.py +++ b/sorteerhoed/webserver.py @@ -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""" +# {hit.worker_id} +# {hit.turk_ip} +# {hit.turk_country} +# {fee} +# {hit.accept_time} +# {duration} +# """ +# ) - fee = f"${hit.fee:.2}" if hit.fee else "-" - - rows.append( - f""" - {hit.worker_id} - {hit.turk_ip} - {hit.turk_country} - {fee} - {hit.accept_time} - {duration} - """ - ) - - 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] diff --git a/www/backend/backend.html b/www/backend/backend.html new file mode 100644 index 0000000..831ad99 --- /dev/null +++ b/www/backend/backend.html @@ -0,0 +1,40 @@ + + + + + + + + + + + +
+
+ + +
+ {{hit.assignment.worker_id}} + {{hit.assignment.turk_country}} + +
+ +
+ + + + +
+ + + + diff --git a/www/backend/script.js b/www/backend/script.js new file mode 100644 index 0000000..92a2378 --- /dev/null +++ b/www/backend/script.js @@ -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); \ No newline at end of file diff --git a/www/backend/style.css b/www/backend/style.css new file mode 100644 index 0000000..82134ca --- /dev/null +++ b/www/backend/style.css @@ -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; +} \ No newline at end of file