Mercurial > personas_backend
view PersonasBackend/personas/models.py @ 44:9e11c9374822
The JSON feeds now properly provide absolute URLs to personas, regardless of whether the persona images are self-hosted or not.
author | Atul Varma <varmaa@toolness.com> |
---|---|
date | Tue, 04 Mar 2008 11:15:10 -0600 |
parents | 564818c67b57 |
children | cf6b5f26e902 |
line wrap: on
line source
# ***** BEGIN LICENSE BLOCK ***** # Version: MPL 1.1/GPL 2.0/LGPL 2.1 # # The contents of this file are subject to the Mozilla Public License Version # 1.1 (the "License"); you may not use this file except in compliance with # the License. You may obtain a copy of the License at # http://www.mozilla.org/MPL/ # # Software distributed under the License is distributed on an "AS IS" basis, # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License # for the specific language governing rights and limitations under the # License. # # The Original Code is PersonasBackend. # # The Initial Developer of the Original Code is Mozilla. # Portions created by the Initial Developer are Copyright (C) 2008 # the Initial Developer. All Rights Reserved. # # Contributor(s): # Atul Varma <atul@mozilla.org> # # Alternatively, the contents of this file may be used under the terms of # either the GNU General Public License Version 2 or later (the "GPL"), or # the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), # in which case the provisions of the GPL or the LGPL are applicable instead # of those above. If you wish to allow use of your version of this file only # under the terms of either the GPL or the LGPL, and not to allow others to # use your version of this file under the terms of the MPL, indicate your # decision by deleting the provisions above and replace them with the notice # and other provisions required by the GPL or the LGPL. If you do not delete # the provisions above, a recipient may use your version of this file under # the terms of any one of the MPL, the GPL or the LGPL. # # ***** END LICENSE BLOCK ***** """ models.py This module contains the models for the Personas application. Note that changes to Personas are automatically revision-controlled. Among other things, this allows for: (A) A humane user interface that supports undo operations. (B) A workflow that allows for Personas to be changed by a submitter and reviewed while the original, unchanged Persona remains published and accessible by clients. """ import datetime from django.db import models from django.contrib.auth.models import User from django.contrib.sites.models import Site def _external_url_or_hosted_media_url( external, media ): """ Given an absolute URL representing an externally-hosted resource OR an absolute or relative URL representing a hosted resource, returns an absolute URL to the resource. >>> _external_url_or_hosted_media_url( ... 'http://blarg.com/', None ... ) 'http://blarg.com/' >>> _external_url_or_hosted_media_url( ... None, '/personas/headers/blah.jpg' ... ) u'http://example.com/personas/headers/blah.jpg' >>> _external_url_or_hosted_media_url( ... None, 'http://blah.com/personas/headers/blah.jpg' ... ) 'http://blah.com/personas/headers/blah.jpg' """ # TODO: Consider uncommenting the following assertions; they may # make some tests break, though. # #assert external or media, \ # "one parameter must be non-null" #assert (not external and media), \ # "both paramaters cannot be be non-null" if external: return external else: if not media.startswith( "/" ): return media else: return "http://%s%s" % ( Site.objects.get_current().domain, media ) class Category(models.Model): """ Represents a Personas category; every Persona can be assigned one, and only one, category. """ class Admin: pass class Meta: verbose_name_plural = "categories" name = models.CharField( "Category name", max_length=50, unique=True, ) def __str__(self): return self.name class Persona(models.Model): """ Encapsulates the most recent revision of a Persona. """ class Admin: pass MAX_NAME_LENGTH = 50 name = models.CharField( max_length=MAX_NAME_LENGTH, blank=False, ) category = models.ForeignKey( Category, related_name="personas", null=False, ) description = models.TextField( help_text = "HTML is allowed.", blank=False, ) header_img_url = models.URLField( "Header image URL", help_text=("URL for the image that will be placed behind " "the browser's top chrome. Only needed if " "a header image file is not specified."), verify_exists=True, blank=True, ) MAX_FILE_NAME_LENGTH = 255 header_img_file = models.ImageField( "Header image file", help_text=("File for the image that will be placed behind " "the browser's top chrome. Only needed if " "a header image URL is not specified."), upload_to="headers", blank=True, max_length=MAX_FILE_NAME_LENGTH, ) footer_img_url = models.URLField( "Footer image URL", help_text=("URL for the image that will be placed behind " "the browser's bottom chrome. Only needed if " "a footer image file is not specified."), verify_exists=True, blank=True, max_length=MAX_FILE_NAME_LENGTH, ) footer_img_file = models.ImageField( "Footer image file", help_text=("File for the image that will be placed behind " "the browser's bottom chrome. Only needed if " "a footer image URL is not specified."), upload_to="footers", blank=True, ) MAX_COLOR_SCHEME_LENGTH = 10 color_scheme = models.CharField( help_text=("If 'light', any text displayed over the " "Persona will be dark. If 'dark', any text " "displayed over the Persona will be light."), max_length=MAX_COLOR_SCHEME_LENGTH, choices=(("light", "Light"), ("dark", "Dark")), blank=False, default="light", ) date_updated = models.DateTimeField( # This ensures that this field is updated with the current # timestamp whenever the record is changed. auto_now=True, null=False, editable=False, ) updater = models.ForeignKey( User, help_text="The user who made this revision.", related_name="personas", null=True, ) revision = models.PositiveIntegerField( "Revision number", help_text=("This number is incremented whenever the Persona " "is changed."), null=False, editable=False, ) MAX_STATUS_LENGTH = 10 status = models.CharField( max_length=MAX_STATUS_LENGTH, choices=(("published", "Published"), ("needs_review", "Needs Review"), ("deleted", "Deleted")), blank=False, default="published", # TODO: Add help text for this, and figure out what all # possible statuses are. ) def __str__(self): return self.name # These are the properties of this record that are "versioned", # i.e. tracked by built-in revision control when changed. VERSIONED_PROPERTIES = ( "name", "category", "description", "header_img_url", "header_img_file", "footer_img_url", "footer_img_file", "color_scheme", "status", ) def get_header_url(self): """ Regardless of how a header image was submitted or where it's located, return an absolute URL to its location. """ return _external_url_or_hosted_media_url( self.header_img_url, self.get_header_img_file_url() ) def get_footer_url(self): """ Regardless of how a footer image was submitted or where it's located, return an absolute URL to its location. """ return _external_url_or_hosted_media_url( self.footer_img_url, self.get_footer_img_file_url() ) def __make_new_revision(self): """ Detect if any of our versioned properties have changed, and if so, make a new revision. """ original = Persona.objects.get(id=self.id) assert original.revision == self.revision delta = {} for attr in self.VERSIONED_PROPERTIES: origValue = getattr(original, attr) newValue = getattr(self, attr) if origValue != newValue: delta[attr] = origValue if delta: rev = Revision( revision_of = original, date_updated = original.date_updated, updater = original.updater, revision = original.revision ) for attr in delta: setattr(rev, attr, delta[attr]) setattr(rev, "has_%s" % attr, True) rev.save() self.revision += 1 def save(self): """ Saves the object. If a versioned property is changed, a new revision is generated automatically. Example: >>> p = Persona(name='Test Persona', ... description='Thos is a test.', ... header_img_file='test.png', ... category=Category.objects.get(name='General')) >>> p.save() >>> p.revision 0 >>> p.revisions.all() [] >>> p.description = 'This is a test.' >>> p.header_img_file = 'blarg.png' >>> p.save() >>> p.revision 1 >>> p.revisions.all() [<Revision: Test Persona - r0>] >>> p.revisions.get(revision=0).description u'Thos is a test.' >>> p.revisions.get(revision=0).header_img_file u'test.png' """ if self.id == None: self.revision = 0 else: # TODO: See who made the update; depending on how much we # trust them, this may mean: # # * rejecting the change, # * accepting the changes but marking the persona's # status as "needs review", # * accepting the changes. self.__make_new_revision() super(Persona, self).save() class Revision(models.Model): """ Represents an old revision of a Persona. This record only stores a "reverse delta" relative to the revision that follows it; for instance, if a user changes a Persona at revision 0 by modifying its description, then the Revision record for revision 0 will contain only the description for the Persona at that revision, since all other properties can be obtained by looking at revision 1. In this way, the complete "picture" of revision 0 can be reconstructed by starting at the record for revision 0 and traveling to newer revisions until every field of the Persona has been populated. """ class Meta: unique_together = (("revision_of", "revision"),) ordering = ["revision"] # Auto-generated fields revision_of = models.ForeignKey( Persona, verbose_name="The Persona that this record is an old revision of", related_name="revisions", null=False, ) date_updated = models.DateTimeField( null=False, ) updater = models.ForeignKey( User, related_name="revisions", null=True, ) revision = models.PositiveIntegerField( null=False, ) # Versioned fields for attr in Persona.VERSIONED_PROPERTIES: locals()["has_%s" % attr] = models.BooleanField(default=False) del attr name = models.CharField( max_length=Persona.MAX_NAME_LENGTH, null=True, ) category = models.ForeignKey( Category, related_name="revisions", null=True, ) description = models.TextField( null=True, ) # TODO: Add validation that ensures that either header_img_url or # header_img_file is non-null; same goes for footer. header_img_url = models.URLField( null=True, ) header_img_file = models.CharField( max_length=Persona.MAX_FILE_NAME_LENGTH, null=True ) footer_img_url = models.URLField( null=True, ) footer_img_file = models.CharField( max_length=Persona.MAX_FILE_NAME_LENGTH, null=True ) color_scheme = models.CharField( max_length=Persona.MAX_COLOR_SCHEME_LENGTH, null=True, ) status = models.CharField( max_length=Persona.MAX_STATUS_LENGTH, null=True, ) def __str__(self): return "%s - r%s" % (self.revision_of.name, self.revision) def resolve(self): """ Fills-in (resolves) all the fields of the revision by computing reverse deltas to future/present revisions. >>> p = Persona(name='Test Persona', ... description='Thos is a test.', ... footer_img_file='test.png', ... category=Category.objects.get(name='General')) >>> p.save() >>> p.description = 'This is a test.' >>> p.save() >>> p.name = 'Blarg' >>> p.save() >>> p.description = "This is a great test." >>> p.save() >>> p.header_img_url = "http://www.test.com" >>> p.footer_img_file = "test2.png" >>> p.save() >>> r = p.revisions.get(revision=0) >>> r.resolve() >>> r.name u'Test Persona' >>> r.description u'Thos is a test.' >>> r.header_img_url u'' >>> r.footer_img_file u'test.png' """ persona = self.revision_of futureRevs = persona.revisions.filter(revision__gt=self.revision) attrsLeft = [ attr for attr in Persona.VERSIONED_PROPERTIES if getattr(self, attr) == None ] for rev in futureRevs: attrsFound = [] for attr in attrsLeft: if getattr(rev, "has_%s" % attr): setattr(self, attr, getattr(rev, attr)) attrsFound.append(attr) if attrsFound: attrsLeft = [ attr for attr in attrsLeft if attr not in attrsFound ] if not attrsLeft: break for attr in attrsLeft: setattr(self, attr, getattr(persona, attr)) def save(self): """ The save method for a Revision can only be called once, because Revision objects are meant to be immutable: >>> p = Persona(name='Test Persona', ... category=Category.objects.get(name='General')) >>> p.save() >>> p.description = 'This is a test.' >>> p.save() >>> r = p.revisions.get(revision=0) >>> r.status = "deleted" >>> r.save() Traceback (most recent call last): ... AssertionError: Revisions are immutable """ if self.id == None: super(Revision, self).save() else: raise AssertionError("Revisions are immutable")