1
|
|
#!/usr/bin/env python3
|
2
|
|
|
3
|
|
# Contest Management System - http://cms-dev.github.io/
|
4
|
|
# Copyright © 2011-2017 Luca Wehrstedt <luca.wehrstedt@gmail.com>
|
5
|
|
#
|
6
|
|
# This program is free software: you can redistribute it and/or modify
|
7
|
|
# it under the terms of the GNU Affero General Public License as
|
8
|
|
# published by the Free Software Foundation, either version 3 of the
|
9
|
|
# License, or (at your option) any later version.
|
10
|
|
#
|
11
|
|
# This program is distributed in the hope that it will be useful,
|
12
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
13
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
14
|
|
# GNU Affero General Public License for more details.
|
15
|
|
#
|
16
|
|
# You should have received a copy of the GNU Affero General Public License
|
17
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
18
|
|
|
19
|
1
|
import argparse
|
20
|
1
|
import functools
|
21
|
1
|
import json
|
22
|
1
|
import logging
|
23
|
1
|
import os
|
24
|
1
|
import pprint
|
25
|
1
|
import re
|
26
|
1
|
import shutil
|
27
|
1
|
import time
|
28
|
1
|
from datetime import datetime
|
29
|
|
|
30
|
1
|
import gevent
|
31
|
1
|
from gevent.pywsgi import WSGIServer
|
32
|
1
|
from werkzeug.exceptions import HTTPException, BadRequest, Unauthorized, \
|
33
|
|
Forbidden, NotFound, NotAcceptable, UnsupportedMediaType
|
34
|
1
|
from werkzeug.routing import Map, Rule
|
35
|
0
|
from werkzeug.wrappers import Request, Response
|
36
|
0
|
from werkzeug.wsgi import responder, wrap_file, SharedDataMiddleware, \
|
37
|
|
DispatcherMiddleware
|
38
|
|
|
39
|
|
# Needed for initialization. Do not remove.
|
40
|
0
|
import cmsranking.Logger # noqa
|
41
|
0
|
from cmscommon.eventsource import EventSource
|
42
|
0
|
from cmsranking.Config import Config
|
43
|
0
|
from cmsranking.Contest import Contest
|
44
|
0
|
from cmsranking.Entity import InvalidData
|
45
|
0
|
from cmsranking.Scoring import ScoringStore
|
46
|
0
|
from cmsranking.Store import Store
|
47
|
0
|
from cmsranking.Subchange import Subchange
|
48
|
0
|
from cmsranking.Submission import Submission
|
49
|
0
|
from cmsranking.Task import Task
|
50
|
0
|
from cmsranking.Team import Team
|
51
|
0
|
from cmsranking.User import User
|
52
|
|
|
53
|
|
|
54
|
0
|
logger = logging.getLogger(__name__)
|
55
|
|
|
56
|
|
|
57
|
1
|
class CustomUnauthorized(Unauthorized):
|
58
|
|
|
59
|
1
|
def __init__(self, realm_name):
|
60
|
0
|
super().__init__()
|
61
|
0
|
self.realm_name = realm_name
|
62
|
|
|
63
|
1
|
def get_response(self, environ=None):
|
64
|
0
|
response = super().get_response(environ)
|
65
|
|
# XXX With werkzeug-0.9 a full-featured Response object is
|
66
|
|
# returned: there is no need for this.
|
67
|
0
|
response = Response.force_type(response)
|
68
|
0
|
response.www_authenticate.set_basic(self.realm_name)
|
69
|
0
|
return response
|
70
|
|
|
71
|
|
|
72
|
1
|
class StoreHandler:
|
73
|
|
|
74
|
1
|
def __init__(self, store, username, password, realm_name):
|
75
|
1
|
self.store = store
|
76
|
1
|
self.username = username
|
77
|
1
|
self.password = password
|
78
|
1
|
self.realm_name = realm_name
|
79
|
|
|
80
|
1
|
self.router = Map([
|
81
|
|
Rule("/<key>", methods=["GET"], endpoint="get"),
|
82
|
|
Rule("/", methods=["GET"], endpoint="get_list"),
|
83
|
|
Rule("/<key>", methods=["PUT"], endpoint="put"),
|
84
|
|
Rule("/", methods=["PUT"], endpoint="put_list"),
|
85
|
|
Rule("/<key>", methods=["DELETE"], endpoint="delete"),
|
86
|
|
Rule("/", methods=["DELETE"], endpoint="delete_list"),
|
87
|
|
], encoding_errors="strict")
|
88
|
|
|
89
|
1
|
def __call__(self, environ, start_response):
|
90
|
1
|
return self.wsgi_app(environ, start_response)
|
91
|
|
|
92
|
1
|
@responder
|
93
|
|
def wsgi_app(self, environ, start_response):
|
94
|
1
|
route = self.router.bind_to_environ(environ)
|
95
|
1
|
try:
|
96
|
1
|
endpoint, args = route.match()
|
97
|
0
|
except HTTPException as exc:
|
98
|
0
|
return exc
|
99
|
|
|
100
|
1
|
request = Request(environ)
|
101
|
1
|
request.encoding_errors = "strict"
|
102
|
|
|
103
|
1
|
response = Response()
|
104
|
|
|
105
|
1
|
try:
|
106
|
1
|
if endpoint == "get":
|
107
|
0
|
self.get(request, response, args["key"])
|
108
|
1
|
elif endpoint == "get_list":
|
109
|
0
|
self.get_list(request, response)
|
110
|
1
|
elif endpoint == "put":
|
111
|
0
|
self.put(request, response, args["key"])
|
112
|
1
|
elif endpoint == "put_list":
|
113
|
1
|
self.put_list(request, response)
|
114
|
0
|
elif endpoint == "delete":
|
115
|
0
|
self.delete(request, response, args["key"])
|
116
|
0
|
elif endpoint == "delete_list":
|
117
|
0
|
self.delete_list(request, response)
|
118
|
|
else:
|
119
|
0
|
raise RuntimeError()
|
120
|
0
|
except HTTPException as exc:
|
121
|
0
|
return exc
|
122
|
|
|
123
|
1
|
return response
|
124
|
|
|
125
|
1
|
def authorized(self, request):
|
126
|
1
|
return request.authorization is not None and \
|
127
|
|
request.authorization.type == "basic" and \
|
128
|
|
request.authorization.username == self.username and \
|
129
|
|
request.authorization.password == self.password
|
130
|
|
|
131
|
1
|
def get(self, request, response, key):
|
132
|
|
# Limit charset of keys.
|
133
|
0
|
if re.match("^[A-Za-z0-9_]+$", key) is None:
|
134
|
0
|
return NotFound()
|
135
|
0
|
if key not in self.store:
|
136
|
0
|
raise NotFound()
|
137
|
|
|
138
|
0
|
response.status_code = 200
|
139
|
0
|
response.headers['Timestamp'] = "%0.6f" % time.time()
|
140
|
0
|
response.mimetype = "application/json"
|
141
|
0
|
response.data = json.dumps(self.store.retrieve(key))
|
142
|
|
|
143
|
1
|
def get_list(self, request, response):
|
144
|
0
|
response.status_code = 200
|
145
|
0
|
response.headers['Timestamp'] = "%0.6f" % time.time()
|
146
|
0
|
response.mimetype = "application/json"
|
147
|
0
|
response.data = json.dumps(self.store.retrieve_list())
|
148
|
|
|
149
|
1
|
def put(self, request, response, key):
|
150
|
|
# Limit charset of keys.
|
151
|
0
|
if re.match("^[A-Za-z0-9_]+$", key) is None:
|
152
|
0
|
return Forbidden()
|
153
|
0
|
if not self.authorized(request):
|
154
|
0
|
logger.warning("Unauthorized request.",
|
155
|
|
extra={'location': request.url,
|
156
|
|
'details': repr(request.authorization)})
|
157
|
0
|
raise CustomUnauthorized(self.realm_name)
|
158
|
0
|
if request.mimetype != "application/json":
|
159
|
0
|
logger.warning("Unsupported MIME type.",
|
160
|
|
extra={'location': request.url,
|
161
|
|
'details': request.mimetype})
|
162
|
0
|
raise UnsupportedMediaType()
|
163
|
|
|
164
|
0
|
try:
|
165
|
0
|
data = json.load(request.stream)
|
166
|
0
|
except (TypeError, ValueError):
|
167
|
0
|
logger.warning("Wrong JSON.",
|
168
|
|
extra={'location': request.url})
|
169
|
0
|
raise BadRequest()
|
170
|
|
|
171
|
0
|
try:
|
172
|
0
|
if key not in self.store:
|
173
|
0
|
self.store.create(key, data)
|
174
|
|
else:
|
175
|
0
|
self.store.update(key, data)
|
176
|
0
|
except InvalidData as err:
|
177
|
0
|
logger.warning("Invalid data: %s" % str(err), exc_info=False,
|
178
|
|
extra={'location': request.url,
|
179
|
|
'details': pprint.pformat(data)})
|
180
|
0
|
raise BadRequest()
|
181
|
|
|
182
|
0
|
response.status_code = 204
|
183
|
|
|
184
|
1
|
def put_list(self, request, response):
|
185
|
1
|
if not self.authorized(request):
|
186
|
0
|
logger.info("Unauthorized request.",
|
187
|
|
extra={'location': request.url,
|
188
|
|
'details': repr(request.authorization)})
|
189
|
0
|
raise CustomUnauthorized(self.realm_name)
|
190
|
1
|
if request.mimetype != "application/json":
|
191
|
0
|
logger.warning("Unsupported MIME type.",
|
192
|
|
extra={'location': request.url,
|
193
|
|
'details': request.mimetype})
|
194
|
0
|
raise UnsupportedMediaType()
|
195
|
|
|
196
|
1
|
try:
|
197
|
1
|
data = json.load(request.stream)
|
198
|
0
|
except (TypeError, ValueError):
|
199
|
0
|
logger.warning("Wrong JSON.",
|
200
|
|
extra={'location': request.url})
|
201
|
0
|
raise BadRequest()
|
202
|
|
|
203
|
1
|
try:
|
204
|
1
|
self.store.merge_list(data)
|
205
|
0
|
except InvalidData as err:
|
206
|
0
|
logger.warning("Invalid data: %s" % str(err), exc_info=False,
|
207
|
|
extra={'location': request.url,
|
208
|
|
'details': pprint.pformat(data)})
|
209
|
0
|
raise BadRequest()
|
210
|
|
|
211
|
1
|
response.status_code = 204
|
212
|
|
|
213
|
1
|
def delete(self, request, response, key):
|
214
|
|
# Limit charset of keys.
|
215
|
0
|
if re.match("^[A-Za-z0-9_]+$", key) is None:
|
216
|
0
|
return NotFound()
|
217
|
0
|
if key not in self.store:
|
218
|
0
|
raise NotFound()
|
219
|
0
|
if not self.authorized(request):
|
220
|
0
|
logger.info("Unauthorized request.",
|
221
|
|
extra={'location': request.url,
|
222
|
|
'details': repr(request.authorization)})
|
223
|
0
|
raise CustomUnauthorized(self.realm_name)
|
224
|
|
|
225
|
0
|
self.store.delete(key)
|
226
|
|
|
227
|
0
|
response.status_code = 204
|
228
|
|
|
229
|
1
|
def delete_list(self, request, response):
|
230
|
0
|
if not self.authorized(request):
|
231
|
0
|
logger.info("Unauthorized request.",
|
232
|
|
extra={'location': request.url,
|
233
|
|
'details': repr(request.authorization)})
|
234
|
0
|
raise CustomUnauthorized(self.realm_name)
|
235
|
|
|
236
|
0
|
self.store.delete_list()
|
237
|
|
|
238
|
0
|
response.status_code = 204
|
239
|
|
|
240
|
|
|
241
|
1
|
class DataWatcher(EventSource):
|
242
|
|
"""Receive the messages from the entities store and redirect them."""
|
243
|
|
|
244
|
1
|
def __init__(self, stores, buffer_size):
|
245
|
1
|
self._CACHE_SIZE = buffer_size
|
246
|
1
|
EventSource.__init__(self)
|
247
|
|
|
248
|
1
|
stores["contest"].add_create_callback(
|
249
|
|
functools.partial(self.callback, "contest", "create"))
|
250
|
1
|
stores["contest"].add_update_callback(
|
251
|
|
functools.partial(self.callback, "contest", "update"))
|
252
|
1
|
stores["contest"].add_delete_callback(
|
253
|
|
functools.partial(self.callback, "contest", "delete"))
|
254
|
|
|
255
|
1
|
stores["task"].add_create_callback(
|
256
|
|
functools.partial(self.callback, "task", "create"))
|
257
|
1
|
stores["task"].add_update_callback(
|
258
|
|
functools.partial(self.callback, "task", "update"))
|
259
|
1
|
stores["task"].add_delete_callback(
|
260
|
|
functools.partial(self.callback, "task", "delete"))
|
261
|
|
|
262
|
1
|
stores["team"].add_create_callback(
|
263
|
|
functools.partial(self.callback, "team", "create"))
|
264
|
1
|
stores["team"].add_update_callback(
|
265
|
|
functools.partial(self.callback, "team", "update"))
|
266
|
1
|
stores["team"].add_delete_callback(
|
267
|
|
functools.partial(self.callback, "team", "delete"))
|
268
|
|
|
269
|
1
|
stores["user"].add_create_callback(
|
270
|
|
functools.partial(self.callback, "user", "create"))
|
271
|
1
|
stores["user"].add_update_callback(
|
272
|
|
functools.partial(self.callback, "user", "update"))
|
273
|
1
|
stores["user"].add_delete_callback(
|
274
|
|
functools.partial(self.callback, "user", "delete"))
|
275
|
|
|
276
|
1
|
stores["scoring"].add_score_callback(self.score_callback)
|
277
|
|
|
278
|
1
|
def callback(self, entity, event, key, *args):
|
279
|
1
|
self.send(entity, "%s %s" % (event, key))
|
280
|
|
|
281
|
1
|
def score_callback(self, user, task, score):
|
282
|
|
# FIXME Use score_precision.
|
283
|
1
|
self.send("score", "%s %s %0.2f" % (user, task, score))
|
284
|
|
|
285
|
|
|
286
|
1
|
class SubListHandler:
|
287
|
|
|
288
|
1
|
def __init__(self, stores):
|
289
|
1
|
self.task_store = stores["task"]
|
290
|
1
|
self.scoring_store = stores["scoring"]
|
291
|
|
|
292
|
1
|
self.router = Map([
|
293
|
|
Rule("/<user_id>", methods=["GET"], endpoint="sublist"),
|
294
|
|
], encoding_errors="strict")
|
295
|
|
|
296
|
1
|
def __call__(self, environ, start_response):
|
297
|
0
|
return self.wsgi_app(environ, start_response)
|
298
|
|
|
299
|
1
|
def wsgi_app(self, environ, start_response):
|
300
|
0
|
route = self.router.bind_to_environ(environ)
|
301
|
0
|
try:
|
302
|
0
|
endpoint, args = route.match()
|
303
|
0
|
except HTTPException as exc:
|
304
|
0
|
return exc(environ, start_response)
|
305
|
|
|
306
|
0
|
assert endpoint == "sublist"
|
307
|
|
|
308
|
0
|
request = Request(environ)
|
309
|
0
|
request.encoding_errors = "strict"
|
310
|
|
|
311
|
0
|
if request.accept_mimetypes.quality("application/json") <= 0:
|
312
|
0
|
raise NotAcceptable()
|
313
|
|
|
314
|
0
|
result = list()
|
315
|
0
|
for task_id in self.task_store._store.keys():
|
316
|
0
|
result.extend(
|
317
|
|
self.scoring_store.get_submissions(
|
318
|
|
args["user_id"], task_id
|
319
|
|
).values()
|
320
|
|
)
|
321
|
0
|
result.sort(key=lambda x: (x.task, x.time))
|
322
|
0
|
result = list(a.__dict__ for a in result)
|
323
|
|
|
324
|
0
|
response = Response()
|
325
|
0
|
response.status_code = 200
|
326
|
0
|
response.mimetype = "application/json"
|
327
|
0
|
response.data = json.dumps(result)
|
328
|
|
|
329
|
0
|
return response(environ, start_response)
|
330
|
|
|
331
|
|
|
332
|
1
|
class HistoryHandler:
|
333
|
|
|
334
|
1
|
def __init__(self, stores):
|
335
|
1
|
self.scoring_store = stores["scoring"]
|
336
|
|
|
337
|
1
|
def __call__(self, environ, start_response):
|
338
|
0
|
return self.wsgi_app(environ, start_response)
|
339
|
|
|
340
|
1
|
def wsgi_app(self, environ, start_response):
|
341
|
0
|
request = Request(environ)
|
342
|
0
|
request.encoding_errors = "strict"
|
343
|
|
|
344
|
0
|
if request.accept_mimetypes.quality("application/json") <= 0:
|
345
|
0
|
raise NotAcceptable()
|
346
|
|
|
347
|
0
|
result = list(self.scoring_store.get_global_history())
|
348
|
|
|
349
|
0
|
response = Response()
|
350
|
0
|
response.status_code = 200
|
351
|
0
|
response.mimetype = "application/json"
|
352
|
0
|
response.data = json.dumps(result)
|
353
|
|
|
354
|
0
|
return response(environ, start_response)
|
355
|
|
|
356
|
|
|
357
|
1
|
class ScoreHandler:
|
358
|
|
|
359
|
1
|
def __init__(self, stores):
|
360
|
1
|
self.scoring_store = stores["scoring"]
|
361
|
|
|
362
|
1
|
def __call__(self, environ, start_response):
|
363
|
0
|
return self.wsgi_app(environ, start_response)
|
364
|
|
|
365
|
1
|
def wsgi_app(self, environ, start_response):
|
366
|
0
|
request = Request(environ)
|
367
|
0
|
request.encoding_errors = "strict"
|
368
|
|
|
369
|
0
|
if request.accept_mimetypes.quality("application/json") <= 0:
|
370
|
0
|
raise NotAcceptable()
|
371
|
|
|
372
|
0
|
result = dict()
|
373
|
0
|
for u_id, tasks in self.scoring_store._scores.items():
|
374
|
0
|
for t_id, score in tasks.items():
|
375
|
0
|
if score.get_score() > 0.0:
|
376
|
0
|
result.setdefault(u_id, dict())[t_id] = score.get_score()
|
377
|
|
|
378
|
0
|
response = Response()
|
379
|
0
|
response.status_code = 200
|
380
|
0
|
response.headers['Timestamp'] = "%0.6f" % time.time()
|
381
|
0
|
response.mimetype = "application/json"
|
382
|
0
|
response.data = json.dumps(result)
|
383
|
|
|
384
|
0
|
return response(environ, start_response)
|
385
|
|
|
386
|
|
|
387
|
1
|
class ImageHandler:
|
388
|
1
|
EXT_TO_MIME = {
|
389
|
|
'png': 'image/png',
|
390
|
|
'jpg': 'image/jpeg',
|
391
|
|
'gif': 'image/gif',
|
392
|
|
'bmp': 'image/bmp'
|
393
|
|
}
|
394
|
|
|
395
|
1
|
MIME_TO_EXT = dict((v, k) for k, v in EXT_TO_MIME.items())
|
396
|
|
|
397
|
1
|
def __init__(self, location, fallback):
|
398
|
1
|
self.location = location
|
399
|
1
|
self.fallback = fallback
|
400
|
|
|
401
|
1
|
self.router = Map([
|
402
|
|
Rule("/<name>", methods=["GET"], endpoint="get"),
|
403
|
|
], encoding_errors="strict")
|
404
|
|
|
405
|
1
|
def __call__(self, environ, start_response):
|
406
|
0
|
return self.wsgi_app(environ, start_response)
|
407
|
|
|
408
|
1
|
@responder
|
409
|
|
def wsgi_app(self, environ, start_response):
|
410
|
0
|
route = self.router.bind_to_environ(environ)
|
411
|
0
|
try:
|
412
|
0
|
endpoint, args = route.match()
|
413
|
0
|
except HTTPException as exc:
|
414
|
0
|
return exc
|
415
|
|
|
416
|
0
|
location = self.location % args
|
417
|
|
|
418
|
0
|
request = Request(environ)
|
419
|
0
|
request.encoding_errors = "strict"
|
420
|
|
|
421
|
0
|
response = Response()
|
422
|
|
|
423
|
0
|
available = list()
|
424
|
0
|
for extension, mimetype in self.EXT_TO_MIME.items():
|
425
|
0
|
if os.path.isfile(location + '.' + extension):
|
426
|
0
|
available.append(mimetype)
|
427
|
0
|
mimetype = request.accept_mimetypes.best_match(available)
|
428
|
0
|
if mimetype is not None:
|
429
|
0
|
path = "%s.%s" % (location, self.MIME_TO_EXT[mimetype])
|
430
|
|
else:
|
431
|
0
|
path = self.fallback
|
432
|
0
|
mimetype = 'image/png' # FIXME Hardcoded type.
|
433
|
|
|
434
|
0
|
response.status_code = 200
|
435
|
0
|
response.mimetype = mimetype
|
436
|
0
|
response.last_modified = \
|
437
|
|
datetime.utcfromtimestamp(os.path.getmtime(path))\
|
438
|
|
.replace(microsecond=0)
|
439
|
|
|
440
|
|
# TODO check for If-Modified-Since and If-None-Match
|
441
|
|
|
442
|
0
|
response.response = wrap_file(environ, open(path, 'rb'))
|
443
|
0
|
response.direct_passthrough = True
|
444
|
|
|
445
|
0
|
return response
|
446
|
|
|
447
|
|
|
448
|
1
|
class RootHandler:
|
449
|
|
|
450
|
1
|
def __init__(self, location):
|
451
|
1
|
self.path = os.path.join(location, "Ranking.html")
|
452
|
|
|
453
|
1
|
def __call__(self, environ, start_response):
|
454
|
0
|
return self.wsgi_app(environ, start_response)
|
455
|
|
|
456
|
1
|
@responder
|
457
|
|
def wsgi_app(self, environ, start_response):
|
458
|
0
|
request = Request(environ)
|
459
|
0
|
request.encoding_errors = "strict"
|
460
|
|
|
461
|
0
|
response = Response()
|
462
|
0
|
response.status_code = 200
|
463
|
0
|
response.mimetype = "text/html"
|
464
|
0
|
response.last_modified = \
|
465
|
|
datetime.utcfromtimestamp(os.path.getmtime(self.path))\
|
466
|
|
.replace(microsecond=0)
|
467
|
|
# TODO check for If-Modified-Since and If-None-Match
|
468
|
0
|
response.response = wrap_file(environ, open(self.path, 'rb'))
|
469
|
0
|
response.direct_passthrough = True
|
470
|
|
|
471
|
0
|
return response
|
472
|
|
|
473
|
|
|
474
|
1
|
class RoutingHandler:
|
475
|
|
|
476
|
1
|
def __init__(self, root_handler, event_handler, logo_handler,
|
477
|
|
score_handler, history_handler):
|
478
|
1
|
self.router = Map([
|
479
|
|
Rule("/", methods=["GET"], endpoint="root"),
|
480
|
|
Rule("/history", methods=["GET"], endpoint="history"),
|
481
|
|
Rule("/scores", methods=["GET"], endpoint="scores"),
|
482
|
|
Rule("/events", methods=["GET"], endpoint="events"),
|
483
|
|
Rule("/logo", methods=["GET"], endpoint="logo"),
|
484
|
|
], encoding_errors="strict")
|
485
|
|
|
486
|
1
|
self.event_handler = event_handler
|
487
|
1
|
self.logo_handler = logo_handler
|
488
|
1
|
self.score_handler = score_handler
|
489
|
1
|
self.history_handler = history_handler
|
490
|
1
|
self.root_handler = root_handler
|
491
|
|
|
492
|
1
|
def __call__(self, environ, start_response):
|
493
|
0
|
return self.wsgi_app(environ, start_response)
|
494
|
|
|
495
|
1
|
def wsgi_app(self, environ, start_response):
|
496
|
0
|
route = self.router.bind_to_environ(environ)
|
497
|
0
|
try:
|
498
|
0
|
endpoint, args = route.match()
|
499
|
0
|
except HTTPException as exc:
|
500
|
0
|
return exc(environ, start_response)
|
501
|
|
|
502
|
0
|
if endpoint == "events":
|
503
|
0
|
return self.event_handler(environ, start_response)
|
504
|
0
|
elif endpoint == "logo":
|
505
|
0
|
return self.logo_handler(environ, start_response)
|
506
|
0
|
elif endpoint == "root":
|
507
|
0
|
return self.root_handler(environ, start_response)
|
508
|
0
|
elif endpoint == "scores":
|
509
|
0
|
return self.score_handler(environ, start_response)
|
510
|
0
|
elif endpoint == "history":
|
511
|
0
|
return self.history_handler(environ, start_response)
|
512
|
|
|
513
|
|
|
514
|
0
|
def main():
|
515
|
|
"""Entry point for RWS.
|
516
|
|
|
517
|
|
return (int): exit code (0 on success, 1 on error)
|
518
|
|
|
519
|
|
"""
|
520
|
1
|
parser = argparse.ArgumentParser(
|
521
|
|
description="Ranking for CMS.")
|
522
|
1
|
parser.add_argument("--config", type=argparse.FileType("rt"),
|
523
|
|
help="override config file")
|
524
|
1
|
parser.add_argument("-d", "--drop", action="store_true",
|
525
|
|
help="drop the data already stored")
|
526
|
1
|
parser.add_argument("-y", "--yes", action="store_true",
|
527
|
|
help="do not require confirmation on dropping data")
|
528
|
1
|
args = parser.parse_args()
|
529
|
|
|
530
|
1
|
config = Config()
|
531
|
1
|
config.load(args.config)
|
532
|
|
|
533
|
1
|
if args.drop:
|
534
|
0
|
if args.yes:
|
535
|
0
|
ans = 'y'
|
536
|
|
else:
|
537
|
0
|
ans = input("Are you sure you want to delete directory %s? [y/N] " %
|
538
|
|
config.lib_dir).strip().lower()
|
539
|
0
|
if ans in ['y', 'yes']:
|
540
|
0
|
print("Removing directory %s." % config.lib_dir)
|
541
|
0
|
shutil.rmtree(config.lib_dir)
|
542
|
|
else:
|
543
|
0
|
print("Not removing directory %s." % config.lib_dir)
|
544
|
0
|
return 0
|
545
|
|
|
546
|
1
|
stores = dict()
|
547
|
|
|
548
|
1
|
stores["subchange"] = Store(
|
549
|
|
Subchange, os.path.join(config.lib_dir, 'subchanges'), stores)
|
550
|
1
|
stores["submission"] = Store(
|
551
|
|
Submission, os.path.join(config.lib_dir, 'submissions'), stores,
|
552
|
|
[stores["subchange"]])
|
553
|
1
|
stores["user"] = Store(
|
554
|
|
User, os.path.join(config.lib_dir, 'users'), stores,
|
555
|
|
[stores["submission"]])
|
556
|
1
|
stores["team"] = Store(
|
557
|
|
Team, os.path.join(config.lib_dir, 'teams'), stores,
|
558
|
|
[stores["user"]])
|
559
|
1
|
stores["task"] = Store(
|
560
|
|
Task, os.path.join(config.lib_dir, 'tasks'), stores,
|
561
|
|
[stores["submission"]])
|
562
|
1
|
stores["contest"] = Store(
|
563
|
|
Contest, os.path.join(config.lib_dir, 'contests'), stores,
|
564
|
|
[stores["task"]])
|
565
|
|
|
566
|
1
|
stores["contest"].load_from_disk()
|
567
|
1
|
stores["task"].load_from_disk()
|
568
|
1
|
stores["team"].load_from_disk()
|
569
|
1
|
stores["user"].load_from_disk()
|
570
|
1
|
stores["submission"].load_from_disk()
|
571
|
1
|
stores["subchange"].load_from_disk()
|
572
|
|
|
573
|
1
|
stores["scoring"] = ScoringStore(stores)
|
574
|
1
|
stores["scoring"].init_store()
|
575
|
|
|
576
|
1
|
toplevel_handler = RoutingHandler(
|
577
|
|
RootHandler(config.web_dir),
|
578
|
|
DataWatcher(stores, config.buffer_size),
|
579
|
|
ImageHandler(
|
580
|
|
os.path.join(config.lib_dir, '%(name)s'),
|
581
|
|
os.path.join(config.web_dir, 'img', 'logo.png')),
|
582
|
|
ScoreHandler(stores),
|
583
|
|
HistoryHandler(stores))
|
584
|
|
|
585
|
1
|
wsgi_app = SharedDataMiddleware(DispatcherMiddleware(
|
586
|
|
toplevel_handler, {
|
587
|
|
'/contests': StoreHandler(
|
588
|
|
stores["contest"],
|
589
|
|
config.username, config.password, config.realm_name),
|
590
|
|
'/tasks': StoreHandler(
|
591
|
|
stores["task"],
|
592
|
|
config.username, config.password, config.realm_name),
|
593
|
|
'/teams': StoreHandler(
|
594
|
|
stores["team"],
|
595
|
|
config.username, config.password, config.realm_name),
|
596
|
|
'/users': StoreHandler(
|
597
|
|
stores["user"],
|
598
|
|
config.username, config.password, config.realm_name),
|
599
|
|
'/submissions': StoreHandler(
|
600
|
|
stores["submission"],
|
601
|
|
config.username, config.password, config.realm_name),
|
602
|
|
'/subchanges': StoreHandler(
|
603
|
|
stores["subchange"],
|
604
|
|
config.username, config.password, config.realm_name),
|
605
|
|
'/faces': ImageHandler(
|
606
|
|
os.path.join(config.lib_dir, 'faces', '%(name)s'),
|
607
|
|
os.path.join(config.web_dir, 'img', 'face.png')),
|
608
|
|
'/flags': ImageHandler(
|
609
|
|
os.path.join(config.lib_dir, 'flags', '%(name)s'),
|
610
|
|
os.path.join(config.web_dir, 'img', 'flag.png')),
|
611
|
|
'/sublist': SubListHandler(stores),
|
612
|
|
}), {'/': config.web_dir})
|
613
|
|
|
614
|
1
|
servers = list()
|
615
|
1
|
if config.http_port is not None:
|
616
|
1
|
http_server = WSGIServer(
|
617
|
|
(config.bind_address, config.http_port), wsgi_app)
|
618
|
1
|
servers.append(http_server)
|
619
|
1
|
if config.https_port is not None:
|
620
|
0
|
https_server = WSGIServer(
|
621
|
|
(config.bind_address, config.https_port), wsgi_app,
|
622
|
|
certfile=config.https_certfile, keyfile=config.https_keyfile)
|
623
|
0
|
servers.append(https_server)
|
624
|
|
|
625
|
1
|
try:
|
626
|
1
|
gevent.joinall(list(gevent.spawn(s.serve_forever) for s in servers))
|
627
|
0
|
except KeyboardInterrupt:
|
628
|
0
|
pass
|
629
|
|
finally:
|
630
|
1
|
gevent.joinall(list(gevent.spawn(s.stop) for s in servers))
|
631
|
0
|
return 0
|