472 lines
19 KiB
Python
472 lines
19 KiB
Python
import re
|
|
from copy import copy
|
|
from Note import Note
|
|
from Persistent import Persistent, quote, quote_fuzzy
|
|
from datetime import datetime
|
|
from pytz import utc
|
|
|
|
|
|
class Notebook( Persistent ):
|
|
"""
|
|
A collection of wiki notes.
|
|
"""
|
|
|
|
WHITESPACE_PATTERN = re.compile( r"\s+" )
|
|
SEARCH_OPERATORS = re.compile( r"[&|!()'\\:]" )
|
|
|
|
READ_ONLY = 0 # user can only view the notes within this notebook
|
|
READ_WRITE = 1 # user can view and edit the notes within this notebook
|
|
READ_WRITE_FOR_OWN_NOTES = 2 # user can only edit their own notes, not notes created by others
|
|
|
|
def __init__( self, object_id, revision = None, name = None, trash_id = None, deleted = False,
|
|
user_id = None, read_write = None, owner = True, rank = None, own_notes_only = False, note_count = None ):
|
|
"""
|
|
Create a new notebook with the given id and name.
|
|
|
|
@type object_id: unicode
|
|
@param object_id: id of the notebook
|
|
@type revision: datetime or NoneType
|
|
@param revision: revision timestamp of the object (optional, defaults to now)
|
|
@type name: unicode or NoneType
|
|
@param name: name of this notebook (optional)
|
|
@type trash_id: Notebook or NoneType
|
|
@param trash_id: id of the notebook where deleted notes from this notebook go to die (optional)
|
|
@type deleted: bool or NoneType
|
|
@param deleted: whether this notebook is currently deleted (optional, defaults to False)
|
|
@type user_id: unicode or NoneType
|
|
@param user_id: id of the user who most recently updated this notebook object (optional)
|
|
@type read_write: bool or NoneType
|
|
@param read_write: whether this view of the notebook is currently read-write. one of:
|
|
READ_ONLY, READ_WRITE, READ_WRITE_FOR_OWN_NOTES (optional, defaults to READ_WRITE)
|
|
@type owner: bool or NoneType
|
|
@param owner: whether this view of the notebook currently has owner-level access (optional, defaults to True)
|
|
@type rank: float or NoneType
|
|
@param rank: indicates numeric ordering of this note in relation to other notebooks
|
|
@type own_notes_only: bool or NoneType
|
|
@param own_notes_only: True makes read_write be READ_WRITE_FOR_OWN_NOTES (optional, defaults to False)
|
|
@type note_count: int or NoneType
|
|
@param note_count: a count of the number of notes within this notebook (optional)
|
|
@rtype: Notebook
|
|
@return: newly constructed notebook
|
|
"""
|
|
Persistent.__init__( self, object_id, revision )
|
|
self.__name = name
|
|
self.__trash_id = trash_id
|
|
self.__deleted = deleted
|
|
self.__user_id = user_id
|
|
|
|
read_write = {
|
|
None: Notebook.READ_WRITE,
|
|
True: Notebook.READ_WRITE,
|
|
False: Notebook.READ_ONLY,
|
|
}.get( read_write, read_write )
|
|
|
|
if own_notes_only is True and read_write != Notebook.READ_ONLY:
|
|
read_write = Notebook.READ_WRITE_FOR_OWN_NOTES
|
|
|
|
self.__read_write = read_write
|
|
self.__owner = owner
|
|
self.__rank = rank
|
|
self.__note_count = note_count
|
|
self.__tags = []
|
|
|
|
@staticmethod
|
|
def create( object_id, name = None, trash_id = None, deleted = False, user_id = None, read_write = None, owner = True, rank = None, own_notes_only = False, note_count = None ):
|
|
"""
|
|
Convenience constructor for creating a new notebook.
|
|
|
|
@type object_id: unicode
|
|
@param object_id: id of the notebook
|
|
@type name: unicode or NoneType
|
|
@param name: name of this notebook (optional)
|
|
@type trash_id: Notebook or NoneType
|
|
@param trash_id: id of the notebook where deleted notes from this notebook go to die (optional)
|
|
@type deleted: bool or NoneType
|
|
@param deleted: whether this notebook is currently deleted (optional, defaults to False)
|
|
@type user_id: unicode or NoneType
|
|
@param user_id: id of the user who most recently updated this notebook object (optional)
|
|
@type read_write: bool or NoneType
|
|
@param read_write: whether this view of the notebook is currently read-write. one of:
|
|
READ_ONLY, READ_WRITE, READ_WRITE_FOR_OWN_NOTES (optional, defaults to READ_WRITE)
|
|
@type owner: bool or NoneType
|
|
@param owner: whether this view of the notebook currently has owner-level access (optional, defaults to True)
|
|
@type rank: float or NoneType
|
|
@param rank: indicates numeric ordering of this note in relation to other notebooks
|
|
@type own_notes_only: bool or NoneType
|
|
@param own_notes_only: True makes read_write be READ_WRITE_FOR_OWN_NOTES (optional, defaults to False)
|
|
@type note_count: int or NoneType
|
|
@param note_count: a count of the number of notes within this notebook (optional)
|
|
@rtype: Notebook
|
|
@return: newly constructed notebook
|
|
"""
|
|
return Notebook( object_id, name = name, trash_id = trash_id, user_id = user_id, read_write = read_write, owner = owner, rank = rank, own_notes_only = own_notes_only, note_count = note_count )
|
|
|
|
@staticmethod
|
|
def sql_load( object_id, revision = None ):
|
|
if revision:
|
|
return "select * from notebook where id = %s and revision = %s;" % ( quote( object_id ), quote( revision ) )
|
|
|
|
return "select * from notebook_current where id = %s;" % quote( object_id )
|
|
|
|
@staticmethod
|
|
def sql_id_exists( object_id, revision = None ):
|
|
if revision:
|
|
return "select id from notebook where id = %s and revision = %s;" % ( quote( object_id ), quote( revision ) )
|
|
|
|
return "select id from notebook_current where id = %s;" % quote( object_id )
|
|
|
|
def sql_exists( self ):
|
|
return Notebook.sql_id_exists( self.object_id, self.revision )
|
|
|
|
def sql_create( self ):
|
|
return \
|
|
"insert into notebook ( id, revision, name, trash_id, deleted, user_id ) " + \
|
|
"values ( %s, %s, %s, %s, %s, %s );" % \
|
|
( quote( self.object_id ), quote( self.revision ), quote( self.__name ),
|
|
quote( self.__trash_id ), quote( self.deleted ), quote( self.user_id ) )
|
|
|
|
def sql_update( self ):
|
|
return self.sql_create()
|
|
|
|
@staticmethod
|
|
def sql_load_by_friendly_id( friendly_id ):
|
|
return "select * from notebook_current where friendly_id( name ) = %s;" % quote( friendly_id )
|
|
|
|
def sql_load_notes_in_rank_order( self, start = 0, count = None ):
|
|
"""
|
|
Return a SQL string to load a list of all the notes within this notebook.
|
|
Note: If the database backend is SQLite, a start parameter cannot be given without also
|
|
providing a count parameter.
|
|
"""
|
|
if count is not None:
|
|
limit_clause = " limit %s" % count
|
|
else:
|
|
limit_clause = ""
|
|
|
|
if start:
|
|
offset_clause = " offset %s" % start
|
|
else:
|
|
offset_clause = ""
|
|
|
|
return "select id, revision, title, contents, notebook_id, startup, deleted_from_id, rank, user_id from note_current where notebook_id = %s order by rank%s%s;" % ( quote( self.object_id ), limit_clause, offset_clause )
|
|
|
|
def sql_load_notes_in_update_order( self, start = 0, count = None ):
|
|
"""
|
|
Return a SQL string to load a list of all the notes within this notebook.
|
|
Note: If the database backend is SQLite, a start parameter cannot be given without also
|
|
providing a count parameter.
|
|
"""
|
|
if count is not None:
|
|
limit_clause = " limit %s" % count
|
|
else:
|
|
limit_clause = ""
|
|
|
|
if start:
|
|
offset_clause = " offset %s" % start
|
|
else:
|
|
offset_clause = ""
|
|
|
|
return "select id, revision, title, contents, notebook_id, startup, deleted_from_id, rank, user_id from note_current where notebook_id = %s order by revision desc%s%s;" % ( quote( self.object_id ), limit_clause, offset_clause )
|
|
|
|
def sql_load_non_startup_notes( self ):
|
|
"""
|
|
Return a SQL string to load a list of the non-startup notes within this notebook.
|
|
"""
|
|
return "select id, revision, title, contents, notebook_id, startup, deleted_from_id, rank, user_id from note_current where notebook_id = %s and startup = 'f' order by revision desc;" % quote( self.object_id )
|
|
|
|
def sql_load_startup_notes( self ):
|
|
"""
|
|
Return a SQL string to load a list of the startup notes within this notebook.
|
|
"""
|
|
return "select id, revision, title, contents, notebook_id, startup, deleted_from_id, rank, user_id from note_current where notebook_id = %s and startup = 't' order by rank;" % quote( self.object_id )
|
|
|
|
def sql_load_notes_in_creation_order( self, start = 0, count = 10, reverse = False ):
|
|
"""
|
|
Return a SQL string to load a list of the most recently created notes within this notebook.
|
|
|
|
@type start: int or NoneType
|
|
@param start: index of recent note to start with (defaults to 0, the most recent note)
|
|
@type count: int or NoneType
|
|
@param count: number of recent notes to return (defaults to 10 notes)
|
|
@type reverse: bool or NoneType
|
|
@param reverse: whether to reverse the chronological order of notes. so if reverse is True,
|
|
the oldest notes are returned instead of the newest (defaults to False)
|
|
"""
|
|
if reverse:
|
|
ordering = u"asc"
|
|
else:
|
|
ordering = u"desc"
|
|
|
|
return \
|
|
"""
|
|
select
|
|
note_current.id, note_current.revision, note_current.title, note_current.contents,
|
|
note_current.notebook_id, note_current.startup, note_current.deleted_from_id,
|
|
note_current.rank, note_current.user_id, luminotes_user_current.username, note_creation.revision as creation
|
|
from
|
|
note_current, luminotes_user_current,
|
|
( select id, min( revision ) as revision from note where notebook_id = %s group by id ) as note_creation
|
|
where
|
|
notebook_id = %s and note_current.id = note_creation.id and
|
|
note_current.user_id = luminotes_user_current.id
|
|
order by
|
|
creation %s
|
|
limit %d offset %d;
|
|
""" % ( quote( self.object_id ), quote( self.object_id ), ordering, count, start )
|
|
|
|
def sql_load_note_by_id( self, note_id ):
|
|
"""
|
|
Return a SQL string to load a particular note within this notebook by the note's id.
|
|
|
|
@type note_id: unicode
|
|
@param note_id: id of note to load
|
|
"""
|
|
return "select id, revision, title, contents, notebook_id, startup, deleted_from_id, rank, user_id from note_current where notebook_id = %s and id = %s;" % ( quote( self.object_id ), quote( note_id ) )
|
|
|
|
def sql_load_note_by_title( self, title ):
|
|
"""
|
|
Return a SQL string to load a particular note within this notebook by the note's title. The
|
|
title lookup is performed case-insensitively.
|
|
|
|
@type note_id: unicode
|
|
@param note_id: title of note to load
|
|
"""
|
|
return "select id, revision, title, contents, notebook_id, startup, deleted_from_id, rank, user_id from note_current where notebook_id = %s and lower( title ) = lower( %s );" % ( quote( self.object_id ), quote( title ) )
|
|
|
|
@staticmethod
|
|
def sql_search_notes( user_id, first_notebook_id, search_text, database_backend ):
|
|
"""
|
|
Return a SQL string to perform a full-text search for notes within notebooks readable by the
|
|
given user whose contents contain the given search_text. This is a case-insensitive search.
|
|
|
|
@type search_text: unicode
|
|
@param search_text: text to search for within the notes
|
|
"""
|
|
if database_backend == Persistent.POSTGRESQL_BACKEND:
|
|
# strip out all search operators
|
|
search_text = Notebook.SEARCH_OPERATORS.sub( u"", search_text ).strip()
|
|
|
|
# join all words with boolean "and" operator
|
|
search_text = u"&".join( Notebook.WHITESPACE_PATTERN.split( search_text ) )
|
|
|
|
return \
|
|
"""
|
|
select id, revision, title, contents, notebook_id, startup, deleted_from_id, rank, user_id, null,
|
|
null, headline( drop_html_tags( contents ), query ) as summary from (
|
|
select
|
|
note_current.id, note_current.revision, note_current.title, note_current.contents,
|
|
note_current.notebook_id, note_current.startup, note_current.deleted_from_id,
|
|
rank_cd( search, query ) as rank, note_current.user_id, null, null, query
|
|
from
|
|
note_current, user_notebook, to_tsquery( 'default', %s ) query
|
|
where
|
|
note_current.notebook_id = user_notebook.notebook_id and user_notebook.user_id = %s and
|
|
note_current.deleted_from_id is null and
|
|
query @@ search order by note_current.notebook_id = %s desc, rank desc limit 20
|
|
) as sub;
|
|
""" % ( quote( search_text ), quote( user_id ),
|
|
quote( first_notebook_id ) )
|
|
else:
|
|
search_text = search_text.strip().lower()
|
|
|
|
# TODO: use SQLite's FTS (full text search) support instead
|
|
return \
|
|
"""
|
|
select
|
|
note_current.*
|
|
from
|
|
note_current, user_notebook
|
|
where
|
|
note_current.notebook_id = user_notebook.notebook_id and user_notebook.user_id = %s and
|
|
note_current.deleted_from_id is null and
|
|
lower( note_current.contents ) like %s
|
|
order by note_current.notebook_id = %s desc, note_current.rank desc limit 20
|
|
""" % ( quote( user_id ), quote_fuzzy( search_text ), quote( first_notebook_id ) )
|
|
|
|
@staticmethod
|
|
def sql_search_titles( notebook_id, search_text ):
|
|
"""
|
|
Return a SQL string to perform a search for notes within the given notebook whose titles contain
|
|
the given search_text. This is a case-insensitive search.
|
|
|
|
@type search_text: unicode
|
|
@param search_text: text to search for within the notes
|
|
"""
|
|
search_text = search_text.strip()
|
|
|
|
return \
|
|
"""
|
|
select id, revision, title, contents, notebook_id, startup, deleted_from_id, rank, user_id, null, null,
|
|
title as summary
|
|
from
|
|
note_current
|
|
where
|
|
notebook_id = %s and
|
|
deleted_from_id is null and
|
|
lower( title ) like %s
|
|
order by
|
|
revision desc limit 20;
|
|
""" % ( quote( notebook_id ),
|
|
quote_fuzzy( search_text.lower() ) )
|
|
|
|
def sql_highest_note_rank( self ):
|
|
"""
|
|
Return a SQL string to determine the highest numbered rank of all notes in this notebook."
|
|
"""
|
|
return "select coalesce( max( rank ), -1 ) from note_current where notebook_id = %s;" % quote( self.object_id )
|
|
|
|
def sql_count_notes( self ):
|
|
"""
|
|
Return a SQL string to count the total number of notes in this notebook.
|
|
"""
|
|
return \
|
|
"select count( id ) from note_current where notebook_id = %s;" % \
|
|
( quote( self.object_id ) )
|
|
|
|
def sql_load_tag_by_name( self, user_id, tag_name ):
|
|
"""
|
|
Return a SQL string to load a tag associated with this notebook by the given user.
|
|
"""
|
|
return \
|
|
"""
|
|
select
|
|
tag.id, tag.revision, tag.notebook_id, tag.user_id, tag.name, tag.description, tag_notebook.value
|
|
from
|
|
tag_notebook, tag
|
|
where
|
|
tag_notebook.notebook_id = %s and
|
|
tag_notebook.user_id = %s and
|
|
tag_notebook.tag_id = tag.id and
|
|
tag.name = %s
|
|
order by tag.name;
|
|
""" % ( quote( self.object_id ), quote( user_id ), quote( tag_name ) )
|
|
|
|
def sql_load_tags( self, user_id ):
|
|
"""
|
|
Return a SQL string to load a list of all the tags associated with this notebook by the given
|
|
user.
|
|
"""
|
|
return \
|
|
"""
|
|
select
|
|
tag.id, tag.revision, tag.notebook_id, tag.user_id, tag.name, tag.description, tag_notebook.value
|
|
from
|
|
tag_notebook, tag
|
|
where
|
|
tag_notebook.notebook_id = %s and
|
|
tag_notebook.user_id = %s and
|
|
tag_notebook.tag_id = tag.id
|
|
order by tag.name;
|
|
""" % ( quote( self.object_id ), quote( user_id ) )
|
|
|
|
def sql_load_note_ids_starting_from_rank( self, start_note_rank ):
|
|
"""
|
|
Return a SQL string to load a list of all the note ids with rank greater than or equal to the
|
|
given rank.
|
|
"""
|
|
return \
|
|
"""
|
|
select
|
|
id
|
|
from
|
|
note_current
|
|
where
|
|
notebook_id = %s and
|
|
rank is not null and
|
|
rank >= %s;
|
|
""" % ( quote( self.object_id ), start_note_rank )
|
|
|
|
def sql_increment_rank( self, start_note_rank ):
|
|
"""
|
|
Return a SQL string to increment the rank for every note in this notebook (in rank order)
|
|
starting from the given note rank. Notes before the given note rank are not updated.
|
|
"""
|
|
return \
|
|
"""
|
|
update
|
|
note_current
|
|
set
|
|
rank = rank + 1,
|
|
revision = %s
|
|
where
|
|
notebook_id = %s and
|
|
rank is not null and
|
|
rank >= %s;
|
|
""" % ( quote( datetime.now( tz = utc ) ), quote( self.object_id ), start_note_rank )
|
|
|
|
def to_dict( self ):
|
|
d = Persistent.to_dict( self )
|
|
|
|
d.update( dict(
|
|
name = self.__name,
|
|
friendly_id = self.friendly_id,
|
|
trash_id = self.__trash_id,
|
|
read_write = self.__read_write,
|
|
owner = self.__owner,
|
|
deleted = self.__deleted,
|
|
user_id = self.__user_id,
|
|
note_count = self.__note_count,
|
|
tags = self.__tags,
|
|
) )
|
|
|
|
return d
|
|
|
|
def __set_name( self, name ):
|
|
self.__name = name
|
|
self.update_revision()
|
|
|
|
HTML_REFERENCE_PATTERN = re.compile( "&[a-zA-Z]+;|&#\d+;" )
|
|
FRIENDLY_ID_STRIP_PATTERN = re.compile( "[^a-zA-Z0-9\-]+" )
|
|
|
|
def __friendly_id( self ):
|
|
# convert to lowercase, remove HTML character/entity refs, collapse whitespace to dashes, strip
|
|
# other punctuation. strip leading/trailing dashes
|
|
friendly_id = self.HTML_REFERENCE_PATTERN.sub( u" ", self.__name.lower() )
|
|
friendly_id = self.WHITESPACE_PATTERN.sub( u"-", friendly_id )
|
|
return self.FRIENDLY_ID_STRIP_PATTERN.sub( u"", friendly_id ).strip( "-" )
|
|
|
|
def __set_read_write( self, read_write ):
|
|
# The read_write member isn't actually saved to the database, so setting it doesn't need to
|
|
# call update_revision().
|
|
read_write = {
|
|
None: Notebook.READ_WRITE,
|
|
True: Notebook.READ_WRITE,
|
|
False: Notebook.READ_ONLY,
|
|
}.get( read_write, read_write )
|
|
|
|
self.__read_write = read_write
|
|
|
|
def __set_owner( self, owner ):
|
|
# The owner member isn't actually saved to the database, so setting it doesn't need to
|
|
# call update_revision().
|
|
self.__owner = owner
|
|
|
|
def __set_deleted( self, deleted ):
|
|
self.__deleted = deleted
|
|
self.update_revision()
|
|
|
|
def __set_user_id( self, user_id ):
|
|
self.__user_id = user_id
|
|
self.update_revision()
|
|
|
|
def __set_rank( self, rank ):
|
|
# The rank member isn't actually saved to the database, so setting it doesn't need to
|
|
# call update_revision().
|
|
self.__rank = rank
|
|
|
|
def __set_tags( self, tags ):
|
|
# The tags member isn't actually saved to the database, so setting it doesn't need to
|
|
# call update_revision().
|
|
self.__tags = tags
|
|
|
|
name = property( lambda self: self.__name, __set_name )
|
|
friendly_id = property( __friendly_id )
|
|
trash_id = property( lambda self: self.__trash_id )
|
|
read_write = property( lambda self: self.__read_write, __set_read_write )
|
|
owner = property( lambda self: self.__owner, __set_owner )
|
|
deleted = property( lambda self: self.__deleted, __set_deleted )
|
|
user_id = property( lambda self: self.__user_id, __set_user_id )
|
|
rank = property( lambda self: self.__rank, __set_rank )
|
|
note_count = property( lambda self: self.__note_count )
|
|
tags = property( lambda self: self.__tags, __set_tags )
|