Browse Source

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

Dan Helfman 9 years ago
parent
commit
d3e040d984

+ 23
- 5
controller/Forums.py View File

@@ -6,7 +6,7 @@ from model.Note import Note
6 6
 from model.Tag import Tag
7 7
 from Expose import expose
8 8
 from Expire import strongly_expire
9
-from Validate import validate, Valid_string, Valid_int
9
+from Validate import validate, Valid_string, Valid_int, Valid_friendly_id
10 10
 from Database import Valid_id, end_transaction
11 11
 from Users import grab_user_id
12 12
 from Notebooks import Notebooks
@@ -165,7 +165,7 @@ class Forum( object ):
165 165
   @end_transaction
166 166
   @grab_user_id
167 167
   @validate(
168
-    thread_id = Valid_id(),
168
+    thread_id = unicode,
169 169
     start = Valid_int( min = 0 ),
170 170
     count = Valid_int( min = 1, max = 50 ),
171 171
     note_id = Valid_id( none_okay = True ),
@@ -176,7 +176,7 @@ class Forum( object ):
176 176
     Provide the information necessary to display a forum thread.
177 177
 
178 178
     @type thread_id: unicode
179
-    @param thread_id: id of thread notebook to display
179
+    @param thread_id: id or "friendly id" of thread notebook to display
180 180
     @type start: unicode or NoneType
181 181
     @param start: index of recent note to start with (defaults to 0, the most recent note)
182 182
     @type count: int or NoneType
@@ -187,6 +187,26 @@ class Forum( object ):
187 187
     @return: rendered HTML page
188 188
     @raise Validation_error: one of the arguments is invalid
189 189
     """
190
+    # first try loading the thread by id, and then if not found, try loading by "friendly id"
191
+    try:
192
+      Valid_id()( thread_id )
193
+      if not self.__database.load( Notebook, thread_id ):
194
+        raise ValueError()
195
+    except ValueError:
196
+      try:
197
+        Valid_friendly_id()( thread_id )
198
+      except ValueError:
199
+        raise cherrypy.NotFound
200
+
201
+      try:
202
+        thread = self.__database.select_one( Notebook, Notebook.sql_load_by_friendly_id( thread_id ) )
203
+      except:
204
+        raise cherrypy.NotFound
205
+      if not thread:
206
+        raise cherrypy.NotFound
207
+
208
+      thread_id = thread.object_id
209
+
190 210
     result = self.__users.current( user_id )
191 211
     result.update( self.__notebooks.old_notes( thread_id, start, count, user_id ) )
192 212
 
@@ -196,8 +216,6 @@ class Forum( object ):
196 216
 
197 217
     return result
198 218
 
199
-  default.exposed = True
200
-
201 219
   @expose()
202 220
   @end_transaction
203 221
   @grab_user_id

+ 11
- 0
controller/Validate.py View File

@@ -1,4 +1,5 @@
1 1
 import cherrypy
2
+import re
2 3
 from cgi import escape
3 4
 from Html_cleaner import Html_cleaner
4 5
 
@@ -167,6 +168,16 @@ class Valid_int( object ):
167 168
     return value
168 169
 
169 170
 
171
+class Valid_friendly_id( object ):
172
+  FRIENDLY_ID_PATTERN = re.compile( "^[a-zA-Z0-9\-]+$" )
173
+
174
+  def __call__( self, value ):
175
+    if self.FRIENDLY_ID_PATTERN.search( value ):
176
+      return value
177
+
178
+    raise ValueError()
179
+
180
+
170 181
 def validate( **expected ):
171 182
   """
172 183
   validate() can be used to require that the arguments of the decorated method successfully pass

+ 1
- 2
controller/test/Test_forums.py View File

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

+ 0
- 27
controller/test/Test_root.py View File

@@ -367,33 +367,6 @@ class Test_root( Test_controller ):
367 367
     assert result.get( "redirect" )
368 368
     assert result.get( "redirect" ).startswith( "https://" )
369 369
 
370
-  def test_blog( self ):
371
-    result = self.http_get(
372
-      "/blog",
373
-    )
374
-
375
-    assert result
376
-    assert u"error" not in result
377
-    assert result[ u"notebook" ].object_id == self.blog_notebook.object_id
378
-
379
-  def test_blog_with_note_id( self ):
380
-    result = self.http_get(
381
-      "/blog?note_id=%s" % self.blog_note.object_id,
382
-    )
383
-
384
-    assert result
385
-    assert u"error" not in result
386
-    assert result[ u"notebook" ].object_id == self.blog_notebook.object_id
387
-
388
-  def test_blog_rss( self ):
389
-    result = self.http_get(
390
-      "/blog?rss",
391
-    )
392
-
393
-    assert result
394
-    assert u"error" not in result
395
-    assert result[ u"notebook" ].object_id == self.blog_notebook.object_id
396
-
397 370
   def test_guide( self ):
398 371
     result = self.http_get(
399 372
       "/guide",

+ 12
- 0
model/Notebook.py View File

@@ -126,6 +126,10 @@ class Notebook( Persistent ):
126 126
   def sql_update( self ):
127 127
     return self.sql_create()
128 128
 
129
+  @staticmethod
130
+  def sql_load_by_friendly_id( friendly_id ):
131
+    return "select * from notebook_current where friendly_id( name ) = %s;" % quote( friendly_id )
132
+
129 133
   def sql_load_notes( self, start = 0, count = None ):
130 134
     """
131 135
     Return a SQL string to load a list of all the notes within this notebook.
@@ -340,6 +344,7 @@ class Notebook( Persistent ):
340 344
 
341 345
     d.update( dict(
342 346
       name = self.__name,
347
+      friendly_id = self.friendly_id,
343 348
       trash_id = self.__trash_id,
344 349
       read_write = self.__read_write,
345 350
       owner = self.__owner,
@@ -355,6 +360,12 @@ class Notebook( Persistent ):
355 360
     self.__name = name
356 361
     self.update_revision()
357 362
 
363
+  FRIENDLY_ID_STRIP_PATTERN = re.compile( "[^a-zA-Z0-9\-]+" )
364
+
365
+  def __friendly_id( self ):
366
+    friendly_id = self.WHITESPACE_PATTERN.sub( u"-", self.__name.lower() )
367
+    return self.FRIENDLY_ID_STRIP_PATTERN.sub( u"", friendly_id )
368
+
358 369
   def __set_read_write( self, read_write ):
359 370
     # The read_write member isn't actually saved to the database, so setting it doesn't need to
360 371
     # call update_revision().
@@ -390,6 +401,7 @@ class Notebook( Persistent ):
390 401
     self.__tags = tags
391 402
 
392 403
   name = property( lambda self: self.__name, __set_name )
404
+  friendly_id = property( __friendly_id )
393 405
   trash_id = property( lambda self: self.__trash_id )
394 406
   read_write = property( lambda self: self.__read_write, __set_read_write )
395 407
   owner = property( lambda self: self.__owner, __set_owner )

+ 4
- 0
model/delta/1.5.7.sql View File

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

+ 1
- 0
model/drop.sql View File

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

+ 6
- 0
model/schema.sql View File

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

+ 5
- 0
model/test/Test_notebook.py View File

@@ -173,6 +173,10 @@ class Test_notebook( object ):
173 173
     assert self.notebook.name == new_name
174 174
     assert self.notebook.revision > previous_revision
175 175
 
176
+  def test_friendly_id( self ):
177
+    self.notebook.name = u"This is Bob's  notebook!"
178
+    assert self.notebook.friendly_id == u"this-is-bobs-notebook"
179
+
176 180
   def test_set_read_write( self ):
177 181
     original_revision = self.notebook.revision
178 182
     self.notebook.read_write = Notebook.READ_WRITE_FOR_OWN_NOTES
@@ -233,6 +237,7 @@ class Test_notebook( object ):
233 237
     d = self.notebook.to_dict()
234 238
 
235 239
     assert d.get( "name" ) == self.name
240
+    assert d.get( "friendly_id" ) == u"my-notebook"
236 241
     assert d.get( "trash_id" ) == self.trash.object_id
237 242
     assert d.get( "read_write" ) == self.read_write
238 243
     assert d.get( "deleted" ) == self.notebook.deleted

+ 1
- 1
view/Forum_page.py View File

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

+ 4
- 1
view/Main_page.py View File

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

Loading…
Cancel
Save