mirror of
https://github.com/Hopiu/django-model-utils.git
synced 2026-03-16 20:00:23 +00:00
added SplitField
This commit is contained in:
parent
746e7c8a70
commit
85157347f1
5 changed files with 231 additions and 3 deletions
|
|
@ -4,6 +4,7 @@ CHANGES
|
|||
tip (unreleased)
|
||||
----------------
|
||||
|
||||
- added SplitField
|
||||
- added ChoiceEnum
|
||||
- added South support for custom model fields
|
||||
|
||||
|
|
|
|||
67
README.txt
67
README.txt
|
|
@ -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
|
||||
===========================
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Reference in a new issue