summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile14
-rw-r--r--config-example.json22
-rwxr-xr-xrater.py172
-rw-r--r--requirements.txt14
-rw-r--r--static/rater.css292
-rw-r--r--static/rater.js337
-rw-r--r--templates/index.html122
7 files changed, 973 insertions, 0 deletions
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..18dfbcd
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,14 @@
1all: install
2
3import:
4 venv/bin/python3 ./rater.py -i
5
6run:
7 PYTHONIOENCODING=utf-8 venv/bin/python3 ./rater.py
8
9venv:
10 python3 -m venv ./venv
11
12install: venv
13 venv/bin/pip install --upgrade pip
14 venv/bin/pip install -r requirements.txt
diff --git a/config-example.json b/config-example.json
new file mode 100644
index 0000000..5c4e4bc
--- /dev/null
+++ b/config-example.json
@@ -0,0 +1,22 @@
1{
2 "track": "polsoc",
3 "track-name": "Ethics%2C+Society+%26+Politics",
4 "host": "127.0.0.1",
5 "port": 5000,
6 "frab-url": "https://frab.cccv.de/",
7 "frab-user": "<FRAB-USER>",
8 "frab-password": "<FRAB-PASSWORD>",
9 "frab-conference": "36C3",
10 "rt-url": "https://rt.cccv.de/",
11 "rt-user": "<RT-USER>",
12 "rt-password": "<RT-PASS>",
13 "categories": [ "Relevanz", "Expertise", "Praesentation", "Nachfrage" ],
14 "frab-person-map": {
15 "anna": 12345,
16 "ben": 6789
17 },
18 "rt-person-map": {
19 "anna": "anna@mail.org",
20 "ben": "ben@mailbox.com"
21 }
22}
diff --git a/rater.py b/rater.py
new file mode 100755
index 0000000..e1ed9c5
--- /dev/null
+++ b/rater.py
@@ -0,0 +1,172 @@
1#!venv/bin/python
2
3from flask import Flask, render_template, jsonify, request
4from flask_sqlalchemy import SQLAlchemy
5from lxml import etree
6from argparse import ArgumentParser
7import requests
8import json
9
10# Use this on FreeBSD when you've compiled pyopenssl with openssl from ports
11# import urllib3.contrib.pyopenssl
12# urllib3.contrib.pyopenssl.inject_into_urllib3()
13
14parser = ArgumentParser(description="C3 rating helper")
15parser.add_argument("-i", action="store_true", dest="frab_import", default=False, help="import events from frab")
16parser.add_argument("-c", "--config", help="Config file location", default="./config.json")
17args = parser.parse_args()
18
19with open(args.config, mode="r", encoding="utf-8") as json_file:
20 config_data = json.load(json_file)
21
22app = Flask(__name__)
23app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///'+config_data.get('frab-conference')+'-'+config_data.get('track')+'.db'
24app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
25app.config['SECRET_KEY'] = 'Silence is golden. Gerd Eist.'
26app.jinja_env.trim_blocks = True
27app.jinja_env.lstrip_blocks = True
28
29db = SQLAlchemy(app)
30
31class Event(db.Model):
32 """An event as dumped from frab"""
33 frab_id = db.Column(db.Integer, primary_key=True)
34 title = db.Column(db.String(1024))
35 subtitle = db.Column(db.String(1024))
36 abstract = db.Column(db.Text())
37 description = db.Column(db.Text())
38 state = db.Column(db.String(64))
39 event_type = db.Column(db.String(64))
40 speakers = db.Column(db.String(1024))
41 coordinator = db.Column(db.String(1024))
42 notes = db.Column(db.Text())
43
44class EventRating(db.Model):
45 """A rating as given by a logged in user"""
46 id = db.Column(db.Integer, primary_key=True)
47 submitter = db.Column(db.String(1024))
48 event_id = db.Column(db.Integer, db.ForeignKey('event.frab_id'))
49 event = db.relationship('Event', backref=db.backref('ratings', lazy='dynamic'))
50 comment = db.Column(db.Text())
51 rating_dict = db.Column(db.String(1024), server_default="{}")
52
53@app.route("/")
54def root():
55 events = Event.query.all()
56 return render_template('index.html', events=events, json=json, config=config_data, cat=config_data.get('categories'))
57
58@app.route('/api/ratings')
59def get_ratings():
60 return jsonify(EventRating.query.all())
61
62@app.route('/api/set_event_state/<eventid>', methods=['POST'])
63def set_sevent_state(eventid):
64 content = request.json
65 dbevent = Event.query.get(eventid)
66 dbevent.state = content.get('state', 'new')
67 db.session.commit()
68 return jsonify({"result":"ok"})
69
70@app.route('/api/set_event_coordinator/<eventid>', methods=['POST'])
71def set_sevent_coordinator(eventid):
72 content = request.json
73 dbevent = Event.query.get(eventid)
74 dbevent.coordinator = content['coordinator']
75 db.session.commit()
76 return jsonify({"result":"ok"})
77
78@app.route('/api/remove_event/<eventid>', methods=['POST'])
79def remove_event(eventid):
80 dbevent = Event.query.get(eventid)
81 if dbevent.state == 'gone':
82 db.session.delete(dbevent)
83 db.session.commit()
84 return jsonify({"result":"ok"})
85
86@app.route('/api/remove_rating/<eventid>', methods=['POST'])
87def remove_rating(eventid):
88 content = request.json
89 rating = EventRating.query.filter_by(event_id = eventid, submitter = content['author']).first()
90 if rating:
91 db.session.delete(rating)
92 db.session.commit()
93 return jsonify({"result":"ok"})
94
95@app.route('/api/add_rating/<eventid>', methods=['POST'])
96def add_rating(eventid):
97 content = request.json
98 print ( str(eventid) + " " + str(content))
99 r = content.get('ratings', '{}');
100
101 rating = EventRating.query.filter_by(event_id = eventid, submitter = content['author']).first()
102
103 rd = json.dumps({ k: r.get(k,'0') for k in ['category1', 'category2', 'category3', 'category4'] })
104 if rating:
105 if 'comment' in content:
106 rating.comment = content['comment']
107 rating.rating_dict = rd
108 else:
109 db.session.add( EventRating( submitter = content.get('author','anonymous'), event_id = eventid, comment = content['comment'], rating_dict = rd))
110
111 db.session.commit()
112 return jsonify({"result":"ok"})
113
114def fetch_talks(config):
115 sess = requests.Session()
116 new_session_page = sess.get(config.get('frab-url'))
117 tree = etree.HTML(new_session_page.text)
118 auth_token = tree.xpath("//meta[@name='csrf-token']")[0].get("content")
119 login_data = dict()
120 login_data['user[email]'] = config.get('frab-user')
121 login_data['user[password]'] = config.get('frab-password')
122 login_data['user[remember_me]'] = 1
123 login_data['authenticity_token'] = auth_token
124
125 frab = config.get('frab-url')
126 conf = config.get('frab-conference')
127 track = config.get('track-name')
128
129 sess.post(frab + 'users/sign_in?conference_acronym=' + conf + '&locale=en', login_data, verify=False)
130 response = sess.get(frab + 'en/'+conf+'/events?track_name=' + track + '&format=json', verify=False, stream=True)
131
132 talks_json = json.loads(response.text)
133
134# with open('dump.txt', mode='wb') as localfile:
135# localfile.write(response.content)
136
137 imported = 0
138 for json_event in talks_json['events']:
139# print (json_event)
140 rawhtml = sess.get(frab + 'en/' + conf + '/events/'+ str(json_event['id']), verify=False, stream=True)
141 tree = etree.HTML(rawhtml.text)
142 submission_notes = tree.xpath('//b[text()="Submission Notes(user and admin):"]')[0].tail.strip()
143
144 dbevent = Event.query.get(json_event['id'])
145 speakers = { speaker['id']: speaker['full_public_name'] for speaker in json_event['speakers'] }
146 if dbevent:
147 dbevent.title = json_event['title']
148 dbevent.subtitle = json_event['subtitle']
149 dbevent.abstract = json_event['abstract']
150 dbevent.description = json_event['description']
151 dbevent.event_type = json_event['type']
152 dbevent.notes = submission_notes
153 if 'state' in json_event:
154 if json_event['state'] != 'new' or dbevent.state == 'gone':
155 dbevent.state = json_event['state']
156 dbevent.speakers = json.dumps(speakers)
157 else:
158 db.session.add( Event( frab_id = json_event['id'], title = json_event['title'], subtitle = json_event['subtitle'], abstract = json_event['abstract'], description = json_event['description'], speakers = json.dumps(speakers), state = json_event.get('state', 'new'), event_type = json_event['type'], notes = submission_notes) )
159 imported += 1
160 for goner in Event.query.filter( Event.frab_id.notin_([ ev['id'] for ev in talks_json['events'] ])).all():
161 goner.state = 'gone'
162 db.session.commit()
163 print ('Conference: ' + conf + ', track: ' + track + ', imported ' + str(len(talks_json['events'])) + ' events, ' + str(imported) + ' new.')
164
165
166if __name__ == "__main__":
167 db.create_all()
168 if args.frab_import:
169 fetch_talks(config_data)
170 else:
171 app.run(host=config_data.get('host'), port=int(config_data.get('port')))
172
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..fa92333
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,14 @@
1certifi==2017.7.27.1
2chardet==3.0.4
3click==6.7
4Flask==0.12.2
5Flask-SQLAlchemy==2.2
6idna==2.6
7itsdangerous==0.24
8Jinja2==2.9.6
9lxml==3.8.0
10MarkupSafe==1.0
11requests==2.18.4
12SQLAlchemy==1.1.14
13urllib3==1.22
14Werkzeug==0.12.2
diff --git a/static/rater.css b/static/rater.css
new file mode 100644
index 0000000..ae54a13
--- /dev/null
+++ b/static/rater.css
@@ -0,0 +1,292 @@
1body {
2 font-family: "HelveticaNeueLight", "HelveticaNeue-Light", "Helvetica Neue Light", "HelveticaNeue", "Helvetica Neue", 'TeXGyreHerosRegular', "Helvetica", "Tahoma", "Geneva", "Arial", sans-serif; font-weight:200; font-stretch:normal;
3}
4
5.username-wrapper {
6 position: absolute;
7 right: 5px;
8}
9
10.changed {
11 background-color: #00f000;
12 color: white;
13}
14
15.clearfix {
16 clear: both;
17}
18
19#event-list {
20 padding-left: 0em;
21 list-style-type: none;
22}
23
24.main-button {
25 vertical-align: top;
26 text-align: center;
27 height: 40px;
28 width: 40px;
29 padding: 0;
30}
31
32.mini-button {
33 margin-right: 0.2em;
34}
35
36button {
37 background-color: #759ae9;
38 background-image: linear-gradient(top, #759ae9 0%, #376fe0 50%, #1a5ad9 50%, #2463de 100%);
39 background-image: -webkit-linear-gradient(top, #759ae9 0%, #376fe0 50%, #1a5ad9 50%, #2463de 100%);
40 border-top: 1px solid #1f58cc;
41 border-right: 1px solid #1b4db3;
42 border-bottom: 1px solid #174299;
43 border-left: 1px solid #1b4db3;
44 border-radius: 4px;
45 box-shadow: inset 0 0 2px 0 rgba(57, 140, 255, 0.8);
46 color: #fff;
47 text-shadow: 0 -1px 1px #1a5ad9;
48}
49
50.event-list-item {
51 vertical-align: top;
52 background-color: #f6f6f6;
53 margin-bottom: 0.5em;
54 border: 1px solid silver;
55 border-radius: 10px;
56 padding: 0 0.5em 0.5em 0.5em;
57 box-sizing: border-box;
58}
59
60.event-list-item[event_state='gone'] {
61 background-image:
62 repeating-linear-gradient(
63 45deg,
64 #eee,
65 #eee 20px,
66 #ddd 20px,
67 #ddd 40px /* determines size */
68 );
69}
70
71.event-list-item[event_type=meeting] .event-title:before { content: 'MEETING '; color: red; font-size: smaller; }
72.event-list-item[event_type=workshop] .event-title:before { content: 'WORKSHOP '; color: red; font-size: smaller; }
73.event-list-item[event_type=concert] .event-title:before { content: 'CONCERT '; color: red; font-size: smaller; }
74.event-list-item[event_type=film] .event-title:before { content: 'FILM '; color: red; font-size: smaller; }
75.event-list-item[event_type=other] .event-title:before { content: 'OTHER '; color: red; font-size: smaller; }
76.event-list-item[event_type=podium] .event-title:before { content: 'PODIUM '; color: red; font-size: smaller; }
77.event-list-item[event_type=performance] .event-title:before { content: 'PERFORMANCE '; color: red; font-size: smaller; }
78.event-list-item[event_type=lightning_talk] .event-title:before { content: 'LIGHTNING '; color: red; font-size: smaller; }
79
80body.two-column .event-list-item {
81 display: inline-block;
82 width: 48%;
83 margin-right: 1%;
84}
85
86body.three-column .event-list-item {
87 display: inline-block;
88 width: 32%;
89 margin-right: 1%;
90}
91
92body.four-column .event-list-item {
93 display: inline-block;
94 width: 23.5%;
95 margin-right: 1%;
96}
97
98.event-rating {
99 display: inline-block;
100 width: 15em;
101 margin: 1em 1em 0 0;
102 padding: 0.2em;
103 background-color: #f0f0f0;
104 border-radius: 10px;
105 vertical-align:top;
106 font-size: smaller;
107}
108
109.event-title,
110.event-subtitle {
111 display: inline;
112 font-weight: bold;
113}
114
115.event-subtitle {
116 font-size: smaller;
117}
118
119.event-rating-comment {
120 min-height: 3em;
121 margin-top: 0.1em;
122 background-color: white;
123}
124
125#Filter, #Username {
126 font-size: x-large;
127 height: 40px;
128}
129
130.label {
131 float: left;
132 min-width: 8em !important;
133 font-style: italic;
134}
135
136.event-persons {
137 margin-top: 0.2em;
138 margin-bottom: 0.2em;
139}
140
141.event-speaker, .event-coordinator {
142 display: inline;
143}
144
145.event-coordinator {
146 margin-right: 0.5em;
147}
148
149.slider {
150 display: inline;
151}
152
153.event-notes,
154.event-description,
155.event-abstract {
156 height: 1.2em;
157 overflow: hidden;
158 cursor: zoom-in;
159 margin-bottom: 0.2em;
160 margin-right: 2em;
161 text-overflow: ellipsis;
162 white-space: nowrap;
163}
164
165.event-notes.full,
166.event-description.full,
167.event-abstract.full {
168 cursor: zoom-out;
169 background: white;
170 overflow: visible;
171 height: auto !important;
172 white-space: initial;
173}
174
175body.only-lectures .lectures-button,
176body.only-todo .todo-button,
177body.show-ratings .ratings-button,
178body.two-column .two-columns,
179body.three-column .three-columns,
180body.four-column .four-columns,
181.event-list-item.editing .edit-button,
182.event-list-item[event_state='accepted'] .accept-button,
183.event-list-item[event_state='rejected'] .reject-button,
184.event-list-item.i-am-coordinator .take-button
185{
186 background-image: -webkit-linear-gradient(top, #f37873 0%, #db504d 50%, #cb0500 50%, #a20601 100%);
187 background-image: linear-gradient(top, #f37873 0%, #db504d 50%, #cb0500 50%, #a20601 100%);
188}
189
190output {
191 margin-left: 1em;
192}
193
194#event-own-rating,
195.event-ratings {
196 display: none;
197 visibility: hidden;
198}
199
200body.show-ratings .event-ratings,
201.event-list-item.editing #event-own-rating {
202 display: block;
203 visibility: initial;
204}
205
206#event-own-rating textarea {
207 min-width: 10em;
208 max-width: 20em;
209 width: 80%;
210}
211
212.event-meter-bar {
213 box-sizing: initial;
214 display: inline;
215 float: left;
216 width: 65px;
217 vertical-align: top;
218 margin: 0;
219 padding: 0;
220}
221
222.has-own-rating .event-meter-bar {
223 height: 65px;
224}
225.has-own-rating .event-meter-bar:after {
226 color: #ff0000;
227 z-index: 1;
228 content: '✓';
229 font-size: 5em;
230 line-height: 1;
231 text-align: center;
232 position: relative;
233 left: 0; top: -65px;
234 width: 65px;
235 height: 65px;
236 opacity: 0.2;
237}
238
239body.safari meter {
240 -webkit-appearance: none;
241 -moz-appearance: none;
242 appearance: none;
243}
244
245meter {
246 vertical-align: top;
247 width: 60px;
248 margin-top: 0.3em;
249 border-radius: 3px;
250 height: 8px;
251
252 -moz-appearance: none;
253}
254
255meter::-moz-meter-bar {
256 background: #ddd;
257}
258
259:-moz-meter-optimum::-moz-meter-bar {
260 background: #afa;
261}
262
263meter.meter-4::-moz-meter-bar {
264 background: #fdd;
265}
266
267meter::-webkit-meter-bar {
268 background: #ddd;
269 box-shadow: 0 2px 3px rgba (0, 0, 0, 0.2) inset;
270 border-radius: 3px;
271}
272
273meter.meter-4::-webkit-meter-bar {
274 background: #fdd;
275}
276
277#status {
278 text-align: center;
279 font-size: xx-large;
280 font-weight: bold;
281 margin-top: 0.5em;
282 width: 100%;
283 clear: both;
284}
285
286body.only-lectures .event-list-item:not([event_type="lecture"]),
287body.only-todo .event-list-item.has-own-rating,
288.filtered,
289.hidden {
290 display: none !important;
291 visibility: hidden !important;
292}
diff --git a/static/rater.js b/static/rater.js
new file mode 100644
index 0000000..ec1bcfe
--- /dev/null
+++ b/static/rater.js
@@ -0,0 +1,337 @@
1function changed_name() {
2 var username = localStorage.getItem("c3-rating-user");
3 var newname = document.getElementById('Username').value;
4 document.getElementById('Username').classList.toggle('changed', username != newname);
5}
6
7function changed_filter() {
8 var filtertext = document.getElementById('Filter').value.toLowerCase();
9 if (filtertext == '') {
10 document.querySelectorAll('.event-list-item.filtered').forEach(function(ev) {
11 ev.classList.remove("filtered");
12 });
13 return;
14 }
15
16 if (!window.inner_texts) {
17 window.inner_texts = {};
18 document.querySelectorAll('.event-list-item').forEach(function(ev) {
19 inner_texts[ev.getAttribute('id')] = ev.innerText.toLowerCase();
20 });
21 }
22
23 Object.keys(window.inner_texts).forEach(function (eid) {
24 var elem = document.getElementById(eid);
25 elem.classList.toggle('filtered', window.inner_texts[eid].indexOf(filtertext) < 0);
26 });
27}
28
29function confirm_name() {
30 localStorage.setItem("c3-rating-user", document.getElementById('Username').value);
31 document.getElementById('Username').classList.remove("changed");
32 update_status();
33}
34
35function toggleHidden(name) {
36 document.getElementById(name).classList.toggle("hidden");
37}
38
39function toggleEdit(eid) {
40 var username = document.getElementById('Username').value;
41 if (!username) {
42 alert( "Please set your name before rating.");
43 return;
44 }
45
46 var ev = document.getElementById('event-'+eid);
47 if (ev.classList.contains('editing')) {
48 ev.classList.toggle('editing', false);
49 return;
50 }
51
52 var other_in_edit = document.getElementsByClassName('editing');
53 if (other_in_edit.length)
54 other_in_edit[0].classList.remove('editing');
55
56 ev.classList.toggle('editing', true);
57
58 var own_rating = document.getElementById('event-own-rating');
59 ev.appendChild(own_rating);
60
61 var myrating = document.querySelectorAll('div#event-rating-'+eid+'[submitter="'+username+'"]');
62 var mycomment = '';
63 if (myrating.length) {
64 mycomment = myrating[0].getElementsByClassName('event-rating-comment')[0].innerHTML;
65 myrating[0].querySelectorAll('meter').forEach(function(meter) {
66 var category = meter.getAttribute('category');
67 document.querySelectorAll('#event-own-rating .slider input[category='+category+']')[0].value = meter.value;
68 changeVal('event-'+category+'-output', meter.value);
69 });
70 } else {
71 document.querySelectorAll('#event-own-rating .slider input').forEach(function(sl) {
72 sl.value = 0;
73 var category = sl.getAttribute('category');
74 changeVal('event-'+category+'-output', '0');
75 });
76 }
77 document.getElementById('event-comment').value = mycomment;
78 own_rating.querySelectorAll('button.remove-rating')[0].classList.toggle('hidden', !myrating.length);
79}
80
81function changeVal(el, value) {
82 document.getElementById(el).innerHTML = value.toString() + " %";
83}
84
85function twocol() {
86 document.body.classList.toggle('two-column');
87 document.body.classList.remove('three-column');
88 document.body.classList.remove('four-column');
89}
90function threecol() {
91 document.body.classList.remove('two-column');
92 document.body.classList.toggle('three-column');
93 document.body.classList.remove('four-column');
94}
95function fourcol() {
96 document.body.classList.remove('two-column');
97 document.body.classList.remove('three-column');
98 document.body.classList.toggle('four-column');
99}
100
101function invert_sort() {
102 var evl = document.getElementById('event-list');
103 var nodes = Array.prototype.slice.call(evl.getElementsByClassName('event-list-item')).reverse().forEach(function(el) {
104 evl.appendChild(el);
105 });
106}
107
108function sort_by(order_function) {
109 var evl = document.getElementById('event-list');
110 Array.prototype.slice.call(evl.getElementsByClassName('event-list-item')).sort(order_function).forEach(function(el) {
111 evl.appendChild(el);
112 });
113}
114
115function myrating_count_sort(elem1, elem2) {
116 var username = document.getElementById('Username').value;
117 return elem2.querySelectorAll('.event-rating[submitter="'+username+'"]').length - elem1.querySelectorAll('.event-rating[submitter="'+username+'"]').length;
118}
119
120function random_sort(elem1, elem2) { return !!Math.floor(Math.random() * 2) ? -1 : 1; }
121function rating_count_sort(elem1, elem2) { return elem2.querySelectorAll('.event-rating').length - elem1.querySelectorAll('.event-rating').length; }
122function rating_1_sort(elem1, elem2) { return elem2.getElementsByClassName('meter-0')[0].getAttribute('value') - elem1.getElementsByClassName('meter-0')[0].getAttribute('value'); }
123function rating_2_sort(elem1, elem2) { return elem2.getElementsByClassName('meter-1')[0].getAttribute('value') - elem1.getElementsByClassName('meter-1')[0].getAttribute('value'); }
124function rating_3_sort(elem1, elem2) { return elem2.getElementsByClassName('meter-2')[0].getAttribute('value') - elem1.getElementsByClassName('meter-2')[0].getAttribute('value'); }
125function rating_4_sort(elem1, elem2) { return elem2.getElementsByClassName('meter-3')[0].getAttribute('value') - elem1.getElementsByClassName('meter-3')[0].getAttribute('value'); }
126function rating_5_sort(elem1, elem2) { return elem2.getElementsByClassName('meter-4')[0].getAttribute('value') - elem1.getElementsByClassName('meter-4')[0].getAttribute('value'); }
127function coordinator_sort(elem1, elem2) { return get_coordinator(elem1).localeCompare(get_coordinator(elem2)); }
128function state_sort(elem1, elem2) { return elem2.getAttribute('event_state').localeCompare(elem1.getAttribute('event_state')); }
129
130function get_coordinator(elem) {
131 var coordinator = elem.getElementsByClassName('event-coordinator');
132 if (coordinator.length)
133 return coordinator[0].getAttribute('coordinator');
134 return '';
135}
136
137function do_remove_rating() {
138 if (confirm('are you sure?') == false)
139 return;
140
141 var in_edit = document.getElementsByClassName('editing');
142 if (!in_edit.length)
143 return;
144 var eid = in_edit[0].getAttribute('id').replace(/^event-/, '');
145
146 var xhttp = new XMLHttpRequest();
147 xhttp.open("POST", "api/remove_rating/"+eid, true);
148 xhttp.setRequestHeader("Content-type", "application/json");
149
150 var username = document.getElementById('Username').value;
151
152 xhttp.onreadystatechange = function() {
153 if (xhttp.readyState == XMLHttpRequest.DONE && xhttp.status == 200) {
154 var myrating = document.querySelectorAll('div#event-rating-'+eid+'[submitter="'+username+'"]');
155 if(myrating.length)
156 myrating[0].parentNode.removeChild(myrating[0]);
157 toggleEdit(eid);
158 update_status();
159 }
160 }
161 xhttp.send( JSON.stringify( { 'author': username } ) );
162}
163
164function do_remove_event(eid) {
165 var ev = document.getElementById('event-'+eid);
166 var xhttp = new XMLHttpRequest();
167 xhttp.open("POST", "api/remove_event/"+eid, true);
168 xhttp.setRequestHeader("Content-type", "application/json");
169 xhttp.onreadystatechange = function() {
170 if (xhttp.readyState == XMLHttpRequest.DONE && xhttp.status == 200) {
171 ev.parentNode.removeChild(ev);
172 }
173 }
174 xhttp.send();
175}
176
177function do_rate() {
178 var in_edit = document.getElementsByClassName('editing');
179 if (!in_edit.length)
180 return;
181 var eid = in_edit[0].getAttribute('id').replace(/^event-/, '');
182
183 var username = document.getElementById('Username').value;
184 if (!username) {
185 alert( "Please set your name before rating.");
186 return;
187 }
188
189 var xhttp = new XMLHttpRequest();
190 xhttp.open("POST", "api/add_rating/"+eid, true);
191 xhttp.setRequestHeader("Content-type", "application/json");
192
193 ratings = {};
194 document.getElementById('event-own-rating').querySelectorAll('.category-slider input').forEach(function(ev) {
195 ratings[ev.getAttribute("category")] = ev.value;
196 });
197 var comment = document.getElementById('event-comment').value;
198
199 xhttp.onreadystatechange = function() {
200 if (xhttp.readyState == XMLHttpRequest.DONE && xhttp.status == 200) {
201 var myrating = document.querySelectorAll('div#event-rating-'+eid+'[submitter="'+username+'"]');
202 if (myrating.length) {
203 myrating = myrating[0];
204 } else {
205 myrating = document.getElementById('event-rating-new').cloneNode(true);
206 myrating.setAttribute('id', 'event-rating-'+eid);
207 myrating.classList.remove('hidden');
208 myrating.setAttribute('submitter', username);
209 myrating.getElementsByClassName('event-rating-submitter')[0].innerHTML = username + ':';
210 document.querySelectorAll('#event-'+eid+' .event-ratings')[0].append(myrating);
211 }
212 myrating.getElementsByClassName('event-rating-comment')[0].innerHTML = comment;
213 for (category in ratings) {
214 myrating.querySelectorAll('meter[category='+category+']')[0].value = ratings[category];
215 myrating.querySelectorAll('.event-rating-category-output[category='+category+']')[0].innerHTML = ' ' + categories[category] + ' ' + ratings[category] + ' %';
216 }
217
218 toggleEdit(eid);
219 update_status();
220 }
221 }
222
223 xhttp.send( JSON.stringify( {
224 'author': username,
225 'comment': comment,
226 'ratings': ratings
227 } ) );
228}
229
230function do_set_state(eid, state) {
231 if ( state == document.getElementById('event-'+eid).getAttribute('event_state'))
232 state = '';
233
234 var xhttp = new XMLHttpRequest();
235 xhttp.open("POST", "api/set_event_state/"+eid, true);
236 xhttp.setRequestHeader("Content-type", "application/json");
237 xhttp.onreadystatechange = function() {
238 if (xhttp.readyState == XMLHttpRequest.DONE && xhttp.status == 200) {
239 document.getElementById('event-'+eid).setAttribute('event_state', state);
240 update_status();
241 }
242 }
243 xhttp.send( JSON.stringify( { 'state': state } ) );
244}
245
246function do_take(eid) {
247 var username = document.getElementById('Username').value;
248 if (!username) {
249 alert( "Please set your name before taking an event.");
250 return;
251 }
252
253 var ev = document.getElementById('event-'+eid);
254 if (ev.classList.contains('i-am-coordinator'))
255 username = '';
256
257 var xhttp = new XMLHttpRequest();
258 xhttp.open("POST", "api/set_event_coordinator/"+eid, true);
259 xhttp.setRequestHeader("Content-type", "application/json");
260 xhttp.onreadystatechange = function() {
261 if (xhttp.readyState == XMLHttpRequest.DONE && xhttp.status == 200) {
262 var coor = ev.getElementsByClassName('event-coordinator');
263 if (coor.length) {
264 if (username) {
265 coor[0].innerHTML = '<em>coordinator: </em> '+username;
266 coor[0].setAttribute('coordinator', username);
267 } else
268 coor[0].parentNode.removeChild(coor[0]);
269 } else {
270 if (username) {
271 var pers = ev.getElementsByClassName('event-persons');
272 coor = document.createElement('div');
273 coor.classList.toggle('event-coordinator', true);
274 coor.setAttribute('coordinator', username);
275 coor.innerHTML = '<em>coordinator: </em> '+username;
276 pers[0].insertBefore(coor, pers[0].firstChild);
277 }
278 }
279 update_status();
280 }
281 }
282 xhttp.send( JSON.stringify( { 'coordinator': username } ) );
283}
284
285function update_status() {
286 var accepted_count = document.querySelectorAll('.event-list-item[event_state=accepted]').length;
287 var rejected_count = document.querySelectorAll('.event-list-item[event_state=rejected]').length;
288 var taken_count = document.querySelectorAll('.event-list-item .event-coordinator').length;
289 var total_count = document.getElementsByClassName('event-list-item').length;
290 var total_voted_count = document.querySelectorAll('.event-rating:first-child').length;
291 var username = document.getElementById('Username').value;
292 var own_voted_count = 0;
293 if (username)
294 own_voted_count = document.querySelectorAll('.event-rating[submitter="'+username+'"]').length;
295 document.getElementById('status').innerHTML = total_count + ' events. ' + accepted_count + ' accepted. ' + rejected_count + ' rejected. ' + (total_count - own_voted_count) + ' todo. ' + (total_count - total_voted_count) + ' unvoted. ' + (total_count - taken_count) + ' untaken.';
296
297 /* Do the math */
298 document.querySelectorAll('.event-list-item').forEach(function(ev) {
299 if (username) {
300 ev.classList.toggle('has-own-rating', ev.querySelectorAll('.event-rating[submitter="'+username+'"]').length > 0);
301 ev.classList.toggle('i-am-coordinator', ev.querySelectorAll('.event-coordinator[coordinator="'+username+'"]').length > 0);
302 }
303
304 var counts = {};
305 var meters = ev.querySelectorAll('.event-rating meter');
306
307 if (!meters.length) {
308 ev.querySelectorAll('.top-meter').forEach(function(meter) {
309 meter.setAttribute('value', 0);
310 });
311 return;
312 }
313
314 meters.forEach(function(rat) {
315 var tmp = counts[rat.getAttribute('category')] || 0;
316 counts[rat.getAttribute('category')] = tmp + parseInt(rat.getAttribute('value'));
317 });
318 var total = 0, i = 0, divisor = meters.length / Object.keys(counts).length;
319 for (category in counts) {
320 var dest_meter = ev.getElementsByClassName('meter-'+i)[0];
321 dest_meter.setAttribute('value', counts[category] / divisor);
322 dest_meter.setAttribute('title', category + ': ' + counts[category] / divisor + ' %' );
323 total += counts[category] / divisor;
324 i++;
325 }
326 ev.getElementsByClassName('meter-4')[0].setAttribute('value', total / Object.keys(counts).length);
327 });
328}
329
330document.addEventListener('DOMContentLoaded', function () {
331 var username = localStorage.getItem("c3-rating-user");
332 if (username)
333 document.getElementById('Username').value = username;
334 if (window.safari !== undefined)
335 document.body.classList.add('safari');
336 update_status();
337});
diff --git a/templates/index.html b/templates/index.html
new file mode 100644
index 0000000..13a8aec
--- /dev/null
+++ b/templates/index.html
@@ -0,0 +1,122 @@
1<html>
2<title>{{ config.get('frab-conference') }} {{ config.get('track') }} rating helper</title>
3<head>
4<script type="text/javascript">categories = {
5 {%- for category in cat -%}
6 'category{{ loop.index }}': '{{ category }}',
7 {%- endfor -%}
8 };
9</script>
10
11<script type=text/javascript src="static/rater.js"></script>
12<link rel="stylesheet" type="text/css" href="static/rater.css">
13</head>
14<body>
15
16<div style="float: left">
17<button class="main-button lectures-button" onclick="document.body.classList.toggle('only-lectures')" title="Show only lectures">lects</button>
18<button class="main-button todo-button" onclick="document.body.classList.toggle('only-todo')" title="Show only my unrated events">todo</button>
19<button class="main-button ratings-button" onclick="document.body.classList.toggle('show-ratings')" title="Show all ratings">rates</button>
20<button class="main-button two-columns" onclick="twocol()" title="Display events in two columns">2col</button>
21<button class="main-button three-columns" onclick="threecol()" title="Display events in three columns">3col</button>
22<button class="main-button four-columns" onclick="fourcol()" title="Display events in four columns">4col</button>
23</div>
24<div style="float: right; margin: 0">
25<button class="main-button" onclick="sort_by(random_sort)" title="Sort events randomly">rand</button>
26<button class="main-button" onclick="sort_by(rating_count_sort)" title="Sort events by amount of ratings">rate count</button>
27<button class="main-button" onclick="sort_by(myrating_count_sort)" title="Sort my rated events first">my rates</button>
28<button class="main-button" onclick="sort_by(coordinator_sort)" title="Sort events by coordinator">coord</button>
29<button class="main-button" onclick="sort_by(rating_1_sort)" title="Sort by meter 1">1</button>
30<button class="main-button" onclick="sort_by(rating_2_sort)" title="Sort by meter 2">2</button>
31<button class="main-button" onclick="sort_by(rating_3_sort)" title="Sort by meter 3">3</button>
32<button class="main-button" onclick="sort_by(rating_4_sort)" title="Sort by meter 4">4</button>
33<button class="main-button" onclick="sort_by(rating_5_sort)" title="Sort by summation meter">∑</button>
34<button class="main-button" onclick="sort_by(state_sort)" title="Sort by event state">state</button>
35<button class="main-button" onclick="invert_sort()" title="Invert sort order">⇅</button>
36</div>
37
38<div style="display: block; margin: 0.5em; height: 1px; clear: both;"></div>
39
40<div class="username-wrapper">
41<input type="text" id="Username" oninput="changed_name()" placeholder="who are you?"><button class="main-button" onclick="confirm_name()" title="Store your username locally">✓</button>
42</div>
43
44<input type="text" id="Filter" oninput="changed_filter()" placeholder="Filter">
45
46<div class="event-rating hidden" id="event-rating-new">
47 <div class="event-rating-submitter"></div>
48 {%- for category in cat -%}
49 <div><meter category="category{{loop.index}}" value="0" min="0" max="100"></meter><span class="event-rating-category-output" category="category{{loop.index}}"> {{ category + " 0 %" }}</span></div>
50 {%- endfor -%}
51 <div class="event-rating-comment"></div>
52</div>
53
54<div id="event-own-rating">
55 <hr/>
56 <div class="label">comment</div><textarea rows="8" id="event-comment"></textarea>
57 {%- for category in cat -%}
58 <div class="category-slider" id="event-category{{loop.index}}-slider">
59 <div class="label">{{category}}:</div>
60 <div class="slider"><input category="category{{loop.index}}" type="range" min="0" max="100" step="5" oninput="changeVal('event-category{{loop.index}}-output', this.value)"></div>
61 <output id="event-category{{loop.index}}-output">0 %</output>
62 </div>
63 {%- endfor -%}
64 <button class="remove-rating hidden" onclick="do_remove_rating()">remove</button>
65 <button onclick="do_rate()">rate</button>
66</div>
67
68<p id='status'>Please wait …</p>
69
70<ul id="event-list">
71{% for ev in events -%}
72 <li class="event-list-item" event_state="{{ev.state}}" event_type="{{ev.event_type}}" id="event-{{ev.frab_id}}">
73 <div class="event-meter-bar">
74 {%- for m in range(1+cat|length) -%}
75 <meter class="top-meter meter-{{m}}" id="event-{{ev.frab_id}}-meter-{{m}}" value="0" min="0" max="100"></meter>
76 {%- endfor -%}
77 </div>
78 {%- if not ev.state == 'gone' -%}
79 <button onclick="do_set_state('{{ev.frab_id}}', 'accepted')" title="accept this event" class="mini-button accept-button">acc</button><button onclick="do_set_state('{{ev.frab_id}}', 'rejected')" title="reject this event" class="mini-button reject-button">rej</button><button onclick="toggleEdit('{{ev.frab_id}}')" title="edit this event" class="edit-button mini-button">edit</button><button onclick="do_take('{{ev.frab_id}}')" title="make me coordinator for this event" class="mini-button take-button">take</button>
80 {%- else -%}
81 <button onclick="do_remove_event('{{ev.frab_id}}')" title="remove this event" class="mini-button remove-button">del</button>
82 {%- endif -%}
83 <div class="event-title"><a href="{{ config.get('frab-url') }}en/{{ config.get('frab-conference')}}/events/{{ ev.frab_id }}/">{{ ev.title }}</a></div>
84 {%- if ev.subtitle -%}
85 <div class="event-subtitle"> {{ ev.subtitle }}</div>
86 {%- endif -%}
87 <div class="event-persons">
88 {%- if ev.coordinator -%}
89 <div class="event-coordinator" coordinator="{{ev.coordinator}}"><em>coordinator: </em> {{ev.coordinator}}</div>
90 {%- endif -%}
91 <div class="event-speaker"><em>speakers: </em>
92 {%- for speaker_id, speaker_name in json.loads(ev.speakers or '{}').items() -%}
93 <a href="{{ config.get('frab-url') }}en/{{ config.get('frab-conference') }}/people/{{speaker_id}}">{{speaker_name}}</a>&nbsp;
94 {%- endfor -%}
95 </div>
96 </div>
97 <div class="event-abstract" onclick="this.classList.toggle('full')"><em>abstract:</em> {{ ev.abstract }}</div>
98 {%- if ev.description -%}
99 <div class="event-description" onclick="this.classList.toggle('full')"><em>description:</em> {{ ev.description }}</div>
100 {%- endif -%}
101 {%- if ev.notes -%}
102 <div class="event-notes" onclick="this.classList.toggle('full')"><em>notes:</em> {{ ev.notes }}</div>
103 {%- endif -%}
104 <div class="event-ratings">
105 {%- for rating in ev.ratings -%}
106 <div class="event-rating" id="event-rating-{{ev.frab_id}}" submitter="{{rating.submitter}}">
107 <div class="event-rating-submitter">{{rating.submitter}}:</div>
108 {%- for category, value in json.loads(rating.rating_dict or '{}').items()|sort -%}
109 <div><meter category="{{category}}" value="{{value}}" min="0" max="100"></meter><span class="event-rating-category-output" category="{{category}}"> {{ cat[loop.index-1] + " " + value|string + " %" }}</span></div>
110 {%- endfor -%}
111 <div class="event-rating-comment">{{rating.comment}}</div>
112 </div>
113 {%- endfor -%}
114 </div>
115 </li>
116{%- else %}
117 <em>No events imported yet</em>
118{%- endfor %}
119<ul>
120
121<body>
122</html>