milton /

Full commit
### IMPORTS ###

    import markdown
    markdown = None

    import textile
    textile = None

import datetime

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
from tagging.fields import TagField

from milton.managers import *



    (COMMENTS_DISABLED, 'Disabled'),
    (COMMENTS_CLOSED, 'Closed'),
    (COMMENTS_ENABLED, 'Enabled'),


    (ENTRY_STATUS_DRAFT, "Draft"),
    (ENTRY_STATUS_PUBLISHED, "Published"),




def format_text(text_format, text):
    output = text
    if text_format == CONTENT_FORMAT_TEXTILE:
        if textile != None:
            output = textile.textile(text)
            print "No Textile support found."
    elif text_format == CONTENT_FORMAT_MARKDOWN:
        if markdown != None:
            output = markdown.markdown(text)
            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([(, getattr(self, 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. '''
            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 =
        ''' 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 =
        super(SPObject, self).save(*args, **kwargs)
        self._original_state = self._as_dict()

### Model Objects ###

class Section(SPObject):
    # TODO: Create admin class that auto-gens the slug
    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.")
    class Meta:
        verbose_name = "section"
        verbose_name_plural = "sections"
    def __unicode__(self):
    def get_absolute_url(self):
        return ('story-archive',(), {'section':self.slug})

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",, 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
        get_latest_by = 'date_published'
    def __init__(self, *args, **kwargs):
        super(Resource, self).__init__(*args, **kwargs)
        if != None:
            self._original_url = self.get_absolute_url()
            self._original_url = ""
    def __unicode__(self):
        return self.title
    def visible(self):
        return (self.status == ENTRY_STATUS_PUBLISHED and self.date_published <
    def modified_since_publication(self):
        # Allow for some immediate changes for formatting, etc.
        forgiveness = datetime.timedelta(minutes=1)
        return (self.date_modified > (self.date_published + forgiveness))
    def formatted_teaser(self):
        return format_text(self.teaser_format, self.teaser)
    def formatted_content(self):
        return format_text(self.content_format, self.content)
    def previous(self):
        query = Story.published.order_by('-date_published', 'id').filter(date_published__lt=self.date_published)[0:1]
        if len(query):
            return query[0]
            return None
    def next(self):
        query = Story.published.order_by('date_published', 'id').filter(date_published__gt=self.date_published)[0:1]
        if len(query):
            return query[0]
            return None
    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:
            self._original_url = self.get_absolute_url()
        if update_teaser_history:
            ch = ContentHistory(owner=self, field_name="teaser", content=self.teaser)
        if update_content_history:
            ch = ContentHistory(owner=self, field_name="content", content=self.content)

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'
    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_all_urls(self):
        urls = [];
        if self.section:
            section = self.section.slug
            section = None
        kwargs = {
            'section': section,
            'year':    self.date_published.year,
            'month':   "%02d"%self.date_published.month,
            'day':     "%02d",
            'slug':    self.slug,
        urls.append(reverse('story-detail', kwargs = kwargs))
        del kwargs['slug']
        urls.append(reverse('story-archive', kwargs = kwargs))

        del kwargs['day']
        urls.append(reverse('story-archive', kwargs = kwargs))
        del kwargs['month']
        urls.append(reverse('story-archive', kwargs = kwargs))
        del kwargs['year']
        urls.append(reverse('story-archive', kwargs = kwargs))
        return urls
    def get_absolute_url(self):
        kwargs = {
            'year':    self.date_published.year,
            'month':   "%02d"%self.date_published.month,
            'day':     "%02d",
            'slug':    self.slug,
        if self.section and len(self.section.slug):
            kwargs['section'] = self.section.slug
        return ('story-detail', (), kwargs)


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, 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 (301)?')
    class Meta:
        verbose_name = "redirect"
        verbose_name_plural = "redirects"
        ordering = ['-date_created']
        unique_together = ("site", "original")
    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:
                destination = self.target_object.get_absolute_url()
            except Exception, e:
        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 u"%s: %s" % (self.owner, self.field_name)
    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')