EuroDjangoCon Django Patterns

86
Django Patterns James Tauber (and Brian Rosner)

description

by James Tauber and Bryan Rosner

Transcript of EuroDjangoCon Django Patterns

Page 1: EuroDjangoCon Django Patterns

Django PatternsJames Tauber

(and Brian Rosner)

Page 2: EuroDjangoCon Django Patterns

Django Patterns

this is the start not the end of the matter

hope it will form basis for more conversation during rest of conference

Page 3: EuroDjangoCon Django Patterns

What do I mean by “patterns”?

not the same as best practices

not quite the same as snippets

sometimes just idioms (not proper “design patterns”)

more descriptive than prescriptive

Page 4: EuroDjangoCon Django Patterns

Exampleenvironment-specific settings

try: from local_settings import *except ImportError: pass

Page 5: EuroDjangoCon Django Patterns

ExampleChange Log Model

class LogMessage(models.Model):

thing = models.ForeignKey(Thing) user = models.ForeignKey(User) timestamp = models.DateTimeField( default=datetime.now) message = models.TextField()

Page 6: EuroDjangoCon Django Patterns

ExampleChange Log Model

class LogMessage(models.Model):

thing = models.ForeignKey(Thing) user = models.ForeignKey(User) timestamp = models.DateTimeField( default=datetime.now) message = models.TextField()

not a reusable model, more “parameterized snippet”

Page 7: EuroDjangoCon Django Patterns

Some patterns can be turned in to constructs

generic views

render_to_response

new redirect in Django 1.1

view decorators

Page 8: EuroDjangoCon Django Patterns

Django IS Python

Page 9: EuroDjangoCon Django Patterns

I hope to give...

beginning Djangonauts something to try out

intermediate Djangonauts something to think about

advanced Djangonauts something to laugh at

Page 10: EuroDjangoCon Django Patterns
Page 11: EuroDjangoCon Django Patterns

Concepts

views

urlconfs

models

templates

forms

template tags

middleware

context processors

management commands

Page 12: EuroDjangoCon Django Patterns

Model Patterns

Page 13: EuroDjangoCon Django Patterns

The “Atom” Model

class Genre(models.Model): name = models.CharField( max_length=100, unique=True) def __unicode__(self): return self.name

Page 14: EuroDjangoCon Django Patterns

Common Fields

slug

title

description

creator

creation_timestampsometimes just “user” and

“timestamp” when an event

Page 15: EuroDjangoCon Django Patterns

“pseudo foreign key”

class Node(models.Model): ... subgraph = models.IntegerField( db_index=True)

Page 16: EuroDjangoCon Django Patterns

entity attribute values

class GameInformation(models.Model):

game = models.ForeignKey(Game) attribute = models.CharField( max_length=50) value = models.CharField( max_length=50)

Page 17: EuroDjangoCon Django Patterns

entity attribute valuesclass Attribute(models.Model):

node = models.ForeignKey(Node) attribute_type = models.CharField( max_length=20, db_index=True) value = models.CharField(max_length=50, db_index=True)

class Meta: unique_together = ( ('node', 'attribute_type', 'value'), )

class Node(models.Model): ... def attribute(self, attr_type): return [v["value"] for v in Attribute.objects.filter(node=self, attribute_type=attr_type).values()]

Page 18: EuroDjangoCon Django Patterns

zip choices

mood = models.CharField( blank=True, max_length=20, choices=zip(MOODS, MOODS))

Page 19: EuroDjangoCon Django Patterns

top-levelobject creation helper

GAME_POINTS = { "RATED_GAME": 1, ...}

def earn_game_points(user, reason): points = GAME_POINTS[reason] GamePointAward(user=user, points=points, reason=reason).save()

...earn_game_points(user, "RATED_GAME")

Page 20: EuroDjangoCon Django Patterns

Basic “Change Log”

class LogMessage(models.Model):

thing = models.ForeignKey(Thing) user = models.ForeignKey(User) timestamp = models.DateTimeField( default=datetime.now) message = models.TextField()

Page 21: EuroDjangoCon Django Patterns

association modelswithout ManyToManyField

class WishListItem(models.Model):

user = models.ForeignKey(User, related_name="wishlist") game = models.ForeignKey(Game) timestamp = models.DateTimeField( default=datetime.now)

...user_wishlist_items = user.wishlist_items.all() # will avoid O(n) queries to the Game model:user_wishlist_items = user.wishlist_items.select_related("game")

Page 22: EuroDjangoCon Django Patterns

symmetrical associations “friendships”

class Friendship(models.Model):

to_user = models.ForeignKey(User, related_name="_unused1") from_user = models.ForeignKey(User, related_name="_unused2_") timestamp = models.DateTimeField(default=datetime.now)

objects = FriendshipManager()

class Meta: unique_together = ( ('to_user', 'from_user'), )

Page 23: EuroDjangoCon Django Patterns

class FriendshipManager(models.Manager): def friends_for_user(self, user): friends = [] for friendship in self.filter(from_user=user) .select_related(depth=1): friends.append({"friend": friendship.to_user, "friendship": friendship}) for friendship in self.filter(to_user=user) .select_related(depth=1): friends.append({"friend": friendship.from_user, "friendship": friendship}) return friends

def are_friends(self, user1, user2): if self.filter(from_user=user1, to_user=user2) .count() > 0: return True if self.filter(from_user=user2, to_user=user1) .count() > 0: return True return False

Page 24: EuroDjangoCon Django Patterns

Trees

class ForumCategory(models.Model):

name = models.CharField( max_length=100) parent = models.ForeignKey('self', null=True, blank=True, related_name="subcategories")

Page 25: EuroDjangoCon Django Patterns

multi-strata treesclass Forum(models.Model):

title = models.CharField(max_length=100) description = models.TextField() creation_date = models.DateTimeField(default=datetime.now)

# must only have one of these (or neither): parent = models.ForeignKey('self', null=True, blank=True, related_name="subforums") category = models.ForeignKey(ForumCategory, null=True, blank=True, related_name="forums")

Page 26: EuroDjangoCon Django Patterns

lattice

class Node(models.Model): children = models.ManyToManyField('self', symmetrical=False, related_name='parents', blank=True)

Page 27: EuroDjangoCon Django Patterns

Denormalization and Calculated Field

Patterns

Page 28: EuroDjangoCon Django Patterns

usernameas well as user

class Profile(models.Model): user = models.ForeignKey(User, unique=True) username = models.CharField( max_length=50)

Page 29: EuroDjangoCon Django Patterns

view counts

view_count = models.IntegerField( default=0, editable=False)

def inc_views(self): self.view_count += 1 self.save()

Page 30: EuroDjangoCon Django Patterns

count based on another model

class BlogComment(models.Model): blog_post = models.ForeignKey(BlogPost, related_name="comments") ... class BlogPost(models.Model): ... commment_count = models.IntegerField(default=0, editable=False) def update_comment_count(self): self.comment_count = self.comments.count() self.save() def blog_comment_save(sender, instance=None, created=False, **kwargs): if instance and created: blog_post = instance.blog_post blog_post.update_comment_count() def blog_comment_delete(sender, instance=None, **kwargs): if instance: blog_post = instance.blog_post blog_post.update_comment_count() post_save.connect(blog_comment_save, sender=BlogComment)post_delete.connect(blog_comment_delete, sender=BlogComment)

Page 31: EuroDjangoCon Django Patterns

value based on another model

class MediaItem(models.Model): overall_rating = models.DecimalField(max_digits=3, decimal_places=1, default=0, editable=False) def update_rating(self): total_rating = 0 total_count = 0 for rating in self.ratings.all(): total_rating += rating.rating total_count += 1 self.overall_rating = str(1. * total_rating / total_count) self.save() class MediaRating(models.Model): media_item = models.ForeignKey(MediaItem, related_name="ratings") user = models.ForeignKey(Member) rating = models.IntegerField(default=0) timestamp = models.DateTimeField(default=datetime.now) class Meta: unique_together = ("media_item", "user")

Page 32: EuroDjangoCon Django Patterns

denormalizedforeign key

class Forum(models.Model): ... last_reply = models.ForeignKey("ForumReply", null=True, editable=False)

Page 33: EuroDjangoCon Django Patterns

Advice onCalculated Fields

make sure they are editable=False

where possible make them re-calculable(although doesn’t alway make sense)

be VERY careful with admin delete when calculated field is foreign keyreally really wish there was a way to say “if foreign key object is deleted, just null this field”

Page 34: EuroDjangoCon Django Patterns

markup fields

class TownHallUpdate(models.Model): content = models.TextField() content_html = models.TextField(editable=False)

def save(self, **kwargs): self.content_html = textile.textile(sanitize_html(self.content)) super(TownHallUpdate, self).save(**kwargs)

Page 35: EuroDjangoCon Django Patterns

one active at a timeclass TownHallUpdate(models.Model):

active = models.BooleanField(default=False, help_text="There can only be one active update. Marking the checkbox will mark any other update as inactive.")

def save(self, **kwargs): if self.active: # check for another active one and mark it inactive try: update = TownHallUpdate.objects.get(active=True) except TownHallUpdate.DoesNotExist: pass else: update.active = False update.save() super(TownHallUpdate, self).save(**kwargs)

later in view:

townhall_update = TownHallUpdate.objects.get(active=True)

Page 36: EuroDjangoCon Django Patterns

Query Patterns

Page 37: EuroDjangoCon Django Patterns

within date range

.filter( Q(start_date__lt=now) | Q(start_date=None), Q(end_date__gt=now) | Q(end_date=None),)

Page 38: EuroDjangoCon Django Patterns

ordering by nullables

top_blogs = Blog.objects.filter(overall_rating__isnull=False) .order_by("-overall_rating")[:3]

Page 39: EuroDjangoCon Django Patterns

View Patterns

Page 40: EuroDjangoCon Django Patterns

standard object retrieval

def community_blog(request, blog_id): blog = get_object_or_404(Blog, id=blog_id)

Page 41: EuroDjangoCon Django Patterns

standard render

return render_to_response("app/bar.html", { "foo": foo, }, context_instance=RequestContext(request))

Page 42: EuroDjangoCon Django Patterns

most views

def community_blog(request, blog_id): blog = get_object_or_404(Blog, id=blog_id) ...extra queries... return render_to_response("app/bar.html", { "blog": blog, ... }, context_instance=RequestContext(request))

Page 43: EuroDjangoCon Django Patterns

passing in template name and form class

def create(request, form_class=TribeForm, template_name="tribes/create.html"):

Page 44: EuroDjangoCon Django Patterns

building upcontext dictionary

ctx = {}

... if form.had_valid_code: ctx["valid_code"] = True ctx["form"] = form else: ctx["valid_code"] = False

... if beta_code: valid_code = True ctx["form"] = BetaSignupForm(initial={"beta_code": code}) else: valid_code = False ctx["valid_code"] = valid_code

return render_to_response("main/beta_signup.html", ctx, context_instance=RequestContext(request))

Page 45: EuroDjangoCon Django Patterns

submit to different URL then redirect back

@login_required@require_POSTdef comment_submit(request, article_id): ... handle post ... return HttpResponseRedirect( reverse(article_page, args=[article_id]))

Page 46: EuroDjangoCon Django Patterns

lots of form submitsif request.POST["action"] == "add comment": ...elif request.POST["action"] == "delete comment": ...elif request.POST["action"] == "rename node": ...elif request.POST["action"] == "new node": ...elif request.POST["action"] == "detach node": ...elif request.POST["action"] == "put node": ...elif request.POST["action"] == "add attribute": ...elif request.POST["action"] == "delete attribute": ...elif request.POST["action"] == "promote attribute": ...elif request.POST["action"] == "demote attribute": ...

Page 47: EuroDjangoCon Django Patterns

handle nextdef handle_next(request, default=None): if default is None: default = reverse("home") redirect_to = request.REQUEST.get("next", default) if "://" in redirect_to: redirect_to = default return redirect_to @login_requireddef accept_friendship(request, friend_request_id): redirect_to = handle_next(request) FriendshipRequest.objects.get(pk=friend_request_id) .accept() return HttpResponseRedirect(redirect_to)

Page 48: EuroDjangoCon Django Patterns

poor man’s searchquery = request.GET.get("query", "")if query: search_results = Member.objects.filter( Q(username__icontains=query) | Q(first_name__icontains=query) | Q(last_name__icontains=query) | Q(user__email__icontains=query) )else: search_results = Member.objects.none()

# note "" is used as query default as it is passed through to template for form

Page 49: EuroDjangoCon Django Patterns

conditional adding of filters to a chain

games = Game.objects.filter(platform__in=PLATFORMS)

query = request.GET.get("query", "")letter = request.GET.get("letter", None)

if query: filtered_games = games.filter( Q(title__icontains=query) | Q(description__icontains=query) )else: filtered_games = games

if letter: filtered_games = filtered_games.filter(title__istartswith=letter)

Page 50: EuroDjangoCon Django Patterns

can encapsulate that in a manager

class AnnouncementManager(models.Manager):  def current(self, exclude=[], site_wide=False, for_members=False):    queryset = self.all()    if site_wide:      queryset = queryset.filter(site_wide=True)    if exclude:      queryset = queryset.exclude(pk__in=exclude)    if not for_members:      queryset = queryset.filter(members_only=False)    queryset = queryset.order_by("-creation_date")    return queryset 

Page 51: EuroDjangoCon Django Patterns

get first (1)

top_reviews = game.user_reviews.order_by("-timestamp")

if top_reviews: top_review = top_reviews[0]else: top_review = None

Page 52: EuroDjangoCon Django Patterns

get first (2)

top_reviews = game.user_reviews.order_by("-timestamp")[:1]

# rely on template to do .0

Page 53: EuroDjangoCon Django Patterns

Form Patterns

Page 54: EuroDjangoCon Django Patterns

form handling (1)

if request.method == "POST": form = form_class(request.POST) if form.is_valid(): ...else: form = form_class()

Page 55: EuroDjangoCon Django Patterns

form handling (2)

form = form_class(request.POST or None)

if tribe_form.is_valid(): ...

# relies on fact request.POST is False if # request.method != "POST"# AND that is_valid fails if form is unbound

Page 56: EuroDjangoCon Django Patterns

“already taken” patterndef clean_username(self): try: user = User.objects.get( username__iexact = self.cleaned_data["username"]) except User.DoesNotExist: return self.cleaned_data["username"] raise forms.ValidationError("This username is already taken. Please choose another.")

# note iexact!

Page 57: EuroDjangoCon Django Patterns

the user formclass UserForm(forms.Form):

def __init__(self, *args, **kwargs): self.user = kwargs.pop("user") super(UserForm, self).__init__(*args, **kwargs)

# can be used for any additional info

# can also pass in to save() but then # can’t use in validation

Page 58: EuroDjangoCon Django Patterns

order fieldsclass UserPrivacySettingForm(forms.ModelForm): class Meta: model = UserPrivacySetting fields = ( "public", "members", "groups", "friends")

def __init__(self, *args, **kwargs): super(UserPrivacySettingForm, self) .__init__(*args, **kwargs) self.fields.keyOrder = self._meta.fields

# in 1.1 fields ordering is now significant

Page 59: EuroDjangoCon Django Patterns

overriding a field typeclass ComposeMessageForm(forms.ModelForm): class Meta: model = Message fields = ("to_user", "subject", "body") def __init__(self, member, *args, **kwargs): super(ComposeMessageForm, self) .__init__(*args, **kwargs) self.fields["to_user"] = FriendChoiceField(member, label=u"Recipient")

Page 60: EuroDjangoCon Django Patterns

URL Patterns

Page 61: EuroDjangoCon Django Patterns

serving mediaif settings.SERVE_MEDIA: urlpatterns += patterns('', (r'^site_media/(?P<path>.*)$', 'django.views.static.serve', {'document_root': settings.MEDIA_ROOT} ), )

# personally I prefer to separate SERVE_MEDIA # from DEBUG although former can default to # latter

Page 62: EuroDjangoCon Django Patterns

per-app URLs

url(r'^community/', include('community.urls')),

Page 63: EuroDjangoCon Django Patterns

named urls

url(r'^article/(\d+)/$', ..., name="article")

{% url article article_id %}

# use of {% url %} keeps you DRY

# use of named-url makes it easier to# drop in replacement apps

Page 64: EuroDjangoCon Django Patterns

URL space

^article/$ or ^articles/$^article/(\d+)/$ or ^article/(\w+)/$^article/(\d+)/comment/$^article/add/$...

Page 65: EuroDjangoCon Django Patterns

Template Patterns

Page 66: EuroDjangoCon Django Patterns

using app name as template folder name

myapp/ templates/ myapp/ myapp_template.html

template_name = "myapp/myapp_template.html"

Page 67: EuroDjangoCon Django Patterns

sections of a site have own base template

templates/ base.html section1/ base.html some_page.html

Page 68: EuroDjangoCon Django Patterns

types of templates

top-level

extensions

includes

Page 69: EuroDjangoCon Django Patterns

lists that maybe empty

{% if results %} <ul> {% for result in results %} <li>...</li> {% endfor %} </ul>{% else %} <p>No results.</p>{% endif %}

# in 1.1 {% empty %} can be used for# non-wrapped cases

Page 70: EuroDjangoCon Django Patterns

provide an override AND extension point

{% block extra_body_base %} {% urchin %} <script src="{{ MEDIA_URL }}base.js" type="text/javascript"></script> {% block extra_body %}{% endblock %}{% endblock %}

# bottom-level template doesn't have to # remember block.super

Page 71: EuroDjangoCon Django Patterns

Template Tag Patterns

Page 72: EuroDjangoCon Django Patterns

types of template tags

generate content

modify context

Page 73: EuroDjangoCon Django Patterns

basic inclusion tag

@register.inclusion_tag("includes/media_item.html")def media_item(media_item): return { "media_item": media_item, }

# could also just use {% include ... %} and rely on # context (possibly with 'with')

Page 74: EuroDjangoCon Django Patterns

slip in MEDIA_URL

@register.inclusion_tag("community/inclusion_tags/member_item.html")def member_item(member): return { "member": member, "MEDIA_URL": settings.MEDIA_URL, }

Page 75: EuroDjangoCon Django Patterns

out-of-band calculations on objects

@register.simple_tagdef default_avatar(member): if member.gender == "guy": filename = "avatar/guy_default.jpg" else: filename = "avatar/girl_default.jpg" return filename

Page 76: EuroDjangoCon Django Patterns

the context, the whole context and nothing but the context

@register.inclusion_tag("includes/nav.html", takes_context=True)def nav(context): return context

# is this any different than {% include ... %} ?

Page 77: EuroDjangoCon Django Patterns

context processors

inbox counts

other values calculated off current user that are in header or footer

especially if request object is needed

don’t have to load and call template tag

Page 78: EuroDjangoCon Django Patterns

Settings Patterns

Page 79: EuroDjangoCon Django Patterns

make relative file paths absolute

from os.path import join, dirname

MEDIA_ROOT = join(dirname(__file__), "site_media")

TEMPLATE_DIRS = ( join(dirname(__file__), "templates"),)

Page 80: EuroDjangoCon Django Patterns

environment-specific settings (1)

try: from local_settings import *except ImportError: pass

Page 81: EuroDjangoCon Django Patterns

environment-specific settings (2)

have all per-environment settings files checked-in to version control with symlink from settings.py

Page 82: EuroDjangoCon Django Patterns

Meta Patterns

Page 83: EuroDjangoCon Django Patterns

Endo Approach(framework-like)

Page 84: EuroDjangoCon Django Patterns

Exo Approach(library-like)

Page 85: EuroDjangoCon Django Patterns

Parting Thoughts

keep the conversation going during conference

going to put these (and more) up on eldarion.com

pattern library will grow over time