# Copyright (c) 2022, TU Wien
# All rights reserved.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.
import os
from jinja2 import ChoiceLoader, Environment, FileSystemLoader, PrefixLoader
from tornado import web
from traitlets import Dict, Float, List, Unicode, config, default
GRADER_COOKIE_NAME = "grader_service_login"
[docs]
class GraderServer(config.LoggingConfigurable, web.Application):
"""As an unmanage jupyter hub service, the application gets these
environment variables from the hub
See jupyterhub docs section on launching-a-hub-managed-service
link: https://shorturl.at/yDGHZ"""
cookie_max_age_days = Float(
14,
help="""Number of days for a login cookie to be valid.
Default is two weeks.
""",
).tag(config=True)
static_file_path = Unicode(
os.path.join(os.path.dirname(__file__), "static"),
help="""Absolute to the static file directory.""",
).tag(config=True)
logo_file = Unicode(
"", help="Specify path to a logo image to override the Grader logo in the banner."
).tag(config=True)
@default("logo_file")
def _logo_file_default(self):
return os.path.join(os.path.dirname(__file__), "static", "images", "grader_logo.png")
template_paths = List(
help="Paths to search for jinja templates, before using the default templates."
).tag(config=True)
@default("template_paths")
def _template_paths_default(self):
return [os.path.join(os.path.dirname(__file__), "templates")]
jinja_environment_options = Dict(
help="Supply extra arguments that will be passed to Jinja environment."
).tag(config=True)
template_vars = Dict(
help="""Extra variables to be passed into jinja templates.
Values in dict may contain callable objects.
If value is callable, the current user is passed as argument.
Example::
def callable_value(user):
# user is generated by handlers.base.get_current_user
with open("/tmp/file.txt", "r") as f:
ret = f.read()
ret = ret.replace("<username>", user.name)
return ret
c.JupyterHub.template_vars = {
"key1": "value1",
"key2": callable_value,
}
"""
).tag(config=True)
def __init__(
self,
grader_service_dir: str,
base_url: str,
authenticator,
oauth_provider,
session_maker,
**kwargs,
):
kwargs.update(dict(static_path=self.static_file_path))
super().__init__(**kwargs)
self.grader_service_dir = grader_service_dir
self.base_url = base_url
self.authenticator = authenticator
self.oauth_provider = oauth_provider
self.cookie_name = GRADER_COOKIE_NAME
self.session_maker = session_maker
jinja_options = dict(autoescape=True, enable_async=True)
jinja_options.update(self.jinja_environment_options)
base_path = self._template_paths_default()[0]
if base_path not in self.template_paths:
self.template_paths.append(base_path)
loader = ChoiceLoader(
[
PrefixLoader({"templates": FileSystemLoader([base_path])}, "/"),
FileSystemLoader(self.template_paths),
]
)
self.jinja_env = Environment(loader=loader, **jinja_options)
# We need a sync jinja environment too, for the times we *must* use sync
# code - particularly in RequestHandler.write_error. Since *that*
# is called from inside the asyncio event loop, we can't actulaly just
# schedule it on the loop - without starting another thread with its
# own loop, which seems not worth the trouble. Instead, we create another
# environment, exactly like this one, but sync
del jinja_options["enable_async"]
self.jinja_env_sync = Environment(loader=loader, **jinja_options)