CSC/ECE 517 Fall 2013/oss cmh

From Expertiza_Wiki
Jump to navigation Jump to search

Introduction

What the invitation controller does and the process advertising, creating invites and accepting invites.

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 “Skin 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.

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.

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

The accept method within the controller had too much functionality. We first moved much of the functionality within the accept method to a model accept method which purpose is to make sure that all items that need to be taken care of upon a method accept are in fact handled. Just moving this code to the model did not solve the issue of complexity. We created smaller methods to handle individual changes that needed to be taken care of for example upon an accept the users topic must be updated, the topics they are waitlisted for must be dropped, amongst many other things. We took these smaller changes that needed to be made and moved them into separate methods amongst various models like signed_up_user.rb and teams_user.rb. We also noticed that some variable names did not do a good job of describing their focus so we changed these variable names to increase readability. After our refactoring the accept method is much smaller and much more readable.<ref> Coding Conventions "Ruby On Rails coding conventions, standards and best practices" </ref>


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 signupsheet.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

Some of the error messages which were not getting displayed are now getting displayed properly as shown below:

Code Changes

1)The flash[:notice] is changes 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/>