Commits

Adam Knight committed 990c5eb

Integrated an auto-detecting site object since Django's couldn't handle it.

  • Participants
  • Parent commits 24e1309

Comments (0)

Files changed (13)

     actions           = [action_mark_draft, action_mark_published]
     readonly_fields = ['date_created', 'date_modified']
     fieldsets       = [
-        ('',                {'fields': ['title', 'url', 'sites']}),
+        ('',                {'fields': ['title', 'url', 'site']}),
         (_('Content'),        {'fields': ['content_format', 'content', 'template_name']}),
         (_('Availability'), {'fields': ['status', 'date_published', 'date_hidden', 'date_created', 'date_modified'], 'classes':('collapse',)}),
     ]
 ### STORIES ###
 
 class StoryAdmin(admin.ModelAdmin):
-    list_display = ('title', 'tags', 'section', 'status', 'allow_comments', 'date_published', 'date_hidden', 'date_modified')
+    list_display = ('title', 'site_name', 'section', 'status', 'allow_comments', 'date_published', 'date_hidden', 'date_modified')
     list_editable = ('status','section')
-    list_filter = ('section', 'date_published', 'date_hidden', 'date_modified', 'status')
+    list_filter = ('section', 'date_published', 'date_hidden', 'date_modified', 'status', 'site__name')
     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]
     readonly_fields = ['date_created', 'date_modified']
     fieldsets = [
-        ('',                {'fields': ['user', 'title', 'slug', 'tags', 'section', 'allow_comments', 'sites']}),
+        ('',                {'fields': ['user', 'title', 'slug', 'tags', 'section', 'allow_comments', 'site']}),
         ('Content',            {'fields': ['teaser_format', 'teaser', 'content_format', 'content']}),
         ('Availability',    {'fields': ['status', 'date_published', 'date_hidden', 'date_created', 'date_modified'], 'classes':('collapse',)}),
         ]
+    def site_name(self, obj):
+        return obj.site.name
 
 admin.site.register(Story, StoryAdmin)
 
 ### REDIRECTION ###
 
 class RedirectAdmin(admin.ModelAdmin):
-    list_display = ("original", "current", "content_type", "object_id", "permanent", "date_modified", "date_created")
+    list_display = ("original", "current", "site", "content_type", "object_id", "permanent", "date_modified", "date_created")
     fieldsets = (
         ("Source", {"fields": ["site", "original"]}),
         ("Destination", {"fields": ["current", "content_type", "object_id"]}),
         ("Options", {"fields": ["permanent"]})
     )
+    list_filter = ('site__name', 'date_created', 'date_modified', 'permanent', 'content_type')
     readonly_fields = ['date_created', 'date_modified']
 
 admin.site.register(Redirect, RedirectAdmin)
+
+### SITE ###
+
+class MiltonSiteAdmin(admin.ModelAdmin):
+    list_display = ("name", "base_url", "alias_for")
+
+admin.site.register(MiltonSite, MiltonSiteAdmin)

context_preprocessors/__init__.py

-from django.contrib.sites.models import Site
-from milton.models import Section
+from milton.models import Section, MiltonSite
 
 def general(request):
     return {
-        "site": Site.objects.get_current(),
+        "site": MiltonSite.objects.get_current(),
         "sections": Section.objects.all(),
     }

drupal_support/management/commands/convertdrupal.py

 import datetime
+import logging
 import re
 from optparse import make_option
 
 from django.db.models import Q
+from django.contrib.sites.models import Site
 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 milton.drupal_support.models import *
-from milton.models import Section, Page, Story, Redirect, ContentHistory
+from milton.models import Section, Page, Story, Redirect, ContentHistory, MiltonSite
 from milton.templatetags.slugify import slugify
 
 from milton.threadedcomments.models import MPTTComment
 
+
 def vancode2int(vancode):
     if len(vancode):
         result = int(vancode[1:], 36)
     #     make_option('-v', '--verbose', dest='verbose', action="store", default="0", type="choice", choices=['0','1','2'], help='Verbose output'),
         make_option(None,'--comments', dest='comments', action="store_true", default=True, help="Import comments (default)."),
         make_option(None,'--no-comments', dest='comments', action="store_false", default=True, help="Do not import comments."),
-        make_option(None,'--site-id', dest='site_id', action="store", help="Site ID to import records to (defaults to the one in settings)."),
+        make_option(None,'--site-id', dest='site_id', action="store", help="Site ID to import records to."),
+        make_option(None,'--site-url', dest='site_url', action="store", help="Lookup an existing site by URL."),
     )
-    help = 'Convert Drupal 6 tables to SP objects.    The tables must be in the same database as SP.'
+    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):
-	import_comments = options.get('comments', True)
+        log = logging.getLogger("milton")
+        
+    	import_comments = options.get('comments', True)
         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
+        if debug:
+            log.setLevel(logging.DEBUG)
+        elif verbose:
+            log.setLevel(logging.INFO)
+        else:
+            log.setLevel(logging.WARNING)
+        
+        
+        log.info("Starting Drupal conversion.")
+        log.debug("* Importing data with %s as the content owner." % (user.username,))
         
         # Prefetch the destination site
         if options.get("site_id", None):
-            site_obj = Site.objects.get(pk=options.get("site_id"))
-        else:
-            site_obj = Site.objects.get_current()
+            site_obj = MiltonSite.objects.get(pk=options.get("site_id"))
+        elif options.get("site_url", None):
+            site_obj = MiltonSite.objects.get_site_for_URL(options.get("site_url"))
+        
+        if not site_obj:
+            raise CommandError("No site specified. Use either --site-id or --site-url to specify a site for the import.")
+        
+        # Follow the rabbit
+        # while site_obj.alias_for:
+            # site_obj = site_obj.alias_for
         
-        if verbose: print "* Using site %s" % (site_obj.name,)
+        log.info("* Using site %s" % (site_obj.name,))
         
         try:
             # Get a list of nodes together and filter to core nodes.
             node_list = DrupalNode.objects.filter(type__in=("blog", "story", "page")).order_by("nid")
-            if verbose: print "Found %d nodes." % len(node_list)
+            log.info("Found %d nodes." % len(node_list))
             
             if len(node_list) == 0:
                 raise CommandError("No supported Drupal nodes found in the current database.")
             # Create some sections
             (blog_section, c) = Section.objects.get_or_create(name="Blog", slug="blog")
             if c:
-                if debug: print "* Created blog section."
+                log.debug("* Created blog section.")
                 blog_section.save()
             
             (story_section, c) = Section.objects.get_or_create(name="Story", slug="story")
             if c:
-                if debug: print "* Created story section."
+                log.debug("* Created story section.")
                 story_section.save()
     
             blog_section = None # I don't want to change the URLs for these.
                 # See if we've imported this node before
                 node_aliases = Redirect.objects.filter(original='/node/%d' % node.nid, site=site_obj)
                 if node_aliases.count() > 0:
-                    if verbose: print "Skipping node %d" % node.nid
+                    log.info("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)
+                log.info("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."
+                        log.debug("Node is a blog.")
                         section = blog_section
                     else:
-                        if debug: print "Node is a story."
+                        log.debug("Node is a story.")
                         section = story_section
                     
                     # Create an Story for the node
                         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)
+                        log.info("WARNING: No body content found for %s (%d)" % (obj.title, obj.id))
                 
                     # Save the story
                     obj.save()
                     
                     # Add to the site
-                    obj.sites.add(site_obj)
+                    obj.site = site_obj
                     
                     # Import terms as tags
                     terms = node.current_revision.terms.all()
                     tags = [t.name for t in terms]
                     obj.tags = ','.join(tags) + ','
-                    if debug: print "* Set tags to: %s" % obj.tags
+                    log.debug("* 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."
+                    log.debug("Node is a page.")
                     
                     # Create an object for the node
                     obj = Page(
                     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)
+                        log.info("WARNING: No body content found for %s (%d)" % (obj.title, obj.id))
                     
                     # Save
                     obj.save()
                     
                     # Add to the current site
-                    obj.sites.add(site_obj)
+                    obj.site = site_obj
                     
                     # 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
+                        log.debug("* Page aliases: %s" % (aliases,))
                         obj.url = '/' + aliases[0].dst
                     
                     # Ensure the modification date is proper
                 
                 else:
                     # Bail if we don't have something to work on
-                    if verbose: print "Unsupported type:", node.type
+                    log.info("Unsupported type:", node.type)
                     continue
                 
                 # Lookup any URL aliases for this node and create redirects
                     try:
                         r = Redirect(original=dst, target_object=obj, site=site_obj)
                         r.save()
-                        if debug: print " Created redirect from", dst
+                        log.debug(" Created redirect from %s" % (dst,))
                     except Exception, e:
-                        if verbose: print "* Failed to create redirect from", dst, "to", obj, ":", e
+                        log.info("* Failed to create redirect from %s to %s :%s" % (dst, obj, e))
                         continue
                 
                 # Create a ContentHistory for each old body value
                         )
                         ch.save()
                         
-                        if debug: print " Added revision %s" % (ch.date_created,)
+                        log.debug(" Added revision %s" % (ch.date_created,))
                 
                 # Bring over the comments
                 if import_comments:
                 else:
                     comments = []
                 
-                # if debug: print "  Comments:", comments.count()
+                # log.debug("  Comments:", comments.count())
                 for comment in comments:
-                    if debug: print "  Importing comment: ", comment.thread, comment.subject
+                    log.debug("  Importing comment: %s %s" % (comment.thread, comment.subject))
                     
                     # To handle the nested comments:
                     #     Break up the thread property: 01.01.01 -> [1,1,1]
                         # [0,0,5]
                         parents[0] = parents[0] - 1
                     
-                    if debug: print "** Parents:", parents, "(%s)" % comment.thread
+                    log.debug("** Parents: %s (%s)" % (parents, comment.thread))
                     
                     c_obj = None
                     if len(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."
+                            log.info("*** 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])
+                                log.info("** 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
                     
                         user_url = unicode(comment.homepage)[:200],
                         submit_date = datetime.datetime.fromtimestamp(comment.timestamp),
                         comment = unicode(comment.comment),
-                        site = site_obj,
+                        site = Site.objects.get_current(),
                         is_public = (not comment.status),
                     )
                     c.save()
                     c.parent = c_obj
                     c.save()
                     
-                    if verbose: print " Imported comment (%d) %s" % (comment.cid, comment.subject)
+                    log.info(" Imported comment (%d) %s" % (comment.cid, comment.subject))
                 
-                if debug: print " Finished node: %s (%s)" % (obj.title, obj.get_absolute_url())
+                log.debug(" 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."
+        log.info("Done.")
     
     def get_comments_for_node(self, node):
         '''

fixtures/initial_data.json

+[
+    {
+        "fields": {
+            "alias_for": null, 
+            "base_url": "", 
+            "date_created": "2013-01-01T00:00:00", 
+            "date_modified": "2013-01-01T00:00:00", 
+            "description_html": "", 
+            "footer_html": "", 
+            "name": "Milton", 
+            "slogan": ""
+        }, 
+        "model": "milton.miltonsite", 
+        "pk": 1
+    }
+]
-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 = 1,
-            sites__in=[Site.objects.get_current()]
-        )
 import urlparse
 from django.conf import settings
 from django.contrib.auth.models import User
-from django.contrib.site.models import Site
 from django.core.urlresolvers import reverse
 from milton.models import *
 from milton.xmlrpc import public
     return _decorate
 
 def full_url(url=''):
-    return urlparse.urljoin("http://"+str(Site.objects.get_current_site().domain, url)
+    return urlparse.urljoin(str(MiltonSite.objects.get_current().base_url, url)
 
 # example... this is what wordpress returns:
 # {'permaLink': 'http://gabbas.wordpress.com/2006/05/09/hello-world/',

middleware/PageMiddleware.py

+from logging import getLogger
+
+from milton.models import Page
+from milton.views import view_page
+
+logger = getLogger(__name__)
+
+class PageMiddleware(object):
+    def process_request(self, request):
+        path = request.path_info
+        page = None
+        
+        # Look for a Page that matches; pass on failure.
+        try:
+            logger.debug("PageMiddleware: Looking for a page for %s" % (path,))
+            page = Page.published.get(url=path)
+        except Page.DoesNotExist, e:
+            return None
+        
+        logger.debug("PageMiddleware: Found for a page for %s" % (path,))
+        return view_page(request, path)

middleware/RedirectionMiddleware.py

+from logging import getLogger
+
+from django.conf import settings
+from django.http import HttpResponsePermanentRedirect, HttpResponseRedirect, HttpResponseGone, Http404
+from django.template import RequestContext, loader
+
+from milton.models import Redirect
+
+logger = getLogger(__name__)
+
+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]
+            
+            logger.debug("Redirect: Looking for %s" % (path,))
+
+            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:
+                logger.debug("Redirect: Object exists, but is unpublished.")
+                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
+            logger.debug("Redirect: No redirect found for for %s" % (path,))
+            return response
+        except:
+            if settings.DEBUG:
+                raise
+        # If we're here, we didn't catch something -- just pass along the 404.
+        logger.debug("Redirect: No alternate plan for %s; passing along the NotFound." % (path,))
+        return response

middleware/RequestMiddleware.py

+from threading import local
+
+thread_local = local()
+thread_local.request = None
+
+class RequestMiddleware(object):
+    
+    def process_request(self, request):
+        global thread_local
+        thread_local.request = request
+
+    @classmethod
+    def current_request(cls):
+        return thread_local.request

middleware/__init__.py

-from logging import getLogger
-
-from django.conf import settings
-from django.http import HttpResponsePermanentRedirect, HttpResponseRedirect, HttpResponseGone, Http404
-from django.template import RequestContext, loader
-
-from milton.models import Redirect, Page
-from milton.views import story_archive, view_page
-
-
-logger = getLogger(__name__)
-
-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]
-            
-            logger.debug("Redirect: Looking for %s" % (path,))
-
-            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:
-                logger.debug("Redirect: Object exists, but is unpublished.")
-                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
-            logger.debug("Redirect: No redirect found for for %s" % (path,))
-            return response
-        except:
-            if settings.DEBUG:
-                raise
-        # If we're here, we didn't catch something -- just pass along the 404.
-        logger.debug("Redirect: No alternate plan for %s; passing along the NotFound." % (path,))
-        return response
-
-
-class PageMiddleware(object):
-    def process_request(self, request):
-        path = request.path_info
-        page = None
-        
-        # Look for a Page that matches; pass on failure.
-        try:
-            logger.debug("PageMiddleware: Looking for a page for %s" % (path,))
-            page = Page.published.get(url=path)
-        except Page.DoesNotExist, e:
-            return None
-        
-        logger.debug("PageMiddleware: Found for a page for %s" % (path,))
-        return view_page(request, path)
     textile = None
 
 import datetime
+import logging
+import urlparse
+import os
 
 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.contrib.sites.managers import CurrentSiteManager
 from django.db import models
+from django.db.models import Q
 from django.utils.safestring import mark_safe
 from tagging.fields import TagField
 
-from milton.managers import *
+from milton.middleware.RequestMiddleware import RequestMiddleware
 
+log = logging.getLogger(__name__)
 
 ### CONSTANTS ###
 
     return mark_safe(output)
 
 
+### MANAGERS ###
+
+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 = 1,
+            site=MiltonSite.objects.get_current()
+        )
+
+class MiltonSiteManager (models.Manager):
+    def get_current(self):
+        site = None
+        request = RequestMiddleware.current_request()
+        
+        if request:
+            site = self.get_site_for_URL(request.build_absolute_uri())
+            
+        if not site:
+            site = self.get_query_set().get(pk=1)
+            
+        return site
+
+    def get_site_for_URL(self, url):
+        '''
+    Input: http://bob.blogs.public.example.com/blogs/bob/ted/sam/index.html?count=1&path=/some/data&interesting
+    Hostname: bob.blogs.public.example.com
+    Path: /blogs/bob/ted/sam
+    Result: ['bob.blogs.public.example.com/blogs/bob/ted/sam',
+             'bob.blogs.public.example.com/blogs/bob/ted',
+             'blogs.public.example.com/blogs/bob/ted/sam',
+             'bob.blogs.public.example.com/blogs/bob',
+             'blogs.public.example.com/blogs/bob/ted',
+             'public.example.com/blogs/bob/ted/sam',
+             'bob.blogs.public.example.com/blogs',
+             'blogs.public.example.com/blogs/bob',
+             'public.example.com/blogs/bob/ted',
+             'blogs.public.example.com/blogs',
+             'example.com/blogs/bob/ted/sam',
+             'bob.blogs.public.example.com',
+             'public.example.com/blogs/bob',
+             'example.com/blogs/bob/ted',
+             'blogs.public.example.com',
+             'public.example.com/blogs',
+             'example.com/blogs/bob',
+             'public.example.com',
+             'example.com/blogs',
+             'example.com']
+        '''
+        parts = urlparse.urlparse(url)
+        hostname = parts.hostname
+        path = os.path.dirname(parts.path)
+
+        if not (len(hostname) and len(path)):
+            return None
+
+        # Generate all valid permutations of the hostname and path.
+        permutations = ['']
+        hostnames = []
+        paths = []
+
+        hostname_parts = hostname.split(".") # ["bob", "example", "com"]
+        path_parts = path.split("/") # ["blogs", "bob"]
+
+        while len(hostname_parts) > 0:
+            hostnames.append(".".join(hostname_parts))
+            hostname_parts.pop(0)
+        while len(path_parts) > 0:
+            paths.append("/".join(path_parts))
+            path_parts.pop(-1)
+
+        for hostname in hostnames:
+            for path in paths:
+                permutations.append(hostname + path)
+
+        permutations.sort(key=len, reverse=True)
+
+        # Find the best matching site.
+        query = self.get_query_set().filter(base_url__in=permutations)
+        best_choice = None
+        for site in query:
+            if not best_choice:
+                best_choice = site
+                continue
+            
+            if len(site.base_url) > len(best_choice.base_url):
+                best_choice = site
+        
+        return best_choice
+
+
+class MiltonCurrentSiteManager (CurrentSiteManager):
+    def get_query_set(self):
+        if not self.__is_validated:
+            self._validate_field_name()
+        return super(MiltonCurrentSiteManager, self).get_query_set().filter(**{self.__field_name + '__id__exact': MiltonSite.objects.get_current().id})
+
+
 ### SPObject ###
 
 class SPObject (models.Model):
 
 ### Model Objects ###
 
+class MiltonSite(SPObject):
+    '''
+    The Base URL is special.  It works similar to Drupal's concept of sites in that several variations are generated, all are searched against, and the longest match wins (so that we can have one DB query).
+    http://mysite.bob.example.com/blog/bob/ will look for:
+        mysite.bob.example.com/blog/bob
+        mysite.bob.example.com/blog
+        bob.example.com/blog/bob
+        mysite.bob.example.com
+        example.com/blog/bob
+        bob.example.com
+        example.com
+        [empty]
+    Yes, an empty string is valid as a default site or a single-site installation with multiple domain names. Note that paths are not searched alone and the domain is always included.
+    '''
+    base_url = models.CharField("Base URL", max_length=255, blank=True, unique=True, help_text="An installation-wide unique value that will match against the request URL and uniquely identifies this site (eg. http://example.com/bob or http://bob.example.com). This value is also used to compose permalinks. See documentation for full details.") #TODO: documentation...
+    alias_for = models.ForeignKey('MiltonSite', null=True, blank=True, help_text="When this site is matched, use the site indicated here instead. Site aliases will never show their own content.")
+    
+    name = models.CharField("Site Name", max_length=255, blank=True, help_text="The human-readable, publicly-displayed name of the site.")
+    slogan = models.CharField("Slogan", max_length=255, blank=True, help_text="A short description or slogan for the site; a subhead.")
+    description_html = models.TextField("Description HTML", blank=True, help_text="This will be passed to templates as-is.")
+    footer_html = models.TextField("Footer HTML", blank=True, help_text="This will be passed to templates as-is.")
+    
+    objects = MiltonSiteManager()
+    
+    class Meta:
+        verbose_name = "site"
+
+    def __unicode__(self):
+        return self.name
+
+
 class Section(SPObject):
     # TODO: Create admin class that auto-gens the slug
+    site = models.ForeignKey(MiltonSite, null=True, blank=True, help_text='The site(s) this item is accessible at. Set to no value to use on all sites.')
     name = models.CharField("Section Name", max_length=255)
     slug = models.SlugField("Section Slug", max_length=255)
-    # page = models.ForeignKey(Page, null=True, blank=True, help_text="A page to display instead of a section index.")
+    # page = models.ForeignKey(Page, null=True, blank=True, help_text="A page to display instead of a section index.") # TODO: Would love this...
     
     class Meta:
         verbose_name = "section"
 
 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.')
+    site = models.ForeignKey(MiltonSite, null=True, blank=True, help_text='The site this item is accessible at. Set to no value to use on all sites (eg. a privacy policy or TOS).')
     title = models.CharField("Title", max_length=255, null=False, blank=False)
     
     teaser = models.TextField(null=True, blank=True)
 ### REDIRECTION ###
 
 class Redirect(SPObject):
-    site = models.ForeignKey(Site, default=settings.SITE_ID, help_text='The site this redirect is applicable to.')
+    site = models.ForeignKey(MiltonSite, null=True, blank=True, help_text='The site this redirect is active for. Set to no value to use on all sites.')
     original = models.CharField("Original path", max_length=255, 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,
     def testAbsoluteURL(self):
         obj = Page(title="URL Test", url="/url-test")
         obj.save()
-        obj.sites = [settings.SITE_ID]
+        obj.site = MiltonSite.objects.get_current()
         obj.save()
         
         url = obj.get_absolute_url()
     def testObjectRedirectTemporary(self):
         obj = Page(title="TestTemp", url="/test-temp", content="Lorum ipsum etc.")
         obj.save()
-        obj.sites = [settings.SITE_ID]
+        obj.site = MiltonSite.objects.get_current()
         obj.save()
         
         r = Redirect(original="/moved-obj-temp", target_object=obj, permanent=False)
     def testObjectRedirectPermanent(self):
         obj = Page(title="TestPerm", url="/test-perm", content="Lorum ipsum etc.")
         obj.save()
-        obj.sites = [settings.SITE_ID]
+        obj.site = MiltonSite.objects.get_current()
         obj.save()
         
         r = Redirect(original="/moved-obj-perm", target_object=obj, permanent=True)
     def testMissingObjectRedirect(self):
         obj = Page(title="TestGone", url="/test-gone", content="Lorum ipsum etc.")
         obj.save()
-        obj.sites = [settings.SITE_ID]
+        obj.site = MiltonSite.objects.get_current()
         obj.save()
         
         r = Redirect(original="/missing-obj", target_object=obj)
 from django.template import loader, Context, RequestContext
 from django.shortcuts import render_to_response, get_object_or_404
 from django.conf import settings
-from django.contrib.sites.models import *
 
 from milton.models import *
 
 
 def story_archive(request, section=None, year=None, month=None, day=None, teasers=None):
     if teasers == None:
-        teasers = settings.ARCHIVES_SHOW_TEASERS_ONLY
+        teasers = settings.ARCHIVES_SHOW_TEASERS_ONLY or True
     
     # If there's a section, try and load it then filter the stories by it
     try:
         url = "/" + url
 
     try:
-        page = Page.published.get(url__exact=url, sites__id__exact=settings.SITE_ID)
+        page = Page.published.get(url__exact=url)
     
     except Page.DoesNotExist, e:
         raise Http404
     if tags.count() == 0:
         raise Http404
     
+    # FIXME: Does not respect published status!
     items = TaggedItem.objects.get_intersection_by_model(Story, tags)
     
     # Paginate the results