Refactored the way note summaries are created in the link pulldown window.
This commit is contained in:
parent
45f94aa188
commit
0152b49475
|
@ -1,3 +1,4 @@
|
||||||
|
import re
|
||||||
import cherrypy
|
import cherrypy
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from Expose import expose
|
from Expose import expose
|
||||||
|
@ -29,6 +30,8 @@ class Access_error( Exception ):
|
||||||
|
|
||||||
|
|
||||||
class Notebooks( object ):
|
class Notebooks( object ):
|
||||||
|
WHITESPACE_PATTERN = re.compile( u"\s+" )
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Controller for dealing with notebooks and their notes, corresponding to the "/notebooks" URL.
|
Controller for dealing with notebooks and their notes, corresponding to the "/notebooks" URL.
|
||||||
"""
|
"""
|
||||||
|
@ -161,9 +164,10 @@ class Notebooks( object ):
|
||||||
notebook_id = Valid_id(),
|
notebook_id = Valid_id(),
|
||||||
note_id = Valid_id(),
|
note_id = Valid_id(),
|
||||||
revision = Valid_revision(),
|
revision = Valid_revision(),
|
||||||
|
summarize = Valid_bool(),
|
||||||
user_id = Valid_id( none_okay = True ),
|
user_id = Valid_id( none_okay = True ),
|
||||||
)
|
)
|
||||||
def load_note( self, notebook_id, note_id, revision = None, user_id = None ):
|
def load_note( self, notebook_id, note_id, revision = None, summarize = False, user_id = None ):
|
||||||
"""
|
"""
|
||||||
Return the information on a particular note by its id.
|
Return the information on a particular note by its id.
|
||||||
|
|
||||||
|
@ -173,6 +177,9 @@ class Notebooks( object ):
|
||||||
@param note_id: id of note to return
|
@param note_id: id of note to return
|
||||||
@type revision: unicode or NoneType
|
@type revision: unicode or NoneType
|
||||||
@param revision: revision timestamp of the note (optional)
|
@param revision: revision timestamp of the note (optional)
|
||||||
|
@type summarize: bool or NoneType
|
||||||
|
@param summarize: True to return a summary of the note's contents, False to return full text
|
||||||
|
(optional, defaults to False)
|
||||||
@type user_id: unicode or NoneType
|
@type user_id: unicode or NoneType
|
||||||
@param user_id: id of current logged-in user (if any), determined by @grab_user_id
|
@param user_id: id of current logged-in user (if any), determined by @grab_user_id
|
||||||
@rtype: json dict
|
@rtype: json dict
|
||||||
|
@ -196,7 +203,7 @@ class Notebooks( object ):
|
||||||
if notebook and note.notebook_id == notebook.trash_id:
|
if notebook and note.notebook_id == notebook.trash_id:
|
||||||
if revision:
|
if revision:
|
||||||
return dict(
|
return dict(
|
||||||
note = note,
|
note = summarize and self.summarize_note( note ) or note,
|
||||||
)
|
)
|
||||||
|
|
||||||
return dict(
|
return dict(
|
||||||
|
@ -207,7 +214,7 @@ class Notebooks( object ):
|
||||||
raise Access_error()
|
raise Access_error()
|
||||||
|
|
||||||
return dict(
|
return dict(
|
||||||
note = note,
|
note = summarize and self.summarize_note( note ) or note,
|
||||||
)
|
)
|
||||||
|
|
||||||
@expose( view = Json )
|
@expose( view = Json )
|
||||||
|
@ -216,9 +223,10 @@ class Notebooks( object ):
|
||||||
@validate(
|
@validate(
|
||||||
notebook_id = Valid_id(),
|
notebook_id = Valid_id(),
|
||||||
note_title = Valid_string( min = 1, max = 500 ),
|
note_title = Valid_string( min = 1, max = 500 ),
|
||||||
|
summarize = Valid_bool(),
|
||||||
user_id = Valid_id( none_okay = True ),
|
user_id = Valid_id( none_okay = True ),
|
||||||
)
|
)
|
||||||
def load_note_by_title( self, notebook_id, note_title, user_id ):
|
def load_note_by_title( self, notebook_id, note_title, summarize = False, user_id = None ):
|
||||||
"""
|
"""
|
||||||
Return the information on a particular note by its title.
|
Return the information on a particular note by its title.
|
||||||
|
|
||||||
|
@ -226,6 +234,9 @@ class Notebooks( object ):
|
||||||
@param notebook_id: id of notebook the note is in
|
@param notebook_id: id of notebook the note is in
|
||||||
@type note_title: unicode
|
@type note_title: unicode
|
||||||
@param note_title: title of the note to return
|
@param note_title: title of the note to return
|
||||||
|
@type summarize: bool or NoneType
|
||||||
|
@param summarize: True to return a summary of the note's contents, False to return full text
|
||||||
|
(optional, defaults to False)
|
||||||
@type user_id: unicode or NoneType
|
@type user_id: unicode or NoneType
|
||||||
@param user_id: id of current logged-in user (if any), determined by @grab_user_id
|
@param user_id: id of current logged-in user (if any), determined by @grab_user_id
|
||||||
@rtype: json dict
|
@rtype: json dict
|
||||||
|
@ -244,9 +255,59 @@ class Notebooks( object ):
|
||||||
note = self.__database.select_one( Note, notebook.sql_load_note_by_title( note_title ) )
|
note = self.__database.select_one( Note, notebook.sql_load_note_by_title( note_title ) )
|
||||||
|
|
||||||
return dict(
|
return dict(
|
||||||
note = note,
|
note = summarize and self.summarize_note( note ) or note,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def summarize_note( self, note ):
|
||||||
|
"""
|
||||||
|
Create a truncated note summary for the given note, and then return the note with its summary
|
||||||
|
set.
|
||||||
|
|
||||||
|
@type note: model.Note or NoneType
|
||||||
|
@param note: note to summarize, or None
|
||||||
|
@rtype: model.Note or NoneType
|
||||||
|
@return: note with its summary member set, or None if no note was provided
|
||||||
|
"""
|
||||||
|
MAX_SUMMARY_LENGTH = 40
|
||||||
|
word_count = 10
|
||||||
|
|
||||||
|
if note is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if note.contents is None:
|
||||||
|
return note
|
||||||
|
|
||||||
|
# remove all HTML from the contents and also remove the title
|
||||||
|
summary = Html_nuker().nuke( note.contents ).strip()
|
||||||
|
if note.title and summary.startswith( note.title ):
|
||||||
|
summary = summary[ len( note.title ) : ]
|
||||||
|
|
||||||
|
# split the summary on whitespace
|
||||||
|
words = self.WHITESPACE_PATTERN.split( summary )
|
||||||
|
|
||||||
|
def first_words( words, word_count ):
|
||||||
|
return u" ".join( words[ : word_count ] )
|
||||||
|
|
||||||
|
# find a summary less than MAX_SUMMARY_LENGTH and, if possible, truncated on a word boundary
|
||||||
|
truncated = False
|
||||||
|
summary = first_words( words, word_count )
|
||||||
|
|
||||||
|
while len( summary ) > MAX_SUMMARY_LENGTH:
|
||||||
|
word_count -= 1
|
||||||
|
summary = first_words( words, word_count )
|
||||||
|
|
||||||
|
# if the first word is just ridiculously long, truncate it without finding a word boundary
|
||||||
|
if word_count == 1:
|
||||||
|
summary = summary[ : MAX_SUMMARY_LENGTH ]
|
||||||
|
truncated = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if truncated or word_count < len( words ):
|
||||||
|
summary += " ..."
|
||||||
|
|
||||||
|
note.summary = summary
|
||||||
|
return note
|
||||||
|
|
||||||
@expose( view = Json )
|
@expose( view = Json )
|
||||||
@strongly_expire
|
@strongly_expire
|
||||||
@grab_user_id
|
@grab_user_id
|
||||||
|
@ -607,8 +668,8 @@ class Notebooks( object ):
|
||||||
"""
|
"""
|
||||||
Search the notes within a particular notebook for the given search text. Note that the search
|
Search the notes within a particular notebook for the given search text. Note that the search
|
||||||
is case-insensitive, and all HTML tags are ignored. Notes with title matches are generally
|
is case-insensitive, and all HTML tags are ignored. Notes with title matches are generally
|
||||||
ranked higher than matches that are only in the note contents. The returned notes have their
|
ranked higher than matches that are only in the note contents. The returned notes include
|
||||||
normal contents replaced with summary contents with the search terms highlighted.
|
content summaries with the search terms highlighted.
|
||||||
|
|
||||||
@type notebook_id: unicode
|
@type notebook_id: unicode
|
||||||
@param notebook_id: id of notebook to search
|
@param notebook_id: id of notebook to search
|
||||||
|
|
|
@ -384,6 +384,24 @@ class Test_notebooks( Test_controller ):
|
||||||
user = self.database.load( User, self.user.object_id )
|
user = self.database.load( User, self.user.object_id )
|
||||||
assert user.storage_bytes == 0
|
assert user.storage_bytes == 0
|
||||||
|
|
||||||
|
def test_load_note_with_summary( self ):
|
||||||
|
self.login()
|
||||||
|
|
||||||
|
result = self.http_post( "/notebooks/load_note/", dict(
|
||||||
|
notebook_id = self.notebook.object_id,
|
||||||
|
note_id = self.note.object_id,
|
||||||
|
summarize = True,
|
||||||
|
), session_id = self.session_id )
|
||||||
|
|
||||||
|
note = result[ "note" ]
|
||||||
|
|
||||||
|
assert note.object_id == self.note.object_id
|
||||||
|
assert note.title == self.note.title
|
||||||
|
assert note.contents == self.note.contents
|
||||||
|
assert note.summary == u"blah"
|
||||||
|
user = self.database.load( User, self.user.object_id )
|
||||||
|
assert user.storage_bytes == 0
|
||||||
|
|
||||||
def test_load_note_by_title( self ):
|
def test_load_note_by_title( self ):
|
||||||
self.login()
|
self.login()
|
||||||
|
|
||||||
|
@ -435,6 +453,64 @@ class Test_notebooks( Test_controller ):
|
||||||
user = self.database.load( User, self.user.object_id )
|
user = self.database.load( User, self.user.object_id )
|
||||||
assert user.storage_bytes == 0
|
assert user.storage_bytes == 0
|
||||||
|
|
||||||
|
def test_load_note_by_title_with_summary( self ):
|
||||||
|
self.login()
|
||||||
|
|
||||||
|
result = self.http_post( "/notebooks/load_note_by_title/", dict(
|
||||||
|
notebook_id = self.notebook.object_id,
|
||||||
|
note_title = self.note.title,
|
||||||
|
summarize = True,
|
||||||
|
), session_id = self.session_id )
|
||||||
|
|
||||||
|
note = result[ "note" ]
|
||||||
|
|
||||||
|
assert note.object_id == self.note.object_id
|
||||||
|
assert note.title == self.note.title
|
||||||
|
assert note.contents == self.note.contents
|
||||||
|
assert note.summary == u"blah"
|
||||||
|
user = self.database.load( User, self.user.object_id )
|
||||||
|
assert user.storage_bytes == 0
|
||||||
|
|
||||||
|
def test_summarize_note( self ):
|
||||||
|
note = cherrypy.root.notebooks.summarize_note( self.note )
|
||||||
|
|
||||||
|
assert note.summary == u"blah"
|
||||||
|
|
||||||
|
def test_summarize_note_truncated_at_word_boundary( self ):
|
||||||
|
self.note.contents = u"<h3>the title</h3>" + u"foo bar baz quux " * 10
|
||||||
|
note = cherrypy.root.notebooks.summarize_note( self.note )
|
||||||
|
|
||||||
|
assert note.summary == u"foo bar baz quux foo bar baz quux foo ..."
|
||||||
|
|
||||||
|
def test_summarize_note_truncated_at_character_boundary( self ):
|
||||||
|
self.note.contents = u"<h3>the title</h3>" + u"foobarbazquux" * 10
|
||||||
|
note = cherrypy.root.notebooks.summarize_note( self.note )
|
||||||
|
|
||||||
|
assert note.summary == u"foobarbazquuxfoobarbazquuxfoobarbazquuxf ..."
|
||||||
|
|
||||||
|
def test_summarize_note_with_short_words( self ):
|
||||||
|
self.note.contents = u"<h3>the title</h3>" + u"a b c d e f g h i j k l"
|
||||||
|
note = cherrypy.root.notebooks.summarize_note( self.note )
|
||||||
|
|
||||||
|
assert note.summary == u"a b c d e f g h i j ..."
|
||||||
|
|
||||||
|
def test_summarize_note_without_title( self ):
|
||||||
|
self.note.contents = "foo bar baz quux"
|
||||||
|
note = cherrypy.root.notebooks.summarize_note( self.note )
|
||||||
|
|
||||||
|
assert note.summary == u"foo bar baz quux"
|
||||||
|
|
||||||
|
def test_summarize_note_without_contents( self ):
|
||||||
|
self.note.contents = None
|
||||||
|
note = cherrypy.root.notebooks.summarize_note( self.note )
|
||||||
|
|
||||||
|
assert note.summary == None
|
||||||
|
|
||||||
|
def test_summarize_note_none( self ):
|
||||||
|
note = cherrypy.root.notebooks.summarize_note( None )
|
||||||
|
|
||||||
|
assert note == None
|
||||||
|
|
||||||
def test_lookup_note_id( self ):
|
def test_lookup_note_id( self ):
|
||||||
self.login()
|
self.login()
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ class Note( Persistent ):
|
||||||
TITLE_PATTERN = re.compile( u"<h3>(.*?)</h3>", flags = re.IGNORECASE )
|
TITLE_PATTERN = re.compile( u"<h3>(.*?)</h3>", flags = re.IGNORECASE )
|
||||||
|
|
||||||
def __init__( self, object_id, revision = None, title = None, contents = None, notebook_id = None,
|
def __init__( self, object_id, revision = None, title = None, contents = None, notebook_id = None,
|
||||||
startup = None, deleted_from_id = None, rank = None, creation = None ):
|
startup = None, deleted_from_id = None, rank = None, creation = None, summary = None ):
|
||||||
"""
|
"""
|
||||||
Create a new note with the given id and contents.
|
Create a new note with the given id and contents.
|
||||||
|
|
||||||
|
@ -32,12 +32,15 @@ class Note( Persistent ):
|
||||||
@param rank: indicates numeric ordering of this note in relation to other startup notes
|
@param rank: indicates numeric ordering of this note in relation to other startup notes
|
||||||
@type creation: datetime or NoneType
|
@type creation: datetime or NoneType
|
||||||
@param creation: creation timestamp of the object (optional, defaults to None)
|
@param creation: creation timestamp of the object (optional, defaults to None)
|
||||||
|
@type summary: unicode or NoneType
|
||||||
|
@param summary: textual summary of the note's contents (optional, defaults to None)
|
||||||
@rtype: Note
|
@rtype: Note
|
||||||
@return: newly constructed note
|
@return: newly constructed note
|
||||||
"""
|
"""
|
||||||
Persistent.__init__( self, object_id, revision )
|
Persistent.__init__( self, object_id, revision )
|
||||||
self.__title = title
|
self.__title = title
|
||||||
self.__contents = contents
|
self.__contents = contents
|
||||||
|
self.__summary = summary
|
||||||
self.__notebook_id = notebook_id
|
self.__notebook_id = notebook_id
|
||||||
self.__startup = startup or False
|
self.__startup = startup or False
|
||||||
self.__deleted_from_id = deleted_from_id
|
self.__deleted_from_id = deleted_from_id
|
||||||
|
@ -45,7 +48,7 @@ class Note( Persistent ):
|
||||||
self.__creation = creation
|
self.__creation = creation
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create( object_id, contents = None, notebook_id = None, startup = None, rank = None, creation = None ):
|
def create( object_id, contents = None, notebook_id = None, startup = None, rank = None, creation = None, summary = None ):
|
||||||
"""
|
"""
|
||||||
Convenience constructor for creating a new note.
|
Convenience constructor for creating a new note.
|
||||||
|
|
||||||
|
@ -61,10 +64,12 @@ class Note( Persistent ):
|
||||||
@param rank: indicates numeric ordering of this note in relation to other startup notes
|
@param rank: indicates numeric ordering of this note in relation to other startup notes
|
||||||
@type creation: datetime or NoneType
|
@type creation: datetime or NoneType
|
||||||
@param creation: creation timestamp of the object (optional, defaults to None)
|
@param creation: creation timestamp of the object (optional, defaults to None)
|
||||||
|
@type summary: unicode or NoneType
|
||||||
|
@param summary: textual summary of the note's contents (optional, defaults to None)
|
||||||
@rtype: Note
|
@rtype: Note
|
||||||
@return: newly constructed note
|
@return: newly constructed note
|
||||||
"""
|
"""
|
||||||
note = Note( object_id, notebook_id = notebook_id, startup = startup, rank = rank, creation = creation )
|
note = Note( object_id, notebook_id = notebook_id, startup = startup, rank = rank, creation = creation, summary = summary )
|
||||||
note.contents = contents
|
note.contents = contents
|
||||||
|
|
||||||
return note
|
return note
|
||||||
|
@ -86,6 +91,9 @@ class Note( Persistent ):
|
||||||
else:
|
else:
|
||||||
self.__title = None
|
self.__title = None
|
||||||
|
|
||||||
|
def __set_summary( self, summary ):
|
||||||
|
self.__summary = summary
|
||||||
|
|
||||||
def __set_notebook_id( self, notebook_id ):
|
def __set_notebook_id( self, notebook_id ):
|
||||||
self.__notebook_id = notebook_id
|
self.__notebook_id = notebook_id
|
||||||
self.update_revision()
|
self.update_revision()
|
||||||
|
@ -141,6 +149,7 @@ class Note( Persistent ):
|
||||||
d = Persistent.to_dict( self )
|
d = Persistent.to_dict( self )
|
||||||
d.update( dict(
|
d.update( dict(
|
||||||
contents = self.__contents,
|
contents = self.__contents,
|
||||||
|
summary = self.__summary,
|
||||||
title = self.__title,
|
title = self.__title,
|
||||||
deleted_from_id = self.__deleted_from_id,
|
deleted_from_id = self.__deleted_from_id,
|
||||||
creation = self.__creation,
|
creation = self.__creation,
|
||||||
|
@ -150,6 +159,7 @@ class Note( Persistent ):
|
||||||
|
|
||||||
title = property( lambda self: self.__title )
|
title = property( lambda self: self.__title )
|
||||||
contents = property( lambda self: self.__contents, __set_contents )
|
contents = property( lambda self: self.__contents, __set_contents )
|
||||||
|
summary = property( lambda self: self.__summary, __set_summary )
|
||||||
notebook_id = property( lambda self: self.__notebook_id, __set_notebook_id )
|
notebook_id = property( lambda self: self.__notebook_id, __set_notebook_id )
|
||||||
startup = property( lambda self: self.__startup, __set_startup )
|
startup = property( lambda self: self.__startup, __set_startup )
|
||||||
deleted_from_id = property( lambda self: self.__deleted_from_id, __set_deleted_from_id )
|
deleted_from_id = property( lambda self: self.__deleted_from_id, __set_deleted_from_id )
|
||||||
|
|
|
@ -161,9 +161,10 @@ class Notebook( Persistent ):
|
||||||
|
|
||||||
return \
|
return \
|
||||||
"""
|
"""
|
||||||
select id, revision, title, headline( drop_html_tags( contents ), query ), notebook_id, startup, deleted_from_id from (
|
select id, revision, title, contents, notebook_id, startup, deleted_from_id, rank, null,
|
||||||
|
headline( drop_html_tags( contents ), query ) as summary from (
|
||||||
select
|
select
|
||||||
id, revision, title, contents, notebook_id, startup, deleted_from_id, query, rank_cd( search, query ) as rank
|
id, revision, title, contents, notebook_id, startup, deleted_from_id, rank_cd( search, query ) as rank, null, query
|
||||||
from
|
from
|
||||||
note_current, to_tsquery( 'default', %s ) query
|
note_current, to_tsquery( 'default', %s ) query
|
||||||
where
|
where
|
||||||
|
|
|
@ -8,18 +8,20 @@ class Test_note( object ):
|
||||||
self.object_id = u"17"
|
self.object_id = u"17"
|
||||||
self.title = u"title goes here"
|
self.title = u"title goes here"
|
||||||
self.contents = u"<h3>%s</h3>blah" % self.title
|
self.contents = u"<h3>%s</h3>blah" % self.title
|
||||||
|
self.summary = None
|
||||||
self.notebook_id = u"18"
|
self.notebook_id = u"18"
|
||||||
self.startup = False
|
self.startup = False
|
||||||
self.rank = 17.5
|
self.rank = 17.5
|
||||||
self.creation = datetime.now()
|
self.creation = datetime.now()
|
||||||
self.delta = timedelta( seconds = 1 )
|
self.delta = timedelta( seconds = 1 )
|
||||||
|
|
||||||
self.note = Note.create( self.object_id, self.contents, self.notebook_id, self.startup, self.rank, self.creation )
|
self.note = Note.create( self.object_id, self.contents, self.notebook_id, self.startup, self.rank, self.creation, self.summary )
|
||||||
|
|
||||||
def test_create( self ):
|
def test_create( self ):
|
||||||
assert self.note.object_id == self.object_id
|
assert self.note.object_id == self.object_id
|
||||||
assert datetime.now( tz = utc ) - self.note.revision < self.delta
|
assert datetime.now( tz = utc ) - self.note.revision < self.delta
|
||||||
assert self.note.contents == self.contents
|
assert self.note.contents == self.contents
|
||||||
|
assert self.note.summary == None
|
||||||
assert self.note.title == self.title
|
assert self.note.title == self.title
|
||||||
assert self.note.notebook_id == self.notebook_id
|
assert self.note.notebook_id == self.notebook_id
|
||||||
assert self.note.startup == self.startup
|
assert self.note.startup == self.startup
|
||||||
|
@ -36,6 +38,7 @@ class Test_note( object ):
|
||||||
|
|
||||||
assert self.note.revision > previous_revision
|
assert self.note.revision > previous_revision
|
||||||
assert self.note.contents == new_contents
|
assert self.note.contents == new_contents
|
||||||
|
assert self.note.summary == None
|
||||||
assert self.note.title == new_title
|
assert self.note.title == new_title
|
||||||
assert self.note.notebook_id == self.notebook_id
|
assert self.note.notebook_id == self.notebook_id
|
||||||
assert self.note.startup == self.startup
|
assert self.note.startup == self.startup
|
||||||
|
@ -53,6 +56,7 @@ class Test_note( object ):
|
||||||
# html should be stripped out of the title
|
# html should be stripped out of the title
|
||||||
assert self.note.revision > previous_revision
|
assert self.note.revision > previous_revision
|
||||||
assert self.note.contents == new_contents
|
assert self.note.contents == new_contents
|
||||||
|
assert self.note.summary == None
|
||||||
assert self.note.title == new_title
|
assert self.note.title == new_title
|
||||||
assert self.note.notebook_id == self.notebook_id
|
assert self.note.notebook_id == self.notebook_id
|
||||||
assert self.note.startup == self.startup
|
assert self.note.startup == self.startup
|
||||||
|
@ -70,6 +74,7 @@ class Test_note( object ):
|
||||||
# should only use the first title
|
# should only use the first title
|
||||||
assert self.note.revision > previous_revision
|
assert self.note.revision > previous_revision
|
||||||
assert self.note.contents == new_contents
|
assert self.note.contents == new_contents
|
||||||
|
assert self.note.summary == None
|
||||||
assert self.note.title == new_title
|
assert self.note.title == new_title
|
||||||
assert self.note.notebook_id == self.notebook_id
|
assert self.note.notebook_id == self.notebook_id
|
||||||
assert self.note.startup == self.startup
|
assert self.note.startup == self.startup
|
||||||
|
@ -77,6 +82,22 @@ class Test_note( object ):
|
||||||
assert self.note.rank == self.rank
|
assert self.note.rank == self.rank
|
||||||
assert self.note.creation == self.creation
|
assert self.note.creation == self.creation
|
||||||
|
|
||||||
|
def test_set_summary( self ):
|
||||||
|
summary = u"summary goes here..."
|
||||||
|
original_revision = self.note.revision
|
||||||
|
|
||||||
|
self.note.summary = summary
|
||||||
|
|
||||||
|
assert self.note.revision == original_revision
|
||||||
|
assert self.note.contents == self.contents
|
||||||
|
assert self.note.summary == summary
|
||||||
|
assert self.note.title == self.title
|
||||||
|
assert self.note.notebook_id == self.notebook_id
|
||||||
|
assert self.note.startup == self.startup
|
||||||
|
assert self.note.deleted_from_id == None
|
||||||
|
assert self.note.rank == self.rank
|
||||||
|
assert self.note.creation == self.creation
|
||||||
|
|
||||||
def test_set_notebook_id( self ):
|
def test_set_notebook_id( self ):
|
||||||
previous_revision = self.note.revision
|
previous_revision = self.note.revision
|
||||||
self.note.notebook_id = u"54"
|
self.note.notebook_id = u"54"
|
||||||
|
@ -111,6 +132,7 @@ class Test_note( object ):
|
||||||
assert d.get( "object_id" ) == self.note.object_id
|
assert d.get( "object_id" ) == self.note.object_id
|
||||||
assert datetime.now( tz = utc ) - d.get( "revision" ) < self.delta
|
assert datetime.now( tz = utc ) - d.get( "revision" ) < self.delta
|
||||||
assert d.get( "contents" ) == self.contents
|
assert d.get( "contents" ) == self.contents
|
||||||
|
assert d.get( "summary" ) == self.summary
|
||||||
assert d.get( "title" ) == self.title
|
assert d.get( "title" ) == self.title
|
||||||
assert d.get( "deleted_from_id" ) == None
|
assert d.get( "deleted_from_id" ) == None
|
||||||
assert d.get( "creation" ) == self.note.creation
|
assert d.get( "creation" ) == self.note.creation
|
||||||
|
@ -121,6 +143,7 @@ class Test_note_blank( Test_note ):
|
||||||
self.object_id = u"17"
|
self.object_id = u"17"
|
||||||
self.title = None
|
self.title = None
|
||||||
self.contents = None
|
self.contents = None
|
||||||
|
self.summary = None
|
||||||
self.notebook_id = None
|
self.notebook_id = None
|
||||||
self.startup = False
|
self.startup = False
|
||||||
self.rank = None
|
self.rank = None
|
||||||
|
@ -133,6 +156,7 @@ class Test_note_blank( Test_note ):
|
||||||
assert self.note.object_id == self.object_id
|
assert self.note.object_id == self.object_id
|
||||||
assert datetime.now( tz = utc ) - self.note.revision < self.delta
|
assert datetime.now( tz = utc ) - self.note.revision < self.delta
|
||||||
assert self.note.contents == None
|
assert self.note.contents == None
|
||||||
|
assert self.note.summary == None
|
||||||
assert self.note.title == None
|
assert self.note.title == None
|
||||||
assert self.note.notebook_id == None
|
assert self.note.notebook_id == None
|
||||||
assert self.note.startup == False
|
assert self.note.startup == False
|
||||||
|
|
|
@ -383,8 +383,7 @@ Editor.prototype.start_link = function () {
|
||||||
} else {
|
} else {
|
||||||
this.link_started = null;
|
this.link_started = null;
|
||||||
this.exec_command( "createLink", "/notebooks/" + this.notebook_id + "?note_id=new" );
|
this.exec_command( "createLink", "/notebooks/" + this.notebook_id + "?note_id=new" );
|
||||||
var link = this.find_link_at_cursor();
|
return this.find_link_at_cursor();
|
||||||
signal( this, "resolve_link", link_title( link ), link );
|
|
||||||
}
|
}
|
||||||
} else if ( this.document.selection ) { // browsers such as IE
|
} else if ( this.document.selection ) { // browsers such as IE
|
||||||
var range = this.document.selection.createRange();
|
var range = this.document.selection.createRange();
|
||||||
|
@ -400,8 +399,7 @@ Editor.prototype.start_link = function () {
|
||||||
} else {
|
} else {
|
||||||
this.link_started = null;
|
this.link_started = null;
|
||||||
this.exec_command( "createLink", "/notebooks/" + this.notebook_id + "?note_id=new" );
|
this.exec_command( "createLink", "/notebooks/" + this.notebook_id + "?note_id=new" );
|
||||||
var link = this.find_link_at_cursor();
|
return this.find_link_at_cursor();
|
||||||
signal( this, "resolve_link", link_title( link ), link );
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -430,7 +428,6 @@ Editor.prototype.end_link = function () {
|
||||||
range.pasteHTML( "" );
|
range.pasteHTML( "" );
|
||||||
}
|
}
|
||||||
|
|
||||||
signal( this, "resolve_link", link_title( link ), link );
|
|
||||||
return link;
|
return link;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -562,6 +559,48 @@ Editor.prototype.shutdown = function( event ) {
|
||||||
} } );
|
} } );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Editor.prototype.summarize = function () {
|
||||||
|
var summary = strip( scrapeText( this.document.body ) );
|
||||||
|
|
||||||
|
// remove the title from the scraped summary text
|
||||||
|
if ( summary.indexOf( this.title ) == 0 )
|
||||||
|
summary = summary.substr( this.title.length );
|
||||||
|
|
||||||
|
if ( summary.length == 0 )
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var MAX_SUMMARY_LENGTH = 40;
|
||||||
|
var word_count = 10;
|
||||||
|
|
||||||
|
// split the summary on whitespace
|
||||||
|
var words = summary.split( /\s+/ );
|
||||||
|
|
||||||
|
function first_words( words, word_count ) {
|
||||||
|
return words.slice( 0, word_count ).join( " " );
|
||||||
|
}
|
||||||
|
|
||||||
|
var truncated = false;
|
||||||
|
summary = first_words( words, word_count );
|
||||||
|
|
||||||
|
// find a summary less than MAX_SUMMARY_LENGTH and, if possible, truncated on a word boundary
|
||||||
|
while ( summary.length > MAX_SUMMARY_LENGTH ) {
|
||||||
|
word_count -= 1;
|
||||||
|
summary = first_words( words, word_count );
|
||||||
|
|
||||||
|
// if the first word is just ridiculously long, truncate it without finding a word boundary
|
||||||
|
if ( word_count == 1 ) {
|
||||||
|
summary = summary.substr( 0, MAX_SUMMARY_LENGTH );
|
||||||
|
truncated = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( truncated || word_count < words.length )
|
||||||
|
summary += " ...";
|
||||||
|
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
|
||||||
// convenience function for parsing a link that has an href URL containing a query string
|
// convenience function for parsing a link that has an href URL containing a query string
|
||||||
function parse_query( link ) {
|
function parse_query( link ) {
|
||||||
if ( !link || !link.href )
|
if ( !link || !link.href )
|
||||||
|
|
|
@ -490,22 +490,43 @@ Wiki.prototype.resolve_link = function ( note_title, link, callback ) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
|
if ( callback ) {
|
||||||
|
this.invoker.invoke(
|
||||||
|
"/notebooks/load_note_by_title", "GET", {
|
||||||
|
"notebook_id": this.notebook_id,
|
||||||
|
"note_title": note_title,
|
||||||
|
"summarize": true
|
||||||
|
},
|
||||||
|
function ( result ) {
|
||||||
|
if ( result && result.note ) {
|
||||||
|
link.href = "/notebooks/" + self.notebook_id + "?note_id=" + result.note.object_id;
|
||||||
|
} else {
|
||||||
|
link.href = "/notebooks/" + self.notebook_id + "?" + queryString(
|
||||||
|
[ "title", "note_id" ],
|
||||||
|
[ note_title, "null" ]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
callback( ( result && result.note ) ? result.note.summary : null );
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.invoker.invoke(
|
this.invoker.invoke(
|
||||||
"/notebooks/" + ( callback ? "load_note_by_title" : "lookup_note_id" ), "GET", {
|
"/notebooks/lookup_note_id", "GET", {
|
||||||
"notebook_id": this.notebook_id,
|
"notebook_id": this.notebook_id,
|
||||||
"note_title": note_title
|
"note_title": note_title
|
||||||
},
|
},
|
||||||
function ( result ) {
|
function ( result ) {
|
||||||
if ( result && ( result.note || result.note_id ) ) {
|
if ( result && result.note_id ) {
|
||||||
link.href = "/notebooks/" + self.notebook_id + "?note_id=" + ( result.note ? result.note.object_id : result.note_id );
|
link.href = "/notebooks/" + self.notebook_id + "?note_id=" + result.note_id;
|
||||||
} else {
|
} else {
|
||||||
link.href = "/notebooks/" + self.notebook_id + "?" + queryString(
|
link.href = "/notebooks/" + self.notebook_id + "?" + queryString(
|
||||||
[ "title", "note_id" ],
|
[ "title", "note_id" ],
|
||||||
[ note_title, "null" ]
|
[ note_title, "null" ]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if ( callback )
|
|
||||||
callback( ( result && result.note ) ? result.note.contents : null );
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -596,7 +617,6 @@ Wiki.prototype.create_editor = function ( id, note_text, deleted_from_id, revisi
|
||||||
}
|
}
|
||||||
|
|
||||||
connect( editor, "load_editor", this, "load_editor" );
|
connect( editor, "load_editor", this, "load_editor" );
|
||||||
connect( editor, "resolve_link", this, "resolve_link" );
|
|
||||||
connect( editor, "hide_clicked", function ( event ) { self.hide_editor( event, editor ) } );
|
connect( editor, "hide_clicked", function ( event ) { self.hide_editor( event, editor ) } );
|
||||||
connect( editor, "submit_form", function ( url, form ) {
|
connect( editor, "submit_form", function ( url, form ) {
|
||||||
self.invoker.invoke( url, "POST", null, null, form );
|
self.invoker.invoke( url, "POST", null, null, form );
|
||||||
|
@ -857,11 +877,18 @@ Wiki.prototype.toggle_link_button = function ( event ) {
|
||||||
if ( this.focused_editor && this.focused_editor.read_write ) {
|
if ( this.focused_editor && this.focused_editor.read_write ) {
|
||||||
this.focused_editor.focus();
|
this.focused_editor.focus();
|
||||||
if ( this.toggle_image_button( "createLink" ) )
|
if ( this.toggle_image_button( "createLink" ) )
|
||||||
this.focused_editor.start_link();
|
link = this.focused_editor.start_link();
|
||||||
else
|
else
|
||||||
link = this.focused_editor.end_link();
|
link = this.focused_editor.end_link();
|
||||||
|
|
||||||
this.display_link_pulldown( this.focused_editor, link );
|
if ( link ) {
|
||||||
|
var self = this;
|
||||||
|
this.resolve_link( link_title( link ), link, function ( summary ) {
|
||||||
|
self.display_link_pulldown( self.focused_editor, link );
|
||||||
|
} );
|
||||||
|
} else {
|
||||||
|
this.display_link_pulldown( this.focused_editor );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
event.stop();
|
event.stop();
|
||||||
|
@ -1124,23 +1151,23 @@ Wiki.prototype.display_search_results = function ( result ) {
|
||||||
if ( !note.title ) continue;
|
if ( !note.title ) continue;
|
||||||
|
|
||||||
if ( note.contents.length == 0 ) {
|
if ( note.contents.length == 0 ) {
|
||||||
var preview = "empty note";
|
var summary = "empty note";
|
||||||
} else {
|
} else {
|
||||||
var preview = note.contents;
|
var summary = note.summary;
|
||||||
|
|
||||||
// if the preview appears not to end with a complete sentence, add "..."
|
// if the summary appears not to end with a complete sentence, add "..."
|
||||||
if ( !/[?!.]\s*$/.test( preview ) )
|
if ( !/[?!.]\s*$/.test( summary ) )
|
||||||
preview = preview + " <b>...</b>";
|
summary = summary + " <b>...</b>";
|
||||||
}
|
}
|
||||||
|
|
||||||
var preview_span = createDOM( "span" );
|
var summary_span = createDOM( "span" );
|
||||||
preview_span.innerHTML = preview;
|
summary_span.innerHTML = summary;
|
||||||
|
|
||||||
appendChildNodes( list,
|
appendChildNodes( list,
|
||||||
createDOM( "p", {},
|
createDOM( "p", {},
|
||||||
createDOM( "a", { "href": "/notebooks/" + this.notebook_id + "?note_id=" + note.object_id }, note.title ),
|
createDOM( "a", { "href": "/notebooks/" + this.notebook_id + "?note_id=" + note.object_id }, note.title ),
|
||||||
createDOM( "br" ),
|
createDOM( "br" ),
|
||||||
preview_span
|
summary_span
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1756,7 +1783,7 @@ function Link_pulldown( wiki, notebook_id, invoker, editor, link ) {
|
||||||
this.invoker = invoker;
|
this.invoker = invoker;
|
||||||
this.editor = editor;
|
this.editor = editor;
|
||||||
this.title_field = createDOM( "input", { "class": "text_field", "size": "30", "maxlength": "256" } );
|
this.title_field = createDOM( "input", { "class": "text_field", "size": "30", "maxlength": "256" } );
|
||||||
this.note_preview = createDOM( "span", {} );
|
this.note_summary = createDOM( "span", {} );
|
||||||
this.previous_title = "";
|
this.previous_title = "";
|
||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
|
@ -1768,12 +1795,12 @@ function Link_pulldown( wiki, notebook_id, invoker, editor, link ) {
|
||||||
|
|
||||||
appendChildNodes( this.div, createDOM( "span", { "class": "field_label" }, "links to: " ) );
|
appendChildNodes( this.div, createDOM( "span", { "class": "field_label" }, "links to: " ) );
|
||||||
appendChildNodes( this.div, this.title_field );
|
appendChildNodes( this.div, this.title_field );
|
||||||
appendChildNodes( this.div, this.note_preview );
|
appendChildNodes( this.div, this.note_summary );
|
||||||
|
|
||||||
// links with targets are considered links to external sites
|
// links with targets are considered links to external sites
|
||||||
if ( link.target ) {
|
if ( link.target ) {
|
||||||
this.title_field.value = link.href;
|
this.title_field.value = link.href;
|
||||||
replaceChildNodes( this.note_preview, "web link" );
|
replaceChildNodes( this.note_summary, "web link" );
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1785,20 +1812,21 @@ function Link_pulldown( wiki, notebook_id, invoker, editor, link ) {
|
||||||
if ( ( id == undefined || id == "new" || id == "null" ) && title.length > 0 ) {
|
if ( ( id == undefined || id == "new" || id == "null" ) && title.length > 0 ) {
|
||||||
if ( title == "all notes" ) {
|
if ( title == "all notes" ) {
|
||||||
this.title_field.value = title;
|
this.title_field.value = title;
|
||||||
this.display_preview( title, "list of all notes in this notebook" );
|
this.display_summary( title, "list of all notes in this notebook" );
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( title == "search results" ) {
|
if ( title == "search results" ) {
|
||||||
this.title_field.value = title;
|
this.title_field.value = title;
|
||||||
this.display_preview( title, "current search results" );
|
this.display_summary( title, "current search results" );
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.invoker.invoke(
|
this.invoker.invoke(
|
||||||
"/notebooks/load_note_by_title", "GET", {
|
"/notebooks/load_note_by_title", "GET", {
|
||||||
"notebook_id": this.notebook_id,
|
"notebook_id": this.notebook_id,
|
||||||
"note_title": title
|
"note_title": title,
|
||||||
|
"summarize": true
|
||||||
},
|
},
|
||||||
function ( result ) {
|
function ( result ) {
|
||||||
// if the user has already started typing something, don't overwrite it
|
// if the user has already started typing something, don't overwrite it
|
||||||
|
@ -1806,10 +1834,10 @@ function Link_pulldown( wiki, notebook_id, invoker, editor, link ) {
|
||||||
return;
|
return;
|
||||||
if ( result.note ) {
|
if ( result.note ) {
|
||||||
self.title_field.value = result.note.title;
|
self.title_field.value = result.note.title;
|
||||||
self.display_preview( result.note.title, result.note.contents );
|
self.display_summary( result.note.title, result.note.summary );
|
||||||
} else {
|
} else {
|
||||||
self.title_field.value = title;
|
self.title_field.value = title;
|
||||||
replaceChildNodes( self.note_preview, "empty note" );
|
replaceChildNodes( self.note_summary, "empty note" );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -1817,20 +1845,21 @@ function Link_pulldown( wiki, notebook_id, invoker, editor, link ) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// if this link has an actual destination note id set, then see if that note is already open. if
|
// if this link has an actual destination note id set, then see if that note is already open. if
|
||||||
// so, display its title and a preview of its contents
|
// so, display its title and a summary of its contents
|
||||||
var iframe = getElement( "note_" + id );
|
var iframe = getElement( "note_" + id );
|
||||||
if ( iframe ) {
|
if ( iframe ) {
|
||||||
this.title_field.value = iframe.editor.title;
|
this.title_field.value = iframe.editor.title;
|
||||||
this.display_preview( iframe.editor.title, iframe.editor.document );
|
this.display_summary( iframe.editor.title, iframe.editor.summarize() );
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// otherwise, load the destination note from the server, displaying its title and a preview of
|
// otherwise, load the destination note from the server, displaying its title and a summary of
|
||||||
// its contents
|
// its contents
|
||||||
this.invoker.invoke(
|
this.invoker.invoke(
|
||||||
"/notebooks/load_note", "GET", {
|
"/notebooks/load_note", "GET", {
|
||||||
"notebook_id": this.notebook_id,
|
"notebook_id": this.notebook_id,
|
||||||
"note_id": id
|
"note_id": id,
|
||||||
|
"summarize": true
|
||||||
},
|
},
|
||||||
function ( result ) {
|
function ( result ) {
|
||||||
// if the user has already started typing something, don't overwrite it
|
// if the user has already started typing something, don't overwrite it
|
||||||
|
@ -1838,10 +1867,10 @@ function Link_pulldown( wiki, notebook_id, invoker, editor, link ) {
|
||||||
return;
|
return;
|
||||||
if ( result.note ) {
|
if ( result.note ) {
|
||||||
self.title_field.value = result.note.title;
|
self.title_field.value = result.note.title;
|
||||||
self.display_preview( result.note.title, result.note.contents );
|
self.display_summary( result.note.title, result.note.summary );
|
||||||
} else {
|
} else {
|
||||||
self.title_field.value = title;
|
self.title_field.value = title;
|
||||||
replaceChildNodes( self.note_preview, "empty note" );
|
replaceChildNodes( self.note_summary, "empty note" );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -1850,33 +1879,13 @@ function Link_pulldown( wiki, notebook_id, invoker, editor, link ) {
|
||||||
Link_pulldown.prototype = new function () { this.prototype = Pulldown.prototype; };
|
Link_pulldown.prototype = new function () { this.prototype = Pulldown.prototype; };
|
||||||
Link_pulldown.prototype.constructor = Link_pulldown;
|
Link_pulldown.prototype.constructor = Link_pulldown;
|
||||||
|
|
||||||
Link_pulldown.prototype.display_preview = function ( title, contents ) {
|
Link_pulldown.prototype.display_summary = function ( title, summary ) {
|
||||||
if ( !contents ) {
|
if ( !summary )
|
||||||
replaceChildNodes( this.note_preview, "empty note" );
|
replaceChildNodes( this.note_summary, "empty note" );
|
||||||
return;
|
else if ( summary.length == 0 )
|
||||||
}
|
replaceChildNodes( this.note_summary, "empty note" );
|
||||||
|
else
|
||||||
// if contents is a DOM node, just scrape its text
|
replaceChildNodes( this.note_summary, summary );
|
||||||
if ( contents.nodeType ) {
|
|
||||||
contents = strip( scrapeText( contents ) );
|
|
||||||
// otherwise, assume contents is a string, so put it into a DOM node and then scrape its contents
|
|
||||||
} else {
|
|
||||||
var contents_node = createDOM( "span", {} );
|
|
||||||
contents_node.innerHTML = contents;
|
|
||||||
contents = strip( scrapeText( contents_node ) );
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove the title from the scraped contents text
|
|
||||||
if ( contents.indexOf( title ) == 0 )
|
|
||||||
contents = contents.substr( title.length );
|
|
||||||
|
|
||||||
if ( contents.length == 0 ) {
|
|
||||||
replaceChildNodes( this.note_preview, "empty note" );
|
|
||||||
} else {
|
|
||||||
var max_preview_length = 40;
|
|
||||||
var preview = contents.substr( 0, max_preview_length ) + ( ( contents.length > max_preview_length ) ? "..." : "" );
|
|
||||||
replaceChildNodes( this.note_preview, preview );
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Link_pulldown.prototype.title_field_clicked = function ( event ) {
|
Link_pulldown.prototype.title_field_clicked = function ( event ) {
|
||||||
|
@ -1892,13 +1901,13 @@ Link_pulldown.prototype.title_field_changed = function ( event ) {
|
||||||
if ( this.title_field.value == this.previous_title )
|
if ( this.title_field.value == this.previous_title )
|
||||||
return;
|
return;
|
||||||
|
|
||||||
replaceChildNodes( this.note_preview, "" );
|
replaceChildNodes( this.note_summary, "" );
|
||||||
var title = strip( this.title_field.value );
|
var title = strip( this.title_field.value );
|
||||||
this.previous_title = title;
|
this.previous_title = title;
|
||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
this.wiki.resolve_link( title, this.link, function ( contents ) {
|
this.wiki.resolve_link( title, this.link, function ( summary ) {
|
||||||
self.display_preview( title, contents );
|
self.display_summary( title, summary );
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Reference in New Issue