CSC/ECE 517 Spring 2013/OSS E608

From Expertiza_Wiki
Jump to navigation Jump to search

Write-up of This Topic.

E608. Refactoring and Testing - Course Related Classes

Classes Involved:

  • models/course.rb (76 lines)
  • models/course_participant.rb (94 lines)
  • models/course_team.rb (116 lines)
  • models/course_node.rb (95 lines)

What it does: Involves creation and management of courses in Expertiza

What needs to be done:

  • It is not clear what a method named 'create_node' is doing in the 'course' class. Refactor by renaming this method appropriately.
  • In the course_team class the import method seems to be doing too many things. See if this method can be broken down into smaller methods with specific functionality.
  • The 'get' method in course_node.rb appears to be doing a lot. Refactor this method by breaking it up into smaller methods.
  • Look for any unused methods or variables in these files.
  • Also apply other refactorings such as Rename variable, Rename method to give the variables and methods more meaningful names.
  • Write unit tests for all the methods in each of the listed model classes.

Changes for models/course.rb

The following changes were made:

  • Since the method create_node simply creates a course node associated with the newly created or copied course, we replaced its functionality by adding the line CourseNode.create(:node_object_id => course.id) in the course_controller's create method and copy method. The function of getting the CourseNode's parent id is extracted as a class method of CourseNode model, since this parent id is independent of the course. With all of its functionalities refactored to other places, this method is hereby removed.
  • Added the line "has_many :assigments, :dependent => :destroy" to allow CourseTest.test_destroy unit test to pass.

Changes for models/course_participant.rb

  • CourseParticipant.get_parent_name is removed
    • This method is never called in the project, plus method CourseParticipant.get_course_string provides the same functionality as CourseParticipant.get_parent_name. So we deleted this method following the principle of DRY.

Changes for models/course_team.rb

File Location: <Project Directory>/app/models/course_team.rb

import Method

Functionality: import a single course team for a course from an input array. The first element in the array optionally used to specify a name for the team that’s being imported.

Usage: the method is used by the import file controller to import course teams to a course.

Decomposition

The import method can be decompose into 5 separate parts

Part 1 – Preparation

Functionality: ensure that the array has enough elements also that the class that the use wishes to import the team to exist.

if row.length < 2
    raise ArgumentError, "Not enough items" 
end 
course = Course.find(id)
if course == nil
    raise ImportError, "The course with id \""+id.to_s+"\" was not found. <a href='/assignment/new'>Create</a> this assignment?"
end

Part 2 – Naming the New Team

Functionality: depending on the option has_column_name the method will either import the team name from the array if has_column_name is set to true or generate a new team name if has_column_name is set to false.

if options[:has_column_names] == "true"
    name = row[0].to_s.strip
    index = 1
else
    name = generate_team_name()
    index = 0
end 

Part 3 – Handling Duplicate Team

Functionality: if a team with the same team name exist this part of the method will handle the conflict depending on the option handle_duplicate.

currTeam = CourseTeam.find(:first, :conditions => ["name =? and parent_id =?",name,course.id])  
if options[:handle_dups] == "ignore" && currTeam != nil
    return
end  
if currTeam != nil && options[:handle_dups] == "rename"
    name = generate_team_name()
    currTeam = nil
end
if options[:handle_dups] == "replace" && teams.first != nil        
for teamsuser in TeamsUser.find(:all, :conditions => ["team_id =?", currTeam.id])
    teamsuser.destroy
    end    
    currTeam.destroy
    currTeam = nil
end     
   

Part 4 – Create the New Team and Node

Functionality: creates a new team and team node base on the information gather by the above parts.

if currTeam == nil
    currTeam = CourseTeam.new
    currTeam.name = name
    currTeam.parent_id = course.id
    currTeam.save
    parent = CourseNode.find_by_node_object_id(course.id)
    TeamNode.create(:parent_id => parent.id, :node_object_id => currTeam.id)
end

Part 5 – Import the Participants to the New Team

Functionality: verify that the users specified in the array exist than add the users to the newly created team.

while(index < row.length) 
    user = User.find_by_name(row[index].to_s.strip)
    if user == nil
        raise ImportError, "The user \""+row[index].to_s.strip+"\" was not found. <a href='/users/new'>Create</a> this user?"                           
    elsif currTeam != nil         
        currUser = TeamsUser.find(:first, :conditions => ["team_id =? and user_id =?", currTeam.id,user.id])          
        if currUser == nil
            currTeam.add_member(user)            
        end                      
    end
    index = index+1      
end

Refactor

At the first glance from the decomposition we can clearly see that part 5 can be separated into its own method

 def import_participants(starting_index, row)
   index = starting_index
   while(index < row.length)
     user = User.find_by_name(row[index].to_s.strip)
     if user.nil?
       raise ImportError, "The user \""+row[index].to_s.strip+"\" was not found. <a href='/users/new'>Create</a> this user?"
     else
       if TeamsUser.find(:first, :conditions => ["team_id =? and user_id =?", id, user.id]).nil?
         add_member(user)
       end
     end
     index = index + 1
   end
 end

From the decomposition we notice that the logic of part 2 and part 3 can be simplified. Originally, part 4 gets executed regardless if the team name is generated or imported. However, if the team name is generated instead of imported there will not be another team with the same name. Therefore, we extracted part 4 into its own method and modified the logic of part 3.

 def self.handle_duplicate(name, course_id, handle_dups)
   team = find(:first, :conditions => ["name =? and parent_id =?", name, course_id])
   if team.nil? #no duplicate
     return name
   end
   if handle_dups == "ignore" #ignore: not create the new team
     return nil
   end
   if handle_dups == "rename" #rename: rename new team
     return generate_team_name
   end
   if handle_dups == "replace" #replace: delete old team
     team.delete
     return name
   end
 end

after modification part 3 and 4 in the import method becomes

   if options[:has_column_names] == "true"
     name = row[0].to_s.strip
     name = handle_duplicate(name, course_id, options[:handle_dups])
     index = 1
   else
     name = generate_team_name
     index = 0
   end

Miscellaneous Changes

Check in part 1 is wrong because number of element can be less than 2 if the array does not have a team name column.

if (row.length < 2 and options[:has_column_names] == "true") or (row.length < 1 and options[:has_column_names] != "true")
  raise ArgumentError, "Not enough items"
end

Part 4 of the method was simplified to improve the readability.

team = CourseTeam.create(:name => name, :parent_id => course_id)
course_node = CourseNode.find_by_node_object_id(course_id)
TeamNode.create(:parent_id => course_node.id, :node_object_id => team.id)

Other Issues

The input variable session was unused in the method therefore it should be removed, however due to the way that the method is being called in the import file controller this change was not made in this project.

export Method

Functionality Bug

Originally the export method in the CourseTeam class export the assignment teams that are associated with the course that a course team belongs. This is not the behavior that one would expect from the export method in the CourseTeam class.

Upon examining the AssignmentTeam class and confirming with the professor we discover that the export method for CourseTeam should export all of the course teams associated with a course.

def self.export(csv, parent_id, options)
   currentCourse = Course.find(parent_id)
   currentCourse.teams.each { |team|
     tcsv = Array.new
     teamUsers = Array.new
     tcsv.push(team.name)
     if (options["team_name"] == "false")
       teamMembers = TeamsUser.find(:all, :conditions => ['team_id = ?', team.id])
       teamMembers.each do |user|
         teamUsers.push(user.name)
         teamUsers.push(" ")
       end
       tcsv.push(teamUsers)
     end
     tcsv.push(currentCourse.name)
     csv << tcsv
   }
 end

Note that this is a director port from the source of export function in the AssignmentTeam class

Analysis and Refactor

The part of the export method within the if block is use to export all of the participants of a team this part can be separated an turn into its own method to reduce the complexity of the export method

 def export_participants
   userNames = Array.new
   participants = TeamsUser.find(:all, :conditions => ['team_id = ?', self.id])
   participants.each do |participant|
     userNames.push(participant.name)
     userNames.push(" ")
   end
   return userNames
 end

Furthermore, an instance method can be added to the CourseTeam class to export a single course team and the class method export can call the instance method to reduce the complexity and improve the readability.

 def export(team_name_only)
   output = Array.new
   output.push(self.name)
   if team_name_only == "false"
     output.push(self.export_participants)
   end
   course = Course.find(self.parent_id)
   output.push(course.name)
   return output
 end

The export function after refactoring

 def self.export(csv, parent_id, options)
   course = Course.find(parent_id)
   if course.nil?
     raise ImportError, "The course with id \""+course_id.to_s+"\" was not found. <a href='/assignment/new'>Create</a> this course?"
   end
   teams = CourseTeam.find_all_by_parent_id(parent_id)
   teams.each do |team|
     csv << team.export(options[:team_name])
   end
 end

Other Issues

the method name export does not accurately describe the functionality of this method. We suggest that the name should be change to export_all. This change was not made due to the export method in the AssignmentTeam class and the way that the method is being called by the export file controller.

The meaning of the option team_name in the method is ambiguous. Upon further examination we discover that the option team_name control if the method should only export team names without exporting the name of the participants in the team. This option should be renamed to team_name_only for better clarity.

get_export_fields Method

Inferring from the code we think that get_export_field method supposed to returns an array that contains the options that used for controlling the export method.

As we discussed earlier the team_name option for expert method controls if the export should contain the name of the team participants.

Originally the get_export_fields method will include the string “team member” if the option team_name is true. This is the opposite of what we expected if our interpretation of the option team_name is correct.

Further more the get_export_fields method is trying to get the value of string(“team_name”) in hash variable options instead of symbol(:team_name).

After consulting with the professor we confirm that our suspicion is right and made the change accordingly

 def self.get_export_fields(options)
   fields = Array.new
   fields.push("Team Name")
   if (options[:team_name] == "false")
     fields.push("Team members")
   end
   fields.push("Course Name")
 end

copy Method

From reading the code we inferred that this method is used to create a new assignment team and copy all of the participants of the course team to that new assignment team.

If this is truly the intended functionality of this method than this method should belongs to AssignmentTeam class instead of CourseTeam class.

This method calls the create_node_object method in Team class which does not work (explained in the later section) and has ambiguous return type, functionality and name.

Logically the copy method should create a new course team and copy the participants of a team into the new course team. Currently this is dead code and not used in the program.

This function was marked deprecated and we suggest that the function should be removed from the program.

add_participant Method

From reading the code we inferred that this method is used to add a participant to the course that the course team belongs to.

This method is called by the add_member method in the Team class to add a participant to the course when a member is added to a course team.

This method does belong in the Course class and not the CourseTeam class. The Course class already has the method add_participant with similar functionality.

This function was marked deprecated and we suggest that the function should be removed from the program and the add_member method in the Team class should call the add_participant method from Course class.

Changes for models/course_node.rb

  • models/course_node.rb
    • The "get" method was improved.
      • Added "get_courses_managed_by_user" method to hold logic extracted from the "get"method.
      • Added "get_course_query_conditions" method to hold logic extracted from the "get" method.
      • Modified any checks on if the user is a teaching assistant to use polymorphism to make the determination.
      • Removed the initial conditional for setting the "conditions" value since the remaining conditions was overwriting the value regardless of how the initial conditional was evaluated.
      • Renaming the "get" method was considered; however, the name of the method is necessary for polymorphism reasons related to Assignment nodes. As such, a comment was added to explain why the method name should remain.
      • The "sortorder" parameter is given a default value of "ASC" for ascending. This allowed for a related conditional to be removed from the method.
      • The "sortvar" parameter is given a default value of "name" for sorting by course name. This allowed for a related conditional to be removed from the method.
    • Renaming the "get_children" method was considered; however, the name of the method is necessary for polymorphism reasons related to Assignment Nodes. As such, a comment was added to explain why the method name should remain.
    • The "get_survey_distribution_id" method was removed after finding it to be dead code.
  • models/ta.rb
    • Added "is_teaching_assistant?" method to override the inherited method from the User model. This supported usage of polymorphism to determine if the user is a teaching assistant.
  • models/user.rb
    • Added "is_teaching_assistant?" method to enable usage of polymorphism to determine if the user is a teaching assistant.

Methods Found That Are Not Currently Being Used

The following methods were not found to be used currently by the project, but have been left in the code for potential future usages of the functionality:

  • models/course.rb
    • Course.get_participant(user_id)
  • models/course_participant.rb
    • CourseParticipant.copy(assignment_id)
    • CourseParticipant.get_parent_name
  • models/course_node.rb
    • CourseNode.get_modified_date

Miscellaneous Changes for Quality Improvement and Readability

Other Observations and Suggestion for Future Work

Redundant Code in Team Related Classes

Classes: Team, CourseTeam, AssignmentTeam

Files: team.rb, course_team.rb, assignment_team.rb

The class CourseTeam and AssignmentTeam both inherits from the class Team. However that are a lot of duplicated functionalities in order to adhere to the "DRY" principle refactor for the CourseTeam mentioned in this document should be applied to AssignmentTeam and duplicated methods should be extracted to the super-class Team.

Naming Inconsistency in Team Related Classes

Classes: Team, CourseTeam, AssignmentTeam

Files: team.rb, course_team.rb, assignment_team.rb

Participant in a team have many different names in these three classes. For example: member, user, participants. Common vocabulary should be established in-order to eliminate confusion.

Bugs in Team Class

Class: Team

File: team.rb

Method: create_node_object

The create_node_object method in Team class does not work because the class method get_parent_model in both CourseTeam and Assignment team is an instance method not a class method.

From the code of the object and the usage of this code we inferred that the method supposed to create a new course or assignment team and a new team node and return the newly created team as output. This function should be deprecated and removed from the class, the functionality should be move to the new method in the team controller if the functionality is truly necessary.

Inelegance Code in Team Class

Class: Team

File: team.rb

Methods: randomize_all_by_parent, check_for_existing, create_node_object, copy_members

These methods uses Object.const_get() with a input string that dynamically locate the object that are associated with different team types (assignment or course) however the use of Object.const_get() are inconsistent throughout these methods.

We suggests instead of using query methods such as get_participant_type and get_parent_model in the assignment and course team return string containing the name of the object we can use these method to return the object itself thus eliminating the need for Object.const_get().

List of New Unit Tests and Testcases

The following lists shows new unit files and/or new testcases:

  • test/unit/course_node_test.rb (new test file)
    • test_get_children_with_show_true_and_non_TA_and_ascending_order_by_name
    • test_get_children_with_show_true_and_non_TA_and_descending_order_by_name
    • test_get_children_with_show_true_and_TA_and_ascending_order_by_name
    • test_get_children_with_show_true_and_TA_and_descending_order_by_name
    • test_get_children_with_show_false_and_non_TA_and_ascending_order_by_name
    • test_get_children_with_show_false_and_non_TA_and_descending_order_by_name
    • test_get_children_with_show_false_and_TA_and_ascending_order_by_name
    • test_get_children_with_show_false_and_TA_and_descending_order_by_name
    • test_get_children_with_show_false_and_non_TA_and_nil_order_by_nil
    • test_get_name
    • test_get_directory
    • test_get_creation_date
    • test_get_modified_date
    • test_get_teams
    • test_get_parent_id
  • test/unit/course_test.rb (previously existing test file)
    • test_get_teams
    • test_get_path
    • test_get_participants
    • test_get_participant
    • test_add_participant
    • test_copy_participants
  • test/unit/course_participant_test.rb (previously existing test file)
    • test_get_course_string
    • test_import
    • test_get_path
    • test_export
    • test_get_export_field
  • test/unit/course_team_test.rb (new test file)
    • test_retrieval
    • test_import_participants
    • test_export_participants
    • test_instance_export
    • test_handle_duplicate
    • test_import
    • test_class_export

List of Modified Unit Tests

The following unit test files and/or testcases were modified:

  • test/unit/course_test.rb
    • The line "fixtures :courses" was changed to "fixtures :courses,:teams,:users,:participants,:assignments,:nodes,:tree_folders".