The logo of the Phoenix web framework

Even old photos need love

Goals:

Today we are going to get build a simple Phoenix CRUD (Create Read Update Destroy) application. Ultimately, we will be deploying this on Vultr but for now lets focus on some Phoenix basics with a practical goal: Making the best darn photo archive out there, Pharchive.

Requirements

Pharchive

I am an amateur photographer and I have hundreds of pages of photographic negatives which I need to painstakingly keep track of, and make search-able. To aid me in this arduous task I am going to draw on Phoenix.

Pharchive’s data model

Let’s start small, we can always add features later. Here is a list of things that I want Pharchive to do:

  • Keep track of:
    • Physical sheet ids
    • Description of the sheet’s contents
    • The rough time when the roll of film was exposed
    • The film used
      • The manufacturer of the film, just for kicks

This is a pretty straightforward set of Parent Child relationships, so this should be quick!

The models are going to be as follows:

A class diagram describing initial relationships in our data model

These are the type of models that Phoenix’s generators make easy, but first…

Scaffolding a new app

Move to where you would like to have the Pharchive project live in your directory structure, then run mix phoenix.new Pharchive. Don’t forget to answer Y to the question: “Fetch and install dependencies?”. cd Pharchive and run mix ecto.create, this makes sure that you have a database created for the Pharchive application.

At this point you have done all that is required to have a basic phoenix application which can respond to a web request. If you want to test this run mix phoenix.server -p 4000 (the -p 4000 is superfluous as mix phoenix.server defaults to using 4000, but I want to be sure not to lead you astray!) and visiting localhost:4000 in your browser or with curl.

Neat.

Next up we are going to start at the top of our data model with Manufacturer. Manufacturers are film producers, at this point I don’t need to know anything more about them than just a name. Let’s use Phoenix generators to build up a simple CRUD interface for working with Manufacturers.

mix phoenix.gen.html Manufacturer manufacturers name:string

Next we will create the Film model. This is a fairly important piece, which has a few useful attributes.

mix phoenix.gen.html Film films speed:integer name:string short_name:string manufacturer_id:references:manufacturers

Finally we get to the real meaty model, which I am primarily concerned with: Collections.Collections are actual physical objects which I keep track of.

mix phoenix.gen.html Collection collections physical_id:integer uuid:uuid location:string frame_count:integer description:string taken_at:date frame_size:string film_id:references:films

Now that everything is all generated, we have to make sure that our application router has the new pages in place.

file: router.ex

defmodule Pharchive.Router do
  use Pharchive.Web, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_flash
    plug :protect_from_forgery
    plug :put_secure_browser_headers
  end

  scope "/", Pharchive do
    pipe_through :browser # Use the default browser stack
    resources "/manufacturers", ManufacturerController
    resources "/films", FilmController
    resources "/collections", CollectionController
    get "/", PageController, :index
  end
end

Next, we must make sure that our associations are reflected in our models.

file: models/film.ex

defmodule Pharchive.Film do
...
    has_many :collections, Pharchive.Collection
...

file: models/manufacturer.ex

defmodule Pharchive.Manufacturer do
   schema "manufacturers" do
     ...
     has_many :films, Pharchive.Film
     ...
   end
 end

And finally, run a mix ecto.migrate to ensure that the database schema is up to date! With that, Pharchive has been properly setup and is ready for some upgrades.

Enhancements

The next step is automating UUID creation on a collection. Having to type in a fresh UUID for every collection is just going to get tedious. Luckily for us, this is a quick and easy change in web/models/collections.ex

  schema "collections" do
    ...
    field :uuid, Ecto.UUID, autogenerate: true
    ...
  end

  # Remove uuid from the list of required fields
  @required_fields ~w(physical_id location frame_count description taken_at frame_size)

And since we are relying on the application to generate these UUIDs, we can remove this whole form group from the collections form in web/templates/collection/form.eex. Go ahead and do that now.

Neat.

In order to effectively manage our soon to arrive front end packages, we are going to need Bower installed. I like Bower; it is easy to understand, and it plays very nicely with phoenix.

Now that we have Bower installed, lets add some enhancements to our UI. Our form’s date pickers are atrocious. I think we should get something a bit more usable. I chose to use bootstrap-datepicker.

Since jQuery is a prerequisite, we are going to need to tell Bower to install both jquery and bootstrap-datepicker. We can do this by creating a bower.json file at the top of the Pharchive project tree.

{
  "name": "Pharchive",
  "dependencies": {
    "jquery": "~> 2.1",
    "bootstrap-datepicker": "~> 1.5.1"
  }
}

The next time you fire up the Phoenix server you will see that bower fetches jquery and bootstrap-datepicker and makes them available to Phoenix. Next, we will initialize the datepicker in a new file web/static/js/datepicker.js

export default function initializeDatepickers() {
    let options = {}
    $('.datepicker').datepicker(options);
}

Neat!

We only have a few more steps to see this bear fruit. Let’s import our new datepicker function in web/static/js/app.js

...

import initializeDatepickers from "./datepicker"

initializeDatepickers()

And finally, we need to add the datepicker class onto the date input that we want to clean up. In web/templates/collection/form.html.eex change the following line:

    <%= date_select f, :taken_at, class: "form-control" %>

To

    <%= text_input f, :taken_at, class: "form-control datepicker" %>

and boom, our datepicker is now working in the Collection form!

Thats all for today folks, you can review the repo and check out the working code here. Next time we will be working with forms!