Skip to content
This repository has been archived by the owner on Jul 20, 2023. It is now read-only.

2 Event Management

Dennis Tanaka edited this page Sep 13, 2018 · 25 revisions

In this section, we are going to create pages for event management from scratch.

Generate Controller

First, we are going to create a very basic structure for our work by using the command below:

$ rails generate controller Admin/Events index show new edit

or

$ rails g controller Admin/Events index show new edit

The most important items created by the generate controller command are the controller and views. It also edits the config/routes.rb file.

We will first focus our attention in the controller class and the view folder created with the command above.

Set Up the Routes

But before starting the server to see the results, let's edit the config/routes.rb file as the generator does the wrong thing and adds several namespace entries to the file, such as:

namespace :admin do
  get 'events/index'
end

Remove the lines added by the generator and add the following code:

get 'events/index'
get 'events/show'
get 'events/new'
get 'events/edit'

inside the existing namespace.

Start the server and access the URL below to see the results:

/admin/events/index

Take a look at the view file indicated in the rendered page.

Now, let's make the URL /events point to the index action. To do this, make that line look like the following:

get 'events', to: 'events#index'

We can also access the other pages created by the generator:

/admin/events/show
/admin/events/new
/admin/events/edit

Resourceful Route

In the test above, the generator added four entries in the routes.rb file to handle GET requests against the /admin/events path. We are going to keep editing/creating actions in the Events controllers to do things like creating new events or updating existing ones. As we progress, we could write a new route for each action we implement in the Events controller, but such actions are so common and frequently implemented that we have a way to declare all routes at once.

It is done through a resourceful route and we have already seen it when creating the Event Categories pages. So, adding the line below

resources :events

is equivalent to declaring all the routes below:

HTTP Verb Path Controller#Action Used for
GET /admin/events admin/events#index display a list of all events
GET /admin/events/new admin/events#new return an HTML form for creating a new event
POST /admin/events admin/events#create create a new event
GET /admin/events/:id admin/events#show display a specific event
GET /admin/events/:id/edit admin/events#edit return an HTML form for editing an event
PATCH/PUT /admin/events/:id admin/events#update update a specific event
DELETE /admin/events/:id admin/events#destroy delete a specific event

As we are going to implement all those actions, let's use the resourceful route. After that, test your application and commit your changes.

Event Model

Preparation - TimeZone

Before start working with our Event model, let's first do one preparation.

We are going to change the default timezone of our application. To do that, add the following line to the file config/application.rb:

config.time_zone = 'Tokyo'

Back to the Event Model

Before we change our controller and add views to our application, we are going to create the Event model. The table below shows the information contained in a specific event:

events
id integer
title string
location string
start_at datetime
ticket_price integer
ticket_quantity integer
event_category_id integer

This model will translate into a table in the database in our Rails application. We haven't talked about databases at all until now, but it is a good time for it.

We are already using a relational database in our project. On a production environment, we would probably use something like MySQL or PostgreSQL. We could use these or any other database, but they usually require some setup steps, so we are currently sticking to SQLite.

Typically, a relational database requires a server to be running, but SQLite doesn't. It is a simple database engine that stores data in files, comes preloaded with Rails and doesn't require any configuration.

We already have an event_categories table in our database and we are now going to create a new table called events. We could execute this step by writing SQL code, but we don't need to do it with Rails. Let's create our Event model and, next, its corresponding table in the database.

$ rails generate model Event event_category:references title:string location:string start_at:datetime ticket_price:integer ticket_quantity:integer

At this point, Rails still didn't create an events table for us, but it created a migration file. Please, take a look into the contents of the folder db/migrate. The file created at this folder contains the specification of the new table we are about to create. We won't do it now, but we could also edit its contents before applying the migration.

Rails also created a model file: app/models/event.rb. It has very little content in it, but because of inheritance, it already knows how to interact with the database to manipulate Events' data. Now, let's execute the migration to effectively create the events table.

$ rails db:migrate

If there were no errors, our events table should be created in the database. How can we know?

Rails Console

The Rails console allows us to type Ruby code into a command line interface and get its output immediately. One obvious use is to test small pieces of code. Because it loads all the models in our application, we are going to use it to create a couple of events using the table/model we just created.

In the previous step, we created the events table in the database without writing any SQL code. This time, we won't need to use any SQL code to interact with the database either. We are going to do that through Active Record.

Active Record is the M in the MVC model. It is the layer that allows us to communicate with the database by using Ruby code.

To activate it, type:

rails console

or

rails c

In the prompt that appears, we can write any sort of Ruby code. Remember the timezone configuration we did previously? Let's see if it is working as expected.

Type:

> Time.current

It should output the correct date and time in Tokyo. If we hadn't configured the timezone, it would output the current GMT (Greenwich Mean Time).

Nice! Now, let's query all the events in the database. Type:

> Event.all

It should return something like below:

=> #<ActiveRecord::Relation []>

Obviously, it returned an empty list as we've just created the events database and have no entries in it. We are going to create a page to add new events to our database. But for now, let's play with the console a little bit.

First, let's create new events in the console using Active Record by typing the line below in the console:

> Event.create(event_category_id: 1, title: 'Nice Konkatsu Event', location: '銀座', start_at: Time.current + 30.days, ticket_price: 5000, ticket_quantity: 50)

Now, Event.all should return a different result. If it does, let's add two more events and go on:

> Event.create(event_category_id: 2, title: 'Fun Koikatsu Event', location: '池袋', start_at: Time.current + 30.days, ticket_price: 4000, ticket_quantity: 20)

> Event.create(event_category_id: 3, title: 'Gourmet Themed Event', location: '新宿', start_at: Time.current + 30.days, ticket_price: 6000, ticket_quantity: 3)

Let's try again. Type:

> Event.all

Great! The events we created are appearing. We can now exit the console by typing:

> exit

**Note: **You should already have noticed, but every time we use Active Record, Rails logs the query that is used to interact with the database.

Associations

At this point, we have event categories and events. They are related to each other, as an event has a category and multiple events may belong to a category.

In Rails, we can specify associations in our model classes. And it provides a very simple and natural way to access the data of associated entities.

If we take a look at the app/models/event.rb file, we are going to see that Rails already added the necessary association:

belongs_to :event_category

That's because we used event_category:references in our model generation command. But it didn't change the EventCategory model, so add the line below to the app/models/event_category.rb:

  has_many :events

Let's test the associations we just defined in the Rails console.

> Event.first.event_category

> EventCategory.find(3).events

Good, we now have some events in the database and can proceed to the next step. Just don't forget to commit your work.

Index Page

Now, we are going to create an index page to view the events in our database. The page will have the same format as the Event Categories page we created previously with scaffolding.

Let's copy the code below to the file app/views/events/index.html.erb. It is a static HTML page that we are going to turn into a dynamic page.

<h1>Events</h1>

<table>
  <thead>
    <tr>
      <th>Category</th>
      <th>Title</th>
      <th>Location</th>
      <th>Start At</th>
      <th>Ticket Price</th>
      <th>Ticket Quantity</th>
      <th colspan="3"></th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>konkatsu</td>
      <td>Nice Konkatsu Event</td>
      <td>銀座</td>
      <td>2018/02/25 20:00</td>
      <td>5,000円</td>
      <td>50</td>
      <td><a>Show</a></td>
      <td><a>Edit</a></td>
      <td><a>Destroy</a></td>
    </tr>
    <tr>
      <td>koikatsu</td>
      <td>Fun Koikatsu Event</td>
      <td>池袋</td>
      <td>2018/02/25 18:00</td>
      <td>4,000円</td>
      <td>20</td>
      <td><a>Show</a></td>
      <td><a>Edit</a></td>
      <td><a>Destroy</a></td>
    </tr>
    <tr>
      <td>gourmet</td>
      <td>Gourmet Themed Event</td>
      <td>上野</td>
      <td>2018/02/25 12:00</td>
      <td>6,000円</td>
      <td>3</td>
      <td><a>Show</a></td>
      <td><a>Edit</a></td>
      <td><a>Destroy</a></td>
    </tr>
  </tbody>
</table>

<br>

Steps

We first need to provide the view with the data it needs and it needs a list with the events we have in our database. How do we do that? We are able to do it by using instance variables. To do that, edit the index method in the Events controller. Add the following line:

@events = Event.all

Now, let's just add a loop to iterate through all the events:

<% @events.each do |event| %>

<% end %>

And start getting the values for the event variable.

Tip: we can format the date attribute to make it easier to read:

event.start_at.strftime('%Y/%m/%d %H:%M')

Result

<h1>Events</h1>

<table>
  <thead>
    <tr>
      <th>Category</th>
      <th>Title</th>
      <th>Location</th>
      <th>Start At</th>
      <th>Ticket Price</th>
      <th>Ticket Quantity</th>
      <th colspan="3"></th>
    </tr>
  </thead>

  <tbody>
    <% @events.each do |event| %>
      <tr>
        <td><%= event.event_category.name %></td>
        <td><%= event.title %></td>
        <td><%= event.location %></td>
        <td><%= event.start_at.strftime('%Y/%m/%d %H:%M') %></td>
        <td><%= event.ticket_price %>円</td>
        <td><%= event.ticket_quantity %></td>
        <td><%= link_to 'Show', admin_event_path(event) %></td>
        <td><%= link_to 'Edit', edit_admin_event_path(event) %></td>
        <td><%= link_to 'Destroy', admin_event_path(event), method: :delete, data: { confirm: 'Are you sure?' } %></td>
      </tr>
    <% end %>
  </tbody>
</table>

<br>

<%= link_to 'New Event', new_admin_event_path %>

Layout

The data is displayed correctly, but clearly, the layout looks wrong. We can fix it by specifying the correct layout for the events pages. We can do it by editing the Events controller app/controllers/admin/events_controller.rb. Add the following line to it:

layout 'admin'

Show Page

Good! We can now view the list of events we created. Now, let's create a page to view the details of a specific event.

First, we are going to edit the controller to create the data to provide to the view. Edit the show method as shown below:

def show
  @event = Event.find(params[:id])
end

The params variable contains the attributes passed as parameters in the request.

Next, we should edit the show view:

/app/views/admin/events/show.html.erb

Let's start with the following static page:

<p>
  <strong>Category:</strong>
  konkatsu
</p>

<p>
  <strong>Title:</strong>
  Nice Konkatsu Event
</p>

<p>
  <strong>Location:</strong>
  銀座
</p>

<p>
  <strong>Start At:</strong>
  2018/03/02 21:38
</p>

<p>
  <strong>Ticket Price:</strong>
  5000円
</p>

<p>
  <strong>Ticket Quantity:</strong>
  50
</p>

We just have to replace the static values by the attributes in the @event instance variable we assigned in the controller.

Result

<p>
  <strong>Category:</strong>
  <%= @event.event_category.name %>
</p>

<p>
  <strong>Title:</strong>
  <%= @event.title %>
</p>

<p>
  <strong>Location:</strong>
  <%= @event.location %>
</p>

<p>
  <strong>Start At:</strong>
  <%= @event.start_at.strftime('%Y/%m/%d %H:%M') %>
</p>

<p>
  <strong>Ticket Price:</strong>
  <%= @event.ticket_price %>円
</p>

<p>
  <strong>Ticket Quantity:</strong>
  <%= @event.ticket_quantity %>
</p>

<%= link_to 'Edit', edit_admin_event_path(@event) %> | 
<%= link_to 'Back', admin_events_path %>

New Page

The next step is to create the form to create new events. As always, we are going to edit our controller and edit the following method:

def new
  @event = Event.new
end

Then, we are going to edit the view app/views/admin/events/new.html.erb and start with the following content:

<h1>New Event</h1>

<form>
  
  <div class="field">
    <label for=""></label>
    <input type="text" id="">
  </div>
  
  <div class="field">
    <label for=""></label>
    <input type="text" id="">
  </div>
  
  <div class="field">
    <label for=""></label>
    <input type="text" id="">
  </div>
  
  <div class="field">
    <label for=""></label>
    <input type="text" id="">
  </div>
  
  <div class="field">
    <label for=""></label>
    <input type="text" id="">
  </div>
  
  <div class="field">
    <label for=""></label>
    <input type="text" id="">
  </div>
  
  <div class="actions">
    <input type="submit">
  </div>
  
</form>

Steps

First, we are going to use a Rails form helper to create the form by replacing the <form> tag with:

<%= form_with(model: [:admin, @event], local: true) do |form| %>
...
<% end %>

Next, we are going to keep using Rails form helpers to create our inputs by replacing the <label> and <input> tags by code like the below:

<%= form.label :title %>
<%= form.text_field :title, id: :event_title %>

For the datetime field, let's use the following code:

<%= form.label :start_at %>
<%= form.datetime_select :start_at, id: :event_start_at %>

At last, let's add the submit button:

<%= form.submit %>

Result

<h1>New Event</h1>

<%= form_with(model: [:admin, @event], local: true) do |form| %>
  
  <div class="field">
    <%= form.label :event_category_id %>
    <%= form.text_field :event_category_id, id: :event_event_category_id %>
  </div>
  
  <div class="field">
    <%= form.label :title %>
    <%= form.text_field :title, id: :event_title %>
  </div>
  
  <div class="field">
    <%= form.label :location %>
    <%= form.text_field :location, id: :event_location %>
  </div>
  
  <div class="field">
    <%= form.label :start_at %>
    <%= form.datetime_select :start_at, id: :event_start_at %>
  </div>
  
  <div class="field">
    <%= form.label :ticket_price %>
    <%= form.text_field :ticket_price, id: :event_ticket_price %>
  </div>
  
  <div class="field">
    <%= form.label :ticket_quantity %>
    <%= form.text_field :ticket_quantity, id: :event_ticket_quantity %>
  </div>
  
  <div class="actions">
    <%= form.submit %>
  </div>
  
<% end %>

<%= link_to 'Back', admin_events_path  %>

At this point, we should be able to check our New Event page, but we are still unable to create the event. Let's add code to do that!

Create!

To create new events, we have to implement the create action in our controller. One thing to remember is that, according to our routes, the create action dispatches a request to the same path as the index action, but instead of using the GET method, the POST method is used. Also, the create action typically does not require the creation of a create view (although we could).

Let's edit the Events controller to add the following method:

def create
  @event = Event.new(params)
  @event.save
end

If we try to submit our new event, we should be able to create a new event!... Ooops! Something went wrong and you should see the following error:

ActiveModel::ForbiddenAttributesError in Admin::EventsController#create

In the error page, we can see the parameters that the create action received. We are trying to create a new event passing these parameters to the new event instantiation. This process is called "mass assignment" and Rails does not accept mass assignments until the parameters are whitelisted. We should not trust parameters from the Internet and Rails forces us to decide what parameters can be used in our controller or not. This control mechanism is called "Strong Parameters".

Note: it is interesting to see how Rails automatically created the :event hash in the parameters by using the ids we specified in our new.html.erb, such as id: :event_event_category_id

Strong Parameters

To whitelist the parameters we want to allow in our mass assignment, we will change the create method to look like below:

def create
  event_params = params.require(:event).permit(:title, :location, :start_at, :ticket_price, :ticket_quantity, :event_category_id)
  @event = Event.new(event_params)
  @event.save
end

This should be enough for new events to be created. Go back in the browser and press "CREATE EVENT".

redirect_to

Nothing happens, but if we go to /admin/events, we should see the new event created. What happened?

If we take a look at the console, we should see the following message:

No template found for Admin::EventsController#create, rendering head :no_content

As our create method does not specify anything else, Rails tries to find a create view create.html.erb, but it fails as it does not exist. As create actions typically don't require corresponding views, let's modify the create method to give feedback to the user:

def create
  event_params = params.require(:event).permit(:title, :location, :start_at, :ticket_price, :ticket_quantity, :event_category_id)
  @event = Event.new(event_params)
  @event.save
  redirect_to admin_event_path(@event), notice: 'Event was successfully created.'
end

Nice! We are now being redirected to the event details page and a nice message tells us that everything went fine. To display this message, we just had to pass the parameter notice and specify the message we want to show. It works because the Admin layout is already prepared for such task.

The line

redirect_to admin_event_path(@event), notice: 'Event was successfully created.'

could be rewritten as

flash[:notice] = 'Event was successfully created.'
redirect_to admin_event_path(@event)

As, notice and alert keys are a common idiom, convenience accessors are available:

flash.notice = 'Event was successfully created.'

Anything we place in the flash hash will be exposed to the very next action and then cleared out.

Edit page

Now, it's time to edit the edit view for our events. But first, we are going to edit the appropriate action in the controller:

def edit
  @event = Event.find(params[:id])
end

This should do for the edit action, but we are repeating the same code as the show action.

We can improve this situation by adding the following code to the end of the file:

private
  def set_event
    @event = Event.find(params[:id])
  end

Now, we can declare that this new method will be run before some actions. Add the code below at the top of the class:

before_action :set_event, only: [:show, :edit]

Lastly, we can remove the following line from the show and edit methods:

@event = Event.find(params[:id])

View

Now we are going to edit the view file for the edit action: app/views/admin/events/edit.html.erb

In this view, we are going to have a form the same way we have in the new action. How do these forms differ? They don't, they are exactly the same. So, to avoid repetition, we are going to use partial templates.

Partial templates or "partials" allow us to write view code once and include it wherever it is needed. Let's create a partial for the form to create/edit events. To do that, create the file app/views/admin/events/_form.html.erb. That's it, the convention is to prepend the filename with a underscore (_). Transfer the form from new.html.erb to this new file. We also need to change the instance variable @event to simply event. The resulting file will look like below:

<%= form_with(model: [:admin, event], local: true) do |form| %>
  
  <div class="field">
    <%= form.label :event_category_id %>
    <%= form.text_field :event_category_id, id: :event_event_category_id %>
  </div>
  
  <div class="field">
    <%= form.label :title %>
    <%= form.text_field :title, id: :event_title %>
  </div>
  
  <div class="field">
    <%= form.label :location %>
    <%= form.text_field :location, id: :event_location %>
  </div>
  
  <div class="field">
    <%= form.label :start_at %>
    <%= form.datetime_select :start_at, id: :event_start_at %>
  </div>
  
  <div class="field">
    <%= form.label :ticket_price %>
    <%= form.text_field :ticket_price, id: :event_ticket_price %>
  </div>
  
  <div class="field">
    <%= form.label :ticket_quantity %>
    <%= form.text_field :ticket_quantity, id: :event_ticket_quantity %>
  </div>
  
  <div class="actions">
    <%= form.submit %>
  </div>
  
<% end %>

Now, we are going to include this form to new.html.erb by adding the following line:

<%= render 'form', event: @event %>

We can include the form in the edit view the same way. After doing this and adding some navigation links, the file will look like below:

<h1>Event</h1>

<%= render 'form', event: @event %>

<%= link_to 'Show', admin_event_path(@event) %> |
<%= link_to 'Back', admin_events_path  %>

It should be enough to allow the edit page to display correctly. But, we are still unable to save our edits.

Update!

Now, we are going to implement the update action. For the update process, we also need to run the set_event method. So, let's add the action to the before_action line:

before_action :set_event, only: [:show, :edit, :update]

After that, the code below should be enough to update an event:

def update
  event_params = params.require(:event).permit(:title, :location, :start_at, :ticket_price, :ticket_quantity, :event_category_id)
  @event.update(event_params)
  redirect_to admin_event_path(@event), notice: 'Event was successfully updated.'
end

But we have some repeated code again in the line:

event_params = params.require(:event).permit(:title, :location, :start_at, :ticket_price, :ticket_quantity, :event_category_id)

To improve this, we are going to create the following method below the set_event:

def event_params
  params.require(:event).permit(:title, :location, :start_at, :ticket_price, :ticket_quantity, :event_category_id)
end

With this change, we can now remove the line below from the create and edit actions:

event_params = params.require(:event).permit(:title, :location, :start_at, :ticket_price, :ticket_quantity, :event_category_id)

At this point, we are just missing a way to delete events.

Destroy!

The only action left is the destroy action. We don't need a view for that action too. So, let's just add the following method to the controller:

def destroy
  @event.destroy
  redirect_to admin_events_url, notice: 'Event was successfully destroyed.'
end

And add the action to the before_action:

before_action :set_event, only: [:show, :edit, :update, :destroy]

Now, we are able to execute all management actions in our events.

Data Validation

We can now execute all the standard actions in our events, what is nice. But there's still a big problem. Have you tried to create a new event keeping its fields blank? If not, let's try it out! For now, please fill in only the "Event Category" field.

Yes, there's a problem here. Our application is not validating any of the values filled by the user. It does not make sense to create events without a title, a location and tickets information, does it?

Thankfully, Active Record provides us with many features for data validation. We are going to use what is called Active Record helpers and apply them to our models. That way, Rails will validate our data every time we try to save or update an entry, before the new or modified data reaches the database.

Presence

Let's first make sure that we just update our database if the user provides all the information needed. To do that, we are going to use the presence helper. We want to validate all the fields in our Event model, so add the following line to the model:

validates :event_category, :title, :location, :start_at, :ticket_price, :ticket_quantity, presence: true

Now, let's try adding a new event the same way we did before.

This time, Rails showed us an error page. This is because, both our controller and view are not ready to handle error messages.

Check the Results

First, we are going to make changes to our controller. We need to check the result of the save and update methods to show error messages to the user. These methods will look like the following after the changes:

def create
  @event = Event.new(event_params)
    
  if @event.save
    redirect_to admin_event_path(@event), notice: 'Event was successfully created.'
  else
    flash.now['alert'] = 'Event could not be created'
    render :new
  end
end
def update
  if @event.update(event_params)
    redirect_to admin_event_path(@event), notice: 'Event was successfully updated.'
  else
    flash.now[:alert] = 'Event could not be updated'
    render :edit
  end
end

Now, let's try creating the event again.

View

Nice! This time, the interface informed us that the event could not be created. But we can do better and inform the user what exactly is wrong about the data that is being provided. To do so, we are going to edit our views.

But actually, we already have this in our application. The event categories management form already shows that information because it is using a partial that was created previously to do just that. The partial path is shown below:

/app/views/admin/event_categories/_form.html.erb

You can take a look at the implementation later. For now, let's just use it to show errors in our form. To do this, let's edit the partial app/views/admin/events/_form.html.erb and add the following line to it:

<%= render 'admin/shared/form_errors', entity: event %>

Now, we should be good to go. Let's try adding a new event with blank fields again.

Good! Now it is doing proper validation!

More Validators

Let's finish the data validation section by using a couple more helpers.

Add the following lines to the model:

validates :title, length: { maximum: 80 }, uniqueness: true
validates :location, length: { maximum: 20 }
validates :ticket_price, :ticket_quantity, numericality: { only_integer: true, greater_than: 0 }

We are also going to add a validation for the start_at field. Let's say we want to allow only date/time starting from a week from now. We can do this using validates_each:

validates_each :start_at do |record, attr, value|
  record.errors.add(attr, 'must be at least a week from now') if value < Time.current + 1.week
end

Let's consider the validation finished for our Event model. You can read more about data validation at http://guides.rubyonrails.org/active_record_validations.html to learn more.