CSC/ECE 517 Fall 2013/oss cmh

From Expertiza_Wiki
Jump to navigation Jump to search

Introduction

The invitation controller within Expertiza handles the invite functionality which allows user to send and receives invitations. Users can create teams and invite users to join there team. The invitation controller handles the creation of the invite and the accept or decline of an invite. The invitation controller also handles requests to join teams via advertisements. After advertisements are sent a user can request to receive and invite to the team and subsequent functionality is handled by the invitation controller once an invite is sent. Our project was to refactor the code around invitations mainly dealing with the invitation controller and invitation model.

Project Description

Classes: invitation_controller.rb (104 lines) invitation.rb (5 lines)

What it does: Handles invitations to join teams.

What needs to be done: The code is deeply nested and quite confusing. There should be a single method responsible for finding whether a user is already on a team, adding someone onto a waitlist, dropping someone off of a waitlist, changing/updating the topic that the user is assigned to, etc. Some of these methods should be in model classes, such as invitation.rb and signed_up_user.rb. Break the complicated methods into shorter methods with clear names, and place these methods in the most appropriate class, moving a lot of functionality to the model classes.

Currently, after someone joins a team, pending invitations are not removed. There should be a method handling deletion of invitations (including declined invitations) to a user after that user joins a team. This should be a model method.

Though this class is not long, the code looks and reads like it is very complicated. Breaking it down into multiple methods with clear names and proper division of functionality between classes will be a challenge.

Motivation

Briefly talk about why invitation controller needed refactoring.

Design Changes

Create Method

Before Refactor

def create    
    user = User.find_by_name(params[:user][:name].strip)
    team = AssignmentTeam.find_by_id(params[:team_id])
    student = AssignmentParticipant.find(params[:student_id])
    return unless current_user_id?(student.user_id)

    #check if the invited user is valid
    if !user
      flash[:notice] = "\"#{params[:user][:name].strip}\" does not exist. Please make sure the name entered is correct."
    else
      participant = AssignmentParticipant.find(:first, :conditions => ['user_id =? and parent_id =?', user.id, student.parent_id])
      if !participant
        flash[:notice] = "\"#{params[:user][:name].strip}\" is not a participant of this assignment."
      else
        team_member = TeamsUser.find(:all, :conditions => ['team_id =? and user_id =?', team.id, user.id])
        #check if invited user is already in the team
        if (team_member.size > 0)
          flash[:notice] = "\"#{user.name}\" is already a member of team."
        else
          sent_invitation = Invitation.find(:all, :conditions => ['from_id = ? and to_id = ? and assignment_id = ? and reply_status = "W"', student.user_id, user.id, student.parent_id])
          #check if the invited user is already invited (i.e. awaiting reply)
          if sent_invitation.length == 0
            @invitation = Invitation.new
            @invitation.to_id = user.id
            @invitation.from_id = student.user_id
            @invitation.assignment_id = student.parent_id
            @invitation.reply_status = 'W'
            @invitation.save
          else
            flash[:notice] = "You have already sent an invitation to \"#{user.name}\"."
          end
        end
      end
    end
    redirect_to :controller => 'student_team', :action => 'view', :id=> student.id
end

After Refactor

def create
    user = User.find_by_name(params[:user][:name].strip)
    team = AssignmentTeam.find_by_id(params[:team_id])
    student = AssignmentParticipant.find(params[:student_id])
    return unless current_user_id?(student.user_id)

    #check if the invited user is valid
    if !user
      flash[:note] = "\"#{params[:user][:name].strip}\" does not exist. Please make sure the name entered is correct."
    else
      participant= AssignmentParticipant.first( :conditions => ['user_id =? and parent_id =?', user.id, student.parent_id])
      #check if the user is a participant of the assignment
      if !participant
        flash[:note] = "\"#{params[:user][:name].strip}\" is not a participant of this assignment."
      else
        team_member = TeamsUser.all(:conditions => ['team_id =? and user_id =?', team.id, user.id])
        #check if invited user is already in the team
        if (team_member.size > 0)
          flash[:note] = "\"#{user.name}\" is already a member of team."
        else
          #check if the invited user is already invited (i.e. awaiting reply)
          if Invitation.is_invited?(student.user_id, user.id, student.parent_id)
            @invitation = Invitation.new
            @invitation.to_id = user.id
            @invitation.from_id = student.user_id
            @invitation.assignment_id = student.parent_id
            @invitation.reply_status = 'W'
            @invitation.save
          else
            flash[:note] = "You have already sent an invitation to \"#{user.name}\"."
          end
        end
      end
    end

    update_join_team_request

    redirect_to :controller => 'student_team', :action => 'view', :id=> student.id
end

Accept Method

The theme that we went with for refactoring the accept method is “Skinny Controller, Fat Model.” <ref> Best Practices "10 Ruby on Rails Best Practices" </ref> We wanted to move much of the logistics out of the accept method letting models handle this while keeping the controller limited to being the interface between the view and model. Before accept method was refactored it was very dense and contained too much functionality. The accept method was in the invitation controller was performing much of the invitation logic that should have been handled in the invitation model and various other models throughout the application. The accept method also played many different roles in that not only was it handling items that the model was responsible for but it was performing many different operations. There are many actions that need to take place when a user accepts an invitation and the controller method was taking care of all of these things. We began to eliminate these issues by extracting smaller operations that the accept method was handling into various methods and moving this code into various models such as the invitation.rb, signed_up_user.rb and team_user.rb.

Skinny Controller, Fat Model

Before: invitation_controller.rb - accept method
The following code served the purpose of moving a user out of their current team when they accept an invite to join another team. The code actually belonged in the teams_user model because it was affecting the teams_user table in the DB. The code was extracted into the subsequent method displayed below and called from the invitation controller.

old_entry = TeamsUser.find(:first, :conditions => ['user_id = ? and team_id = ?', student.user_id, params[:team_id]])
if old_entry != nil
   old_entry.destroy
end

After: invitation_controller.rb - accept method

TeamsUser.remove_previous_team(student.user_id, params[:team_id])

After: teams-user.rb
The two methods displayed below are the sample method. The first iteration of refactoring produced the first version of the method. After reviewing method names and variable names we found that our first solution could be improved and this came up with the second and final version of the method. <ref> Coding Conventions "Ruby On Rails coding conventions, standards and best practices" </ref>

def self.remove_previous_team(user_id, team_id)
    old_entry = TeamsUser.find(:first, :conditions => ['user_id = ? and team_id = ?', user_id, team_id])
    if old_entry != nil
      old_entry.destroy
    end
end

def self.remove_team(user_id, team_id)
    team_user = TeamsUser.find(:first, :conditions => ['user_id = ? and team_id = ?', user_id, team_id])
    if team_user != nil
      team_user.destroy
    end
end

Before: invitation_controller.rb - accept method
As with the previous example we found many more examples where a few lines of code serving one purpose could be extracted into its own method. The below code serves the purpose of removing a users team from the assignment team table in the DB. This would take place if a user joins a new team and they had previously created a team and was the only member of that team.

old_team = AssignmentTeam.find(:first, :conditions => ['id = ?', params[:team_id]])
if old_team != nil
   old_team.destroy
end

After: invitation_controller.rb - accept method
This is the subsequent method call for the extracted code which was placed in the invitation controller accept method.

AssignmentTeam.remove_team_by_id(params[:team_id]) 

After: assignment_team.rb
The method created in assignment_team.rb can be seen below.

#Remove a team given the team id
def self.remove_team_by_id(id)
  old_team = AssignmentTeam.find(id)
  if old_team != nil
    old_team.destroy
  end
end

As in the two previous examples there were many method extractions from code within the accept method.

Code Readability

Scopes
Scopes are a useful feature of rails that for creations of reusable constraints for database interactions. Scopes help create reusable code and more readable code. Where one would typically have to write a whole query within their code a scope and be used to minimize the need to write the same query over and over again and decrease the amount of code.

Query within accept method:

other_members = TeamsUser.find(:all, :conditions => ['team_id = ?', params[:team_id]])

Query within accept method with scope:

other_members = TeamsUser.by_team_id(params[:team_id])

Scope Added in teams_user.rb:

scope :by_team_id, ->(team_id) { where("team_id = ?", team_id) }

The help promote better code readability we added a few more scopes during the course of refactoring the accept method.

The below example shows the before and after version of a query. The subsequent version makes the query more readable.
Before:

SignUpTopic.find(old_teams_signup.topic_id)['assignment_id']

After:

SignUpTopic.find(old_teams_signup.topic_id).assignment_id

Variable Names
The following variable was used to capture where a member could be added to a team (i.e. is there enough space on the team for the user to join). The boolean value was returned from a method call and stored in the following variable but we felt the variable name was not an accurate representation of it's purpose

add_member_return

so we renamed it to the following.

can_add_member

After the above design changes the accept method still contained a quite a few calls to the various methods. This cause us to create a model method called accept_invite which served the purpose of encapsulating these function call into a single method that was not in the controller. This method was called from the accept method thus making the accept method considerably smaller. The purpose of the accept_invite method is to handle all of the changes that take place once a user accepts an invite. Below the starting and final version of the accept method can be seen. The accept method is much smaller and more readable since our refactoring.

Before Refactor

def accept
    @inv = Invitation.find(params[:inv_id])
    @inv.reply_status = 'A'
    @inv.save
    
    student = Participant.find(params[:student_id])
    
    #if you are on a team and you accept another invitation, remove your previous entry in the teams_users table.
    old_entry = TeamsUser.find(:first, :conditions => ['user_id = ? and team_id = ?', student.user_id, params[:team_id]])
    if old_entry != nil
      old_entry.destroy
    end
    
    #if you are on a team and you accept another invitation and if your old team does not have any members, delete the entry for the team
    other_members = TeamsUser.find(:all, :conditions => ['team_id = ?', params[:team_id]])
    if other_members.nil? || other_members.length == 0
      old_team = AssignmentTeam.find(:first, :conditions => ['id = ?', params[:team_id]])
      if old_team != nil
        old_team.destroy
      end

      #if a signup sheet exists then release all the topics selected by this team into the pool.
      old_teams_signups = SignedUpUser.find_all_by_creator_id(params[:team_id])
      if !old_teams_signups.nil?
        for old_teams_signup in old_teams_signups
          if old_teams_signup.is_waitlisted == false # i.e., if the old team was occupying a slot, & thus is releasing a slot ...
            first_waitlisted_signup = SignedUpUser.find_by_topic_id_and_is_waitlisted(old_teams_signup.topic_id, true)
            if !first_waitlisted_signup.nil?
              #As this user is going to be allocated a confirmed topic, all of his waitlisted topic signups should be purged
              first_waitlisted_signup.is_waitlisted = false
              first_waitlisted_signup.save

              #Also update the participant table. But first_waitlisted_signup.creator_id is the team id
              #so find one of the users on the team because the update_topic_id function in participant
              #will take care of updating all the participants on the team
              user_id = TeamsUser.find(:first, :conditions => {:team_id => first_waitlisted_signup.creator_id}).user_id
              participant = Participant.find_by_user_id_and_parent_id(user_id,old_team.assignment.id)
              participant.update_topic_id(old_teams_signup.topic_id)
               
              SignUpTopic.cancel_all_waitlists(first_waitlisted_signup.creator_id, SignUpTopic.find(old_teams_signup.topic_id)['assignment_id'])
            end # if !first_waitlisted_signup.nil
            # Remove the now-empty team from the slot it is occupying.
          end # if old_teams_signup.is_waitlisted == false
          old_teams_signup.destroy
        end
    end
 end

After Refactor

def accept
  @inv = Invitation.find(params[:inv_id])
  @inv.reply_status = 'A'
  @inv.save

  invited_user_id = Participant.find(params[:student_id]).user_id
    
  #Remove the users previous team since they are accepting an invite for possibly a new team.
  TeamsUser.remove_team(invited_user_id, params[:team_id])
  #Accept the invite and return boolean on whether the add was successful
  add_successful = Invitation.accept_invite(params[:team_id], @inv.from_id, @inv.to_id)
  #If add wasn't successful because team was full display message
  unless add_successful
    flash[:error]= "The team already has the maximum number of members."
  end

  redirect_to :controller => 'student_team', :action => 'view', :id => Participant.find(params[:student_id]).id
end

Bug Fixes

Invite Requests

Problem Description

When a particular user responds to a advertisement sent out by another user (advertiser) a new Request invitation is created which is displayed in the advertiser's received request section of the team page as shown below:


When the advertiser clicks on the invite the corresponding received request should be removed from the received requests list and the request sender should see the status as accepted when he logs in again as shown below:

Complete steps to reproduce the bug fix

1. Login as admin with password"pass"
2. Create new course and an assignment within the course
    - Set all the properties appropriately
    - Set the assignment as a team assignment
    - Set the deadlines properly.
3. Add 2 participants(user1 and user2) to the assignment(users need to be created who act as participants)
4. Create a topic within the assignment
5. Login as user1 and select the topic in the signup sheet
6. Create an advertisement in the your team link of the assignments
7. Now login as user2 and click on signup sheet. You should see an advertisement sent out by user1.
8. Click on the advertise link and request an invitation.
9. Login as user1 again and go to the your team link.
10. User1 should see the the invitation request in the received requests with the invite and decline button next to it.
11. When user1 clicks on the invite button the request invitation should be removed and when user2 logs in again he should see the sent request as accepted.

Code changes done to fix the bug

A new method was introduced to update the "status" field of the join_table_requests .The method is a follows:

def update_join_team_request
    old_entry = JoinTeamRequest.first(:conditions => ['participant_id =? and team_id =?', params[:participant_id],params[:team_id]])
    if old_entry
       old_entry.update_attribute("status",'A')
    end
end

Error Messages

Problem Description

We working on bug fixes and refactoring we noticed that some of the error messages which were not bring displayed. The issue was that the create method was setting these error messages using notice which was not handled by the flash messages partial used throughout the application. We fixed this so that error message are now being displayed properly as shown below:

Code Changes

  1. The flash[:notice] is changed to flash[:note] in the invitation_controller.rb file
  2. The _flash_messages.html.erb file is updated to include :notice which may be used in some other parts of the application.

Future Work

References

<references/>