milton / models.py

#!/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", 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
        get_latest_by = 'date_published'
    
    @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)
    
    @property
    def previous(self):
        try:
            return self.get_previous_by_date_published()
        except self.DoesNotExist:
            return None
    
    @property
    def next(self):
        try:
            return self.get_next_by_date_published()
        except self.DoesNotExist:
            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: 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)
    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):
        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
    
    @models.permalink
    def get_absolute_url(self):
        kwargs = {
            'year':    self.date_published.year,
            'month':   "%02d"%self.date_published.month,
            'day':     "%02d"%self.date_published.day,
            'slug':    self.slug,
        }
        
        if self.section and len(self.section.slug):
            kwargs['section'] = self.section.slug
        
        return ('story-detail', (), 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')
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.