##// END OF EJS Templates
Merge pull request #2175 from minrk/staticfile...
Bussonnier Matthias -
r8044:0eec72e0 merge
parent child Browse files
Show More
@@ -0,0 +1,7 b''
1 /*
2 Placeholder for custom user CSS
3
4 mainly to be overridden in profile/static/css/custom.css
5
6 This will always be an empty file in IPython
7 */ No newline at end of file
@@ -0,0 +1,7 b''
1 /*
2 Placeholder for custom user javascript
3
4 mainly to be overridden in profile/static/js/custom.js
5
6 This will always be an empty file in IPython
7 */
@@ -23,18 +23,18 b' import urllib2'
23 import tempfile
23 import tempfile
24 import tarfile
24 import tarfile
25
25
26 from IPython.frontend.html import notebook as nbmod
26 from IPython.utils.path import locate_profile
27
27
28 #-----------------------------------------------------------------------------
28 #-----------------------------------------------------------------------------
29 # Imports
29 # Imports
30 #-----------------------------------------------------------------------------
30 #-----------------------------------------------------------------------------
31
31
32 def install_mathjax(tag='v1.1', replace=False):
32 def install_mathjax(tag='v2.0', replace=False, dest=None):
33 """Download and install MathJax for offline use.
33 """Download and install MathJax for offline use.
34
34
35 This will install mathjax to the 'static' dir in the IPython notebook
35 You can use this to install mathjax to a location on your static file
36 package, so it will fail if the caller does not have write access
36 path. This includes the `static` directory within your IPython profile,
37 to that location.
37 which is the default location for this install.
38
38
39 MathJax is a ~15MB download, and ~150MB installed.
39 MathJax is a ~15MB download, and ~150MB installed.
40
40
@@ -43,23 +43,34 b" def install_mathjax(tag='v1.1', replace=False):"
43
43
44 replace : bool [False]
44 replace : bool [False]
45 Whether to remove and replace an existing install.
45 Whether to remove and replace an existing install.
46 tag : str ['v1.1']
46 tag : str ['v2.0']
47 Which tag to download. Default is 'v1.1', the current stable release,
47 Which tag to download. Default is 'v2.0', the current stable release,
48 but alternatives include 'v1.1a' and 'master'.
48 but alternatives include 'v1.1' and 'master'.
49 dest : path
50 The path to the directory in which mathjax will be installed.
51 The default is `IPYTHONDIR/profile_default/static`.
52 dest must be on your notebook static_path when you run the notebook server.
53 The default location works for this.
49 """
54 """
50 mathjax_url = "https://github.com/mathjax/MathJax/tarball/%s"%tag
51
55
52 nbdir = os.path.dirname(os.path.abspath(nbmod.__file__))
56 mathjax_url = "https://github.com/mathjax/MathJax/tarball/%s" % tag
53 static = os.path.join(nbdir, 'static')
57
58 if dest is None:
59 dest = os.path.join(locate_profile('default'), 'static')
60
61 if not os.path.exists(dest):
62 os.mkdir(dest)
63
64 static = dest
54 dest = os.path.join(static, 'mathjax')
65 dest = os.path.join(static, 'mathjax')
55
66
56 # check for existence and permissions
67 # check for existence and permissions
57 if not os.access(static, os.W_OK):
68 if not os.access(static, os.W_OK):
58 raise IOError("Need have write access to %s"%static)
69 raise IOError("Need have write access to %s" % static)
59 if os.path.exists(dest):
70 if os.path.exists(dest):
60 if replace:
71 if replace:
61 if not os.access(dest, os.W_OK):
72 if not os.access(dest, os.W_OK):
62 raise IOError("Need have write access to %s"%dest)
73 raise IOError("Need have write access to %s" % dest)
63 print "removing previous MathJax install"
74 print "removing previous MathJax install"
64 shutil.rmtree(dest)
75 shutil.rmtree(dest)
65 else:
76 else:
@@ -67,13 +78,13 b" def install_mathjax(tag='v1.1', replace=False):"
67 return
78 return
68
79
69 # download mathjax
80 # download mathjax
70 print "Downloading mathjax source..."
81 print "Downloading mathjax source from %s ..." % mathjax_url
71 response = urllib2.urlopen(mathjax_url)
82 response = urllib2.urlopen(mathjax_url)
72 print "done"
83 print "done"
73 # use 'r|gz' stream mode, because socket file-like objects can't seek:
84 # use 'r|gz' stream mode, because socket file-like objects can't seek:
74 tar = tarfile.open(fileobj=response.fp, mode='r|gz')
85 tar = tarfile.open(fileobj=response.fp, mode='r|gz')
75 topdir = tar.firstmember.path
86 topdir = tar.firstmember.path
76 print "Extracting to %s"%dest
87 print "Extracting to %s" % dest
77 tar.extractall(static)
88 tar.extractall(static)
78 # it will be mathjax-MathJax-<sha>, rename to just mathjax
89 # it will be mathjax-MathJax-<sha>, rename to just mathjax
79 os.rename(os.path.join(static, topdir), dest)
90 os.rename(os.path.join(static, topdir), dest)
@@ -16,8 +16,15 b' Authors:'
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18
18
19 import logging
20 import Cookie
19 import Cookie
20 import datetime
21 import email.utils
22 import hashlib
23 import logging
24 import mimetypes
25 import os
26 import stat
27 import threading
21 import time
28 import time
22 import uuid
29 import uuid
23
30
@@ -31,6 +38,7 b' from IPython.external.decorator import decorator'
31 from IPython.zmq.session import Session
38 from IPython.zmq.session import Session
32 from IPython.lib.security import passwd_check
39 from IPython.lib.security import passwd_check
33 from IPython.utils.jsonutil import date_default
40 from IPython.utils.jsonutil import date_default
41 from IPython.utils.path import filefind
34
42
35 try:
43 try:
36 from docutils.core import publish_string
44 from docutils.core import publish_string
@@ -735,4 +743,178 b' class RSTHandler(AuthenticatedHandler):'
735 self.set_header('Content-Type', 'text/html')
743 self.set_header('Content-Type', 'text/html')
736 self.finish(html)
744 self.finish(html)
737
745
746 # to minimize subclass changes:
747 HTTPError = web.HTTPError
748
749 class FileFindHandler(web.StaticFileHandler):
750 """subclass of StaticFileHandler for serving files from a search path"""
751
752 _static_paths = {}
753 # _lock is needed for tornado < 2.2.0 compat
754 _lock = threading.Lock() # protects _static_hashes
755
756 def initialize(self, path, default_filename=None):
757 if isinstance(path, basestring):
758 path = [path]
759 self.roots = tuple(
760 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in path
761 )
762 self.default_filename = default_filename
763
764 @classmethod
765 def locate_file(cls, path, roots):
766 """locate a file to serve on our static file search path"""
767 with cls._lock:
768 if path in cls._static_paths:
769 return cls._static_paths[path]
770 try:
771 abspath = os.path.abspath(filefind(path, roots))
772 except IOError:
773 # empty string should always give exists=False
774 return ''
775
776 # os.path.abspath strips a trailing /
777 # it needs to be temporarily added back for requests to root/
778 if not (abspath + os.path.sep).startswith(roots):
779 raise HTTPError(403, "%s is not in root static directory", path)
780
781 cls._static_paths[path] = abspath
782 return abspath
783
784 def get(self, path, include_body=True):
785 path = self.parse_url_path(path)
786
787 # begin subclass override
788 abspath = self.locate_file(path, self.roots)
789 # end subclass override
790
791 if os.path.isdir(abspath) and self.default_filename is not None:
792 # need to look at the request.path here for when path is empty
793 # but there is some prefix to the path that was already
794 # trimmed by the routing
795 if not self.request.path.endswith("/"):
796 self.redirect(self.request.path + "/")
797 return
798 abspath = os.path.join(abspath, self.default_filename)
799 if not os.path.exists(abspath):
800 raise HTTPError(404)
801 if not os.path.isfile(abspath):
802 raise HTTPError(403, "%s is not a file", path)
803
804 stat_result = os.stat(abspath)
805 modified = datetime.datetime.fromtimestamp(stat_result[stat.ST_MTIME])
806
807 self.set_header("Last-Modified", modified)
808
809 mime_type, encoding = mimetypes.guess_type(abspath)
810 if mime_type:
811 self.set_header("Content-Type", mime_type)
812
813 cache_time = self.get_cache_time(path, modified, mime_type)
814
815 if cache_time > 0:
816 self.set_header("Expires", datetime.datetime.utcnow() + \
817 datetime.timedelta(seconds=cache_time))
818 self.set_header("Cache-Control", "max-age=" + str(cache_time))
819 else:
820 self.set_header("Cache-Control", "public")
821
822 self.set_extra_headers(path)
823
824 # Check the If-Modified-Since, and don't send the result if the
825 # content has not been modified
826 ims_value = self.request.headers.get("If-Modified-Since")
827 if ims_value is not None:
828 date_tuple = email.utils.parsedate(ims_value)
829 if_since = datetime.datetime.fromtimestamp(time.mktime(date_tuple))
830 if if_since >= modified:
831 self.set_status(304)
832 return
833
834 with open(abspath, "rb") as file:
835 data = file.read()
836 hasher = hashlib.sha1()
837 hasher.update(data)
838 self.set_header("Etag", '"%s"' % hasher.hexdigest())
839 if include_body:
840 self.write(data)
841 else:
842 assert self.request.method == "HEAD"
843 self.set_header("Content-Length", len(data))
844
845 @classmethod
846 def get_version(cls, settings, path):
847 """Generate the version string to be used in static URLs.
848
849 This method may be overridden in subclasses (but note that it
850 is a class method rather than a static method). The default
851 implementation uses a hash of the file's contents.
852
853 ``settings`` is the `Application.settings` dictionary and ``path``
854 is the relative location of the requested asset on the filesystem.
855 The returned value should be a string, or ``None`` if no version
856 could be determined.
857 """
858 # begin subclass override:
859 static_paths = settings['static_path']
860 if isinstance(static_paths, basestring):
861 static_paths = [static_paths]
862 roots = tuple(
863 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in static_paths
864 )
865
866 try:
867 abs_path = filefind(path, roots)
868 except IOError:
869 logging.error("Could not find static file %r", path)
870 return None
871
872 # end subclass override
873
874 with cls._lock:
875 hashes = cls._static_hashes
876 if abs_path not in hashes:
877 try:
878 f = open(abs_path, "rb")
879 hashes[abs_path] = hashlib.md5(f.read()).hexdigest()
880 f.close()
881 except Exception:
882 logging.error("Could not open static file %r", path)
883 hashes[abs_path] = None
884 hsh = hashes.get(abs_path)
885 if hsh:
886 return hsh[:5]
887 return None
888
889
890 # make_static_url and parse_url_path totally unchanged from tornado 2.2.0
891 # but needed for tornado < 2.2.0 compat
892 @classmethod
893 def make_static_url(cls, settings, path):
894 """Constructs a versioned url for the given path.
895
896 This method may be overridden in subclasses (but note that it is
897 a class method rather than an instance method).
898
899 ``settings`` is the `Application.settings` dictionary. ``path``
900 is the static path being requested. The url returned should be
901 relative to the current host.
902 """
903 static_url_prefix = settings.get('static_url_prefix', '/static/')
904 version_hash = cls.get_version(settings, path)
905 if version_hash:
906 return static_url_prefix + path + "?v=" + version_hash
907 return static_url_prefix + path
908
909 def parse_url_path(self, url_path):
910 """Converts a static URL path into a filesystem path.
911
912 ``url_path`` is the path component of the URL with
913 ``static_url_prefix`` removed. The return value should be
914 filesystem path relative to ``static_path``.
915 """
916 if os.path.sep != "/":
917 url_path = url_path.replace("/", os.path.sep)
918 return url_path
919
738
920
@@ -48,7 +48,8 b' from .handlers import (LoginHandler, LogoutHandler,'
48 MainKernelHandler, KernelHandler, KernelActionHandler, IOPubHandler,
48 MainKernelHandler, KernelHandler, KernelActionHandler, IOPubHandler,
49 ShellHandler, NotebookRootHandler, NotebookHandler, NotebookCopyHandler,
49 ShellHandler, NotebookRootHandler, NotebookHandler, NotebookCopyHandler,
50 RSTHandler, AuthenticatedFileHandler, PrintNotebookHandler,
50 RSTHandler, AuthenticatedFileHandler, PrintNotebookHandler,
51 MainClusterHandler, ClusterProfileHandler, ClusterActionHandler
51 MainClusterHandler, ClusterProfileHandler, ClusterActionHandler,
52 FileFindHandler,
52 )
53 )
53 from .notebookmanager import NotebookManager
54 from .notebookmanager import NotebookManager
54 from .clustermanager import ClusterManager
55 from .clustermanager import ClusterManager
@@ -67,6 +68,7 b' from IPython.zmq.ipkernel import ('
67 )
68 )
68 from IPython.utils.traitlets import Dict, Unicode, Integer, List, Enum, Bool
69 from IPython.utils.traitlets import Dict, Unicode, Integer, List, Enum, Bool
69 from IPython.utils import py3compat
70 from IPython.utils import py3compat
71 from IPython.utils.path import filefind
70
72
71 #-----------------------------------------------------------------------------
73 #-----------------------------------------------------------------------------
72 # Module globals
74 # Module globals
@@ -153,7 +155,8 b' class NotebookWebApplication(web.Application):'
153
155
154 settings = dict(
156 settings = dict(
155 template_path=os.path.join(os.path.dirname(__file__), "templates"),
157 template_path=os.path.join(os.path.dirname(__file__), "templates"),
156 static_path=os.path.join(os.path.dirname(__file__), "static"),
158 static_path=ipython_app.static_file_path,
159 static_handler_class = FileFindHandler,
157 cookie_secret=os.urandom(1024),
160 cookie_secret=os.urandom(1024),
158 login_url="%s/login"%(base_project_url.rstrip('/')),
161 login_url="%s/login"%(base_project_url.rstrip('/')),
159 )
162 )
@@ -355,6 +358,20 b' class NotebookApp(BaseIPythonApplication):'
355 websocket_host = Unicode("", config=True,
358 websocket_host = Unicode("", config=True,
356 help="""The hostname for the websocket server."""
359 help="""The hostname for the websocket server."""
357 )
360 )
361
362 extra_static_paths = List(Unicode, config=True,
363 help="""Extra paths to search for serving static files.
364
365 This allows adding javascript/css to be available from the notebook server machine,
366 or overriding individual files in the IPython"""
367 )
368 def _extra_static_paths_default(self):
369 return [os.path.join(self.profile_dir.location, 'static')]
370
371 @property
372 def static_file_path(self):
373 """return extra paths + the default location"""
374 return self.extra_static_paths + [os.path.join(os.path.dirname(__file__), "static")]
358
375
359 mathjax_url = Unicode("", config=True,
376 mathjax_url = Unicode("", config=True,
360 help="""The url for MathJax.js."""
377 help="""The url for MathJax.js."""
@@ -362,13 +379,11 b' class NotebookApp(BaseIPythonApplication):'
362 def _mathjax_url_default(self):
379 def _mathjax_url_default(self):
363 if not self.enable_mathjax:
380 if not self.enable_mathjax:
364 return u''
381 return u''
365 static_path = self.webapp_settings.get("static_path", os.path.join(os.path.dirname(__file__), "static"))
366 static_url_prefix = self.webapp_settings.get("static_url_prefix",
382 static_url_prefix = self.webapp_settings.get("static_url_prefix",
367 "/static/")
383 "/static/")
368 if os.path.exists(os.path.join(static_path, 'mathjax', "MathJax.js")):
384 try:
369 self.log.info("Using local MathJax")
385 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), self.static_file_path)
370 return static_url_prefix+u"mathjax/MathJax.js"
386 except IOError:
371 else:
372 if self.certfile:
387 if self.certfile:
373 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
388 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
374 base = u"https://c328740.ssl.cf1.rackcdn.com"
389 base = u"https://c328740.ssl.cf1.rackcdn.com"
@@ -378,6 +393,9 b' class NotebookApp(BaseIPythonApplication):'
378 url = base + u"/mathjax/latest/MathJax.js"
393 url = base + u"/mathjax/latest/MathJax.js"
379 self.log.info("Using MathJax from CDN: %s", url)
394 self.log.info("Using MathJax from CDN: %s", url)
380 return url
395 return url
396 else:
397 self.log.info("Using local MathJax from %s" % mathjax)
398 return static_url_prefix+u"mathjax/MathJax.js"
381
399
382 def _mathjax_url_changed(self, name, old, new):
400 def _mathjax_url_changed(self, name, old, new):
383 if new and not self.enable_mathjax:
401 if new and not self.enable_mathjax:
@@ -245,6 +245,6 b' data-notebook-id={{notebook_id}}'
245 <script src="{{ static_url("js/tooltip.js") }}" type="text/javascript" charset="utf-8"></script>
245 <script src="{{ static_url("js/tooltip.js") }}" type="text/javascript" charset="utf-8"></script>
246 <script src="{{ static_url("js/notebookmain.js") }}" type="text/javascript" charset="utf-8"></script>
246 <script src="{{ static_url("js/notebookmain.js") }}" type="text/javascript" charset="utf-8"></script>
247
247
248 <script src="{{ static_url("js/contexthint.js") }} charset="utf-8"></script>
248 <script src="{{ static_url("js/contexthint.js") }}" charset="utf-8"></script>
249
249
250 {% end %}
250 {% end %}
@@ -12,6 +12,8 b''
12 <link rel="stylesheet" href="{{static_url("css/page.css") }}" type="text/css"/>
12 <link rel="stylesheet" href="{{static_url("css/page.css") }}" type="text/css"/>
13 {% block stylesheet %}
13 {% block stylesheet %}
14 {% end %}
14 {% end %}
15 <link rel="stylesheet" href="{{ static_url("css/custom.css") }}" type="text/css" />
16
15
17
16 {% block meta %}
18 {% block meta %}
17 {% end %}
19 {% end %}
@@ -53,6 +55,8 b''
53 {% block script %}
55 {% block script %}
54 {% end %}
56 {% end %}
55
57
58 <script src="{{static_url("js/custom.js") }}" type="text/javascript" charset="utf-8"></script>
59
56 </body>
60 </body>
57
61
58 </html>
62 </html>
General Comments 0
You need to be logged in to leave comments. Login now