Forms—Comparing Django to Rails

??? words · ??? min read

This article is a short study in web application design. We will be comparing Rails to Django, using a simple web form as an example.

Rails

Let’s begin with some typical Rails code.

# A model, with a custom validation
class Post < ApplicationRecord
  validate :body_includes_title

  def body_includes_title
    unless body.include?(title)
      errors.add(:base, 'Body must contain title')
    end
  end
end

# A controller for rendering a form, and handling its submission
class PostsController < ApplicationController
  before_action :set_post

  def edit
  end

  def update
    if @post.update(post_params)
      redirect_to @post
    else
      render :edit
    end
  end

  private

    def set_post
      @post = Post.find(params[:id])
    end

    def post_params
      require(:post).permit(:title, :body)
    end
end
<%# A view that renders the form, displaying errors per-field %>
<%= form_for(@post) do |f| %>
  <%= @post.errors[:base].full_messages %>
  <%= f.text_field :title %> <%= @post.errors[:title].full_messages %>
  <%= f.text_area :body %> <%= @post.errors[:body].full_messages %>
<% end %>

Django

Here is the same functionality, translated into Django.

Disclaimer: I am not a Django developer, so take this implementation with a grain of salt.

from django.db import models
from django import forms
from django.shortcuts import render, redirect

# The model
class Post(models.Model):
    title = models.CharField()
    body = models.CharField()

# The form, with a custom validation
class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = ['title', 'body']

    def clean(self):
        cleaned_data = super().clean()
        body = cleaned_data.get("body")
        title = cleaned_data.get("tags")
        if not title in body:
            self.add_error(None, "Body must contain title")

# The request handler (equivalent of a controller)
def update_post(request, post_id):
    post = get_object_or_404(Post, pk=post_id)
    if request.method == 'GET'
        form = PostForm(instance=post)
    else
        form = PostForm(request.POST, instance=post)
        if form.is_valid():
            post = form.save()
            return redirect(post)

    return render(request, 'update_post.html', {'form': form})
<!-- The view that renders the form -->
<form action="/posts/{{ form.id }}" action="POST">
  {{ form }}
</form>
<!-- A different way to write the view, with more control over rendered HTML -->
<form action="/posts/{{ form.id }}" action="POST">
  {{ form.non_field_errors }}
  <input name="title" value="{{ form.title }}" /> {{ form.title.errors }}
  <textarea name="body">{{ form.body }}</textarea> {{ form.body.errors }}
</form>

Rubified Django

Before we dive into the comparison, let’s translate the Django code into what it might look like if it were implemented in Ruby.

# The model
class Post < Django::Models::Model
  attr :title, Django::Model::StringField.new
  attr :body, Django::Model::StringField.new
end

# The form
class PostForm < Django::Forms::ModelForm
  model Post
  fields [:title, :body]

  def clean
    attrs = super
    unless attrs[:body].include?(attrs[:title])
      add_error(nil, "Body must contain title")
    end
  end
end

# The controller
module PostsController
  extend Django::Shortcuts # provides `render` and `redirect_to` methods

  def self.update(request, post_id)
    post = Post.find(post_id)
    if request.method.get?
      form = PostForm.new(instance: post)
    else
      form = PostForm.new(request.params, instance: post)
      if form.valid?
        post = form.save
        return redirect_to(post)
      end
    end

    return render(request, 'posts/edit.html', form: form)
  end
end
<%# The view %>
<%= form_for(@form) do %>
  <%= @form %>
<% end %>
<%# A different way to write the view, with more control over rendered HTML %>
<%= form_for(@form) do %>
  <%= @form.non_field_errors %>
  <input name="title" value="<%= @form.title %>" /> <%= @form.title.errors %>
  <textarea name="body"><%= @form.body %></textarea> <%= @form.body.errors %>
<% end %>

Observations

Python tends to use namespaced constants

Ruby doesn’t really have a module system. The require method is basically just eval, running the contents of a file within the global namespace. All Ruby files have access to all other Ruby files that have ever been required previously. This means we don’t write a lot of requires at the top of each file, but all code can affect, and be affected by, all other code.

Python has a more sophisticated module system. Each file is a module, and must explicitly declare which other modules it wants access to. This means that each file has a bunch of imports at the top, but can’t accidentally interact with all other code in the application.

Django models contain attribute definitions

Rails model classes load their attributes from the database at runtime. The benefit of this approach is that the model attributes always reflect the columns of their database table. The downside is that we can not programmatically access the attributes of a model without a database connection.

Django has built-in form objects

Rails makes no distinction between models and forms, at least in the controller. Model objects are used to render forms, and controllers pass user input directly into model objects for validation.

Many Rails projects make use of form objects, but Rails does not provide them out of the box. ActiveModel::Model can be used to make form objects fairly easily, but still, forms are not a first-class citizen.

Django comes with many different kinds of form objects. There a generic Form class, which can coerce and validate HTTP params without being tied to any model. Then there are various different ModelForm classes, which act upon model objects.

Django forms can take fields from models

A common complaint about Rails form objects is that they duplicate validation logic that already exists in models.

Django forms take their fields and validations from models objects, limiting duplication. They are not limited to the fields in the model, though. They can take a subset of the model fields, add extra fields of their own, and replace model fields with different ones.

Django controllers are functions

In Rails, request handlers are implemented as methods (actions) on controller objects. The HTTP request and response are part of the mutable internal state of each controller object. This design means that action methods take no parameters, and have no meaningful return value.

In Django, request handlers are simple functions. The HTTP request is passed in as an argument, and the return value is the HTTP response. There is no need for the handler to be a method on a class, or to inherit from anything. This is a functional design, much like Rack.

Django does not route based on HTTP method

GET and POST requests to the same URL are usually routed to separate controller actions in Rails. For forms, both actions usually need to load the model with something like @my_model = MyModel.find(params[:id]). This duplication is often removed with a before_action.

Django sends GET and POST requests to a single request handling function. The function then looks at the HTTP method on the request object, and acts appropriately. This removes the need for something like before_action, which Django’s design would not be able to accommodate easily, anyway.

Roda removes this duplication in a similar way, with its nested routing:

r.on "blog_post", Integer do |id|
  blog_post = BlogPost.find(id)

  r.get do
    # render the form
  end

  r.post do
    # handle the submitted form
  end
end

Django forms render themselves

Django form objects can render themselves into HTML. The form’s field objects have various different options which control how they are rendered.

Rails takes a more explicit approach, using FormBuilders to generate HTML based on model objects. Automatic form rendering wouldn’t be feasible without control over which fields to render, at which point you basically have form objects.

Django forms contain business logic

Django ModelForm objects perform their task by calling the save method, which returns the updated model object.

With the ability to render themselves, this makes Django forms a strange blend of model, view and business logic responsibilities. Put another way: they are complicated.

Form objects are typically implemented this way in Rails, too, but they usually don’t render themselves.

Django form errors are on the fields

Django renders values and errors using field objects, something like this:

{{ post.title }} {{ post.title.errors }}

In contrast, each Rails model has a ActiveModel::Errors object, used something like this:

<%= @post.title %> <%= @post.errors[:title] %>

For errors that don’t belong to a single field, Django uses a method on the form:

{{ form.non_field_errors }}

Whereas Rails uses the :base error key as a sentinel value:

<%= @post.errors[:base] %>

Strong params are not necessary

Passing unfiltered params into model objects results in mass assignment vulnerabilities. Rails was embarrassingly burnt by these vulnerabilities in the past, resulting in the strong parameters feature.

Django form objects explicitly declare all the fields that they accept. Any extra fields present in the request params are ignored, preventing mass assignment.

Django view arguments are explicit

Rails passes view arguments by magically copying instance variables from the controller. It is possible to pass view arguments explicitly in Rails with render locals: {...}, but controller instance variables are the preferred choice. This leads to a difference between normal views using instance variables, and partials which require locals.

Django passes view arguments explicitly. There is no distinction between normal views and partials.

Django also has mutable errors

Both Rails and Django have designed their validation with a mutable set of errors. Models and forms start with an empty error set, then errors may be added to the set during validation, and afterwards the object is valid if the error set is still empty.

Got questions? Comments? Milk?

Shoot an email to [email protected] or hit me up on Twitter (@tom_dalling).

← Previously: Dependency Injection Containers vs Hard-coded Constants

Join The Pigeonhole

Don't miss the next post! Subscribe to Ruby Pigeon mailing list and get the next post sent straight to your inbox.