Commits

Adam Knight committed 024fdf9

Just the app now.

Comments (0)

Files changed (73)

PLAN.textile

-h1. SitePoet
-
-SitePoet is the realization of something I started with PHP a long time ago.  I've always wanted a site I'd written but the mechanics of the database and so on really bog me down and made the several attempts fail.  So, this time, I'm going to start out simple and then build slowly, starting with something that can take a Drupal import and then adding features to that later on.
-
-h1. Plan
-
-Since other CMSes tend to feel restrictive after a while, I plan on using Django and public classes and add-ons to create a CMS for my own use.  Since I can specify the function of the site myself, I expect the following:
-
-*CMS*
-
-* Dated posts (blog-ish or news-ish)
-* Static pages (/about or products)
-* Re-usable blocks, each with a target zone for the template (left sidebar, footer, etc.)
-* Versioned posts.  The mechanics of this could get interesting, but when a post is changed, the old copy should be around for the sake of archiving.  It should be available to the template as well, in case I choose to publish a version history.
-* Private posts.  The system should use Django's groups to allow a post to be read only by a specific group of logged-in users.
-* Multiple copyrights.  There should be License objects that hold either a logo or a text blurb along with a title for various methods of license (public domain, copyright, CC, etc.) and entries should have a menu to change it.
-* Image attachments via ImageField
-* General file attachments
-* Nested comments.  I love this feature in Drupal and plan to keep using it.  Hopefully I can make it more visible.
-* Tagging.  Moving away from categories, the site should allow for tags and include some navigation facility for them as well.
-* Spam protection.  Using either Akismet or Mollom, the site should check for and filter spammy content.  This may mean the introduction of a moderation queue.
-* Email subscriptions to entries.  Readers should be able to add themselves to a list per-entry to be notified when comments are made on the entry.  They must also have a way of removing themselves from this list.
-* Subscriptions to new content.  The site must have RSS and/or Atom feeds as well.  These should be: site, site comments, per-page comments, and per-tag.
-* Statistics.  The site should keep statistics internally for every page hit.  The information logged should be optimized so that there is minimal database locking.  This should happen in middleware and be outside of any transaction middleware that would block a result.  It should log at the end of the request so the result code can be included and should include the page processing time.
-
-*URLs*
-
-* URL management should be central.
-* When an object is saved, it should save a redirect with its current URL and its content type.
-* If the URL ever changes, that redirect will start to get hit and serve as a safety net for the object.  It will always redirect to the object's current URL as returned by get_absolute_url.
-
-
-h1. ToDo Ideas
-
-* Remove the archive browser from being rooted at /; add another urls.py entry for it so it can be repurposed.
-* Add apps for codepoetry and macgeekery, such as products (manages software releases, including appcasts), projects (connects to bitbucket, maybe?), questions (asked and answered, with email notifications), and user profiles (AdSense IDs, among other things).
-* Allow Drupal import to import a specific taxonomy as the section list instead of content types.
-* Allow SITE_ID and other variables to be set from a SitePoetSite object, or something like that.  That way one set of FCGI instances can serve all hosted sites instead of needing one class per site so that the settings file is done right.
-* Add ability to set the default state of comments for sites that do not want to use it. Don't bother loading comment-related features (such as template tags) in such a case.
+h1. SitePoet
+
+SitePoet is the realization of something I started with PHP a long time ago.  I've always wanted a site I'd written but the mechanics of the database and so on really bog me down and made the several attempts fail.  So, this time, I'm going to start out simple and then build slowly, starting with something that can take a Drupal import and then adding features to that later on.
+
+h1. Plan
+
+Since other CMSes tend to feel restrictive after a while, I plan on using Django and public classes and add-ons to create a CMS for my own use.  Since I can specify the function of the site myself, I expect the following:
+
+*CMS*
+
+* Dated posts (blog-ish or news-ish)
+* Static pages (/about or products)
+* Re-usable blocks, each with a target zone for the template (left sidebar, footer, etc.)
+* Versioned posts.  The mechanics of this could get interesting, but when a post is changed, the old copy should be around for the sake of archiving.  It should be available to the template as well, in case I choose to publish a version history.
+* Private posts.  The system should use Django's groups to allow a post to be read only by a specific group of logged-in users.
+* Multiple copyrights.  There should be License objects that hold either a logo or a text blurb along with a title for various methods of license (public domain, copyright, CC, etc.) and entries should have a menu to change it.
+* Image attachments via ImageField
+* General file attachments
+* Nested comments.  I love this feature in Drupal and plan to keep using it.  Hopefully I can make it more visible.
+* Tagging.  Moving away from categories, the site should allow for tags and include some navigation facility for them as well.
+* Spam protection.  Using either Akismet or Mollom, the site should check for and filter spammy content.  This may mean the introduction of a moderation queue.
+* Email subscriptions to entries.  Readers should be able to add themselves to a list per-entry to be notified when comments are made on the entry.  They must also have a way of removing themselves from this list.
+* Subscriptions to new content.  The site must have RSS and/or Atom feeds as well.  These should be: site, site comments, per-page comments, and per-tag.
+* Statistics.  The site should keep statistics internally for every page hit.  The information logged should be optimized so that there is minimal database locking.  This should happen in middleware and be outside of any transaction middleware that would block a result.  It should log at the end of the request so the result code can be included and should include the page processing time.
+
+*URLs*
+
+* URL management should be central.
+* When an object is saved, it should save a redirect with its current URL and its content type.
+* If the URL ever changes, that redirect will start to get hit and serve as a safety net for the object.  It will always redirect to the object's current URL as returned by get_absolute_url.
+
+
+h1. ToDo Ideas
+
+* Remove the archive browser from being rooted at /; add another urls.py entry for it so it can be repurposed.
+* Add apps for codepoetry and macgeekery, such as products (manages software releases, including appcasts), projects (connects to bitbucket, maybe?), questions (asked and answered, with email notifications), and user profiles (AdSense IDs, among other things).
+* Allow Drupal import to import a specific taxonomy as the section list instead of content types.
+* Allow SITE_ID and other variables to be set from a SitePoetSite object, or something like that.  That way one set of FCGI instances can serve all hosted sites instead of needing one class per site so that the settings file is done right.
+* Add ability to set the default state of comments for sites that do not want to use it. Don't bother loading comment-related features (such as template tags) in such a case.
Empty file added.
+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"
+
+def action_open_comments(model_admin, request, query_set):
+	query_set.update(allow_comments=COMMENTS_ENABLED)
+action_open_comments.short_description = "Allow comments on the selected items"
+
+def action_close_comments(model_admin, request, query_set):
+	query_set.update(allow_comments=COMMENTS_CLOSED)
+action_close_comments.short_description = "Close comments on the selected items"
+
+def action_disable_comments(model_admin, request, query_set):
+	query_set.update(allow_comments=COMMENTS_DISABLED)
+action_disable_comments.short_description = "Disable and hide comments on the selected items"
+
+### 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):
+	form		   = PageForm
+	list_display   = ('title', 'absolute_url', 'visible', '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()
+
+admin.site.register(Page, PageAdmin)
+
+### STORIES ###
+
+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, action_open_comments, action_close_comments, action_disable_comments]
+	fieldsets = [
+		('',				{'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(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)
+
+### SECTIONS ###
+
+class SectionAdmin(admin.ModelAdmin):
+	list_display = ("name", "slug")
+	prepopulated_fields = {"slug": ("name",)}
+
+admin.site.register(Section, SectionAdmin)
+	
+### REDIRECTION ###
+
+class RedirectAdmin(admin.ModelAdmin):
+	list_display = ("original", "current", "content_type", "object_id", "permanent", "date_modified", "date_created")
+	fieldsets = (
+		("Source", {"fields": ["site", "original"]}),
+		("Destination", {"fields": ["current", "content_type", "object_id"]}),
+		("Options", {"fields": ["permanent"]})
+	)
+admin.site.register(Redirect, RedirectAdmin)

context_preprocessors/__init__.py

+from django.conf import settings
+from django.contrib.sites.models import Site
+
+def general(request):
+	return {
+		"site": Site.objects.get_current(),
+	}

drupal_support/__init__.py

Empty file added.

drupal_support/management/__init__.py

Empty file added.

drupal_support/management/commands/__init__.py

Empty file added.

drupal_support/management/commands/convertdrupal.py

+import datetime
+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 django.core.management.base import BaseCommand, CommandError
+
+from sitepoet.drupal_support.models import *
+from sitepoet.models import *
+from sitepoet.templatetags.slugify import slugify
+
+from threadedcomments.models import ThreadedComment
+
+def vancode2int(vancode):
+	if len(vancode):
+		result = int(vancode[1:], 36)
+	else:
+		result = None
+	return result
+
+class Command(BaseCommand):
+	# option_list = BaseCommand.option_list + (
+	# 	make_option('--format', default='json', dest='format', help='Specifies the output serialization format for fixtures.'),
+	# 	make_option('--indent', default=None, dest='indent', type='int', help='Specifies the indent level to use when pretty-printing output'),
+	# 	make_option('-e', '--exclude', dest='exclude',action='append', default=[], help='App to exclude (use multiple --exclude to exclude multiple apps).'),
+	# 	make_option('-v', '--verbose', dest='verbose', action="store", default="0", type="choice", choices=['0','1','2'], help='Verbose output'),
+	# )
+	help = 'Convert Drupal 6 tables to SP objects.	The tables must be in the same database as SP.'
+	# args = '[appname ...]'
+	
+	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__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()
+			
+			(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
+				node_aliases = Redirect.objects.filter(original='/node/%d' % node.nid)
+				if node_aliases.count() > 0:
+					if verbose: print "Skipping node %d" % node.nid
+					continue
+				
+				# 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,
+						)
+				
+					# Set the current content value
+					try:
+						contents = node.current_revision.get_parsed_contents()
+					
+						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()
+					
+					# 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()
+				
+				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()
+				
+				# 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 = obj,
+							field_name = "content",
+							content = contents['body'],
+						)
+						ch.save()
+						
+						if debug: print " Added revision %s" % (ch.date_created)
+				
+				# Bring over the comments
+				comments = self.get_comments_for_node(node)
+				if debug: print "  Comments:", comments.all()
+				for comment in comments:
+					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)
+					# 	For each remaining level, get the right relationship in a loop:
+					# 		Set an object to the first listed comment.
+					# 		Pop that value off the array.
+					# 		If there're more items left, set the loop object to that child of the current object.
+					# 		When we run out of list items, we found the parent.
+					
+					# 01.00.05.07/ to 01.00.05.07 to [01,00,05,07]
+					parents = comment.thread[:-1].split('.')
+					# [1,0,5,7]
+					parents = map(vancode2int, parents)
+					# [1,0,5]
+					parents = parents[:-1]
+					if len(parents):
+						# [0,0,5]
+						parents[0] = parents[0] - 1
+					
+					if debug: print "** Parents:", parents, "(%s)" % comment.thread
+					
+					c_obj = None
+					if len(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:
+							c_obj = obj_comments[parents[0]]
+						except IndexError, e:
+							if verbose: print "*** Parent of comment subtree not found.  This can happen if a thread has a deleted comment."
+						parents = parents[1:]
+						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:
+								if verbose: print "** Object has %d comments, but the parent should be item %d" % (c_obj.children.count(), parents[0])
+								break
+						if debug and c_obj: print "** Suspected parent:", c_obj.title
+					
+					# Actually create the comment now
+					c = ThreadedComment(
+						content_object = obj,
+						title = unicode(comment.subject)[:200],
+						ip_address = unicode(comment.hostname)[:15],
+						user_name = unicode(comment.name)[:50],
+						user_email = unicode(comment.mail)[:75],
+						user_url = unicode(comment.homepage)[:200],
+						submit_date = datetime.datetime.fromtimestamp(comment.timestamp),
+						comment = unicode(comment.comment),
+						site = Site.objects.get_current(),
+						is_public = (not comment.status),
+					)
+					c.save()
+					
+					# 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 debug: print " Finished node: %s (%s)" % (obj.title, obj.get_absolute_url())
+			
+		except Exception, e:
+			if show_traceback:
+				raise
+			raise CommandError("Error: %s" % e)
+		
+		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()
+		
+		cursor.execute("SELECT cid FROM comments as c WHERE nid = %s ORDER BY SUBSTRING(c.thread, 1, (LENGTH(c.thread) - 1))", [node.nid])
+		rows = cursor.fetchall()
+		ids = [c[0] for c in rows]
+		comments = DrupalComment.objects.filter(pk__in=ids)
+		
+		return comments

drupal_support/models.py

+import re
+from django.db import models
+from django.contrib import admin
+
+class DrupalComment(models.Model):
+	cid = models.IntegerField(primary_key=True)
+	pid = models.IntegerField(null=True, blank=True, default=0)
+	nid = models.ForeignKey("DrupalNode", db_column="nid")
+	uid = models.ForeignKey("DrupalUser", db_column="uid")
+	subject = models.CharField(null=True, blank=True, max_length=192)
+	comment = models.TextField(null=True, blank=True, default="")
+	hostname = models.CharField(null=True, blank=True, max_length=384)
+	timestamp = models.IntegerField(null=True, blank=True, default=0)
+	status = models.IntegerField(null=True, blank=True, default=0)
+	format = models.IntegerField(null=True, blank=True, default=0)
+	thread = models.CharField(null=True, blank=True, max_length=765)
+	name = models.CharField(null=True, blank=True, max_length=180)
+	mail = models.CharField(null=True, blank=True, max_length=192)
+	homepage = models.CharField(null=True, blank=True, max_length=765)
+	class Meta:
+		db_table = u'comments'
+
+class DrupalNode(models.Model):
+	nid = models.IntegerField(primary_key=True)
+	type = models.CharField(max_length=96, default="")
+	title = models.CharField(max_length=765, default="")
+	uid = models.ForeignKey("DrupalUser", db_column="uid")
+	status = models.IntegerField(default=0)
+	created = models.IntegerField(default=0)
+	changed = models.IntegerField(default=0)
+	comment = models.IntegerField(default=0)
+	promote = models.IntegerField(default=0)
+	moderate = models.IntegerField(default=0)
+	sticky = models.IntegerField(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'
+
+class DrupalNodeRevision(models.Model):
+	nid = models.ForeignKey("DrupalNode", db_column="nid", related_name="revisions")
+	vid = models.IntegerField(primary_key=True, default=0)
+	uid = models.ForeignKey("DrupalUser", db_column="uid")
+	title = models.CharField(max_length=765, default="")
+	body = models.TextField(default="")
+	teaser = models.TextField(default="")
+	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=255, default="")
+	dst = models.CharField(unique=True, max_length=255, default="")
+	language = models.CharField(max_length=36, default="")
+
+	def __unicode__(self):
+		return "%s to %s" % (self.src, self.dst)
+
+	class Meta:
+		db_table = u'url_alias'
+
+class DrupalUser(models.Model):
+	uid = models.IntegerField(primary_key=True)
+	name = models.CharField(null=True, unique=True, max_length=180, default="")
+	pass_field = models.CharField(null=True, max_length=96, db_column='pass', default="") # Field renamed because it was a Python reserved word. Field name made lowercase.
+	mail = models.CharField(null=True, max_length=192, blank=True, default="")
+	mode = models.IntegerField(null=True, default=0)
+	sort = models.IntegerField(null=True, blank=True, default=0)
+	threshold = models.IntegerField(null=True, blank=True, default=0)
+	theme = models.CharField(null=True, max_length=765, default="")
+	signature = models.CharField(null=True, max_length=765, default="")
+	created = models.IntegerField(null=True, default=0)
+	access = models.IntegerField(null=True, default=0)
+	status = models.IntegerField(null=True, default=0)
+	timezone = models.CharField(null=True, max_length=24, blank=True, default="")
+	language = models.CharField(null=True, max_length=36, default="")
+	picture = models.CharField(null=True, max_length=765, default="")
+	init = models.CharField(null=True, max_length=192, blank=True, default="")
+	data = models.TextField(null=True, blank=True, default="")
+	login = models.IntegerField(null=True, default=0)
+	class Meta:
+		db_table = u'users'
+
+admin.site.register(DrupalComment)
+admin.site.register(DrupalNode)
+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'

management/__init__.py

Empty file added.

management/commands/__init__.py

Empty file added.
+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(
+			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()]
+		)
+# http://www.allyourpixel.com/post/metaweblog-38-django/
+
+import urlparse
+from django.contrib.auth.models import User
+from sitepoet.models import *
+from tagging.models import Tag
+from tagging.utils import parse_tag_input
+from sitepoet.xmlrpc import public
+from django.conf import settings
+from django.core.urlresolvers import reverse
+import xmlrpclib # import DateTime
+
+def authenticated(pos=1):
+	"""
+	A decorator for functions that require authentication.
+	Assumes that the username & password are the second & third parameters.
+	Doesn't perform real authorization (yet), it just checks that the
+	user is_superuser.
+	"""
+	
+	def _decorate(func):
+		def _wrapper(*args, **kwargs):
+			username = args[pos+0]
+			password = args[pos+1]
+			args = args[0:pos]+args[pos+2:]
+			try:
+				user = User.objects.get(username__exact=username)
+			except User.DoesNotExist:
+				raise ValueError("Authentication Failure")
+			if not user.check_password(password):
+				raise ValueError("Authentication Failure")
+			if not user.is_superuser:
+				raise ValueError("Authorization Failure")
+			return func(user, *args, **kwargs)
+		
+		return _wrapper
+	return _decorate
+
+def full_url(url):
+	return urlparse.urljoin(settings.SITE_URL, url)
+
+# example... this is what wordpress returns:
+# {'permaLink': 'http://gabbas.wordpress.com/2006/05/09/hello-world/',
+#  'description': 'Welcome to <a href="http://wordpress.com/">Wordpress.com</a>. This is your first post. Edit or delete it and start blogging!',
+#  'title': 'Hello world!',
+#  'mt_excerpt': '',
+#  'userid': '217209',
+#  'dateCreated': <DateTime u'20060509T16:24:39' at 2c7580>,
+#  'link': 'http://gabbas.wordpress.com/2006/05/09/hello-world/',
+#  'mt_text_more': '',
+#  'mt_allow_comments': 1,
+#  'postid': '1',
+#  'categories': ['Uncategorized'],
+#  'mt_allow_pings': 1}
+
+def format_date(d):
+	if not d: return None
+	return xmlrpclib.DateTime(d.isoformat())
+
+def post_struct(post):
+	link = full_url(post.get_absolute_url())
+	struct = {
+		'postid': post.id,
+		'title': post.title,
+		'mt_basename': post.slug,
+		'link': link,
+		'permaLink': link,
+		'description': post.teaser,
+		'mt_tags': post.tags,
+		'userid': post.user.id,
+		# 'mt_excerpt': post.teaser,
+		'mt_text_more': post.content,
+		# 'mt_allow_comments': 1,
+		# 'mt_allow_pings': 1
+		'mt_convert_breaks': str(post.content_format), #Sadly, we have to use one format for both components.
+		}
+	if post.date_published:
+		struct['dateCreated'] = format_date(post.date_published)
+	
+	return struct
+
+def tag_struct(tag):
+	struct = {
+		'categoryId': tag.id,
+		'categoryName': tag.name,
+	}
+	return struct
+
+def _update_post(post, struct):
+	if struct.get('dateCreated', None):
+		post.date_published = struct['dateCreated']
+	if struct.has_key('title'):
+		post.title = struct['title']
+	if struct.has_key('description'):
+		post.teaser = struct['description']
+	if struct.has_key('mt_text_more'):
+		post.content = struct['mt_text_more']
+	if struct.has_key('mt_tags'):
+		post.tags = struct['mt_tags']
+	elif struct.has_key('mt_keywords'):
+		post.tags = struct('mt_keywords')
+	if struct.has_key('mt_basename'):
+		post.slug = struct['mt_basename']
+
+''' Blogger API V1 '''
+
+@public
+@authenticated(pos=2)
+def blogger_newPost(user, appkey, blogid, content, publish):
+	pass
+
+@public
+@authenticated(pos=2)
+def blogger_editPost(user, appkey, postid, content, publish):
+	pass
+
+@public
+@authenticated(pos=2)
+def blogger_deletePost(user, appkey, postid, publish):
+	post = Story.objects.get(id=postid)
+	post.delete()
+	return True
+
+@public
+@authenticated()
+def blogger_getUsersBlogs(user, appkey):
+	"""
+	an array of <struct>'s containing the ID (blogid), name
+	(blogName), and URL (url) of each blog.
+	"""
+	print "entered"
+	sections = Section.objects.all()
+	print "got sections"
+	result = [{
+			'blogid': section.slug,
+			'blogName': section.name,
+			'url': settings.SITE_URL + reverse('story-archive', kwargs={'section':section.slug})
+			} for section in sections]
+	print "results:", result
+	return result
+
+@public
+@authenticated()
+def blogger_getUserInfo(user, appkey):
+	return {
+		'nickname':user.username,
+		'userid':user.id,
+		'url':settings.SITE_URL,
+		'email':user.email,
+		'lastname':user.last_name,
+		'firstname':user.first_name,
+		}
+
+@public
+@authenticated()
+def metaWeblog_getCategories(user, blogid):
+	tags = Tag.objects.all()
+	result = [{'description':tag.name} for tag in tags]
+	return result
+
+@public
+@authenticated()
+def metaWeblog_getPost(user, postid):
+	post = Story.objects.get(id=postid)
+	return post_struct(post)
+
+@public
+@authenticated()
+def metaWeblog_getRecentPosts(user, blogid, num_posts):
+	posts = Story.objects.filter(section__name__exact=blogid).order_by('-date_published')[:int(num_posts)]
+	return [post_struct(post) for post in posts]
+
+@public
+@authenticated()
+def metaWeblog_newPost(user, blogid, struct, publish):
+	post = Story(status = publish)
+	post.user = user
+	_update_post(post, struct)
+	section = Section.objects.get(name__exact=blogid)
+	post.section = blogid
+	post.save()
+	return post.id
+
+@public
+@authenticated()
+def metaWeblog_editPost(user, postid, struct, publish):
+	post = Story.objects.get(id=postid)
+	post.status = publish
+	_update_post(post, struct)
+	post.save()
+	return True
+
+# http://qoli.de/blog/2007/nov/19/implementing-metaweblog-api/
+@public
+@authenticated()
+def metaWeblog_newMediaObject(user, blogid, struct):		
+	bits = b64decode(struct['bits'])
+	name = struct['name']
+	mime = struct['type']
+
+	attachment= Attachment(content= bits,
+				   filename= name,
+				   contenttype= mime)
+	attachment.save_content_file(name, bits)
+	attachment.save()
+
+	return {'url': attachment.get_content_url()}
+
+@public
+@authenticated()
+def mt_getPostCategories(user, postid):
+	post = Story.objects.get(id=int(postid))
+	if (post.tags):
+		tags = [tag_struct(t) for t in Tag.objects.filter(name__in=parse_tag_input(post.tags))]
+	else:
+		tags = []
+	return tags
+
+@public
+@authenticated()
+def mt_getCategoryList(user, blogid):
+	return [tag_struct(t) for t in Tag.objects.all()]
+
+@public
+def mt_supportedTextFilters():
+	return [{'label': f[1], 'key': str(f[0])} for f in CONTENT_FORMATTERS]

middleware/__init__.py

+from django.conf import settings
+from django.http import HttpResponsePermanentRedirect, HttpResponseRedirect, HttpResponseGone, Http404
+from django.template import Template, RequestContext, loader
+
+from sitepoet.models import Redirect, Page, Story
+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]
+			
+			# Also try without the trailing slash if someone tacked it on for us
+			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 the destination is not visible, let the 404 shine.
+			if hasattr(r.target_object, 'visible') and r.target_object.visible == False:
+				return response
+			
+			# 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.
+				context = RequestContext(request, {
+					'title':'410 Gone',
+					'content':'the resource you seek / has been taken by time / seek answers within'
+				})
+				template = loader.get_template("error.html")
+				return HttpResponseGone(template.render(context))
+			
+			if r.permanent == True:
+				# 301 Moved
+				return HttpResponsePermanentRedirect(destination)
+			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
+#!/usr/bin/env python2.5
+
+### IMPORTS ###
+
+try:
+	import markdown
+except:
+	markdown = None
+
+try:
+	import textile
+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.conf import settings
+from django.contrib.contenttypes import generic
+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 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
+	if format == CONTENT_FORMAT_TEXTILE:
+		if textile != None:
+			output = textile.textile(text)
+		else:
+			print "No Textile support found."
+	elif format == CONTENT_FORMAT_MARKDOWN:
+		if markdown != None:
+			output = markdown.markdown(text)
+		else:
+			print "No Markdown support found."
+	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)
+	
+	class Meta:
+		abstract = True
+	
+	def __init__(self, *args, **kwargs):
+		super(SPObject, self).__init__(*args, **kwargs)
+		self._original_state = self._as_dict()
+	
+	def _as_dict(self):
+		'''
+		Returns the values of non-relationship model objects in a dictionary.
+		'''
+		return dict([(f.name, getattr(self, f.name)) for f in self._meta.local_fields if not f.rel])
+	
+	def get_changed_fields(self):
+		'''
+		Returns a dictionary where the key is the field that changed and the value is the original value.
+		'''
+		new_state = self._as_dict()
+		return dict([(key, value) for key, value in self._original_state.iteritems() if value != new_state[key]])
+	
+	def is_dirty_field(self, field):
+		''' Returns True if a field has been changed since the object was last saved. '''
+		try:
+			return self.__dict__.get(field, None) != self._original_state[field]
+		except KeyError:
+			return True
+	
+	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(*args, **kwargs)
+		
+		self._original_state = self._as_dict()
+
+
+### Model Objects ###
+
+class Resource(SPObject):
+	# Properties
+	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
+	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
+	
+	@property
+	def visible(self):
+		return (self.status == ENTRY_STATUS_PUBLISHED and self.date_published < datetime.datetime.now())
+	
+	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, *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(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 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 = "section"
+		verbose_name_plural = "sections"
+	
+	def __unicode__(self):
+		return self.name
+	
+	@models.permalink
+	def get_absolute_url(self):
+		return ('section-archive',(), {'section':self.slug})
+
+
+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)
+
+	def get_absolute_url(self):
+		return self.url
+
+
+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.")
+	
+	class Meta:
+		verbose_name = "story"
+		verbose_name_plural = "stories"
+		get_latest_by = 'date_published'
+		ordering = ['-date_published','-date_modified']
+	
+	def show_comments(self):
+		return self.allow_comments != COMMENTS_DISABLED
+	
+	def allow_commenting(self):
+		return self.allow_comments == COMMENTS_ENABLED
+	
+	def get_absolute_url(self):
+		if self.section:
+			section = self.section.slug
+		else:
+			section = None
+		
+		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 ###
+
+class Redirect(SPObject):
+	site = models.ForeignKey(Site, default=settings.SITE_ID, help_text='The site this redirect is applicable to.')
+	original = models.CharField("Original path", max_length=255, unique=True, db_index=True,
+		help_text='The original path for this resource.  This must be an absolute path starting from the root of the site.')
+	current = models.CharField("Current path", max_length=255, null=True, blank=True,
+		help_text='The current path to this resource, if it is not an object.')
+	
+	content_type = models.ForeignKey(ContentType, null=True, blank=True, help_text='If this redirect is to an object, select an object type.')
+	object_id = models.PositiveIntegerField("Object ID", null=True, blank=True, help_text='The ID of the object this redirect will point to.')
+	target_object = generic.GenericForeignKey()
+	
+	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)
+	
+	def destination(self):
+		destination = None
+		if self.current and len(self.current):
+			destination = self.current
+		elif self.target_object != None:
+			try:
+				destination = self.target_object.get_absolute_url()
+			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:
+		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')
+

sp/__init__.py

Empty file removed.

sp/lifestream/PLAN.textile

-*Lifestream*
-
-* The site should allow mirroring of my social sites in a Friendfeed-like way.  This could be by using Friendfeed or by using syncr or something like it to pull information off of sites and into the DB.  This should work with:
-** Reddit
-** Digg
-** delicious
-** magnolia
-** last.fm
-** hulu
-** twitter
-** facebook?
-** Flickr
-* The database of information should not expire and should have friendly URLs when possible that include the service, the date, and some information about the item.  This is an archive.
-* Initial import should go back as far as the APIs allow.
-* An RSS/Atom feed should be available.
-* There should be a way of quickly getting all items for a particular day as well for the blog module to make a daily post out of it (though it would not be a real post, it would be generated every time).

sp/lifestream/__init__.py

Empty file removed.

sp/lifestream/models.py

-from django.db import models
-
-# Create your models here.

sp/lifestream/urls.py

Empty file removed.

sp/lifestream/views.py

-# Create your views here.

sp/manage.py

-#!/usr/bin/env python
-from django.core.management import execute_manager
-try:
-    import settings # Assumed to be in the same directory.
-except ImportError:
-    import sys
-    sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__)
-    sys.exit(1)
-
-if __name__ == "__main__":
-    execute_manager(settings)

sp/settings.py

-import os, sys, django
-
-# Get some handy paths to make absolute paths relative to the project
-DJANGO_ROOT = os.path.dirname(os.path.realpath(django.__file__))
-SITE_ROOT = os.path.dirname(os.path.realpath(__file__))
-
-# Everything is relative to the install by default
-os.chdir(SITE_ROOT)
-
-###
-
-# Django settings for sitepoet project.
-
-DEBUG = True
-TEMPLATE_DEBUG = DEBUG
-
-ADMINS = (
-	# ('Your Name', 'your_email@domain.com'),
-)
-
-MANAGERS = ADMINS
-
-DATABASE_ENGINE = 'sqlite3'			# 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
-DATABASE_NAME = 'sitepoet.sqlite'	# Or path to database file if using sqlite3.
-DATABASE_USER = ''					# Not used with sqlite3.
-DATABASE_PASSWORD = ''				# Not used with sqlite3.
-DATABASE_HOST = ''					# Set to empty string for localhost. Not used with sqlite3.
-DATABASE_PORT = ''					# Set to empty string for default. Not used with sqlite3.
-
-# Local time zone for this installation. Choices can be found here:
-# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
-# although not all choices may be available on all operating systems.
-# If running in a Windows environment this must be set to the same as your
-# system time zone.
-TIME_ZONE = 'America/Chicago'
-
-# Language code for this installation. All choices can be found here:
-# http://www.i18nguy.com/unicode/language-identifiers.html
-LANGUAGE_CODE = 'en-us'
-
-SITE_ID = 1
-
-# If you set this to False, Django will make some optimizations so as not
-# to load the internationalization machinery.
-USE_I18N = False
-
-# Absolute path to the directory that holds media.
-# Example: "/home/media/media.lawrence.com/"
-MEDIA_ROOT = ''
-
-# URL that handles the media served from MEDIA_ROOT. Make sure to use a
-# trailing slash if there is a path component (optional in other cases).
-# Examples: "http://media.lawrence.com", "http://example.com/media/"
-MEDIA_URL = ''
-
-# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
-# trailing slash.
-# Examples: "http://foo.com/media/", "/media/".
-ADMIN_MEDIA_PREFIX = '/media/'
-
-# Make this unique, and don't share it with anybody.
-## Define this in settings_local!
-# SECRET_KEY = ''
-
-# List of callables that know how to import templates from various sources.
-TEMPLATE_LOADERS = (
-	'django.template.loaders.filesystem.load_template_source',
-	'django.template.loaders.app_directories.load_template_source',
-)
-
-MIDDLEWARE_CLASSES = (
-	'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',
-)
-
-ROOT_URLCONF = 'urls'
-
-TEMPLATE_DIRS = (
-	# Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
-	# Always use forward slashes, even on Windows.
-	# Don't forget to use absolute paths, not relative paths.
-	"templates",
-)
-
-INSTALLED_APPS = (
-	'django.contrib.auth',
-	'django.contrib.contenttypes',
-	'django.contrib.sessions',
-	'django.contrib.sites',
-	'django.contrib.admin',
-	'django.contrib.admindocs',
-	'django.contrib.markup',
-	'django.contrib.humanize',
-	'django.contrib.comments',
-	'sitepoet',
-#	'sitepoet.drupal_support',
-	'tagging',
-	'threadedcomments',
-)
-
-TEMPLATE_CONTEXT_PROCESSORS = (
-    "django.core.context_processors.auth",
-    "django.core.context_processors.debug",
-    "django.core.context_processors.i18n",
-    "django.core.context_processors.media",
-    "django.core.context_processors.request",
-    "sitepoet.context_preprocessors.general",
-)
-
-COMMENTS_APP = 'threadedcomments'
-
-# Common Middleware
-
-APPEND_SLASH = True
-PREPEND_WWW = False
-USE_ETAGS = True
-
-# Some defaults
-
-SP_SITE_NAME = "SitePoet Site"
-SP_PAGE_SIZE = 10
-SP_URL_TEMPLATE = "$section$slug"
-
-try:
-	from settings_local import *
-except Exception, e:
-	print "No local settings found; using defaults."

sp/sitepoet/__init__.py

Empty file removed.

sp/sitepoet/admin.py

-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"
-
-def action_open_comments(model_admin, request, query_set):
-	query_set.update(allow_comments=COMMENTS_ENABLED)
-action_open_comments.short_description = "Allow comments on the selected items"
-
-def action_close_comments(model_admin, request, query_set):
-	query_set.update(allow_comments=COMMENTS_CLOSED)
-action_close_comments.short_description = "Close comments on the selected items"
-
-def action_disable_comments(model_admin, request, query_set):
-	query_set.update(allow_comments=COMMENTS_DISABLED)
-action_disable_comments.short_description = "Disable and hide comments on the selected items"
-
-### 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):
-	form		   = PageForm
-	list_display   = ('title', 'absolute_url', 'visible', '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()
-
-admin.site.register(Page, PageAdmin)
-
-### STORIES ###
-
-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, action_open_comments, action_close_comments, action_disable_comments]
-	fieldsets = [
-		('',				{'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(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)
-
-### SECTIONS ###
-
-class SectionAdmin(admin.ModelAdmin):
-	list_display = ("name", "slug")
-	prepopulated_fields = {"slug": ("name",)}
-
-admin.site.register(Section, SectionAdmin)
-	
-### REDIRECTION ###
-
-class RedirectAdmin(admin.ModelAdmin):
-	list_display = ("original", "current", "content_type", "object_id", "permanent", "date_modified", "date_created")
-	fieldsets = (
-		("Source", {"fields": ["site", "original"]}),
-		("Destination", {"fields": ["current", "content_type", "object_id"]}),
-		("Options", {"fields": ["permanent"]})
-	)
-admin.site.register(Redirect, RedirectAdmin)

sp/sitepoet/context_preprocessors/__init__.py

-from django.conf import settings
-from django.contrib.sites.models import Site
-
-def general(request):
-	return {
-		"site": Site.objects.get_current(),
-	}

sp/sitepoet/drupal_support/__init__.py

Empty file removed.

sp/sitepoet/drupal_support/management/__init__.py

Empty file removed.

sp/sitepoet/drupal_support/management/commands/__init__.py

Empty file removed.

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

-import datetime
-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 django.core.management.base import BaseCommand, CommandError
-
-from sitepoet.drupal_support.models import *
-from sitepoet.models import *
-from sitepoet.templatetags.slugify import slugify
-
-from threadedcomments.models import ThreadedComment
-
-def vancode2int(vancode):
-	if len(vancode):
-		result = int(vancode[1:], 36)
-	else:
-		result = None
-	return result
-
-class Command(BaseCommand):
-	# option_list = BaseCommand.option_list + (
-	# 	make_option('--format', default='json', dest='format', help='Specifies the output serialization format for fixtures.'),
-	# 	make_option('--indent', default=None, dest='indent', type='int', help='Specifies the indent level to use when pretty-printing output'),
-	# 	make_option('-e', '--exclude', dest='exclude',action='append', default=[], help='App to exclude (use multiple --exclude to exclude multiple apps).'),
-	# 	make_option('-v', '--verbose', dest='verbose', action="store", default="0", type="choice", choices=['0','1','2'], help='Verbose output'),
-	# )
-	help = 'Convert Drupal 6 tables to SP objects.	The tables must be in the same database as SP.'
-	# args = '[appname ...]'
-	
-	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__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()
-			
-			(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
-				node_aliases = Redirect.objects.filter(original='/node/%d' % node.nid)
-				if node_aliases.count() > 0:
-					if verbose: print "Skipping node %d" % node.nid
-					continue
-				
-				# 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,
-						)
-				
-					# Set the current content value
-					try:
-						contents = node.current_revision.get_parsed_contents()
-					
-						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()
-					
-					# 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()
-				
-				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()
-				
-				# 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 = obj,
-							field_name = "content",
-							content = contents['body'],
-						)
-						ch.save()
-						
-						if debug: print " Added revision %s" % (ch.date_created)
-				
-				# Bring over the comments
-				comments = self.get_comments_for_node(node)
-				if debug: print "  Comments:", comments.all()
-				for comment in comments:
-					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)
-					# 	For each remaining level, get the right relationship in a loop:
-					# 		Set an object to the first listed comment.
-					# 		Pop that value off the array.
-					# 		If there're more items left, set the loop object to that child of the current object.
-					# 		When we run out of list items, we found the parent.
-					
-					# 01.00.05.07/ to 01.00.05.07 to [01,00,05,07]
-					parents = comment.thread[:-1].split('.')
-					# [1,0,5,7]
-					parents = map(vancode2int, parents)
-					# [1,0,5]
-					parents = parents[:-1]
-					if len(parents):
-						# [0,0,5]
-						parents[0] = parents[0] - 1
-					
-					if debug: print "** Parents:", parents, "(%s)" % comment.thread
-					
-					c_obj = None
-					if len(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:
-							c_obj = obj_comments[parents[0]]
-						except IndexError, e:
-							if verbose: print "*** Parent of comment subtree not found.  This can happen if a thread has a deleted comment."
-						parents = parents[1:]
-						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:
-								if verbose: print "** Object has %d comments, but the parent should be item %d" % (c_obj.children.count(), parents[0])
-								break
-						if debug and c_obj: print "** Suspected parent:", c_obj.title
-					
-					# Actually create the comment now
-					c = ThreadedComment(
-						content_object = obj,
-						title = unicode(comment.subject)[:200],
-						ip_address = unicode(comment.hostname)[:15],
-						user_name = unicode(comment.name)[:50],
-						user_email = unicode(comment.mail)[:75],
-						user_url = unicode(comment.homepage)[:200],
-						submit_date = datetime.datetime.fromtimestamp(comment.timestamp),
-						comment = unicode(comment.comment),
-						site = Site.objects.get_current(),
-						is_public = (not comment.status),
-					)
-					c.save()
-					
-					# 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 debug: print " Finished node: %s (%s)" % (obj.title, obj.get_absolute_url())
-			
-		except Exception, e:
-			if show_traceback:
-				raise
-			raise CommandError("Error: %s" % e)
-		
-		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()
-		
-		cursor.execute("SELECT cid FROM comments as c WHERE nid = %s ORDER BY SUBSTRING(c.thread, 1, (LENGTH(c.thread) - 1))", [node.nid])
-		rows = cursor.fetchall()
-		ids = [c[0] for c in rows]
-		comments = DrupalComment.objects.filter(pk__in=ids)
-		
-		return comments

sp/sitepoet/drupal_support/models.py

-import re
-from django.db import models
-from django.contrib import admin
-
-class DrupalComment(models.Model):
-	cid = models.IntegerField(primary_key=True)
-	pid = models.IntegerField(null=True, blank=True, default=0)
-	nid = models.ForeignKey("DrupalNode", db_column="nid")
-	uid = models.ForeignKey("DrupalUser", db_column="uid")
-	subject = models.CharField(null=True, blank=True, max_length=192)
-	comment = models.TextField(null=True, blank=True, default="")
-	hostname = models.CharField(null=True, blank=True, max_length=384)
-	timestamp = models.IntegerField(null=True, blank=True, default=0)
-	status = models.IntegerField(null=True, blank=True, default=0)
-	format = models.IntegerField(null=True, blank=True, default=0)
-	thread = models.CharField(null=True, blank=True, max_length=765)
-	name = models.CharField(null=True, blank=True, max_length=180)
-	mail = models.CharField(null=True, blank=True, max_length=192)
-	homepage = models.CharField(null=True, blank=True, max_length=765)
-	class Meta:
-		db_table = u'comments'
-
-class DrupalNode(models.Model):
-	nid = models.IntegerField(primary_key=True)
-	type = models.CharField(max_length=96, default="")
-	title = models.CharField(max_length=765, default="")
-	uid = models.ForeignKey("DrupalUser", db_column="uid")
-	status = models.IntegerField(default=0)
-	created = models.IntegerField(default=0)
-	changed = models.IntegerField(default=0)
-	comment = models.IntegerField(default=0)
-	promote = models.IntegerField(default=0)
-	moderate = models.IntegerField(default=0)
-	sticky = models.IntegerField(default=0)
-	current_revision = models.ForeignKey("DrupalNodeRevision", db_column="vid")