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 * psycopg 2.0
* simplejson 1.3 * simplejson 1.3
* pytz 2006p * pytz 2006p
* Python Imaging Library 1.1
In Debian GNU/Linux, you can issue the following command to install these In Debian GNU/Linux, you can issue the following command to install these
packages: packages:
apt-get install python2.4 python-cherrypy postgresql-8.1 \ 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 database setup

View File

@ -5,6 +5,8 @@ import time
import urllib import urllib
import tempfile import tempfile
import cherrypy import cherrypy
from PIL import Image
from cStringIO import StringIO
from threading import Lock, Event from threading import Lock, Event
from Expose import expose from Expose import expose
from Validate import validate, Valid_int, Valid_bool, Validation_error 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.Blank_page import Blank_page
from view.Json import Json from view.Json import Json
from view.Progress_bar import stream_progress, stream_quota_error, quota_error_script, general_error_script 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 ): class Access_error( Exception ):
@ -238,9 +241,10 @@ class Files( object ):
@validate( @validate(
file_id = Valid_id(), file_id = Valid_id(),
quote_filename = Valid_bool( none_okay = True ), quote_filename = Valid_bool( none_okay = True ),
preview = Valid_bool( none_okay = True ),
user_id = Valid_id( 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. 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 @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 as UTF-8. IE expects quoting while Firefox doesn't (optional, defaults
to False) 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 @type user_id: unicode or NoneType
@param user_id: id of current logged-in user (if any) @param user_id: id of current logged-in user (if any)
@rtype: unicode @rtype: generator
@return: file data @return: file data
@raise Access_error: the current user doesn't have access to the notebook that the file is in @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 # 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: try:
cherrypy.session.release_lock() cherrypy.session.release_lock()
except KeyError: 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 ): if not db_file or not self.__users.check_access( user_id, db_file.notebook_id ):
raise Access_error() 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 cherrypy.response.headerMap[ u"Content-Type" ] = db_file.content_type
@ -290,6 +304,142 @@ class Files( object ):
return stream() 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 ) @expose( view = Upload_page )
@strongly_expire @strongly_expire
@end_transaction @end_transaction

View File

@ -183,6 +183,24 @@ class Test_files( Test_controller ):
def test_download_with_unicode_unquoted_filename( self ): def test_download_with_unicode_unquoted_filename( self ):
self.test_download( self.unicode_filename, quote_filename = False ) 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 ): def test_download_without_login( self ):
self.login() self.login()
@ -238,6 +256,57 @@ class Test_files( Test_controller ):
assert u"access" in result[ u"body" ][ 0 ] 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 ): def test_upload_page( self ):
self.login() self.login()

View File

@ -643,3 +643,10 @@ img {
width: 40em; width: 40em;
height: 4em; 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 // special case for links to uploaded files
if ( !link.target && /\/files\//.test( link.href ) ) { if ( !link.target && /\/files\//.test( link.href ) ) {
if ( !/\/files\/new$/.test( link.href ) ) if ( !/\/files\/new$/.test( link.href ) ) {
location.href = link.href; window.open( link.href );
return false; event.stop();
}
return true;
} }
event.stop(); event.stop();

View File

@ -2537,15 +2537,26 @@ function File_link_pulldown( wiki, notebook_id, invoker, editor, link ) {
"title": "delete file" "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, createDOM( "span", { "class": "field_label" }, "filename: " ) );
appendChildNodes( this.div, this.filename_field ); appendChildNodes( this.div, this.filename_field );
appendChildNodes( this.div, this.file_size ); appendChildNodes( this.div, this.file_size );
appendChildNodes( this.div, " " ); appendChildNodes( this.div, " " );
appendChildNodes( this.div, delete_button ); 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 // get the file's name and size from the server
this.invoker.invoke( this.invoker.invoke(
"/files/stats", "GET", { "/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 ),
),
),
),
)