Commits

Adam Knight  committed 6f8442a Merge

testing is now mainline

  • Participants
  • Parent commits db12f36, b936f8a

Comments (0)

Files changed (20)

 	'django.middleware.gzip.GZipMiddleware',
 	'django.middleware.http.ConditionalGetMiddleware',
 	'sitepoet.middleware.RedirectionMiddleware',
+	'sitepoet.middleware.PageMiddleware',
 	'django.middleware.common.CommonMiddleware',
 	'django.contrib.sessions.middleware.SessionMiddleware',
 	'django.contrib.auth.middleware.AuthenticationMiddleware',
 
 SP_SITE_NAME = "SitePoet Site"
 SP_PAGE_SIZE = 10
+SP_URL_TEMPLATE = "$section$slug"
 
 try:
 	from settings_local import *

File sitepoet/admin.py

-from models import *
+from django import forms
 from django.contrib import admin
+from django.utils.translation import ugettext_lazy as _
+
+from sitepoet.models import *
+
+## ACTIONS ##
+
+def action_mark_draft(model_admin, request, query_set):
+	query_set.update(status=ENTRY_STATUS_DRAFT)
+action_mark_draft.short_description = "Change status of selected items to Draft"
+
+def action_mark_published(model_admin, request, query_set):
+	query_set.update(status=ENTRY_STATUS_PUBLISHED)
+action_mark_published.short_description = "Change status of selected items to Published"
+
 
 ### PAGES ###
 
+class PageForm(forms.ModelForm):
+	url = forms.RegexField(label=_("URL"), max_length=255, regex=r'^[-\w/]+$',
+		help_text	  = _("Example: '/about/contact/'. Make sure to have leading"
+					  " and trailing slashes."),
+		error_message = _("This value must contain only letters, numbers,"
+						  " underscores, dashes or slashes."))
+	class Meta:
+		model = Page
+
+
 class PageAdmin(admin.ModelAdmin):
-	list_display = ('title', 'absolute_url', 'visibility', 'status', 'date_published', 'date_modified')
-	fieldsets = [
-		('Page',				{'fields': ['title', 'path']}),
-		('Availability',		{'fields': ['date_published', 'status', 'sites']}),
-		('Content',				{'fields': ['content', 'content_format']}),
-		]
+	form		   = PageForm
+	list_display   = ('title', 'absolute_url', 'visibility', 'status', 'date_created', 'date_modified', 'date_published', 'date_hidden')
+	list_editable  = ('status',)
+	list_filter	   = ('date_published', 'date_modified', 'status')
+	date_hierarchy = 'date_published'
+	search_fields  = ('title', 'url')
+	actions		   = [action_mark_draft, action_mark_published]
+	fieldsets	   = [
+		('',				{'fields': ['title', 'url', 'sites']}),
+		(_('Content'),		{'fields': ['content_format', 'content', 'template_name']}),
+		(_('Availability'), {'fields': ['status', 'date_published', 'date_hidden'], 'classes':('collapse',)}),
+	]
 	
 	def absolute_url(self, obj):
 		return obj.get_absolute_url()
 	
 	def visibility(self, obj):
 		f = Page.published.filter(pk=obj.id)
-		return ("Not Visible", "Visible")[(obj in f)]
+		return (_("Not Visible"), _("Visible"))[(obj in f)]
 
 admin.site.register(Page, PageAdmin)
 
-### ARTICLES ###
-
-# class ArticleRevisionInlineAdmin(admin.StackedInline):
-# 	model = ArticleRevision
-# 	max_num = 1
+### STORIES ###
 
-class ArticleAdmin(admin.ModelAdmin):
-	list_display = ('title', 'tags', 'status', 'allow_comments', 'date_published', 'date_modified')
-	# inlines = [ArticleRevisionInlineAdmin]
+class StoryAdmin(admin.ModelAdmin):
+	list_display = ('title', 'tags', 'status', 'allow_comments', 'date_published', 'date_hidden', 'date_modified')
+	list_editable = ('status',)
+	list_filter = ('date_published', 'date_hidden', 'date_modified', 'status')
+	date_hierarchy = 'date_published'
+	search_fields = ('title', '^user__username', 'slug')
 	prepopulated_fields = {"slug": ("title",)}
+	actions = [action_mark_draft, action_mark_published]
 	fieldsets = [
-		('Article',				{'fields': ['user', 'title', 'slug', 'tags']}),
-		('Availability',		{'fields': ['date_published', 'status', 'allow_comments', 'sites']}),
-		('Content',				{'fields': ['teaser_format', 'teaser', 'content_format', 'content']}),
+		('',				{'fields': ['user', 'title', 'slug', 'tags', 'section']}),
+		('Content',			{'fields': ['teaser_format', 'teaser', 'content_format', 'content']}),
+		('Availability',	{'fields': ['status', 'date_published', 'date_hidden', 'allow_comments', 'sites'], 'classes':('collapse',)}),
 		]
 
-admin.site.register(Article, ArticleAdmin)
-# admin.site.register(ArticleRevision)
-admin.site.register(ContentHistory)
+admin.site.register(Story, StoryAdmin)
+
+class ContentHistoryAdmin(admin.ModelAdmin):
+	list_display = ('title', 'content_type', 'object_id', 'field_name', 'date_created')
+	list_filter = ('field_name', 'date_created')
+	date_hierarchy = 'date_created'
+	search_fields = ('field_name', 'content')
+	
+	def title(self, obj):
+		return obj.__unicode__()
+	title.short_description = "Item Name"
+	
+admin.site.register(ContentHistory, ContentHistoryAdmin)
 
-### NEWS ITEMS ###
+### SECTIONS ###
 
-class NewsAdmin(admin.ModelAdmin):
-	list_display = ('title', 'status', 'date_published', 'date_modified')
-	fieldsets = [
-		('News',				{'fields': ['title', 'path']}),
-		('Availability',		{'fields': ['date_published', 'status', 'sites']}),
-		('Content',				{'fields': ['content', 'content_format']}),
-		]
-admin.site.register(News, NewsAdmin)
+class SectionAdmin(admin.ModelAdmin):
+	list_display = ("name", "slug")
+	prepopulated_fields = {"slug": ("name",)}
 
+admin.site.register(Section, SectionAdmin)
+	
 ### REDIRECTION ###
 
 class RedirectAdmin(admin.ModelAdmin):

File sitepoet/drupal_support/management/commands/convertdrupal.py

 import re
 from optparse import make_option
 
+from django.db.models import Q
 from django.contrib.auth.models import User
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.sites.models import Site
 from threadedcomments.models import ThreadedComment
 
 def vancode2int(vancode):
-	# print "--", vancode
 	if len(vancode):
 		result = int(vancode[1:], 36)
 	else:
 		result = None
-	# print "==",result
 	return result
 
 class Command(BaseCommand):
 	def handle(self, *app_labels, **options):
 		show_traceback = options.get('traceback', False)
 		verbose = int(options.get('verbosity', 0))
+		debug = (verbose == 2)
 		user = User.objects.get(pk=1)
 		
+		if verbose: print "Starting Drupal conversion."
+		if debug: print "* Debug logging enabled."
+		if debug: print "* Importing data with %s as the content owner." % user.username
+		
 		try:
 			# Get a list of nodes together and filter out blog nodes.
-			node_list = DrupalNode.objects.filter(type="blog")
-			if verbose: print "Found %d blog nodes." % len(node_list)
+			node_list = DrupalNode.objects.filter(type__in=("blog", "story", "page")).order_by("nid")
+			if verbose: print "Found %d nodes." % len(node_list)
+			
+			if len(node_list) == 0:
+				raise CommandError("No Drupal nodes found in the current database.")
+			
+			# Create some sections
+			(blog_section, c) = Section.objects.get_or_create(name="blog")
+			if c:
+				if debug: print "* Created blog section."
+				blog_section.save()
 			
-			if len(node_list):
-				print "Converting Drupal blog nodes ..."
-			else:
-				print "No Drupal blog nodes found."
+			(story_section, c) = Section.objects.get_or_create(name="story")
+			if c:
+				if debug: print "* Created story section."
+				story_section.save()
 			
 			for node in node_list:
+				# Our document object
+				obj = None
+				
 				# See if we've imported this node before
-				try:
-					article = Article.objects.get(pk=node.nid, title=node.title)
+				node_aliases = Redirect.objects.filter(original='/node/%d' % node.nid)
+				if node_aliases.count() > 0:
 					if verbose: print "Skipping node %d" % node.nid
 					continue
-				except Article.DoesNotExist:
-					if verbose: print "Processing node %d..." % node.nid
-					
-					# Create an Article for the node
-					article = Article(
-						user=user,
-						id=node.nid,
-						title=node.title,
-						date_created=datetime.datetime.fromtimestamp(node.created),
-						date_modified=datetime.datetime.fromtimestamp(node.changed),
-						date_published=datetime.datetime.fromtimestamp(node.created),
-						slug=slugify(node.title),
+				
+				# We haven't imported this.  Let's begin...
+				if verbose: print "Processing node (%d) %s" % (node.nid, node.title)
+				
+				if node.type == "blog" or node.type == "story":
+					if node.type == "blog":
+						if debug: print "Node is a blog."
+						section = blog_section
+					else:
+						if debug: print "Node is a story."
+						section = story_section
+					
+					# Create an Story for the node
+					obj = Story(
+						user = user,
+						title = node.title,
+						date_created = datetime.datetime.fromtimestamp(node.created),
+						date_modified = datetime.datetime.fromtimestamp(node.changed),
+						date_published = datetime.datetime.fromtimestamp(node.created),
+						slug = slugify(node.title),
+						allow_comments = node.comment,
+						status = node.status,
+						section = section,
 						)
 				
-				# Lookup any URL aliases for this node and create redirects
-				try:
-					aliases = DrupalUrlAlias.objects.filter(src='node/%d' % node.nid)
+					# Set the current content value
+					try:
+						contents = node.current_revision.get_parsed_contents()
 					
-					# First, the core "node/1" links should still work
-					r = Redirect(original="/node/%d" % node.nid, target_object=article)
-					r.save()
+						obj.teaser = contents['teaser']
+						obj.content = contents['body']
+					except DrupalNodeRevision.DoesNotExist, e:
+						if verbose: print "WARNING: No body content found for %s (%d)" % (obj.title, obj.id)
+				
+					# Save the story
+					obj.save()
 					
-					# Now we get any others that were made
-					for alias in aliases:
-						r = Redirect(original="/" + alias.dst, target_object=article)
-						r.save()
-						if verbose == 2: print " Created redirect from", alias.dst
-						
-				except DrupalUrlAlias.DoesNotExist, e:
-					alias = ''
+					# Add to the current site
+					obj.sites.add(Site.objects.get_current())
+					
+					# Import terms as tags
+					terms = node.current_revision.terms.all()
+					tags = []
+					for term in terms:
+						tags.append(term.name)
+					obj.tags = ','.join(tags) + ','
+					if debug: print "* Set tags to: %s" % obj.tags
+					
+					# Ensure the modification date is proper
+					obj.date_modified = datetime.datetime.fromtimestamp(node.changed)
+					obj.save()
+					
+				elif node.type == "page":
+					if debug: print "Node is a page."
+					
+					# Create an object for the node
+					obj = Page(
+						title = node.title,
+						date_created = datetime.datetime.fromtimestamp(node.created),
+						date_modified = datetime.datetime.fromtimestamp(node.changed),
+						date_published = datetime.datetime.fromtimestamp(node.created),
+						status = node.status,
+						)
+					
+					# Set the current content value
+					try:
+						obj.content = node.current_revision.body
+					except DrupalNodeRevision.DoesNotExist, e:
+						if verbose: print "WARNING: No body content found for %s (%d)" % (obj.title, obj.id)
+					
+					# Save
+					obj.save()
+					
+					# Add to the current site
+					obj.sites.add(Site.objects.get_current())
+					
+					# Find the most recent URL for this object and assign it
+					aliases = DrupalUrlAlias.objects.filter(src='node/%d' % node.nid).order_by('-pid')
+					if aliases.count():
+						if debug: print "* Page aliases:", aliases
+						obj.url = '/' + aliases[0].dst
+					
+					# Ensure the modification date is proper
+					obj.date_modified = datetime.datetime.fromtimestamp(node.changed)
+					obj.save()
 				
-				# Set the current content value
-				try:
-					# Drupal stores a teaser and a body, but the teaser is almost always a substr of the body.
-					# So, we need to see if that's the case.  If it is, then we break it up so we have a teaser and body.
-					# If it's not, then we store each separately.
-					# But first, we check for the easy case of "<!-- break -->"
-					
-					drupal_teaser = node.revisions.order_by('-timestamp')[0].teaser
-					drupal_body = node.revisions.order_by('-timestamp')[0].body
-					
-					break_re = re.compile('(?P<before>.*?)<!--\s*break\s*-->(?P<after>.*)', re.S|re.I)
-					results = break_re.search(drupal_body)
-					if results:
-						# Found a break marker, so we're golden.
-						article.teaser = results.group('before')
-						article.content = results.group('after')
-					else:
-						# Did not find a break marker.  So we do some string math.
-						# If the teaser is found in the body, it is deleted.  If it is not found,
-						# then it's left alone.  Should work...
-						clean_teaser = re.escape(drupal_teaser)
-						
-						article.teaser = drupal_teaser
-						article.content = re.sub(clean_teaser, '', drupal_body, 1)
-				except ObjectDoesNotExist, e:
-					print "WARNING: No body content found for %s (%d)" % (article.title, article.id)
+				else:
+					# Bail if we don't have something to work on
+					if verbose: print "Unsupported type:", node.type
+					continue
+				
+				# Lookup any URL aliases for this node and create redirects
+				aliases = DrupalUrlAlias.objects.filter(src='node/%d' % node.nid)
+				
+				# First, the core "node/1" links should still work
+				r = Redirect(original="/node/%d" % node.nid, target_object=obj)
+				r.save()
 				
-				# Save the article
-				article.save()
+				# Now we get any others that were made
+				for alias in aliases:
+					dst = "/" + alias.dst
+					if obj.__class__ == Page and dst == obj.url: continue
+					try:
+						r = Redirect(original=dst, target_object=obj)
+						r.save()
+						if debug: print " Created redirect from", dst
+					except Exception, e:
+						if verbose: print "* Failed to create redirect from", dst, "to", obj, ":", e
+						continue
 				
 				# Create a ContentHistory for each old body value
 				if node.revisions.count() > 1:
 					for revision in node.revisions.all()[1:]:
 						date = datetime.datetime.fromtimestamp(revision.timestamp)
-					
+						
+						contents = revision.get_parsed_contents()
+						
+						ch = ContentHistory(
+							date_created = date,
+							date_modified = date,
+							owner = obj,
+							field_name = "teaser",
+							content = contents['teaser'],
+						)
+						ch.save()
+						
 						ch = ContentHistory(
 							date_created = date,
 							date_modified = date,
-							owner = article,
+							owner = obj,
 							field_name = "content",
-							content = revision.body,
+							content = contents['body'],
 						)
 						ch.save()
-						if verbose == 2: print " Added revision %s" % (ch.date_created)
+						
+						if debug: print " Added revision %s" % (ch.date_created)
 				
 				# Bring over the comments
 				comments = self.get_comments_for_node(node)
-				if verbose == 2: print "  Comments:", comments.all()
+				if debug: print "  Comments:", comments.all()
 				for comment in comments:
-					if verbose == 2: print "  Importing comment: ", comment.thread, comment.subject
-
+					if debug: print "  Importing comment: ", comment.thread, comment.subject
+					
 					# To handle the nested comments:
 					# 	Break up the thread property: 01.01.01 -> [1,1,1]
 					# 	Pop off the last item, as that is the location of the current comment (which we cannot directly set)
 						# [0,0,5]
 						parents[0] = parents[0] - 1
 					
-					# print "** Parents:", parents, "(%s)" % comment.thread
+					if debug: print "** Parents:", parents, "(%s)" % comment.thread
 					
-					obj = None
+					c_obj = None
 					if len(parents):
-						ct = ContentType.objects.get_for_model(Article)
-						article_comments = ThreadedComment.objects.filter(content_type=ct, object_pk=str(article.id)).order_by('submit_date')
-						if verbose == 2:
-							print "Article comments (%d):" % article_comments.count(), article_comments
-							print "Parents (%d):" % len(parents), parents
+						ct = ContentType.objects.get_for_model(obj.__class__)
+						obj_comments = ThreadedComment.objects.filter(content_type=ct, object_pk=str(obj.id)).order_by('submit_date')
+						if debug:
+							print "* Object comments (%d):" % obj_comments.count(), obj_comments
+							print "* Parents (%d):" % len(parents), parents
 						try:
-							obj = article_comments[parents[0]]
+							c_obj = obj_comments[parents[0]]
 						except IndexError, e:
-							print "*** Parent of comment subtree not found.  This can happen if a thread has a deleted comment."
+							if verbose: print "*** Parent of comment subtree not found.  This can happen if a thread has a deleted comment."
 						parents = parents[1:]
-						while obj and len(parents):
-							if obj.children.count() > parents[0]:
-								obj = obj.children.all()[parents[0]]
+						while c_obj and len(parents):
+							if c_obj.children.count() > parents[0]:
+								c_obj = c_obj.children.all()[parents[0]]
 								parents = parents[1:]
 							else:
-								print "** Article has %d comments, but the parent should be item %d" % (obj.children.count(), parents[0])
+								if verbose: print "** Object has %d comments, but the parent should be item %d" % (c_obj.children.count(), parents[0])
 								break
-						# print "** Suspected parent:", obj.title
+						if debug: print "** Suspected parent:", c_obj.title
 					
+					# Actually create the comment now
 					c = ThreadedComment(
-						content_object = article,
+						content_object = obj,
 						title = unicode(comment.subject)[:200],
 						ip_address = unicode(comment.hostname)[:15],
 						user_name = unicode(comment.name)[:50],
 						submit_date = datetime.datetime.fromtimestamp(comment.timestamp),
 						comment = unicode(comment.comment),
 						site = Site.objects.get_current(),
+						is_public = (not comment.status),
 					)
 					c.save()
-					c.parent = obj
+					
+					# Now that the comment exists, we can set the parent.
+					c.parent = c_obj
 					c.save()
+					
+					if verbose: print " Imported comment (%d) %s" % (comment.cid, comment.subject)
 				
-				if verbose == 2: print " Finished article: %s (%s)" % (article.title, article.get_absolute_url())
-				
+				if debug: print " Finished node: %s (%s)" % (obj.title, obj.get_absolute_url())
+			
 		except Exception, e:
 			if show_traceback:
 				raise
 			raise CommandError("Error: %s" % e)
 		
-		print "Done."
+		if verbose: print "Done."
 	
 	def get_comments_for_node(self, node):
+		'''
+		This generally requires a "real" database like Postgresql or MySQL.  Sqlite does not support SUBSTRING.
+		But, that's okay.  Drupal required MySQL anyway, so do the conversion on a copy of the DB there, then
+		migrate to whatever else you want to use.
+		'''
 		from django.db import connection
 		cursor = connection.cursor()
 		

File sitepoet/drupal_support/models.py

-# Create your models here.
-# This is an auto-generated Django model module.
-# You'll have to do the following manually to clean this up:
-#	  * Rearrange models' order
-#	  * Make sure each model has one field with primary_key=True
-# Feel free to rename the models, but don't rename db_table values or field names.
-#
-# Also note: You'll have to insert the output of 'django-admin.py sqlcustom [appname]'
-# into your database.
-
+import re
 from django.db import models
 from django.contrib import admin
 
 	promote = models.IntegerField(default=0)
 	moderate = models.IntegerField(default=0)
 	sticky = models.IntegerField(default=0)
-	vid = models.IntegerField(unique=True, default=0)
+	current_revision = models.ForeignKey("DrupalNodeRevision", db_column="vid")
 	language = models.CharField(max_length=36, default="")
 	tnid = models.IntegerField(default=0)
 	translate = models.IntegerField(default=0)
+	
+	terms = models.ManyToManyField('DrupalTermData', through="DrupalTermNode", blank=True)
+
 	class Meta:
 		db_table = u'node'
 
 	timestamp = models.IntegerField(default=0)
 	format = models.IntegerField(default=0)
 	log = models.TextField(null=True, blank=True, default="")
+	
+	terms = models.ManyToManyField('DrupalTermData', through="DrupalTermNode", blank=True)
+	
 	class Meta:
 		db_table = u'node_revisions'
+	
+	def get_parsed_contents(self):
+		'''
+		Drupal stores a teaser and a body, but the teaser is almost always a substr of the body.
+		So, we need to see if that's the case.	If it is, then we break it up so we have a teaser and body.
+		If it's not, then we store each separately.
+		But first, we check for the easy case of "<!-- break -->"
+		'''
+		break_re = re.compile('(?P<before>.*?)<!--\s*break\s*-->(?P<after>.*)', re.S|re.I)
+		results = break_re.search(self.body)
+		
+		p_teaser = None
+		p_content = None
+		
+		if results:
+			# Found a break marker, so we're golden.
+			p_teaser = results.group('before')
+			p_content = results.group('after')
+		else:
+			# Did not find a break marker.	So we do some string math.
+			# If the teaser is found in the body, it is deleted.  If it is not found,
+			# then it's left alone.	 Should work...
+			p_teaser = self.teaser
+			p_content = re.sub(re.escape(self.teaser), '', self.body, 1)
+		
+		return {'teaser':p_teaser, 'body':p_content}
 
 class DrupalUrlAlias(models.Model):
 	pid = models.IntegerField(primary_key=True, default="")
-	src = models.CharField(max_length=384, default="")
-	dst = models.CharField(unique=True, max_length=384, default="")
+	src = models.CharField(max_length=255, default="")
+	dst = models.CharField(unique=True, max_length=255, default="")
 	language = models.CharField(max_length=36, default="")
 
 	def __unicode__(self):
 admin.site.register(DrupalNodeRevision)
 admin.site.register(DrupalUrlAlias)
 admin.site.register(DrupalUser)
+
+class DrupalTermData(models.Model):
+	'''The actual terms'''
+	tid = models.IntegerField(primary_key=True)
+	vid = models.IntegerField()
+	name = models.CharField(max_length=765)
+	description = models.TextField(blank=True)
+	weight = models.IntegerField()
+	class Meta:
+		db_table = u'term_data'
+	def __unicode__(self):
+		return self.name
+
+class DrupalTermHierarchy(models.Model):
+	'''Term parents/children'''
+	tid = models.ForeignKey(DrupalTermData, primary_key=True, related_name="tid_set", db_column="tid")
+	parent = models.ForeignKey(DrupalTermData, primary_key=True, db_column="parent")
+	class Meta:
+		db_table = u'term_hierarchy'
+
+class DrupalTermNode(models.Model):
+	'''Mapping of terms to nodes'''
+	nid = models.ForeignKey(DrupalNode, primary_key=True, db_column="nid")
+	tid = models.ForeignKey(DrupalTermData, primary_key=True, db_column="tid")
+	vid = models.ForeignKey(DrupalNodeRevision, db_column='vid')
+	class Meta:
+		db_table = u'term_node'
+
+admin.site.register(DrupalTermData)
+admin.site.register(DrupalTermHierarchy)
+admin.site.register(DrupalTermNode)
+
+# class DrupalTermRelation(models.Model):
+#	  tid1 = models.IntegerField(unique=True)
+#	  tid2 = models.IntegerField()
+#	  trid = models.IntegerField(primary_key=True)
+#	  class Meta:
+#		  db_table = u'term_relation'
+# 
+# class DrupalTermSynonym(models.Model):
+#	  tid = models.IntegerField()
+#	  name = models.CharField(max_length=765)
+#	  tsid = models.IntegerField(primary_key=True)
+#	  class Meta:
+#		  db_table = u'term_synonym'
+# 
+# class DrupalVocabulary(models.Model):
+#	  vid = models.IntegerField(primary_key=True)
+#	  name = models.CharField(max_length=765)
+#	  description = models.TextField(blank=True)
+#	  help = models.CharField(max_length=765)
+#	  relations = models.IntegerField()
+#	  hierarchy = models.IntegerField()
+#	  multiple = models.IntegerField()
+#	  required = models.IntegerField()
+#	  weight = models.IntegerField()
+#	  module = models.CharField(max_length=765)
+#	  tags = models.IntegerField()
+#	  class Meta:
+#		  db_table = u'vocabulary'

File sitepoet/managers.py

 from django.db import models
+from django.db.models import Q
+from django.contrib.sites.models import Site
 import datetime
 
 class PublishedManager (models.Manager):
 	def get_query_set(self):
-		return super(PublishedManager,self).get_query_set().filter(status = True, date_published__lte = datetime.datetime.now)
+		return super(PublishedManager,self).get_query_set().filter(
+			Q(date_published=None) | Q(date_published__lte=datetime.datetime.now),
+			Q(date_hidden=None) | Q(date_hidden__gt =datetime.datetime.now),
+			status = True,
+			sites__in=[Site.objects.get_current()]
+		)

File sitepoet/metaweblog.py

 
 import urlparse
 from django.contrib.auth.models import User
-from sitepoet.models import Article
+from sitepoet.models import Story
 from tagging.models import Tag
 from sitepoet.xmlrpc import public
 from django.conf import settings
 @public
 @authenticated()
 def metaWeblog_getPost(user, postid):
-    post = Article.objects.get(id=postid)
+    post = Story.objects.get(id=postid)
     return post_struct(post)
 
 @public
 @authenticated()
 def metaWeblog_getRecentPosts(user, blogid, num_posts):
-    posts = Article.objects.order_by('-pub_date')[:int(num_posts)]
+    posts = Story.objects.order_by('-pub_date')[:int(num_posts)]
     return [post_struct(post) for post in posts]
 
 @public
 def metaWeblog_newPost(user, blogid, struct, publish):
     body = struct['description']
     # todo - parse out technorati tags
-    post = Article(title = struct['title'],
+    post = Story(title = struct['title'],
                 body = body,
                 author = user,
                 create_date = struct['dateCreated'],
 @public
 @authenticated()
 def metaWeblog_editPost(user, postid, struct, publish):
-    post = Article.objects.get(id=postid)
+    post = Story.objects.get(id=postid)
     title = struct.get('title', None)
     if title is not None:
         post.title = title
 @public
 @authenticated(pos=2)
 def blogger_deletePost(user, appkey, postid, publish):
-    post = Article.objects.get(id=postid)
+    post = Story.objects.get(id=postid)
     post.delete()
     return True
 

File sitepoet/middleware/__init__.py

 from django.http import HttpResponsePermanentRedirect, HttpResponseRedirect, HttpResponseGone
-from sitepoet.models import Redirect
 from django.conf import settings
+from django.http import Http404
+
+from sitepoet.models import Redirect
+from sitepoet.views import view_page
+
 
 class RedirectionMiddleware(object):
 	def process_response(self, request, response):
 		# If the response found something, return it.
 		if response.status_code != 404:
 			return response
-		
 		try:
 			# Look for an exact redirect
 			path = request.path
 			paths = [path]
-			
 			# Try without the trailing slash
 			if settings.APPEND_SLASH:
 				paths.append(path[:path.rfind('/')] + path[path.rfind('/')+1:])
-			
 			r = Redirect.objects.get(site__id__exact=settings.SITE_ID, original__in=paths)
-			
 			# If there's a redirect, process it
 			destination = r.destination()
-			
+			# If we're about to tell someone to go where we already are, stop and think...
+			if destination == request.path_info:
+				return response
 			if destination == None:
 				# No destination means it's been removed, so make it a dead end.
 				# 410 Gone
 				else:
 					# 302 Found
 					return HttpResponseRedirect(destination)
-		
 		except Redirect.DoesNotExist, e:
 			# Otherwise, return the original response
 			return response
-		
 		except:
 			if settings.DEBUG:
 				raise
-		
 		# If we're here, we didn't catch something -- just pass along the 404.
 		return response
+
+
+class PageMiddleware(object):
+	def process_response(self, request, response):
+		# If the response found something, return it.
+		if response.status_code != 404:
+			return response
+		# Otherwise, see if we can get a response from the view
+		try:
+			return view_page(request, request.path_info)
+		# If the view couldn't find a page, then return the original response (don't pollute)
+		except Http404:
+			return response
+		# If anything else goes wrong here, just ignore it and return the original response (again, don't pollute)
+		except:
+			if settings.DEBUG:
+				raise
+			return response

File sitepoet/models.py

+#!/usr/bin/env python2.5
+
+### IMPORTS ###
+
 try:
 	import markdown
 except:
 	textile = None
 
 import datetime
+import string
 
 from django.core.urlresolvers import reverse
 from django.contrib.auth.models import User
 from django.contrib.sites.models import *
-from django.db import models
-from django.contrib.contenttypes.models import ContentType
+from django.conf import settings
 from django.contrib.contenttypes import generic
-from django.utils.safestring import mark_safe 
-from django.core.cache import cache
-
+from django.contrib.contenttypes.models import ContentType
+from django.db import models
+from django.utils.safestring import mark_safe
+import tagging
 from tagging.fields import TagField
 
-from managers import *
-from django.conf import settings
-import datetime
+from sitepoet.managers import *
+from sitepoet.templatetags.slugify import slugify
+
+
+### CONSTANTS ###
+
+COMMENTS_DISABLED = 0
+COMMENTS_CLOSED = 1
+COMMENTS_ENABLED =2
+
+COMMENT_STATES = (
+	(COMMENTS_DISABLED, 'Disabled'),
+	(COMMENTS_CLOSED, 'Closed'),
+	(COMMENTS_ENABLED, 'Enabled'),
+)
+
 
+ENTRY_STATUS_DRAFT = 0
+ENTRY_STATUS_PUBLISHED = 1
+
+ENTRY_STATUSES = (
+	(ENTRY_STATUS_DRAFT, "Draft"),
+	(ENTRY_STATUS_PUBLISHED, "Published"),
+)
+
+
+CONTENT_FORMAT_NONE = 0
+CONTENT_FORMAT_TEXTILE = 1
+CONTENT_FORMAT_MARKDOWN = 2
+
+CONTENT_FORMATTERS = (
+	(CONTENT_FORMAT_NONE, "None"),
+	(CONTENT_FORMAT_TEXTILE, "Textile"),
+	(CONTENT_FORMAT_MARKDOWN, "Markdown"),
+)
+
+
+### GLOBAL FUNCTIONS ###
 
 def format_text(format, text):
 	output = text
 	return mark_safe(output)
 
 
+### SPObject ###
+
 class SPObject (models.Model):
 	date_created = models.DateTimeField("Date Created", editable=False)
 	date_modified = models.DateTimeField("Date Modified", editable=False)
 		except KeyError:
 			return True
 	
-	def save(self, force_insert=False, force_update=False):
+	def save(self, *args, **kwargs):
 		if not self.date_created:
 			self.date_created = datetime.datetime.now()
 		
 		''' If the modification date was either not set or not manually changed since we were made, update it. '''
 		if not self.date_modified or not self.is_dirty_field('date_modified'):
 			self.date_modified = datetime.datetime.now()
-		
-		super(SPObject, self).save(force_insert=force_insert, force_update=force_update)
+				
+		super(SPObject, self).save(*args, **kwargs)
 		
 		self._original_state = self._as_dict()
-		
-		# Reset the cache for this object
-		url = self.get_absolute_url()
-		if url:
-			cache.delete(url)
-	
-	def get_absolute_url(self):
-		return None
-
-COMMENTS_DISABLED = 0
-COMMENTS_CLOSED = 1
-COMMENTS_ENABLED =2
 
-COMMENT_STATES = (
-	(COMMENTS_DISABLED, 'Disabled'),
-	(COMMENTS_CLOSED, 'Closed'),
-	(COMMENTS_ENABLED, 'Enabled'),
-)
 
+### Model Objects ###
 
-ENTRY_STATUS_DRAFT = 0
-ENTRY_STATUS_PUBLISHED = 1
-
-ENTRY_STATUSES = (
-	(ENTRY_STATUS_DRAFT, "Draft"),
-	(ENTRY_STATUS_PUBLISHED, "Published"),
-)
-
-
-CONTENT_FORMAT_NONE = 0
-CONTENT_FORMAT_TEXTILE = 1
-CONTENT_FORMAT_MARKDOWN = 2
-
-CONTENT_FORMATTERS = (
-	(CONTENT_FORMAT_NONE, "None"),
-	(CONTENT_FORMAT_TEXTILE, "Textile"),
-	(CONTENT_FORMAT_MARKDOWN, "Markdown"),
-)
-
-
-class Document(SPObject):
+class Resource(SPObject):
 	# Properties
-	sites = models.ManyToManyField(Site, default=[settings.SITE_ID], help_text='The site(s) this item is accessible at.')
-	title = models.CharField("Title", max_length=255)
-	path = models.CharField("URL Path", max_length=255, null=True, blank=True)
+	sites = models.ManyToManyField(Site, default=[settings.SITE_ID], null=False, blank=False, help_text='The site(s) this item is accessible at.')
+	title = models.CharField("Title", max_length=255, null=False, blank=False)
+	
+	teaser = models.TextField(null=True, blank=True)
+	teaser_format = models.PositiveIntegerField("Teaser Format", choices=CONTENT_FORMATTERS, default=CONTENT_FORMAT_TEXTILE, help_text="The formatter the teaser should be run through when rendering.")
 	content = models.TextField("Content", null=True, blank=True, help_text='The content to display on the page.')
 	content_format = models.PositiveIntegerField("Content Format", choices=CONTENT_FORMATTERS, default=CONTENT_FORMAT_TEXTILE, help_text="The formatter the content should be run through when rendering.")
 	
 	# Publishing status
-	date_published = models.DateTimeField("Date Published", null=True, blank=True, default=datetime.datetime.now, help_text="Item will become visible after this date.  Future posting is supported.")
 	status = models.IntegerField("Status", choices=ENTRY_STATUSES, default=ENTRY_STATUS_PUBLISHED, help_text="Only published items will be visible on the site.")
-	
+	date_published = models.DateTimeField("Date Published", null=True, blank=True, default=datetime.datetime.now, help_text="Item will become visible after this date.  Future posting is supported.")
+	date_hidden = models.DateTimeField("Date Hidden", null=True, blank=True, help_text="Item will be hidden past this date.  No value indicates a perpetual item (most common).")
+		
 	# Object managers
 	objects = models.Manager()
 	published = PublishedManager()
 	
+	class Meta:
+		abstract = True
+	
+	def __init__(self, *args, **kwargs):
+		super(Resource, self).__init__(*args, **kwargs)
+		if self.id != None:
+			self._original_url = self.get_absolute_url()
+		else:
+			self._original_url = ""
+	
 	def __unicode__(self):
 		return self.title
-		
+	
+	@property
+	def formatted_teaser(self):
+		return format_text(self.teaser_format, self.teaser)
+	
 	@property
 	def formatted_content(self):
 		return format_text(self.content_format, self.content)
 	
-	def save(self, force_insert=False, force_update=False):
-		update_redirect = (self.path and len(self.path) and self.is_dirty_field("path"))
+	def save(self, *args, **kwargs):
+		update_teaser_history = (self.teaser and self.is_dirty_field("teaser"))
 		update_content_history = (self.content and self.is_dirty_field("content"))
 		
-		super(Document, self).save(force_insert=force_insert, force_update=force_update)
-
-		# Create a redirect with the current URL if the path has changed.
-		# We use the current URL since we cannot do this with the old one since it's changed. As long as
-		# we keep creating these with the current URL, we should be covered every time the property is changed.
-		if update_redirect:
-			try:
-				r = Redirect.objects.get(original=self.get_absolute_url())
-			except Redirect.DoesNotExist:
-				r = Redirect(original=self.get_absolute_url(), target_object=self, permanent=True)
-				r.save()
-			
+		super(Resource, self).save(*args, **kwargs)
+		
+		'''
+		Create a redirect with the current URL if the path has changed.
+		We use the current URL so that if this object is ever deleted or hidden, we can display a 410/Gone message.
+		Since we create these from the start of life for the object, all previous URLs will be recorded EXCEPT
+		for the case when two objects will have shared the same URL at some point, at which case the original
+		object wins (in the automated fashion; admins can always update the Redirect object manually).
+		'''
+		if self._original_url != self.get_absolute_url():
+			(r, c) = Redirect.objects.get_or_create(
+				original = self.get_absolute_url(),
+				defaults = {
+					'target_object': self,
+					'permanent': True
+				}
+			)
+			if c: r.save()
+			self._original_url = self.get_absolute_url()
+		
+		if update_teaser_history:
+			ch = ContentHistory(owner=self, field_name="teaser", content=self.teaser)
+			ch.save()
+		
 		if update_content_history:
 			ch = ContentHistory(owner=self, field_name="content", content=self.content)
 			ch.save()
-	
-	class Meta:
-		abstract = True
-		get_latest_by = 'date_published'
-		ordering = ['-date_published','-date_modified']
 
 
-class ContentHistory(SPObject):
-	content_type = models.ForeignKey(ContentType, null=True, blank=True)
-	object_id = models.PositiveIntegerField(null=True, blank=True)
-	owner = generic.GenericForeignKey()
-	field_name = models.CharField(max_length=255)
-	content = models.TextField("Content", null=True, blank=True)
-	
-	def __unicode__(self):
-		return "%s: %s" % (self.owner, self.field_name)
+class Section(SPObject):
+	# TODO: Create admin class that auto-gens the slug
+	name = models.CharField("Section Name", max_length=255, null=False, blank=False)
+	slug = models.SlugField("Section Slug", max_length=255, null=False, blank=False)
+	# page = models.ForeignKey(Page, null=True, blank=True, help_text="A page to display instead of a section index.")
 	
 	class Meta:
-		verbose_name = "Content History"
-		verbose_name_plural = "Content Histories"
+		verbose_name = "section"
+		verbose_name_plural = "sections"
+	
+	def __unicode__(self):
+		return self.name
 	
-	@classmethod
-	def history(owner, field):
-		#FIXME: owner won't work here, need to search by ID and PK
-		return ContentHistory.objects.filter(owner=owner, field_name=field).order_by('-date_created')
-
-
-class Page(Document):
 	@models.permalink
 	def get_absolute_url(self):
-		return ("page_detail", [self.path])
+		return ('section-archive',(), {'section':self.name})
+
+
+class Page(Resource):
+	url = models.CharField("URL", max_length=255, db_index=True, help_text="The URL for this page. Ensure it begins and ends with a slash.")
+	template_name = models.CharField("Template", max_length=255, blank=True, help_text="If specified, this template will be used instead of 'page.html'")
+
+	class Meta:
+		ordering = ('url',)
+		verbose_name = "page"
+		verbose_name_plural = "pages"
+		get_latest_by = 'date_published'
+		ordering = ['-date_published','-date_modified']
 	
+	def __unicode__(self):
+		return "%s; %s" % (self.url, self.title)
 
-class News(Document):
-	@models.permalink
 	def get_absolute_url(self):
-		return ("news_detail", (), {
-			'year':self.date_published.year,
-			'month':"%02d"%self.date_published.month,
-			'day':"%02d"%self.date_published.day,
-			'slug':self.slug})
+		return self.url
 
-	class Meta:
-		verbose_name = "News item"
-		verbose_name_plural = "News items"
 
-class Article(Document):
-	# Properties
-	user = models.ForeignKey(User)
-	slug = models.SlugField("URL Title", max_length=255, unique_for_date="date_published")
+class Story(Resource):
+	slug = models.SlugField("URL Slug", max_length=255, null=False, blank=False)
+	user = models.ForeignKey(User, null=False, blank=False)
 	tags = TagField()
+	section = models.ForeignKey(Section, null=True, blank=True, help_text="The section of the site this story will reside in.")
+	show_metadata = models.BooleanField("Show Metadata", default=True, help_text="Toggles the display of author and date information.")
 	allow_comments = models.IntegerField("Comments", choices=COMMENT_STATES, default=COMMENTS_ENABLED, help_text="If comments are disabled then existing comments will be hidden.  Choose 'Closed' to display existing comments and prevent new comments from being added.")
-	teaser = models.TextField(null=True, blank=True)
-	teaser_format = models.PositiveIntegerField("Teaser Format", choices=CONTENT_FORMATTERS, default=CONTENT_FORMAT_TEXTILE, help_text="The formatter the teaser should be run through when rendering.")
 	
-	@property
-	def formatted_teaser(self):
-		return format_text(self.teaser_format, self.teaser)
+	class Meta:
+		verbose_name = "story"
+		verbose_name_plural = "stories"
+		get_latest_by = 'date_published'
+		ordering = ['-date_published','-date_modified']
 	
-	def get_absolute_url(self):
-		url = None
-		# try:
-		url = reverse("article_detail", kwargs = {
-				'year':self.date_published.year,
-				'month':"%02d"%self.date_published.month,
-				'day':"%02d"%self.date_published.day,
-				'slug':self.slug})
-		# except:
-		# 	pass
-		return url
+	def show_comments(self):
+		return self.allow_comments != COMMENTS_DISABLED
 	
-	def save(self, force_insert=False, force_update=False):
-		update_redirect = (self.date_published and self.slug and self.is_dirty_field("date_published") and self.is_dirty_field("slug"))
-		update_teaser_history = (self.teaser and self.is_dirty_field("teaser"))
-		
-		super(Article, self).save(force_insert=force_insert, force_update=force_update)
-		
-		if update_redirect:
-			try:
-				r = Redirect.objects.get(original=self.get_absolute_url())
-			except Redirect.DoesNotExist:
-				r = Redirect(original=self.get_absolute_url(), target_object=self, permanent=True)
-				r.save()
+	def allow_commenting(self):
+		return self.allow_comments == COMMENTS_ENABLED
+	
+	def get_absolute_url(self):
+		if self.section:
+			section = self.section.name
+		else:
+			section = None
 		
-		if update_teaser_history:
-			ch = ContentHistory(owner=self, field_name="teaser", content=self.teaser)
-			ch.save()
-
+		kwargs = {
+			'section': section,
+			'year':    self.date_published.year,
+			'month':   "%02d"%self.date_published.month,
+			'day':     "%02d"%self.date_published.day,
+			'slug':    self.slug,
+		}
+		return reverse('story-detail', kwargs = kwargs)
 
 ### REDIRECTION ###
 
 	
 	permanent = models.BooleanField(default=True, help_text='Is this redirect permanent (code 301)?')
 	
+	class Meta:
+		verbose_name = "redirect"
+		verbose_name_plural = "redirects"
+		ordering = ['-date_created']
+	
 	def __unicode__(self):
 		return "%s to (%s, %s)" % (self.original, self.current, self.target_object)
 	
 			except Exception, e:
 				pass
 		return destination
+
+
+### HISTORY ###
+
+class ContentHistory(SPObject):
+	content_type = models.ForeignKey(ContentType, null=True, blank=True)
+	object_id = models.PositiveIntegerField(null=True, blank=True)
+	owner = generic.GenericForeignKey()
+	field_name = models.CharField(max_length=255)
+	content = models.TextField("Content", null=True, blank=True)
 	
 	class Meta:
-		ordering = ['-date_created']
+		verbose_name = "content history"
+		verbose_name_plural = "content histories"
+		ordering = ['-date_created', 'content_type', 'object_id']
+	
+	def __unicode__(self):
+		return "%s: %s" % (self.owner, self.field_name)
+	
+	@classmethod
+	def history(owner, field):
+		#FIXME: owner won't work here, need to search by ID and PK
+		return ContentHistory.objects.filter(owner=owner, field_name=field).order_by('-date_created')
+

File sitepoet/templates/article.html

-{% load markup %}
-<div class="article full">
-    <h2>
-        <a href="{{article.get_absolute_url}}">{{article.title}}</a>
-    </h2>
-    <p class="byline">
-        By <span class="author">{{article.user.username}}</span> on <span class="date">{{article.date_published|date}}</span>
-    </p>
-    <p class="teaser">
-        {{article.formatted_teaser}}
-    </p>
-{% if not options.teasers %}
-    <p class="body">
-        {{article.formatted_content}}
-    </p>
-{% endif %}
-</div>

File sitepoet/templates/article_archive.html

-{% extends "base.html" %}
-{% load pager %}
-{% block content %}
-<div class="article-list">
-{% for article in page.object_list %}
-{% include "article.html" %}
-{% endfor %}
-</div>
-{% if page.has_other_pages %}
-<ul class="pager">
-{% pager 5 %}
-</ul>
-{% endif %}
-{% endblock %}

File sitepoet/templates/article_comment.html

-<div class="comment">
-	<h3>{{comment.user_name}}</h3>
-	<p>{{comment.comment}}</p>
-</div>

File sitepoet/templates/article_detail.html

-{% extends "base.html" %}
-{% block content %}
-{% load threadedcomments_tags %}
-{% include "article.html" %}
-
-<div id="comments">
-{% get_comment_list for article as comment_list %}
-{% for comment in comment_list|annotate_tree %}
-{% include "comment.html" %}
-{% endfor %}
-</div>
-
-<div id="newcomment">
-{% render_comment_form for article %}
-</div>
-
-{% endblock %}

File sitepoet/templates/comment.html

 <div class="comment" style="margin-left: {{comment.depth}}pc;">
+	<h3>{{comment.user_name}}</h3>
 	{% if comment.title %}<p class="title" /> {{comment.title}}{% endif %}
 	<p class="content" /> {{comment.comment}}
 	<ul class="links">

File sitepoet/templates/page.html

 {% block content %}
 <div class="page">
     <h2>
-        <a href="{{page.get_absolute_url}}">{{page.title}}</a>
+        <a href="{{object.get_absolute_url}}">{{object.title}}</a>
     </h2>
     <p class="body">
-        {{page.formatted_content}}
+        {{object.formatted_content}}
     </p>
 </div>
 {% endblock %}

File sitepoet/templates/story.html

+{% load markup %}
+<div class="story {% if options.teasers %}teaser{% else %}full{% endif %}">
+    <h2>
+        <a href="{{object.get_absolute_url}}">{{object.title}}</a>
+    </h2>
+    <p class="byline">
+        By <span class="author">{{object.user.username}}</span> on <span class="date">{{object.date_published|date}}</span>
+    </p>
+    <p class="teaser">
+        {{object.formatted_teaser}}
+    </p>
+{% if not options.teasers %}
+    <p class="body">
+        {{object.formatted_content}}
+    </p>
+{% endif %}
+</div>

File sitepoet/templates/story_archive.html

+{% extends "base.html" %}
+{% load pager %}
+{% block content %}
+<div class="story-list">
+{% for object in list.object_list %}
+{% include "story.html" %}
+{% endfor %}
+</div>
+{% if page.has_other_pages %}
+<ul class="pager">
+{% pager 5 %}
+</ul>
+{% endif %}
+{% endblock %}

File sitepoet/templates/story_detail.html

+{% extends "base.html" %}
+{% block content %}
+{% load threadedcomments_tags %}
+{% include "story.html" %}
+
+{% if object.show_comments %}
+<div id="comments">
+{% get_comment_list for object as comment_list %}
+{% for comment in comment_list|annotate_tree %}
+{% include "comment.html" %}
+{% endfor %}
+</div>
+{% endif %}
+
+{% if object.allow_commenting %}
+<div id="newcomment">
+{% render_comment_form for object %}
+</div>
+{% endif %}
+
+{% endblock %}

File sitepoet/templatetags/slugify.py

 def slugify(string):
 	string = re.sub('\s+', '-', string)
 	string = re.sub('[^\w.-]', '', string)
-	return string.strip('_.- ').lower()
+	string = string.strip('_.- ').lower()
+	return string or "no-title"

File sitepoet/urls.py

 from django.conf.urls.defaults import *
-from models import Article
+from models import Story
 from django.conf import settings
 
 urlpatterns = patterns('sitepoet.views',
     (r'^comments/', include('django.contrib.comments.urls')),
 	
 	# Articles
-	url(r'^(?P<year>\d+)/(?P<month>\d+)/(?P<day>\d+)/(?P<slug>.+)/$',	'article_detail',	name="article_detail"),
-	url(r'^(?P<year>\d+)/(?P<month>\d+)/(?P<day>\d+)/$',				'article_archive',	name="article_archive_day"),
-	url(r'^(?P<year>\d+)/(?P<month>\d+)/$',								'article_archive',	name="article_archive_month"),
-	url(r'^(?P<year>\d+)/$',											'article_archive',	name="article_archive_year"),
-	url(r'^$',															'article_archive',	kwargs={"teasers": True},	name="article_archive"),
-	
-	# News
-	url(r'^news/(?P<year>\d+)/(?P<month>\d+)/(?P<day>\d+)/(?P<slug>.+)$',	'news_detail',	name="news_detail"),
-	url(r'^news/(?P<year>\d+)/(?P<month>\d+)/(?P<day>\d+)/$',				'news_archive',	name="news_archive_day"),
-	url(r'^news/(?P<year>\d+)/(?P<month>\d+)/$',							'news_archive',	name="news_archive_month"),
-	url(r'^news/(?P<year>\d+)/$',											'news_archive',	name="news_archive_year"),
-	url(r'^news/$',															'news_archive',	kwargs={"teasers": True},	name="news_archive"),
-	
-	# Pages
-	url(r'^(?P<path>.*)$',												'page_detail',		name="page_detail"),
+	url(
+		regex  = r'^(?:(?P<section>[^/]+)/)?(?P<year>\d{4})/(?P<month>\d{1,2})/(?P<day>\d{1,2})/(?P<slug>.+)/$',
+		view   = 'story_detail',
+		name   = "story-detail"
+	),
+	url(
+		regex  = r'^(?:(?P<section>[^/]+)/)?(?:(?P<year>\d{4})/)?(?:(?P<month>\d{1,2})/)?(?:(?P<day>\d{1,2})/)?$',
+		view   = 'story_archive',
+		kwargs = { "teasers": True },
+		name   = "story-archive"
+	),
+)
+
+urlpatterns += patterns('sitepoet.views',
+	(r'^(?P<url>.*)$', 'view_page'),
 )
 
-urlpatterns += patterns('',
-	# MetaWeblog access point
-	url(r'^xmlrpc/', 'sitepoet.xmlrpc.view', kwargs={'module':'sitepoet.metaweblog'}),
-)
+# urlpatterns += patterns('',
+# 	# MetaWeblog access point
+# 	url(r'^xmlrpc/', 'sitepoet.xmlrpc.view', kwargs={'module':'sitepoet.metaweblog'}),
+# )

File sitepoet/views.py

-from models import *
 from django.core.paginator import Paginator, InvalidPage, EmptyPage
 from django.core.xheaders import populate_xheaders
-from django.http import HttpResponse, Http404
+from django.http import HttpResponse, HttpResponseRedirect, Http404
 from django.template import loader, Context, RequestContext
-from django.shortcuts import render_to_response
+from django.shortcuts import render_to_response, get_object_or_404
 from django.conf import settings
 from django.contrib.sites.models import *
 
+from sitepoet.models import *
 
-### Articles ###
 
-def article_detail(request, year=None, month=None, day=None, slug=None):
-	# Fetch an article
+### Stories ###
+
+def story_detail(request, section=None, year=None, month=None, day=None, slug=None):
+	# Fetch a story
 	try:
-		article = Article.published.get(
+		story = Story.published.get(
+			section__name = section,
 			date_published__year = int(year),
 			date_published__month = int(month),
 			date_published__day = int(day),
 			slug = slug
 			)
-	except Article.DoesNotExist, e:
+	except Story.DoesNotExist, e:
 		# No result?  404.
 		raise Http404
 	
 	# Prepare the template data
 	context = {
-		"title":article.title,
-		"article": article
+		"title":  story.title,
+		"object": story
 	}
 	
-	return render_to_response("article_detail.html", context, context_instance=RequestContext(request))
+	response = render_to_response("story_detail.html", context, context_instance=RequestContext(request))
+	populate_xheaders(request, response, Story, story.id)
+	return response
+
 
-def article_archive(request, year=None, month=None, day=None, teasers=False):
-	# Fetch a list of articles
-	articles = Article.published.all()
+def story_archive(request, section=None, year=None, month=None, day=None, teasers=False):
+	# If there's a section, try and load it then filter the stories by it
+	if section:
+		obj = get_object_or_404(Section, name=section)
+		stories = Story.published.filter(section=obj)
+	else:
+		stories = Story.published.all()
+	
+	# Fetch a list of stories
 	if year:
-		articles = articles.filter(date_published__year=int(year))
+		stories = stories.filter(date_published__year=int(year))
 		if month:
-			articles = articles.filter(date_published__month=int(month))
+			stories = stories.filter(date_published__month=int(month))
 			if day:
-				articles = articles.filter(date_published__day=int(day))
+				stories = stories.filter(date_published__day=int(day))
 	
 	# Paginate the results
 	page = int(request.GET.get('page', 1))		# Requested page number
-	paginator = Paginator(articles.select_related(depth=2), settings.SP_PAGE_SIZE)
+	paginator = Paginator(stories.select_related(depth=2), settings.SP_PAGE_SIZE)
 	
 	try:
-		articles_page = paginator.page(page)
+		stories_page = paginator.page(page)
 	except (EmptyPage, InvalidPage):
-		articles_page = paginator.page(paginator.num_pages)
+		stories_page = paginator.page(paginator.num_pages)
 	
 	# Prepare the context
 	context = {
 		"options": {
 			"teasers": teasers
 		},
-		"page": articles_page
+		"list": stories_page
 	}
 	
 	if year and month and day:
 	elif year:
 		context['title'] = "%04d" % int(year)
 	
-	return render_to_response("article_archive.html", context, context_instance=RequestContext(request))
+	templates = []
+	if section:
+		templates.append("section/" + section + "/story_archive.html")
+	templates.append("story_archive.html")
+	
+	return render_to_response(templates, context, context_instance=RequestContext(request))
 
 
-### News items ###
+def view_page(request, url):
+	# URLs begin with a slash, but a bad admin or urls.py can remove it.
+	if not url.startswith('/'):
+		url = "/" + url
 
-def news_detail(request, year=None, month=None, day=None, slug=None):
-	# Fetch a news item
 	try:
-		post = News.published.get(
-			date_published__year = int(year),
-			date_published__month = int(month),
-			date_published__day = int(day),
-			slug = slug
-			)
-	except News.DoesNotExist, e:
-		# No result?  404.
+		page = Page.published.get(url__exact=url, sites__id__exact=settings.SITE_ID)
+	
+	except Page.DoesNotExist, e:
+		# If there's no trailing slash, and we're using it, then try that URL next.
+		if not url.endswith('/') and settings.APPEND_SLASH:
+			return HttpResponseRedirect("%s/" % request.path_info)
+		
+		# Otherwise, give up.
 		raise Http404
 	
-	# Prepare the template data
-	context = {
-		"title":post.title,
-		"post": post
-	}
-
-	return render_to_response(("news_detail.html", "article_detail.html"), context, context_instance=RequestContext(request))
+	except Page.MultipleObjectsReturned, e:
+		raise Http404 #What else to do?  You broke it, man.  One URL = One Object.
 
-def news_archive(request, year=None, month=None, day=None, teasers=True):
-	# Fetch a list of posts
-	posts = News.published.all()
-	if year:
-		posts = posts.filter(date_published__year=int(year))
-		if month:
-			posts = posts.filter(date_published__month=int(month))
-			if day:
-				posts = posts.filter(date_published__day=int(day))
-	
-	# Paginate the results
-	page = int(request.GET.get('page', 1))		# Requested page number
-	paginator = Paginator(posts.select_related(depth=2), settings.SP_PAGE_SIZE)
-	
-	try:
-		posts_page = paginator.page(page)
-	except (EmptyPage, InvalidPage):
-		posts_page = paginator.page(paginator.num_pages)
-	
 	# Prepare the context
 	context = {
-		"options": {
-			"teasers": teasers
-		},
-		"page": posts_page
+		"title":  page.title,
+		"object": page,
+		"flatpage": page, #Let people use the same template to a degree
 	}
-	
-	if year and month and day:
-		context['title'] = "%04d-%02d-%02d" % (int(year), int(month), int(day))
-	elif year and month:
-		context['title'] = "%04d-%02d" % (int(year), int(month))
-	elif year:
-		context['title'] = "%04d" % int(year)
-	
-	return render_to_response(("news_archive.html", "article_archive.html"), context, context_instance=RequestContext(request))
-
 
-## Pages ###
+	# Create the template list
+	if page.template_name:
+		templates = [page.template_name, "page.html"]
+	else:
+		templates = ["page.html"]
 
-def page_detail(request, path):
-	paths = [path]
-	
-	if not path.startswith('/'):
-		paths.append("/" + path)
-	
-	# Look for the exact path
-	try:
-		page = Page.published.get(path__in=paths, sites__id__exact=settings.SITE_ID)
-	except Page.DoesNotExist:
-		if settings.APPEND_SLASH:
-			for p in paths:
-				try:
-					page = Page.published.get(path=p+"/", sites__id__exact=settings.SITE_ID)
-					return HttpResponseRedirect(p)
-				except Page.DoesNotExist:
-					continue
-		raise Http404
-	
-	# Prepare the context
-	context = {
-		"page": page
-	}
-	
-	# Serve page.
-	response = render_to_response("page.html", context, context_instance=RequestContext(request))
+	response = render_to_response(templates, context, context_instance=RequestContext(request))
 	populate_xheaders(request, response, Page, page.id)
 	return response