diff options
author | erdgeist <erdgeist@erdgeist.org> | 2025-01-04 02:46:47 +0100 |
---|---|---|
committer | erdgeist <erdgeist@erdgeist.org> | 2025-01-04 02:46:47 +0100 |
commit | 7f155dc09e2b8862d68ee40d514de16c064bf449 (patch) | |
tree | 23c8967b2257c8c7748cd60c34ebad9e302793e0 | |
parent | ab32e563be8d99010245fc546817c5a2526d7b09 (diff) |
Get prototype working
-rw-r--r-- | fullnarp.py | 143 | ||||
-rwxr-xr-x | halfnarp2.py | 50 | ||||
-rw-r--r-- | requirements.txt | 1 | ||||
-rw-r--r-- | 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 | |||
38 | speaker availibilities. It should only be served behind some auth. | 38 | speaker availibilities. It should only be served behind some auth. |
39 | """ | 39 | """ |
40 | 40 | ||
41 | # Global placeholders for engine and session factory | ||
42 | engine = None | ||
43 | SessionLocal = None | ||
44 | Base = declarative_base() | ||
45 | |||
46 | |||
47 | class 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 |
42 | current_version = {} | 55 | current_version = {} |
43 | newest_version = 0 | 56 | newest_version = 0 |
@@ -46,22 +59,32 @@ current_version_lock = asyncio.Lock() # Lock for managing access to the global | |||
46 | clients = {} # Key: websocket, Value: {'client_id': ..., 'last_version': ...} | 59 | clients = {} # Key: websocket, Value: {'client_id': ..., 'last_version': ...} |
47 | 60 | ||
48 | 61 | ||
62 | async def bootstrap_client(websocket): | ||
63 | """Provide lecture list and votes count to new client""" | ||
64 | |||
65 | |||
49 | async def notify_clients(): | 66 | async 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 | ||
67 | async def handle_client(websocket): | 90 | async 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 | ||
143 | async def main(): | 195 | async 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 | |||
9 | import json | 9 | import json |
10 | import uuid | 10 | import uuid |
11 | import markdown | 11 | import markdown |
12 | from datetime import datetime, time, timedelta | ||
12 | from html_sanitizer import Sanitizer | 13 | from html_sanitizer import Sanitizer |
13 | from hashlib import sha256 | 14 | from hashlib import sha256 |
14 | 15 | ||
15 | db = SQLAlchemy(app) | ||
16 | app = Flask(__name__) | 16 | app = Flask(__name__) |
17 | db = SQLAlchemy() | ||
17 | 18 | ||
18 | 19 | ||
19 | class TalkPreference(db.Model): | 20 | class TalkPreference(db.Model): |
@@ -118,21 +119,18 @@ def get_preferences(public_uid): | |||
118 | 119 | ||
119 | 120 | ||
120 | def filter_keys_halfnarp(session): | 121 | def 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): | |||
143 | def filter_keys_fullnarp(session, speakers): | 141 | def 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 | |||
17 | markdown | 17 | markdown |
18 | psycopg2 | 18 | psycopg2 |
19 | gunicorn | 19 | gunicorn |
20 | 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 @@ | |||
1 | let ws; // WebSocket instance | 1 | let ws; // WebSocket instance |
2 | let allrooms = ['1','2','3'] | ||
3 | let allminutes = ['00','05','10','15','20','25','30','35','40','45','50','55'] | ||
4 | let allhours = ['10','11','12','13','14','15','16','17','18','19','20','21','22','23','00','01','02']; | ||
5 | let alldays = ['1','2','3','4']; | ||
6 | let raw_votes; | ||
2 | 7 | ||
3 | function toggle_grid(whichDay) { | 8 | function 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 | ||
16 | function 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 | |||
11 | function distribute_votes() { | 138 | function 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 | ||
39 | function corr_for_eventids(id1, id2) { | 166 | function 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 | ||
54 | function show_all_correlates(el) { | 181 | function 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 | ||
93 | function display_correlation() { | 220 | function display_correlation() { |
@@ -104,8 +231,8 @@ function display_correlation() { | |||
104 | function mark_correlation(dest, comp) { | 231 | function 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 | ||
267 | function getFullnarpData(lastupdate) { | 396 | function 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) { | |||
302 | function do_the_fullnarp() { | 456 | function 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'); |