class CommonRequestHandler(RequestHandler): """Encapsulates shared RequestHandler functionality. """ # Whether the login cookie duration has to be refreshed when # this handler is called. Useful to filter asynchronous # requests. refresh_cookie = True def __init__(self, *args, **kwargs): super(CommonRequestHandler, self).__init__(*args, **kwargs) self.timestamp = None self.sql_session = None self.r_params = None self.contest = None self.url = None def prepare(self): """This method is executed at the beginning of each request. """ super(CommonRequestHandler, self).prepare() self.timestamp = make_datetime() self.set_header("Cache-Control", "no-cache, must-revalidate") self.sql_session = Session() self.sql_session.expire_all() self.url = create_url_builder(get_url_root(self.request.path)) def finish(self, *args, **kwargs): """Finish this response, ending the HTTP request. We override this method in order to properly close the database. TODO - Now that we have greenlet support, this method could be refactored in terms of context manager or something like that. So far I'm leaving it to minimize changes. """ if self.sql_session is not None: try: self.sql_session.close() except Exception as error: logger.warning("Couldn't close SQL connection: %r", error) try: super(CommonRequestHandler, self).finish(*args, **kwargs) except IOError: # When the client closes the connection before we reply, # Tornado raises an IOError exception, that would pollute # our log with unnecessarily critical messages logger.debug("Connection closed before our reply.") @property def service(self): return self.application.service
class CommonRequestHandler(RequestHandler): """Encapsulates shared RequestHandler functionality. """ # Whether the login cookie duration has to be refreshed when # this handler is called. Useful to filter asynchronous # requests. refresh_cookie = True def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.timestamp = make_datetime() self.sql_session = Session() self.r_params = None self.contest = None self.url = None def prepare(self): """This method is executed at the beginning of each request. """ super().prepare() self.url = Url(get_url_root(self.request.path)) self.set_header("Cache-Control", "no-cache, must-revalidate") def finish(self, *args, **kwargs): """Finish this response, ending the HTTP request. We override this method in order to properly close the database. TODO - Now that we have greenlet support, this method could be refactored in terms of context manager or something like that. So far I'm leaving it to minimize changes. """ if self.sql_session is not None: try: self.sql_session.close() except Exception as error: logger.warning("Couldn't close SQL connection: %r", error) try: super().finish(*args, **kwargs) except OSError: # When the client closes the connection before we reply, # Tornado raises an OSError exception, that would pollute # our log with unnecessarily critical messages logger.debug("Connection closed before our reply.") @property def service(self): return self.application.service
class BaseHandler(CommonRequestHandler): """Base RequestHandler for this application. All the RequestHandler classes in this application should be a child of this class. """ def try_commit(self): """Try to commit the current session. If not successful display a warning in the webpage. return (bool): True if commit was successful, False otherwise. """ try: self.sql_session.commit() except IntegrityError as error: self.application.service.add_notification( make_datetime(), "Operation failed.", "%s" % error) return False else: self.application.service.add_notification( make_datetime(), "Operation successful.", "") return True def safe_get_item(self, cls, ident, session=None): """Get item from database of class cls and id ident, using session if given, or self.sql_session if not given. If id is not found, raise a 404. cls (type): class of object to retrieve. ident (string): id of object. session (Session|None): session to use. return (object): the object with the given id. raise (HTTPError): 404 if not found. """ if session is None: session = self.sql_session entity = cls.get_from_id(ident, session) if entity is None: raise tornado.web.HTTPError(404) return entity def prepare(self): """This method is executed at the beginning of each request. """ # Attempt to update the contest and all its references # If this fails, the request terminates. self.set_header("Cache-Control", "no-cache, must-revalidate") self.sql_session = Session() self.sql_session.expire_all() self.contest = None def render_params(self): """Return the default render params used by almost all handlers. return (dict): default render params """ params = {} params["timestamp"] = make_datetime() params["contest"] = self.contest params["url_root"] = get_url_root(self.request.path) if self.contest is not None: params["phase"] = self.contest.phase(params["timestamp"]) # Keep "== None" in filter arguments. SQLAlchemy does not # understand "is None". params["unanswered"] = self.sql_session.query(Question)\ .join(Participation)\ .filter(Participation.contest_id == self.contest.id)\ .filter(Question.reply_timestamp == None)\ .filter(Question.ignored == False)\ .count() # noqa # TODO: not all pages require all these data. params["contest_list"] = self.sql_session.query(Contest).all() params["task_list"] = self.sql_session.query(Task).all() params["user_list"] = self.sql_session.query(User).all() return params def finish(self, *args, **kwds): """Finish this response, ending the HTTP request. We override this method in order to properly close the database. TODO - Now that we have greenlet support, this method could be refactored in terms of context manager or something like that. So far I'm leaving it to minimize changes. """ self.sql_session.close() try: tornado.web.RequestHandler.finish(self, *args, **kwds) except IOError: # When the client closes the connection before we reply, # Tornado raises an IOError exception, that would pollute # our log with unnecessarily critical messages logger.debug("Connection closed before our reply.") def write_error(self, status_code, **kwargs): if "exc_info" in kwargs and \ kwargs["exc_info"][0] != tornado.web.HTTPError: exc_info = kwargs["exc_info"] logger.error( "Uncaught exception (%r) while processing a request: %s", exc_info[1], ''.join(traceback.format_exception(*exc_info))) # Most of the handlers raise a 404 HTTP error before r_params # is defined. If r_params is not defined we try to define it # here, and if it fails we simply return a basic textual error notice. if self.r_params is None: try: self.r_params = self.render_params() except: self.write("A critical error has occurred :-(") self.finish() return self.render("error.html", status_code=status_code, **self.r_params) get_string = argument_reader(lambda a: a, empty="") # When a checkbox isn't active it's not sent at all, making it # impossible to distinguish between missing and False. def get_bool(self, dest, name): """Parse a boolean. dest (dict): a place to store the result. name (string): the name of the argument and of the item. """ value = self.get_argument(name, False) try: dest[name] = bool(value) except: raise ValueError("Can't cast %s to bool." % value) get_int = argument_reader(parse_int) get_timedelta_sec = argument_reader(parse_timedelta_sec) get_timedelta_min = argument_reader(parse_timedelta_min) get_datetime = argument_reader(parse_datetime) get_ip_address_or_subnet = argument_reader(parse_ip_address_or_subnet) def get_submission_format(self, dest): """Parse the submission format. Using the two arguments "submission_format_choice" and "submission_format" set the "submission_format" item of the given dictionary. dest (dict): a place to store the result. """ choice = self.get_argument("submission_format_choice", "other") if choice == "simple": filename = "%s.%%l" % dest["name"] format_ = [SubmissionFormatElement(filename)] elif choice == "other": value = self.get_argument("submission_format", "[]") if value == "": value = "[]" format_ = [] try: for filename in json.loads(value): format_ += [SubmissionFormatElement(filename)] except ValueError: raise ValueError("Submission format not recognized.") else: raise ValueError("Submission format not recognized.") dest["submission_format"] = format_ def get_time_limit(self, dest, field): """Parse the time limit. Read the argument with the given name and use its value to set the "time_limit" item of the given dictionary. dest (dict): a place to store the result. field (string): the name of the argument to use. """ value = self.get_argument(field, None) if value is None: return if value == "": dest["time_limit"] = None else: try: value = float(value) except: raise ValueError("Can't cast %s to float." % value) if not 0 <= value < float("+inf"): raise ValueError("Time limit out of range.") dest["time_limit"] = value def get_memory_limit(self, dest, field): """Parse the memory limit. Read the argument with the given name and use its value to set the "memory_limit" item of the given dictionary. dest (dict): a place to store the result. field (string): the name of the argument to use. """ value = self.get_argument(field, None) if value is None: return if value == "": dest["memory_limit"] = None else: try: value = int(value) except: raise ValueError("Can't cast %s to float." % value) if not 0 < value: raise ValueError("Invalid memory limit.") dest["memory_limit"] = value def get_task_type(self, dest, name, params): """Parse the task type. Parse the arguments to get the task type and its parameters, and fill them in the "task_type" and "task_type_parameters" items of the given dictionary. dest (dict): a place to store the result. name (string): the name of the argument that holds the task type name. params (string): the prefix of the names of the arguments that hold the parameters. """ name = self.get_argument(name, None) if name is None: raise ValueError("Task type not found.") try: class_ = get_task_type_class(name) except KeyError: raise ValueError("Task type not recognized: %s." % name) params = json.dumps(class_.parse_handler(self, params + name + "_")) dest["task_type"] = name dest["task_type_parameters"] = params def get_score_type(self, dest, name, params): """Parse the score type. Parse the arguments to get the score type and its parameters, and fill them in the "score_type" and "score_type_parameters" items of the given dictionary. dest (dict): a place to store the result. name (string): the name of the argument that holds the score type name. params (string): the name of the argument that hold the parameters. """ name = self.get_argument(name, None) if name is None: raise ValueError("Score type not found.") try: get_score_type_class(name) except KeyError: raise ValueError("Score type not recognized: %s." % name) params = self.get_argument(params, None) if params is None: raise ValueError("Score type parameters not found.") dest["score_type"] = name dest["score_type_parameters"] = params
class BaseHandler(CommonRequestHandler): """Base RequestHandler for this application. All the RequestHandler classes in this application should be a child of this class. """ def try_commit(self): """Try to commit the current session. If not successful display a warning in the webpage. return (bool): True if commit was successful, False otherwise. """ try: self.sql_session.commit() except IntegrityError as error: self.application.service.add_notification(make_datetime(), "Operation failed.", "%s" % error) return False else: self.application.service.add_notification(make_datetime(), "Operation successful.", "") return True def safe_get_item(self, cls, ident, session=None): """Get item from database of class cls and id ident, using session if given, or self.sql_session if not given. If id is not found, raise a 404. cls (type): class of object to retrieve. ident (string): id of object. session (Session|None): session to use. return (object): the object with the given id. raise (HTTPError): 404 if not found. """ if session is None: session = self.sql_session entity = cls.get_from_id(ident, session) if entity is None: raise tornado.web.HTTPError(404) return entity def prepare(self): """This method is executed at the beginning of each request. """ # Attempt to update the contest and all its references # If this fails, the request terminates. self.set_header("Cache-Control", "no-cache, must-revalidate") self.sql_session = Session() self.sql_session.expire_all() self.contest = None def render_params(self): """Return the default render params used by almost all handlers. return (dict): default render params """ params = {} params["timestamp"] = make_datetime() params["contest"] = self.contest params["url_root"] = get_url_root(self.request.path) if self.contest is not None: params["phase"] = self.contest.phase(params["timestamp"]) # Keep "== None" in filter arguments. SQLAlchemy does not # understand "is None". params["unanswered"] = self.sql_session.query(Question)\ .join(Participation)\ .filter(Participation.contest_id == self.contest.id)\ .filter(Question.reply_timestamp == None)\ .filter(Question.ignored == False)\ .count() # noqa # TODO: not all pages require all these data. params["contest_list"] = self.sql_session.query(Contest).all() params["task_list"] = self.sql_session.query(Task).all() params["user_list"] = self.sql_session.query(User).all() params["team_list"] = self.sql_session.query(Team).all() return params def finish(self, *args, **kwds): """Finish this response, ending the HTTP request. We override this method in order to properly close the database. TODO - Now that we have greenlet support, this method could be refactored in terms of context manager or something like that. So far I'm leaving it to minimize changes. """ self.sql_session.close() try: tornado.web.RequestHandler.finish(self, *args, **kwds) except IOError: # When the client closes the connection before we reply, # Tornado raises an IOError exception, that would pollute # our log with unnecessarily critical messages logger.debug("Connection closed before our reply.") def write_error(self, status_code, **kwargs): if "exc_info" in kwargs and \ kwargs["exc_info"][0] != tornado.web.HTTPError: exc_info = kwargs["exc_info"] logger.error( "Uncaught exception (%r) while processing a request: %s", exc_info[1], ''.join(traceback.format_exception(*exc_info))) # Most of the handlers raise a 404 HTTP error before r_params # is defined. If r_params is not defined we try to define it # here, and if it fails we simply return a basic textual error notice. if self.r_params is None: try: self.r_params = self.render_params() except: self.write("A critical error has occurred :-(") self.finish() return self.render("error.html", status_code=status_code, **self.r_params) get_string = argument_reader(lambda a: a, empty="") # When a checkbox isn't active it's not sent at all, making it # impossible to distinguish between missing and False. def get_bool(self, dest, name): """Parse a boolean. dest (dict): a place to store the result. name (string): the name of the argument and of the item. """ value = self.get_argument(name, False) try: dest[name] = bool(value) except: raise ValueError("Can't cast %s to bool." % value) get_int = argument_reader(parse_int) get_timedelta_sec = argument_reader(parse_timedelta_sec) get_timedelta_min = argument_reader(parse_timedelta_min) get_datetime = argument_reader(parse_datetime) get_ip_address_or_subnet = argument_reader(parse_ip_address_or_subnet) def get_submission_format(self, dest): """Parse the submission format. Using the two arguments "submission_format_choice" and "submission_format" set the "submission_format" item of the given dictionary. dest (dict): a place to store the result. """ choice = self.get_argument("submission_format_choice", "other") if choice == "simple": filename = "%s.%%l" % dest["name"] format_ = [SubmissionFormatElement(filename)] elif choice == "other": value = self.get_argument("submission_format", "[]") if value == "": value = "[]" format_ = [] try: for filename in json.loads(value): format_ += [SubmissionFormatElement(filename)] except ValueError: raise ValueError("Submission format not recognized.") else: raise ValueError("Submission format not recognized.") dest["submission_format"] = format_ def get_time_limit(self, dest, field): """Parse the time limit. Read the argument with the given name and use its value to set the "time_limit" item of the given dictionary. dest (dict): a place to store the result. field (string): the name of the argument to use. """ value = self.get_argument(field, None) if value is None: return if value == "": dest["time_limit"] = None else: try: value = float(value) except: raise ValueError("Can't cast %s to float." % value) if not 0 <= value < float("+inf"): raise ValueError("Time limit out of range.") dest["time_limit"] = value def get_memory_limit(self, dest, field): """Parse the memory limit. Read the argument with the given name and use its value to set the "memory_limit" item of the given dictionary. dest (dict): a place to store the result. field (string): the name of the argument to use. """ value = self.get_argument(field, None) if value is None: return if value == "": dest["memory_limit"] = None else: try: value = int(value) except: raise ValueError("Can't cast %s to float." % value) if not 0 < value: raise ValueError("Invalid memory limit.") dest["memory_limit"] = value def get_task_type(self, dest, name, params): """Parse the task type. Parse the arguments to get the task type and its parameters, and fill them in the "task_type" and "task_type_parameters" items of the given dictionary. dest (dict): a place to store the result. name (string): the name of the argument that holds the task type name. params (string): the prefix of the names of the arguments that hold the parameters. """ name = self.get_argument(name, None) if name is None: raise ValueError("Task type not found.") try: class_ = get_task_type_class(name) except KeyError: raise ValueError("Task type not recognized: %s." % name) params = json.dumps(class_.parse_handler(self, params + name + "_")) dest["task_type"] = name dest["task_type_parameters"] = params def get_score_type(self, dest, name, params): """Parse the score type. Parse the arguments to get the score type and its parameters, and fill them in the "score_type" and "score_type_parameters" items of the given dictionary. dest (dict): a place to store the result. name (string): the name of the argument that holds the score type name. params (string): the name of the argument that hold the parameters. """ name = self.get_argument(name, None) if name is None: raise ValueError("Score type not found.") try: get_score_type_class(name) except KeyError: raise ValueError("Score type not recognized: %s." % name) params = self.get_argument(params, None) if params is None: raise ValueError("Score type parameters not found.") dest["score_type"] = name dest["score_type_parameters"] = params def render_params_for_submissions(self, query, page, page_size=50): """Add data about the requested submissions to r_params. submission_query (sqlalchemy.orm.query.Query): the query giving back all interesting submissions. page (int): the index of the page to display. page_size(int): the number of submissions per page. """ query = query\ .options(joinedload(Submission.task))\ .options(joinedload(Submission.participation))\ .options(joinedload(Submission.files))\ .options(joinedload(Submission.token))\ .options(joinedload(Submission.results) .joinedload(SubmissionResult.evaluations))\ .order_by(Submission.timestamp.desc()) offset = page * page_size count = query.count() if self.r_params is None: self.r_params = self.render_params() # A page showing paginated submissions can use these # parameters: total number of submissions, submissions to # display in this page, index of the current page, total # number of pages. self.r_params["submission_count"] = count self.r_params["submissions"] = \ query.slice(offset, offset + page_size).all() self.r_params["submission_page"] = page self.r_params["submission_pages"] = \ (count + page_size - 1) // page_size
class BaseHandler(CommonRequestHandler): """Base RequestHandler for this application. All the RequestHandler classes in this application should be a child of this class. """ # Whether the login cookie duration has to be refreshed when # this handler is called. Useful to filter asynchronous # requests. refresh_cookie = True def __init__(self, *args, **kwargs): super(BaseHandler, self).__init__(*args, **kwargs) self.timestamp = None self.cookie_lang = None self.browser_lang = None self.langs = None self._ = None def prepare(self): """This method is executed at the beginning of each request. """ self.timestamp = make_datetime() self.set_header("Cache-Control", "no-cache, must-revalidate") self.sql_session = Session() self.contest = Contest.get_from_id(self.application.service.contest, self.sql_session) self._ = self.locale.translate self.r_params = self.render_params() def get_current_user(self): """The name is get_current_user because tornado wants it that way, but this is really a get_current_participation. Gets the current participation from cookies. If a valid cookie is retrieved, return a Participation tuple (specifically: the Participation involving the username specified in the cookie and the current contest). Otherwise (e.g. the user exists but doesn't participate in the current contest), return None. """ remote_ip = self.request.remote_ip if self.contest.ip_autologin: self.clear_cookie("login") participations = self.sql_session.query(Participation)\ .filter(Participation.contest == self.contest)\ .filter(Participation.ip == remote_ip)\ .all() if len(participations) == 1: return participations[0] if len(participations) > 1: logger.error("Multiple users have IP %s." % (remote_ip)) else: logger.error("No user has IP %s" % (remote_ip)) # If IP autologin is set, we do not allow password logins. return None if self.get_secure_cookie("login") is None: return None # Parse cookie. try: cookie = pickle.loads(self.get_secure_cookie("login")) username = cookie[0] password = cookie[1] last_update = make_datetime(cookie[2]) except: self.clear_cookie("login") return None # Check if the cookie is expired. if self.timestamp - last_update > \ timedelta(seconds=config.cookie_duration): self.clear_cookie("login") return None # Load user from DB. user = self.sql_session.query(User)\ .filter(User.username == username)\ .first() # Check if user exists. if user is None: self.clear_cookie("login") return None # Load participation from DB. participation = self.sql_session.query(Participation)\ .filter(Participation.contest == self.contest)\ .filter(Participation.user == user)\ .first() # Check if participaton exists. if participation is None: self.clear_cookie("login") return None # If a contest-specific password is defined, use that. If it's # not, use the user's main password. if participation.password is None: correct_password = user.password else: correct_password = participation.password # Check if user is allowed to login. if password != correct_password: self.clear_cookie("login") return None # Check if user is using the right IP (or is on the right subnet) if self.contest.ip_restriction and participation.ip is not None \ and not check_ip(self.request.remote_ip, participation.ip): self.clear_cookie("login") return None # Check if user is hidden if participation.hidden and self.contest.block_hidden_participations: self.clear_cookie("login") return None if self.refresh_cookie: self.set_secure_cookie("login", pickle.dumps((user.username, password, make_timestamp())), expires_days=None) return participation def get_user_locale(self): self.langs = self.application.service.langs lang_codes = self.langs.keys() if len(self.contest.allowed_localizations) > 0: lang_codes = filter_language_codes( lang_codes, self.contest.allowed_localizations) # TODO We fallback on "en" if no language matches: we could # return 406 Not Acceptable instead. # Select the one the user likes most. http_langs = [lang_code.replace("_", "-") for lang_code in lang_codes] self.browser_lang = parse_accept_header( self.request.headers.get("Accept-Language", ""), LanguageAccept).best_match(http_langs, "en") self.cookie_lang = self.get_cookie("language", None) if self.cookie_lang in http_langs: lang_code = self.cookie_lang else: lang_code = self.browser_lang self.set_header("Content-Language", lang_code) return self.langs[lang_code.replace("-", "_")] @staticmethod def _get_token_status(obj): """Return the status of the tokens for the given object. obj (Contest or Task): an object that has the token_* attributes. return (int): one of 0 (disabled), 1 (enabled/finite) and 2 (enabled/infinite). """ if obj.token_mode == "disabled": return 0 elif obj.token_mode == "finite": return 1 elif obj.token_mode == "infinite": return 2 else: raise RuntimeError("Unknown token_mode value.") def render_params(self): """Return the default render params used by almost all handlers. return (dict): default render params """ ret = {} ret["timestamp"] = self.timestamp ret["contest"] = self.contest ret["url_root"] = get_url_root(self.request.path) ret["phase"] = self.contest.phase(self.timestamp) ret["printing_enabled"] = (config.printer is not None) if self.current_user is not None: participation = self.current_user res = compute_actual_phase( self.timestamp, self.contest.start, self.contest.stop, self.contest.per_user_time, participation.starting_time, participation.delay_time, participation.extra_time) ret["actual_phase"], ret["current_phase_begin"], \ ret["current_phase_end"], ret["valid_phase_begin"], \ ret["valid_phase_end"] = res if ret["actual_phase"] == 0: ret["phase"] = 0 # set the timezone used to format timestamps ret["timezone"] = get_timezone(participation.user, self.contest) # some information about token configuration ret["tokens_contest"] = self._get_token_status(self.contest) t_tokens = sum(self._get_token_status(t) for t in self.contest.tasks) if t_tokens == 0: ret["tokens_tasks"] = 0 # all disabled elif t_tokens == 2 * len(self.contest.tasks): ret["tokens_tasks"] = 2 # all infinite else: ret["tokens_tasks"] = 1 # all finite or mixed # TODO Now all language names are shown in the active language. # It would be better to show them in the corresponding one. ret["lang_names"] = {} for lang_code, trans in self.langs.iteritems(): language_name = None try: language_name = translate_language_country_code( lang_code, trans) except ValueError: language_name = translate_language_code( lang_code, trans) ret["lang_names"][lang_code.replace("_", "-")] = language_name ret["cookie_lang"] = self.cookie_lang ret["browser_lang"] = self.browser_lang return ret def finish(self, *args, **kwds): """Finish this response, ending the HTTP request. We override this method in order to properly close the database. TODO - Now that we have greenlet support, this method could be refactored in terms of context manager or something like that. So far I'm leaving it to minimize changes. """ if hasattr(self, "sql_session"): try: self.sql_session.close() except Exception as error: logger.warning("Couldn't close SQL connection: %r", error) try: tornado.web.RequestHandler.finish(self, *args, **kwds) except IOError: # When the client closes the connection before we reply, # Tornado raises an IOError exception, that would pollute # our log with unnecessarily critical messages logger.debug("Connection closed before our reply.") def write_error(self, status_code, **kwargs): if "exc_info" in kwargs and \ kwargs["exc_info"][0] != tornado.web.HTTPError: exc_info = kwargs["exc_info"] logger.error( "Uncaught exception (%r) while processing a request: %s", exc_info[1], ''.join(traceback.format_exception(*exc_info))) # We assume that if r_params is defined then we have at least # the data we need to display a basic template with the error # information. If r_params is not defined (i.e. something went # *really* bad) we simply return a basic textual error notice. if hasattr(self, 'r_params'): self.render("error.html", status_code=status_code, **self.r_params) else: self.write("A critical error has occurred :-(") self.finish()