witten
/
luminotes
Archived
1
0
Fork 0

More work on the discussion forums.

This commit is contained in:
Dan Helfman 2008-10-30 15:26:27 -07:00
parent d62b885e46
commit b4a40d2c25
7 changed files with 231 additions and 59 deletions

1
NEWS
View File

@ -7,6 +7,7 @@
* Laid some of the foundational groundwork for future tags support.
* Made the subscription pricing page a little less confusing by hiding some
of the bigger plans by default.
* Increased the limit on characters per note from 25,000 to 50,000.
* Fixed an occasional bug that caused unexpected logouts. The solution was
to move the session information into the database where it could be
properly locked.

View File

@ -1,9 +1,10 @@
import cherrypy
from model.User import User
from model.Notebook import Notebook
from model.Note import Note
from model.Tag import Tag
from Expose import expose
from Validate import validate, Valid_string
from Validate import validate, Valid_string, Valid_int
from Database import Valid_id, end_transaction
from Users import grab_user_id
from Notebooks import Notebooks
@ -64,6 +65,8 @@ class Forums( object ):
class Forum( object ):
DEFAULT_THREAD_NAME = u"new discussion"
def __init__( self, database, notebooks, users, name ):
"""
Create a new Forum object, representing a single forum.
@ -108,12 +111,13 @@ class Forum( object ):
if anonymous is None:
raise Access_error()
threads = self.__database.select_many(
# load a list of the threads in this forum, excluding those with a default name
threads = [ thread for thread in self.__database.select_many(
Notebook,
anonymous.sql_load_notebooks(
parents_only = False, undeleted_only = True, tag_name = u"forum", tag_value = self.__name
)
)
) if thread.name != self.DEFAULT_THREAD_NAME ]
# put threads in reverse chronological order by creation date
threads.reverse()
@ -126,9 +130,40 @@ class Forum( object ):
result[ "threads" ] = threads
return result
# default() is just an alias for Notebooks.default()
def default( self, *args, **kwargs ):
return self.__notebooks.default( *args, **kwargs )
@expose( view = Main_page )
@end_transaction
@grab_user_id
@validate(
thread_id = Valid_id(),
start = Valid_int( min = 0 ),
count = Valid_int( min = 1, max = 50 ),
note_id = Valid_id( none_okay = True ),
user_id = Valid_id( none_okay = True ),
)
def default( self, thread_id, start = 0, count = 10, note_id = None, user_id = None ):
"""
Provide the information necessary to display a forum thread.
@type thread_id: unicode
@param thread_id: id of thread notebook to display
@type start: unicode 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 display (defaults to 10 notes)
@type note_id: unicode or NoneType
@param note_id: id of single note to load (optional)
@rtype: unicode
@return: rendered HTML page
@raise Validation_error: one of the arguments is invalid
"""
result = self.__users.current( user_id )
result.update( self.__notebooks.old_notes( thread_id, start, count, user_id ) )
# if a single note was requested, just return that one note
if note_id:
result[ "notes" ] = [ note for note in result[ "notes" ] if note.object_id == note_id ]
return result
default.exposed = True
@ -140,7 +175,8 @@ class Forum( object ):
)
def create_thread( self, user_id ):
"""
Create a new forum post and give it a default name. Then redirect to that new post thread.
Create a new forum thread with a blank post, and give the thread a default name. Then redirect
to that new thread.
@type user_id: unicode or NoneType
@param user_id: id of current logged-in user (if any)
@ -162,7 +198,7 @@ class Forum( object ):
# create the new notebook thread
thread_id = self.__database.next_id( Notebook, commit = False )
thread = Notebook.create( thread_id, u"new forum post", user_id = user.object_id )
thread = Notebook.create( thread_id, self.DEFAULT_THREAD_NAME, user_id = user.object_id )
self.__database.save( thread, commit = False )
# associate the forum tag with the new notebook thread
@ -178,6 +214,11 @@ class Forum( object ):
commit = False,
)
# create a blank post in which the user can start off the thread
note_id = self.__database.next_id( Notebook, commit = False )
note = Note.create( note_id, u"<h3>", notebook_id = thread_id, startup = True, rank = 0, user_id = user_id )
self.__database.save( note, commit = False )
self.__database.commit()
return dict(

View File

@ -656,7 +656,7 @@ class Notebooks( object ):
@validate(
notebook_id = Valid_id(),
note_id = Valid_id(),
contents = Valid_string( min = 1, max = 25000, escape_html = False ),
contents = Valid_string( min = 1, max = 50000, escape_html = False ),
startup = Valid_bool(),
previous_revision = Valid_revision( none_okay = True ),
user_id = Valid_id( none_okay = True ),
@ -685,7 +685,8 @@ class Notebooks( object ):
@return: {
'new_revision': User_revision of saved note, or None if nothing was saved
'previous_revision': User_revision immediately before new_revision, or None if the note is new
'storage_bytes': current storage usage by user,
'storage_bytes': current storage usage by user
'rank': integer rank of the saved note, or None
}
@raise Access_error: the current user doesn't have access to the given notebook
@raise Validation_error: one of the arguments is invalid
@ -768,6 +769,7 @@ class Notebooks( object ):
new_revision = new_revision,
previous_revision = previous_revision,
storage_bytes = user and user.storage_bytes or 0,
rank = note.rank,
)
@expose( view = Json )
@ -1332,6 +1334,13 @@ class Notebooks( object ):
@raise Validation_error: one of the arguments is invalid
"""
notebook = self.__users.load_notebook( user_id, notebook_id, read_write = True, owner = True )
# special case to allow the creator of a READ_WRITE_FOR_OWN_NOTES notebook to rename it
if notebook is None:
notebook = self.__users.load_notebook( user_id, notebook_id, read_write = True )
if not ( notebook.read_write == Notebook.READ_WRITE_FOR_OWN_NOTES and notebook.user_id == user_id ):
raise Access_error()
user = self.__database.load( User, user_id )
if not user or not notebook:
@ -1670,7 +1679,7 @@ class Notebooks( object ):
def recent_notes( self, notebook_id, start = 0, count = 10, user_id = None ):
"""
Return the given notebook's recently updated notes in reverse chronological order by creation
Return the given notebook's recently created notes in reverse chronological order by creation
time.
@type notebook_id: unicode
@ -1690,11 +1699,42 @@ class Notebooks( object ):
if notebook is None:
raise Access_error()
recent_notes = self.__database.select_many( Note, notebook.sql_load_recent_notes( start, count ) )
notes = self.__database.select_many( Note, notebook.sql_load_recent_notes( start, count ) )
result = self.__users.current( user_id )
result.update( self.contents( notebook_id, user_id = user_id ) )
result[ "notes" ] = recent_notes
result[ "notes" ] = notes
result[ "start" ] = start
result[ "count" ] = count
return result
def old_notes( self, notebook_id, start = 0, count = 10, user_id = None ):
"""
Return the given notebook's oldest notes in chronological order by creation time.
@type notebook_id: unicode
@param notebook_id: id of the notebook containing the notes
@type start: unicode or NoneType
@param start: index of recent note to start with (defaults to 0, the oldest note)
@type count: int or NoneType
@param count: number of notes to return (defaults to 10 notes)
@type user_id: unicode or NoneType
@param user_id: id of current logged-in user (if any)
@rtype: dict
@return: data for Main_page() constructor
@raise Access_error: the current user doesn't have access to the given notebook or note
"""
notebook = self.__users.load_notebook( user_id, notebook_id )
if notebook is None:
raise Access_error()
notes = self.__database.select_many( Note, notebook.sql_load_recent_notes( start, count, reverse = True ) )
result = self.__users.current( user_id )
result.update( self.contents( notebook_id, user_id = user_id ) )
result[ "notes" ] = notes
result[ "start" ] = start
result[ "count" ] = count
@ -1813,7 +1853,7 @@ class Notebooks( object ):
else:
break
contents = Valid_string( max = 25000, escape_html = plaintext, require_link_target = True )( row[ content_column ] )
contents = Valid_string( max = 50000, escape_html = plaintext, require_link_target = True )( row[ content_column ] )
if plaintext:
contents = contents.replace( u"\n", u"<br />" )

View File

@ -1912,7 +1912,7 @@ class Test_notebooks( Test_controller ):
# save over an existing note supplying new (too long) contents and a new title
previous_revision = self.note.revision
new_note_contents = u"<h3>new title</h3>new blah" * 962
new_note_contents = u"<h3>new title</h3>new blah" * 1923
result = self.http_post( "/notebooks/save_note/", dict(
notebook_id = self.notebook.object_id,
note_id = self.note.object_id,

View File

@ -151,7 +151,7 @@ class Notebook( Persistent ):
"""
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_recent_notes( self, start = 0, count = 10 ):
def sql_load_recent_notes( self, start = 0, count = 10, reverse = False ):
"""
Return a SQL string to load a list of the most recently created notes within this notebook.
@ -159,7 +159,15 @@ class Notebook( Persistent ):
@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
@ -172,9 +180,9 @@ class Notebook( Persistent ):
where
notebook_id = %s and note_current.id = note_creation.id
order by
creation desc
creation %s
limit %d offset %d;
""" % ( quote( self.object_id ), quote( self.object_id ), count, start )
""" % ( quote( self.object_id ), quote( self.object_id ), ordering, count, start )
def sql_load_note_by_id( self, note_id ):
"""

View File

@ -27,7 +27,7 @@ function Wiki( invoker ) {
this.font_size = null;
this.small_toolbar = false;
this.large_toolbar_bottom = 0;
this.autosaver = Autosaver( this );
this.autosaver = null;
var total_notes_count_node = getElement( "total_notes_count" );
if ( total_notes_count_node )
@ -70,6 +70,10 @@ function Wiki( invoker ) {
this.display_message( "Luminotes support for your web browser (" + beta_agent + ") is currently in beta. If you encounter any problems, please contact support so that they can be fixed!" );
}
if ( this.notebook.read_write != NOTEBOOK_READ_WRITE_FOR_OWN_NOTES ) {
this.autosaver = Autosaver( this );
}
var deleted_id = getElement( "deleted_id" ).value;
var skip_empty_message = deleted_id ? true : false;
@ -296,13 +300,18 @@ Wiki.prototype.populate = function ( startup_notes, current_notes, note_read_wri
for ( var i in current_notes ) {
var note = current_notes[ i ];
if ( !note_read_write )
var read_write = NOTEBOOK_READ_ONLY;
else
var read_write = this.notebook.read_write;
this.create_editor(
note.object_id,
getElement( "static_note_" + note.object_id ).innerHTML,
note.deleted_from_id,
note.revision,
note.creation,
this.notebook.read_write != NOTEBOOK_READ_ONLY && note_read_write, false, focus, null,
read_write, false, focus, null,
note.user_id
);
focus = false;
@ -317,15 +326,7 @@ Wiki.prototype.populate = function ( startup_notes, current_notes, note_read_wri
if ( this.notebook.read_write != NOTEBOOK_READ_ONLY ) {
connect( window, "onunload", function ( event ) { self.editor_focused( null, true ); } );
if ( this.notebook.read_write == NOTEBOOK_READ_WRITE ||
( this.notebook.read_write == NOTEBOOK_READ_WRITE_FOR_OWN_NOTES &&
this.user.username && this.user.username != "anonymous" )
)
connect( "newNote", "onclick", this, "create_blank_editor" );
else
connect( "newNote", "onclick", function( event ) { self.display_message( 'Please login first. No account? Click "sign up".' ) } );
connect( "newNote", "onclick", this, "create_blank_editor" );
connect( "createLink", "onclick", this, "toggle_link_button" );
if ( this.notebook.read_write == NOTEBOOK_READ_WRITE )
connect( "attachFile", "onclick", this, "toggle_attach_button" );
@ -451,6 +452,12 @@ Wiki.prototype.background_clicked = function ( event ) {
Wiki.prototype.create_blank_editor = function ( event ) {
if ( event ) event.stop();
if ( this.notebook.read_write == NOTEBOOK_READ_WRITE_FOR_OWN_NOTES &&
( !this.user.username || this.user.username == "anonymous" ) ) {
this.display_message( 'Please login first. No account? Click "sign up".' );
return;
}
this.clear_messages();
this.clear_pulldowns();
@ -565,6 +572,13 @@ Wiki.prototype.load_editor = function ( note_title, note_id, revision, previous_
}
}
// if the notebook's read_write is NOTEBOOK_READ_WRITE_FOR_OWN_NOTES, then instead of opening
// a new post, display an error message
if ( this.notebook.read_write == NOTEBOOK_READ_WRITE_FOR_OWN_NOTES ) {
this.display_message( "No such forum post! (A forum link must point to another post in this discussion or an external web page.)" );
return;
}
this.invoker.invoke(
"/notebooks/load_note_by_title", "GET", {
"notebook_id": this.notebook_id,
@ -576,6 +590,13 @@ Wiki.prototype.load_editor = function ( note_title, note_id, revision, previous_
return;
}
// if the notebook's read_write is NOTEBOOK_READ_WRITE_FOR_OWN_NOTES, maintain displayed note
// order by opening an existing note on its own page
if ( this.notebook.read_write == NOTEBOOK_READ_WRITE_FOR_OWN_NOTES ) {
window.location = window.location.protocol + '//' + window.location.host + window.location.pathname + '?note_id=' + note_id;
return;
}
this.invoker.invoke(
"/notebooks/load_note", "GET", {
"notebook_id": this.notebook_id,
@ -789,7 +810,14 @@ Wiki.prototype.create_editor = function ( id, note_text, deleted_from_id, revisi
if ( !read_write && creation ) {
var short_creation = this.brief_revision( creation );
note_text = '<p>' + short_creation + ' | <a href="/blog?note_id=' + id + '" target="_top">permalink</a></p>' + note_text;
if ( user_id )
var by = ' by ' + user_id;
else
var by = '';
note_text = '<p class="small_text">Posted' + by + ' on ' + short_creation +
' | <a href="' + window.location.protocol + '//' + window.location.host + window.location.pathname +
'?note_id=' + id + '" target="_top">permalink</a></p>' + note_text;
}
var startup = this.startup_notes[ id ];
@ -1560,6 +1588,10 @@ Wiki.prototype.save_editor = function ( editor, fire_and_forget, callback, synch
else if ( self.startup_notes[ editor.id ] )
delete self.startup_notes[ editor.id ];
// special case to rename a NOTEBOOK_READ_WRITE_FOR_OWN_NOTES when its first note is renamed
if ( result.rank == 0 && self.notebook.read_write == NOTEBOOK_READ_WRITE_FOR_OWN_NOTES )
self.end_notebook_rename( editor.title, true );
if ( callback )
callback();
if ( !suppress_save_signal )
@ -2726,8 +2758,9 @@ Wiki.prototype.start_notebook_rename = function () {
notebook_name_field.select();
}
Wiki.prototype.end_notebook_rename = function () {
var new_notebook_name = getElement( "notebook_name_field" ).value;
Wiki.prototype.end_notebook_rename = function ( new_notebook_name, prevent_rename_on_click ) {
if ( !new_notebook_name )
new_notebook_name = getElement( "notebook_name_field" ).value;
// if the new name is blank or reserved, don't actually rename the notebook
if ( /^\s*$/.test( new_notebook_name ) )
@ -2739,24 +2772,32 @@ Wiki.prototype.end_notebook_rename = function () {
}
// rename the notebook in the header
var notebook_header_name = createDOM(
"span",
{ "id": "notebook_header_name", "title": "Rename this notebook." },
createDOM( "strong", {}, new_notebook_name )
);
replaceChildNodes( "notebook_header_area", notebook_header_name );
if ( prevent_rename_on_click ) {
var notebook_header_name = createDOM(
"span", {},
createDOM( "strong", {}, new_notebook_name )
);
replaceChildNodes( "notebook_header_area", notebook_header_name );
} else {
var notebook_header_name = createDOM(
"span",
{ "id": "notebook_header_name", "title": "Rename this notebook." },
createDOM( "strong", {}, new_notebook_name )
);
replaceChildNodes( "notebook_header_area", notebook_header_name );
var self = this;
connect( notebook_header_name, "onclick", function ( event ) {
self.start_notebook_rename();
event.stop();
} );
var self = this;
connect( notebook_header_name, "onclick", function ( event ) {
self.start_notebook_rename();
event.stop();
} );
}
// rename the notebook link on the right side of the page
replaceChildNodes(
"notebook_" + this.notebook.object_id,
document.createTextNode( new_notebook_name )
);
var notebook_link = getElement( "notebook_" + this.notebook.object_id );
if ( notebook_link ) {
replaceChildNodes( notebook_link, document.createTextNode( new_notebook_name ) );
}
// rename the notebook within the rss link (if any)
var notebook_rss_link = getElement( "notebook_rss_link" );

View File

@ -68,6 +68,7 @@ class Main_page( Page ):
u"object_id" : note.object_id,
u"revision" : note.revision,
u"deleted_from_id" : note.deleted_from_id,
u"user_id": note.user_id,
u"creation" : note.creation,
} for note in notes ]
@ -89,6 +90,8 @@ class Main_page( Page ):
else:
updates_path = None
forum_tags = [ tag for tag in notebook.tags if tag.name == u"forum" ]
if notebook.name == u"Luminotes":
notebook_path = u"/"
updates_path = None # no RSS feed for the main notebook
@ -96,6 +99,9 @@ class Main_page( Page ):
notebook_path = u"/guide"
elif notebook.name == u"Luminotes blog":
notebook_path = u"/blog"
elif forum_tags:
forum_tag = forum_tags[ 0 ]
notebook_path = u"/forums/%s/%s" % ( forum_tag.value, notebook.object_id )
else:
notebook_path = u"/notebooks/%s" % notebook.object_id
@ -203,11 +209,14 @@ class Main_page( Page ):
Div(
id = u"deleted_notebooks",
),
self.page_navigation( notebook_path, len( notes ), total_notes_count, start, count ),
( notebook.read_write == Notebook.READ_WRITE_FOR_OWN_NOTES ) and \
Div( u"When you're done with your comment, click the save button to publish it.", class_ = u"small_text" ) or None,
Div(
Span( id = u"notes_top" ),
id = u"notes",
),
( notebook.read_write != Notebook.READ_ONLY ) and Div(
( notebook.read_write == Notebook.READ_WRITE ) and Div(
id = u"blank_note_stub",
class_ = u"blank_note_stub_hidden_border",
) or None,
@ -220,17 +229,7 @@ class Main_page( Page ):
u"document.getElementById( 'static_notes' ).style.display = 'none';",
type = u"text/javascript",
),
# make page navigation for those notebooks that require it (such as the blog)
( start is not None and count is not None and len( notes ) > 1 ) and Div(
( start > 0 ) and Div( A(
u"previous page",
href = "%s?start=%d&count=%d" % ( notebook_path, max( start - count, 0 ), count ),
) ) or None,
( start + count < total_notes_count ) and Div( A(
u"next page",
href = "%s?start=%d&count=%d" % ( notebook_path, min( start + count, total_notes_count - 1 ), count ),
) ) or None,
) or None,
self.page_navigation( notebook_path, len( notes ), total_notes_count, start, count, top = False ),
id = u"notebook_background",
corners = ( u"tl", ),
),
@ -246,3 +245,45 @@ class Main_page( Page ):
id = u"everything_area",
),
)
def page_navigation( self, notebook_path, displayed_notes_count, total_notes_count, start, notes_per_page, top = True ):
if start is None or notes_per_page is None:
return None
if displayed_notes_count == 1 and displayed_notes_count < total_notes_count:
if top is True:
return None
return Div(
Span(
A(
u"return to the discussion",
href = "%s" % notebook_path,
),
),
)
if start == 0 and notes_per_page >= total_notes_count:
return None
return Div(
( start > 0 ) and Span(
A(
u"previous",
href = "%s?start=%d&count=%d" % ( notebook_path, max( start - notes_per_page, 0 ), notes_per_page ),
),
u" | ",
) or None,
[ Span(
( start == page_start ) and Strong( unicode( page_number + 1 ) ) or A(
Strong( unicode( page_number + 1 ) ),
href = "%s?start=%d&count=%d" % ( notebook_path, page_start, notes_per_page ),
),
) for ( page_number, page_start ) in enumerate( range( 0, total_notes_count, notes_per_page ) ) ],
( start + notes_per_page < total_notes_count ) and Span(
u" | ",
A(
u"next",
href = "%s?start=%d&count=%d" % ( notebook_path, min( start + notes_per_page, total_notes_count - 1 ), notes_per_page ),
),
) or None,
)