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 Expose import expose
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 Users import grab_user_id
from Notebooks import Notebooks
@ -165,7 +165,7 @@ class Forum( object ):
@end_transaction
@grab_user_id
@validate(
thread_id = Valid_id(),
thread_id = unicode,
start = Valid_int( min = 0 ),
count = Valid_int( min = 1, max = 50 ),
note_id = Valid_id( none_okay = True ),
@ -176,7 +176,7 @@ class Forum( object ):
Provide the information necessary to display a forum thread.
@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
@param start: index of recent note to start with (defaults to 0, the most recent note)
@type count: int or NoneType
@ -187,6 +187,26 @@ class Forum( object ):
@return: rendered HTML page
@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.update( self.__notebooks.old_notes( thread_id, start, count, user_id ) )
@ -196,8 +216,6 @@ class Forum( object ):
return result
default.exposed = True
@expose()
@end_transaction
@grab_user_id

View File

@ -1,4 +1,5 @@
import cherrypy
import re
from cgi import escape
from Html_cleaner import Html_cleaner
@ -167,6 +168,16 @@ class Valid_int( object ):
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 ):
"""
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 )
headers = result.get( "headers" )
assert headers
assert headers.get( "Location" ) == u"http:///login?after_login=%s" % urllib.quote( path )
assert headers.get( "Status" ) == u"404 Not Found"
def __make_notes( self ):
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" ).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 ):
result = self.http_get(
"/guide",

View File

@ -126,6 +126,10 @@ class Notebook( Persistent ):
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( self, start = 0, count = None ):
"""
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(
name = self.__name,
friendly_id = self.friendly_id,
trash_id = self.__trash_id,
read_write = self.__read_write,
owner = self.__owner,
@ -355,6 +360,12 @@ class Notebook( Persistent ):
self.__name = name
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 ):
# The read_write member isn't actually saved to the database, so setting it doesn't need to
# call update_revision().
@ -390,6 +401,7 @@ class Notebook( Persistent ):
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 )

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 FUNCTION drop_html_tags( text );
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;
$_$ language plpgsql;
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 (
id text NOT NULL,
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 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 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.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 ):
original_revision = self.notebook.revision
self.notebook.read_write = Notebook.READ_WRITE_FOR_OWN_NOTES
@ -233,6 +237,7 @@ class Test_notebook( object ):
d = self.notebook.to_dict()
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( "read_write" ) == self.read_write
assert d.get( "deleted" ) == self.notebook.deleted

View File

@ -49,7 +49,7 @@ class Forum_page( Product_page ):
[ Div(
A(
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(
self.post_count( thread, forum_name ),

View File

@ -103,7 +103,10 @@ class Main_page( Page ):
notebook_path = u"/guide"
elif forum_tags:
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:
notebook_path = u"/notebooks/%s" % notebook.object_id