Recently, I have rediscovered Ruby on Rails. In my opinion, it is still the best framework if you want to shell out a simple, CRUD based web application in a short time. Beyond that, is of course capable of much more but this is what it excels at. You don’t need to worry too much about basic security features, e.g. cross-site scripting, and you get fully functional web pages for your models out of the box.
The whole train of thought came up because I am not really good at routine tasks. At the same time, meetings with people to work on certain topics are a large part of my work.
One of the problems with meetings is that, in the best case, everybody comes out of the meeting with a good feeling of understanding and a clear picture of what needs to be done. But then, thirty minutes later, if you ask two people independently what was decided, they will already tell you different stories. That is just how the human brain works. We build models in our brain and they are not the same for every person. When we test the models of two different people with questions, then they will result in different answers. This effect is well researched and every police officer interrogating witnesses will confirm it.
But meetings don’t always go that well. I have experienced meetings where not everybody was sure about the purpose of the meeting, the authority of the group meeting for the purpose, or the roles of the various participants. These meetings tend to feel a bit forced as the agenda (if there is an agenda) is worked through and there is a lot of silence in between, which is usually taken as agreement with whatever is being presented.
And then there is the worst scenario, where there are tensions of various kinds create conflict that is hard to resolve. Often, the actual tensions are not explored, the reasons not understood, and so it is difficult to address them. I’m not saying that understanding the reasons will make the tensions magically go away–but not understanding them will certainly not help.
There is a book out there (there is always a book), that gives a formal framework for structuring an organization into circles and providing guidance like templates and rule books. It is called “Holacracy” after the method the authors are proposing. I disagree with the name because there is no “a” in Greek ὅλος, hólos “whole”. But that is a discussion for another day. Just note that I use a spelling I personally find more appropriate, but if you want to buy the book or visit the web site, mind the different spelling.
Holocracy proposes to structure an organization into a hierarchical structure of “circles”, i.e. groups of people, and a “constitution” that they follow and that provides basic information like the purpose of the group, its authority, the roles in the group, and its inner workings and interaction with other groups. It specifically addresses the problems I have mentioned in the previous section: The need for creating agendas and keeping minutes, the need to define purpose, the need to address tensions. For me as a programmer, it is quite appealing because Holocracy lends itself well to implementation in a tool. It is almost like an algorithm for organizations. (And that may well be its greatest flaw, but that is another story that will be told another time.)
The Holocracy advocates claim of course that it is not the tool that makes the method successful but really it is a change of culture and a different mindset. And yes, that is most likely true. As the saying goes: A fool with a tool is still a fool, just a more dangerous one. But I am a programmer and I am bad at routine jobs.
There are already implementations of Holocracy tools out there but in the end, it is not that hard. A simple web application can capture the mechanical aspects and leave your head free to do the mind shift. And what programmer would I be if I did not write my own implementation of it?
And this is where Ruby on Rails enters the picture. Defining a relatively simple domain model (circles, people, agendas, decisions, etc.) with some simple algorithms operating on them, that is something that can very easily done with Rails. For now, I will not at all care about styling the web pages and focus on functionality. Chances are that the front-end will be replaced by some neat browser-side code anyway. It is more important to get the basic structure in place.
Implementing the tool will be too much for a single article, so this might turn into another series of Saturday projects. To give you a flavor though, let’s start by creating an implementation for Circles, People, and the Roles. I am assuming that Rails is already installed, which I suggest you do in a *nix environment. If you are on Windows, the Windows Subsystem for Linux will do nicely, even though it can be a bit slow. I start, as in so many other frameworks, by creating a new Rails application in a convenient directory.
$ rails new holo create create README.md create Rakefile create .ruby-version create config.ru create .gitignore create Gemfile run git init from "." ... ├─ firstname.lastname@example.org ├─ email@example.com ├─ firstname.lastname@example.org ├─ email@example.com ├─ firstname.lastname@example.org ├─ email@example.com └─ firstname.lastname@example.org Done in 73.22s. Webpacker successfully installed 🎉 🍰 $ cd holo
Creating the scaffolds
After grinding away for some time, a subdirectory for the app is created and populated with quite a massive amount of stuff. (These are technical terms.) I change into the new subdirectory and create scaffolds for my three entities:
$ bin/rails generate scaffold Person name:string email:string Running via Spring preloader in process 357 invoke active_record create db/migrate/20210108141507_create_people.rb create app/models/person.rb invoke test_unit ... create app/assets/stylesheets/people.scss invoke scss create app/assets/stylesheets/scaffolds.scss
A scaffold is a database table, a model class in rails, a controller class, a set of views for the model, and a set of test classes for all of those things. Also, the routes (or URLs) through which these things can be accessed are automatically created, as well as a full REST API for the common operations. Neat? Let me do the other two.
$ bin/rails generate scaffold Circle name:string purpose:text Running via Spring preloader in process 391 invoke active_record create db/migrate/20210108141830_create_circles.rb create app/models/circle.rb ...
$ bin/rails generate scaffold Role name:string person:references circle:references Running via Spring preloader in process 426 invoke active_record create db/migrate/20210108141933_create_roles.rb ...
Note how I simply specified that a role will reference a person and a circle. Rails will pick that up and do the right thing based on matching the names of things.
Creating the database schema
Now the code is in place. Next, I will look at the database schema creation, which is done in code, as it should be. For this, Rails creates migration classes. These can be adjusted as necessary and then run against a development database, which is of course already set up by the framework. Why would I need to do that by hand, after all?
This is the automatically created Person schema migration:
class CreatePeople < ActiveRecord::Migration[6.0] def change create_table :people do |t| t.string :name t.string :email t.timestamps end end end
It creates a table with two string columns. Ignore the
timestamps for now; this is part of the optimistic transaction strategy that Rails employs. But we might want to ensure that both name and email are given for every person, and that the email is unique as it identifies the user. I will add this to the migration before I execute it.
class CreatePeople < ActiveRecord::Migration[6.0] def change create_table :people do |t| t.string :name, null: false t.string :email, null: false t.index :email, unique: true t.timestamps end end end
Pretty self-explanatory. I will also have a look at the Role schema migration. Here is the generated code:
class CreateRoles < ActiveRecord::Migration[6.0] def change create_table :roles do |t| t.string :name t.references :person, null: false, foreign_key: true t.references :circle, null: false, foreign_key: true t.timestamps end end end
This looks pretty much perfect already. I will just add another non-null constraint and a default value for the name.
class CreateRoles < ActiveRecord::Migration[6.0] def change create_table :roles do |t| t.string :name, null: false, default: "Unnamed Circle" t.references :person, null: false, foreign_key: true t.references :circle, null: false, foreign_key: true t.timestamps end end end
Now, I am all set to run the migrations:
$ bin/rails db:migrate == 20210108141507 CreatePeople: migrating ===================================== -- create_table(:people) -> 0.0158s == 20210108141507 CreatePeople: migrated (0.0159s) ============================ == 20210108141830 CreateCircles: migrating ==================================== -- create_table(:circles) -> 0.0115s == 20210108141830 CreateCircles: migrated (0.0117s) =========================== == 20210108141933 CreateRoles: migrating ====================================== -- create_table(:roles) -> 0.0239s == 20210108141933 CreateRoles: migrated (0.0241s) =============================
All set. For fun, let’s have a look at the actual database. For development, I am using SQLite3, which of course will be replaced by something more robust like PostgreSQL or MariaDB for production. Fortunately, Rails abstracts away all the idiosyncrasies of the underlying databases unless I specifically want to use them.
$ bin/rails dbconsole SQLite version 3.31.1 2020-01-27 19:55:54 Enter ".help" for usage hints. sqlite> .tables ar_internal_metadata people schema_migrations circles roles sqlite> .schema people CREATE TABLE IF NOT EXISTS "people" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar NOT NULL, "email" varchar NOT NULL, "created_at" datetime(6) NOT NULL, "updated_at" datetime(6) NOT NULL); CREATE UNIQUE INDEX "index_people_on_email" ON "people" ("email"); sqlite> .schema roles CREATE TABLE IF NOT EXISTS "roles" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar DEFAULT 'Unnamed Circle' NOT NULL, "person_id" integer NOT NULL, "circle_id" integer NOT NULL, "created_at" datetime(6) NOT NULL, "updated_at" datetime(6) NOT NULL, CONSTRAINT "fk_rails_7a19f4fbb4" FOREIGN KEY ("person_id") REFERENCES "people" ("id") , CONSTRAINT "fk_rails_3cae5e4d90" FOREIGN KEY ("circle_id") REFERENCES "circles" ("id") ); CREATE INDEX "index_roles_on_person_id" ON "roles" ("person_id"); CREATE INDEX "index_roles_on_circle_id" ON "roles" ("circle_id"); sqlite> .schema circles CREATE TABLE IF NOT EXISTS "circles" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar, "purpose" text, "created_at" datetime(6) NOT NULL, "updated_at" datetime(6) NOT NULL);
If you are SQL inclined, you can wade through this and confirm that Rails actually did the right thing. Obviously, I have left away quite a lot of things, most notably user authentication. I can add this later as there are ready-made solutions, gems in Ruby lingo, for it. Next, I will have a look at models.
Looking at models
A model is a Ruby representation of a domain concept. Rails has automatically create three of them, as instructed, and it will be able to store them in and retrieve them from the database. However, I want to do a little more. Instead of working with IDs and foreign keys, I want Rails to populate collections automatically and efficiently where entites refer to each other. To do that, I will amend the models.
The basic model is empty because it inherits all of its behavior from a framework superclass:
class Person < ApplicationRecord end
So I will help Rails by adding some information about its place in the world:
class Person < ApplicationRecord has_many :roles has_many :circles, through: :roles validates :name, presence: true validates :email, presence: true, uniqueness: true end
Again, I find this pretty self-explanatory. The
through phrase tells the Person model that its relationship to Circles is many-to-many and Roles are actually used as the intermediary, the join table in SQL lingo. The
presence phrase checks that the name or email are neither null nor a zero length string. Convenient.
Let’s have a look at the other two model classes:
class Circle < ApplicationRecord has_many :roles has_many :people, through: :roles validates :name, presence: true end
The Role class is already prepopulated a little more, because when we created the scaffold, we told it already to
reference the Circles and People. This is the generated code:
class Role < ApplicationRecord belongs_to :person belongs_to :circle end
I only need to add some validation to make the user interface aware.
class Role < ApplicationRecord belongs_to :person belongs_to :circle validates :name, presence: true validates :person, presence: true validates :circle, presence: true end
Trying it out
Now let’s see what we have done. Rails comes with a simple server for development purposes.
Before we try it out, let me save you a little googling. If you are on a proper *nix machine, skip to the next paragraph. But if you are in Windows and using the Windows Subsystem for Linux to follow along, please change the file
holo/config/environments/development.rb as follows to circumvent some quirks of this platform:
Rails.application.configure do ... # Add this line anywhere: config.web_console.permissions = ['192.168.0.0/16', '172.16.0.0/12', '10.0.0.0/8'] ... # Find this line and change it as follows: # config.file_watcher = ActiveSupport::EventedFileUpdateChecker config.file_watcher = ActiveSupport::FileUpdateChecker ...
This will allow you to use the web console, a useful debugging tool, in your browser, and it will ensure that the Rails development server can pick up changes in your source code and immediately serve them to you despite the ~broken~ special Windows file system.
Now is the big time. I start the development server like this:
$ bin/rails s -b 0.0.0.0 => Booting Puma => Rails 184.108.40.206 application starting in development => Run `rails server --help` for more startup options Puma starting in single mode... * Version 4.3.7 (ruby 2.7.2-p137), codename: Mysterious Traveller * Min threads: 5, max threads: 5 * Environment: development * Listening on tcp://0.0.0.0:3000
And there we go. I don’t want to see the Hello World page Rails automatically created so I point my browser to the appropriate IP address and request the
Awesome. I like the no nonsense style. More seriously, of course I can use all kinds of CSS and whatnot to make it suit my taste but this is about function and not style. To prove it, let’s use the REST API instead.
This is what no people look like in JSON. And we had to do nothing additional to get this API. Now go build your fancy Angular front-end on top of that. But fist, let’s add some people. I’ll go back to the web page and click “Add Person”. Let’s look at the web page.
This is not ideal. The web page uses a standard text field for the email field. I want to harness the power of HTML5 and tell the browser that this is a text field. I will edit the form source code for this. Here is the generated code:
<%= form_with(model: person, local: true) do |form| %> <% if person.errors.any? %> <div id="error_explanation"> <h2><%= pluralize(person.errors.count, "error") %> prohibited this person from being saved:</h2> <ul> <% person.errors.full_messages.each do |message| %> <li><%= message %></li> <% end %> </ul> </div> <% end %> <div class="field"> <%= form.label :name %> <%= form.text_field :name %> </div> <div class="field"> <%= form.label :email %> <%= form.text_field :email %> </div> <div class="actions"> <%= form.submit %> </div> <% end %>
It is the weird mix of HTML and Ruby that you will have seen in other templating engines before. Rails provided s lot of helpers to do the usual things. So indeed, I will use an
email_field instead of the
<%= form_with(model: person, local: true) do |form| %> ... <div class="field"> <%= form.label :email %> <%= form.email_field :email %> </div> ... <% end %>
And indeed, now the browser shows this:
Let’s add someone nameless.
We are not happy. I will add a proper name.
And let’s go back to the people overview.
Ha! I have added you as well. Sneaky, huh?
Now this may not seem like much but really it is. Keep in mind that all of this happened in only a few minutes, and I have already create the same functionality for circles and roles.
For now, the article is already quite long, so I will continue another day.