CSC/ECE 517 Fall 2017/E1746 Refactor user.rb: Difference between revisions

From Expertiza_Wiki
Jump to navigation Jump to search
No edit summary
 
(17 intermediate revisions by the same user not shown)
Line 9: Line 9:
[http://expertiza.ncsu.edu/ Expertiza] is an open source project based on [http://rubyonrails.org/ Ruby on Rails] framework. Expertiza allows the instructor to create new assignments and customize new or existing assignments. It also allows the instructor to create a list of topics the students can sign up for. Students can form teams in Expertiza to work on various projects and assignments. Students can also peer review other students' submissions. Expertiza supports submission across various document types, including the URLs and wiki pages.
[http://expertiza.ncsu.edu/ Expertiza] is an open source project based on [http://rubyonrails.org/ Ruby on Rails] framework. Expertiza allows the instructor to create new assignments and customize new or existing assignments. It also allows the instructor to create a list of topics the students can sign up for. Students can form teams in Expertiza to work on various projects and assignments. Students can also peer review other students' submissions. Expertiza supports submission across various document types, including the URLs and wiki pages.


===Problem Statement===
===Test Plan===
The following tasks were accomplished in this project:
The following tasks were accomplished in this project:


*Complete the pending tests in user_spec.rb
*Complete the pending tests in user_spec.rb
*Refactor get_user_list method
::There are many pending tests needed to be finished. And for the methods which are needed to be refactored or renamed, this is also can been seen as writing failing tests first.
*Refactor get_user_list method.
::The get_user_list methods contain many conditions and each of them has a lot of code which makes the method very long. The factor is to transfer the statements to the corresponding subclasses ,and writing method calls instead.
*Refactor self.search_users method
*Refactor self.search_users method
*Rename method and change all other places it is used
::The method has many repetitive codes which are used in the conditionals. We refactor it by writing a single method call and let the method call to choose the conditional.
*Use find_by instead of dynamic method
*Rename methods and change all other places it is used.
*Use find_by instead of dynamic method.
::The find_by_name or find_by_email method which are used in the user.rb is like old style code. And this kind of code can be replaced with new dynamic ones like, find_by.


===Current Implementation===
===Missions and Solutions===


=====Missions and Solutions=====
====Mission 1:Complete the pending tests in user_spec.rb====
 
* '''Mission 1''':Complete the pending tests in user_spec.rb
::There are many pending tests in the file 'user.rb'. What we have done is finishing these pending tests and makes them pass.The user_spec.rb file on github is below.
::There are many pending tests in the file 'user.rb'. What we have done is finishing these pending tests and makes them pass.The user_spec.rb file on github is below.
  include Rails.application.routes.url_helpers
  https://github.com/AnranZhou/expertiza/blob/master/spec/models/user_spec.rb
describe User do
  let(:user) do
    User.new name: 'abc', fullname: 'abc xyz', email: 'abcxyz@gmail.com', password: '12345678', password_confirmation: '12345678',
            email_on_submission: 1, email_on_review: 1, email_on_review_of_review: 0, copy_of_emails: 1, handle: 'handle'
  end
  let(:user1) { User.new name: 'abc', fullname: 'abc bbc', email: 'abcbbc@gmail.com', password: '123456789', password_confirmation: '123456789' }
  let(:user2) { User.new name: 'abc', fullname: 'abc bbc', email: 'abcbbe@gmail.com', password: '123456789', password_confirmation: '123456789' }
 
  describe '#name' do
    it 'returns the name of the user' do
      expect(user.name).to eq('abc')
    end
    it 'Validate presence of name which cannot be blank' do
      expect(user).to be_valid
      user.name = '  '
      expect(user).not_to be_valid
    end
    it 'Validate that name is always unique' do
      expect(user1).to validate_uniqueness_of(:name)
    end
  end
 
  describe '#fullname' do
    it 'returns the full name of the user' do
      expect(user.fullname).to eq('abc xyz')
    end
  end
 
  describe '#email' do
    it 'returns the email of the user' do
      expect(user.email).to eq('abcxyz@gmail.com')
    end
 
    it 'Validate presence of email which cannot be blank' do
      user.email = '  '
      expect(user).not_to be_valid
    end
 
    it 'Validate the email format' do
      user.email = 'a@x'
      expect(user).not_to be_valid
    end
 
    it 'Validate the email format' do
      user.email = 'ax.com'
      expect(user).not_to be_valid
    end
 
    it 'Validate the email format' do
      user.email = 'axc'
      expect(user).not_to be_valid
    end
 
    it 'Validate the email format' do
      user.email = '123'
      expect(user).not_to be_valid
    end
 
    it 'Validate the email format correctness' do
      user.email = 'a@x.com'
      expect(user).to be_valid
    end
  end
 
  describe '#salt_first?' do
    it 'will always return true' do
      expect(user.salt_first?).to be true
    end
  end
 
  describe '#get_available_users' do
    before(:each) do
      role = Role.new
    end
    it 'returns the first 10 visible users' do
      all_users = double
      user_mock1 = double(:role=>'Instructor')
      user_mock2 = double(:role=>'Administrator')
      user_mock3 = double(:role=>'Student')
      allow(@role).to receive(:get_parents).and_return(['Instructor','Administrator'])
      allow(User).to receive(:all).and_return([user_mock1,user_mock2,user_mock3])
      allow(all_users).to receive(:select).and_yield(user_mock2).and_yield(user_mock2).and_yield(user_mock3)
      expect(user.get_available_users("abc")).to eq ([user_mock1,user_mock2])
    end
  end
 
  describe '#can_impersonate?' do
    it 'can impersonate target user if current user is super admin' do
      allow(user1).to receive_message_chain("role.super_admin?"){true}
      expect(user1.can_impersonate?(user)).to be true
    end
    it 'can impersonate target user if current user is the TA of target user'do
      allow(user1).to receive_message_chain("role.super_admin?"){false}
      allow(user1).to receive(:is_teaching_assistant_for?).and_return(user)
      expect(user1.can_impersonate?(user)).to be true
 
    end
    it 'can impersonate target user if current user is the recursively parent of target user'do
      allow(user1).to receive_message_chain("role.super_admin?"){true}
      allow(user1).to receive(:is_recursively_parent_of).and_return(user)
      expect(user1.can_impersonate?(user)).to be true
    end
    it 'cannot impersonate target user if current user does not satisfy all requirements'do
      allow(user1).to receive_message_chain("role.super_admin?"){false}
      allow(user1).to receive_message_chain("role.ta?"){false}
      expect(user1.can_impersonate?(user)).to be false
    end
  end
 
  describe '#is_recursively_parent_of' do
    context 'when the parent of target user (user) is nil' do
      it 'returns false' do
        allow(user).to receive(:parent).and_return(nil)
        expect(user1.is_recursively_parent_of(user)).to eq false
      end
    end
 
    context 'when the parent of target user (user) is current user (user1)' do
      it 'returns true' do
        allow(user).to receive(:parent).and_return(user1)
        expect(user1.is_recursively_parent_of(user)).to eq true
      end
    end
 
    context 'when the parent of target user (user) is not current user (user1), but super admin (user2)' do
      it 'returns false' do
        allow(user).to receive(:parent).and_return(user2)
        allow(user2).to receive_message_chain("role.super_admin?") { true }
        expect(user1.is_recursively_parent_of(user)).to eq false
      end
    end
  end


  describe '#get_user_list' do
====Mission 2:Refactor get_user_list method====
::The task is to write failing test first and ,move if conditions to corresponding subclasses (eg. ta.rb, instructor.rb) with same method name,then replace the conditional with the relevant method calls.
::First writing the failing test first according to the requirements of the refactor.
<pre>
#describe '#get_user_list' do
     context 'when current user is super admin' do
     context 'when current user is super admin' do
       it 'fetches all users' do
       it 'fetches all users' do
        user_list=double.as_null_object
         allow(user).to receive_message_chain("role.super_admin?"){ true }
         allow(user).to receive_message_chain("role.super_admin?"){ true }
        allow(User).to receive_message_chain("all.find_each").and_yield(user1).and_yield(user2)
         allow(user).to receive_message_chain("role.instructor?"){ false }
         allow(user).to receive_message_chain("role.instructor?"){ false }
         allow(user).to receive_message_chain("role.ta?"){false}
         allow(user).to receive_message_chain("role.ta?"){false}
         User.all.find_each do |user|
         allow(SuperAdministrator).to receive(:get_user_list).and_return([user1,user2])
          user_list<<user
         expect(user.get_user_list).to eq ([user1,user2])
        end
         user.get_user_list
        end
       end
       end
 
  end
     context 'when current user is an instructor' do
     context 'when current user is an instructor' do
      before(:each) do
        course = Course.new
        assignment = Assignment.new
      end
       it 'fetches all users in his/her course/assignment' do
       it 'fetches all users in his/her course/assignment' do
        user_list = double
        course = double
        assignment = double
         allow(user).to receive_message_chain("role.super_admin?"){ false }
         allow(user).to receive_message_chain("role.super_admin?"){ false }
         allow(user).to receive_message_chain("role.instructor?"){ true }
         allow(user).to receive_message_chain("role.instructor?"){ true }
        allow(Course).to receive_message_chain(:where,:find_each).and_yield(course)
        allow(course).to receive(:get_participants).and_return(user1)
        allow(Assignment).to receive_message_chain(:where,:find_each).and_yield(assignment)
        allow(assignment).to receive(:participants).and_return(user2)
        allow_any_instance_of(User).to receive(:empty?).and_return(false)
        allow_any_instance_of(User).to receive(:each).and_yield(user)
        allow_any_instance_of(User).to receive(:user).and_return(user)
        allow_any_instance_of(User).to receive_message_chain(:role,:hasAllPrivilegesOf).and_return(true)
         allow(user).to receive_message_chain("role.ta?"){false}
         allow(user).to receive_message_chain("role.ta?"){false}
         expect(user.get_user_list).to eq ([user])
        allow(Instructor).to receive(:get_user_list).and_return([user1,user2])
         expect(user.get_user_list).to eq ([user1,user2])
       end
       end
     end
     end


     context 'when current user is a TA' do
     context 'when current user is a TA' do
      before(:each) do
        course = Course.new
        end
       it 'fetches all users in his/her courses'do
       it 'fetches all users in his/her courses'do
        courses=double
        course=double
         allow(user).to receive_message_chain("role.super_admin?"){ false }
         allow(user).to receive_message_chain("role.super_admin?"){ false }
         allow(user).to receive_message_chain("role.ta?"){ true }
         allow(user).to receive_message_chain("role.ta?"){ true }
         allow(user).to receive_message_chain("role.instructor?"){ false }
         allow(user).to receive_message_chain("role.instructor?"){ false }
         allow(Ta).to receive(:get_mapped_courses).and_return(courses)
         allow(Ta).to receive(:get_user_list).and_return([user1,user2])
        allow(courses).to receive(:each).and_yield(course)
         expect(user.get_user_list).to eq ([user1,user2])
        allow(Course).to receive(:find).and_return(course)
        allow(course).to receive(:get_participants).and_return(user1)
        allow_any_instance_of(User).to receive(:empty?).and_return(false)
        allow_any_instance_of(User).to receive(:each).and_yield(user)
        allow_any_instance_of(User).to receive(:user).and_return(user)
        allow_any_instance_of(User).to receive_message_chain(:role,:hasAllPrivilegesOf).and_return(true)
         expect(user.get_user_list).to eq ([user])
     end
     end
     end
     end
   end
   end
 
</pre>
  describe '#super_admin?' do
::The next step is to move if conditionals to its relatively subclasses. In this case,the superadministrator.rb, instructor.rb and ta.rb.
    it 'returns ture if role name is Super-Administrator' do
<pre>
      allow(user).to receive_message_chain("role.name"){'Super-Administrator'}
#class SuperAdministrator < User
      expect(user.super_admin?).to be_truthy
  def self.get_user_list
    end
     user_list=[]
 
    User.all.find_each do |user|
     it 'returns false if role name is not Super-Administrator' do
    user_list << user
      allow(user).to receive_message_chain("role.name"){'CAt'}
      expect(user.super_admin?).to be_falsey
     end
     end
   end
   end
 
end
   describe '#is_creator_of?' do
</pre>
     it 'returns true of current user (user) is the creator of target user (user1)' do
<pre>
      allow(user1).to receive(:creator).and_return(user)
   #def self.get_user_list
       expect(user.is_creator_of?(user1)).to be true
    user_list=[]
     participants = []
    Course.where(instructor_id: self.id).find_each do |course|
       participants << course.get_participants
     end
     end
 
     Assignment.where(instructor_id: self.id).find_each do |assignment|
     it 'returns false of current user (user) is not the creator of target user (user1)' do
       participants << assignment.participants
      allow(user1).to receive(:creator).and_return(user2)
      expect(user.is_creator_of?(user1)).to be false
       expect(user2.is_creator_of?(user1)).to be true
 
     end
     end
  end
    participants.each do |p_s|
 
      next if p_s.empty?
  describe '.import' do
       p_s.each do |p|
    it 'raises error if import column does not equal to 3' do
        user_list << p.user if self.role.hasAllPrivilegesOf(p.user.role)
       row = ["abc","abc xyz"]
      end
      _row_header = double
      seesion = {:user=>user}
      _id = double
      expect { User.import(row, _row_header,seesion,_id) }.to raise_error("Not enough items: expect 3 columns: your login name, your full name (first and last name, not seperated with the delimiter), and your email.")
     end
     end
    it 'updates an existing user with info from impor file' do
end
      row = ["abc","abc xyz","abcxyz@gamil.com"]
</pre>
      _row_header = double
<pre>
      seesion = {:user=>user}
#def self.get_user_list
      _id = double
   get_user_list=[]
      allow(User).to receive(:find_by_name).and_return(user)
   courses.each do |course_id|
      allow_any_instance_of(User).to receive(:nil?).and_return(false)
       course = Course.find(course_id)
      allow_any_instance_of(User).to receive(:id).and_return(1)
       participants << course.get_participants
      expect_any_instance_of(User).to receive(:save)
      User.import(row,_row_header,seesion,_id)
    end
 
   end
 
   describe '.yesorno' do
    it 'returns yes when input is true' do
       expect(User.yesorno(true)).to eq "yes"
    end
    it 'returns no when input is false' do
      expect(User.yesorno(false)).to eq "no"
    end
    it 'returns empty string when input is other content' do
      content = "TEXT"
      expect(User.yesorno(content)).to eq ""
    end
  end
 
  describe '.find_by_login' do
    context 'when user\'s email is stored in DB' do
       it 'finds user by email' do
        email = 'abcxyz@gmail.com'
        allow(User).to receive(:find_by_email).with(email).and_return(user)
        expect(User.find_by_login(email)).to eq user
      end
     end
     end
 
     participants.each do |p_s|
     context 'when user\'s email is not stored in DB' do
       next if p_s.empty?
       it 'finds user by email if the local part of email is the same as username' do
      p_s.each do |p|
        allow(User).to receive(:find_by_email).and_return(nil)
         user_list << p.user if self.role.hasAllPrivilegesOf(p.user.role)
         allow(User).to receive(:where).and_return([{name: 'abc', fullname: 'abc bbc'}])
        expect(User.find_by_login('abcxyz@gmail.com')).to eq ({:name=>"abc", :fullname=>"abc bbc"})
       end
       end
     end
     end
  end
end
 
</pre>
  describe '#get_instructor' do
::Then replace the conditional with the relevant method calls.
    it 'gets the instructor id' do
<pre>
      allow(user).to receive(:id).and_return(123)
#def get_user_list
      expect(user.get_instructor).to eq(123)
    user_list = []
      end
    # If the user is a super admin, fetch all users
  end
    if self.role.super_admin?
 
      return SuperAdministrator.get_user_list
  describe '#instructor_id' do
    it 'returns id when role of current user is a super admin' do
      allow(user).to receive_message_chain(:role,:name).and_return('Super-Administrator')
      allow(user).to receive(:id).and_return(1)
      expect(user.instructor_id).to eq(1)
     end
     end


    it 'returns id when role of current user is an Administrator' do
      allow(user).to receive_message_chain(:role,:name).and_return('Administrator')
      allow(user).to receive(:id).and_return(2)
      expect(user.instructor_id).to eq(2)
    end


     it 'returns id when role of current user is an Instructor' do
     # If the user is an instructor, fetch all users in his course/assignment
      allow(user).to receive_message_chain(:role,:name).and_return('Instructor')
    if self.role.instructor?
       allow(user).to receive(:id).and_return(3)
       return Instructor.get_user_list
      expect(user.instructor_id).to eq(3)
     end
     end


     it 'returns instructor_id when role of current user is a TA' do
     # If the user is a TA, fetch all users in his courses
      allow(user).to receive_message_chain(:role,:name).and_return('Teaching Assistant')
    if self.role.ta?
       allow(Ta).to receive(:get_my_instructor).and_return(4)
       return Ta.get_user_list
      expect(user.instructor_id).to eq(4)
     end
     end


     it 'raise an error when role of current user is other type' do
     # Add the children to the list
       allow(user).to receive_message_chain(:role,:name).and_return('abc')
    unless self.role.super_admin?
       expect{user.instructor_id}.to raise_error(NotImplementedError,"for role abc")
       User.all.find_each do |u|
        if is_recursively_parent_of(u)
          user_list << u unless user_list.include?(u)
        end
       end
     end
     end


    user_list.uniq
   end
   end
</pre>
::So the refactor of get_user_list method has been done.
====Mission 3:Refactor self.search_users method.====
::This task is required to write the failing test first and extract duplicated code to a new method.
::First, write the failing test.
<pre>
#descirbe '.search_users' do


  describe '.export' do
    let(:role) { Role.new }
     before(:each) do
     before(:each) do
      allow(user).to receive_message_chain(:role,:name).and_return('abc')
        allow(User).to receive_message_chain(:order,:where).and_return(user)
      allow(user).to receive_message_chain(:parent,:name).and_return('abc')
      allow(User).to receive(:all).and_return([user])
      allow_any_instance_of(User).to receive(:each).and_yield(user)
     end
     end


     it 'exports all information setting in options' do
     it 'when the search_by is 1' do
       options={"personal_details"=>"true", "role"=>"true","parent"=>"true","email_options"=>"true","handle"=>"true"}
       search_by = "1"
       csv=[]
      user_id = double
       User.export(csv,0 , options)
       letter = 'name'
       expect(csv).to eq([[user.name,user.fullname,user.email,
       search_filter = '%' + letter + '%'
                                                user.role.name,user.parent.name,user.email_on_submission, user.email_on_review,
       expect(User).to receive_message_chain(:order,:where).with("(role_id in (?) or id = ?) and name like ?", role.get_available_roles, user_id, search_filter)
                                                user.email_on_review_of_review, user.copy_of_emails,user.handle]])
      expect(User.search_users(role,user_id,letter,search_by)).to eq user
     end
     end


     it 'exports only personal_details'do
     it 'when the search_by is 2' do
       options={"personal_details"=>"true", "role"=>"false","parent"=>"false","email_options"=>"false","handle"=>"false"}
       search_by = "2"
       csv=[]
      user_id = double
       User.export(csv,0 , options)
      letter = 'fullname'
       expect(csv).to eq([[user.name,user.fullname,user.email]])
       search_filter = '%' + letter + '%'
       expect(User).to receive_message_chain(:order,:where).with("(role_id in (?) or id = ?) and fullname like ?", role.get_available_roles, user_id, search_filter)
       expect(User.search_users(role,user_id,letter,search_by)).to eq user
     end
     end


     it 'exports only current role and parent' do
     it 'when the search_by is 3' do
       options={"personal_details"=>"false", "role"=>"true","parent"=>"true","email_options"=>"false","handle"=>"false"}
       search_by = "3"
       csv=[]
      user_id = double
       User.export(csv,0 , options)
      letter = 'email'
       expect(csv).to eq([[user.role.name,user.parent.name]])
       search_filter = '%' + letter + '%'
       expect(User).to receive_message_chain(:order,:where).with("(role_id in (?) or id = ?) and email like ?", role.get_available_roles, user_id, search_filter)
       expect(User.search_users(role,user_id,letter,search_by)).to eq user
     end
     end


     it 'exports only email_options' do
     it 'when the search_by is default value' do
       options={"personal_details"=>"false", "role"=>"false","parent"=>"false","email_options"=>"true","handle"=>"false"}
       search_by = nil
       csv=[]
      user_id = double
       User.export(csv,0 , options)
       letter = ''
       expect(csv).to eq([[user.email_on_submission, user.email_on_review,user.email_on_review_of_review, user.copy_of_emails]])
       search_filter = letter + '%'
    end
       expect(User).to receive_message_chain(:order,:where).with("(role_id in (?) or id = ?) and name like ?", role.get_available_roles, user_id, search_filter)
 
       expect(User.search_users(role,user_id,letter,search_by)).to eq user
    it 'exports only handle' do
      options={"personal_details"=>"false", "role"=>"false","parent"=>"false","email_options"=>"false","handle"=>"true"}
      csv=[]
       User.export(csv,0 , options)
      expect(csv).to eq([[user.handle]])
     end
     end
   end
   end
 
</pre>
   describe '.export_fields' do
::Then,extract duplicated code to a new method.Since this is kind of changing original method.So below is the changing on github.
    it 'exports all information setting in options' do
<pre>
      options={"personal_details"=>"true","role"=>"true","parent"=>"true","email_options"=>"true","handle"=>"true"}
#   def self.search_users(role, user_id, letter, search_by)
      expect(User.export_fields(options)).to eq(["name","full name","email","role","parent","email on submission","email on review","email on metareview","handle"])
-    if search_by == '1' # search by user name
+    key_word = {'1'=>'name','2'=>'fullname','3'=>'email'}
+    if key_word.include? search_by
        search_filter = '%' + letter + '%'
-      users = User.order('name').where("(role_id in (?) or id = ?) and name like ?", role.get_available_roles, user_id, search_filter)
-    elsif search_by == '2' # search by full name
-      search_filter = '%' + letter + '%'
-      users = User.order('name').where("(role_id in (?) or id = ?) and fullname like ?", role.get_available_roles, user_id, search_filter)
-    elsif search_by == '3' # search by email
-      search_filter = '%' + letter + '%'
-      users = User.order('name').where("(role_id in (?) or id = ?) and email like ?", role.get_available_roles, user_id, search_filter)
+      users = User.order('name').where("(role_id in (?) or id = ?) and #{key_word[search_by]} like ?", role.get_available_roles, user_id, search_filter)
      else # default used when clicking on letters
        search_filter = letter + '%'
        users = User.order('name').where("(role_id in (?) or id = ?) and name like ?", role.get_available_roles, user_id, search_filter)
</pre>
====Mission 4:Rename method and change all other places it is used====
::First, is to write failing test of the renamed method which has been finished in mission one.So just skip to the second part to rename them.
::is_recursively_parent_of → recursively_parent_of?
<pre>
  -      expect(user1.is_recursively_parent_of(user)).to eq false
  +      expect(user1.recursively_parent_of(user)).to eq false
</pre>
::is_creator_of? →  creator_of?
<pre>
-  describe '#is_creator_of?' do
+  describe '#creator_of?' do
      it 'returns true of current user (user) is the creator of target user (user1)' do
        allow(user1).to receive(:creator).and_return(user)
-      expect(user.is_creator_of?(user1)).to be true
+      expect(user.creator_of?(user1)).to be true
      end
 
      it 'returns false of current user (user) is not the creator of target user (user1)' do
        allow(user1).to receive(:creator).and_return(user2)
-      expect(user.is_creator_of?(user1)).to be false
-      expect(user2.is_creator_of?(user1)).to be true
+      expect(user.creator_of?(user1)).to be false
+      expect(user2.creator_of?(user1)).to be true
 
      end
     end
     end
 
@@ -382,16 +382,16 @@
    it 'exports only personal_details' do
       end
      options={"personal_details"=>"true","role"=>"false","parent"=>"false","email_options"=>"false","handle"=>"false"}
       expect(User.export_fields(options)).to eq(["name","full name","email"])
     end
     end
 
</pre>
    it 'exports only current role and parent' do
::is_teaching_assistant_for? →  teaching_assistant_for?
      options={"personal_details"=>"false","role"=>"true","parent"=>"true","email_options"=>"false","handle"=>"false"}
<pre>
       expect(User.export_fields(options)).to eq(["role","parent"])
-  describe '#is_teaching_assistant_for?' do
+  describe '#teaching_assistant_for?' do
      it 'returns false if current user is not a TA' do
        allow(user1).to receive_message_chain("role.ta?"){ false }
-      expect(user1.is_teaching_assistant_for?(user)).to be_falsey
+      expect(user1.teaching_assistant_for?(user)).to be_falsey
      end
 
      it 'returns false if current user is a TA, but target user is not a student'do
        allow(user1).to receive_message_chain("role.ta?"){true }
        allow(user).to receive_message_chain("role.name").and_return('teacher')
-      expect(user1.is_teaching_assistant_for?(user)).to be_falsey
+      expect(user1.teaching_assistant_for?(user)).to be_falsey
      end
 
      it 'returns true if current user is a TA of target user'do
@@ -401,23 +401,23 @@
      c1=Course.new
      allow(user1).to receive_message_chain(:courses_assisted_with,:any?).and_yield(c1)
       allow_any_instance_of(Course).to receive_message_chain(:assignments,:map,:flatten,:map,:include?,:user).and_return(true)
-    expect(user1.is_teaching_assistant_for?(user)).to be true
+    expect(user1.teaching_assistant_for?(user)).to be true
      end
     end
     end
 
 
    it 'exports only email_options' do
      options={"personal_details"=>"false","role"=>"false","parent"=>"false","email_options"=>"true","handle"=>"false"}
</pre>
      expect(User.export_fields(options)).to eq(["email on submission","email on review","email on metareview"])
::is_teaching_assistant? →  teaching_assistant?
<pre>
  -  describe '#is_teaching_assistant?' do
  +  describe '#teaching_assistant?' do
      it 'returns true if current user is a TA' do
        allow(user).to receive_message_chain("role.ta?"){ true }
-      expect(user.is_teaching_assistant?).to be true
+      expect(user.teaching_assistant?).to be true
      end
 
      it 'returns false if current user is not a TA' do
        allow(user).to receive_message_chain("role.ta?"){ false }
-      expect(user.is_teaching_assistant?).to be_falsey
+      expect(user.teaching_assistant?).to be_falsey
      end
     end
     end
</pre>
::After writing the test, it is going to rename the method in the user.rb. Below is the changed part of the code.
<pre>
-    return true if self.is_teaching_assistant_for?(user)
-    return true if self.is_recursively_parent_of(user)
+    return true if self.teaching_assistant_for?(user)
+    return true if self.recursively_parent_of(user)
-    def is_recursively_parent_of(user)
+    def recursively_parent_of(user)
-    self.is_recursively_parent_of(p)
+    self.recursively_parent_of(p)
-    if is_recursively_parent_of(u)
+    if recursively_parent_of(u)
-    def is_creator_of?(user)
+    def creator_of?(user)
-    def is_teaching_assistant_for?(student)
-    return false unless is_teaching_assistant?
+    def teaching_assistant_for?(student)
+    return false unless teaching_assistant?
-    def is_teaching_assistant?
+    def teaching_assistant?
</pre>


    it 'exports only handle' do
====Mission 5:Use find_by instead of dynamic method====
      options={"personal_details"=>"false","role"=>"false","parent"=>"false","email_options"=>"false","handle"=>"true"}
::There are three lines using  find_by_name and find_by_email which are now be instead of find_by[params].The task is to replace the old one to refactor the method.
      expect(User.export_fields(options)).to eq(["handle"])
::First,writing the failing tests,here is the original test code including find_by method.
    end
<pre>
  end
  allow(User).to receive(:find_by_name).and_return(user)
</pre>
And below is the new failing test
<pre>
  allow(User).to receive(:find_by).and_return(user)
</pre>


  describe '.from_params' do
And so as the other codes contain find_by method
    it 'returns user by user_id fetching from params' do
<pre>
      params = {
-      allow(User).to receive(:find_by).with(email).and_return(user)
        :user_id => 1,
+      allow(User).to receive(:find_by).and_return(user)
      }
-      allow(User).to receive(:find_by_email).and_return(nil)
      allow(User).to receive(:find).and_return(user)
+      allow(User).to receive(:find_by).and_return(nil)
      expect(User.from_params(params)).to eq user
-      allow(User).to receive(:find_by_name).and_return(user)
    end
+      allow(User).to receive(:find_by).and_return(user)
    it 'returns user by user name fetching from params' do
</pre>
      params = {
And then change the find_by_name and find_by_name in the user.rb file in to dynamic one.
        :user => {
<pre>
          :name => 'abc'
-      user = User.find_by_name(row[0])
        }
+      user = User.find_by name: row[0]
      }
-      user = User.find_by_email(login)
      allow(User).to receive(:find_by_name).and_return(user)
+      user = User.find_by email: login
      expect(User.from_params(params)).to eq user
-      User.find_by_name(params[:user][:name])
    end
+      User.find_by name: params[:user][:name]
    it 'raises an error when Expertiza cannot find user' do
</pre>
      params = {
        :user => {
          :name => 'ncsu'
        }
      }
      allow(user).to receive(:nil?).and_return(true)
      expect {User.from_params(params)}.to raise_error("Please <a href='http://localhost:3000/users/new'>create an account</a> for this user to continue.")
    end
  end
 
  describe '#is_teaching_assistant_for?' do
    it 'returns false if current user is not a TA' do
      allow(user1).to receive_message_chain("role.ta?"){ false }
      expect(user1.is_teaching_assistant_for?(user)).to be_falsey
    end
 
    it 'returns false if current user is a TA, but target user is not a student'do
      allow(user1).to receive_message_chain("role.ta?"){true }
      allow(user).to receive_message_chain("role.name").and_return('teacher')
      expect(user1.is_teaching_assistant_for?(user)).to be_falsey
    end
 
    it 'returns true if current user is a TA of target user'do
    allow(Ta).to receive(:find).and_return(user1)
    allow(user1).to receive_message_chain("role.ta?"){ true }
    allow(user).to receive_message_chain("role.name").and_return('Student')
    c1=Course.new
    allow(user1).to receive_message_chain(:courses_assisted_with,:any?).and_yield(c1)
    allow_any_instance_of(Course).to receive_message_chain(:assignments,:map,:flatten,:map,:include?,:user).and_return(true)
    expect(user1.is_teaching_assistant_for?(user)).to be true
    end
  end


  describe '#is_teaching_assistant?' do
===References===
    it 'returns true if current user is a TA' do
      allow(user).to receive_message_chain("role.ta?"){ true }
      expect(user.is_teaching_assistant?).to be true
    end


    it 'returns false if current user is not a TA' do
#[https://github.com/expertiza/expertiza Expertiza on GitHub]
      allow(user).to receive_message_chain("role.ta?"){ false }
#[https://github.com/WintersLt/expertiza GitHub Project Repository Fork]
      expect(user.is_teaching_assistant?).to be_falsey
#[http://expertiza.ncsu.edu/ The live Expertiza website]
    end
#[http://bit.ly/myexpertiza  Demo link]
  end
#[http://wikis.lib.ncsu.edu/index.php/Expertiza Expertiza project documentation wiki]
end
#[https://relishapp.com/rspec Rspec Documentation]
#Clean Code: A handbook of agile software craftsmanship. Author: Robert C Martin

Latest revision as of 00:12, 2 November 2017

E1746 [Test First Development] Refactor user.rb

This page provides a description of the Expertiza based OSS project.

About Expertiza

Expertiza is an open source project based on Ruby on Rails framework. Expertiza allows the instructor to create new assignments and customize new or existing assignments. It also allows the instructor to create a list of topics the students can sign up for. Students can form teams in Expertiza to work on various projects and assignments. Students can also peer review other students' submissions. Expertiza supports submission across various document types, including the URLs and wiki pages.

Test Plan

The following tasks were accomplished in this project:

  • Complete the pending tests in user_spec.rb
There are many pending tests needed to be finished. And for the methods which are needed to be refactored or renamed, this is also can been seen as writing failing tests first.
  • Refactor get_user_list method.
The get_user_list methods contain many conditions and each of them has a lot of code which makes the method very long. The factor is to transfer the statements to the corresponding subclasses ,and writing method calls instead.
  • Refactor self.search_users method
The method has many repetitive codes which are used in the conditionals. We refactor it by writing a single method call and let the method call to choose the conditional.
  • Rename methods and change all other places it is used.
  • Use find_by instead of dynamic method.
The find_by_name or find_by_email method which are used in the user.rb is like old style code. And this kind of code can be replaced with new dynamic ones like, find_by.

Missions and Solutions

Mission 1:Complete the pending tests in user_spec.rb

There are many pending tests in the file 'user.rb'. What we have done is finishing these pending tests and makes them pass.The user_spec.rb file on github is below.
https://github.com/AnranZhou/expertiza/blob/master/spec/models/user_spec.rb

Mission 2:Refactor get_user_list method

The task is to write failing test first and ,move if conditions to corresponding subclasses (eg. ta.rb, instructor.rb) with same method name,then replace the conditional with the relevant method calls.
First writing the failing test first according to the requirements of the refactor.
#describe '#get_user_list' do
    context 'when current user is super admin' do
      it 'fetches all users' do
        allow(user).to receive_message_chain("role.super_admin?"){ true }
        allow(user).to receive_message_chain("role.instructor?"){ false }
        allow(user).to receive_message_chain("role.ta?"){false}
        allow(SuperAdministrator).to receive(:get_user_list).and_return([user1,user2])
        expect(user.get_user_list).to eq ([user1,user2])
      end
  end
    context 'when current user is an instructor' do
      it 'fetches all users in his/her course/assignment' do
        allow(user).to receive_message_chain("role.super_admin?"){ false }
        allow(user).to receive_message_chain("role.instructor?"){ true }
        allow(user).to receive_message_chain("role.ta?"){false}
        allow(Instructor).to receive(:get_user_list).and_return([user1,user2])
        expect(user.get_user_list).to eq ([user1,user2])
      end
    end

    context 'when current user is a TA' do
      it 'fetches all users in his/her courses'do
        allow(user).to receive_message_chain("role.super_admin?"){ false }
        allow(user).to receive_message_chain("role.ta?"){ true }
        allow(user).to receive_message_chain("role.instructor?"){ false }
        allow(Ta).to receive(:get_user_list).and_return([user1,user2])
        expect(user.get_user_list).to eq ([user1,user2])
    end
    end
  end
The next step is to move if conditionals to its relatively subclasses. In this case,the superadministrator.rb, instructor.rb and ta.rb.
#class SuperAdministrator < User
  def self.get_user_list
    user_list=[]
    User.all.find_each do |user|
    user_list << user
    end
  end
end
  #def self.get_user_list
    user_list=[]
    participants = []
    Course.where(instructor_id: self.id).find_each do |course|
      participants << course.get_participants
    end
    Assignment.where(instructor_id: self.id).find_each do |assignment|
      participants << assignment.participants
    end
    participants.each do |p_s|
      next if p_s.empty?
      p_s.each do |p|
        user_list << p.user if self.role.hasAllPrivilegesOf(p.user.role)
      end
    end
end
#def self.get_user_list
  get_user_list=[]
  courses.each do |course_id|
      course = Course.find(course_id)
      participants << course.get_participants
    end
    participants.each do |p_s|
      next if p_s.empty?
      p_s.each do |p|
        user_list << p.user if self.role.hasAllPrivilegesOf(p.user.role)
      end
    end
end
Then replace the conditional with the relevant method calls.
#def get_user_list
    user_list = []
    # If the user is a super admin, fetch all users
    if self.role.super_admin?
       return SuperAdministrator.get_user_list
    end


    # If the user is an instructor, fetch all users in his course/assignment
    if self.role.instructor?
      return Instructor.get_user_list
    end

    # If the user is a TA, fetch all users in his courses
    if self.role.ta?
      return Ta.get_user_list
    end

    # Add the children to the list
    unless self.role.super_admin?
      User.all.find_each do |u|
        if is_recursively_parent_of(u)
          user_list << u unless user_list.include?(u)
        end
      end
    end

    user_list.uniq
  end
So the refactor of get_user_list method has been done.

Mission 3:Refactor self.search_users method.

This task is required to write the failing test first and extract duplicated code to a new method.
First, write the failing test.
 #descirbe '.search_users' do

    let(:role) { Role.new }
    before(:each) do
        allow(User).to receive_message_chain(:order,:where).and_return(user)
    end

    it 'when the search_by is 1' do
      search_by = "1"
      user_id = double
      letter = 'name'
      search_filter = '%' + letter + '%'
      expect(User).to receive_message_chain(:order,:where).with("(role_id in (?) or id = ?) and name like ?", role.get_available_roles, user_id, search_filter)
      expect(User.search_users(role,user_id,letter,search_by)).to eq user
    end

    it 'when the search_by is 2' do
      search_by = "2"
      user_id = double
      letter = 'fullname'
      search_filter = '%' + letter + '%'
      expect(User).to receive_message_chain(:order,:where).with("(role_id in (?) or id = ?) and fullname like ?", role.get_available_roles, user_id, search_filter)
      expect(User.search_users(role,user_id,letter,search_by)).to eq user
    end

    it 'when the search_by is 3' do
      search_by = "3"
      user_id = double
      letter = 'email'
      search_filter = '%' + letter + '%'
      expect(User).to receive_message_chain(:order,:where).with("(role_id in (?) or id = ?) and email like ?", role.get_available_roles, user_id, search_filter)
      expect(User.search_users(role,user_id,letter,search_by)).to eq user
    end

    it 'when the search_by is default value' do
      search_by = nil
      user_id = double
      letter = ''
      search_filter = letter + '%'
      expect(User).to receive_message_chain(:order,:where).with("(role_id in (?) or id = ?) and name like ?", role.get_available_roles, user_id, search_filter)
      expect(User.search_users(role,user_id,letter,search_by)).to eq user
    end
  end
Then,extract duplicated code to a new method.Since this is kind of changing original method.So below is the changing on github.
#   def self.search_users(role, user_id, letter, search_by)
 -    if search_by == '1' # search by user name
 +    key_word = {'1'=>'name','2'=>'fullname','3'=>'email'}
 +    if key_word.include? search_by
        search_filter = '%' + letter + '%'
 -      users = User.order('name').where("(role_id in (?) or id = ?) and name like ?", role.get_available_roles, user_id, search_filter)
 -    elsif search_by == '2' # search by full name
 -      search_filter = '%' + letter + '%'
 -      users = User.order('name').where("(role_id in (?) or id = ?) and fullname like ?", role.get_available_roles, user_id, search_filter)
 -    elsif search_by == '3' # search by email
 -      search_filter = '%' + letter + '%'
 -      users = User.order('name').where("(role_id in (?) or id = ?) and email like ?", role.get_available_roles, user_id, search_filter)
 +      users = User.order('name').where("(role_id in (?) or id = ?) and #{key_word[search_by]} like ?", role.get_available_roles, user_id, search_filter)
      else # default used when clicking on letters
        search_filter = letter + '%'
        users = User.order('name').where("(role_id in (?) or id = ?) and name like ?", role.get_available_roles, user_id, search_filter)

Mission 4:Rename method and change all other places it is used

First, is to write failing test of the renamed method which has been finished in mission one.So just skip to the second part to rename them.
is_recursively_parent_of → recursively_parent_of?
  -       expect(user1.is_recursively_parent_of(user)).to eq false
  +       expect(user1.recursively_parent_of(user)).to eq false
is_creator_of? → creator_of?
 -  describe '#is_creator_of?' do
 +  describe '#creator_of?' do
      it 'returns true of current user (user) is the creator of target user (user1)' do
        allow(user1).to receive(:creator).and_return(user)
 -      expect(user.is_creator_of?(user1)).to be true
 +      expect(user.creator_of?(user1)).to be true
      end
  
      it 'returns false of current user (user) is not the creator of target user (user1)' do
        allow(user1).to receive(:creator).and_return(user2)
 -      expect(user.is_creator_of?(user1)).to be false
 -      expect(user2.is_creator_of?(user1)).to be true
 +      expect(user.creator_of?(user1)).to be false
 +      expect(user2.creator_of?(user1)).to be true
  
      end
    end
 @@ -382,16 +382,16 @@
      end
    end
is_teaching_assistant_for? → teaching_assistant_for?
 -  describe '#is_teaching_assistant_for?' do
 +  describe '#teaching_assistant_for?' do
      it 'returns false if current user is not a TA' do
        allow(user1).to receive_message_chain("role.ta?"){ false }
 -      expect(user1.is_teaching_assistant_for?(user)).to be_falsey
 +      expect(user1.teaching_assistant_for?(user)).to be_falsey
      end
  
      it 'returns false if current user is a TA, but target user is not a student'do
        allow(user1).to receive_message_chain("role.ta?"){true }
        allow(user).to receive_message_chain("role.name").and_return('teacher')
 -      expect(user1.is_teaching_assistant_for?(user)).to be_falsey
 +      expect(user1.teaching_assistant_for?(user)).to be_falsey
      end
  
      it 'returns true if current user is a TA of target user'do
 @@ -401,23 +401,23 @@
      c1=Course.new
      allow(user1).to receive_message_chain(:courses_assisted_with,:any?).and_yield(c1)
      allow_any_instance_of(Course).to receive_message_chain(:assignments,:map,:flatten,:map,:include?,:user).and_return(true)
 -    expect(user1.is_teaching_assistant_for?(user)).to be true
 +    expect(user1.teaching_assistant_for?(user)).to be true
      end
    end
  
 
is_teaching_assistant? → teaching_assistant?
  -  describe '#is_teaching_assistant?' do
  +  describe '#teaching_assistant?' do
      it 'returns true if current user is a TA' do
        allow(user).to receive_message_chain("role.ta?"){ true }
 -      expect(user.is_teaching_assistant?).to be true
 +      expect(user.teaching_assistant?).to be true
      end
  
      it 'returns false if current user is not a TA' do
        allow(user).to receive_message_chain("role.ta?"){ false }
 -      expect(user.is_teaching_assistant?).to be_falsey
 +      expect(user.teaching_assistant?).to be_falsey
      end
    end
After writing the test, it is going to rename the method in the user.rb. Below is the changed part of the code.
 -    return true if self.is_teaching_assistant_for?(user)
 -    return true if self.is_recursively_parent_of(user)
 +    return true if self.teaching_assistant_for?(user)
 +    return true if self.recursively_parent_of(user)
 -    def is_recursively_parent_of(user)
 +    def recursively_parent_of(user)
 -    self.is_recursively_parent_of(p)
 +    self.recursively_parent_of(p)
 -    if is_recursively_parent_of(u)
 +    if recursively_parent_of(u)
 -    def is_creator_of?(user)
 +    def creator_of?(user)
 -    def is_teaching_assistant_for?(student)
 -    return false unless is_teaching_assistant?
 +    def teaching_assistant_for?(student)
 +    return false unless teaching_assistant?
 -    def is_teaching_assistant?
 +    def teaching_assistant?

Mission 5:Use find_by instead of dynamic method

::There are three lines using find_by_name and find_by_email which are now be instead of find_by[params].The task is to replace the old one to refactor the method.

First,writing the failing tests,here is the original test code including find_by method.
  allow(User).to receive(:find_by_name).and_return(user)

And below is the new failing test

  allow(User).to receive(:find_by).and_return(user)

And so as the other codes contain find_by method

 -      allow(User).to receive(:find_by).with(email).and_return(user)
 +      allow(User).to receive(:find_by).and_return(user)
 -      allow(User).to receive(:find_by_email).and_return(nil)
 +      allow(User).to receive(:find_by).and_return(nil)
 -      allow(User).to receive(:find_by_name).and_return(user)
 +      allow(User).to receive(:find_by).and_return(user)

And then change the find_by_name and find_by_name in the user.rb file in to dynamic one.

 -      user = User.find_by_name(row[0])
 +      user = User.find_by name: row[0]
 -      user = User.find_by_email(login)
 +      user = User.find_by email: login
 -      User.find_by_name(params[:user][:name])
 +      User.find_by name: params[:user][:name]

References

  1. Expertiza on GitHub
  2. GitHub Project Repository Fork
  3. The live Expertiza website
  4. Demo link
  5. Expertiza project documentation wiki
  6. Rspec Documentation
  7. Clean Code: A handbook of agile software craftsmanship. Author: Robert C Martin