Commits

Anonymous committed cd62414

added SplitField

Comments (0)

Files changed (5)

 tip (unreleased)
 ----------------
 
+- added SplitField
 - added ChoiceEnum
 - added South support for custom model fields
 
 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
 ===========================
 

model_utils/fields.py

 from datetime import datetime
 
 from django.db import models
+from django.conf import settings
 
 class AutoCreatedField (models.DateTimeField):
     """
         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

model_utils/tests/models.py

 
 from model_utils.models import InheritanceCastModel, TimeStampedModel
 from model_utils.managers import QueryManager
+from model_utils.fields import SplitField
 
 
 class InheritParent(InheritanceCastModel):
 
     class Meta:
         ordering = ('order',)
+
+class Article(models.Model):
+    title = models.CharField(max_length=50)
+    body = SplitField()
+
+    def __unicode__(self):
+        return self.title

model_utils/tests/tests.py

 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')
 
     def test_iteration(self):
         self.assertEquals(tuple(self.STATUS), ((0, 'DRAFT'), (1, 'PUBLISHED')))
-    
+
+
 class InheritanceCastModelTests(TestCase):
     def setUp(self):
         self.parent = InheritParent.objects.create()
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.