-
Notifications
You must be signed in to change notification settings - Fork 22
2 Event Management
In this section, we are going to create pages for event management from scratch.
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.
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
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.
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'
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?
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.
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.
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>
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')
<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 %>
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'
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.
<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 %>
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>
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 %>
<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!
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
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".
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.
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])
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.
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.
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.
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.
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.
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.
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!
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.