Phoenix Generator

(Use git checkout 4.phoenix_generator to catch up with the class)

Because adding CRUD application is so common, Phoenix comes with a task to generate all that code for us. Let’s generate a model to represent the level of the talk (e.g. beginner, advance).

mix phx.gen.html Schedule Audience audiences slug:string:unique name:string

Notice at the end of the command, it told us to add the new route resources "/audiences", AudienceController to our router. This will map all our CRUD operations. Let’s open our router lib/fawkes_web/router.ex and add:

resources "/audiences", AudienceController

To check to see what routes were added, let’s run

mix phx.routes

Now let’s run our migration to create the table:

mix ecto.migrate

Alright, let’s check to see if it works http://localhost:4000/audiences.

Well, that was a lot of work.

Other models

(Use git checkout 4a.generate_other_models to catch up with the class)

Let’s generate a few more models for our application.

mix phx.gen.html Schedule Slot schedule_slots slug:string:unique start_time:utc_datetime end_time:utc_datetime

Let’s add the resource to our router lib/fawkes_web/router.ex:

resources "/schedule_slots", SlotController

Now let’s add locations. Because we don’t need the front end for this, we will only generate the schema:

mix phx.gen.schema Schedule.Location locations slug:string:unique name:string

Same for event.

mix phx.gen.schema Schedule.Event events slug:string:unique name:string slot_id:references:schedule_slots

For the talk, we need to view the talks, so we will generate everything.

mix phx.gen.html Schedule Talk talks slug:string:unique title:string slot_id:references:schedule_slots audience_id:references:audiences location_id:references:locations description:text

Let’s add the resource to our router lib/fawkes_web/router.ex:

resources "/talks", TalkController

Speaker

(Use git checkout 4b.speaker to catch up with the class)

Before we generate the speaker, let’s generate a migration to add citext to our database. Citext will ignore case when searching for text.

mix ecto.gen.migration enable_citext_extension

Open up the migration file priv/repo/migrations/[date]_enable_citext_extension.exs. Add code to add citext.

defmodule Fawkes.Repo.Migrations.EnableCitextExtension do
  use Ecto.Migration

  def change do
    execute "CREATE EXTENSION citext", "DROP EXTENSION citext"
  end
end

Now we’re ready to generate the speaker.

mix phx.gen.html Schedule Speaker profiles talk_id:references:talks slug:string:unique image:string image_url:string first:string last:string company:string github:string twitter:string description:text

Let’s add the routes for the speakers:

resources "/speakers", SpeakerController

We need one more migration to link the category to the talks:

mix ecto.gen.migration create_talks_categories

Open up the migration file priv/repo/migrations/[date]_create_talks_categories.exs, add the code for the migration

defmodule Fawkes.Repo.Migrations.CreateTalksCategories do
  use Ecto.Migration

  def change do
    create table(:talks_categories) do
      add :talk_id, references(:talks, on_delete: :nothing)
      add :category_id, references(:categories, on_delete: :nothing)
    end

    create index(:talks_categories, [:talk_id])
    create index(:talks_categories, [:category_id])
  end
end

Now let’s run our migration to add all the tables:

mix ecto.migrate

Relationships

(Use git checkout 4c.relationships to catch up with the class)

Now that we have our models generated, let’s define their relationship. Open the Category module lib/fawkes/schedule/category.ex and add this code in the schema, below the fields to say that a Category has many talks.

many_to_many :talks, Fawkes.Schedule.Talk, join_through: "talks_categories"

For Audience module lib/fawkes/schedule/audience.ex, add add this code in the schema, below the fields to say that an audience has many talks.

has_many :talks, Fawkes.Schedule.Talk

A speaker belongs to a talk, so open lib/fawkes/schedule/speaker.ex, delete the line for the talk_id field and replace it with the belongs_to relationship:

belongs_to :talk, Fawkes.Schedule.Talk

Remove image, company, github, twitter, and description from the required fields.

|> validate_required([:slug, :image_url, :first, :last])

Add talk_id to the cast

|> cast(attrs, [:slug, :image, :image_url, :first, :last, :company, :github, :twitter, :description, :talk_id])

Open lib/fawkes/schedule/talk.ex, delete the fields for audience, location, slot. Add the following relationships:

many_to_many :categories, Fawkes.Schedule.Category, join_through: "talks_categories"
belongs_to :audience, Fawkes.Schedule.Audience
belongs_to :location, Fawkes.Schedule.Location
belongs_to :slot, Fawkes.Schedule.Slot
has_many :speakers, Fawkes.Schedule.Speaker

So that your schema section looks like this:

  schema "talks" do
    field :description, :string
    field :slug, :string
    field :title, :string

    many_to_many :categories, Fawkes.Schedule.Category, join_through: "talks_categories"
    belongs_to :audience, Fawkes.Schedule.Audience
    belongs_to :location, Fawkes.Schedule.Location
    belongs_to :slot, Fawkes.Schedule.Slot
    has_many :speakers, Fawkes.Schedule.Speaker

    timestamps()
  end

For the cast, add in :audience_id, :location_id, :slot_id like this:

|> cast(attrs, [:slug, :title, :description, :audience_id, :location_id, :slot_id])

In addition to the validation, in the changeset you can set the constraint that the child can’t be created if the parent doesn’t exist.

|> assoc_constraint(:audience)
|> assoc_constraint(:slot)
|> assoc_constraint(:location)

Your talk changeset should look like this:

def changeset(talk, attrs) do
  talk
  |> cast(attrs, [:slug, :title, :description, :audience_id, :location_id, :slot_id])
  |> validate_required([:slug, :title, :description])
  |> unique_constraint(:slug)
  |> assoc_constraint(:audience)
  |> assoc_constraint(:slot)
  |> assoc_constraint(:location)
end

Open lib/fawkes/schedule/slot.ex, let’s add the talk and event relationship.

has_many :talks, Fawkes.Schedule.Talk
has_one :event, Fawkes.Schedule.Event

Open lib/fawkes/schedule/event.ex, add slot_id to the cast call.

|> cast(attrs, [:slug, :name, :slot_id])

Add Timex dependency

(Use git checkout 4d.timex to catch up with the class)

Open mix.exs, after line 33 in the deps block, add a comma, then timex_ecto

{:timex_ecto, "~> 3.3"}

Let’s get the dependency by running:

mix deps.get

Now open lib/fawkes/schedule/slot.ex again. Change the start and end time to Timex.Ecto.DateTime so your slot schema looks like this:

schema "schedule_slots" do
  field :end_time, Timex.Ecto.DateTime
  field :slug, :string
  field :start_time, Timex.Ecto.DateTime

  has_many :talks, Fawkes.Schedule.Talk
  has_one :event, Fawkes.Schedule.Event

  timestamps()
end

One last thing before we can test it out. Open lib/fawkes_web/views/slot_view.ex. Add a function to get the dates.

def conference_dates do
  format = "{YYYY}-{M}-{D}"

  [Timex.parse!("2018-09-04", format),
   Timex.parse!("2018-09-05", format),
   Timex.parse!("2018-09-06", format),
   Timex.parse!("2018-09-07", format)]
end