added SplitField

This commit is contained in:
Carl Meyer 2010-01-15 17:26:59 -05:00
parent 746e7c8a70
commit 85157347f1
5 changed files with 231 additions and 3 deletions

View file

@ -4,6 +4,7 @@ CHANGES
tip (unreleased)
----------------
- added SplitField
- added ChoiceEnum
- added South support for custom model fields

View file

@ -57,6 +57,73 @@ Be careful not to add new choices in the middle of the list, as that
will change the numerical ids for all subsequent choices, which could
impact existing data.
fields.SplitField
=================
A ``TextField`` subclass that automatically pulls an excerpt out of
its content (based on a "split here" marker or a default number of
initial paragraphs) and stores both its content and excerpt values in
the database.
A ``SplitField`` is easy to add to any model definition::
from django.db import models
from model_utils.fields import SplitField
class Article(models.Model):
title = models.CharField(max_length=100)
body = SplitField()
``SplitField`` automatically creates an extra non-editable field
``_body_excerpt`` to store the excerpt. This field doesn't need to be
accessed directly; see below.
Accessing a SplitField on a model
---------------------------------
When accessing an attribute of a model that was declared as a
``SplitField``, a ``SplitText`` object is returned. The ``SplitText``
object has two attributes:
``content``:
The full field contents.
``excerpt``:
The excerpt of ``content`` (read-only).
This object also has a ``__unicode__`` method that returns the full
content, allowing ``SplitField`` attributes to appear in templates
without having to access ``content`` directly.
Assuming the ``Article`` model above::
>>> a = Article.objects.all()[0]
>>> a.body.content
u'some text\n\n<!-- split -->\n\nmore text'
>>> a.body.excerpt
u'some text\n'
>>> unicode(a.body)
u'some text\n\n<!-- split -->\n\nmore text'
Assignment to ``a.body`` is equivalent to assignment to
``a.body.content``.
.. note::
a.body.excerpt is only updated when a.save() is called
Customized excerpting
---------------------
By default, ``SplitField`` looks for the marker ``<!-- split -->``
alone on a line and takes everything before that marker as the
excerpt. This marker can be customized by setting the ``SPLIT_MARKER``
setting.
If no marker is found in the content, the first two paragraphs (where
paragraphs are blocks of text separated by a blank line) are taken to
be the excerpt. This number can be customized by setting the
``SPLIT_DEFAULT_PARAGRAPHS`` setting.
models.InheritanceCastModel
===========================

View file

@ -1,6 +1,7 @@
from datetime import datetime
from django.db import models
from django.conf import settings
class AutoCreatedField (models.DateTimeField):
"""
@ -26,7 +27,98 @@ class AutoLastModifiedField (AutoCreatedField):
value = datetime.now()
setattr(model_instance, self.attname, value)
return value
SPLIT_MARKER = getattr(settings, 'SPLIT_MARKER', '<!-- split -->')
# the number of paragraphs after which to split if no marker
SPLIT_DEFAULT_PARAGRAPHS = getattr(settings, 'SPLIT_DEFAULT_PARAGRAPHS', 2)
_excerpt_field_name = lambda name: '_%s_excerpt' % name
def get_excerpt(content):
excerpt = []
default_excerpt = []
paras_seen = 0
for line in content.splitlines():
if not line.strip():
paras_seen += 1
if paras_seen < SPLIT_DEFAULT_PARAGRAPHS:
default_excerpt.append(line)
if line.strip() == SPLIT_MARKER:
return '\n'.join(excerpt)
excerpt.append(line)
return '\n'.join(default_excerpt)
class SplitText(object):
def __init__(self, instance, field_name, excerpt_field_name):
# instead of storing actual values store a reference to the instance
# along with field names, this makes assignment possible
self.instance = instance
self.field_name = field_name
self.excerpt_field_name = excerpt_field_name
# content is read/write
def _get_content(self):
return self.instance.__dict__[self.field_name]
def _set_content(self, val):
setattr(self.instance, self.field_name, val)
content = property(_get_content, _set_content)
# excerpt is a read only property
def _get_excerpt(self):
return getattr(self.instance, self.excerpt_field_name)
excerpt = property(_get_excerpt)
# allows display via templates without .content necessary
def __unicode__(self):
return self.content
class SplitDescriptor(object):
def __init__(self, field):
self.field = field
self.excerpt_field_name = _excerpt_field_name(self.field.name)
def __get__(self, instance, owner):
if instance is None:
raise AttributeError('Can only be accessed via an instance.')
content = instance.__dict__[self.field.name]
if content is None:
return None
return SplitText(instance, self.field.name, self.excerpt_field_name)
def __set__(self, obj, value):
if isinstance(value, SplitText):
obj.__dict__[self.field.name] = value.content
setattr(obj, self.excerpt_field_name, value.excerpt)
else:
obj.__dict__[self.field.name] = value
class SplitField(models.TextField):
def contribute_to_class(self, cls, name):
excerpt_field = models.TextField(editable=False)
excerpt_field.creation_counter = self.creation_counter+1
cls.add_to_class(_excerpt_field_name(name), excerpt_field)
super(SplitField, self).contribute_to_class(cls, name)
setattr(cls, self.name, SplitDescriptor(self))
def pre_save(self, model_instance, add):
value = super(SplitField, self).pre_save(model_instance, add)
excerpt = get_excerpt(value.content)
setattr(model_instance, _excerpt_field_name(self.attname), excerpt)
return value.content
def value_to_string(self, obj):
value = self._get_val_from_obj(obj)
return value.content
def get_db_prep_value(self, value):
try:
return value.content
except AttributeError:
return value
# allow South to handle these fields smoothly
try:
from south.modelsinspector import add_introspection_rules

View file

@ -2,6 +2,7 @@ from django.db import models
from model_utils.models import InheritanceCastModel, TimeStampedModel
from model_utils.managers import QueryManager
from model_utils.fields import SplitField
class InheritParent(InheritanceCastModel):
@ -26,3 +27,10 @@ class Post(models.Model):
class Meta:
ordering = ('order',)
class Article(models.Model):
title = models.CharField(max_length=50)
body = SplitField()
def __unicode__(self):
return self.title

View file

@ -2,9 +2,68 @@ from django.test import TestCase
from django.contrib.contenttypes.models import ContentType
from model_utils import ChoiceEnum
from model_utils.fields import get_excerpt
from model_utils.tests.models import InheritParent, InheritChild, TimeStamp, \
Post
Post, Article
class GetExcerptTests(TestCase):
def test_split(self):
e = get_excerpt("some content\n\n<!-- split -->\n\nsome more")
self.assertEquals(e, 'some content\n')
def test_auto_split(self):
e = get_excerpt("para one\n\npara two\n\npara three")
self.assertEquals(e, 'para one\n\npara two')
def test_middle_of_para(self):
e = get_excerpt("some text\n<!-- split -->\nmore text")
self.assertEquals(e, 'some text')
def test_middle_of_line(self):
e = get_excerpt("some text <!-- split --> more text")
self.assertEquals(e, "some text <!-- split --> more text")
class SplitFieldTests(TestCase):
full_text = u'summary\n\n<!-- split -->\n\nmore'
excerpt = u'summary\n'
def setUp(self):
self.post = Article.objects.create(
title='example post', body=self.full_text)
def test_unicode_content(self):
self.assertEquals(unicode(self.post.body), self.full_text)
def test_excerpt(self):
self.assertEquals(self.post.body.excerpt, self.excerpt)
def test_content(self):
self.assertEquals(self.post.body.content, self.full_text)
def test_load_back(self):
post = Article.objects.get(pk=self.post.pk)
self.assertEquals(post.body.content, self.post.body.content)
self.assertEquals(post.body.excerpt, self.post.body.excerpt)
def test_assign_to_body(self):
new_text = u'different\n\n<!-- split -->\n\nother'
self.post.body = new_text
self.post.save()
self.assertEquals(unicode(self.post.body), new_text)
def test_assign_to_content(self):
new_text = u'different\n\n<!-- split -->\n\nother'
self.post.body.content = new_text
self.post.save()
self.assertEquals(unicode(self.post.body), new_text)
def test_assign_to_excerpt(self):
def _invalid_assignment():
self.post.body.excerpt = 'this should fail'
self.assertRaises(AttributeError, _invalid_assignment)
class ChoiceEnumTests(TestCase):
def setUp(self):
self.STATUS = ChoiceEnum('DRAFT', 'PUBLISHED')
@ -17,7 +76,8 @@ class ChoiceEnumTests(TestCase):
def test_iteration(self):
self.assertEquals(tuple(self.STATUS), ((0, 'DRAFT'), (1, 'PUBLISHED')))
class InheritanceCastModelTests(TestCase):
def setUp(self):
self.parent = InheritParent.objects.create()