Forms—Comparing Django to Rails
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 require
d previously.
This means we don’t write a lot of require
s 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 import
s 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 FormBuilder
s 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).