• Share
  • Email
  • Embed
  • Like
  • Save
  • Private Content
EuroDjangoCon Django Patterns

EuroDjangoCon Django Patterns



by James Tauber and Bryan Rosner

by James Tauber and Bryan Rosner



Total Views
Views on SlideShare
Embed Views



4 Embeds 59

http://www.slideshare.net 21
http://lanyrd.com 17 12
http://kmolcmsimply.tk 9



Upload Details

Uploaded via as Adobe PDF

Usage Rights

© All Rights Reserved

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.


12 of 2 previous next

  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
  • The font is Anivers which is also used in the Eldarion logo.
    Are you sure you want to
    Your message goes here
  • Hey - great font. May I ask what it is?

    (Nice presentation too :-) very helpful)
    Are you sure you want to
    Your message goes here
Post Comment
Edit your comment

    EuroDjangoCon Django Patterns EuroDjangoCon Django Patterns Presentation Transcript

    • Django Patterns James 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
    • Example environment-specific settings try: from local_settings import * except ImportError: pass
    • Example Change Log Model class LogMessage(models.Model): thing = models.ForeignKey(Thing) user = models.ForeignKey(User) timestamp = models.DateTimeField( default=datetime.now) message = models.TextField()
    • Example Change Log Model not a reusable model, more “parameterized snippet” class LogMessage(models.Model): thing = models.ForeignKey(Thing) user = models.ForeignKey(User) timestamp = models.DateTimeField( default=datetime.now) message = models.TextField()
    • 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 template tags urlconfs middleware models context processors templates management commands forms
    • 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 sometimes just “user” and creation_timestamp “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 values class 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[quot;valuequot;] 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-level object creation helper GAME_POINTS = { quot;RATED_GAMEquot;: 1, ... } def earn_game_points(user, reason): points = GAME_POINTS[reason] GamePointAward(user=user, points=points, reason=reason).save() ... earn_game_points(user, quot;RATED_GAMEquot;)
    • 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 models without ManyToManyField class WishListItem(models.Model): user = models.ForeignKey(User, related_name=quot;wishlistquot;) 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(quot;gamequot;)
    • symmetrical associations “friendships” class Friendship(models.Model): to_user = models.ForeignKey(User, related_name=quot;_unused1quot;) from_user = models.ForeignKey(User, related_name=quot;_unused2_quot;) 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({quot;friendquot;: friendship.to_user, quot;friendshipquot;: friendship}) for friendship in self.filter(to_user=user) .select_related(depth=1): friends.append({quot;friendquot;: friendship.from_user, quot;friendshipquot;: 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=quot;subcategoriesquot;)
    • multi-strata trees class 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=quot;subforumsquot;) category = models.ForeignKey(ForumCategory, null=True, blank=True, related_name=quot;forumsquot;)
    • lattice class Node(models.Model): children = models.ManyToManyField('self', symmetrical=False, related_name='parents', blank=True)
    • Denormalization and Calculated Field Patterns
    • username as 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=quot;commentsquot;) ... 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=quot;ratingsquot;) user = models.ForeignKey(Member) rating = models.IntegerField(default=0) timestamp = models.DateTimeField(default=datetime.now) class Meta: unique_together = (quot;media_itemquot;, quot;userquot;)
    • denormalized foreign key class Forum(models.Model): ... last_reply = models.ForeignKey(quot;ForumReplyquot;, null=True, editable=False)
    • Advice on Calculated 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 key really 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 time class TownHallUpdate(models.Model): active = models.BooleanField(default=False, help_text=quot;There can only be one active update. Marking the checkbox will mark any other update as inactive.quot;) 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(quot;-overall_ratingquot;)[: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(quot;app/bar.htmlquot;, { quot;fooquot;: 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(quot;app/bar.htmlquot;, { quot;blogquot;: blog, ... }, context_instance=RequestContext(request))
    • passing in template name and form class def create(request, form_class=TribeForm, template_name=quot;tribes/create.htmlquot;):
    • building up context dictionary ctx = {} ... if form.had_valid_code: ctx[quot;valid_codequot;] = True ctx[quot;formquot;] = form else: ctx[quot;valid_codequot;] = False ... if beta_code: valid_code = True ctx[quot;formquot;] = BetaSignupForm(initial={quot;beta_codequot;: code}) else: valid_code = False ctx[quot;valid_codequot;] = valid_code return render_to_response(quot;main/beta_signup.htmlquot;, ctx, context_instance=RequestContext(request))
    • submit to different URL then redirect back @login_required @require_POST def comment_submit(request, article_id): ... handle post ... return HttpResponseRedirect( reverse(article_page, args=[article_id]))
    • lots of form submits if request.POST[quot;actionquot;] == quot;add commentquot;: ... elif request.POST[quot;actionquot;] == quot;delete commentquot;: ... elif request.POST[quot;actionquot;] == quot;rename nodequot;: ... elif request.POST[quot;actionquot;] == quot;new nodequot;: ... elif request.POST[quot;actionquot;] == quot;detach nodequot;: ... elif request.POST[quot;actionquot;] == quot;put nodequot;: ... elif request.POST[quot;actionquot;] == quot;add attributequot;: ... elif request.POST[quot;actionquot;] == quot;delete attributequot;: ... elif request.POST[quot;actionquot;] == quot;promote attributequot;: ... elif request.POST[quot;actionquot;] == quot;demote attributequot;: ...
    • handle next def handle_next(request, default=None): if default is None: default = reverse(quot;homequot;) redirect_to = request.REQUEST.get(quot;nextquot;, default) if quot;://quot; in redirect_to: redirect_to = default return redirect_to @login_required def 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 search query = request.GET.get(quot;queryquot;, quot;quot;) 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 quot;quot; 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(quot;queryquot;, quot;quot;) letter = request.GET.get(quot;letterquot;, 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(quot;-creation_datequot;)     return queryset  
    • get first (1) top_reviews = game.user_reviews.order_by(quot;-timestampquot;) if top_reviews: top_review = top_reviews[0] else: top_review = None
    • get first (2) top_reviews = game.user_reviews.order_by(quot;-timestampquot;)[:1] # rely on template to do .0
    • Form Patterns
    • form handling (1) if request.method == quot;POSTquot;: 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 != quot;POSTquot; # AND that is_valid fails if form is unbound
    • “already taken” pattern def clean_username(self): try: user = User.objects.get( username__iexact = self.cleaned_data[quot;usernamequot;]) except User.DoesNotExist: return self.cleaned_data[quot;usernamequot;] raise forms.ValidationError(quot;This username is already taken. Please choose another.quot;) # note iexact!
    • the user form class UserForm(forms.Form): def __init__(self, *args, **kwargs): self.user = kwargs.pop(quot;userquot;) 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 fields class UserPrivacySettingForm(forms.ModelForm): class Meta: model = UserPrivacySetting fields = ( quot;publicquot;, quot;membersquot;, quot;groupsquot;, quot;friendsquot;) 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 type class ComposeMessageForm(forms.ModelForm): class Meta: model = Message fields = (quot;to_userquot;, quot;subjectquot;, quot;bodyquot;) def __init__(self, member, *args, **kwargs): super(ComposeMessageForm, self) .__init__(*args, **kwargs) self.fields[quot;to_userquot;] = FriendChoiceField(member, label=uquot;Recipientquot;)
    • URL Patterns
    • serving media if 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=quot;articlequot;) {% 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 = quot;myapp/myapp_template.htmlquot;
    • 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 may be 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=quot;{{ MEDIA_URL }}base.jsquot; type=quot;text/javascriptquot;></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(quot;includes/media_item.htmlquot;) def media_item(media_item): return { quot;media_itemquot;: media_item, } # could also just use {% include ... %} and rely on # context (possibly with 'with')
    • slip in MEDIA_URL @register.inclusion_tag(quot;community/ inclusion_tags/member_item.htmlquot;) def member_item(member): return { quot;memberquot;: member, quot;MEDIA_URLquot;: settings.MEDIA_URL, }
    • out-of-band calculations on objects @register.simple_tag def default_avatar(member): if member.gender == quot;guyquot;: filename = quot;avatar/guy_default.jpgquot; else: filename = quot;avatar/girl_default.jpgquot; return filename
    • the context, the whole context and nothing but the context @register.inclusion_tag(quot;includes/ nav.htmlquot;, 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__), quot;site_mediaquot;) TEMPLATE_DIRS = ( join(dirname(__file__), quot;templatesquot;), )
    • 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 Tauber jtauber@jtauber.com http://jtauber.com/ twitter: @jtauber Brian Rosner brosner@gmail.com http://oebfare.com/ twitter: @brosner