GH: kevinastone | TW: @kevinastone | LI: kevinastone
The Compositional Model pattern allows you to manage the complexity of your models through compartmentalization of functionality into manageable components.
Decompose models into core reusable mixins
from django.db import models
from django.contrib.auth.models import User
class BlogPost(models.Model):
title = models.CharField(max_length=255)
body = models.TextField()
slug = models.SlugField()
author = models.ForeignKey(User, related_name='posts')
create_date = models.DateTimeField(auto_now_add=True)
modified_date = models.DateTimeField(auto_now=True)
publish_date = models.DateTimeField(null=True)
from django.db import models
from django.contrib.auth.models import User
class BlogPost(models.Model):
title = models.CharField(max_length=255)
body = models.TextField()
slug = models.SlugField()
author = models.ForeignKey(User, related_name='posts')
create_date = models.DateTimeField(auto_now_add=True)
modified_date = models.DateTimeField(auto_now=True)
publish_date = models.DateTimeField(null=True)
from django.db import models
from .behaviors import Authorable, Permalinkable, Timestampable, Publishable
class BlogPost(Authorable, Permalinkable, Timestampable, Publishable, models.Model):
title = models.CharField(max_length=255)
body = models.TextField()
from django.contrib.auth.models import User
class Authorable(models.Model):
author = models.ForeignKey(User)
class Meta:
abstract = True
class Permalinkable(models.Model):
slug = models.SlugField()
class Meta:
abstract = True
...
...
class Publishable(models.Model):
publish_date = models.DateTimeField(null=True)
class Meta:
abstract = True
class Timestampable(models.Model):
create_date = models.DateTimeField(auto_now_add=True)
modified_date = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
class BlogPost(models.Model):
...
@property
def is_published(self):
from django.utils import timezone
return self.publish_date < timezone.now()
@models.permalink
def get_absolute_url(self):
return ('blog-post', (), {
"slug": self.slug,
})
def pre_save(self, instance, add):
from django.utils.text import slugify
if not instance.slug:
instance.slug = slugify(self.title)
class Permalinkable(models.Model):
slug = models.SlugField()
class Meta:
abstract = True
def get_url_kwargs(self, **kwargs):
kwargs.update(getattr(self, 'url_kwargs', {}))
return kwargs
@models.permalink
def get_absolute_url(self):
url_kwargs = self.get_url_kwargs(slug=self.slug)
return (self.url_name, (), url_kwargs)
def pre_save(self, instance, add):
from django.utils.text import slugify
if not instance.slug:
instance.slug = slugify(self.slug_source)
class Publishable(models.Model):
publish_date = models.DateTimeField(null=True)
class Meta:
abstract = True
objects = PassThroughManager.for_queryset_class(PublishableQuerySet)()
def publish_on(self, date=None):
from django.utils import timezone
if not date:
date = timezone.now()
self.publish_date = date
self.save()
@property
def is_published(self):
from django.utils import timezone
return self.publish_date < timezone.now()
from django.db import models
from .behaviors import Authorable, Permalinkable, Timestampable, Publishable
class BlogPost(Authorable, Permalinkable, Timestampable, Publishable, models.Model):
title = models.CharField(max_length=255)
body = models.TextField()
url_name = "blog-post"
@property
def slug_source(self):
return self.title
from django.utils import timezone
from .models import BlogPost
>>> BlogPost.objects.filter(author__username='username1') \
.filter(publish_date__lte=timezone.now())
class BlogPostManager(models.Manager):
def published(self):
from django.utils import timezone
return self.filter(publish_date__lte=timezone.now())
def authored_by(self, author):
return self.filter(author__username=author)
class BlogPost(models.Model):
...
objects = BlogPostManager()
>>> published_posts = BlogPost.objects.published() >>> posts_by_author = BlockPost.objects.authored_by('username1')
>>> BlogPost.objects.authored_by('username1').published() AttributeError: 'QuerySet' object has no attribute 'published'
>>> type(Blogpost.objects.authored_by('username1')) <class 'django.db.models.query.QuerySet'>
from model_utils.managers import PassThroughManager class PublishableQuerySet(models.query.QuerySet): def published(self): from django.utils import timezone return self.filter(publish_date__lte=timezone.now()) class AuthorableQuerySet(models.query.QuerySet): def authored_by(self, author): return self.filter(author__username=author)
class BlogPostQuerySet(AuthorableQuerySet, PublishableQuerySet):
pass
class BlogPost(Authorable, Permalinkable, Timestampable, Publishable, models.Model):
...
objects = PassThroughManager.for_queryset_class(BlogPostQuerySet)()
>>> author_public_posts = BlogPost.objects.authored_by('username1').published()
>>> type(Blogpost.objects.authored_by('username1'))
<class 'example.queryset.BlogPostQuerySet'>
BlogPost.objects.filter(author__username='username1').filter(publish_date__lte=timezone.now())
BlogPost.objects.authored_by('username1').published()
from django.test import TestCase
from .models import BlogPost
class BlogPostTestCase(TestCase):
def test_published_blogpost(self):
from django.utils import timezone
blogpost = BlogPost.objects.create(publish_date=timezone.now())
self.assertTrue(blogpost.is_published)
self.assertIn(blogpost, BlogPost.objects.published())
class BehaviorTestCaseMixin(object):
def get_model(self):
return getattr(self, 'model')
def create_instance(self, **kwargs):
raise NotImplementedError("Implement me")
class PublishableTests(BehaviorTestCaseMixin):
def test_published_blogpost(self):
from django.utils import timezone
obj = self.create_instance(publish_date=timezone.now())
self.assertTrue(obj.is_published)
self.assertIn(obj, self.model.objects.published())
from django.test import TestCase from .models import BlogPost
from .behaviors.tests import PublishableTests class BlogPostTestCase(PublishableTests, TestCase): model = BlogPost def create_instance(self, **kwargs): return BlogPost.objects.create(**kwargs)
class BlogPostTestCase(PublishableTests, AuthorableTests, PermalinkableTests, TimestampableTests, TestCase):
model = BlogPost
def create_instance(self, **kwargs):
return BlogPost.objects.create(**kwargs)
def test_blog_specific_functionality(self):
...
class StaffBlogPostTestCase(PublishableTests, AuthorableTests, PermalinkableTests, TimestampableTests, BaseBlogPostTestCase):
det setUp(self):
self.user = StaffUser()
class AuthorizedUserBlogPostTestCase(PublishableTests, AuthorableTests, PermalinkableTests, TimestampableTests, BaseBlogPostTestCase):
det setUp(self):
self.user = AuthorizedUser()
(Same behavior expected for Staff or Authorized User)
from django.db import models
from .behaviors import Authorable, Permalinkable, Timestampable, Publishable
class BlogPost(Authorable, Permalinkable, Timestampable, Publishable, models.Model):
title = models.CharField(max_length=255)
body = models.TextField()
url_name = "blog-post"
@property
def slug_source(self):
return self.title
class BlogComment(Authorable, Permalinkable, Timestampable, models.Model):
post = models.ForeignKey(BlogPost, related_name='comments')
subject = models.CharField(max_length=255)
body = models.TextField()
url_name = 'blog-comment'
def get_url_kwargs(self, **kwargs):
return super(BlogComment, self).get_url_kwargs(post_slug=self.post.slug, **kwargs)
@property
def slug_source(self):
return self.subject