##// 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 23 import tempfile
24 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 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 33 """Download and install MathJax for offline use.
34 34
35 This will install mathjax to the 'static' dir in the IPython notebook
36 package, so it will fail if the caller does not have write access
37 to that location.
35 You can use this to install mathjax to a location on your static file
36 path. This includes the `static` directory within your IPython profile,
37 which is the default location for this install.
38 38
39 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 44 replace : bool [False]
45 45 Whether to remove and replace an existing install.
46 tag : str ['v1.1']
47 Which tag to download. Default is 'v1.1', the current stable release,
48 but alternatives include 'v1.1a' and 'master'.
46 tag : str ['v2.0']
47 Which tag to download. Default is 'v2.0', the current stable release,
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__))
53 static = os.path.join(nbdir, 'static')
56 mathjax_url = "https://github.com/mathjax/MathJax/tarball/%s" % tag
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 65 dest = os.path.join(static, 'mathjax')
55 66
56 67 # check for existence and permissions
57 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 70 if os.path.exists(dest):
60 71 if replace:
61 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 74 print "removing previous MathJax install"
64 75 shutil.rmtree(dest)
65 76 else:
@@ -67,13 +78,13 b" def install_mathjax(tag='v1.1', replace=False):"
67 78 return
68 79
69 80 # download mathjax
70 print "Downloading mathjax source..."
81 print "Downloading mathjax source from %s ..." % mathjax_url
71 82 response = urllib2.urlopen(mathjax_url)
72 83 print "done"
73 84 # use 'r|gz' stream mode, because socket file-like objects can't seek:
74 85 tar = tarfile.open(fileobj=response.fp, mode='r|gz')
75 86 topdir = tar.firstmember.path
76 print "Extracting to %s"%dest
87 print "Extracting to %s" % dest
77 88 tar.extractall(static)
78 89 # it will be mathjax-MathJax-<sha>, rename to just mathjax
79 90 os.rename(os.path.join(static, topdir), dest)
@@ -16,8 +16,15 b' Authors:'
16 16 # Imports
17 17 #-----------------------------------------------------------------------------
18 18
19 import logging
20 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 28 import time
22 29 import uuid
23 30
@@ -31,6 +38,7 b' from IPython.external.decorator import decorator'
31 38 from IPython.zmq.session import Session
32 39 from IPython.lib.security import passwd_check
33 40 from IPython.utils.jsonutil import date_default
41 from IPython.utils.path import filefind
34 42
35 43 try:
36 44 from docutils.core import publish_string
@@ -735,4 +743,178 b' class RSTHandler(AuthenticatedHandler):'
735 743 self.set_header('Content-Type', 'text/html')
736 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 48 MainKernelHandler, KernelHandler, KernelActionHandler, IOPubHandler,
49 49 ShellHandler, NotebookRootHandler, NotebookHandler, NotebookCopyHandler,
50 50 RSTHandler, AuthenticatedFileHandler, PrintNotebookHandler,
51 MainClusterHandler, ClusterProfileHandler, ClusterActionHandler
51 MainClusterHandler, ClusterProfileHandler, ClusterActionHandler,
52 FileFindHandler,
52 53 )
53 54 from .notebookmanager import NotebookManager
54 55 from .clustermanager import ClusterManager
@@ -67,6 +68,7 b' from IPython.zmq.ipkernel import ('
67 68 )
68 69 from IPython.utils.traitlets import Dict, Unicode, Integer, List, Enum, Bool
69 70 from IPython.utils import py3compat
71 from IPython.utils.path import filefind
70 72
71 73 #-----------------------------------------------------------------------------
72 74 # Module globals
@@ -153,7 +155,8 b' class NotebookWebApplication(web.Application):'
153 155
154 156 settings = dict(
155 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 160 cookie_secret=os.urandom(1024),
158 161 login_url="%s/login"%(base_project_url.rstrip('/')),
159 162 )
@@ -355,6 +358,20 b' class NotebookApp(BaseIPythonApplication):'
355 358 websocket_host = Unicode("", config=True,
356 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 376 mathjax_url = Unicode("", config=True,
360 377 help="""The url for MathJax.js."""
@@ -362,13 +379,11 b' class NotebookApp(BaseIPythonApplication):'
362 379 def _mathjax_url_default(self):
363 380 if not self.enable_mathjax:
364 381 return u''
365 static_path = self.webapp_settings.get("static_path", os.path.join(os.path.dirname(__file__), "static"))
366 382 static_url_prefix = self.webapp_settings.get("static_url_prefix",
367 383 "/static/")
368 if os.path.exists(os.path.join(static_path, 'mathjax', "MathJax.js")):
369 self.log.info("Using local MathJax")
370 return static_url_prefix+u"mathjax/MathJax.js"
371 else:
384 try:
385 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), self.static_file_path)
386 except IOError:
372 387 if self.certfile:
373 388 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
374 389 base = u"https://c328740.ssl.cf1.rackcdn.com"
@@ -378,6 +393,9 b' class NotebookApp(BaseIPythonApplication):'
378 393 url = base + u"/mathjax/latest/MathJax.js"
379 394 self.log.info("Using MathJax from CDN: %s", url)
380 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 400 def _mathjax_url_changed(self, name, old, new):
383 401 if new and not self.enable_mathjax:
@@ -245,6 +245,6 b' data-notebook-id={{notebook_id}}'
245 245 <script src="{{ static_url("js/tooltip.js") }}" type="text/javascript" charset="utf-8"></script>
246 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 250 {% end %}
@@ -12,6 +12,8 b''
12 12 <link rel="stylesheet" href="{{static_url("css/page.css") }}" type="text/css"/>
13 13 {% block stylesheet %}
14 14 {% end %}
15 <link rel="stylesheet" href="{{ static_url("css/custom.css") }}" type="text/css" />
16
15 17
16 18 {% block meta %}
17 19 {% end %}
@@ -53,6 +55,8 b''
53 55 {% block script %}
54 56 {% end %}
55 57
58 <script src="{{static_url("js/custom.js") }}" type="text/javascript" charset="utf-8"></script>
59
56 60 </body>
57 61
58 62 </html>
General Comments 0
You need to be logged in to leave comments. Login now