11import os
2+ import re
23import sys
34
5+ from jupyterhub .spawner import Spawner
46from coursewareuserspawner import CoursewareUserSpawner
57from jinja2 import Environment , BaseLoader
6- from traitlets import Unicode
8+ from traitlets import (
9+ List ,
10+ Tuple ,
11+ Unicode ,
12+ )
713from tornado import web
814
915from .registry import get_registry , split_image_name
@@ -29,7 +35,7 @@ class Repo2DockerSpawner(CoursewareUserSpawner):
2935 {% for image in image_list %}
3036 <label for='image-item-{{ loop.index0 }}' class='form-control input-group'>
3137 <div class='col-md-1'>
32- {% if image.default_course_image %}
38+ {% if image.selected %}
3339 <input type='radio' name='image' id='image-item-{{ loop.index0 }}' value='{{ registry_host }}/{{ image.image_name }}' checked/>
3440 {% else %}
3541 <input type='radio' name='image' id='image-item-{{ loop.index0 }}' value='{{ registry_host }}/{{ image.image_name }}' />
@@ -76,25 +82,87 @@ class Repo2DockerSpawner(CoursewareUserSpawner):
7682 """ ,
7783 )
7884
85+ notebook_dir = Unicode (
86+ '/home/{username}/{coursedir}' ,
87+ ** Spawner .notebook_dir .metadata
88+ )
89+
90+ workdir = Unicode (
91+ '/home/{username}/{coursedir}' ,
92+ ** CoursewareUserSpawner .workdir .metadata
93+ )
94+
95+ admin_home_mount_dirs = List (
96+ trait = Tuple (Unicode (), Unicode ()),
97+ default_value = [
98+ ('{coursedir}/admin_tools' , 'admin_tools' )
99+ ],
100+ ** CoursewareUserSpawner .admin_home_mount_dirs .metadata
101+ )
102+
103+ non_admin_home_mount_dirs = List (
104+ trait = Tuple (Unicode (), Unicode ()),
105+ default_value = [
106+ ('{coursedir}/tools' , 'tools' ),
107+ ('{coursedir}/textbook' , 'textbook/{coursedir}' ),
108+ ('{coursedir}/info' , 'info/{coursedir}' )
109+ ],
110+ ** CoursewareUserSpawner .non_admin_home_mount_dirs .metadata
111+ )
112+
79113 def __init__ (self , * args , ** kwargs ):
114+ self ._course_image = None
115+
80116 super ().__init__ (* args , ** kwargs )
81117
82118 self ._registry = get_registry (config = self .config )
83119
120+ @property
121+ def course_dir (self ):
122+ course_dir = self .name
123+ course_dir = re .sub (r'[^\w\-_\.\(\)\+\[\]\{\}@]' , '_' , course_dir )
124+ return course_dir
125+
126+ @property
127+ def course_image (self ):
128+ return self ._course_image
129+
130+ @course_image .setter
131+ def course_image (self , value ):
132+ self ._course_image = value
133+
134+ def template_namespace (self ):
135+ d = super ().template_namespace ()
136+
137+ d .update (dict (
138+ coursedir = self .course_dir
139+ ))
140+ return d
141+
84142 async def get_options_form (self ):
85143 """
86144 Override the default form to handle the case when there is only one image.
87145 """
88146 images = await self ._registry .list_images ()
147+ image_dict = {i ['image_name' ]: i for i in images }
89148
90149 if not self .user .admin :
91- self ._use_default_course_image (images )
150+ if self .course_image and self .course_image in image_dict :
151+ self .image = self ._registry .get_full_image_name (self .course_image )
152+ else :
153+ self ._use_default_course_image (images )
92154 return ''
93155
94156 if len (images ) <= 1 :
95157 self ._use_initial_course_image (images )
96158 return ''
97159
160+ for i in images :
161+ if self .course_image and self .course_image in image_dict :
162+ i ['selected' ] = (i ['image_name' ] == self .course_image )
163+ else :
164+ i ['selected' ] = i ['default_course_image' ]
165+
98166 image_form_template = Environment (loader = BaseLoader ).from_string (
99167 self .image_form_template
100168 )
@@ -145,6 +213,21 @@ async def _get_cmd_from_image(self):
145213 cmd = image_info ["Config" ]["Cmd" ]
146214 return cmd
147215
216+ def get_args (self ):
217+ args = super ().get_args ()
218+ username = self .user .name
219+ base_url = self .hub .base_url [:- 4 ]
220+ xsrf_cookie_path = base_url + 'user/' + username + '/'
221+ # workaround when jupyterhub<=4.1.5 is used on single-user server
222+ # https://github.com/jupyterhub/jupyterhub/pull/4750
223+ # https://github.com/jupyterhub/jupyterhub/pull/4771
224+ args .append (
225+ '--ServerApp.tornado_settings='
226+ '{"xsrf_cookie_kwargs":{"path":"'
227+ + xsrf_cookie_path + '"}}' )
228+
229+ return args
230+
148231 async def get_command (self ):
149232 image_cmd = await self ._get_cmd_from_image ()
150233 # override cmd for docker-stacks image
@@ -159,7 +242,84 @@ async def get_command(self):
159242
160243 return cmd + self .get_args ()
161244
245+ def get_env (self ):
246+ env = super ().get_env ()
247+ env .update (dict (
248+ CWH_COURSE_NAME = self .course_dir
249+ ))
250+ return env
251+
252+ def _make_user_dirs (self ):
253+ if self .course_dir :
254+ return
255+
256+ home_dir = os .path .join (self .users_dir , self .user .name )
257+ dirs = []
258+ if self .user .admin :
259+ dirs .extend ([
260+ (os .path .join (home_dir , 'textbook' ), 0o777 ),
261+ (os .path .join (home_dir , 'info' ), 0o777 )
262+ ])
263+
264+ statinfo = os .stat (home_dir )
265+ for dirpath , mode in dirs :
266+ self ._make_dir (dirpath , mode , statinfo .st_uid , statinfo .st_gid )
267+
268+ def _make_user_course_dirs (self ):
269+ if not self .course_dir :
270+ return
271+
272+ content_dirs = [
273+ os .path .join (self .admin_data_dir , 'textbook' , self .course_dir ),
274+ os .path .join (self .admin_data_dir , 'info' , self .course_dir )
275+ ]
276+
277+ home_dir = os .path .join (self .users_dir , self .user .name )
278+ course_dirs = [
279+ (os .path .join (home_dir , self .course_dir ), 0o755 )
280+ ]
281+
282+ if self .user .admin :
283+ course_dirs .extend ([
284+ (os .path .join (home_dir , self .course_dir , 'textbook' ), 0o777 ),
285+ (os .path .join (home_dir , self .course_dir , 'info' ), 0o777 )
286+ ])
287+
288+ for dirpath in content_dirs :
289+ self ._make_dir (dirpath , 0o777 , 0 , 0 )
290+ else :
291+ if any ([not os .path .exists (d ) for d in content_dirs ]):
292+ raise web .HTTPError (
293+ 403 ,
294+ 'You are not permitted to create a new course, "%s".' ,
295+ self .course_dir )
296+
297+ statinfo = os .stat (home_dir )
298+ for dirpath , mode in course_dirs :
299+ self ._make_dir (dirpath , mode , statinfo .st_uid , statinfo .st_gid )
300+
301+ def _make_dir (self , dirpath , mode , uid , gid ):
302+ try :
303+ os .mkdir (dirpath , mode )
304+ except FileExistsError :
305+ os .chmod (dirpath , mode )
306+ os .chown (dirpath , uid , gid )
307+
162308 async def create_object (self , * args , ** kwargs ):
309+ server_name = self .name
310+ course_dir = self .course_dir
311+ notebook_dir = self .format_string (self .notebook_dir )
312+ workdir = self .format_string (self .workdir )
313+ self .log .debug (
314+ f"create_object: server_name='{ server_name } '"
315+ f" course_dir='{ course_dir } '"
316+ f" notebook_dir={ notebook_dir } "
317+ f" workdir={ workdir } "
318+ f" image='{ self .image } '" )
319+
320+ self ._make_user_dirs ()
321+ self ._make_user_course_dirs ()
322+
163323 self .docker (
164324 'login' ,
165325 username = self ._registry .username ,
0 commit comments