Rails 4 Application Development HOTSHOT (2014)

Chapter 2. Conference and Event RSVP Management

In the past 4 to 5 years, the number of events have increased manifold. This is due to the mushrooming of several different types of user groups around the world. Also, gatherings of people with similar interests are becoming commonplace nowadays. People with similar interests, for example, biking, food, movies, and blogging, also meet up and discuss topics of their interests.

Mission briefing

In this project, we will create an event and an RSVP creation website. Users of this application can sign up, log in, and create events. Once logged in, users can create, edit, and join events by creating an RSVP for it. Other users can also join events created by other users. The event can be edited only by the event owner.

We will also create a simple admin functionality where we can edit and delete these events. We will allow the admin users to approve or reject users who want to join the events. In the home page, we will have a system-wide feed of the recent events. When a user logs in, he or she will see the edit and delete options in front of the events they have created.

Also, the users will have a section called My Events, as shown in the following screenshot, where they can manage all events and RSVPs in one place:

Mission briefing

Why is it awesome?

Meet-ups are a great way to meet people with similar interests. The Internet has been a catalyst in bringing people together beyond boundaries, grabbing the attention of entrepreneurs. A number of websites enable people to create events and also allow them to register for events for free or for a fee. In a way, we are enabling people to easily organize gatherings, share, and enjoy together. They might end up making friends and having a lot of fun.

We will look at various features such as tagging and tag-based search, go further into ActiveRecord migrations, creating search-friendly URLs, adding states to objects, and using class methods.

Your Hotshot objectives

While building this application, we will go through the following tasks:

·        Creating and administrating events

·        Creating search-friendly URLs for events

·        Adding tags to events

·        Tagging-based search and tag cloud

·        Adding Gravatar for a user

·        Creating RSVPs for events

·        Adding event moderation

·        Creating "My events" to manage events created by users

Mission checklist

We need the following software installed on the system before we start with our mission:

·        Ruby 1.9.3 / Ruby 2.0.0

·        Rails 4.0.0

·        MySQL 6

·        Bootstrap 3.0

·        Sass

·        Sublime Text

·        Devise

·        Git

·        A tool for mockups

Creating and administrating events

Before we begin developing our application, we will take a cue from our previous project and build mockups for our events before we start. Again, we will use MockFlow for our purpose and build it.

Creating and administrating events

Also, we will create a mockup for the event page, as shown in the following screenshot:

Creating and administrating events

In this task, we will look at customizing our event views and also adding the custom before_filter object to protect our events.

Prepare for lift off

Taking a cue from the previous project, add a scaffold for events. The events schema looks as follows:

  create_table "events", force: true do |t|

    t.string   "title"

    t.datetime "start_date"

    t.datetime "end_date"

    t.string   "location"

    t.text     "agenda"

    t.text     "address"

    t.integer  "organizer_id"

    t.datetime "created_at"

    t.datetime "updated_at"

  end

Add devise gem and generate authentication methods for the application.

We will have to associate the user and event; however, the trick here is we will have multiple associations between them. Hence, we create an association with a different name as follows:

app/models/event.rb

class Event < ActiveRecord::Base

  belongs_to :organizers, class_name: "User"

end

app/models/user.rb

class User < ActiveRecord::Base

  devise :database_authenticatable, :registerable,

         :recoverable, :rememberable, :trackable, :validatable

  has_many :organized_events, class_name: "Event", foreign_key: "organizer_id"

end

In order to pass the organizer_id object, we will use our create method, as shown in the following code:

app/controllers/events_controller.rb

  def create

    @event = current_user.organized_events.new(event_params)

    respond_to do |format|

      if @event.save

        format.html { redirect_to @event, notice: 'Event was successfully created.' }

        format.json { render action: 'show', status: :created, location: @event }

      else

        format.html { render action: 'new' }

        format.json { render json: @event.errors, status: :unprocessable_entity }

      end

    end

  end

However, as this method depends on the current_user object, we will add a before_filter object in the following code snippet to allow only the logged in users to create an event:

app/controllers/events_controller.rb

before_filter :authenticate_user!

The following screenshot shows how our form should look when we begin this task:

Prepare for lift off

Engage thrusters

In this task, we will customize our event date formats by performing the following steps:

1.    We will start by customizing the date formats. The default format of date is datetime in Rails; hence, the date is displayed as the date and time function, as shown in the following screenshot:

Engage thrusters

2.    We will customize these events using the strftime function available in Ruby for converting the date to a more human-readable format in the following manner:

3.  Config/locales/en.yml

4.  en:

5.    time:

6.      formats:

7.        date_format: "%m/%d/%Y"

8.   

9.  app/views/index.html.erb

10. 

11.<%= l event.start_date, :format => :date_format %>

12. 

<%= l event.start_time, :format => :date_format %>

This will convert datetime to the date in the format of MM/DD/YYYY, as shown in the following screenshot:

Engage thrusters

13.          Now that dates are formatted, we have a complete event creation and display format with us. However, because it's a Web 2.0 system, a lot of users will log in. We need to protect the events created by particular users and allow only these users to update or delete them, as we want to allow only the event owner to edit and delete the event. In order to do so, we will first add a before_filter method. This method has to be private to keep it protected and has to be called inside the same controller, as shown in the following code snippet:

14.app/controllers/events_controller.rb

15.private

16.  def event_owner!

17. 

18.    authenticate_user!

19.    if @event.organizer_id != current_user.id

20.      redirect_to events_path

21.      flash[:notice] = 'You do not have enough permissions to do this'

22.    end

  end

This will make a method called event_owner available for the events controller. This method will authenticate users using devise and check if the current user ID is the same as the organizer ID of the event. If yes, then it will allow the edit, else it will redirect to all events path. Now that we will have the before_filter event_owner! method in place, we will protect specific methods as follows:

  before_action :event_owner!, only: [:edit,:update,:destroy]

This will restrict the edit, update, and destroy methods so that it can be accessed only by the event owner.

Objective complete – mini debriefing

In the preceding task, we added a custom method to authenticate our specific edit, update, and destroy methods so that only the event owners can do that. This is the kind of admin facility available only to the event owners. The before_action method is a new way to write before_filter in Rails 3.2. The main functionality of before_action is the same; however, both can be used based on the context:

before_action :event_owner!, only: [:edit,:update,:destroy]

We also customized the date format to a more human readable format than the default datetime format of Rails. We defined the format in our locale file and called it in the view directly.

<%= l event.start_date, :format => :date_format %>

This will allow dates to be localized. Apart from this, there are multiple ways to set the datetime format. We can also define the date format inside our helper and call it in the view. We will continue to customize our events in the following tasks.

Creating search-friendly URLs for events

A lot of libraries have now become standard in Rails since the past few versions. Their development and maintenance activity have also caught up with various versions of Ruby and Rails, as there is a lot of community backing these libraries too. A FriendlyId gem is one of the most standard libraries for creating search-friendly URLs also known as slugs. It is highly extensible and customizable.

Creating search-friendly URLs for events

Engage thrusters

We will go ahead and add slugs to our application in this task:

1.    We will add a friendly_id gem and migrations now, as follows:

gem 'friendly_id', '5.0.0.beta1'

Note

Only Version 5.0.0 or above is compatible with Rails 4.

2.    We will first add the friendly_id gem to the Gemfile and bundle it. Once done, we will have to first set up the migrations for slugs. For every model we have, we need to have a column for maintaining slugs:

3.  :~/evenstr$ rails g migration add_slug_to_events slug:string

4.    invoke  active_record

5.        create    db/migrate/20130811083714_add_slug_to_events.rb

6.   

7.  :~/evenstr$ rake db:migrate

8.  ==  AddSlugToEvents: migrating ================================================

9.  -- add_column(:events, :slug, :string)

10.   -> 0.2797s

11.==  AddSlugToEvents: migrated (0.2799s) =======================================

12.          We will then enable slug creation on a model. After adding a column for slugs, we need to enable slugging in the model as follows:

13.  extend FriendlyId

  friendly_id :title, use: :slugged

This will create a slug based on the title of the event. However, if the slug history feature is not enabled, the old URLs will give 404s. In order to maintain the old URLs intact, we need to enable history.

14.          In this step, we will enable the history option for slugs.

In case we need to set up the history and version feature for slugs, we need to generate another migration. This helps us to maintain slugs and their history in a different table and still keeping it unique. This is in case we want to allow the user to edit the URL and still want to resolve the old URLs. This is particularly helpful for use cases such as blogs where a lot of URLs are bookmarked frequently:

:~/eventstr$ rails generate friendly_id

rwub@rwub:~/eventstr$ rake db:migrate

==  CreateFriendlyIdSlugs: migrating ==========================================

-- create_table(:friendly_id_slugs)

   -> 0.1736s

-- add_index(:friendly_id_slugs, :sluggable_id)

   -> 0.1894s

-- add_index(:friendly_id_slugs, [:slug, :sluggable_type])

   -> 0.1778s

-- add_index(:friendly_id_slugs, [:slug, :sluggable_type, :scope], {:unique=>true})

   -> 0.1669s

-- add_index(:friendly_id_slugs, :sluggable_type)

   -> 0.1451s

==  CreateFriendlyIdSlugs: migrated (0.8537s) =================================

15.          If there are some existing records already, we would like to create slugs for them.

Now, as soon as we create events, we get a generated slug for our new events. In case we have events created before adding this functionality, we would have to manually update the slugs. Fire up the console and run the method to save the slugs:

1.9.3-p327 :001 > User.find_each(&:save)

  User Load (0.4ms)  SELECT `users`.* FROM `users` ORDER BY `users`.`id` ASC LIMIT 1000

    (0.2ms)  BEGIN

    (0.3ms)  COMMIT

 => nil

This will create slugs for all the events that were previously created.

Objective complete – mini debriefing

We created functionalities for creating slugs and also gave users the option to keep a history of slugs. Slugs in the preceding task are created from the attribute(s) defined in our case's title. These slugs are created because it is easier to get crawled by search engines and provide better visibility, and in turn user experience to the app user. We used the FriendlyId Version 5 to create slugs. The friendly_id Version 5 is not compatible with the earlier versions. Two major changes in this version are as follows:

·        Slugs do not get updated on the update method. In order to do this, we need to pass a nil value.

·        There are multiple options to create slugs in case one of them is not unique. As shown in the following code, if a slug created with title is not unique, the friendly_id gem will combine title and location to make a slug:

·        friendly_id :slug_candidates, use: :slugged

·         

·          def slug_candidates

·            [

·              :title,

·              [:title, :location],

·            ]

  end

The following screenshot shows how a typical slug looks:

Objective complete – mini debriefing

Adding tags to events

Tags are quite an interesting way to organize the content. As opposed to categories, tags are created and assigned by the user. In this task, we will enable our application so that we can create and save tags for each event. In our case, tags work as a primary way to search and categorize content. Acts_as_taggable is a formidable solution to this problem; however, we will look into building our own tagging method that is similar to acts_as_taggable. This could be a fun challenge as we are trying to emulate the behavior of an advanced gem from scratch.

Engage thrusters

We will now learn how to create tags in the following steps:

1.    Generate a tag model with a single attribute called name:

2.    :~/eventstr$ rails g model tag name:string

3.    invoke  active_record

4.    create    db/migrate/20130818094312_create_tags.rb

5.    create    app/models/tag.rb

6.    invoke    test_unit

7.    create      test/models/tag_test.rb

8.    create      test/fixtures/tags.yml

9.  db/migrations/create_tags.rb

10.class CreateTags < ActiveRecord::Migration

11.  def change

12.    create_table :tags do |t|

13.      t.string :name, index: true

14. 

15.      t.timestamps

16.    end

17.  end

end

Once we have a table to store tags, we would like multiple tags to be associated with an event. Hence, we need to create a join table that will handle multiple tags and events.

18.          Generate a model called taggings to create a join on events and tags, as shown in the following code:

19.  :~/eventstr$ rails g model tagging tag:belongs_to event:belongs_to

20.    minvoke  active_record

21.      create    db/migrate/20130818095026_create_taggings.rb

22.    create    app/models/tagging.rb

23.    invoke    test_unit

24.    create      test/models/tagging_test.rb

    create      test/fixtures/taggings.yml

This will create a table with the following migration:

db/migrations/create_taggings.rb

class CreateTaggings < ActiveRecord::Migration

  def change

    create_table :taggings do |t|

      t.belongs_to :tag, index: true

      t.belongs_to :event, index: true

      t.timestamps

    end

  end

end

Also, it will generate the table in the backend with the respective IDs of the tag and the event.

Engage thrusters

The tagging model already has the association with the event and tag models:

  belongs_to :tag

  belongs_to :event

25.          So, now we need to create an association between events and tags for them to use the ActiveRecord join. Add associations between events and tags.

26.          Inside your event model, define the association with tags:

27.  has_many :taggings

  has_many :tags, through: :taggings

28.          Now, inside your tag model, define the association with events:

29.  has_many :taggings

  has_many :events, through: :taggings

This will create a join between an event and a tag through the taggings table. We now have to write a method to create tags as a part of event creation.

30.          Write a method in the event to enter the tags in a list as comma-separated values, as shown in the following code snippet. These values will be stripped and entered into the database:

31.  def all_tags=(names)

32.    self.tags = names.split(",").map do |t|

33.      Tag.where(name: t.strip).first_or_create!

34.    end

  end

35.          Now, we need to add these values to the form because that's where we will enter the tags. The tags here are case sensitive, so "Awesome" and "awesome" will be treated as two different tags.

36.          These tags should be mounted to a model like an attribute of it. Rails 4 has a new way to set up whitelisted attributes. Strong parameters are no longer supported and attribute_accessor methods. These are now defined in the controller as a private method. We will add all_tags as a whitelisted virtual attribute to events_controller.rb:

37.App/controllers/events_controller.rb

38.  private

39.    # Never trust parameters from the scary internet, only allow the white list through.

40.    def event_params

41.      params.require(:event).permit(:title, :start_date, :start_time, :location, :agenda, :address, :organizer_id, :all_tags)

    end

42.          Add a list of tags to the form:

43.app/views/events/_form.html.erb

44.  <div class="field">

45.    all_tags

46.    <%= f.label :all_tags, "List All Tags, separate each tag by a comma" %><br />

47. 

48.    <%= f.text_field :all_tags %>

  </div>

Engage thrusters

After the values are entered into the the database, we will have to retrieve them and display them somewhere.

49.          Retrieve the tag values from the database.

Inside the event model, add a method to call the tags by name and display them as comma-separated values:

app/models/event.rb

  def all_tags

    tags.map(&:name).join(", ")

  end

50.          Display these values in the view by simply making a call on this method via the object:

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

  <%= event.all_tags %>

52.          We can alternatively display this list using the map method as follows:

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

  <%=raw event.tags.map(&:name).map { |t|  t }.join(', ')%>

Objective complete – mini debriefing

We have added tags to the events now. We used two tables, tags, and taggings to achieve this. The tags table saves the tag values, whereas the taggings table saves the association between tags and the records. We used index:true in our migration here. Theindex:true migration option is the same as add_index.

We then associated our tags and events via the taggings table. The taggings table is basically a join table to save the related values of tags and events. We then called all the tag values and displayed them as a comma-separated value list. The first_or_createmethod in ActiveRecord searches for a tag in the database. If it is not present, then a new tag is created, as shown in the following code snippet:

  self.tags = names.split(",").map do |t

    Tag.where(name: t.strip).first_or_create!

  end

We had to pass tags as an attribute in the form, hence we had to add it to the attribute whitelist. Depreciation of strong parameters is a major change in Rails 4. An older way to define parameters was using attr_accessor :all_tags.

In Rails 3.2, parameters lead to a lot of security vulnerabilities for Rails apps; hence, it was moved to protected methods inside a controller. Also, a method called permit is introduced in order to create a whitelist of parameters to be allowed to pass. Only when this is done, forms will accept a certain attribute.

params.require(:event).permit(:title, :start_date, :start_time, :location, :agenda, :address, :organizer_id, :all_tags)

We also saw how to add them to the views in both the form and displaying the final values. We will now go ahead and create a tag-based search and tag cloud.

The following screenshot shows how our events page looks with a list of Tags:

Objective complete – mini debriefing

Tagging-based search and tag cloud

We will continue from our earlier task where we created tags and entered them in the database to create scope-based searches on tags. We will also count the number of times a particular tag exists and will generate the size of the tag name in the tag cloud based on this. We will do all these tasks using the class methods inside our events model.

Engage thrusters

We will now create a tag cloud from the tags that have been saved.

1.    Add a tag search scope to the event controller.

Pass the value of tag as params and use the tagged_with scope to find all the records that contain a particular tag. Also, we are factoring for the records that do not have any tags associated with them:

app/controllers/events_controller.rb

  def index

    if params[:tag]

      @events = Event.tagged_with(params[:tag])

    else

      @events = Event.all

    end

  end

2.    Add links to tags for searching.

We need to add a link to a tag and a path to search a method for tags:

app/views/events/index.html.erb

<%= event.all_tags.map { |t| link_to t, tag_path(t }.join(', ') %>

However, we have not defined tag_path yet, so our next step will be to add a route.

3.    Add a get route to the method where we added a search method for tag that points to the index action in the events controller:

4.  config/routes.rb

  get 'tags/:tag', to: 'events#index', as: :tag

This will pass :tag as params to the index method and generate a link to the search results page.

Engage thrusters

5.    Until now, we did not have the tagged_with method that would search the tagged events. Let's write that now in the event method:

6.  app/models/event.rb

7.    def self.tagged_with(name)

8.      Tag.find_by_name!(name).events

  end

We can now list the events based on our tags:

Engage thrusters

To generate a crowd, we need to count the number of occurrences of tags in the database.

9.    Add a method to count the number of tags associated with all the events:

10.app/models/event.rb

11.  def self.tag_counts

12.    Tag.select("tags.name, count(taggings.tag_id) as count").

13.      joins(:taggings).group("taggings.tag_id")

  end

Now that we have the tag counts, we need to find a way to style them according to the sizes.

14.          Add a helper method to connect this to the view for counting and rounding off in application_helper.rb, as shown in the following code:

15.app/helper/application_helper.rb

16.def tag_cloud(tags, classes)

17.  max = tags.sort_by(&:count).last

18.  tags.each do |tag|

19.    index = tag.count.to_f / max.count * (classes.size - 1)

20.      yield(tag, classes[index.round])

21.    end

  end

22.          Add styles to different sizes of tags based on the tag count:

23.app/assets/tags.css

24.  .css1 { font-size: 1.0em; }

25.  .css2 { font-size: 1.2em; }

26.  .css3 { font-size: 1.4em; }

  .css4 { font-size: 1.6em; }

We will add this tags.css file to our application.css manifest file:

app/assets/stylesheets/application.css

 *= require tags

Finally, we will display the tags and apply the CSS classes to them.

27.          Create a loop by calling the tag_counts method on the event model. We will also pass an array of CSS classes based on the tag count to resize our text. This loop will identify the number of times a tag appears in a model class and applies the CSS class accordingly:

28.app/views/events/index.html.erb

29.  <div class="col-lg-4">

30.    <h3>Search Tags</h3> 

31.    <div>

32.      <% tag_cloud Event.tag_counts, %w{css1 css2 css3 css4} do |tag, css_class| %>

33.      <%= link_to tag.name, tag_path(tag.name), class: css_class %>

34.     <% end %>

35.   </div>

  </div>

Objective complete – mini debriefing

In the preceding task we saw how to create a tag-based search and tag cloud. In order to create our tag cloud, we created a CSS-based tag cloud, where the size of the tag term will be determined by that tag's number of occurrences in the database.

We then created the tagged_with method in order to search events with a particular tag, as shown in the following code snippet:

def self.tagged_with(name)

  Tag.find_by_name!(name).events

end

In order to count the tags and generate a tag cloud, we wrote the following code snippet:

def tag_cloud(tags, classes)

  max = tags.sort_by(&:count).last

  tags.each do |tag|

    index = tag.count.to_f / max.count * (classes.size - 1)

    yield(tag, classes[index.round])

  end

end

The tags variable returns an array. We run the sort_by(&count) method, which counts the number of occurrences of each tag, sorts the tags, and places the max-counted tag in the max variable. We then matched this max value of tag count with the value of the other tags; classes pass the value of the event in this case. The index variable includes the relative value of popularity of each tag in proportion to the other tags. The yield method finally returns the value of the tag and the popularity index of each tag.

We can refactor these methods to a concern and pass any class we want. Finally, we applied a style to the tag cloud according to the tag count, as shown in the following code snippet:

  <% tag_cloud Event.tag_counts, %w{css1 css2 css3 css4} do |tag, css_class| %>

  <%= link_to tag.name, tag_path(tag.name), class: css_class %>

  <% end %>

We get the following output:

Objective complete – mini debriefing

For tags whose related events have been deleted, the tags will still remain but their taggings will be deleted. The ones with no events will generate a zero-search result.

Adding Gravatar for a user

Gravatar (also known as globally recognized avatar) is an avatar system where you can register your e-mail and upload your avatar image accordingly. This image is automatically displayed based on the e-mail on the websites where the gravatar is available. We will add a facility to automatically display the gravatar image based on the user ID.

Engage thrusters

1.    Add a helper method to make a call on the gravatar method using the user's e-mail, as shown in the following code snippet:

2.  app/helpers/application_helpers.rb

3.   def avatar_url(user)

4.     gravatar_id = Digest::MD5::hexdigest(user.email).downcase

5.     "http://gravatar.com/avatar/#{gravatar_id}.png"

 end

6.    Add a method in the event model to find the event owner. We need this to display the gravatar of the event owner:

7.  app/models/event.rb

8.  def self.event_owner(organizer_id)

9.    User.find_by id: organizer_id

end

10.          In the controller, make a call on this method to call the event owner:

11.app/controllers/events_controller.rb

12.def show

13.  @event_owners = @event.organizers

end

14.          Call the helper method in the view to display the gravatar:

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

16.  <label>Organized By:</label><br/>

17.    <% @event_owners.each do |event_owner|%>

18.    <%= image_tag avatar_url(event_owner) %>

19.    <%= event_owner.email %>

  <br/>

Objective complete – mini debriefing

We have successfully added the gravatar image system to our application and displayed avatars of the users now. We used the gravatar API to search for the gravatar ID according to the user e-mail, as shown in the following code snippet:

def avatar_url(user)

   gravatar_id = Digest::MD5::hexdigest(user.email).downcase

   "http://gravatar.com/avatar/#{gravatar_id}.png"

  end

The gravatar ID accepts an e-mail as a unique field; hence, if the e-mail address of the person is present on the gravatar site, then it will return a gravatar ID. The image can be directly browsed from a direct link to the gravatar ID.

Objective complete – mini debriefing

Creating RSVPs for events

Now we have events and they can also be searched. In order to create RSVPs, we need to allow requests for joining events. In this task, we will allow users to make a request to join an event. This will generate a list of users joining a particular event. This is quite helpful in many ways. We will have to create a model called attendance where we will make a join of events and users. This is because we want to allow many users to join many events.

Engage thrusters

The following steps are performed to create RSVPs for events:

1.    Create a model for attendance with the user ID and event ID. This is a simple join model, and we generate it as follows:

2.  :~/eventstr$ rails g model attendance user_id:integer event_id:integer

3.    invoke  active_record

4.        create    db/migrate/20130818045351_create_attendances.rb                                                                                                                                

5.      create    app/models/attendance.rb                                                                                                                                                        

6.      invoke    test_unit                                                                                                                                                                       

7.      create      test/models/attendance_test.rb                                                                                                                                               

8.      create      test/fixtures/attendances.yml                                                                                                                                                

9.  :~/eventstr$ rake db:migrate

10.==  CreateAttendances: migrating ==============================================                                                                                                                

11.-- create_table(:attendances)                                                                                                                                                                   

12.   -> 0.1784s                                                                                                                                                                                   

13.==  CreateAttendances: migrated (0.1786s) =====================================  

14.          Add associations for the user and event in the attendance model:

15.app/models/attendance.rb

16.belongs_to :event

belongs_to :user

Do the same respectively for the event and user model too. So, in our user model, we can add:

  app/models/user.rb

  has_many :attendances

  has_many :events, :through => :attendances

And the event model looks as follows:

  app/models/event.rb

  has_many :attendances

  has_many :users, :through => :attendances

17.          In the controller, add a method to create attendance and pass event ID and user ID as params, as shown in the following code:

18.app/models/attendance.rb

19.def self.join_event(user_id, event_id,state)

20.  self.create(user_id: user_id, event_id: event_id, state: state)

21.end

22.app/controllers/events_controller.rb

23.  def join

24.    @attendance = Attendance.join_event(current_user.id, params[:event_id], 'request_sent')

25.    'Request Sent' if @attendance.save

26.    respond_with @attendance

  end

27.          Add a link to the Join event for a user to click and send the join request:

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

 <%= link_to "Join", event_join_path(:event_id => @event.id), :class=>"btn btn-success btn-small" %>

This, of course, is not complete without a route.

29.          Now, we will go ahead and add the route:

30.config/routes.rb

31.  resources :events do

32.    get  :join, to: 'events#join', as: 'join

  end

Objective complete – mini debriefing

We have now created a way for users to join events. However, in the current scenario, any user can click on Join and join the event, as shown in the following screenshot. This, in particular, is not a great thing as sometimes seats are limited and there is room only for relevant people in the meet-up. Hence, we need some sort of moderation for our events.

Objective complete – mini debriefing

Adding event moderation

Event moderation is an extremely critical feature that allows only the right kind of people to join the events. We will also look at the state machine in this task. We will use the workflow gem to build a state machine in order to create and manage our states.

A state machine is defined as a predefined sequence of actions that leads a process from one state to another. An event triggers the transition and changes the state.

In the classic example of a traffic signal, we have three states: stop, wait, and move. Each state is defined by a color. So, a signal turning green is a transition event, and this changes the state from stopped to moving.

In our use case, a state machine helps in the moderation process. Moderation is a multistep process, which involves the following steps:

·        The user sends a request to join the event

·        The event owner accepts or rejects the request

A state machine facilitates this process, where the request is an event and moderation is another event.

Engage thrusters

To add an event moderation, we will perform the following steps:

1.    Add the workflow gem and bundle:

gem 'workflow', :github => 'geekq/workflow'

2.    Add a column called state in the attendance table to save the current states of our users:

3.  :~/eventstr$ rails g migration add_state_to_attendance state:string

4.    invoke  active_record

5.        create    db/migrate/20130818052628_add_state_to_attendance.rb

6.  :~/eventstr$ rake db:migrate

7.  ==  AddStateToAttendance: migrating ===========================================

8.  -- add_column(:attendances, :state, :string)

9.     -> 0.2810s

10.==  AddStateToAttendance: migrated (0.2812s) ==================================

We need to define the states and transitions in order to accept and reject the requests.

11.          Include the workflow method in the attendance model and inherit it from the gem. We will create a column called state in our attendance model:

12.app/models/attendance.rb

13.include Workflow

  workflow_column :state

14.          Define states in the attendance model:

15.app/models/attendance.rb

16.  workflow do

17.    state :request_sent do

18.    event :accept, :transitions_to => :accepted

19.    event :reject, :transitions_to => :rejected

20.  end

21.    state :accepted

22.    state :rejected

  end

However, we need to persist the initial state, that is, request_sent. We will do this by saving it with the join method.

23.          Persist the state on the join method. This will allow the user to send the request by clicking on the join link:

24.app/controllers/events_controller.rb

25.  def join

26.    @attendance = Attendance.join_event(current_user.id, params[:event_id], 'request_sent')

27.  end

   )

In order to accept or reject the user, we need to toggle this state value to accept or reject.

28.          Toggle the state of the user attendance for accept and reject:

29.app/controllers/events_controller.rb

30.  before_action :set_event, only: [:show, :edit, :update, :destroy, :accept_request, :reject_request]

31.  def accept_request

32. 

33.    @attendance = Attendance.find_by(id: params[:attendance_id]) rescue nil

34.    @attendance.accept!

35.      'Applicant Accepted' if @attendance.save

36.      respond_with(@attendance)

37. 

38.  end

39. 

40.  def reject_request

41. 

42.    @attendance = Attendance.where(params[:attendance_id]) rescue nil

43.    @attendance.reject!

44.    'Applicant Rejected' if @attendance.save

45.    @respond_with(@attendance)

  end

Accepting and rejecting the events will also have to be wired to the views.

46.          Add routes to toggle the event states:

47.config/routs.rb

48.resources :events do

49.     get  :join, to: 'events#join', as: 'join'

50.     get  :accept_request, to: 'events#accept_request', as: 'accept_request'

51.     get  :reject_request, to: 'events#reject_request', as: 'reject_request'

  end

52.          Display the requests for the event owner. We will do this by adding a class method in the event model:

53.app/views/event.rb

54.  def self.pending_requests(event_id)

55.    Attendance.where(event_id: event_id, state: 'request_sent')

  end

We will now display them in the view:

app/views/events/show.html.erb

<% if user_signed_in? && @event.organizer_id == current_user.id%>

<label>Join Requests</label>

<% if @pending_requestss.present? %>

  <% @pending_requests.each do |p|%>

  <%= image_tag avatar_url(p.user) %>

  <%= p.user.email%>

    <%= link_to 'Accept', event_accept_request_path(:event_id => @event.id, :attendance_id => p.id), :class=>"btn btn-success btn-small" %>

    <%= link_to 'Reject', event_reject_request_path(:event_id => @event.id, :attendance_id => p.id), :class=>"btn btn-danger btn-small" %>

  <%end%>

<%else%>

  <p>No Pending Requests for this event</p>

<%end%>

<%end%>

Engage thrusters

56.          Display the accepted members.

In order to do this, we will first create a scope in our attendance model and a model class method in the event model:

app/model/attendance.rb

  scope :accepted, -> {where(state: 'accepted')}

app/model/

  def self.show_accepted_attendees(event_id)

    Attendance.accepted.where(event_id: event_id)

  end

Call this scope in the controller:

  @attendees = Event.show_accepted_attendees(@event.id)

Display the attendees on the event page:

<label>Attendees</label>

<% @attendees.each do |a|%>

  <%= image_tag avatar_url(a.user) %>

  <%= a.user.email%>

<%end%>

Objective complete – mini debriefing

We have created methods to accept and reject the event membership based on moderation. We used the workflow gem to do so. We defined the states and events in the attendance model:

  workflow do

  state :request_sent do

    event :accept, :transitions_to => :accepted

    event :reject, :transitions_to => :rejected

  end

    state :accepted

    state :rejected

  end

When we created an attendance, that is, when a user clicks on the Join button, we wrote a method to create a new attendance with event_id, user_id, and the state. We associated attendance with events and users. This is because we need to track who wants to attend which event. In order to toggle the state, we just need to call the transition event on the object. This will update the attendance column of the state as follows:

@attendance.accept!

We have also displayed the accepted members and hence we have a confirmed attendee list. In order to do this, we created scopes according to different states:

scope :accepted, -> {where(state: 'accepted')}

We made a call on this scope and passed event_id in the argument to find all the attendances who have been accepted by the moderator for a particular event:

Attendance.accepted.where(event_id: event_id)

Likewise, we have also defined a scope for the members whose moderation is pending:

scope :pending, -> {where(state: 'request_sent')}

We made a query on the scope in our controller:

Attendance.pending.where(event_id: @event.id)

Finally, the pending requests were displayed in our view using the loop:

<% if @pending_request.present? %>

 <% @pending_requests.each do |p|%>

  <%= image_tag avatar_url(p.user) %>

  <%= p.user.email%>

In order to accept the request, we passed event_id and attendance_id to the accept method in our events controller. This method will toggle the state of our attendance to accepted and likewise for rejected. Here, p.id is the the ID of the attendance whose state isrequest_sent and confirmation is pending:

  <%= link_to 'Accept', event_accept_request_path(:event_id => @event.id, :attendance_id => p.id),:class=>"btn btn-success btn-small" %>

  <%= link_to 'Reject', event_reject_request_path(:event_id => @event.id, :attendance_id => p.id), :class=>"btn btn-danger btn-small" %>

<%end%>

The accepted members of an event are shown in the following screenshot:

Objective complete – mini debriefing

Creating "My events" to manage events created by users

"My events" is a task where a user can see all the events created by him/her. This is sometimes very critical as the system feed is shown in the general list, and it is not convenient for a user to search for his events every time he/she needs to edit it. This task will provide them with all those events under one tag, and hence it is very convenient for them.

Engage thrusters

We will now create a separate section to display events created by a particular user:

1.    We already have an association between users and events:

2.  app/models/event.rb

3.    belongs_to :organizers, class_name: "User"

4.  app/models/user.rb

  has_many :organized_events, class_name: "Event", foreign_key: "organizer_id"

We will retrieve all the events organized by a user via the association, but we need to make a call on this method in the controller.

5.    So, we go ahead and make a call on it by passing the current user ID as a parameter to it in the events controller:

6.  App/controllers/events_controller.rb

7.    def my_events

8.      @events = current_user.organized_events

  end

Now, we have the controller method in place, so we need to display this task somewhere.

9.    What we need is a view for my_events under the events views. We will create a blank file called my_events.html.erb. The code in my_events will essentially be the same as index.html.erbb because even here we are making a list of events. We will also refactor tag cloud into a different partial:

10.app/views/events/_tag_cloud.html.erb

11.<!-- Displaying the tag cloud div-->

12. 

13.<div class="col-lg-4">

14.  <h3>Search Tags</h3>

15.  <div>

16.  <!-- Generating the tag cloud -->

17. 

18.  <% tag_cloud Event.tag_counts, %w{css1 css2 css3 css4} do |tag, css_class| %>

19.  <%= link_to tag.name, tag_path(tag.name), class: css_class %>

20.    <% end %>

21.   </div>

22.  </div>

23.</div>

24. 

25.app/views/events/my_events.html.erb

26.<div class="row">

27.  <div class="col-lg-8">

28.    <!--List Recently Created Events-->

29. 

30.    <h3>Recently Created Events</h3>

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

32.    <h3><%= event.title %></h3>

33.        <label>Start Date:</label><%= l event.start_date, :format => :date_format %>

34.        <label>End Date:</label><%= l event.start_time, :format => :date_format %><br/>

35.        <label>Location:</label><%= event.location %><br/>

36.        <label>Address:</label>

37.        <address>

38.    <%= event.address %><br/>

39.        </address>

40.        <label>Agenda:</label>

41.    <%= event.agenda %><br/>

42.        <label>Organized By:</label><br/>

43.        <%@event_owner = Event.event_owner(event.organizer_id)%>

44.        <%= image_tag avatar_url(@event_owner) %>

45.        <%= @event_owner.email %>

46.        <br/>

47.         <!-- Display Tags-->

48. 

49.        <label>Tags:</label><br/>

50.        <%=raw event.tags.map(&:name).map { |t| link_to t, tag_path(t) }.join(', ') %><br/><br/>

51. 

52.        <%= link_to 'Show Details', event, :class=>"btn btn-info btn-small" %>

53.        <% if user_signed_in? && event.organizer_id == current_user.id%>

54.        <%= link_to 'Edit', edit_event_path(event), :class=>"btn btn-primary btn-small" %>

55.        <%= link_to 'Delete', event, method: :delete, data: { confirm: 'Are you sure?' }, :class=>"btn btn-danger btn-small" %>

56.        <%end%><br/><br/>

57.    <% end %>

58. 

59.  </div>

60.  <%= render "tag_cloud"%>

61.<br>

62. 

<%= link_to 'Create an Event', new_event_path, :class=>"btn btn-default btn-primary" %>

63.          Finally, wire this up using a route in routes.rb and we are good to go:

64.config/routes.rb

get  :my_events, to: 'events#my_events', as: 'my_events'

Objective complete – mini debriefing

We created a separate "My events" area where a user can manage their events with ease. We did this by first creating an association between user and events. This association created a method called user_object.organized_events to retrieve all the events organized by a particular user. We used it in our controller by calling this on the current_user object:

current_user.organized_events

It is noteworthy that we have created two different types of associations between the user and event models:

·        One is a named association, where we denoted users as organizers and events as organized_events. In order to identify the models, we use the attribute called class.

·        The other is a has_many :through attendance association, where we made a join table to manage attendees for a particular event.

We also added a navigation called "My events", which is only visible once the user logs in to the system, as shown in the following screenshot:

Objective complete – mini debriefing

Mission accomplished

We have successfully created an event RSVP application, where users can create, administer, and moderate their events. Other users can send requests to join the events. We looked at various concepts such as tagging, tag-based search, tag cloud, and gravatar during the course of this project. Some of the topics we broadly covered in this project are as follows:

·        Creating multiple associations between the same models

·        Adding named associations and the has_many_:through association

·        Using the friendly_id gem to create slugs for each user

·        Creating tags for each event

·        Counting the tags and creating a tag cloud from it

·        Adding a state machine in order to create RSVP for an event

·        Displaying the gravatar of a user

·        Creating joins, usage of scopes, chained queries with scope, and additional conditions

Hotshot challenges

We have a few exercises to look into at the end of this project:

·        Setting the visibility of the Join events button should not be visible to the owners of the events

·        Restricting the owners of the event to join the event

·        Creating a tag-based search in a textbox

·        Displaying similar events for users based on tags

·        Adding validations and tests for the entire application