* Propsetting a bunch of svn:ignores.
* Added a bunch of thumbnail-related methods to controller.Files. * Modified Files.download() method to redirect to image preview if requested. * Implemented image preview to popup full image in a separate window. * Added empty stubs for relevant unit tests. Still to-do. * Added new dependency on python-imaging package (PIL). * Updated file info popup to include clickable thumbnail.
This commit is contained in:
parent
276bb9b5bc
commit
03f015f99a
4
INSTALL
4
INSTALL
|
@ -10,12 +10,14 @@ First, install the prerequisites:
|
|||
* psycopg 2.0
|
||||
* simplejson 1.3
|
||||
* pytz 2006p
|
||||
* Python Imaging Library 1.1
|
||||
|
||||
In Debian GNU/Linux, you can issue the following command to install these
|
||||
packages:
|
||||
|
||||
apt-get install python2.4 python-cherrypy postgresql-8.1 \
|
||||
postgresql-contrib-8.1 python-psycopg2 python-simplejson python-tz
|
||||
postgresql-contrib-8.1 python-psycopg2 python-simplejson \
|
||||
python-tz python-imaging
|
||||
|
||||
|
||||
database setup
|
||||
|
|
|
@ -5,6 +5,8 @@ import time
|
|||
import urllib
|
||||
import tempfile
|
||||
import cherrypy
|
||||
from PIL import Image
|
||||
from cStringIO import StringIO
|
||||
from threading import Lock, Event
|
||||
from Expose import expose
|
||||
from Validate import validate, Valid_int, Valid_bool, Validation_error
|
||||
|
@ -17,6 +19,7 @@ from view.Upload_page import Upload_page
|
|||
from view.Blank_page import Blank_page
|
||||
from view.Json import Json
|
||||
from view.Progress_bar import stream_progress, stream_quota_error, quota_error_script, general_error_script
|
||||
from view.File_preview_page import File_preview_page
|
||||
|
||||
|
||||
class Access_error( Exception ):
|
||||
|
@ -238,9 +241,10 @@ class Files( object ):
|
|||
@validate(
|
||||
file_id = Valid_id(),
|
||||
quote_filename = Valid_bool( none_okay = True ),
|
||||
preview = Valid_bool( none_okay = True ),
|
||||
user_id = Valid_id( none_okay = True ),
|
||||
)
|
||||
def download( self, file_id, quote_filename = False, user_id = None ):
|
||||
def download( self, file_id, quote_filename = False, preview = True, user_id = None ):
|
||||
"""
|
||||
Return the contents of file that a user has previously uploaded.
|
||||
|
||||
|
@ -250,14 +254,17 @@ class Files( object ):
|
|||
@param quote_filename: True to URL quote the filename of the downloaded file, False to leave it
|
||||
as UTF-8. IE expects quoting while Firefox doesn't (optional, defaults
|
||||
to False)
|
||||
@type preview: bool
|
||||
@param preview: True to redirect to a preview page if the file is a valid image, False to
|
||||
unconditionally initiate a download
|
||||
@type user_id: unicode or NoneType
|
||||
@param user_id: id of current logged-in user (if any)
|
||||
@rtype: unicode
|
||||
@rtype: generator
|
||||
@return: file data
|
||||
@raise Access_error: the current user doesn't have access to the notebook that the file is in
|
||||
"""
|
||||
# release the session lock before beginning to stream the download. otherwise, if the
|
||||
# upload is cancelled before it's done, the lock won't be released
|
||||
# download is cancelled before it's done, the lock won't be released
|
||||
try:
|
||||
cherrypy.session.release_lock()
|
||||
except KeyError:
|
||||
|
@ -268,7 +275,14 @@ class Files( object ):
|
|||
if not db_file or not self.__users.check_access( user_id, db_file.notebook_id ):
|
||||
raise Access_error()
|
||||
|
||||
db_file = self.__database.load( File, file_id )
|
||||
# if the file is openable as an image, then allow the user to view it instead of downloading it
|
||||
if preview:
|
||||
server_filename = Upload_file.make_server_filename( file_id )
|
||||
try:
|
||||
Image.open( server_filename )
|
||||
return dict( redirect = u"/files/preview?file_id=%s"e_filename=%s" % ( file_id, quote_filename ) )
|
||||
except IOError:
|
||||
pass
|
||||
|
||||
cherrypy.response.headerMap[ u"Content-Type" ] = db_file.content_type
|
||||
|
||||
|
@ -290,6 +304,142 @@ class Files( object ):
|
|||
|
||||
return stream()
|
||||
|
||||
@expose( view = File_preview_page )
|
||||
@end_transaction
|
||||
@grab_user_id
|
||||
@validate(
|
||||
file_id = Valid_id(),
|
||||
quote_filename = Valid_bool( none_okay = True ),
|
||||
user_id = Valid_id( none_okay = True ),
|
||||
)
|
||||
def preview( self, file_id, quote_filename = False, user_id = None ):
|
||||
"""
|
||||
Return the contents of file that a user has previously uploaded.
|
||||
|
||||
@type file_id: unicode
|
||||
@param file_id: id of the file to view
|
||||
@type quote_filename: bool
|
||||
@param quote_filename: quote_filename value to include in download URL
|
||||
@type user_id: unicode or NoneType
|
||||
@param user_id: id of current logged-in user (if any)
|
||||
@rtype: unicode
|
||||
@return: file data
|
||||
@raise Access_error: the current user doesn't have access to the notebook that the file is in
|
||||
"""
|
||||
db_file = self.__database.load( File, file_id )
|
||||
|
||||
if not db_file or not self.__users.check_access( user_id, db_file.notebook_id ):
|
||||
raise Access_error()
|
||||
|
||||
filename = db_file.filename.replace( '"', r"\"" ).encode( "utf8" )
|
||||
|
||||
return dict(
|
||||
file_id = file_id,
|
||||
filename = filename,
|
||||
quote_filename = quote_filename,
|
||||
)
|
||||
|
||||
@expose()
|
||||
@end_transaction
|
||||
@grab_user_id
|
||||
@validate(
|
||||
file_id = Valid_id(),
|
||||
user_id = Valid_id( none_okay = True ),
|
||||
)
|
||||
def thumbnail( self, file_id, user_id = None ):
|
||||
"""
|
||||
Return a thumbnail for a file that a user has previously uploaded. If a thumbnail cannot be
|
||||
generated for the given file, return a default thumbnail image.
|
||||
|
||||
@type file_id: unicode
|
||||
@param file_id: id of the file to return a thumbnail for
|
||||
@type user_id: unicode or NoneType
|
||||
@param user_id: id of current logged-in user (if any)
|
||||
@rtype: generator
|
||||
@return: thumbnail image data
|
||||
@raise Access_error: the current user doesn't have access to the notebook that the file is in
|
||||
"""
|
||||
try:
|
||||
cherrypy.session.release_lock()
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
db_file = self.__database.load( File, file_id )
|
||||
|
||||
if not db_file or not self.__users.check_access( user_id, db_file.notebook_id ):
|
||||
raise Access_error()
|
||||
|
||||
cherrypy.response.headerMap[ u"Content-Type" ] = u"image/png"
|
||||
|
||||
# attempt to open the file as an image
|
||||
server_filename = Upload_file.make_server_filename( file_id )
|
||||
try:
|
||||
image = Image.open( server_filename )
|
||||
|
||||
# scale the image down into a thumbnail
|
||||
THUMBNAIL_MAX_SIZE = ( 75, 75 ) # in pixels
|
||||
image.thumbnail( THUMBNAIL_MAX_SIZE, Image.ANTIALIAS )
|
||||
except IOError:
|
||||
image = Image.open( "static/images/default_thumbnail.png" )
|
||||
|
||||
# save the image into a memory buffer
|
||||
image_buffer = StringIO()
|
||||
image.save( image_buffer, "PNG" )
|
||||
image_buffer.seek( 0 )
|
||||
|
||||
def stream( image_buffer ):
|
||||
CHUNK_SIZE = 8192
|
||||
|
||||
while True:
|
||||
data = image_buffer.read( CHUNK_SIZE )
|
||||
if len( data ) == 0: break
|
||||
yield data
|
||||
|
||||
return stream( image_buffer )
|
||||
|
||||
@expose()
|
||||
@end_transaction
|
||||
@grab_user_id
|
||||
@validate(
|
||||
file_id = Valid_id(),
|
||||
user_id = Valid_id( none_okay = True ),
|
||||
)
|
||||
def image( self, file_id, user_id = None ):
|
||||
"""
|
||||
Return the contents of an image file that a user has previously uploaded. This is distinct
|
||||
from the download() method above in that it doesn't set HTTP headers for a file download.
|
||||
|
||||
@type file_id: unicode
|
||||
@param file_id: id of the file to return
|
||||
@type user_id: unicode or NoneType
|
||||
@param user_id: id of current logged-in user (if any)
|
||||
@rtype: generator
|
||||
@return: image data
|
||||
@raise Access_error: the current user doesn't have access to the notebook that the file is in
|
||||
"""
|
||||
try:
|
||||
cherrypy.session.release_lock()
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
db_file = self.__database.load( File, file_id )
|
||||
|
||||
if not db_file or not self.__users.check_access( user_id, db_file.notebook_id ):
|
||||
raise Access_error()
|
||||
|
||||
cherrypy.response.headerMap[ u"Content-Type" ] = db_file.content_type
|
||||
|
||||
def stream():
|
||||
CHUNK_SIZE = 8192
|
||||
local_file = Upload_file.open_file( file_id )
|
||||
|
||||
while True:
|
||||
data = local_file.read( CHUNK_SIZE )
|
||||
if len( data ) == 0: break
|
||||
yield data
|
||||
|
||||
return stream()
|
||||
|
||||
@expose( view = Upload_page )
|
||||
@strongly_expire
|
||||
@end_transaction
|
||||
|
|
|
@ -183,6 +183,24 @@ class Test_files( Test_controller ):
|
|||
def test_download_with_unicode_unquoted_filename( self ):
|
||||
self.test_download( self.unicode_filename, quote_filename = False )
|
||||
|
||||
def test_download_image_with_preview_none( self ):
|
||||
raise NotImplementedError()
|
||||
|
||||
def test_download_image_with_preview_true( self ):
|
||||
raise NotImplementedError()
|
||||
|
||||
def test_download_image_with_preview_false( self ):
|
||||
raise NotImplementedError()
|
||||
|
||||
def test_download_non_image_with_preview_none( self ):
|
||||
raise NotImplementedError()
|
||||
|
||||
def test_download_non_image_with_preview_true( self ):
|
||||
raise NotImplementedError()
|
||||
|
||||
def test_download_non_image_with_preview_false( self ):
|
||||
raise NotImplementedError()
|
||||
|
||||
def test_download_without_login( self ):
|
||||
self.login()
|
||||
|
||||
|
@ -238,6 +256,57 @@ class Test_files( Test_controller ):
|
|||
|
||||
assert u"access" in result[ u"body" ][ 0 ]
|
||||
|
||||
def test_preview( self ):
|
||||
raise NotImplementedError()
|
||||
|
||||
def test_preview_with_unicode_filename( self ):
|
||||
raise NotImplementedError()
|
||||
|
||||
def test_preview_with_quote_filename_true( self ):
|
||||
raise NotImplementedError()
|
||||
|
||||
def test_preview_with_quote_filename_false( self ):
|
||||
raise NotImplementedError()
|
||||
|
||||
def test_preview_without_login( self ):
|
||||
raise NotImplementedError()
|
||||
|
||||
def test_preview_without_access( self ):
|
||||
raise NotImplementedError()
|
||||
|
||||
def test_preview_with_unknown_file_id( self ):
|
||||
raise NotImplementedError()
|
||||
|
||||
def test_thumbnail( self ):
|
||||
raise NotImplementedError()
|
||||
|
||||
def test_thumbnail_with_non_image( self ):
|
||||
raise NotImplementedError()
|
||||
|
||||
def test_thumbnail_without_login( self ):
|
||||
raise NotImplementedError()
|
||||
|
||||
def test_thumbnail_without_access( self ):
|
||||
raise NotImplementedError()
|
||||
|
||||
def test_thumbnail_with_unknown_file_id( self ):
|
||||
raise NotImplementedError()
|
||||
|
||||
def test_image( self ):
|
||||
raise NotImplementedError()
|
||||
|
||||
def test_image_with_non_image( self ):
|
||||
raise NotImplementedError()
|
||||
|
||||
def test_image_without_login( self ):
|
||||
raise NotImplementedError()
|
||||
|
||||
def test_image_without_access( self ):
|
||||
raise NotImplementedError()
|
||||
|
||||
def test_image_with_unknown_file_id( self ):
|
||||
raise NotImplementedError()
|
||||
|
||||
def test_upload_page( self ):
|
||||
self.login()
|
||||
|
||||
|
|
|
@ -643,3 +643,10 @@ img {
|
|||
width: 40em;
|
||||
height: 4em;
|
||||
}
|
||||
|
||||
.file_thumbnail {
|
||||
margin-right: 0.5em;
|
||||
vertical-align: top;
|
||||
float: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 872 B |
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 4.7 KiB |
|
@ -379,9 +379,11 @@ Editor.prototype.mouse_clicked = function ( event ) {
|
|||
|
||||
// special case for links to uploaded files
|
||||
if ( !link.target && /\/files\//.test( link.href ) ) {
|
||||
if ( !/\/files\/new$/.test( link.href ) )
|
||||
location.href = link.href;
|
||||
return false;
|
||||
if ( !/\/files\/new$/.test( link.href ) ) {
|
||||
window.open( link.href );
|
||||
event.stop();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
event.stop();
|
||||
|
|
|
@ -2537,15 +2537,26 @@ function File_link_pulldown( wiki, notebook_id, invoker, editor, link ) {
|
|||
"title": "delete file"
|
||||
} );
|
||||
|
||||
var query = parse_query( link );
|
||||
this.file_id = query.file_id;
|
||||
|
||||
if ( /MSIE/.test( navigator.userAgent ) )
|
||||
var quote_filename = true;
|
||||
else
|
||||
var quote_filename = false;
|
||||
|
||||
appendChildNodes( this.div, createDOM( "span", {},
|
||||
createDOM( "a", { href: "/files/download?file_id=" + this.file_id + ""e_filename=" + quote_filename, target: "_new" },
|
||||
createDOM( "img", { "src": "/files/thumbnail?file_id=" + this.file_id, "class": "file_thumbnail" } )
|
||||
)
|
||||
) );
|
||||
|
||||
appendChildNodes( this.div, createDOM( "span", { "class": "field_label" }, "filename: " ) );
|
||||
appendChildNodes( this.div, this.filename_field );
|
||||
appendChildNodes( this.div, this.file_size );
|
||||
appendChildNodes( this.div, " " );
|
||||
appendChildNodes( this.div, delete_button );
|
||||
|
||||
var query = parse_query( link );
|
||||
this.file_id = query.file_id;
|
||||
|
||||
// get the file's name and size from the server
|
||||
this.invoker.invoke(
|
||||
"/files/stats", "GET", {
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
from Tags import Html, Head, Title, Body, Img, Div, A
|
||||
|
||||
|
||||
class File_preview_page( Html ):
|
||||
def __init__( self, file_id, filename, quote_filename ):
|
||||
Html.__init__(
|
||||
self,
|
||||
Head(
|
||||
Title( filename ),
|
||||
),
|
||||
Body(
|
||||
A(
|
||||
Img( src = u"/files/image?file_id=%s" % file_id, style = "border: 0;" ),
|
||||
href = u"/files/download?file_id=%s"e_filename=%s&preview=False" % ( file_id, quote_filename ),
|
||||
),
|
||||
Div(
|
||||
A(
|
||||
u"download %s" % filename,
|
||||
href = u"/files/download?file_id=%s"e_filename=%s&preview=False" % ( file_id, quote_filename ),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
Reference in New Issue