EuroDjangoCon Django Patterns
-
Upload
danny-roa -
Category
Technology
-
view
3.066 -
download
1
description
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 [email protected]://jtauber.com/twitter: @jtauber
Brian [email protected]://oebfare.com/twitter: @brosner