witten
/
luminotes
Archived
1
0
Fork 0

* 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:
Dan Helfman 2008-04-01 21:54:43 +00:00
parent 276bb9b5bc
commit 03f015f99a
10 changed files with 275 additions and 11 deletions

View File

@ -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

View File

@ -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&quote_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

View File

@ -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()

View File

@ -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

View File

@ -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();

View File

@ -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 + "&quote_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", {

23
view/File_preview_page.py Normal file
View File

@ -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&quote_filename=%s&preview=False" % ( file_id, quote_filename ),
),
Div(
A(
u"download %s" % filename,
href = u"/files/download?file_id=%s&quote_filename=%s&preview=False" % ( file_id, quote_filename ),
),
),
),
)