1
# IVLE - Informatics Virtual Learning Environment
2
# Copyright (C) 2009 The University of Melbourne
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
# GNU General Public License for more details.
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
18
# Author: William Grant
20
'''Media file support for the framework.'''
28
from ivle.webapp.base.views import BaseView
29
from ivle.webapp.base.plugins import PublicViewPlugin, ViewPlugin, MediaPlugin
30
from ivle.webapp.errors import NotFound, Forbidden
32
def media_url(req, plugin, path):
33
'''Generates a URL to a media file.
35
Plugin can be a string, in which case it is put into the path literally,
36
or a plugin object, in which case its name is looked up.
38
If a version is specified in the IVLE configuration, a versioned URL will
41
if not isinstance(plugin, basestring):
42
plugin = req.config.reverse_plugins[plugin]
44
media_path = os.path.join('+media', '+' + req.config['media']['version']) \
45
if req.config['media']['version'] else '+media'
47
return req.make_path(os.path.join(media_path, plugin, path))
49
class BaseMediaFileView(BaseView):
50
'''A view for media files.
52
This serves static files from directories registered by plugins.
54
Plugins wishing to export media should declare a 'media' attribute,
55
pointing to the directory to serve (relative to the module's directory).
56
The contents of that directory will then be available under
57
/+media/python.path.to.module.
59
def __init__(self, req, ns, path):
63
def _make_filename(self, req):
64
raise NotImplementedError()
66
def render(self, req):
67
# If it begins with ".." or separator, it's illegal. Die.
68
if self.path.startswith("..") or self.path.startswith('/'):
71
filename = self._make_filename(req)
73
# Find an appropriate MIME type.
74
(type, _) = mimetypes.guess_type(filename)
76
type = 'application/octet-stream'
78
# Get out if it is unreadable or a directory.
79
if not os.access(filename, os.F_OK):
81
if not os.access(filename, os.R_OK) or os.path.isdir(filename):
84
req.content_type = type
85
req.sendfile(filename)
88
class MediaFileView(BaseMediaFileView):
89
'''A view for media files.
91
This serves static files from directories registered by plugins.
93
Plugins wishing to export media should declare a 'media' attribute,
94
pointing to the directory to serve (relative to the module's directory).
95
The contents of that directory will then be available under
96
/+media/python.path.to.module.
100
def _make_filename(self, req):
102
plugin = req.config.plugins[self.ns]
106
if not issubclass(plugin, MediaPlugin):
109
mediadir = plugin.media
110
plugindir = os.path.dirname(inspect.getmodule(plugin).__file__)
112
return os.path.join(plugindir, mediadir, self.path)
114
def get_permissions(self, user):
117
class VersionedMediaFileView(MediaFileView):
118
'''A view for versioned media files, with aggressive caching.
120
This serves static media files with a version string, and requests that
121
browsers cache them for a long time.
124
def __init__(self, req, ns, path, version):
125
super(VersionedMediaFileView, self).__init__(req, ns, path)
126
self.version = version
128
def _make_filename(self, req):
129
if self.version != req.config['media']['version']:
132
# Don't expire for a year.
133
req.headers_out['Expires'] = email.utils.formatdate(
134
timeval=time.time() + (60*60*24*365),
137
return super(VersionedMediaFileView, self)._make_filename(req)
140
# This maps a media namespace to an external dependency directory (in this
141
# case specified by the configuration option media/externals/jquery) and a
142
# list of permitted subpaths.
143
EXTERNAL_MEDIA_MAP = {'jquery': ('jquery', ['jquery.js'])}
145
class ExternalMediaFileView(BaseMediaFileView):
146
'''A view for media files from external dependencies.
148
This serves specific static files from external dependencies as defined in
149
the IVLE configuration.
153
def _make_filename(self, req):
155
extern = EXTERNAL_MEDIA_MAP[self.ns]
159
# Unless it's a whitelisted path, we don't want to hear about it.
160
if self.path not in extern[1]:
163
# Grab the admin-configured path for this particular external dep.
164
externdir = req.config['media']['externals'][extern[0]]
166
assert isinstance(externdir, basestring)
168
return os.path.join(externdir, self.path)
170
def get_permissions(self, user):
173
class ExternalVersionedMediaFileView(ExternalMediaFileView):
174
'''A view for versioned media files from external dependencies, with
177
This serves specific static media files from external dependencies with a
178
version string, and requests that browsers cache them for a long time.
181
def __init__(self, req, ns, path, version):
182
super(ExternalVersionedMediaFileView, self).__init__(req, ns, path)
183
self.version = version
185
def _make_filename(self, req):
186
if self.version != req.config['media']['version']:
189
# Don't expire for a year.
190
req.headers_out['Expires'] = email.utils.formatdate(
191
timeval=time.time() + (60*60*24*365),
194
return super(ExternalVersionedMediaFileView, self)._make_filename(req)
197
class Plugin(ViewPlugin, PublicViewPlugin):
199
('+media/+:version/+external/:ns/*path', ExternalVersionedMediaFileView),
200
('+media/+external/:ns/*path', ExternalMediaFileView),
201
('+media/+:version/:ns/*path', VersionedMediaFileView),
202
('+media/:ns/*path', MediaFileView),