diff options
| -rw-r--r-- | Makefile | 14 | ||||
| -rw-r--r-- | config-example.json | 22 | ||||
| -rwxr-xr-x | rater.py | 172 | ||||
| -rw-r--r-- | requirements.txt | 14 | ||||
| -rw-r--r-- | static/rater.css | 292 | ||||
| -rw-r--r-- | static/rater.js | 337 | ||||
| -rw-r--r-- | templates/index.html | 122 |
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 @@ | |||
| 1 | all: install | ||
| 2 | |||
| 3 | import: | ||
| 4 | venv/bin/python3 ./rater.py -i | ||
| 5 | |||
| 6 | run: | ||
| 7 | PYTHONIOENCODING=utf-8 venv/bin/python3 ./rater.py | ||
| 8 | |||
| 9 | venv: | ||
| 10 | python3 -m venv ./venv | ||
| 11 | |||
| 12 | install: 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 | |||
| 3 | from flask import Flask, render_template, jsonify, request | ||
| 4 | from flask_sqlalchemy import SQLAlchemy | ||
| 5 | from lxml import etree | ||
| 6 | from argparse import ArgumentParser | ||
| 7 | import requests | ||
| 8 | import 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 | |||
| 14 | parser = ArgumentParser(description="C3 rating helper") | ||
| 15 | parser.add_argument("-i", action="store_true", dest="frab_import", default=False, help="import events from frab") | ||
| 16 | parser.add_argument("-c", "--config", help="Config file location", default="./config.json") | ||
| 17 | args = parser.parse_args() | ||
| 18 | |||
| 19 | with open(args.config, mode="r", encoding="utf-8") as json_file: | ||
| 20 | config_data = json.load(json_file) | ||
| 21 | |||
| 22 | app = Flask(__name__) | ||
| 23 | app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///'+config_data.get('frab-conference')+'-'+config_data.get('track')+'.db' | ||
| 24 | app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False | ||
| 25 | app.config['SECRET_KEY'] = 'Silence is golden. Gerd Eist.' | ||
| 26 | app.jinja_env.trim_blocks = True | ||
| 27 | app.jinja_env.lstrip_blocks = True | ||
| 28 | |||
| 29 | db = SQLAlchemy(app) | ||
| 30 | |||
| 31 | class 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 | |||
| 44 | class 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("/") | ||
| 54 | def 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') | ||
| 59 | def get_ratings(): | ||
| 60 | return jsonify(EventRating.query.all()) | ||
| 61 | |||
| 62 | @app.route('/api/set_event_state/<eventid>', methods=['POST']) | ||
| 63 | def 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']) | ||
| 71 | def 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']) | ||
| 79 | def 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']) | ||
| 87 | def 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']) | ||
| 96 | def 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 | |||
| 114 | def 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 | |||
| 166 | if __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 @@ | |||
| 1 | certifi==2017.7.27.1 | ||
| 2 | chardet==3.0.4 | ||
| 3 | click==6.7 | ||
| 4 | Flask==0.12.2 | ||
| 5 | Flask-SQLAlchemy==2.2 | ||
| 6 | idna==2.6 | ||
| 7 | itsdangerous==0.24 | ||
| 8 | Jinja2==2.9.6 | ||
| 9 | lxml==3.8.0 | ||
| 10 | MarkupSafe==1.0 | ||
| 11 | requests==2.18.4 | ||
| 12 | SQLAlchemy==1.1.14 | ||
| 13 | urllib3==1.22 | ||
| 14 | Werkzeug==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 @@ | |||
| 1 | body { | ||
| 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 | |||
| 36 | button { | ||
| 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 | |||
| 80 | body.two-column .event-list-item { | ||
| 81 | display: inline-block; | ||
| 82 | width: 48%; | ||
| 83 | margin-right: 1%; | ||
| 84 | } | ||
| 85 | |||
| 86 | body.three-column .event-list-item { | ||
| 87 | display: inline-block; | ||
| 88 | width: 32%; | ||
| 89 | margin-right: 1%; | ||
| 90 | } | ||
| 91 | |||
| 92 | body.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 | |||
| 175 | body.only-lectures .lectures-button, | ||
| 176 | body.only-todo .todo-button, | ||
| 177 | body.show-ratings .ratings-button, | ||
| 178 | body.two-column .two-columns, | ||
| 179 | body.three-column .three-columns, | ||
| 180 | body.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 | |||
| 190 | output { | ||
| 191 | margin-left: 1em; | ||
| 192 | } | ||
| 193 | |||
| 194 | #event-own-rating, | ||
| 195 | .event-ratings { | ||
| 196 | display: none; | ||
| 197 | visibility: hidden; | ||
| 198 | } | ||
| 199 | |||
| 200 | body.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 | |||
| 239 | body.safari meter { | ||
| 240 | -webkit-appearance: none; | ||
| 241 | -moz-appearance: none; | ||
| 242 | appearance: none; | ||
| 243 | } | ||
| 244 | |||
| 245 | meter { | ||
| 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 | |||
| 255 | meter::-moz-meter-bar { | ||
| 256 | background: #ddd; | ||
| 257 | } | ||
| 258 | |||
| 259 | :-moz-meter-optimum::-moz-meter-bar { | ||
| 260 | background: #afa; | ||
| 261 | } | ||
| 262 | |||
| 263 | meter.meter-4::-moz-meter-bar { | ||
| 264 | background: #fdd; | ||
| 265 | } | ||
| 266 | |||
| 267 | meter::-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 | |||
| 273 | meter.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 | |||
| 286 | body.only-lectures .event-list-item:not([event_type="lecture"]), | ||
| 287 | body.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 @@ | |||
| 1 | function 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 | |||
| 7 | function 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 | |||
| 29 | function confirm_name() { | ||
| 30 | localStorage.setItem("c3-rating-user", document.getElementById('Username').value); | ||
| 31 | document.getElementById('Username').classList.remove("changed"); | ||
| 32 | update_status(); | ||
| 33 | } | ||
| 34 | |||
| 35 | function toggleHidden(name) { | ||
| 36 | document.getElementById(name).classList.toggle("hidden"); | ||
| 37 | } | ||
| 38 | |||
| 39 | function 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 | |||
| 81 | function changeVal(el, value) { | ||
| 82 | document.getElementById(el).innerHTML = value.toString() + " %"; | ||
| 83 | } | ||
| 84 | |||
| 85 | function twocol() { | ||
| 86 | document.body.classList.toggle('two-column'); | ||
| 87 | document.body.classList.remove('three-column'); | ||
| 88 | document.body.classList.remove('four-column'); | ||
| 89 | } | ||
| 90 | function threecol() { | ||
| 91 | document.body.classList.remove('two-column'); | ||
| 92 | document.body.classList.toggle('three-column'); | ||
| 93 | document.body.classList.remove('four-column'); | ||
| 94 | } | ||
| 95 | function fourcol() { | ||
| 96 | document.body.classList.remove('two-column'); | ||
| 97 | document.body.classList.remove('three-column'); | ||
| 98 | document.body.classList.toggle('four-column'); | ||
| 99 | } | ||
| 100 | |||
| 101 | function 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 | |||
| 108 | function 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 | |||
| 115 | function 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 | |||
| 120 | function random_sort(elem1, elem2) { return !!Math.floor(Math.random() * 2) ? -1 : 1; } | ||
| 121 | function rating_count_sort(elem1, elem2) { return elem2.querySelectorAll('.event-rating').length - elem1.querySelectorAll('.event-rating').length; } | ||
| 122 | function rating_1_sort(elem1, elem2) { return elem2.getElementsByClassName('meter-0')[0].getAttribute('value') - elem1.getElementsByClassName('meter-0')[0].getAttribute('value'); } | ||
| 123 | function rating_2_sort(elem1, elem2) { return elem2.getElementsByClassName('meter-1')[0].getAttribute('value') - elem1.getElementsByClassName('meter-1')[0].getAttribute('value'); } | ||
| 124 | function rating_3_sort(elem1, elem2) { return elem2.getElementsByClassName('meter-2')[0].getAttribute('value') - elem1.getElementsByClassName('meter-2')[0].getAttribute('value'); } | ||
| 125 | function rating_4_sort(elem1, elem2) { return elem2.getElementsByClassName('meter-3')[0].getAttribute('value') - elem1.getElementsByClassName('meter-3')[0].getAttribute('value'); } | ||
| 126 | function rating_5_sort(elem1, elem2) { return elem2.getElementsByClassName('meter-4')[0].getAttribute('value') - elem1.getElementsByClassName('meter-4')[0].getAttribute('value'); } | ||
| 127 | function coordinator_sort(elem1, elem2) { return get_coordinator(elem1).localeCompare(get_coordinator(elem2)); } | ||
| 128 | function state_sort(elem1, elem2) { return elem2.getAttribute('event_state').localeCompare(elem1.getAttribute('event_state')); } | ||
| 129 | |||
| 130 | function get_coordinator(elem) { | ||
| 131 | var coordinator = elem.getElementsByClassName('event-coordinator'); | ||
| 132 | if (coordinator.length) | ||
| 133 | return coordinator[0].getAttribute('coordinator'); | ||
| 134 | return ''; | ||
| 135 | } | ||
| 136 | |||
| 137 | function 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 | |||
| 164 | function 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 | |||
| 177 | function 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 | |||
| 230 | function 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 | |||
| 246 | function 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 | |||
| 285 | function 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 | |||
| 330 | document.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> | ||
| 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> | ||
