EuroDjangoCon Django Patterns

Post on 15-Jan-2015

3.066 views 1 download

Tags:

description

by James Tauber and Bryan Rosner

Transcript of EuroDjangoCon Django Patterns

Django PatternsJames Tauber

(and Brian Rosner)

Django Patterns

this is the start not the end of the matter

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

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

Exampleenvironment-specific settings

try: from local_settings import *except ImportError: pass

ExampleChange Log Model

class LogMessage(models.Model):

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

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”

Some patterns can be turned in to constructs

generic views

render_to_response

new redirect in Django 1.1

view decorators

Django IS Python

I hope to give...

beginning Djangonauts something to try out

intermediate Djangonauts something to think about

advanced Djangonauts something to laugh at

Concepts

views

urlconfs

models

templates

forms

template tags

middleware

context processors

management commands

Model Patterns

The “Atom” Model

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

Common Fields

slug

title

description

creator

creation_timestampsometimes just “user” and

“timestamp” when an event

“pseudo foreign key”

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

entity attribute values

class GameInformation(models.Model):

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

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()]

zip choices

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

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")

Basic “Change Log”

class LogMessage(models.Model):

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

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")

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'), )

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

Trees

class ForumCategory(models.Model):

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

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")

lattice

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

Denormalization and Calculated Field

Patterns

usernameas well as user

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

view counts

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

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

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)

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")

denormalizedforeign key

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

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”

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)

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)

Query Patterns

within date range

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

ordering by nullables

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

View Patterns

standard object retrieval

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

standard render

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

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))

passing in template name and form class

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

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))

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]))

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": ...

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)

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

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)

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 

get first (1)

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

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

get first (2)

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

# rely on template to do .0

Form Patterns

form handling (1)

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

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

“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!

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

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

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")

URL 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

per-app URLs

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

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

URL space

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

Template Patterns

using app name as template folder name

myapp/ templates/ myapp/ myapp_template.html

template_name = "myapp/myapp_template.html"

sections of a site have own base template

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

types of templates

top-level

extensions

includes

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

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

Template Tag Patterns

types of template tags

generate content

modify context

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')

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, }

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

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 ... %} ?

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

Settings 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"),)

environment-specific settings (1)

try: from local_settings import *except ImportError: pass

environment-specific settings (2)

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

Meta Patterns

Endo Approach(framework-like)

Exo Approach(library-like)

Parting Thoughts

keep the conversation going during conference

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

pattern library will grow over time

James Tauberjtauber@jtauber.comhttp://jtauber.com/twitter: @jtauber

Brian Rosnerbrosner@gmail.comhttp://oebfare.com/twitter: @brosner