From 7f155dc09e2b8862d68ee40d514de16c064bf449 Mon Sep 17 00:00:00 2001 From: erdgeist Date: Sat, 4 Jan 2025 02:46:47 +0100 Subject: Get prototype working --- fullnarp.py | 143 +++++++++++++----- halfnarp2.py | 50 +++---- requirements.txt | 1 + static/fullnarp.js | 429 ++++++++++++++++++++++++++--------------------------- 4 files changed, 340 insertions(+), 283 deletions(-) diff --git a/fullnarp.py b/fullnarp.py index 6a02b6e..a93ca0a 100644 --- a/fullnarp.py +++ b/fullnarp.py @@ -38,6 +38,19 @@ var/talks_local_fullnarp which contains non-public lectures and privacy relevant speaker availibilities. It should only be served behind some auth. """ +# Global placeholders for engine and session factory +engine = None +SessionLocal = None +Base = declarative_base() + + +class TalkPreference(Base): + __tablename__ = "talk_preference" + uid = Column(String, primary_key=True) + public_uid = Column(String, index=True) + talk_ids = Column(String) + + # Shared state current_version = {} newest_version = 0 @@ -46,22 +59,32 @@ current_version_lock = asyncio.Lock() # Lock for managing access to the global clients = {} # Key: websocket, Value: {'client_id': ..., 'last_version': ...} +async def bootstrap_client(websocket): + """Provide lecture list and votes count to new client""" + + async def notify_clients(): """Notify all connected clients of the current state.""" async with current_version_lock: # Prepare a full state update message with the current version - message = {"current_version": newest_version, "data": current_version} - - # Notify each client about their relevant updates - for client, info in clients.items(): - try: - # Send the state update - await client.send(json.dumps(message)) - # Update the client's last known version - info["last_version"] = newest_version - except ConnectionClosedOK: - # Handle disconnected clients gracefully - pass + message = { + "property": "fullnarp", + "current_version": newest_version, + "data": current_version, + } + + # Notify each client about their relevant updates + for client, info in clients.items(): + try: + # Send the state update + await client.send(json.dumps(message)) + # Update the client's last known version + info["last_version"] = newest_version + + print("Reply: " + json.dumps(message)) + except ConnectionClosedOK: + # Handle disconnected clients gracefully + pass async def handle_client(websocket): @@ -71,31 +94,59 @@ async def handle_client(websocket): clients[websocket] = {"client_id": id(websocket), "last_version": 0} try: - # Send the current global state to the newly connected client - async with current_version_lock: - global newest_version - await websocket.send( - json.dumps({"current_version": newest_version, "data": current_version}) - ) - clients[websocket][ - "last_version" - ] = newest_version # Update last known version - # Listen for updates from the client async for message in websocket: + global newest_version try: # Parse incoming message data = json.loads(message) - - # Update global state with a lock to prevent race conditions - async with current_version_lock: - if "setevent" in data: - eventid = data["setevent"] - day = data["day"] - room = data["room"] - time = data["time"] - lastupdate = data["lastupdate"] - + print("Got command " + message) + + if data.get("action", "") == "bootstrap": + print("Got bootstrap command") + with open("var/talks_local_fullnarp") as data_file: + talks = json.load(data_file) + message = {"property": "pretalx", "data": talks} + await websocket.send(json.dumps(message)) + + with SessionLocal() as session: + preferences = session.query(TalkPreference).all() + m = [] + for pref in preferences: + m.append(json.loads(pref.talk_ids)) + message = {"property": "halfnarp", "data": m} + await websocket.send(json.dumps(message)) + + async with current_version_lock: + message = { + "property": "fullnarp", + "current_version": newest_version, + "data": current_version, + } + await websocket.send(json.dumps(message)) + print("Reply: " + json.dumps(message)) + + elif data.get("action", "") == "reconnect": + + async with current_version_lock: + message = { + "property": "fullnarp", + "current_version": newest_version, + "data": current_version, + } + await websocket.send(json.dumps(message)) + + elif data.get("action", "") == "remove_event": + pass + + elif data.get("action", "") == "set_event": + eventid = data["event_id"] + day = data["day"] + room = data["room"] + time = data["time"] + lastupdate = data["lastupdate"] + + async with current_version_lock: newest_version += 1 # Increment the version print( "Moving event: " @@ -127,8 +178,9 @@ async def handle_client(websocket): ) as outfile: json.dump(current_version, outfile) - # Notify all clients about the updated global state - await notify_clients() + # Notify all clients about the updated global state + await notify_clients() + except json.JSONDecodeError: await websocket.send(json.dumps({"error": "Invalid JSON"})) except websockets.exceptions.ConnectionClosedError as e: @@ -141,6 +193,25 @@ async def handle_client(websocket): async def main(): + parser = ArgumentParser(description="halfnarp2") + parser.add_argument( + "-c", "--config", help="Config file location", default="./config.json" + ) + args = parser.parse_args() + + global engine, SessionLocal + + with open(args.config, mode="r", encoding="utf-8") as json_file: + config = json.load(json_file) + + DATABASE_URL = config.get("database-uri", "sqlite:///test.db") + + print("Connecting to " + DATABASE_URL) + engine = create_engine(DATABASE_URL, echo=False) + SessionLocal = sessionmaker(bind=engine) + Base.metadata.create_all(bind=engine) + + # load state file newest_file = sorted(listdir("versions/"))[-1] global newest_version global current_version @@ -154,8 +225,8 @@ async def main(): current_version = {} newest_version = 0 - async with websockets.serve(handle_client, "localhost", 5009): - print("WebSocket server started on ws://localhost:5009") + async with websockets.serve(handle_client, "localhost", 22378): + print("WebSocket server started on ws://localhost:22378") await asyncio.Future() # Run forever diff --git a/halfnarp2.py b/halfnarp2.py index 8d736a0..827055a 100755 --- a/halfnarp2.py +++ b/halfnarp2.py @@ -9,11 +9,12 @@ import requests import json import uuid import markdown +from datetime import datetime, time, timedelta from html_sanitizer import Sanitizer from hashlib import sha256 -db = SQLAlchemy(app) app = Flask(__name__) +db = SQLAlchemy() class TalkPreference(db.Model): @@ -118,21 +119,18 @@ def get_preferences(public_uid): def filter_keys_halfnarp(session): - abstract_html = markdown.markdown(submission["abstract"], enable_attributes=False) + abstract_html = markdown.markdown(session["abstract"], enable_attributes=False) abstract_clean_html = Sanitizer().sanitize(abstract_html) - slot = submission["slot"] + slot = session["slot"] return { - "title": submission.get("title", "!!! NO TITLE !!!"), - "duration": 60 * submission.get("duration", 40), - "event_id": submission["code"], - "language": submission.get("content_locale", "de"), - "track_id": submission["track_id"], + "title": session.get("title", "!!! NO TITLE !!!"), + "duration": 60 * session.get("duration", 40), + "event_id": session["code"], + "language": session.get("content_locale", "de"), + "track_id": session["track_id"], "speaker_names": ", ".join( - [ - speaker.get("name", "unnamed") - for speaker in submission.get("speakers", {}) - ] + [speaker.get("name", "unnamed") for speaker in session.get("speakers", {})] ), "abstract": abstract_clean_html, "room_id": slot.get("room_id", "room_unknown"), @@ -143,13 +141,13 @@ def filter_keys_halfnarp(session): def filter_keys_fullnarp(session, speakers): abstract_html = markdown.markdown(session["abstract"], enable_attributes=False) abstract_clean_html = Sanitizer().sanitize(abstract_html) - slot = submission["slot"] + slot = session["slot"] speaker_info = [] - for speaker in submission.get("speakers", {}): + for speaker in session.get("speakers", {}): speaker_info.append(speakers[speaker["code"]]) # if len(speakers[speaker['code']]['availabilities']) == 0: - # print ( "Track " + str(submission['track_id']) + ": Speaker " + speaker.get('name', 'unname') + " on session: " + session.get('title', '!!! NO TITLE !!!') + " without availability. https://cfp.cccv.de/orga/event/38c3/submissions/" + session['code'] ) + # print ( "Track " + str(submission['track_id']) + ": Speaker " + speaker.get('name', 'unname') + " on session: " + session.get('title', '!!! NO TITLE !!!') + " without availability. https://cfp.cccv.de/orga/event/38c3/session/" + session['code'] ) """This fixes availabilites ranging to or from exactly midnight to the more likely start of availibility at 8am and end of availibility at 3am""" @@ -179,19 +177,17 @@ def filter_keys_fullnarp(session, speakers): avail["end"] = str(end_new) return { - "title": submission.get("title", "!!! NO TITLE !!!"), - "duration": 60 * submission.get("duration", 40), - "event_id": submission["code"], - "language": submission.get("content_locale", "de"), - "track_id": submission["track_id"], + "title": session.get("title", "!!! NO TITLE !!!"), + "duration": 60 * session.get("duration", 40), + "event_id": session["code"], + "language": session.get("content_locale", "de"), + "track_id": session["track_id"], + "speakers": speaker_info, "speaker_names": ", ".join( - [ - speaker.get("name", "unnamed") - for speaker in submission.get("speakers", {}) - ] + [speaker.get("name", "unnamed") for speaker in session.get("speakers", {})] ), "abstract": abstract_clean_html, - "room_id": slot.get("room_id", "room_unknown"), + "room_id": "room" + str(slot.get("room_id", "_unknown")), "start_time": slot.get("start", "1970-01-01"), } @@ -217,7 +213,7 @@ def fetch_talks(config): speakers = dict((speaker["code"], speaker) for speaker in speakers_json["results"]) sessions = [ - filter_keys(submission) + filter_keys_halfnarp(submission) for submission in talks_json["results"] if submission["state"] == "confirmed" and not "non-public" in submission.get("tags", {}) @@ -278,7 +274,7 @@ if __name__ == "__main__": app.jinja_env.lstrip_blocks = True CORS() - db.init(app) + db.init_app(app) with app.app_context(): db.create_all() diff --git a/requirements.txt b/requirements.txt index e1d7367..78b8f61 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,3 +17,4 @@ html-sanitizer markdown psycopg2 gunicorn +websockets diff --git a/static/fullnarp.js b/static/fullnarp.js index 8b7d36d..3d60592 100644 --- a/static/fullnarp.js +++ b/static/fullnarp.js @@ -1,4 +1,9 @@ let ws; // WebSocket instance +let allrooms = ['1','2','3'] +let allminutes = ['00','05','10','15','20','25','30','35','40','45','50','55'] +let allhours = ['10','11','12','13','14','15','16','17','18','19','20','21','22','23','00','01','02']; +let alldays = ['1','2','3','4']; +let raw_votes; function toggle_grid(whichDay) { var vclasses= [['in-list'], ['in-calendar', 'onlyday1'], ['in-calendar', 'onlyday2'], ['in-calendar', 'onlyday3'], @@ -8,6 +13,128 @@ function toggle_grid(whichDay) { document.body.classList.add(...vclasses[whichDay]); } +function render_lectures(data) { + for (item of data) { + /* Take copy of hidden event template div and select them, if they're in + list of previous prereferences */ + var t = document.getElementById('template').cloneNode(true); + var event_id = item.event_id.toString(); + + t.classList.add('event', 'duration_' + item.duration, 'lang_' + (item.language || 'en')); + t.setAttribute('event_id', event_id); + t.setAttribute('id', 'event_' + event_id); + t.setAttribute('fullnarp-duration', item.duration); + t.setAttribute('draggable', 'true'); + + /* Sort textual info into event div */ + t.querySelector('.title').textContent = item.title; + t.querySelector('.speakers').textContent = item.speaker_names; + t.querySelector('.abstract').append(item.abstract); + + /* Store speakers and their availabilities */ + window.event_speakers[event_id] = item.speakers; + for (speaker of item.speakers) { + var have_avails = false; + for (avail of speaker.availabilities) { + if (avail.id ) { + have_avails = true; + break; + } + } + if (!have_avails) + t.classList.add('has_unavailable_speaker'); + } + + /* Make the event drag&droppable */ + t.ondragstart = function( event, ui ) { + event.stopPropagation(); + + event.dataTransfer.setData('text/plain', this.id ); + event.dataTransfer.dropEffect = 'move'; + event.dataTransfer.effectAllowed = 'move'; + event.target.classList.add('is-dragged'); + } + + /* While dragging make source element small enough to allow + dropping below its original area */ + t.ondrag = function( event, ui ) { + event.stopPropagation(); + event.target.classList.add('is-dragged'); + + /* When drag starts in list view, switch to calendar view */ + if( document.body.classList.contains('in-list') ) { + toggle_grid(5); + document.body.classList.add('was-list'); + } + if( document.body.classList.contains('in-drag') ) + return; + + document.body.classList.add('in-drag'); + /* mark all possible drop points regarding to availability */ + for (hour of allhours) + for (minute of allminutes) + for (day of alldays) + document.querySelectorAll('.grid.day_'+day+'.time_'+hour+minute).forEach(elem => elem.classList.toggle('possible', check_avail(event.target, day, hour+minute))); + + } + + t.ondragend = function( event, ui ) { + event.stopPropagation(); + + /* We removed in-list and the drop did not succeed. Go back to list view */ + if (document.body.classList.contains('was-list')) + toggle_grid(0); + + document.querySelectorAll('.over').forEach(elem => elem.classList.remove('over')); + document.querySelectorAll('.is-dragged').forEach(elem => elem.classList.remove('id-dragged')); + document.querySelectorAll('.possible').forEach(elem => elem.classList.remove('possible')); + document.body.classList.remove('in-drag', 'was-list'); + } + + /* start_time: 2014-12-29T21:15:00+01:00" */ + var start_time = new Date(item.start_time); + + var day = start_time.getDate()-26; + var hour = start_time.getHours(); + var mins = start_time.getMinutes(); + + /* After midnight: sort into yesterday */ + if( hour < 9 ) + day--; + + /* Fix up room for 38c3 */ + room = (item.room_id || 'room_unknown').toString().replace('471','room1').replace('472','room2').replace('473','room3'); + + /* Apply attributes to sort events into calendar */ + t.classList.add(room, 'day_' + day, 'time_' + (hour<10?'0':'') + hour + (mins<10?'0':'') + mins); + t.setAttribute('fullnarp-day', day); + t.setAttribute('fullnarp-time', (hour<10?'0':'') + hour + (mins<10?'0':'') + mins ); + t.setAttribute('fullnarp-room', room.replace('room','')); + + mark_avail(t); + + t.onclick = function(event) { + _this = this; + document.body.classList.remove('in-drag'); + if (document.body.classList.contains('correlate')) { + document.querySelectorAll('.selected').forEach(elem => elem.classList.remove('selected')); + document.querySelectorAll('.event').forEach(elem => mark_correlation(elem, _this)); + } + _this.classList.toggle('selected'); + document.querySelectorAll('.info').forEach(elem => elem.classList.add('hidden')); + event.stopPropagation(); + } + + /* Put new event into DOM tree. Track defaults to 'Other' */ + var track = item.track_id.toString(); + t.classList.add('track_' + track ); + var d = document.getElementById(track); + if (!d) + d = document.getElementById('Other'); + d.append(t); + } +} + function distribute_votes() { document.querySelectorAll('.event').forEach( function(element) { var eid = element.getAttribute('event_id'); @@ -37,8 +164,8 @@ function distribute_votes() { } function corr_for_eventids(id1, id2) { - var d = 0, c = 0, cd = 0, l = window.raw_votes.length; - for (item of window.raw_votes) { + var d = 0, c = 0, cd = 0, l = raw_votes.length; + for (item of raw_votes) { var x = 0; if( item.indexOf(id1) > -1 ) { ++d; x++;} if( item.indexOf(id2) > -1 ) { ++c; cd+=x; } @@ -52,42 +179,42 @@ function corr_for_eventids(id1, id2) { } function show_all_correlates(el) { - /* First identify the room to see what other rooms to consider - correlates always grow from the top slot to the right, - unless there's an overlapping event to the left that starts earlier - */ - var event_room = el.getAttribute('fullnarp-room'); - var event_day = el.getAttribute('fullnarp-day'); - var event_time = el.getAttribute('fullnarp-time'); - - if (!event_time) return; - - var event_start; - try { event_start = time_to_mins(event_time); } catch(e) { return; } - var event_duration = el.getAttribute('fullnarp-duration') / 60; - - /* Only test events to the right, if they start at the exact same time */ - document.querySelectorAll('.event.day_'+event_day).forEach( function(check_el, index) { - var check_room = check_el.getAttribute('fullnarp-room'); - if (event_room == check_room) return; - - var check_time = check_el.getAttribute('fullnarp-time'); - if (!check_time) return; - var check_start = time_to_mins(check_time); - var check_duration = check_el.getAttribute('fullnarp-duration') / 60; - var dist = check_el.getAttribute('fullnarp-room') - event_room; - var overlap = check_start < event_start + event_duration && event_start < check_start + check_duration; - - if (!overlap) return; - if (event_start == check_start && dist <= 0) return; - if (event_start < check_start) return; - - var corr = corr_for_eventids(el.getAttribute('event_id'), check_el.getAttribute('event_id')); - var dir = dist > 0 ? 'r' : 'l'; - var div = document.createElement('div'); - div.classList.add('corrweb', dir.repeat(Math.abs(dist)), 'day_' + event_day, 'room' + event_room, 'time_' + event_time, 'corr_d_' + corr); - document.body.appendChild(div); - }) + /* First identify the room to see what other rooms to consider + correlates always grow from the top slot to the right, + unless there's an overlapping event to the left that starts earlier + */ + var event_room = el.getAttribute('fullnarp-room'); + var event_day = el.getAttribute('fullnarp-day'); + var event_time = el.getAttribute('fullnarp-time'); + + if (!event_time) return; + + var event_start; + try { event_start = time_to_mins(event_time); } catch(e) { return; } + var event_duration = el.getAttribute('fullnarp-duration') / 60; + + /* Only test events to the right, if they start at the exact same time */ + document.querySelectorAll('.event.day_'+event_day).forEach( function(check_el, index) { + var check_room = check_el.getAttribute('fullnarp-room'); + if (event_room == check_room) return; + + var check_time = check_el.getAttribute('fullnarp-time'); + if (!check_time) return; + var check_start = time_to_mins(check_time); + var check_duration = check_el.getAttribute('fullnarp-duration') / 60; + var dist = check_el.getAttribute('fullnarp-room') - event_room; + var overlap = check_start < event_start + event_duration && event_start < check_start + check_duration; + + if (!overlap) return; + if (event_start == check_start && dist <= 0) return; + if (event_start < check_start) return; + + var corr = corr_for_eventids(el.getAttribute('event_id'), check_el.getAttribute('event_id')); + var dir = dist > 0 ? 'r' : 'l'; + var div = document.createElement('div'); + div.classList.add('corrweb', dir.repeat(Math.abs(dist)), 'day_' + event_day, 'room' + event_room, 'time_' + event_time, 'corr_d_' + corr); + document.body.appendChild(div); + }); } function display_correlation() { @@ -104,8 +231,8 @@ function display_correlation() { function mark_correlation(dest, comp) { var id1 = dest.getAttribute('event_id'); var id2 = comp.getAttribute('event_id'); - var d = 0, c = 0, cd = 0, l = window.raw_votes.length; - for (vote of window.raw_votes) { + var d = 0, c = 0, cd = 0, l =raw_votes.length; + for (vote of raw_votes) { var x = 0; if( vote.indexOf(id1) > -1 ) { ++d; x++;} if( vote.indexOf(id2) > -1 ) { ++c; cd+=x; } @@ -208,8 +335,9 @@ function remove_event(event_id) { el.classList.add('pending'); if (ws && ws.readyState === WebSocket.OPEN) { var message = { + action: "remove_event", lastupdate: window.lastupdate, - removeevent: event_id + event_id: event_id } ws.send(JSON.stringify(message)); console.log('Sent:', message); @@ -233,8 +361,9 @@ function set_all_attributes(event_id, day, room, time, from_server) { el.classList.add('pending'); if (ws && ws.readyState === WebSocket.OPEN) { var message = { + action: "set_event", lastupdate: window.lastupdate, - setevent: event_id, + event_id: event_id, day: el.getAttribute('fullnarp-day'), room: el.getAttribute('fullnarp-room'), time: el.getAttribute('fullnarp-time') @@ -264,26 +393,51 @@ function signalFullnarpConnect(state) { document.body.classList.add(state); } -function getFullnarpData(lastupdate) { +function getFullnarpData() { signalFullnarpConnect('fullnarp-connecting'); - ws = new WebSocket('wss://erdgeist.org/38C3/halfnarp/fullnarp-ws'); + ws = new WebSocket('wss://content.events.ccc.de/fullnarp/ws/'); ws.onopen = () => { console.log('Connected to WebSocket server'); - //stateElement.textContent = 'Connected'; + var message = { + action: raw_votes ? "reconnect" : "bootstrap" + }; + ws.send(JSON.stringify(message)); + console.log('Sent:', message); }; ws.onmessage = (event) => { signalFullnarpConnect('fullnarp-connected'); const data = JSON.parse(event.data); console.log('Received:', data); - for (const [eventid, event_new] of Object.entries(data.data)) { - if (document.getElementById(eventid)) - set_all_attributes(eventid, 'day_'+event_new['day'], 'room'+event_new['room'], 'time_'+event_new['time'], true ) + + switch (data.property) { + + case 'pretalx': + render_lectures(data.data); + break; + + case 'halfnarp': + for (eventidlist of data.data) + for (eventid of eventidlist) + window.votes[eventid] = 1 + (window.votes[eventid] || 0 ); + raw_votes = data.data; + distribute_votes(); + break; + + case 'fullnarp': + for (const [eventid, event_new] of Object.entries(data.data)) { + if (document.getElementById(eventid)) + set_all_attributes(eventid, 'day_'+event_new['day'], 'room'+event_new['room'], 'time_'+event_new['time'], true ) + } + window.lastupdate = data.current_version; + current_version_string = ('00000'+data.current_version).slice(-5); + document.querySelector('.version').innerHTML = 'Version: '+data.current_version+''; + break; + + default: + console.log(`Unknown property: ${data['property']}.`); } - window.lastupdate = data.current_version; - current_version_string = ('00000'+data.current_version).slice(-5); - document.querySelector('.version').innerHTML = 'Version: '+data.current_version+''; }; ws.onerror = (error) => { @@ -302,11 +456,6 @@ function getFullnarpData(lastupdate) { function do_the_fullnarp() { var halfnarpAPI = 'talks_38C3.json'; var fullnarpAPI = 'votes_38c3.json'; - var allrooms = ['1','2','3'] - var allminutes = ['00','05','10','15','20','25','30','35','40','45','50','55'] - var allhours = ['10','11','12','13','14','15','16','17','18','19','20','21','22','23','00','01','02']; - var alldays = ['1','2','3','4']; - var voted = 0; window.event_speakers = {}; window.votes = {}; @@ -383,8 +532,8 @@ function do_the_fullnarp() { elem.classList.add('guide', 'time_' + hour + '00'); document.body.append(elem); - for (minute of allminutes) { - for (room of allrooms) { + for (minute of allminutes) + for (room of allrooms) for (day of alldays) { elem = document.createElement('div'); elem.classList.add('grid', 'time_' + hour + minute, 'day_' + day, 'room' + room ); @@ -408,170 +557,10 @@ function do_the_fullnarp() { return false; } } - } - } } - /* Fetch list of votes to display */ - fetch(`${fullnarpAPI}?format=json`) - .then(response => { - if (!response.ok) { - throw new Error(`HTTP error when fetching fullnarp data! status: ${response.status}`); - } - return response.json(); - }).then(data => { - window.raw_votes = data; - for (eventidlist of data) - for (eventid of eventidlist) - window.votes[eventid] = 1 + (window.votes[eventid] || 0 ); - if( ++voted == 2 ) { - window.lastupdate = 0; - distribute_votes(); - getFullnarpData(0); - } - }).catch(error => { - console.error('Fetch error:', error); - }); - - - /* Fetch list of lectures to display */ - fetch(`${halfnarpAPI}?format=json`) - .then(response => { - if (!response.ok) { - throw new Error(`HTTP error when fetching halfnarp data! status: ${response.status}`); - } - return response.json(); - }).then(data => { - for (item of data) { - /* Take copy of hidden event template div and select them, if they're in - list of previous prereferences */ - var t = document.getElementById('template').cloneNode(true); - var event_id = item.event_id.toString(); - t.classList.add('event', 'duration_' + item.duration, 'lang_' + (item.language || 'en')); - t.setAttribute('event_id', event_id); - t.setAttribute('id', 'event_' + event_id) - t.setAttribute( 'fullnarp-duration', item.duration); - - /* Sort textual info into event div */ - t.querySelector('.title').textContent = item.title; - t.querySelector('.speakers').textContent = item.speaker_names; - t.querySelector('.abstract').append(item.abstract); - - /* Store speakers and their availabilities */ - window.event_speakers[event_id] = item.speakers; - for (speaker of item.speakers) { - var have_avails = false; - if (!speaker.availabilities) - console.log("Foo"); - for (avail of speaker.availabilities) { - if (avail.id ) { - have_avails = true; - break; - } - } - if (!have_avails) - t.classList.add('has_unavailable_speaker'); - } - - t.setAttribute('draggable', 'true'); - - /* Make the event drag&droppable */ - t.ondragstart = function( event, ui ) { - event.stopPropagation(); - - event.dataTransfer.setData('text/plain', this.id ); - event.dataTransfer.dropEffect = 'move'; - event.dataTransfer.effectAllowed = 'move'; - event.target.classList.add('is-dragged'); - } - - /* While dragging make source element small enough to allow - dropping below its original area */ - t.ondrag = function( event, ui ) { - event.stopPropagation(); - event.target.classList.add('is-dragged'); - - /* When drag starts in list view, switch to calendar view */ - if( document.body.classList.contains('in-list') ) { - toggle_grid(5); - document.body.classList.add('was-list'); - } - if( document.body.classList.contains('in-drag') ) - return; - - document.body.classList.add('in-drag'); - /* mark all possible drop points regarding to availability */ - for (hour of allhours) - for (minute of allminutes) - for (day of alldays) - document.querySelectorAll('.grid.day_'+day+'.time_'+hour+minute).forEach(elem => elem.classList.toggle('possible', check_avail(event.target, day, hour+minute))); - - } - - t.ondragend = function( event, ui ) { - event.stopPropagation(); - - /* We removed in-list and the drop did not succeed. Go back to list view */ - if (document.body.classList.contains('was-list')) - toggle_grid(0); - - document.querySelectorAll('.over').forEach(elem => elem.classList.remove('over')); - document.querySelectorAll('.is-dragged').forEach(elem => elem.classList.remove('id-dragged')); - document.querySelectorAll('.possible').forEach(elem => elem.classList.remove('possible')); - document.body.classList.remove('in-drag', 'was-list'); - } - - /* start_time: 2014-12-29T21:15:00+01:00" */ - var start_time = new Date(item.start_time); - - var day = start_time.getDate()-26; - var hour = start_time.getHours(); - var mins = start_time.getMinutes(); - - /* After midnight: sort into yesterday */ - if( hour < 9 ) - day--; - - /* Fix up room for 38c3 */ - room = (item.room_id || 'room_unknown').toString().replace('471','room1').replace('472','room2').replace('473','room3'); - - /* Apply attributes to sort events into calendar */ - t.classList.add(room, 'day_' + day, 'time_' + (hour<10?'0':'') + hour + (mins<10?'0':'') + mins); - t.setAttribute('fullnarp-day', day); - t.setAttribute('fullnarp-time', (hour<10?'0':'') + hour + (mins<10?'0':'') + mins ); - t.setAttribute('fullnarp-room', room.replace('room','')); - - mark_avail(t); - - t.onclick = function(event) { - _this = this; - document.body.classList.remove('in-drag'); - if (document.body.classList.contains('correlate')) { - document.querySelectorAll('.selected').forEach(elem => elem.classList.remove('selected')); - document.querySelectorAll('.event').forEach(elem => mark_correlation(elem, _this)); - } - _this.classList.toggle('selected'); - document.querySelectorAll('.info').forEach(elem => elem.classList.add('hidden')); - event.stopPropagation(); - } - - /* Put new event into DOM tree. Track defaults to 'Other' */ - var track = item.track_id.toString(); - t.classList.add('track_' + track ); - var d = document.getElementById(track); - if (!d) - d = document.querySelector('#Other'); - d.append(t); - }; - - if( ++voted == 2 ) { - window.lastupdate = 0; - distribute_votes(); - getFullnarpData(0); - } - }).catch(error => { - console.error('Fetch error:', error); - }); + window.lastupdate = 0; + getFullnarpData(); document.onkeypress = function(e) { document.body.classList.remove('in-drag'); -- cgit v1.2.3