summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorerdgeist <erdgeist@erdgeist.org>2025-01-04 02:46:47 +0100
committererdgeist <erdgeist@erdgeist.org>2025-01-04 02:46:47 +0100
commit7f155dc09e2b8862d68ee40d514de16c064bf449 (patch)
tree23c8967b2257c8c7748cd60c34ebad9e302793e0
parentab32e563be8d99010245fc546817c5a2526d7b09 (diff)
Get prototype working
-rw-r--r--fullnarp.py143
-rwxr-xr-xhalfnarp2.py50
-rw-r--r--requirements.txt1
-rw-r--r--static/fullnarp.js429
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
38speaker availibilities. It should only be served behind some auth. 38speaker availibilities. It should only be served behind some auth.
39""" 39"""
40 40
41# Global placeholders for engine and session factory
42engine = None
43SessionLocal = None
44Base = declarative_base()
45
46
47class TalkPreference(Base):
48 __tablename__ = "talk_preference"
49 uid = Column(String, primary_key=True)
50 public_uid = Column(String, index=True)
51 talk_ids = Column(String)
52
53
41# Shared state 54# Shared state
42current_version = {} 55current_version = {}
43newest_version = 0 56newest_version = 0
@@ -46,22 +59,32 @@ current_version_lock = asyncio.Lock() # Lock for managing access to the global
46clients = {} # Key: websocket, Value: {'client_id': ..., 'last_version': ...} 59clients = {} # Key: websocket, Value: {'client_id': ..., 'last_version': ...}
47 60
48 61
62async def bootstrap_client(websocket):
63 """Provide lecture list and votes count to new client"""
64
65
49async def notify_clients(): 66async def notify_clients():
50 """Notify all connected clients of the current state.""" 67 """Notify all connected clients of the current state."""
51 async with current_version_lock: 68 async with current_version_lock:
52 # Prepare a full state update message with the current version 69 # Prepare a full state update message with the current version
53 message = {"current_version": newest_version, "data": current_version} 70 message = {
54 71 "property": "fullnarp",
55 # Notify each client about their relevant updates 72 "current_version": newest_version,
56 for client, info in clients.items(): 73 "data": current_version,
57 try: 74 }
58 # Send the state update 75
59 await client.send(json.dumps(message)) 76 # Notify each client about their relevant updates
60 # Update the client's last known version 77 for client, info in clients.items():
61 info["last_version"] = newest_version 78 try:
62 except ConnectionClosedOK: 79 # Send the state update
63 # Handle disconnected clients gracefully 80 await client.send(json.dumps(message))
64 pass 81 # Update the client's last known version
82 info["last_version"] = newest_version
83
84 print("Reply: " + json.dumps(message))
85 except ConnectionClosedOK:
86 # Handle disconnected clients gracefully
87 pass
65 88
66 89
67async def handle_client(websocket): 90async def handle_client(websocket):
@@ -71,31 +94,59 @@ async def handle_client(websocket):
71 clients[websocket] = {"client_id": id(websocket), "last_version": 0} 94 clients[websocket] = {"client_id": id(websocket), "last_version": 0}
72 95
73 try: 96 try:
74 # Send the current global state to the newly connected client
75 async with current_version_lock:
76 global newest_version
77 await websocket.send(
78 json.dumps({"current_version": newest_version, "data": current_version})
79 )
80 clients[websocket][
81 "last_version"
82 ] = newest_version # Update last known version
83
84 # Listen for updates from the client 97 # Listen for updates from the client
85 async for message in websocket: 98 async for message in websocket:
99 global newest_version
86 try: 100 try:
87 # Parse incoming message 101 # Parse incoming message
88 data = json.loads(message) 102 data = json.loads(message)
89 103 print("Got command " + message)
90 # Update global state with a lock to prevent race conditions 104
91 async with current_version_lock: 105 if data.get("action", "") == "bootstrap":
92 if "setevent" in data: 106 print("Got bootstrap command")
93 eventid = data["setevent"] 107 with open("var/talks_local_fullnarp") as data_file:
94 day = data["day"] 108 talks = json.load(data_file)
95 room = data["room"] 109 message = {"property": "pretalx", "data": talks}
96 time = data["time"] 110 await websocket.send(json.dumps(message))
97 lastupdate = data["lastupdate"] 111
98 112 with SessionLocal() as session:
113 preferences = session.query(TalkPreference).all()
114 m = []
115 for pref in preferences:
116 m.append(json.loads(pref.talk_ids))
117 message = {"property": "halfnarp", "data": m}
118 await websocket.send(json.dumps(message))
119
120 async with current_version_lock:
121 message = {
122 "property": "fullnarp",
123 "current_version": newest_version,
124 "data": current_version,
125 }
126 await websocket.send(json.dumps(message))
127 print("Reply: " + json.dumps(message))
128
129 elif data.get("action", "") == "reconnect":
130
131 async with current_version_lock:
132 message = {
133 "property": "fullnarp",
134 "current_version": newest_version,
135 "data": current_version,
136 }
137 await websocket.send(json.dumps(message))
138
139 elif data.get("action", "") == "remove_event":
140 pass
141
142 elif data.get("action", "") == "set_event":
143 eventid = data["event_id"]
144 day = data["day"]
145 room = data["room"]
146 time = data["time"]
147 lastupdate = data["lastupdate"]
148
149 async with current_version_lock:
99 newest_version += 1 # Increment the version 150 newest_version += 1 # Increment the version
100 print( 151 print(
101 "Moving event: " 152 "Moving event: "
@@ -127,8 +178,9 @@ async def handle_client(websocket):
127 ) as outfile: 178 ) as outfile:
128 json.dump(current_version, outfile) 179 json.dump(current_version, outfile)
129 180
130 # Notify all clients about the updated global state 181 # Notify all clients about the updated global state
131 await notify_clients() 182 await notify_clients()
183
132 except json.JSONDecodeError: 184 except json.JSONDecodeError:
133 await websocket.send(json.dumps({"error": "Invalid JSON"})) 185 await websocket.send(json.dumps({"error": "Invalid JSON"}))
134 except websockets.exceptions.ConnectionClosedError as e: 186 except websockets.exceptions.ConnectionClosedError as e:
@@ -141,6 +193,25 @@ async def handle_client(websocket):
141 193
142 194
143async def main(): 195async def main():
196 parser = ArgumentParser(description="halfnarp2")
197 parser.add_argument(
198 "-c", "--config", help="Config file location", default="./config.json"
199 )
200 args = parser.parse_args()
201
202 global engine, SessionLocal
203
204 with open(args.config, mode="r", encoding="utf-8") as json_file:
205 config = json.load(json_file)
206
207 DATABASE_URL = config.get("database-uri", "sqlite:///test.db")
208
209 print("Connecting to " + DATABASE_URL)
210 engine = create_engine(DATABASE_URL, echo=False)
211 SessionLocal = sessionmaker(bind=engine)
212 Base.metadata.create_all(bind=engine)
213
214 # load state file
144 newest_file = sorted(listdir("versions/"))[-1] 215 newest_file = sorted(listdir("versions/"))[-1]
145 global newest_version 216 global newest_version
146 global current_version 217 global current_version
@@ -154,8 +225,8 @@ async def main():
154 current_version = {} 225 current_version = {}
155 newest_version = 0 226 newest_version = 0
156 227
157 async with websockets.serve(handle_client, "localhost", 5009): 228 async with websockets.serve(handle_client, "localhost", 22378):
158 print("WebSocket server started on ws://localhost:5009") 229 print("WebSocket server started on ws://localhost:22378")
159 await asyncio.Future() # Run forever 230 await asyncio.Future() # Run forever
160 231
161 232
diff --git a/halfnarp2.py b/halfnarp2.py
index 8d736a0..827055a 100755
--- a/halfnarp2.py
+++ b/halfnarp2.py
@@ -9,11 +9,12 @@ import requests
9import json 9import json
10import uuid 10import uuid
11import markdown 11import markdown
12from datetime import datetime, time, timedelta
12from html_sanitizer import Sanitizer 13from html_sanitizer import Sanitizer
13from hashlib import sha256 14from hashlib import sha256
14 15
15db = SQLAlchemy(app)
16app = Flask(__name__) 16app = Flask(__name__)
17db = SQLAlchemy()
17 18
18 19
19class TalkPreference(db.Model): 20class TalkPreference(db.Model):
@@ -118,21 +119,18 @@ def get_preferences(public_uid):
118 119
119 120
120def filter_keys_halfnarp(session): 121def filter_keys_halfnarp(session):
121 abstract_html = markdown.markdown(submission["abstract"], enable_attributes=False) 122 abstract_html = markdown.markdown(session["abstract"], enable_attributes=False)
122 abstract_clean_html = Sanitizer().sanitize(abstract_html) 123 abstract_clean_html = Sanitizer().sanitize(abstract_html)
123 slot = submission["slot"] 124 slot = session["slot"]
124 125
125 return { 126 return {
126 "title": submission.get("title", "!!! NO TITLE !!!"), 127 "title": session.get("title", "!!! NO TITLE !!!"),
127 "duration": 60 * submission.get("duration", 40), 128 "duration": 60 * session.get("duration", 40),
128 "event_id": submission["code"], 129 "event_id": session["code"],
129 "language": submission.get("content_locale", "de"), 130 "language": session.get("content_locale", "de"),
130 "track_id": submission["track_id"], 131 "track_id": session["track_id"],
131 "speaker_names": ", ".join( 132 "speaker_names": ", ".join(
132 [ 133 [speaker.get("name", "unnamed") for speaker in session.get("speakers", {})]
133 speaker.get("name", "unnamed")
134 for speaker in submission.get("speakers", {})
135 ]
136 ), 134 ),
137 "abstract": abstract_clean_html, 135 "abstract": abstract_clean_html,
138 "room_id": slot.get("room_id", "room_unknown"), 136 "room_id": slot.get("room_id", "room_unknown"),
@@ -143,13 +141,13 @@ def filter_keys_halfnarp(session):
143def filter_keys_fullnarp(session, speakers): 141def filter_keys_fullnarp(session, speakers):
144 abstract_html = markdown.markdown(session["abstract"], enable_attributes=False) 142 abstract_html = markdown.markdown(session["abstract"], enable_attributes=False)
145 abstract_clean_html = Sanitizer().sanitize(abstract_html) 143 abstract_clean_html = Sanitizer().sanitize(abstract_html)
146 slot = submission["slot"] 144 slot = session["slot"]
147 145
148 speaker_info = [] 146 speaker_info = []
149 for speaker in submission.get("speakers", {}): 147 for speaker in session.get("speakers", {}):
150 speaker_info.append(speakers[speaker["code"]]) 148 speaker_info.append(speakers[speaker["code"]])
151 # if len(speakers[speaker['code']]['availabilities']) == 0: 149 # if len(speakers[speaker['code']]['availabilities']) == 0:
152 # 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'] ) 150 # 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'] )
153 151
154 """This fixes availabilites ranging to or from exactly midnight to the more likely start of availibility at 8am and end of availibility at 3am""" 152 """This fixes availabilites ranging to or from exactly midnight to the more likely start of availibility at 8am and end of availibility at 3am"""
155 153
@@ -179,19 +177,17 @@ def filter_keys_fullnarp(session, speakers):
179 avail["end"] = str(end_new) 177 avail["end"] = str(end_new)
180 178
181 return { 179 return {
182 "title": submission.get("title", "!!! NO TITLE !!!"), 180 "title": session.get("title", "!!! NO TITLE !!!"),
183 "duration": 60 * submission.get("duration", 40), 181 "duration": 60 * session.get("duration", 40),
184 "event_id": submission["code"], 182 "event_id": session["code"],
185 "language": submission.get("content_locale", "de"), 183 "language": session.get("content_locale", "de"),
186 "track_id": submission["track_id"], 184 "track_id": session["track_id"],
185 "speakers": speaker_info,
187 "speaker_names": ", ".join( 186 "speaker_names": ", ".join(
188 [ 187 [speaker.get("name", "unnamed") for speaker in session.get("speakers", {})]
189 speaker.get("name", "unnamed")
190 for speaker in submission.get("speakers", {})
191 ]
192 ), 188 ),
193 "abstract": abstract_clean_html, 189 "abstract": abstract_clean_html,
194 "room_id": slot.get("room_id", "room_unknown"), 190 "room_id": "room" + str(slot.get("room_id", "_unknown")),
195 "start_time": slot.get("start", "1970-01-01"), 191 "start_time": slot.get("start", "1970-01-01"),
196 } 192 }
197 193
@@ -217,7 +213,7 @@ def fetch_talks(config):
217 speakers = dict((speaker["code"], speaker) for speaker in speakers_json["results"]) 213 speakers = dict((speaker["code"], speaker) for speaker in speakers_json["results"])
218 214
219 sessions = [ 215 sessions = [
220 filter_keys(submission) 216 filter_keys_halfnarp(submission)
221 for submission in talks_json["results"] 217 for submission in talks_json["results"]
222 if submission["state"] == "confirmed" 218 if submission["state"] == "confirmed"
223 and not "non-public" in submission.get("tags", {}) 219 and not "non-public" in submission.get("tags", {})
@@ -278,7 +274,7 @@ if __name__ == "__main__":
278 app.jinja_env.lstrip_blocks = True 274 app.jinja_env.lstrip_blocks = True
279 CORS() 275 CORS()
280 276
281 db.init(app) 277 db.init_app(app)
282 278
283 with app.app_context(): 279 with app.app_context():
284 db.create_all() 280 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
17markdown 17markdown
18psycopg2 18psycopg2
19gunicorn 19gunicorn
20websockets
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 @@
1let ws; // WebSocket instance 1let ws; // WebSocket instance
2let allrooms = ['1','2','3']
3let allminutes = ['00','05','10','15','20','25','30','35','40','45','50','55']
4let allhours = ['10','11','12','13','14','15','16','17','18','19','20','21','22','23','00','01','02'];
5let alldays = ['1','2','3','4'];
6let raw_votes;
2 7
3function toggle_grid(whichDay) { 8function toggle_grid(whichDay) {
4 var vclasses= [['in-list'], ['in-calendar', 'onlyday1'], ['in-calendar', 'onlyday2'], ['in-calendar', 'onlyday3'], 9 var vclasses= [['in-list'], ['in-calendar', 'onlyday1'], ['in-calendar', 'onlyday2'], ['in-calendar', 'onlyday3'],
@@ -8,6 +13,128 @@ function toggle_grid(whichDay) {
8 document.body.classList.add(...vclasses[whichDay]); 13 document.body.classList.add(...vclasses[whichDay]);
9} 14}
10 15
16function render_lectures(data) {
17 for (item of data) {
18 /* Take copy of hidden event template div and select them, if they're in
19 list of previous prereferences */
20 var t = document.getElementById('template').cloneNode(true);
21 var event_id = item.event_id.toString();
22
23 t.classList.add('event', 'duration_' + item.duration, 'lang_' + (item.language || 'en'));
24 t.setAttribute('event_id', event_id);
25 t.setAttribute('id', 'event_' + event_id);
26 t.setAttribute('fullnarp-duration', item.duration);
27 t.setAttribute('draggable', 'true');
28
29 /* Sort textual info into event div */
30 t.querySelector('.title').textContent = item.title;
31 t.querySelector('.speakers').textContent = item.speaker_names;
32 t.querySelector('.abstract').append(item.abstract);
33
34 /* Store speakers and their availabilities */
35 window.event_speakers[event_id] = item.speakers;
36 for (speaker of item.speakers) {
37 var have_avails = false;
38 for (avail of speaker.availabilities) {
39 if (avail.id ) {
40 have_avails = true;
41 break;
42 }
43 }
44 if (!have_avails)
45 t.classList.add('has_unavailable_speaker');
46 }
47
48 /* Make the event drag&droppable */
49 t.ondragstart = function( event, ui ) {
50 event.stopPropagation();
51
52 event.dataTransfer.setData('text/plain', this.id );
53 event.dataTransfer.dropEffect = 'move';
54 event.dataTransfer.effectAllowed = 'move';
55 event.target.classList.add('is-dragged');
56 }
57
58 /* While dragging make source element small enough to allow
59 dropping below its original area */
60 t.ondrag = function( event, ui ) {
61 event.stopPropagation();
62 event.target.classList.add('is-dragged');
63
64 /* When drag starts in list view, switch to calendar view */
65 if( document.body.classList.contains('in-list') ) {
66 toggle_grid(5);
67 document.body.classList.add('was-list');
68 }
69 if( document.body.classList.contains('in-drag') )
70 return;
71
72 document.body.classList.add('in-drag');
73 /* mark all possible drop points regarding to availability */
74 for (hour of allhours)
75 for (minute of allminutes)
76 for (day of alldays)
77 document.querySelectorAll('.grid.day_'+day+'.time_'+hour+minute).forEach(elem => elem.classList.toggle('possible', check_avail(event.target, day, hour+minute)));
78
79 }
80
81 t.ondragend = function( event, ui ) {
82 event.stopPropagation();
83
84 /* We removed in-list and the drop did not succeed. Go back to list view */
85 if (document.body.classList.contains('was-list'))
86 toggle_grid(0);
87
88 document.querySelectorAll('.over').forEach(elem => elem.classList.remove('over'));
89 document.querySelectorAll('.is-dragged').forEach(elem => elem.classList.remove('id-dragged'));
90 document.querySelectorAll('.possible').forEach(elem => elem.classList.remove('possible'));
91 document.body.classList.remove('in-drag', 'was-list');
92 }
93
94 /* start_time: 2014-12-29T21:15:00+01:00" */
95 var start_time = new Date(item.start_time);
96
97 var day = start_time.getDate()-26;
98 var hour = start_time.getHours();
99 var mins = start_time.getMinutes();
100
101 /* After midnight: sort into yesterday */
102 if( hour < 9 )
103 day--;
104
105 /* Fix up room for 38c3 */
106 room = (item.room_id || 'room_unknown').toString().replace('471','room1').replace('472','room2').replace('473','room3');
107
108 /* Apply attributes to sort events into calendar */
109 t.classList.add(room, 'day_' + day, 'time_' + (hour<10?'0':'') + hour + (mins<10?'0':'') + mins);
110 t.setAttribute('fullnarp-day', day);
111 t.setAttribute('fullnarp-time', (hour<10?'0':'') + hour + (mins<10?'0':'') + mins );
112 t.setAttribute('fullnarp-room', room.replace('room',''));
113
114 mark_avail(t);
115
116 t.onclick = function(event) {
117 _this = this;
118 document.body.classList.remove('in-drag');
119 if (document.body.classList.contains('correlate')) {
120 document.querySelectorAll('.selected').forEach(elem => elem.classList.remove('selected'));
121 document.querySelectorAll('.event').forEach(elem => mark_correlation(elem, _this));
122 }
123 _this.classList.toggle('selected');
124 document.querySelectorAll('.info').forEach(elem => elem.classList.add('hidden'));
125 event.stopPropagation();
126 }
127
128 /* Put new event into DOM tree. Track defaults to 'Other' */
129 var track = item.track_id.toString();
130 t.classList.add('track_' + track );
131 var d = document.getElementById(track);
132 if (!d)
133 d = document.getElementById('Other');
134 d.append(t);
135 }
136}
137
11function distribute_votes() { 138function distribute_votes() {
12 document.querySelectorAll('.event').forEach( function(element) { 139 document.querySelectorAll('.event').forEach( function(element) {
13 var eid = element.getAttribute('event_id'); 140 var eid = element.getAttribute('event_id');
@@ -37,8 +164,8 @@ function distribute_votes() {
37} 164}
38 165
39function corr_for_eventids(id1, id2) { 166function corr_for_eventids(id1, id2) {
40 var d = 0, c = 0, cd = 0, l = window.raw_votes.length; 167 var d = 0, c = 0, cd = 0, l = raw_votes.length;
41 for (item of window.raw_votes) { 168 for (item of raw_votes) {
42 var x = 0; 169 var x = 0;
43 if( item.indexOf(id1) > -1 ) { ++d; x++;} 170 if( item.indexOf(id1) > -1 ) { ++d; x++;}
44 if( item.indexOf(id2) > -1 ) { ++c; cd+=x; } 171 if( item.indexOf(id2) > -1 ) { ++c; cd+=x; }
@@ -52,42 +179,42 @@ function corr_for_eventids(id1, id2) {
52} 179}
53 180
54function show_all_correlates(el) { 181function show_all_correlates(el) {
55 /* First identify the room to see what other rooms to consider 182 /* First identify the room to see what other rooms to consider
56 correlates always grow from the top slot to the right, 183 correlates always grow from the top slot to the right,
57 unless there's an overlapping event to the left that starts earlier 184 unless there's an overlapping event to the left that starts earlier
58 */ 185 */
59 var event_room = el.getAttribute('fullnarp-room'); 186 var event_room = el.getAttribute('fullnarp-room');
60 var event_day = el.getAttribute('fullnarp-day'); 187 var event_day = el.getAttribute('fullnarp-day');
61 var event_time = el.getAttribute('fullnarp-time'); 188 var event_time = el.getAttribute('fullnarp-time');
62 189
63 if (!event_time) return; 190 if (!event_time) return;
64 191
65 var event_start; 192 var event_start;
66 try { event_start = time_to_mins(event_time); } catch(e) { return; } 193 try { event_start = time_to_mins(event_time); } catch(e) { return; }
67 var event_duration = el.getAttribute('fullnarp-duration') / 60; 194 var event_duration = el.getAttribute('fullnarp-duration') / 60;
68 195
69 /* Only test events to the right, if they start at the exact same time */ 196 /* Only test events to the right, if they start at the exact same time */
70 document.querySelectorAll('.event.day_'+event_day).forEach( function(check_el, index) { 197 document.querySelectorAll('.event.day_'+event_day).forEach( function(check_el, index) {
71 var check_room = check_el.getAttribute('fullnarp-room'); 198 var check_room = check_el.getAttribute('fullnarp-room');
72 if (event_room == check_room) return; 199 if (event_room == check_room) return;
73 200
74 var check_time = check_el.getAttribute('fullnarp-time'); 201 var check_time = check_el.getAttribute('fullnarp-time');
75 if (!check_time) return; 202 if (!check_time) return;
76 var check_start = time_to_mins(check_time); 203 var check_start = time_to_mins(check_time);
77 var check_duration = check_el.getAttribute('fullnarp-duration') / 60; 204 var check_duration = check_el.getAttribute('fullnarp-duration') / 60;
78 var dist = check_el.getAttribute('fullnarp-room') - event_room; 205 var dist = check_el.getAttribute('fullnarp-room') - event_room;
79 var overlap = check_start < event_start + event_duration && event_start < check_start + check_duration; 206 var overlap = check_start < event_start + event_duration && event_start < check_start + check_duration;
80 207
81 if (!overlap) return; 208 if (!overlap) return;
82 if (event_start == check_start && dist <= 0) return; 209 if (event_start == check_start && dist <= 0) return;
83 if (event_start < check_start) return; 210 if (event_start < check_start) return;
84 211
85 var corr = corr_for_eventids(el.getAttribute('event_id'), check_el.getAttribute('event_id')); 212 var corr = corr_for_eventids(el.getAttribute('event_id'), check_el.getAttribute('event_id'));
86 var dir = dist > 0 ? 'r' : 'l'; 213 var dir = dist > 0 ? 'r' : 'l';
87 var div = document.createElement('div'); 214 var div = document.createElement('div');
88 div.classList.add('corrweb', dir.repeat(Math.abs(dist)), 'day_' + event_day, 'room' + event_room, 'time_' + event_time, 'corr_d_' + corr); 215 div.classList.add('corrweb', dir.repeat(Math.abs(dist)), 'day_' + event_day, 'room' + event_room, 'time_' + event_time, 'corr_d_' + corr);
89 document.body.appendChild(div); 216 document.body.appendChild(div);
90 }) 217 });
91} 218}
92 219
93function display_correlation() { 220function display_correlation() {
@@ -104,8 +231,8 @@ function display_correlation() {
104function mark_correlation(dest, comp) { 231function mark_correlation(dest, comp) {
105 var id1 = dest.getAttribute('event_id'); 232 var id1 = dest.getAttribute('event_id');
106 var id2 = comp.getAttribute('event_id'); 233 var id2 = comp.getAttribute('event_id');
107 var d = 0, c = 0, cd = 0, l = window.raw_votes.length; 234 var d = 0, c = 0, cd = 0, l =raw_votes.length;
108 for (vote of window.raw_votes) { 235 for (vote of raw_votes) {
109 var x = 0; 236 var x = 0;
110 if( vote.indexOf(id1) > -1 ) { ++d; x++;} 237 if( vote.indexOf(id1) > -1 ) { ++d; x++;}
111 if( vote.indexOf(id2) > -1 ) { ++c; cd+=x; } 238 if( vote.indexOf(id2) > -1 ) { ++c; cd+=x; }
@@ -208,8 +335,9 @@ function remove_event(event_id) {
208 el.classList.add('pending'); 335 el.classList.add('pending');
209 if (ws && ws.readyState === WebSocket.OPEN) { 336 if (ws && ws.readyState === WebSocket.OPEN) {
210 var message = { 337 var message = {
338 action: "remove_event",
211 lastupdate: window.lastupdate, 339 lastupdate: window.lastupdate,
212 removeevent: event_id 340 event_id: event_id
213 } 341 }
214 ws.send(JSON.stringify(message)); 342 ws.send(JSON.stringify(message));
215 console.log('Sent:', message); 343 console.log('Sent:', message);
@@ -233,8 +361,9 @@ function set_all_attributes(event_id, day, room, time, from_server) {
233 el.classList.add('pending'); 361 el.classList.add('pending');
234 if (ws && ws.readyState === WebSocket.OPEN) { 362 if (ws && ws.readyState === WebSocket.OPEN) {
235 var message = { 363 var message = {
364 action: "set_event",
236 lastupdate: window.lastupdate, 365 lastupdate: window.lastupdate,
237 setevent: event_id, 366 event_id: event_id,
238 day: el.getAttribute('fullnarp-day'), 367 day: el.getAttribute('fullnarp-day'),
239 room: el.getAttribute('fullnarp-room'), 368 room: el.getAttribute('fullnarp-room'),
240 time: el.getAttribute('fullnarp-time') 369 time: el.getAttribute('fullnarp-time')
@@ -264,26 +393,51 @@ function signalFullnarpConnect(state) {
264 document.body.classList.add(state); 393 document.body.classList.add(state);
265} 394}
266 395
267function getFullnarpData(lastupdate) { 396function getFullnarpData() {
268 signalFullnarpConnect('fullnarp-connecting'); 397 signalFullnarpConnect('fullnarp-connecting');
269 ws = new WebSocket('wss://erdgeist.org/38C3/halfnarp/fullnarp-ws'); 398 ws = new WebSocket('wss://content.events.ccc.de/fullnarp/ws/');
270 399
271 ws.onopen = () => { 400 ws.onopen = () => {
272 console.log('Connected to WebSocket server'); 401 console.log('Connected to WebSocket server');
273 //stateElement.textContent = 'Connected'; 402 var message = {
403 action: raw_votes ? "reconnect" : "bootstrap"
404 };
405 ws.send(JSON.stringify(message));
406 console.log('Sent:', message);
274 }; 407 };
275 408
276 ws.onmessage = (event) => { 409 ws.onmessage = (event) => {
277 signalFullnarpConnect('fullnarp-connected'); 410 signalFullnarpConnect('fullnarp-connected');
278 const data = JSON.parse(event.data); 411 const data = JSON.parse(event.data);
279 console.log('Received:', data); 412 console.log('Received:', data);
280 for (const [eventid, event_new] of Object.entries(data.data)) { 413
281 if (document.getElementById(eventid)) 414 switch (data.property) {
282 set_all_attributes(eventid, 'day_'+event_new['day'], 'room'+event_new['room'], 'time_'+event_new['time'], true ) 415
416 case 'pretalx':
417 render_lectures(data.data);
418 break;
419
420 case 'halfnarp':
421 for (eventidlist of data.data)
422 for (eventid of eventidlist)
423 window.votes[eventid] = 1 + (window.votes[eventid] || 0 );
424 raw_votes = data.data;
425 distribute_votes();
426 break;
427
428 case 'fullnarp':
429 for (const [eventid, event_new] of Object.entries(data.data)) {
430 if (document.getElementById(eventid))
431 set_all_attributes(eventid, 'day_'+event_new['day'], 'room'+event_new['room'], 'time_'+event_new['time'], true )
432 }
433 window.lastupdate = data.current_version;
434 current_version_string = ('00000'+data.current_version).slice(-5);
435 document.querySelector('.version').innerHTML = '<a href="https://content.events.ccc.de/fullnarp/versions/fullnarp_'+current_version_string+'.json">Version: '+data.current_version+'</a>';
436 break;
437
438 default:
439 console.log(`Unknown property: ${data['property']}.`);
283 } 440 }
284 window.lastupdate = data.current_version;
285 current_version_string = ('00000'+data.current_version).slice(-5);
286 document.querySelector('.version').innerHTML = '<a href="https://erdgeist.org/38C3/halfnarp/versions/fullnarp_'+current_version_string+'.json">Version: '+data.current_version+'</a>';
287 }; 441 };
288 442
289 ws.onerror = (error) => { 443 ws.onerror = (error) => {
@@ -302,11 +456,6 @@ function getFullnarpData(lastupdate) {
302function do_the_fullnarp() { 456function do_the_fullnarp() {
303 var halfnarpAPI = 'talks_38C3.json'; 457 var halfnarpAPI = 'talks_38C3.json';
304 var fullnarpAPI = 'votes_38c3.json'; 458 var fullnarpAPI = 'votes_38c3.json';
305 var allrooms = ['1','2','3']
306 var allminutes = ['00','05','10','15','20','25','30','35','40','45','50','55']
307 var allhours = ['10','11','12','13','14','15','16','17','18','19','20','21','22','23','00','01','02'];
308 var alldays = ['1','2','3','4'];
309 var voted = 0;
310 window.event_speakers = {}; 459 window.event_speakers = {};
311 window.votes = {}; 460 window.votes = {};
312 461
@@ -383,8 +532,8 @@ function do_the_fullnarp() {
383 elem.classList.add('guide', 'time_' + hour + '00'); 532 elem.classList.add('guide', 'time_' + hour + '00');
384 document.body.append(elem); 533 document.body.append(elem);
385 534
386 for (minute of allminutes) { 535 for (minute of allminutes)
387 for (room of allrooms) { 536 for (room of allrooms)
388 for (day of alldays) { 537 for (day of alldays) {
389 elem = document.createElement('div'); 538 elem = document.createElement('div');
390 elem.classList.add('grid', 'time_' + hour + minute, 'day_' + day, 'room' + room ); 539 elem.classList.add('grid', 'time_' + hour + minute, 'day_' + day, 'room' + room );
@@ -408,170 +557,10 @@ function do_the_fullnarp() {
408 return false; 557 return false;
409 } 558 }
410 } 559 }
411 }
412 }
413 } 560 }
414 561
415 /* Fetch list of votes to display */ 562 window.lastupdate = 0;
416 fetch(`${fullnarpAPI}?format=json`) 563 getFullnarpData();
417 .then(response => {
418 if (!response.ok) {
419 throw new Error(`HTTP error when fetching fullnarp data! status: ${response.status}`);
420 }
421 return response.json();
422 }).then(data => {
423 window.raw_votes = data;
424 for (eventidlist of data)
425 for (eventid of eventidlist)
426 window.votes[eventid] = 1 + (window.votes[eventid] || 0 );
427 if( ++voted == 2 ) {
428 window.lastupdate = 0;
429 distribute_votes();
430 getFullnarpData(0);
431 }
432 }).catch(error => {
433 console.error('Fetch error:', error);
434 });
435
436
437 /* Fetch list of lectures to display */
438 fetch(`${halfnarpAPI}?format=json`)
439 .then(response => {
440 if (!response.ok) {
441 throw new Error(`HTTP error when fetching halfnarp data! status: ${response.status}`);
442 }
443 return response.json();
444 }).then(data => {
445 for (item of data) {
446 /* Take copy of hidden event template div and select them, if they're in
447 list of previous prereferences */
448 var t = document.getElementById('template').cloneNode(true);
449 var event_id = item.event_id.toString();
450 t.classList.add('event', 'duration_' + item.duration, 'lang_' + (item.language || 'en'));
451 t.setAttribute('event_id', event_id);
452 t.setAttribute('id', 'event_' + event_id)
453 t.setAttribute( 'fullnarp-duration', item.duration);
454
455 /* Sort textual info into event div */
456 t.querySelector('.title').textContent = item.title;
457 t.querySelector('.speakers').textContent = item.speaker_names;
458 t.querySelector('.abstract').append(item.abstract);
459
460 /* Store speakers and their availabilities */
461 window.event_speakers[event_id] = item.speakers;
462 for (speaker of item.speakers) {
463 var have_avails = false;
464 if (!speaker.availabilities)
465 console.log("Foo");
466 for (avail of speaker.availabilities) {
467 if (avail.id ) {
468 have_avails = true;
469 break;
470 }
471 }
472 if (!have_avails)
473 t.classList.add('has_unavailable_speaker');
474 }
475
476 t.setAttribute('draggable', 'true');
477
478 /* Make the event drag&droppable */
479 t.ondragstart = function( event, ui ) {
480 event.stopPropagation();
481
482 event.dataTransfer.setData('text/plain', this.id );
483 event.dataTransfer.dropEffect = 'move';
484 event.dataTransfer.effectAllowed = 'move';
485 event.target.classList.add('is-dragged');
486 }
487
488 /* While dragging make source element small enough to allow
489 dropping below its original area */
490 t.ondrag = function( event, ui ) {
491 event.stopPropagation();
492 event.target.classList.add('is-dragged');
493
494 /* When drag starts in list view, switch to calendar view */
495 if( document.body.classList.contains('in-list') ) {
496 toggle_grid(5);
497 document.body.classList.add('was-list');
498 }
499 if( document.body.classList.contains('in-drag') )
500 return;
501
502 document.body.classList.add('in-drag');
503 /* mark all possible drop points regarding to availability */
504 for (hour of allhours)
505 for (minute of allminutes)
506 for (day of alldays)
507 document.querySelectorAll('.grid.day_'+day+'.time_'+hour+minute).forEach(elem => elem.classList.toggle('possible', check_avail(event.target, day, hour+minute)));
508
509 }
510
511 t.ondragend = function( event, ui ) {
512 event.stopPropagation();
513
514 /* We removed in-list and the drop did not succeed. Go back to list view */
515 if (document.body.classList.contains('was-list'))
516 toggle_grid(0);
517
518 document.querySelectorAll('.over').forEach(elem => elem.classList.remove('over'));
519 document.querySelectorAll('.is-dragged').forEach(elem => elem.classList.remove('id-dragged'));
520 document.querySelectorAll('.possible').forEach(elem => elem.classList.remove('possible'));
521 document.body.classList.remove('in-drag', 'was-list');
522 }
523
524 /* start_time: 2014-12-29T21:15:00+01:00" */
525 var start_time = new Date(item.start_time);
526
527 var day = start_time.getDate()-26;
528 var hour = start_time.getHours();
529 var mins = start_time.getMinutes();
530
531 /* After midnight: sort into yesterday */
532 if( hour < 9 )
533 day--;
534
535 /* Fix up room for 38c3 */
536 room = (item.room_id || 'room_unknown').toString().replace('471','room1').replace('472','room2').replace('473','room3');
537
538 /* Apply attributes to sort events into calendar */
539 t.classList.add(room, 'day_' + day, 'time_' + (hour<10?'0':'') + hour + (mins<10?'0':'') + mins);
540 t.setAttribute('fullnarp-day', day);
541 t.setAttribute('fullnarp-time', (hour<10?'0':'') + hour + (mins<10?'0':'') + mins );
542 t.setAttribute('fullnarp-room', room.replace('room',''));
543
544 mark_avail(t);
545
546 t.onclick = function(event) {
547 _this = this;
548 document.body.classList.remove('in-drag');
549 if (document.body.classList.contains('correlate')) {
550 document.querySelectorAll('.selected').forEach(elem => elem.classList.remove('selected'));
551 document.querySelectorAll('.event').forEach(elem => mark_correlation(elem, _this));
552 }
553 _this.classList.toggle('selected');
554 document.querySelectorAll('.info').forEach(elem => elem.classList.add('hidden'));
555 event.stopPropagation();
556 }
557
558 /* Put new event into DOM tree. Track defaults to 'Other' */
559 var track = item.track_id.toString();
560 t.classList.add('track_' + track );
561 var d = document.getElementById(track);
562 if (!d)
563 d = document.querySelector('#Other');
564 d.append(t);
565 };
566
567 if( ++voted == 2 ) {
568 window.lastupdate = 0;
569 distribute_votes();
570 getFullnarpData(0);
571 }
572 }).catch(error => {
573 console.error('Fetch error:', error);
574 });
575 564
576 document.onkeypress = function(e) { 565 document.onkeypress = function(e) {
577 document.body.classList.remove('in-drag'); 566 document.body.classList.remove('in-drag');