witten
/
luminotes
Archived
1
0
Fork 0

Converting export features to work as separate plugins. Made exported filenames based on the name of the exported notebook.

This commit is contained in:
Dan Helfman 2009-02-18 23:52:18 -08:00
parent b018abafa2
commit 26722eee93
13 changed files with 324 additions and 123 deletions

7
NEWS
View File

@ -1,5 +1,10 @@
1.6.7: ?
*
* When you export your notebook as an HTML or CSV file, the saved filename is
now based on the name of your notebook, for instance "my-to-do-list.html"
or "ideas-for-my-novel.csv".
* Converted the existing HTML and CSV export features to work as separate
export plugins. This means that a new export format can be implemented as
a new plugin.
1.6.6: February 16, 2009
* Luminotes now recognizes "mailto:" links as external links, so you can

View File

@ -143,7 +143,7 @@ settings = {
"stream_response": True,
"encoding_filter.on": False,
},
"/notebooks/export_csv": {
"/notebooks/export": {
"stream_response": True,
"encoding_filter.on": False,
},

View File

@ -70,18 +70,31 @@ def expose( view = None, rss = None ):
if isinstance( result, types.GeneratorType ):
return result
redirect = result.get( u"redirect", None )
redirect = result.get( u"redirect" )
encoding = result.get( u"manual_encode" )
if encoding:
del( result[ u"manual_encode" ] )
def render( view, result, encoding = None ):
output = unicode( view( **result ) )
if not encoding:
return output
return output.encode( encoding )
# try using the supplied view to render the result
try:
if view_override is None:
if rss and use_rss:
cherrypy.response.headers[ u"Content-Type" ] = u"application/xml"
return unicode( rss( **result ) ).encode( "utf8" )
return render( rss, result, encoding or "utf8" )
elif view:
return unicode( view( **result ) )
return render( view, result, encoding )
elif result.get( "view" ):
result_view = result.get( "view" )
del( result[ "view" ] )
return render( result_view, result, encoding )
else:
return unicode( view_override( **result ) )
return render( view_override, result, encoding )
except:
if redirect is None:
if original_error:

View File

@ -1,8 +1,6 @@
import re
import cgi
import csv
import cherrypy
from cStringIO import StringIO
from datetime import datetime
from Expose import expose
from Validate import validate, Valid_string, Validation_error, Valid_bool, Valid_int
@ -21,7 +19,6 @@ from model.File import File
from model.Tag import Tag
from view.Main_page import Main_page
from view.Json import Json
from view.Html_file import Html_file
from view.Note_tree_area import Note_tree_area
from view.Notebook_rss import Notebook_rss
from view.Updates_rss import Updates_rss
@ -1191,61 +1188,33 @@ class Notebooks( object ):
notes = notes,
)
@expose( view = Html_file )
@weakly_expire
@end_transaction
@grab_user_id
@validate(
notebook_id = Valid_id(),
user_id = Valid_id( none_okay = True ),
)
def export_html( self, notebook_id, user_id ):
"""
Download the entire contents of the given notebook as a stand-alone HTML page (no JavaScript).
@type notebook_id: unicode
@param notebook_id: id of notebook to download
@type user_id: unicode
@param user_id: id of current logged-in user (if any), determined by @grab_user_id
@rtype: unicode
@return: rendered HTML page with appropriate headers to trigger a download
@raise Access_error: the current user doesn't have access to the given notebook
@raise Validation_error: one of the arguments is invalid
"""
notebook = self.__users.load_notebook( user_id, notebook_id )
if not notebook:
raise Access_error()
startup_notes = self.__database.select_many( Note, notebook.sql_load_startup_notes() )
other_notes = self.__database.select_many( Note, notebook.sql_load_non_startup_notes() )
return dict(
notebook_name = notebook.name,
notes = startup_notes + other_notes,
)
@expose()
@weakly_expire
@end_transaction
@grab_user_id
@validate(
notebook_id = Valid_id(),
format = Valid_string( min = 1, max = 100 ),
user_id = Valid_id( none_okay = True ),
)
def export_csv( self, notebook_id, user_id ):
def export( self, notebook_id, format, user_id ):
"""
Download the entire contents of the given notebook as a CSV file.
Download the entire contents of the given notebook as a stand-alone file.
@type notebook_id: unicode
@param notebook_id: id of notebook to download
@type format: unicode
@param format: string indicating the export plugin to use, currently one of: "html", "csv"
@type user_id: unicode
@param user_id: id of current logged-in user (if any), determined by @grab_user_id
@rtype: unicode
@return: CSV file with appropriate headers to trigger a download
@rtype: unicode or generator (for streaming files)
@return: exported file with appropriate headers to trigger a download
@raise Access_error: the current user doesn't have access to the given notebook
@raise Validation_error: one of the arguments is invalid
"""
if format not in ( "html", "csv" ):
raise Access_error()
notebook = self.__users.load_notebook( user_id, notebook_id )
if not notebook:
@ -1253,37 +1222,19 @@ class Notebooks( object ):
startup_notes = self.__database.select_many( Note, notebook.sql_load_startup_notes() )
other_notes = self.__database.select_many( Note, notebook.sql_load_non_startup_notes() )
notes = startup_notes + other_notes
notes = startup_notes + other_notes
buffer = StringIO()
writer = csv.writer( buffer )
import imp
from plugins.Invoke import invoke
cherrypy.response.headerMap[ u"Content-Disposition" ] = u"attachment; filename=wiki.csv"
cherrypy.response.headerMap[ u"Content-Type" ] = u"text/csv;charset=utf-8"
def stream():
writer.writerow( ( u"contents", u"title", u"note_id", u"startup", u"username", u"revision_date" ) )
yield buffer.getvalue()
buffer.truncate( 0 )
for note in notes:
user = None
if note.user_id:
user = self.__database.load( User, note.user_id )
writer.writerow( (
note.contents and note.contents.strip().encode( "utf8" ) or None,
note.title and note.title.strip().encode( "utf8" ) or None,
note.object_id,
note.startup and 1 or 0,
note.user_id and user and user.username and user.username.encode( "utf8" ) or u"",
note.revision,
) )
yield buffer.getvalue()
buffer.truncate( 0 )
return stream()
return invoke(
plugin_type = u"export",
plugin_name = format,
database = self.__database,
notebook = notebook,
notes = startup_notes + other_notes,
response_headers = cherrypy.response.headerMap,
)
@expose( view = Json )
@end_transaction

View File

@ -4478,10 +4478,12 @@ class Test_notebooks( Test_controller ):
self.database.save( note3 )
result = self.http_get(
"/notebooks/export_html/%s" % self.notebook.object_id,
"/notebooks/export?notebook_id=%s&format=html" % self.notebook.object_id,
session_id = self.session_id,
)
assert result.get( "notebook_name" ) == self.notebook.name
assert result.get( "notebook" ).object_id == self.notebook.object_id
assert result.get( "view" )
notes = result.get( "notes" )
assert len( notes ) == self.database.select_one( int, self.notebook.sql_count_notes() )
@ -4517,7 +4519,7 @@ class Test_notebooks( Test_controller ):
note3 = Note.create( "55", u"<h3>blah</h3>foo", notebook_id = self.notebook.object_id )
self.database.save( note3 )
path = "/notebooks/export_html/%s" % self.notebook.object_id
path = "/notebooks/export?notebook_id=%s&format=html" % self.notebook.object_id
result = self.http_get(
path,
session_id = self.session_id,
@ -4537,37 +4539,26 @@ class Test_notebooks( Test_controller ):
assert result.get( "error" )
def test_export_csv( self, note_contents = None ):
def test_export_csv( self ):
self.login()
if not note_contents:
note_contents = u"<h3>blah</h3>foo"
note_contents = u"<h3>blah</h3>foo"
note3 = Note.create( "55", note_contents, notebook_id = self.notebook.object_id )
self.database.save( note3 )
result = self.http_get(
"/notebooks/export_csv/%s" % self.notebook.object_id,
"/notebooks/export?notebook_id=%s&format=csv" % self.notebook.object_id,
session_id = self.session_id,
)
headers = result[ u"headers" ]
assert headers
assert headers[ u"Content-Type" ] == u"text/csv;charset=utf-8"
assert headers[ u"Content-Disposition" ] == 'attachment; filename=wiki.csv'
assert headers[ u"Content-Disposition" ] == 'attachment; filename=%s.csv' % self.notebook.friendly_id
gen = result[ u"body" ]
assert isinstance( gen, types.GeneratorType )
pieces = []
try:
for piece in gen:
pieces.append( piece )
except AttributeError, exc:
if u"session_storage" not in str( exc ):
raise exc
csv_data = "".join( pieces )
assert result[ u"body" ]
csv_data = result[ u"body" ][ 0 ]
reader = csv.reader( StringIO( csv_data ) )
row = reader.next()
@ -4612,29 +4603,11 @@ class Test_notebooks( Test_controller ):
assert note_count == expected_note_count
def test_export_csv_with_unicode( self ):
self.test_export_csv( note_contents = u"<h3>blah</h3>ümlaut.png" )
def test_export_csv_without_note_title( self ):
self.test_export_csv( note_contents = u"there's no title" )
def test_export_csv_with_trailing_newline_in_title( self ):
self.test_export_csv( note_contents = u"<h3>blah\n</h3>foo" )
def test_export_csv_with_trailing_newline_in_contents( self ):
self.test_export_csv( note_contents = u"<h3>blah</h3>foo\n" )
def test_export_csv_with_blank_username( self ):
self.user._User__username = None
self.database.save( self.user )
self.test_export_csv( note_contents = u"<h3>blah</h3>foo" )
def test_export_csv_without_login( self ):
note3 = Note.create( "55", u"<h3>blah</h3>foo", notebook_id = self.notebook.object_id )
self.database.save( note3 )
path = "/notebooks/export_csv/%s" % self.notebook.object_id
path = "/notebooks/export?notebook_id=%s&format=csv" % self.notebook.object_id
result = self.http_get(
path,
session_id = self.session_id,
@ -4648,7 +4621,7 @@ class Test_notebooks( Test_controller ):
self.login()
result = self.http_get(
"/notebooks/export_csv/%s" % self.unknown_notebook_id,
"/notebooks/export?notebook_id=%s&format=csv" % self.unknown_notebook_id,
session_id = self.session_id,
)

10
plugins/Invoke.py Normal file
View File

@ -0,0 +1,10 @@
import imp
import plugins
def invoke( plugin_type, plugin_name, *args, **kwargs ):
plugin_name = u"%s_%s" % ( plugin_type, plugin_name )
plugin_location = imp.find_module( plugin_name, plugins.__path__ )
plugin_module = imp.load_module( plugin_name, *plugin_location )
function = getattr( plugin_module, plugin_type )
return apply( function, args, kwargs )

0
plugins/__init__.py Normal file
View File

View File

@ -0,0 +1,38 @@
import csv
from cStringIO import StringIO
from model.User import User
def export( database, notebook, notes, response_headers ):
"""
Format the given notes as a CSV file and return it as a streaming generator.
"""
buffer = StringIO()
writer = csv.writer( buffer )
response_headers[ u"Content-Disposition" ] = u"attachment; filename=%s.csv" % notebook.friendly_id
response_headers[ u"Content-Type" ] = u"text/csv;charset=utf-8"
def stream():
writer.writerow( ( u"contents", u"title", u"note_id", u"startup", u"username", u"revision_date" ) )
yield buffer.getvalue()
buffer.truncate( 0 )
for note in notes:
user = None
if note.user_id:
user = database.load( User, note.user_id )
writer.writerow( (
note.contents and note.contents.strip().encode( "utf8" ) or None,
note.title and note.title.strip().encode( "utf8" ) or None,
note.object_id,
note.startup and 1 or 0,
note.user_id and user and user.username and user.username.encode( "utf8" ) or u"",
note.revision,
) )
yield buffer.getvalue()
buffer.truncate( 0 )
return stream()

View File

@ -0,0 +1,122 @@
# -*- coding: utf8 -*-
import csv
import types
import cherrypy
from cStringIO import StringIO
from pysqlite2 import dbapi2 as sqlite
from model.User import User
from model.Note import Note
from model.Notebook import Notebook
from controller.Database import Database, Connection_wrapper
from controller.test.Stub_cache import Stub_cache
from plugins.Invoke import invoke
class Test_export_csv( object ):
def setUp( self ):
self.database = Database(
Connection_wrapper( sqlite.connect( ":memory:", detect_types = sqlite.PARSE_DECLTYPES, check_same_thread = False ) ),
cache = Stub_cache(),
)
self.database.execute_script( file( "model/schema.sqlite" ).read(), commit = True )
self.username = u"mulder"
self.password = u"trustno1"
self.email_address = u"outthere@example.com"
self.user = User.create( self.database.next_id( User ), self.username, self.password, self.email_address )
self.database.save( self.user, commit = False )
self.trash = Notebook.create( self.database.next_id( Notebook ), u"trash" )
self.database.save( self.trash, commit = False )
self.notebook = Notebook.create( self.database.next_id( Notebook ), u"notebook", self.trash.object_id, user_id = self.user.object_id )
self.database.save( self.notebook, commit = False )
note_id = self.database.next_id( Note )
self.note1 = Note.create( note_id, u"<h3>my title</h3>blah", notebook_id = self.notebook.object_id, startup = True, user_id = self.user.object_id )
self.database.save( self.note1, commit = False )
note_id = self.database.next_id( Note )
self.note2 = Note.create( note_id, u"<h3>other title</h3>whee", notebook_id = self.notebook.object_id, user_id = self.user.object_id )
self.database.save( self.note2, commit = False )
def test_export_csv( self, note_contents = None ):
if not note_contents:
note_contents = u"<h3>blah</h3>foo"
note3 = Note.create( self.database.next_id( Note ), note_contents, notebook_id = self.notebook.object_id, user_id = self.user.object_id )
self.database.save( note3 )
response_headers = {}
expected_notes = ( self.note1, self.note2, note3 )
result = invoke(
"export",
"csv",
self.database,
self.notebook,
expected_notes,
response_headers,
)
assert response_headers
assert response_headers[ u"Content-Type" ] == u"text/csv;charset=utf-8"
assert response_headers[ u"Content-Disposition" ] == 'attachment; filename=%s.csv' % self.notebook.friendly_id
assert isinstance( result, types.GeneratorType )
pieces = []
for piece in result:
pieces.append( piece )
csv_data = "".join( pieces )
reader = csv.reader( StringIO( csv_data ) )
row = reader.next()
expected_header = [ u"contents", u"title", u"note_id", u"startup", u"username", u"revision_date" ]
assert row == expected_header
note_count = 0
# assert that startup notes come first, then normal notes in descending revision order
for row in reader:
assert len( row ) == len( expected_header )
( contents, title, note_id, startup, username, revision_date ) = row
assert note_count < len( expected_notes )
expected_note = expected_notes[ note_count ]
assert expected_note
assert contents.decode( "utf8" ) == expected_note.contents.strip()
if expected_note.title:
assert title.decode( "utf8" ) == expected_note.title.strip()
else:
assert not title
assert note_id.decode( "utf8" ) == expected_note.object_id
assert startup.decode( "utf8" ) == expected_note.startup and u"1" or "0"
assert username.decode( "utf8" ) == ( expected_note.user_id and self.user.username or u"" )
assert revision_date.decode( "utf8" ) == unicode( expected_note.revision )
note_count += 1
assert note_count == len( expected_notes )
def test_export_csv_with_unicode( self ):
self.test_export_csv( note_contents = u"<h3>blah</h3>ümlaut.png" )
def test_export_csv_without_note_title( self ):
self.test_export_csv( note_contents = u"there's no title" )
def test_export_csv_with_trailing_newline_in_title( self ):
self.test_export_csv( note_contents = u"<h3>blah\n</h3>foo" )
def test_export_csv_with_trailing_newline_in_contents( self ):
self.test_export_csv( note_contents = u"<h3>blah</h3>foo\n" )
def test_export_csv_with_blank_username( self ):
self.user._User__username = None
self.database.save( self.user )
self.test_export_csv( note_contents = u"<h3>blah</h3>foo" )

View File

@ -1,13 +1,13 @@
import re
import cherrypy
from Tags import Html, Head, Title, Style, Meta, Body, H1, Div, Span, Hr, A
from view.Tags import Html, Head, Title, Style, Meta, Body, H1, Div, Span, Hr, A
class Html_file( Html ):
NOTE_LINK_PATTERN = re.compile( u'<a\s+href="[^"]*(?:\/notebooks\/)?[^>]+[?&]note_id=([a-z0-9]*)"[^>]*>', re.IGNORECASE )
IMAGE_PATTERN = re.compile( u'<img [^>]* ?/?>', re.IGNORECASE )
def __init__( self, notebook_name, notes ):
def __init__( self, notebook, notes ):
relinked_notes = {} # map from note id to relinked note contents
# relink all note links so they point to named anchors within the page. also, for now, remove all
@ -17,18 +17,18 @@ class Html_file( Html ):
contents = self.IMAGE_PATTERN.sub( '', contents )
relinked_notes[ note.object_id ] = contents
cherrypy.response.headerMap[ u"Content-Disposition" ] = u"attachment; filename=wiki.html"
cherrypy.response.headerMap[ u"Content-Disposition" ] = u"attachment; filename=%s.html" % notebook.friendly_id
Html.__init__(
self,
Head(
Style( file( u"static/css/download.css" ).read(), type = u"text/css" ),
Meta( content = u"text/html; charset=UTF-8", http_equiv = u"content-type" ),
Title( notebook_name ),
Title( notebook.name ),
),
Body(
Div(
H1( notebook_name ),
H1( notebook.name ),
[ Span(
A( name = u"note_%s" % note.object_id ),
Div(

View File

@ -0,0 +1,14 @@
from Html_file import Html_file
def export( database, notebook, notes, response_headers ):
"""
Format the given notes as an HTML file by relying on controller.Expose.expose() to use Html_file
as the view.
"""
return dict(
notebook = notebook,
notes = notes,
view = Html_file,
manual_encode = u"utf8",
)

View File

@ -0,0 +1,75 @@
# -*- coding: utf8 -*-
import types
import cherrypy
from cStringIO import StringIO
from pysqlite2 import dbapi2 as sqlite
from model.User import User
from model.Note import Note
from model.Notebook import Notebook
from controller.Database import Database, Connection_wrapper
from controller.test.Stub_cache import Stub_cache
from plugins.Invoke import invoke
class Test_export_html( object ):
def setUp( self ):
self.database = Database(
Connection_wrapper( sqlite.connect( ":memory:", detect_types = sqlite.PARSE_DECLTYPES, check_same_thread = False ) ),
cache = Stub_cache(),
)
self.database.execute_script( file( "model/schema.sqlite" ).read(), commit = True )
self.username = u"mulder"
self.password = u"trustno1"
self.email_address = u"outthere@example.com"
self.user = User.create( self.database.next_id( User ), self.username, self.password, self.email_address )
self.database.save( self.user, commit = False )
self.trash = Notebook.create( self.database.next_id( Notebook ), u"trash" )
self.database.save( self.trash, commit = False )
self.notebook = Notebook.create( self.database.next_id( Notebook ), u"notebook", self.trash.object_id, user_id = self.user.object_id )
self.database.save( self.notebook, commit = False )
note_id = self.database.next_id( Note )
self.note1 = Note.create( note_id, u"<h3>my title</h3>blah", notebook_id = self.notebook.object_id, startup = True, user_id = self.user.object_id )
self.database.save( self.note1, commit = False )
note_id = self.database.next_id( Note )
self.note2 = Note.create( note_id, u"<h3>other title</h3>whee", notebook_id = self.notebook.object_id, user_id = self.user.object_id )
self.database.save( self.note2, commit = False )
def test_export_html( self ):
note3 = Note.create( "55", u"<h3>blah</h3>foo", notebook_id = self.notebook.object_id )
self.database.save( note3 )
response_headers = {}
expected_notes = ( self.note1, self.note2, note3 )
result = invoke(
"export",
"html",
self.database,
self.notebook,
expected_notes,
response_headers,
)
# response headers should be unchanged
assert response_headers == {}
notes = result.get( "notes" )
assert len( notes ) == len( expected_notes )
# assert that the notes are in the expected order
for ( note, expected_note ) in zip( notes, expected_notes ):
assert note.object_id == expected_note.object_id
assert note.revision == expected_note.revision
assert note.title == expected_note.title
assert note.contents == expected_note.contents
assert note.notebook_id == expected_note.notebook_id
assert note.startup == expected_note.startup
assert note.deleted_from_id == expected_note.deleted_from_id
assert note.rank == expected_note.rank
assert note.user_id == expected_note.user_id
assert note.creation == expected_note.creation

View File

@ -3695,14 +3695,14 @@ function Export_pulldown( wiki, notebook_id, invoker, anchor ) {
this.invoker = invoker;
this.html_link = createDOM( "a", {
"href": "/notebooks/export_html/" + notebook_id,
"href": "/notebooks/export?notebook_id=" + notebook_id + "&format=html",
"class": "pulldown_label",
"title": "Download this notebook as a stand-alone HTML web page."
},
"HTML web page"
);
this.csv_link = createDOM( "a", {
"href": "/notebooks/export_csv/" + notebook_id,
"href": "/notebooks/export?notebook_id=" + notebook_id + "&format=csv",
"class": "pulldown_label",
"title": "Download this notebook as a CSV spreadsheet file."
},