witten
/
luminotes
Archived
1
0
Fork 0

Blog post URLs are now user-friendly and SEO-friendly.

This commit is contained in:
Dan Helfman 2008-11-18 15:11:58 -08:00
parent bf22f8a950
commit d3e040d984
11 changed files with 68 additions and 36 deletions

View File

@ -6,7 +6,7 @@ from model.Note import Note
from model.Tag import Tag from model.Tag import Tag
from Expose import expose from Expose import expose
from Expire import strongly_expire from Expire import strongly_expire
from Validate import validate, Valid_string, Valid_int from Validate import validate, Valid_string, Valid_int, Valid_friendly_id
from Database import Valid_id, end_transaction from Database import Valid_id, end_transaction
from Users import grab_user_id from Users import grab_user_id
from Notebooks import Notebooks from Notebooks import Notebooks
@ -165,7 +165,7 @@ class Forum( object ):
@end_transaction @end_transaction
@grab_user_id @grab_user_id
@validate( @validate(
thread_id = Valid_id(), thread_id = unicode,
start = Valid_int( min = 0 ), start = Valid_int( min = 0 ),
count = Valid_int( min = 1, max = 50 ), count = Valid_int( min = 1, max = 50 ),
note_id = Valid_id( none_okay = True ), note_id = Valid_id( none_okay = True ),
@ -176,7 +176,7 @@ class Forum( object ):
Provide the information necessary to display a forum thread. Provide the information necessary to display a forum thread.
@type thread_id: unicode @type thread_id: unicode
@param thread_id: id of thread notebook to display @param thread_id: id or "friendly id" of thread notebook to display
@type start: unicode or NoneType @type start: unicode or NoneType
@param start: index of recent note to start with (defaults to 0, the most recent note) @param start: index of recent note to start with (defaults to 0, the most recent note)
@type count: int or NoneType @type count: int or NoneType
@ -187,6 +187,26 @@ class Forum( object ):
@return: rendered HTML page @return: rendered HTML page
@raise Validation_error: one of the arguments is invalid @raise Validation_error: one of the arguments is invalid
""" """
# first try loading the thread by id, and then if not found, try loading by "friendly id"
try:
Valid_id()( thread_id )
if not self.__database.load( Notebook, thread_id ):
raise ValueError()
except ValueError:
try:
Valid_friendly_id()( thread_id )
except ValueError:
raise cherrypy.NotFound
try:
thread = self.__database.select_one( Notebook, Notebook.sql_load_by_friendly_id( thread_id ) )
except:
raise cherrypy.NotFound
if not thread:
raise cherrypy.NotFound
thread_id = thread.object_id
result = self.__users.current( user_id ) result = self.__users.current( user_id )
result.update( self.__notebooks.old_notes( thread_id, start, count, user_id ) ) result.update( self.__notebooks.old_notes( thread_id, start, count, user_id ) )
@ -196,8 +216,6 @@ class Forum( object ):
return result return result
default.exposed = True
@expose() @expose()
@end_transaction @end_transaction
@grab_user_id @grab_user_id

View File

@ -1,4 +1,5 @@
import cherrypy import cherrypy
import re
from cgi import escape from cgi import escape
from Html_cleaner import Html_cleaner from Html_cleaner import Html_cleaner
@ -167,6 +168,16 @@ class Valid_int( object ):
return value return value
class Valid_friendly_id( object ):
FRIENDLY_ID_PATTERN = re.compile( "^[a-zA-Z0-9\-]+$" )
def __call__( self, value ):
if self.FRIENDLY_ID_PATTERN.search( value ):
return value
raise ValueError()
def validate( **expected ): def validate( **expected ):
""" """
validate() can be used to require that the arguments of the decorated method successfully pass validate() can be used to require that the arguments of the decorated method successfully pass

View File

@ -300,8 +300,7 @@ class Test_forums( Test_controller ):
result = self.http_get( path ) result = self.http_get( path )
headers = result.get( "headers" ) headers = result.get( "headers" )
assert headers assert headers.get( "Status" ) == u"404 Not Found"
assert headers.get( "Location" ) == u"http:///login?after_login=%s" % urllib.quote( path )
def __make_notes( self ): def __make_notes( self ):
note_id = self.database.next_id( Note, commit = False ) note_id = self.database.next_id( Note, commit = False )

View File

@ -367,33 +367,6 @@ class Test_root( Test_controller ):
assert result.get( "redirect" ) assert result.get( "redirect" )
assert result.get( "redirect" ).startswith( "https://" ) assert result.get( "redirect" ).startswith( "https://" )
def test_blog( self ):
result = self.http_get(
"/blog",
)
assert result
assert u"error" not in result
assert result[ u"notebook" ].object_id == self.blog_notebook.object_id
def test_blog_with_note_id( self ):
result = self.http_get(
"/blog?note_id=%s" % self.blog_note.object_id,
)
assert result
assert u"error" not in result
assert result[ u"notebook" ].object_id == self.blog_notebook.object_id
def test_blog_rss( self ):
result = self.http_get(
"/blog?rss",
)
assert result
assert u"error" not in result
assert result[ u"notebook" ].object_id == self.blog_notebook.object_id
def test_guide( self ): def test_guide( self ):
result = self.http_get( result = self.http_get(
"/guide", "/guide",

View File

@ -126,6 +126,10 @@ class Notebook( Persistent ):
def sql_update( self ): def sql_update( self ):
return self.sql_create() 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( self, start = 0, count = None ): def sql_load_notes( self, start = 0, count = None ):
""" """
Return a SQL string to load a list of all the notes within this notebook. Return a SQL string to load a list of all the notes within this notebook.
@ -340,6 +344,7 @@ class Notebook( Persistent ):
d.update( dict( d.update( dict(
name = self.__name, name = self.__name,
friendly_id = self.friendly_id,
trash_id = self.__trash_id, trash_id = self.__trash_id,
read_write = self.__read_write, read_write = self.__read_write,
owner = self.__owner, owner = self.__owner,
@ -355,6 +360,12 @@ class Notebook( Persistent ):
self.__name = name self.__name = name
self.update_revision() self.update_revision()
FRIENDLY_ID_STRIP_PATTERN = re.compile( "[^a-zA-Z0-9\-]+" )
def __friendly_id( self ):
friendly_id = self.WHITESPACE_PATTERN.sub( u"-", self.__name.lower() )
return self.FRIENDLY_ID_STRIP_PATTERN.sub( u"", friendly_id )
def __set_read_write( self, read_write ): 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 # The read_write member isn't actually saved to the database, so setting it doesn't need to
# call update_revision(). # call update_revision().
@ -390,6 +401,7 @@ class Notebook( Persistent ):
self.__tags = tags self.__tags = tags
name = property( lambda self: self.__name, __set_name ) name = property( lambda self: self.__name, __set_name )
friendly_id = property( __friendly_id )
trash_id = property( lambda self: self.__trash_id ) trash_id = property( lambda self: self.__trash_id )
read_write = property( lambda self: self.__read_write, __set_read_write ) read_write = property( lambda self: self.__read_write, __set_read_write )
owner = property( lambda self: self.__owner, __set_owner ) owner = property( lambda self: self.__owner, __set_owner )

4
model/delta/1.5.7.sql Normal file
View File

@ -0,0 +1,4 @@
CREATE FUNCTION friendly_id(text) RETURNS text
AS $_$select regexp_replace( regexp_replace( lower( $1 ), '\\s+', '-', 'g' ), '[^a-zA-Z0-9\\-]', '', 'g' );$_$
LANGUAGE sql IMMUTABLE;
CREATE INDEX notebook_friendly_id_index ON notebook USING btree (friendly_id(name));

View File

@ -19,3 +19,4 @@ DROP TABLE schema_version;
DROP TABLE session; DROP TABLE session;
DROP FUNCTION drop_html_tags( text ); DROP FUNCTION drop_html_tags( text );
DROP FUNCTION log_note_revision(); DROP FUNCTION log_note_revision();
DROP FUNCTION friendly_id(text);

View File

@ -25,6 +25,10 @@ create function log_note_revision() returns trigger as $_$
end; end;
$_$ language plpgsql; $_$ language plpgsql;
ALTER FUNCTION public.log_note_revision() OWNER TO luminotes; ALTER FUNCTION public.log_note_revision() OWNER TO luminotes;
CREATE FUNCTION friendly_id(text) RETURNS text
AS $_$select regexp_replace( regexp_replace( lower( $1 ), '\\s+', '-', 'g' ), '[^a-zA-Z0-9\\-]', '', 'g' );$_$
LANGUAGE sql IMMUTABLE;
ALTER FUNCTION public.friendly_id(text) OWNER TO luminotes;
CREATE TABLE file ( CREATE TABLE file (
id text NOT NULL, id text NOT NULL,
revision timestamp with time zone, revision timestamp with time zone,
@ -235,6 +239,8 @@ CREATE INDEX note_current_user_id_index ON note_current USING btree (user_id);
CREATE INDEX note_current_search_index ON note_current USING gist (search); CREATE INDEX note_current_search_index ON note_current USING gist (search);
CREATE INDEX notebook_friendly_id_index ON notebook USING btree (friendly_id(name));
CREATE INDEX password_reset_email_address_index ON password_reset USING btree (email_address); CREATE INDEX password_reset_email_address_index ON password_reset USING btree (email_address);
CREATE INDEX download_access_transaction_id_index ON download_access USING btree (transaction_id); CREATE INDEX download_access_transaction_id_index ON download_access USING btree (transaction_id);

View File

@ -173,6 +173,10 @@ class Test_notebook( object ):
assert self.notebook.name == new_name assert self.notebook.name == new_name
assert self.notebook.revision > previous_revision assert self.notebook.revision > previous_revision
def test_friendly_id( self ):
self.notebook.name = u"This is Bob's notebook!"
assert self.notebook.friendly_id == u"this-is-bobs-notebook"
def test_set_read_write( self ): def test_set_read_write( self ):
original_revision = self.notebook.revision original_revision = self.notebook.revision
self.notebook.read_write = Notebook.READ_WRITE_FOR_OWN_NOTES self.notebook.read_write = Notebook.READ_WRITE_FOR_OWN_NOTES
@ -233,6 +237,7 @@ class Test_notebook( object ):
d = self.notebook.to_dict() d = self.notebook.to_dict()
assert d.get( "name" ) == self.name assert d.get( "name" ) == self.name
assert d.get( "friendly_id" ) == u"my-notebook"
assert d.get( "trash_id" ) == self.trash.object_id assert d.get( "trash_id" ) == self.trash.object_id
assert d.get( "read_write" ) == self.read_write assert d.get( "read_write" ) == self.read_write
assert d.get( "deleted" ) == self.notebook.deleted assert d.get( "deleted" ) == self.notebook.deleted

View File

@ -49,7 +49,7 @@ class Forum_page( Product_page ):
[ Div( [ Div(
A( A(
thread.name, thread.name,
href = os.path.join( base_path, thread.object_id ), href = os.path.join( base_path, ( forum_name == u"blog" ) and thread.friendly_id or thread.object_id ),
), ),
Span( Span(
self.post_count( thread, forum_name ), self.post_count( thread, forum_name ),

View File

@ -103,7 +103,10 @@ class Main_page( Page ):
notebook_path = u"/guide" notebook_path = u"/guide"
elif forum_tags: elif forum_tags:
forum_tag = forum_tags[ 0 ] forum_tag = forum_tags[ 0 ]
notebook_path = u"/forums/%s/%s" % ( forum_tag.value, notebook.object_id ) if forum_tag.value == u"blog":
notebook_path = u"/blog/%s" % notebook.friendly_id
else:
notebook_path = u"/forums/%s/%s" % ( forum_tag.value, notebook.object_id )
else: else:
notebook_path = u"/notebooks/%s" % notebook.object_id notebook_path = u"/notebooks/%s" % notebook.object_id