CSC/ECE 517 Fall 2024 - E2485. Allow reviewers to bid on what to review
Project Overview
This project will fix the defects and improve the functionality in Expertiza of the Reviewer Bidding feature. It will make the functionality smoother, more reliable, and align with Expertiza's collaborative learning objectives.
The Review Bidding feature lets reviewers focus on topics they know best or care about most. If someone feels qualified or interested in a topic, they can bid to review submissions in that area. This helps ensure reviews are insightful and enjoyable for the reviewer while benefiting the other participants and the overall process.
Problem Statements
Assigning reviews to users on Expertiza involves a complex bidding algorithm that requires optimization before production deployment. Below are key areas we plan to address as per the project requirements:
- Code Refactoring and Best Practices
- Utilize Authentication Utilities: Modify the
action_allowed
function in theReviewBidsController
to use existing authentication utilities, enhancing code reusability and adhering to the Single Responsibility Principle (SRP) and Separation of Concerns (SoC). - Eliminate Code Duplication: Refactor
review_bids_others_work
in theSignUpSheetController
to comply with the Don't Repeat Yourself (DRY) principle. - Rename Functions for Clarity: Change
run_bidding_algorithm
inReviewBidsController
toassign_reviewers
to better reflect its purpose. - Refactor Methods in
review_bid.rb
:: Convert class methods to instance methods or relocate them to helper classes for improved code organization.
- Utilize Authentication Utilities: Modify the
- Functionality Enhancements
- Handle Missing Bids Gracefully: Adjust the bidding algorithm to accommodate students who do not bid, allowing them to select from the remaining options.
- Support Both Individual and Team Assignments: Ensure the code functions correctly for participant and team-based assignments when signing up for topics.
- User Interface Improvements
- Increase Transparency in the Bidding Process: Display messages indicating how many students are eligible to bid, how many have submitted bids, and the deadline.
- Accurate Bid Status Indicators: Ensure topic colors change correctly based on the number of outstanding bids, providing clear visual feedback to users.
- Testing and Validation
- Exhaustive Edge Case Testing: Conduct thorough testing for edge cases not previously covered, adding additional test cases as needed.
Design Goals
While addressing the existing defects and enhancing the Reviewer Bidding feature, we plan to adhere to the following design guidelines:
- Code Quality and Maintainability
- Improve Code Structure: Implement refactoring strategies to enhance code readability and maintainability, ensuring adherence to best practices like the DRY principle.
- Standardize Authentication Logic: Utilize a centralized authentication utility to promote consistency and reusability across the application.
- System Stability and Flexibility
- Enhance Algorithm Robustness: Modify the bidding algorithm to handle scenarios where students do not bid, ensuring the system remains stable under all conditions.
- Ensure Assignment Versatility: Design the system to seamlessly support both individual and team assignments without additional configuration.
- User Experience Enhancements
- Increase Bidding Transparency: Develop user interface elements that clearly display bidding status, deadlines, and participation metrics.
- Implement Dynamic Visual Feedback: Use real-time visual cues, such as color changes, to inform users about the bidding status instantly.
- Testing and Validation
- Develop Comprehensive Test Suites: Create extensive automated tests covering edge cases and critical functionalities to guarantee reliable system behavior.
UML Diagram
Below is a visual representation of the models associated with the review bids functionality, depicting the data structures and their relationships.
ReviewBid: The ReviewBid
manages the bidding process, where participants indicate their preferences for reviewing different topics or assignments. This helps distribute review tasks fairly based on participants' interests.
Assignment: The Assignment
model represents team or individual assignments in Expertiza. It stores all the essential information required to keep the assignment process organized, like managing participants, setting due dates, forming teams, and overseeing the review process.
SignUpTopic: The SignUpTopic
model identifies specific topics or areas within an assignment for which participants can sign up. It manages the availability of these topics, tracks bids, coordinates team sign-ups, and assigns topics based on both bids and participant availability.
Participant: The Participant
model is about the individual users who participate in an assignment. This includes students, instructors, teaching assistants, or other relevant roles. This model is responsible for managing key user information, monitoring interactions with the assignment, keeping track of team memberships, and recording things like badges and review grades.
Implementation
Decoupling Authorization Logic from ReviewBidsController
Problem | Design Goals |
---|---|
action_allowed needs to use the authentication utilities
The function |
Authentication Consistency: Remove authentication logic in the controller and call logic in the authentication utility class instead to ensure consistent use of the same logic across the application, foster reusability, and eliminate DRY. |
Code Readability and Maintainability: Refactor and simplify code, especially where the code violates the DRY principle, to reduce complexity and make the code base more straightforward to understand and maintain. |
Authorization Flow Before Refactoring
The sequence diagram depicts the authorization process within the ReviewBidsController
. When a user invokes an action, the controller checks if the action is allowed by verifying the user's role through the current_role_name()
method in the ApplicationController
. Depending on the action and the user's role, the controller either authorizes the user to proceed or denies access, ultimately executing the action logic if authorized and responding to the user.
ReviewBidsController
Updates
We simplified the action_allowed?
method by decoupling authorization logic from the controller to use helper methods in the AuthorizationHelper
module. The ReviewBidsController
should not handle authentication, which is a violation of SRP.
Note: We also made similar updates to the action_allowed
methods for SignUpController
and StudentReviewController
.
Original Code | Refactored Code |
---|---|
def action_allowed?
case params[:action]
when 'show', 'set_priority', 'index'
['Instructor',
'Teaching Assistant',
'Administrator',
'Super-Administrator',
'Student'].include? current_role_name and
((%w[list].include? action_name) ? are_needed_authorizations_present?(params[:id], "participant", "reader", "submitter", "reviewer") : true)
else
['Instructor',
'Teaching Assistant',
'Administrator',
'Super-Administrator'].include? current_role_name
end
end
|
def action_allowed?
case params[:action]
when 'show', 'set_priority', 'index', 'list'
current_user_has_student_privileges? && list_authorization_check
else
current_user_has_ta_privileges?
end
end
|
Refactor ReviewBidsController#index
action
Previously, the ReviewBidsController's index
action was overloaded with responsibilities—it handled everything from locating and validating assignments to fetching review mappings and counting completed reviews. This all-in-one approach made the controller action messy.
We have moved the business logic from the controller action and into dedicated service objects. With this change, we introduced two key service objects: ParticipantService
and ReviewService
. The ParticipantService
focuses on retrieving the participant along with their assignments, while the ReviewService
takes on the task of managing review mappings and calculating review-related stats, like how many reviews have been completed.
This separation of responsibilities aligns with the Single Responsibility Principle. The logic for fetching assignments, retrieving review mappings, and counting reviews is now more flexible and reusable throughout the application. For example, we used methods in the ReviewService
in both the ReviewBidsController
and the StudentReviewController
, helping to avoid code duplication.The article Rails Service Objects Tutorial served as a guide on when and how to use service objects in Rails.
Old Code | New Code |
---|---|
def index
@participant = AssignmentParticipant.find(params[:id])
return unless current_user_id?(@participant.user_id)
@assignment = @participant.assignment
@review_mappings = ReviewResponseMap.where(reviewer_id: @participant.id)
# Finding how many reviews have been completed
@num_reviews_completed = 0
@review_mappings.each do |map|
@num_reviews_completed += 1 if !map.response.empty? && map.response.last.is_submitted
end
# render view for completing reviews after review bidding has been completed
render 'sign_up_sheet/review_bids_others_work'
end
|
def index
@assignment = participant_service.assignment
unless @assignment.is_a?(Assignment)
flash[:error] = 'Assignment not found.'
redirect_back fallback_location: root_path && return
end
@review_mappings = review_service.review_mappings
@num_reviews_completed = review_service.review_counts[:completed]
render 'sign_up_sheet/review_bids_others_work'
end
|
Optimize ReviewBidsController#set_priority
Action functionality
In the earlier version of the set_priority
action in the ReviewBidsController
, the server only needed a couple of key pieces of information: an array of topic IDs (params[:topic]) and the participant ID (params[:id}). From there, it would figure out the assignment_id by using the first topic as a reference. This is risky because the assignment_id
associated with the topic may not be a valid assignment_id
.
With the new implementation, we structured the parameters to include the participant_id
, an array of topic IDs, and assignment_id
. This change ensures that the server always receives complete and reliable information, preventing issues caused by missing or inconsistent data. It also removes the need for the server to perform unnecessary lookups, as in the case of assignment_id
, allowing the controller to focus on receiving requests and rendering appropriate responses.
While these updates have cleaned up the set_priority action, they still contain a lot of business logic, like validations and priority updates. To make the code even more maintainable, there’s room for further refactoring—perhaps moving some of this logic into a service class or model method to streamline the controller even more.
Old Code | New Code |
---|---|
def set_priority
if params[:topic].nil?
ReviewBid.where(participant_id: params[:id]).destroy_all
else
participant = AssignmentParticipant.find_by(id: params[:id])
assignment_id = SignUpTopic.find(params[:topic].first).assignment.id
@bids = ReviewBid.where(participant_id: params[:id])
signed_up_topics = ReviewBid.where(participant_id: params[:id]).map(&:signuptopic_id)
signed_up_topics -= params[:topic].map(&:to_i)
signed_up_topics.each do |topic|
ReviewBid.where(signuptopic_id: topic, participant_id: params[:id]).destroy_all
end
params[:topic].each_with_index do |topic_id, index|
bid_existence = ReviewBid.where(signuptopic_id: topic_id, participant_id: params[:id])
if bid_existence.empty?
ReviewBid.create(priority: index + 1, signuptopic_id: topic_id, participant_id: params[:id], assignment_id: assignment_id)
else
ReviewBid.where(signuptopic_id: topic_id, participant_id: params[:id]).update_all(priority: index + 1)
end
end
end
redirect_to action: 'show', assignment_id: params[:assignment_id], id: params[:id]
end
|
def set_priority
participant_id = params[:participant_id].to_i
selected_topic_ids = params[:topic]&.map(&:to_i) || []
assignment_id = params[:assignment_id].to_i
unless current_user_can_modify_participant?(participant_id)
flash[:error] = 'You are not authorized to modify these bids.'
respond_to do |format|
format.html { redirect_back fallback_location: root_path }
format.json { render json: { status: 'unauthorized' }, status: :unauthorized }
end
return
end
if selected_topic_ids.empty?
ReviewBid.where(participant_id: participant_id).destroy_all
else
assignment = Assignment.find_by(id: assignment_id)
if assignment.nil?
flash[:error] = "Invalid assignment."
respond_to do |format|
format.html { redirect_back fallback_location: root_path }
format.json { render json: { status: 'invalid_assignment' }, status: :unprocessable_entity }
end
return
end
unless SignUpTopic.where(id: selected_topic_ids, assignment_id: assignment_id).count == selected_topic_ids.size
flash[:error] = "One or more selected topics are invalid."
respond_to do |format|
format.html { redirect_back fallback_location: root_path }
format.json { render json: { status: 'invalid_topics' }, status: :unprocessable_entity }
end
return
end
ReviewBid.where(participant_id: participant_id).where.not(signuptopic_id: selected_topic_ids).destroy_all
selected_topic_ids.each_with_index do |topic_id, index|
bid = ReviewBid.find_or_initialize_by(signuptopic_id: topic_id, participant_id: participant_id)
bid.priority = index + 1
bid.assignment_id = assignment_id
bid.save!
end
end
respond_to do |format|
format.html { redirect_to action: 'show', assignment_id: assignment_id, id: participant_id, notice: 'Review bids updated successfully.' }
format.json { render json: { status: 'success' }, status: :ok }
end
rescue ActiveRecord::RecordInvalid => e
flash[:error] = "Failed to update priorities: #{e.message}"
respond_to do |format|
format.html { redirect_back fallback_location: root_path }
format.json { render json: { status: 'error', message: e.message }, status: :unprocessable_entity }
end
end
|
Client Side Changes to Support ReviewBidsController#set_priority
Changes
We refactored the client-side code to ensure it passed the correct information from the view to the set_priority
action. On the client side, we now grab the topicIDs and send them as selected_topic_ids
, allowing the controller to figure out which topics were selected and in what order.
When working with jQuery, AJAX is awesome because it lets us update the server without reloading the page, making the user experience smooth and responsive. When we pull the participant_id
from the DOM, we can grab it along with other data, like the list of topics, and include it as a single request. This way, we don’t have to rely on the URL to know which participant we’re working with, which makes the code cleaner.
We also added an update handler so that any time the order of the selected topic changes in the view, information is sent to the appropriate controller action. This ensures that the controller and the view remain in sync, with real-time updates, and includes a mechanism for providing feedback if something goes wrong.
Note: One interesting aspect of the application was the mix of JavaScript and CoffeeScript code. It wasn't clear why both types of code were used.
Old Code | New Code |
---|---|
jQuery ->
$("#topics").sortable
cursor: 'move',
opacity: 0.65,
tolerance: 'pointer'
connectWith: ".connectedSortable"
items: ">*:not(.sort-disabled)"
$("#selections").sortable
cursor: 'move',
opacity: 0.65,
tolerance: 'pointer'
connectWith: ".connectedSortable"
items: ">*:not(.sort-disabled)"
update: ->
$.post($(this).data('update-url'), $(this).sortable('serialize'))
|
jQuery(document).ready ($) ->
sendUpdatedOrder = ->
sortedIDs = $(this).sortable('toArray')
topicIDs = sortedIDs.map (id) ->
id.replace 'topic_', ''
participant_id = $(this).data('participant-id')
assignment_id = new URL($(this).data('update-url'), window.location.origin).searchParams.get('assignment_id')
$.ajax
type: 'POST'
url: $(this).data('update-url')
data:
topic: topicIDs
participant_id: participant_id
assignment_id: assignment_id
success: (response) ->
console.log 'Selections updated successfully.'
error: (xhr, status, error) ->
alert 'An error occurred while saving your selections. Please try again.'
console.error error
$("#topics").sortable
cursor: 'move'
opacity: 0.65
tolerance: 'pointer'
connectWith: ".connectedSortable"
items: ">*:not(.sort-disabled)"
$("#selections").sortable
cursor: 'move'
opacity: 0.65
tolerance: 'pointer'
connectWith: ".connectedSortable"
items: ">*:not(.sort-disabled)"
update: sendUpdatedOrder
|
SignUpSheetController
Updates
The ReviewBidsController#index
method renders the review_bids_others_work
action However, the view is associated with the SignUpSheetController
.
class ReviewBidsController < ApplicationController
require 'json'
require 'uri'
require 'net/http'
require 'rest_client'
# provides variables for reviewing page located at views/review_bids/others_work.html.erb
def index
@participant = AssignmentParticipant.find(params[:id])
return unless current_user_id?(@participant.user_id)
@assignment = @participant.assignment
@review_mappings = ReviewResponseMap.where(reviewer_id: @participant.id)
# Finding how many reviews have been completed
@num_reviews_completed = 0
@review_mappings.each do |map|
@num_reviews_completed += 1 if !map.response.empty? && map.response.last.is_submitted
end
# render view for completing reviews after review bidding has been completed
render 'sign_up_sheet/review_bids_others_work'
end
end
review_bids_others_work.html.erb
View Updates
The view is technically part of the SignUpSheetController, but it's being rendered by the ReviewBidsController, which initially threw us off. After chatting with the professor, we decided not to change this view. He indicated that the plan is to reuse these views for both assignment and review bidding.
We suggest building the assignment bidding and review bidding features separately first to ensure each one works independently. Once that’s done, we can clean things up by combining any shared functionality. This could include creating shared views or even introducing an abstract model to handle both types of bidding more efficiently. It could also mean replacing the ReviewBidsController with something more generic, like a BidsController, to keep things reusable and easier to maintain down the line.
Rename run_bidding_algorithm
method
Problem | Design Goals |
---|---|
run_bidding_algorithm should be assign_reviewers
|
Code Readability and Maintainability: Refactor and simplify code, especially in places where the code violates the DRY principle, to reduce complexity and make the code base more straightforward to understand and maintain. |
System Stability: Debug the existing bidding algorithm and fine-tune the assignment logic. Also, the existing functionality related to the reviewer bidding feature should work and be enhanced if required so that it does not fail in circumstances such as when a student does not bid. |
The primary activity here is to refactor the method name run_bidding_algorithm
to assign_reviewers
and fix the hardcoded URL. However, the match topic bidding algorithm no longer works since Heroku is no longer free, per the professor. The reference link below has detailed information about the web service that we can leverage to gain that understanding.
Reference Link for Webservice Details: match_topics webservice link details
Updated Implementation
The original requirement was to rename the run_bidding_algorithm method to assign_reviewers, but after digging into the code, we realized that renaming it didn’t make sense. The method runs the bidding algorithm to match topics, and there’s already a separate method to handle assigning review topics.
However, we improved the logic in the assign_bidding
method (which calls the private run_bidding_algorithm
) by introducing a separate BiddingAlgorithmService
object. This keeps the business logic out of the controller action, which makes things cleaner and more maintainable.
review_bids_controller
def run_bidding_algorithm
review_bid = ReviewBid.new
bidding_data = review_bid.bidding_data
matched_topics = BiddingAlgorithmService.new(bidding_data).run
raise ArgumentError, 'Failed to assign reviewers. Please try again later.' unless matched_topics
matched_topics
end
bidding_algorithm_service
# frozen_string_literal: true
# The `BiddingAlgorithmService` run the bid assignment algorithm
# Sends student IDs, topic IDs, student preferences, and timestamps to the web service
# The web service returns the matched assignments in the JSON response body
class BiddingAlgorithmService
SERVICE_URL = 'http://app-csc517.herokuapp.com/match_topics'.freeze
# MOCK_DATA is based on the documentation detailing how the webservice behaves.
# Reference: https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2020_-_E2085._Allow_reviewers_to_bid_on_what_to_review#Webservice
MOCK_DATA = {
36239 => [3970, 3972, 3975],
36240 => [3973, 3974, 3972],
36241 => [3969, 3971, 3972],
36242 => [3969, 3971, 3973],
36243 => [3969, 3970, 3971]
}.freeze
def initialize(bidding_data)
@bidding_data = bidding_data
end
def run
return MOCK_DATA if Rails.application.config.use_mock_bidding_algorithm
perform_request
rescue RestClient::ExceptionWithResponse, JSON::ParserError
false
end
private
def perform_request
response = RestClient.post(
SERVICE_URL,
@bidding_data.to_json,
content_type: :json,
accept: :json
)
validate_response(response)
JSON.parse(response.body)
end
def validate_response(response)
raise 'Invalid response format' unless response.headers[:content_type] && response.headers[:content_type].include?('application/json')
end
end
Resolve Functionality Issues for Non Bidders
Problem | Design Goals |
---|---|
Currently it doesn't work if some student does not bid. In this case, algorithm needs to be fixed to ignore anyone who didn’t bid, and let them choose from what’s left over. | Code Readability and Maintainability:
Refactor and simplify code, especially in places where the code violates the DRY principle, to reduce complexity and make the code base more straightforward to understand and maintain. |
System Stability:
Debug the existing bidding algorithm and fine-tune the assignment logic. Also, the existing functionality related to the reviewer bidding feature should work and be enhanced if required so that it does not fail in circumstances such as when a student does not bid. |
ReviewBidsController
Updates
We plan to do several things within this controller. We will set a default state for non-bidders that we can point to to give them options to select topics to review from the leftovers. We will consolidate @sign_up_topics
to resolve the DRY violation. Lastly, we will remove business logic from the show method and push it to a new helper to conform to the SRP.
Current Implementation
class ReviewBidsController < ApplicationController
require 'json'
require 'uri'
require 'net/http'
require 'rest_client'
# Additional method
# Provides variables for review bidding page
def show
@participant = AssignmentParticipant.find(params[:id].to_i)
@assignment = @participant.assignment
@sign_up_topics = SignUpTopic.where(assignment_id: @assignment.id, private_to: nil)
my_topic = SignedUpTeam.topic_id(@participant.parent_id, @participant.user_id)
@sign_up_topics -= SignUpTopic.where(assignment_id: @assignment.id, id: my_topic)
@num_participants = AssignmentParticipant.where(parent_id: @assignment.id).count
@selected_topics = nil # This is used to list the topics assigned to review (i.e., select == assigned)
@bids = ReviewBid.where(participant_id: @participant, assignment_id: @assignment.id)
signed_up_topics = []
@bids.each do |bid|
sign_up_topic = SignUpTopic.find_by(id: bid.signuptopic_id)
signed_up_topics << sign_up_topic if sign_up_topic
end
signed_up_topics &= @sign_up_topics
@sign_up_topics -= signed_up_topics
@bids = signed_up_topics
@num_of_topics = @sign_up_topics.size
@assigned_review_maps = []
ReviewResponseMap.where(reviewed_object_id: @assignment.id, reviewer_id: @participant.id).each do |review_map|
@assigned_review_maps << review_map
end
# Explicitly render view since it's in the sign up sheet views
render 'sign_up_sheet/review_bids_show'
end
# Additional methods
end
Updated Implementation
# Assigns bidding topics to reviewers
def assign_bidding
assignment = validate_assignment(params[:assignment_id])
reviewers = validate_reviewers(assignment.id)
reviewer_ids = reviewers.map(&:id)
matched_topics = run_bidding_algorithm
if matched_topics.blank?
flash[:alert] = 'Topic or assignment is missing'
redirect_back fallback_location: root_path && return
end
leftover_topics = find_leftover_topics(assignment.id, matched_topics)
assign_leftover_topics(reviewer_ids, matched_topics, leftover_topics)
ReviewBid.new.assign_review_topics(matched_topics)
assignment.update!(can_choose_topic_to_review: false)
flash[:notice] = 'Reviewers were successfully assigned to topics.'
redirect_back fallback_location: root_path
rescue ArgumentError => e
Rails.logger.error "ArgumentError: #{e.message}"
redirect_back fallback_location: root_path, alert: e.message
rescue ActiveRecord::RecordInvalid => e
Rails.logger.error "ActiveRecord::RecordInvalid: #{e.message}"
redirect_back fallback_location: root_path, alert: 'Failed to assign reviewers due to database error. Please try again later.'
rescue ActiveRecord::ActiveRecordError => e
Rails.logger.error "ActiveRecord::ActiveRecordError: #{e.message}"
redirect_back fallback_location: root_path, alert: 'Failed to assign reviewers due to database error. Please try again later.'
rescue StandardError => e
Rails.logger.error "StandardError: #{e.message}"
redirect_back fallback_location: root_path, alert: 'Failed to assign reviewers. Please try again later.'
end
private
#service methods here ...
def find_leftover_topics(assignment_id, matched_topics)
all_topic_ids = SignUpTopic.where(assignment_id: assignment_id).pluck(:id)
assigned_topic_ids = matched_topics.map { |match| match[:topic_id] }
# Calculate leftover topics
all_topic_ids - assigned_topic_ids
end
def assign_leftover_topics(reviewer_ids, matched_topics, leftover_topics)
return if leftover_topics.blank?
# Find non-bidders by excluding already matched reviewers
non_bidders = reviewer_ids - matched_topics.keys
# Assign leftover topics to non-bidders in a round-robin fashion
non_bidders.each_with_index do |reviewer_id, index|
topic_id = leftover_topics[index % leftover_topics.length]
ReviewBid.create(priority: 1, signuptopic_id: topic_id, participant_id: reviewer_id)
end
end
Confirm Functionality for Single-person Teams
Problem | Design Goals |
---|---|
Make sure your code works on individual assignments, i.e., assignments where participants sign up for topics instead of teams. In this case, a team is created for each participant when they sign up. So the code should work for assignments to which either individuals or teams submit. | Code Readability and Maintainability:
Refactor and simplify code, especially in places where the code violates the DRY principle, to reduce complexity and make the code base more straightforward to understand and maintain. |
System Stability:
Debug the existing bidding algorithm and fine-tune the assignment logic. Also, the existing functionality related to the reviewer bidding feature should work and be enhanced if required so that it does not fail in circumstances such as when a student does not bid. |
ReviewBidsController
Updates
We plan to make an update to focus on topic rather than team or have it where teams are updated based on topic id.We will probably utilize sign up topic or a similar object to accomplish this.
Bid Status Messaging
Problem | Design Goals |
---|---|
To make the functionality more intuitive, include a message to say how many students are eligible to submit bids, how many have submitted their bids, when the deadline for submitting bids is. | Code Readability and Maintainability:
Refactor and simplify code, especially in places where the code violates the DRY principle, to reduce complexity and make the code base more straightforward to understand and maintain. |
System Stability:
Debug the existing bidding algorithm and fine-tune the assignment logic. Also, the existing functionality related to the reviewer bidding feature should work and be enhanced if required so that it does not fail in circumstances such as when a student does not bid. |
ReviewBidsHelper
Updates
The logic for these messages will go into the helper file. It is effectively the same reasoning as our migration of other business/functionality code from the controller to the helper to conform to the SRP. It will go hand in hand with the SignUpTopic portion of the code.
Refactor ReviewBid
Model
Problem | Design Goals |
---|---|
Why are the methods in review_bid.rb class methods? Can we change them to instance methods or move it to helpers? | Code Readability and Maintainability:
Refactor and simplify code, especially in places where the code violates the DRY principle, to reduce complexity and make the code base more straightforward to understand and maintain. |
System Stability:
Debug the existing bidding algorithm and fine-tune the assignment logic. Also, the existing functionality related to the reviewer bidding feature should work and be enhanced if required so that it does not fail in circumstances such as when a student does not bid. |
ReviewBid
Model Updates
Since the current class methods are tied to ReviewBid
model instances, we will change them to instance methods. There is no need to move them to helpers.
Review Bid Model Flow Before Refactoring
This sequence diagram describes how the ReviewBidsController
interacts with the ReviewBid
model in assigning review topics to reviewers according to their bids. Once the assign_bidding
action is initiated by the user, the controller gathers the IDs of reviewers and calls ReviewBid.bidding_data
to gather bidding information. That is sent out to an external bidding algorithm, and the matched topics that come back are used by ReviewBid.assign_review_topics
to create ReviewResponseMap
entries, assigning topics to reviewers for the assignment.
Current Implementation
class ReviewBid < ApplicationRecord
belongs_to :topic, class_name: 'SignUpTopic'
belongs_to :participant, class_name: 'Participant'
belongs_to :assignment, class_name: 'Assignment'
# method to get bidding data
# returns the bidding data needed for the assigning algorithm
# student_ids, topic_ids, student_preferences, topic_preferences, max reviews allowed
def self.bidding_data(assignment_id, reviewer_ids)
# create basic hash and set basic hash data
bidding_data = { 'tid' => [], 'users' => {}, 'max_accepted_proposals' => [] }
bidding_data['tid'] = SignUpTopic.where(assignment_id: assignment_id).ids
bidding_data['max_accepted_proposals'] = Assignment.where(id: assignment_id).pluck(:num_reviews_allowed).first
# loop through reviewer_ids to get reviewer specific bidding data
reviewer_ids.each do |reviewer_id|
bidding_data['users'][reviewer_id] = reviewer_bidding_data(reviewer_id, assignment_id)
end
bidding_data
end
# assigns topics to reviews as matched by the webservice algorithm
def self.assign_review_topics(assignment_id, reviewer_ids, matched_topics, _min_num_reviews = 2)
# if review response map already created, delete it
if ReviewResponseMap.where(reviewed_object_id: assignment_id)
ReviewResponseMap.where(reviewed_object_id: assignment_id).destroy_all
end
# loop through reviewer_ids to assign reviews to each reviewer
reviewer_ids.each do |reviewer_id|
topics_to_assign = matched_topics[reviewer_id.to_s]
topics_to_assign.each do |topic|
assign_topic_to_reviewer(assignment_id, reviewer_id, topic)
end
end
end
# method to assign a single topic to a reviewer
def self.assign_topic_to_reviewer(assignment_id, reviewer_id, topic)
team_to_review = SignedUpTeam.where(topic_id: topic).pluck(:team_id).first
team_to_review.nil? ? [] : ReviewResponseMap.create(reviewed_object_id: assignment_id, reviewer_id: reviewer_id, reviewee_id: team_to_review, type: 'ReviewResponseMap')
end
# method for getting individual reviewer_ids bidding data
# returns user's bidding data hash
def self.reviewer_bidding_data(reviewer_id, assignment_id)
reviewer_user_id = AssignmentParticipant.find(reviewer_id).user_id
self_topic = SignedUpTeam.topic_id(assignment_id, reviewer_user_id)
bidding_data = { 'tid' => [], 'otid' => self_topic, 'priority' => [], 'time' => [] }
bids = ReviewBid.where(participant_id: reviewer_id)
# loop through each bid for a topic to get specific data
bids.each do |bid|
bidding_data['tid'] << bid.signuptopic_id
bidding_data['priority'] << bid.priority
bidding_data['time'] << bid.updated_at
end
bidding_data
end
end
Updated Implementation
class ReviewBid < ApplicationRecord
belongs_to :topic, class_name: 'SignUpTopic'
belongs_to :participant, class_name: 'Participant'
# method to get bidding data
# returns the bidding data needed for the assigning algorithm
# student_ids, topic_ids, student_preferences, topic_preferences, max reviews allowed
# Instance method to get bidding data for this review bid
def bidding_data
{
'tid' => topic_ids,
'otid' => self_topic,
'priority' => priorities,
'time' => updated_times
}
end
# bid object to limit db touches for similar data acquisitions
def bids
@bids ||= ReviewBid.where(participant_id: participant_id)
end
def topic_ids
bids.pluck(:signuptopic_id)
end
def priorities
bids.pluck(:priority)
end
def updated_times
bids.pluck(:updated_at)
end
def self_topic
return nil unless topic && topic.assignment_id
SignedUpTeam.topic_id(topic.assignment_id, participant.user_id)
end
# Assign topics to reviews
def assign_review_topics(matched_topics)
validate_topics_and_assignment!(matched_topics)
ReviewResponseMap.where(reviewed_object_id: topic.assignment_id).destroy_all
matched_topics.each do |reviewer_id, topics|
Array(topics).each { |topic_id| assign_topic_to_reviewer(reviewer_id, topic_id) }
end
end
# Assign a single topic to a reviewer
def assign_topic_to_reviewer(reviewer_id, topic_id)
team_to_review = SignedUpTeam.where(topic_id: topic_id).pluck(:team_id).first
return if team_to_review.nil?
ReviewResponseMap.create(
reviewed_object_id: topic.assignment_id,
reviewer_id: reviewer_id,
reviewee_id: team_to_review,
type: 'ReviewResponseMap'
)
# error handling
rescue StandardError => e
Rails.logger.error("Failed to assign topic #{topic_id} to reviewer #{reviewer_id}: #{e.message}")
end
private
def validate_topics_and_assignment!(matched_topics)
raise ArgumentError, 'Topic or assignment is missing' if topic.nil? || topic.assignment_id.nil?
raise ArgumentError, 'Matched topics must be a Hash' unless matched_topics.is_a?(Hash)
end
end
ReviewBidsController
Updates
Once we changed them to instance methods, we modified their use in the controller code to instantiate the object first.
Schema.db
Updates
We also need to remove the foreign key reference to Assignment
in the ReviewBid
model since the Participant
model already has access to Assignment
, and that is the Assignment
we should be referencing. This will also involve modifying the database to remove the foreign key reference to the Assignment
.
Steps required:
- Review the records in the
ReviewBid
table in the database to see if there are any records and if those records have values forassignment_id
- Create a migration file using the command:
rails generate migration RemoveAssignmentFromReviewBids
- In the generated file, indicate the change that needs to be made using the change method and migration columns
remove_foreign_key
andremove_column
. Migration should be straightforward since there is no cascading behavior (i.e.,dependent:destroy
) to worry about. - Once comfortable with the updates to the generated file, run the migration using the
rails db:migrate
command. - Run a query in the DB to ensure no orphaned records are in the
ReviewBid
table after the updates.
Implement Edge Case Testing
Problem | Design Goals |
---|---|
In the previous implementation wiki, there are edge cases which are not exhaustively tested. Should test those edge cases thoroughly and add more edge case testing - link | Code Readability and Maintainability:
Refactor and simplify code, especially in places where the code violates the DRY principle, to reduce complexity and make the code base more straightforward to understand and maintain. |
System Stability:
Debug the existing bidding algorithm and fine-tune the assignment logic. Also, the existing functionality related to the reviewer bidding feature should work and be enhanced if required so that it does not fail in circumstances such as when a student does not bid. |
ReviewBidsHelper
Updates
TODO:We just got initial test results back. We still need to figure out why tests are failing and what may be slipping through the cracks.
Design Patterns and Principles
Below are the design patterns and principles we plan to use to address the problem statements outlined in the project requirements:
- Single Responsibility Principle (SRP)
- Controller Responsibility
- To address the issue of authentication logic residing in
ReviewBidsController
, we will refactor the code to ensure the controller solely handles request and response flows. - Authentication logic will be moved to dedicated methods in
AuthorizationHelper
, making the codebase easier to maintain and test. - We will move this logic into helper methods or models, ensuring each component has a single responsibility and improving code readability.
- To address the issue of authentication logic residing in
- Controller Responsibility
- Separation of Concerns (SoC)
- By relocating authorization logic out of the controller, we separate authentication from request handling.
- Adhering to SoC enhances readability and maintainability by allowing each part of the application to focus on its specific role.
- Don't Repeat Yourself (DRY)
- We will inspect the existing implementation for code duplication, particularly in the controller actions.
- Any repetitive code will be refactored or removed to reduce redundancy and improve clarity.
- Dependency Inversion Principle (DIP)
- We plan to update class methods to instance methods, allowing the controller to depend on abstractions rather than concrete implementations.
- Clear and intentional naming during refactoring will enhance code clarity and maintainability.
Test Plan
The Excel spreadsheet below details our test plan for testing the code we changed or created to ensure code coverage. The process involved reviewing the code and determining all the possible scenarios that needed to be covered based on what the method was doing, then creating test cases accordingly.
- Open Test Matrix Spreadsheet
- ReviewBidsController updated test cases
- SignUpSheetController updated test cases
- ReviewBid model updated test cases
Team
Mentor
- Ed Gehringer
Members
- Gavin Teague
- Janice Uwujaren
Links
Pull Request: Expertiza Pull Request #2901
Demo Video: E2485 Review Bidding Feature Demo Video
Previous Wiki: Link
Reference Link for Webservice Details: match_topics webservice link details
Guidance on Using Service Objects in Rails: Rails Service Objects Tutorial
References