CSC/ECE 517 Fall 2021 - E2130. Refactor submitted content controller.rb: Difference between revisions

From Expertiza_Wiki
Jump to navigation Jump to search
(Created page with "'''E2127: Refactor 'submitted_content_controller' Controller''' This page provides a brief description of the '''Expertiza''' project. The project is aimed at refactoring the...")
 
 
(21 intermediate revisions by one other user not shown)
Line 1: Line 1:
'''E2127: Refactor 'submitted_content_controller' Controller'''
'''E2130: Refactor 'submitted_content_controller' '''


This page provides a brief description of the '''Expertiza''' project. The project is aimed at refactoring the 'Teams_Controller' Controller, which contains the methods to create new teams, list the existing teams, create new teams by inheriting existing teams, deleting existing teams and creating multiple teams at once by assigning them random topics. The project entailed, refactoring some part of the controller to improve the readability of code and then creating test cases in [https://en.wikipedia.org/wiki/RSpec RSpec] to verify the methods it possesses.  
This page provides detailed explanation of the Submitted Content Controller which is a part of the '''Expertiza''' project. The aim of the project is to refactor the 'submitted_content_controller', which contains the methods to submit assignment related information such as submit & remove hyperlinks, files and other relevant information that could be part of the assignment. It also handles views based on the user roles and permissions they have.
The project involved refactoring some parts of the controller to incorporate the OODD principles so that the readability of code could be improved.  


==Introduction to Expertiza==
==About Expertiza==
[http://expertiza.ncsu.edu/ Expertiza] is a peer review based system which provides incremental learning from the class. This project has been developed together by faculty and students using [http://rubyonrails.org/ Ruby on Rails] framework. Expertiza allows the instructor to create, edit and delete assignments, create new assignment topics, assign them to a particular class or selected students, have students work on teams and then review each other's assignments at the end. For the students, they can signup for topics, form teams, and submit their projects and assignments.  
The [http://expertiza.ncsu.edu/ Expertiza] platform employs a divide-and-conquer strategy for creating reusable learning objects via active-learning exercises built entirely on [http://rubyonrails.org/ Ruby on Rails] framework. Students get to choose from a list of tasks to complete either individually or in teams. They then prepare their work and submit it to a peer-review mechanism. On submission, other students can assess their peers work and provide feedback. Expertiza encourages students to collaborate in order to improve the learning experiences from one another. It aids their learning by making them translate what is taught in the lectures and apply those concepts to a real-world issue.
Students then review the work done by other students and give suggestions to improve. Teams after reviews are allotted scores and they can refer to the peer comments to further improve their work. It also supports submission of different file types for assignments, including the URLs and wiki pages.


== Project Description ==
==Problem Statement==
'''Teams_Controller''' primarily handles the team creation, deletion and other behaviours. Most of those are called from the instructor’s view. When manual testing is done, most of the methods can be called by clicking the “Create teams” icon from both assignments and courses.
'''submitted_content_controller''' had some problems that violate essential Rails design principles which needed to be rectified. Issues included some methods being too long which needed to be broken down, a few methods needed better naming and a few that were no longer needed.
The following tasks have been performed as per the requirements.


===Create Method===
Broadly, the following issues were addressed as a part of refactoring this controller:
The method '''Create''' is called when an instructor tries to create a team manually. This works for both, creating ''assignment teams'' and ''course teams''. Assignment teams are teams made to do a particular assignment together and ''Course Teams'' are teams which are made for the whole course.
* Renaming methods to more appropriate and functionality specific names.
* The existing code was reused to perform either the same function or re-purposed to do a similar function adhering to standards and improving overall quality of the code.
* Introduction of modular code in order to make each module easier to understand, test and refactor independently of others.


===Delete Method===
==Problems and Solutions ==
The method '''Delete''' is called when an instructor tries to delete a team manually. This works for both, deleting ''assignment teams'' and ''course teams''.
* '''Problem 1''': action_allowed method should be changed to use new_access_control methods.  
 
:The original code does not make use of the new access control methods which are a part of the authorization_helper.rb
===List Method===
<pre>
The method '''List''' lists all the team nodes and the instructor is able to expand each team node to see the user nodes as well.
def action_allowed?
 
    ['Instructor',
===Inherit Method===
    'Teaching Assistant',
The method '''Inherit''' inherits teams from course to assignments, but the “Inherit Teams From Course” option should not display when either 1) we are editing a course object or 2) the current assignment object does not have a course associated with it.
    'Administrator',
 
    'Super-Administrator',
===Running Tests===
    'Student'].include? current_role_name and
  rspec spec/controllers/teams_controller_spec.rb
    ((%w[edit].include? action_name) ? are_needed_authorizations_present?(params[:id], "reader", "reviewer") : true) and
 
     one_team_can_submit_work?
===Files Modified===
  1. teams_controller.rb
  2. team.rb
  3. signed_up_team.rb
  4. routes.rb
  5. app/views/teams/new.html.erb
 
== Refactoring ==
[https://en.wikipedia.org/wiki/Code_refactoring '''Refactoring'''] is restructuring of code without the need of changing any external behavior. It reduces complexity and improves readability. It also becomes easy to extend the application with respect to different modules and their functionalities.
Some common techniques to refactor are:
 
* Moving methods to appropriate modules
* Breaking methods into more meaningful functionality
* Creating more generalized code.
* Renaming methods and variable.
* Inheritance
 
== Tasks ==
 
===Refactoring delete method===
Description: The delete method manipulates a waitlist. This code is moved to signed_up_team.rb model class.
 
Initially, the code was:
 
  def delete
    # delete records in team, teams_users, signed_up_teams table
    @team = Team.find_by(id: params[:id])
    unless @team.nil?
      course = Object.const_get(session[:team_type]).find(@team.parent_id)
      @signed_up_team = SignedUpTeam.where(team_id: @team.id)
      @teams_users = TeamsUser.where(team_id: @team.id)
      if @signed_up_team == 1 && !@signUps.first.is_waitlisted # this team hold a topic
        # if there is another team in waitlist, make this team hold this topic
        topic_id = @signed_up_team.first.topic_id
        next_wait_listed_team = SignedUpTeam.where(topic_id: topic_id, is_waitlisted: true).first
        # if slot exist, then confirm the topic for this team and delete all waitlists for this team
        SignUpTopic.assign_to_first_waiting_team(next_wait_listed_team) if next_wait_listed_team
      end
      @sign_up_team.destroy_all if @sign_up_team
      @teams_users.destroy_all if @teams_users
      @team.destroy if @team
      undo_link("The team \"#{@team.name}\" has been successfully deleted.")
    end
    redirect_to :back
  end
 
Which was changed to:
 
  def delete
    # delete records in team, teams_users, signed_up_teams table
    @team = Team.find_by(id: params[:id])
     unless @team.nil?
      course = Object.const_get(session[:team_type]).find(@team.parent_id)
      @signed_up_team = SignedUpTeam.where(team_id: @team.id)
      @teams_users = TeamsUser.where(team_id: @team.id)
      # On team deletion topic team was holding will be assigned to first team in waitlist.
      SignedUpTeam.assign_topic_to_first_in_waitlist_post_team_deletion(@signed_up_team, @signups)
      @sign_up_team.destroy_all if @sign_up_team
      @teams_users.destroy_all if @teams_users
      @team.destroy if @team
      undo_link("The team \"#{@team.name}\" has been successfully deleted.")
    end
    redirect_to :back
   end
   end
 
</pre>
and a new method was introduced in signed_up_team.rb model:
* '''Solution''': On using the new access control methods:  
  # this method checks when a team is deleted if there is a team in waitlist for the topic
<pre>
  # deleted team was holding, then assign topic to first team in waitlist
   def action_allowed?
   def self.assign_topic_to_first_in_waitlist_post_team_deletion (signed_up_team, signups)
     if %w[edit update list_submissions].include? params[:action]
     if signed_up_team == 1 && !signups.first.is_waitlisted # this team hold a topic
       current_user_has_admin_privileges? || current_user_teaching_staff_of_assignment?(params[:id])
      # if there is another team in waitlist, make this team hold this topic
      topic_id = signed_up_team.first.topic_id
      next_wait_listed_team = SignedUpTeam.where(topic_id: topic_id, is_waitlisted: true).first
      # if slot exist, then confirm the topic for this team and delete all waitlists for this team
      SignUpTopic.assign_to_first_waiting_team(next_wait_listed_team) if next_wait_listed_team
    end
  end
 
===Refactoring inherit method===
 
Description: The code that copies a list is moved to team.rb model class.
 
Initially, the code was:
  def inherit
    assignment = Assignment.find(params[:id])
    if assignment.course_id >= 0
      course = Course.find(assignment.course_id)
       teams = course.get_teams
      unless teams.empty?
        teams.each do |team|
          team.copy(assignment.id)
        end
      else
        flash[:note] = "No teams were found when trying to inherit."
      end
     else
     else
       flash[:error] = "No course was found for this assignment."
       current_user_has_ta_privileges?
     end
     end
    redirect_to controller: 'teams', action: 'list', id: assignment.id
  end


Which was changed to:
     case params[:action]
  def inherit
     when 'edit'
     assignment = Assignment.find(params[:id])
       current_user_has_student_privileges? &&
     if assignment.course_id >= 0
       are_needed_authorizations_present?(params[:id], "reader", "reviewer")
       course = Course.find(assignment.course_id)
    when 'submit_file', 'submit_hyperlink'
       teams = course.get_teams
      current_user_has_student_privileges? &&
      unless teams.empty?
       one_team_can_submit_work?
        # copy_assignment method copies teams to assignment
        Team.copy_assignment(teams,assignment)
      else
        flash[:note] = "No teams were found when trying to inherit."
       end
     else
     else
       flash[:error] = "No course was found for this assignment."
       current_user_has_student_privileges?
     end
     end
    redirect_to controller: 'teams', action: 'list', id: assignment.id
   end
   end
</pre>


and a new method was introduced in team.rb model:
2. '''Problem 2''': Remove the comment  “# hence use team count for the check”.
  def self.copy_assignment(teams,assignment)
::The current code no longer checks for the team count to see if a participant belongs to a team. Comment on line #19 removed.
    teams.each do |team|
      team.copy(assignment.id)
    end
  end
 
===Refactoring create method===
 
Description: It creates new teams against a parent id
 
Initially the create method was:
 
  def create
    parent = Object.const_get(session[:team_type]).find(params[:id])
    begin
      Team.check_for_existing(parent, params[:team][:name], session[:team_type])
      @team = Object.const_get(session[:team_type] + 'Team').create(name: params[:team][:name], parent_id: parent.id)
      TeamNode.create(parent_id: parent.id, node_object_id: @team.id)
      undo_link("The team \"#{@team.name}\" has been successfully created.")
      redirect_to action: 'list', id: parent.id
    rescue TeamExistsError
      flash[:error] = $ERROR_INFO
      redirect_to action: 'new', id: parent.id
    end
  end


Which changed to:


  def create
3. '''Problem 3''': Change the method name view to something more informative of the method.
     begin
:The original code uses the generic 'view' method name to display a view corresponding to a case when submissions cannot be accepted, for instance in the case when a deadline is passed.
      parent = get_parent_and_check_if_exists(params[:id])
<pre>
      @team = Object.const_get(session[:team_type] + 'Team').create(name: params[:team][:name], parent_id: parent.id)
def view
      TeamNode.create(parent_id: parent.id, node_object_id: @team.id)
     @participant = AssignmentParticipant.find(params[:id])
      undo_link("The team \"#{@team.name}\" has been successfully created.")
    return unless current_user_id?(@participant.user_id)
      redirect_to action: 'list', id: parent.id
    @assignment = @participant.assignment
    rescue TeamExistsError
    # @can_submit is the flag indicating if the user can submit or not in current stage
      flash_and_redirect_on_update_or_create_error('new', parent_id)
    @can_submit = false
    end
    @stage = @assignment.get_current_stage(SignedUpTeam.topic_id(@participant.parent_id, @participant.user_id))
    redirect_to action: 'edit', id: params[:id], view: true
   end
   end
 
</pre>
  # called to fetch parent and check if team with same name and type already exists.
* '''Solution''': We found changing the name to disable_submission to be more apt in this case
  def get_parent_and_check_if_exists(parent_id)
<pre>
     parent = Object.const_get(session[:team_type]).find(parent_id)
def disable_submission
     Team.check_for_existing(parent, params[:team][:name], session[:team_type])
     @participant = AssignmentParticipant.find(params[:id])
    return parent
    return unless current_user_id?(@participant.user_id)
    @assignment = @participant.assignment
    # @can_submit is the flag indicating if the user can submit or not in current stage
    @can_submit = false
    @stage = @assignment.get_current_stage(SignedUpTeam.topic_id(@participant.parent_id, @participant.user_id))
     redirect_to action: 'edit', id: params[:id], view: true
   end
   end
</pre>


  # to flash and redirect the user when there is any update or create error
4. '''Problem 4''': submit_hyperlink is filled with a lot of logging code.
  def flash_and_redirect_on_update_or_create_error(action, id)
:Logging is essential to understand the behavior of the application and to debug unexpected issues or for simply tracking events as in the production environment, we can’t debug issues without proper log files.
     flash[:error] = $ERROR_INFO
* '''Solution''': We added a function for the sole purpose of logging so that it can be reused wherever required.
    redirect_to action: action, id: id
<pre>
def log_info(controller_name, participant_name, message, request)
     ExpertizaLogger.info LoggerMessage.new(controller_name, participant_name, message, request)
   end
   end
</pre>


===Refactoring update method===


Description: It updates an existing team name
5. '''Problem 5''': Check the validity of the comment ”# Note: This is not used yet in the view until we all decide to do so”.
:
* '''Solution''': The function to remove_hyperlink performs as expected, hence the comment is no longer valid.


Initially the update method was:


   def update
6. '''Problem 6''': submit_file method is long.
     @team = Team.find(params[:id])
:
     parent = Object.const_get(session[:team_type]).find(@team.parent_id)
* '''Solution''': Implemented modular code by separating the function into simpler functions.
     begin
<pre>
      Team.check_for_existing(parent, params[:team][:name], session[:team_type])
   def tested
      @team.name = params[:team][:name]
     @current_folder = DisplayOption.new
      @team.save
     @current_folder.name = "/"
      flash[:success] = "The team \"#{@team.name}\" has been successfully updated."
     @current_folder.name = FileHelper.sanitize_folder(params[:current_folder][:name]) if params[:current_folder]
      undo_link("")
    curr_directory = if params[:origin] == 'review'
      redirect_to action: 'list', id: parent.id
                      participant.review_file_path(params[:response_map_id]).to_s + @current_folder.name
    rescue TeamExistsError
                    else
      flash[:error] = $ERROR_INFO
                      participant.team.path.to_s + @current_folder.name
      redirect_to action: 'edit', id: @team.id
                    end
     end
     return curr_directory
   end
   end
</pre>


Which changed to:  
7. '''Problem 7''': Move mail_assigned_reviewers to a mailer class.
 
:
  # It updates an existing team name
* '''Solution''': Moved the mail_assigned_reviewers to mailer_helper.rb
  def update
<pre>
     @team = Team.find(params[:id])
def self.mail_assigned_reviewers(team)
     begin
     maps = ResponseMap.where(reviewed_object_id: @participant.assignment.id, reviewee_id: team.id, type: 'ReviewResponseMap')
       parent = get_parent_and_check_if_exists(@team.parent_id)
     unless maps.nil?
      @team.name = params[:team][:name]
       maps.each do |map|
      @team.save
        # Mailing function
      flash[:success] = "The team \"#{@team.name}\" has been successfully updated."
        Mailer.general_email(
      undo_link("")
          to: User.find(Participant.find(map.reviewer_id).user_id).email,
      redirect_to action: 'list', id: parent.id
          subject: "Link to update the review for Assignment '#{@participant.assignment.name}'",
    rescue TeamExistsError
          cc: User.find_by(@participant.assignment.instructor_id).email,
      flash_and_redirect_on_update_or_create_error('edit', @team.id)
          link: "Link: https://expertiza.ncsu.edu/response/new?id=#{map.id}",
          assignment: @participant.assignment.name
        ).deliver_now
      end
     end
     end
   end
   end
</pre>


  # called to fetch parent and check if team with same name and type already exists.
8. '''Problem 8''': Change the method name of folder_action.
  def get_parent_and_check_if_exists(parent_id)
:
     parent = Object.const_get(session[:team_type]).find(parent_id)
* '''Solution''': Renamed folder_action to perform_folder_action.  
    Team.check_for_existing(parent, params[:team][:name], session[:team_type])
<pre>
     return parent
def perform_folder_action
  end
     @participant = AssignmentParticipant.find(params[:id])
 
     return unless current_user_id?(@participant.user_id)
  # to flash and redirect the user when there is any update or create error
     @current_folder = DisplayOption.new
  def flash_and_redirect_on_update_or_create_error(action, id)
     @current_folder.name = "/"
     flash[:error] = $ERROR_INFO
    @current_folder.name = FileHelper.sanitize_folder(params[:current_folder][:name]) if params[:current_folder]
     redirect_to action: action, id: id
     if params[:faction][:delete]
  end
      SubmittedFiles.delete_selected_files
 
     elsif params[:faction][:rename]
 
      SubmittedFiles.rename_selected_file
===Create Teams===
     elsif params[:faction][:move]
 
      SubmittedFiles.move_selected_file
The new method self.create_teams() is created in Team.rb model and placed the common of create_teams method.
     elsif params[:faction][:copy]
  # This function is used to create teams with random names.
      SubmittedFiles.copy_selected_file
  # Instructors can call by clicking "Create temas" icon anc then click "Create teams" at the bottom.
     elsif params[:faction][:create]
  def self.create_teams(session,params)
       create_new_folder
    parent = Object.const_get(session[:team_type]).find(params[:id])
     Team.randomize_all_by_parent(parent, session[:team_type], params[:team_size].to_i)
  end
 
Exiting code of create_teams() method in teams_controller.rb
 
  # This function is used to create teams with random names.
  # Instructors can call by clicking "Create temas" icon anc then click "Create teams" at the bottom.
  def create_teams
     parent = Object.const_get(session[:team_type]).find(params[:id])
     Team.randomize_all_by_parent(parent, session[:team_type], params[:team_size].to_i)
    undo_link("Random teams have been successfully created.")
     ExpertizaLogger.info LoggerMessage.new(controller_name, '', 'Random teams have been successfully created', request)
    redirect_to action: 'list', id: parent.id
  end
 
Modified the teams_controller.rb file and it is now using the Team.create_teams(session,params) present in Team.rb model to create teams.
 
  # This function is used to create teams with random names.
  # Instructors can call by clicking "Create temas" icon anc then click "Create teams" at the bottom.
  def create_teams
    Team.create_teams(session,params)
    undo_link("Random teams have been successfully created.")
    ExpertizaLogger.info LoggerMessage.new(controller_name, '', 'Random teams have been successfully created', request)
    redirect_to action: 'list', id: parent.id
  end
 
===signUps should be signups===
Existing code for the signUps nouns are modified to signups.
 
Existing code teams_controller.rb
 
def delete
    # delete records in team, teams_users, signed_up_teams table
     @team = Team.find_by(id: params[:id])
    unless @team.nil?
      course = Object.const_get(session[:team_type]).find(@team.parent_id)
      @signed_up_team = SignedUpTeam.where(team_id: @team.id)
      @teams_users = TeamsUser.where(team_id: @team.id)
 
      SignedUpTeam.assign_topic_to_first_in_waitlist_post_team_deletion(@signed_up_team, @signUps)
 
      @sign_up_team.destroy_all if @sign_up_team
      @teams_users.destroy_all if @teams_users
       @team.destroy if @team
      undo_link("The team \"#{@team.name}\" has been successfully deleted.")
     end
     end
     redirect_to :back
     redirect_to action: 'edit', id: @participant.id
   end
   end
</pre>


Modified code in teams_controller.rb
9. '''Problem 9''': Some of the actions in the code (from line 187 to 247) could perhaps be moved to another class
 
:
def delete
* '''Solution''': Separated functions related to submission of files into a new helper class called submitted_files_helper.rb.
    # delete records in team, teams_users, signed_up_teams table
<pre>
    @team = Team.find_by(id: params[:id])
module SubmittedFiles
    unless @team.nil?
      course = Object.const_get(session[:team_type]).find(@team.parent_id)
      @signed_up_team = SignedUpTeam.where(team_id: @team.id)
      @teams_users = TeamsUser.where(team_id: @team.id)
 
      SignedUpTeam.assign_topic_to_first_in_waitlist_post_team_deletion(@signed_up_team, @signups)


      @sign_up_team.destroy_all if @sign_up_team
def move_selected_file
      @teams_users.destroy_all if @teams_users
  old_filename = params[:directories][params[:chk_files]] + "/" + params[:filenames][params[:chk_files]]
       @team.destroy if @team
  newloc = @participant.dir_path
       undo_link("The team \"#{@team.name}\" has been successfully deleted.")
  newloc += "/"
  newloc += params[:faction][:move]
  begin
       FileHelper.move_file(old_filename, newloc)
       flash[:note] = "The file was successfully moved from \"/#{params[:filenames][params[:chk_files]]}\" to \"/#{params[:faction][:move]}\""
    rescue StandardError => e
      flash[:error] = "There was a problem moving the file: " + e.message
     end
     end
    redirect_to :back
end
  end
 
Existing code signed_up_team.rb
 
  def self.assign_topic_to_first_in_waitlist_post_team_deletion (signed_up_team, signUps)
    if signed_up_team == 1 && !signups.first.is_waitlisted # this team hold a topic
      # if there is another team in waitlist, make this team hold this topic
      topic_id = signed_up_team.first.topic_id
      next_wait_listed_team = SignedUpTeam.where(topic_id: topic_id, is_waitlisted: true).first
      # if slot exist, then confirm the topic for this team and delete all waitlists for this team
      SignUpTopic.assign_to_first_waiting_team(next_wait_listed_team) if next_wait_listed_team
    end
  end
 
Modified code in signed_up_team.rb
 
  def self.assign_topic_to_first_in_waitlist_post_team_deletion (signed_up_team, signups)
    if signed_up_team == 1 && !signups.first.is_waitlisted # this team hold a topic
      # if there is another team in waitlist, make this team hold this topic
      topic_id = signed_up_team.first.topic_id
      next_wait_listed_team = SignedUpTeam.where(topic_id: topic_id, is_waitlisted: true).first
      # if slot exist, then confirm the topic for this team and delete all waitlists for this team
      SignUpTopic.assign_to_first_waiting_team(next_wait_listed_team) if next_wait_listed_team
    end
  end
 
== Testing the Teams_Controller ==
 
=== Instructions for testing Rspecs ===
 
1. git clone https://github.com/bhaskarsinha1311/expertiza.git
 
2. Change to the expertiza directory and then perform "bundle install" and rake db:migrate.
 
3. Start the rails server
 
4. In a new terminal and in the expertiza directory, perform the following commands:
    i.  rspec spec/controllers/teams_controller_spec.rb
    ii. rspec spec/features/inherit_teams_display_spec.rb
    iii. rspec spec/features/list_teams_spec.rb
 
The results to the above commands can be found [https://www.youtube.com/watch?v=CcSruLHQoeU/ here].
 
===Testing Inherit Method===


To test that while creating an assignment team, the inherit teams section should be displayed, we log in as instructor got to list page and click on Create Team Link. On this page, we test that it should contain the string 'Inherit Teams From Course'.
To do this test manually, user need lo log in as an Instructor and navigate to assignments page for any course. There the user can click on 'Create Teams' icon and will be redirected to the Create teams page for an assignment, there the user can check for the 'inherit teams' button in the view, ideally, it should be there.
  it 'should display inherit teams while creating an assignment team' do
    create(:assignment)
    create(:assignment_node)
    create(:assignment_team)
    login_as("instructor6")
    visit '/teams/list?id=1&type=Assignment'
    click_link 'Create Team'
    expect(page).to have_content('Inherit Teams From Course')
  end


To test that while creating a course team, the inherit teams section should not be displayed, we do same steps in the previous case but finally, we test that the page does NOT contain 'Inherit Teams from Course'.
def rename_selected_file
To do this test manually, user need lo log in as an Instructor and navigate to courses page. There the user can click on 'Create Teams' icon and will be redirected to 'create team' page, ideally, the 'Inherit Teams' button should not be displayed.
old_filename = params[:directories][params[:chk_files]] + "/" + params[:filenames][params[:chk_files]]
new_filename = params[:directories][params[:chk_files]] + "/" + FileHelper.sanitize_filename(params[:faction][:rename])
begin
  raise "A file already exists in this directory with the name \"#{params[:faction][:rename]}\"" if File.exist?(new_filename)
  File.send("rename", old_filename, new_filename)
rescue StandardError => e
  flash[:error] = "There was a problem renaming the file: " + e.message
end
end




  it 'should not display inherit teams while creating a course team' do
    create(:course)
    create(:course_node)
    create(:course_team)
    login_as("instructor6")
    visit '/teams/list?id=1&type=Course'
    click_link 'Create Team'
    expect(page).to have_no_content('Inherit Teams From Course')
  end


Similarly, for creating a team for an assignment without a course, we test that the page does not contain 'Inherit Teams From Course'. For this particular test case, we added a new factory object defined as an assignment with the course set to nil.
def delete_selected_files
This can not be tested manually because assignment cannot be created without a course parent.


  filename = params[:directories][params[:chk_files]] + "/" + params[:filenames][params[:chk_files]]
  FileUtils.rm_r(filename)
  participant = Participant.find_by(id: params[:id])
  assignment = participant.try(:assignment)
  team = participant.try(:team)
  SubmissionRecord.create(team_id: team.try(:id),
                          content: filename,
                          user: participant.try(:name),
                          assignment_id: assignment.try(:id),
                          operation: "Remove File")
  ExpertizaLogger.info LoggerMessage.new(controller_name, @participant.name, 'The selected file has been deleted.', request)
end


   it 'should not display inherit teams while creating team for an assignment without a course' do
def copy_selected_file
    create(:assignment_without_course)
   old_filename = params[:directories][params[:chk_files]] + "/" + params[:filenames][params[:chk_files]]
     create(:assignment_without_course_node)
  new_filename = params[:directories][params[:chk_files]] + "/" + FileHelper.sanitize_filename(params[:faction][:copy])
     create(:assignment_without_course_team)
  begin
     login_as("instructor6")  
     raise "A file with this name already exists. Please delete the existing file before copying." if File.exist?(new_filename)
    visit '/teams/list?id=1&type=Assignment'
     raise "The referenced file does not exist." unless File.exist?(old_filename)
     click_link 'Create Team'
     FileUtils.cp_r(old_filename, new_filename)
    expect(page).to have_no_content('Inherit Teams From Course')
  rescue StandardError => e
     flash[:error] = "There was a problem copying the file: " + e.message
   end
   end
end


===Testing Create Method===
end


This test checks that after creating an assignment team the count in the teams table increase by 1.
</pre>
This test cannot be performed manually as it is testing the count of a database table.
But, the user can check this functionality online by logging in as an instructor, going to the assignment page of any course and clicking on 'create teams' icon. Following up with entering the required details and clicking on 'create team'.


    describe "POST #create" do
== Testing Requirements ==
    context "with an assignment team" do
      it "increases count by 1" do
        expect{create :assignment_team, assignment: @assignment}.to change(Team,:count).by(1)
      end
    it "redirects to the list page" do
      end
    end
  end


The same test an be applied to create a course team as well.
1. git clone https://github.com/Neelkanth7/expertiza


    context "with a course team" do
2. Change directory to expertiza. Run "bundle install" and rails db:migrate.
      it "increases the count by 1" do
        expect{create :course_team, course: @course}.to change(Team,:count).by(1)
      end
    end


===Testing Delete Method===
3. Start the rails server.
   
The delete method should work for deleting both assignment and course teams. We check this functionality by deleting the respective team and then check if the count of Teams goes down by 1.
The user can check this test manually by logging in as an instructor navigating to the page of any team for an assignment and clicking on 'Delete Team' icon.  


  context "with an assignment team " do
4. Run the following command in a new terminal of the expertiza directory:  
      it "deletes an assignment team" do
     irspec spec/controllers/submitted_content_controller_spec.rb (Test file mentioned missing)
        @assignment = create(:assignment)
        @a_team = create(:assignment_team)
        expect{ @a_team.delete }.to change(Team, :count).by(-1)
      end
    end
 
Same Delete test has been applied to the course team
The manual check can also be performed in a similar way by logging in as an instructor navigating to the page of any team for an assignment and clicking on 'Delete Team' icon.
 
   
    context "with a course team " do
      it "deletes a course team" do
        @course = create(:course)
        @c_team = create(:course_team)
        expect{ @c_team.delete }.to change(Team, :count).by(-1)
      end
     end
 
===Testing List Method===
 
The list method lists all the team nodes. This test that we have written, checks whether the instructor is able to view the team nodes in the list view.
To do this test manually user can log into the system as an instructor and click on any course, the course teams will be displayed. Also, the user can click on any team node to view information about that team.
 
describe 'List Team' do
   
  it 'should list all team nodes' do
    create(:assignment)
    create(:assignment_node)
    assignment_team = create(:assignment_team)
    team_user = create(:team_user)
    login_as("instructor6")
    visit '/teams/list?id=1&type=Assignment'
    page.all('#theTable tr').each do |tr|
      expect(tr).to have_content?(assignment_team.name)
    end
    end
  end


==References==
==References==


* [https://github.com/bhaskarsinha1311/expertiza/ Github] link to the project.
* [https://github.com/Neelkanth7/expertiza Github] link to the project.
* [https://docs.google.com/document/d/1TFC0G3zW-xkJYuloh_BmvWnj3dGZB23GOIYuikNRf_I/edit Link] to the project description.
* [https://docs.google.com/document/d/19FHqVoYUw0HzqGfZ68H-Z-SYmeP_LoHS2rkXjnxeaSE/edit#heading=h.jpaayj9sgw5h Link] to the project description.
* [https://www.youtube.com/watch?v=CcSruLHQoeU/ Youtube video of tests]

Latest revision as of 20:07, 6 November 2021

E2130: Refactor 'submitted_content_controller'

This page provides detailed explanation of the Submitted Content Controller which is a part of the Expertiza project. The aim of the project is to refactor the 'submitted_content_controller', which contains the methods to submit assignment related information such as submit & remove hyperlinks, files and other relevant information that could be part of the assignment. It also handles views based on the user roles and permissions they have. The project involved refactoring some parts of the controller to incorporate the OODD principles so that the readability of code could be improved.

About Expertiza

The Expertiza platform employs a divide-and-conquer strategy for creating reusable learning objects via active-learning exercises built entirely on Ruby on Rails framework. Students get to choose from a list of tasks to complete either individually or in teams. They then prepare their work and submit it to a peer-review mechanism. On submission, other students can assess their peers work and provide feedback. Expertiza encourages students to collaborate in order to improve the learning experiences from one another. It aids their learning by making them translate what is taught in the lectures and apply those concepts to a real-world issue.

Problem Statement

submitted_content_controller had some problems that violate essential Rails design principles which needed to be rectified. Issues included some methods being too long which needed to be broken down, a few methods needed better naming and a few that were no longer needed.

Broadly, the following issues were addressed as a part of refactoring this controller:

  • Renaming methods to more appropriate and functionality specific names.
  • The existing code was reused to perform either the same function or re-purposed to do a similar function adhering to standards and improving overall quality of the code.
  • Introduction of modular code in order to make each module easier to understand, test and refactor independently of others.

Problems and Solutions

  • Problem 1: action_allowed method should be changed to use new_access_control methods.
The original code does not make use of the new access control methods which are a part of the authorization_helper.rb
def action_allowed?
    ['Instructor',
     'Teaching Assistant',
     'Administrator',
     'Super-Administrator',
     'Student'].include? current_role_name and
    ((%w[edit].include? action_name) ? are_needed_authorizations_present?(params[:id], "reader", "reviewer") : true) and
    one_team_can_submit_work?
  end
  • Solution: On using the new access control methods:
  def action_allowed?
    if %w[edit update list_submissions].include? params[:action]
      current_user_has_admin_privileges? || current_user_teaching_staff_of_assignment?(params[:id])
    else
      current_user_has_ta_privileges?
    end

    case params[:action]
    when 'edit'
      current_user_has_student_privileges? &&
      are_needed_authorizations_present?(params[:id], "reader", "reviewer")
    when 'submit_file', 'submit_hyperlink'
      current_user_has_student_privileges? &&
      one_team_can_submit_work?
    else
      current_user_has_student_privileges?
    end
  end

2. Problem 2: Remove the comment “# hence use team count for the check”.

The current code no longer checks for the team count to see if a participant belongs to a team. Comment on line #19 removed.


3. Problem 3: Change the method name view to something more informative of the method.

The original code uses the generic 'view' method name to display a view corresponding to a case when submissions cannot be accepted, for instance in the case when a deadline is passed.
 def view
    @participant = AssignmentParticipant.find(params[:id])
    return unless current_user_id?(@participant.user_id)
    @assignment = @participant.assignment
    # @can_submit is the flag indicating if the user can submit or not in current stage
    @can_submit = false
    @stage = @assignment.get_current_stage(SignedUpTeam.topic_id(@participant.parent_id, @participant.user_id))
    redirect_to action: 'edit', id: params[:id], view: true
  end
  • Solution: We found changing the name to disable_submission to be more apt in this case
 def disable_submission
    @participant = AssignmentParticipant.find(params[:id])
    return unless current_user_id?(@participant.user_id)
    @assignment = @participant.assignment
    # @can_submit is the flag indicating if the user can submit or not in current stage
    @can_submit = false
    @stage = @assignment.get_current_stage(SignedUpTeam.topic_id(@participant.parent_id, @participant.user_id))
    redirect_to action: 'edit', id: params[:id], view: true
  end

4. Problem 4: submit_hyperlink is filled with a lot of logging code.

Logging is essential to understand the behavior of the application and to debug unexpected issues or for simply tracking events as in the production environment, we can’t debug issues without proper log files.
  • Solution: We added a function for the sole purpose of logging so that it can be reused wherever required.
 def log_info(controller_name, participant_name, message, request)
    ExpertizaLogger.info LoggerMessage.new(controller_name, participant_name, message, request)
  end


5. Problem 5: Check the validity of the comment ”# Note: This is not used yet in the view until we all decide to do so”.

  • Solution: The function to remove_hyperlink performs as expected, hence the comment is no longer valid.


6. Problem 6: submit_file method is long.

  • Solution: Implemented modular code by separating the function into simpler functions.
  def tested
    @current_folder = DisplayOption.new
    @current_folder.name = "/"
    @current_folder.name = FileHelper.sanitize_folder(params[:current_folder][:name]) if params[:current_folder]
    curr_directory = if params[:origin] == 'review'
                       participant.review_file_path(params[:response_map_id]).to_s + @current_folder.name
                     else
                       participant.team.path.to_s + @current_folder.name
                     end
    return curr_directory
  end

7. Problem 7: Move mail_assigned_reviewers to a mailer class.

  • Solution: Moved the mail_assigned_reviewers to mailer_helper.rb
 def self.mail_assigned_reviewers(team)
    maps = ResponseMap.where(reviewed_object_id: @participant.assignment.id, reviewee_id: team.id, type: 'ReviewResponseMap')
    unless maps.nil?
      maps.each do |map|
        # Mailing function
        Mailer.general_email(
          to: User.find(Participant.find(map.reviewer_id).user_id).email,
          subject:  "Link to update the review for Assignment '#{@participant.assignment.name}'",
          cc: User.find_by(@participant.assignment.instructor_id).email,
          link: "Link: https://expertiza.ncsu.edu/response/new?id=#{map.id}",
          assignment: @participant.assignment.name
        ).deliver_now
      end
    end
  end

8. Problem 8: Change the method name of folder_action.

  • Solution: Renamed folder_action to perform_folder_action.
 def perform_folder_action
    @participant = AssignmentParticipant.find(params[:id])
    return unless current_user_id?(@participant.user_id)
    @current_folder = DisplayOption.new
    @current_folder.name = "/"
    @current_folder.name = FileHelper.sanitize_folder(params[:current_folder][:name]) if params[:current_folder]
    if params[:faction][:delete]
      SubmittedFiles.delete_selected_files
    elsif params[:faction][:rename]
      SubmittedFiles.rename_selected_file
    elsif params[:faction][:move]
      SubmittedFiles.move_selected_file
    elsif params[:faction][:copy]
      SubmittedFiles.copy_selected_file
    elsif params[:faction][:create]
      create_new_folder
    end
    redirect_to action: 'edit', id: @participant.id
  end

9. Problem 9: Some of the actions in the code (from line 187 to 247) could perhaps be moved to another class

  • Solution: Separated functions related to submission of files into a new helper class called submitted_files_helper.rb.
module SubmittedFiles

def move_selected_file
  old_filename = params[:directories][params[:chk_files]] + "/" + params[:filenames][params[:chk_files]]
  newloc = @participant.dir_path
  newloc += "/"
  newloc += params[:faction][:move]
  begin
      FileHelper.move_file(old_filename, newloc)
      flash[:note] = "The file was successfully moved from \"/#{params[:filenames][params[:chk_files]]}\" to \"/#{params[:faction][:move]}\""
    rescue StandardError => e
      flash[:error] = "There was a problem moving the file: " + e.message
    end
end


def rename_selected_file
old_filename = params[:directories][params[:chk_files]] + "/" + params[:filenames][params[:chk_files]]
new_filename = params[:directories][params[:chk_files]] + "/" + FileHelper.sanitize_filename(params[:faction][:rename])
begin
  raise "A file already exists in this directory with the name \"#{params[:faction][:rename]}\"" if File.exist?(new_filename)
  File.send("rename", old_filename, new_filename)
rescue StandardError => e
  flash[:error] = "There was a problem renaming the file: " + e.message
end
end



def delete_selected_files

  filename = params[:directories][params[:chk_files]] + "/" + params[:filenames][params[:chk_files]]
  FileUtils.rm_r(filename)
  participant = Participant.find_by(id: params[:id])
  assignment = participant.try(:assignment)
  team = participant.try(:team)
  SubmissionRecord.create(team_id: team.try(:id),
                          content: filename,
                          user: participant.try(:name),
                          assignment_id: assignment.try(:id),
                          operation: "Remove File")
  ExpertizaLogger.info LoggerMessage.new(controller_name, @participant.name, 'The selected file has been deleted.', request)
end

def copy_selected_file
  old_filename = params[:directories][params[:chk_files]] + "/" + params[:filenames][params[:chk_files]]
  new_filename = params[:directories][params[:chk_files]] + "/" + FileHelper.sanitize_filename(params[:faction][:copy])
  begin
    raise "A file with this name already exists. Please delete the existing file before copying." if File.exist?(new_filename)
    raise "The referenced file does not exist." unless File.exist?(old_filename)
    FileUtils.cp_r(old_filename, new_filename)
  rescue StandardError => e
    flash[:error] = "There was a problem copying the file: " + e.message
  end
end

end

Testing Requirements

1. git clone https://github.com/Neelkanth7/expertiza

2. Change directory to expertiza. Run "bundle install" and rails db:migrate.

3. Start the rails server.

4. Run the following command in a new terminal of the expertiza directory:

   i.  rspec spec/controllers/submitted_content_controller_spec.rb (Test file mentioned missing)

References

  • Github link to the project.
  • Link to the project description.