Skip to content

Commit

Permalink
Merge cadc560 into ef449f6
Browse files Browse the repository at this point in the history
  • Loading branch information
saisanthoshG committed Apr 24, 2024
2 parents ef449f6 + cadc560 commit f8feb79
Show file tree
Hide file tree
Showing 7 changed files with 218 additions and 22 deletions.
84 changes: 64 additions & 20 deletions app/controllers/lottery_controller.rb
@@ -1,29 +1,26 @@
class LotteryController < ApplicationController
include LotteryHelper
include AuthorizationHelper

require 'json'
require 'rest_client'

# Give permission to run the bid to appropriate roles
def action_allowed?
current_user_has_ta_privileges?
end

# This method sends a request to a web service that uses k-means and students' bidding data
# to build teams automatically.
# The webservice tries to create teams with sizes close to the max team size
# allowed by the assignment by potentially combining existing smaller teams
# that have similar bidding info/priorities associated with the assignment's sign-up topics.
#
# rubocop:disable Metrics/AbcSize

def run_intelligent_assignment
assignment = Assignment.find(params[:id])
teams = assignment.teams

users_bidding_info = construct_users_bidding_info(assignment.sign_up_topics, teams)
bidding_data = { users: users_bidding_info, max_team_size: assignment.max_team_size }
ExpertizaLogger.info LoggerMessage.new(controller_name, session[:user].name, "Bidding data for assignment #{assignment.name}: #{bidding_data}", request)

begin
url = WEBSERVICE_CONFIG['topic_bidding_webservice_url']
response = RestClient.post url, bidding_data.to_json, content_type: :json, accept: :json
Expand All @@ -36,12 +33,70 @@ def run_intelligent_assignment
rescue StandardError => e
flash[:error] = e.message
end

redirect_to controller: 'tree_display', action: 'list'
end

# Prepares data for displaying the bidding details for each topic within an assignment.
# It calculates the number of bids for each priority (1, 2, 3) per topic and also computes
# the overall percentages of teams that received their first, second, and third choice.
# This method is responsible for calculating the bidding table data for an assignment.
def calculate_bidding_summary_based_on_priority
@assignment = Assignment.find(params[:id])
@topics = @assignment.sign_up_topics.includes(:bids)
@topic_data = @topics.map do |topic|
# Count the total number of bids for the topic.
total_bids = topic.bids.count
# Count the number of first, second, and third priority bids.
first_bids = topic.bids.where(priority: 1).count
second_bids = topic.bids.where(priority: 2).count
third_bids = topic.bids.where(priority: 3).count
# Extract the team names for the bids.
bidding_teams = topic.bids.includes(:team).map { |bid| bid.team.name }

# Calculate the percentage of first priority bids.
percentage_first = if total_bids > 0
# If there are any bids, calculate the percentage.
(first_bids.to_f / total_bids * 100).round(2)
else
# If there are no bids, the percentage is 0.
0
end
# Return a hash containing all the calculated and retrieved data for the topic.
{
id: topic.id,
name: topic.topic_name,
first_bids: first_bids,
second_bids: second_bids,
third_bids: third_bids,
total_bids: total_bids,
percentage_first: percentage_first,
bidding_teams: bidding_teams
}
end
end

private

# Computes the count of assigned teams for each priority level (1, 2, 3) across all topics.
# It checks each team associated with a topic and determines if the team's bid matches
# one of the priority levels, incrementing the respective count if so.
def compute_priority_counts(assigned_teams_by_topic, bids_by_topic)
priority_counts = { 1 => 0, 2 => 0, 3 => 0 }
assigned_teams_by_topic.each do |topic_id, teams|
teams.each do |team|
bid_info = bids_by_topic[topic_id].find { |bid| bid[:team] == team }
priority_counts[bid_info[:priority]] += 1 if bid_info
end
end
priority_counts
end

# Calculates the percentages of teams that received their first, second, and third choice
# based on the counts of teams at each priority level.
def compute_percentages(priority_counts, total_teams)
priority_counts.transform_values { |count| (count.to_f / total_teams * 100).round(2) }
end

# Generate user bidding information hash based on students who haven't signed up yet
# This associates a list of bids corresponding to sign_up_topics to a user
# Structure of users_bidding_info variable: [{user_id1, bids_1}, {user_id2, bids_2}]
Expand Down Expand Up @@ -84,11 +139,7 @@ def construct_teams_bidding_info(unassigned_teams, sign_up_topics)
# teams
def create_new_teams_for_bidding_response(teams, assignment, users_bidding_info)
teams.each do |user_ids|
if assignment.auto_assign_mentor
new_team = MentoredTeam.create_team_with_users(assignment.id, user_ids)
else
new_team = AssignmentTeam.create_team_with_users(assignment.id, user_ids)
end
new_team = AssignmentTeam.create_team_with_users(assignment.id, user_ids)
# Select data from `users_bidding_info` variable that only related to team members in current team
current_team_members_info = users_bidding_info.select { |info| user_ids.include? info[:pid] }.map { |info| info[:ranks] }
Bid.merge_bids_from_different_users(new_team.id, assignment.sign_up_topics, current_team_members_info)
Expand All @@ -102,12 +153,8 @@ def assign_available_slots(teams_bidding_info)
teams_bidding_info.each do |tb|
tb[:bids].each do |bid|
topic_id = bid[:topic_id]
num_of_signed_up_teams = SignedUpTeam.where(topic_id: topic_id).count
max_choosers = SignUpTopic.find(bid[:topic_id]).try(:max_choosers)
if num_of_signed_up_teams < max_choosers
SignedUpTeam.create(team_id: tb[:team_id], topic_id: bid[:topic_id])
break
end
max_choosers = SignUpTopic.find(topic_id).try(:max_choosers)
SignedUpTeam.create(team_id: tb[:team_id], topic_id: topic_id) if SignedUpTeam.where(topic_id: topic_id).count < max_choosers
end
end
end
Expand All @@ -124,7 +171,6 @@ def match_new_teams_to_topics(assignment)
unassigned_teams = assignment.teams.reload.select do |t|
SignedUpTeam.where(team_id: t.id, is_waitlisted: 0).blank? && Bid.where(team_id: t.id).any?
end

# Sorting unassigned_teams by team size desc, number of bids in current team asc
# again, we need to find a way to to merge bids that came from different previous teams
# then sorting unassigned_teams by number of bids in current team (less is better)
Expand All @@ -134,10 +180,8 @@ def match_new_teams_to_topics(assignment)
[TeamsUser.where(team_id: t2.id).size, Bid.where(team_id: t1.id).size] <=>
[TeamsUser.where(team_id: t1.id).size, Bid.where(team_id: t2.id).size]
end

teams_bidding_info = construct_teams_bidding_info(unassigned_teams, sign_up_topics)
assign_available_slots(teams_bidding_info)

# Remove is_intelligent property from assignment so that it can revert to the default sign-up state
assignment.update_attributes(is_intelligent: false)
flash[:success] = 'The intelligent assignment was successfully completed for ' + assignment.name + '.'
Expand Down
12 changes: 12 additions & 0 deletions app/helpers/lottery_helper.rb
@@ -1,2 +1,14 @@
module LotteryHelper
def background_color_by_percentage(percentage)
case percentage
when 0...33
'background-color: #ffcccc;' # Light red for low percentages
when 33...66
'background-color: #ffcc99;' # Light orange for medium percentages
when 66..100
'background-color: #ccffcc;' # Light green for high percentages
else
'background-color: none;' # No background if outside range
end
end
end
26 changes: 24 additions & 2 deletions app/views/assignments/edit/_topics.html.erb
@@ -1,15 +1,37 @@
<h3>Topics for <%= @assignment_form.assignment.name %> assignment</h3><br>
<style>
.custom-button {
display: inline-block;
padding: 5px 5px;
background-color: white;
color: black;
text-decoration: none;
border: 1px solid black;
border-radius: 5px;
font: Arial;
}
</style>
<script>
const enableBiddingCheckbox = document.getElementById('enable-bidding-checkbox');
const biddingButton = document.getElementById('bidding-button');

enableBiddingCheckbox.addEventListener('change', function() {
biddingButton.style.display = enableBiddingCheckbox.checked ? 'inline-block' : 'none';
});
</script>
<input name="assignment_form[assignment][allow_suggestions]" type="hidden" value="false"/>
<%= check_box_tag('assignment_form[assignment][allow_suggestions]', 'true', @assignment_form.assignment.allow_suggestions) %>
<%= label_tag('assignment_form[assignment][allow_suggestions]', 'Allow topic suggestions from students?') %>
<br>
<input name="assignment_form[assignment][is_intelligent]" type="hidden" value="false"/>
<%= check_box_tag('assignment_form[assignment][is_intelligent]', 'true', @assignment_form.assignment.is_intelligent?)%>
<%= check_box_tag('assignment_form[assignment][is_intelligent]', 'true', @assignment_form.assignment.is_intelligent?, id: 'enable-bidding-checkbox')%>
<%= label_tag('assignment_form[assignment][is_intelligent]', 'Enable bidding for topics?') %>
<img src="/assets/info.png" title="This feature allow students to &quot;bid&quot; for topics.
Instructor must specify when topics are assigned, by going to the Due Dates tab and
entering a due date for &quot;signup&quot;."/>
<br>

<%= button_to 'Show bids by priority', {:controller => 'lottery', :action => 'calculate_bidding_summary_based_on_priority', :id => @assignment_form.assignment.id}, :method => :get, class: 'custom-button', id: 'bidding-button' %>

<input name="assignment_form[assignment][can_review_same_topic]" type="hidden" value="false"/>
<%= check_box_tag('assignment_form[assignment][can_review_same_topic]', 'true', @assignment_form.assignment.can_review_same_topic?)%>
<%= label_tag('assignment_form[assignment][can_review_same_topic]', 'Enable authors to review others working on same topic?') %>
Expand Down
@@ -0,0 +1,50 @@
<!-- Display the assignment name in the bidding summary header -->
<h3>Assignment Bidding Summary by Priority: <%= @assignment.name %></h3>

<!-- Define the structure of the bidding summary table -->
<table class="table table-bordered table-hover">
<thead>
<tr>
<!-- Define the table headers with specified widths for better alignment -->
<th>Topic id</th>
<th>Topic name</th>
<th>#1 bids</th>
<th>#2 bids</th>
<th>#3 bids</th>
<th>Total bids</th>
<!-- Set fixed width for the 'Percentage of #1 bids' column -->
<th style="width: 200px;">Percentage of #1 bids</th>
<!-- Set fixed width for the 'Bidding teams' column -->
<th style="width: 200px;">Bidding teams</th>
</tr>
</thead>
<tbody>
<!-- Iterate over each topic to display its bidding information -->
<% @topic_data.each do |topic| %>
<tr>
<!-- Display topic id, name, bid counts, total bids and bidding teams -->
<td><%= topic[:id] %></td>
<td><%= topic[:name] %></td>
<td><%= topic[:first_bids] %></td>
<td><%= topic[:second_bids] %></td>
<td><%= topic[:third_bids] %></td>
<td><%= topic[:total_bids] %></td>
<!-- Calculate and display the percentage of first bids with tooltip and custom background -->
<td title="Calculated as (#1 Bids / Total Bids) * 100">
<span style="<%= background_color_by_percentage(topic[:percentage_first]) %> font-weight: bold; padding: 3px; border-radius: 4px;">
<%= topic[:percentage_first] %> %
</span>
</td>
<!-- List all the bidding teams for the topic -->
<td>
<% topic[:bidding_teams].each do |team| %>
<%= team %><br>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>

<!-- Link to navigate back to the previous page -->
<%= link_to 'Back', :back %>
3 changes: 3 additions & 0 deletions config/routes.rb
Expand Up @@ -665,4 +665,7 @@
post 'student_task/publishing_rights_update', controller: :student_task, action: :publishing_rights_update, method: :put
get 'student_view/flip_view', controller: :student_view, action: :flip_view
# updated route and added specific controller action upon accessing this route
# Add a new route for Calculating bidding summary based on priority
get 'lottery/run_intelligent_assignment/:id', to: 'lottery#run_intelligent_assignment', as: 'run_intelligent_assignment'
get 'assignments/:id/calculate_bidding_summary_based_on_priority', to: 'lottery#calculate_bidding_summary_based_on_priority', as: 'calculate_bidding_summary_based_on_priority'
end
38 changes: 38 additions & 0 deletions spec/controllers/lottery_controller_spec.rb
Expand Up @@ -72,6 +72,44 @@
end
end

describe '#calculate_bidding_summary_based_on_priority' do
it 'calculates and returns bidding summary data for topics' do
# Setup test data
# Instead of using assignment: assignment, use assignment_id: assignment.id to directly set the foreign key
team = create(:team, assignment_id: assignment.id)
bid = create(:bid, topic: topic1, team: team, priority: 1)
team_name = create(:team_name, team: team)

allow(Assignment).to receive(:find).with(assignment.id).and_return(assignment)
allow(assignment).to receive(:sign_up_topics).and_return([topic1])
allow(topic1).to receive_message_chain(:bids, :includes).and_return([bid])
allow(bid).to receive_message_chain(:team, :name).and_return(team_name)

# Mock params
params = { id: assignment.id }
allow(controller).to receive(:params).and_return(params)

# Expected data structure from calculate_bidding_summary_based_on_priority
expected_topic_data = [
{
id: topic1.id,
name: topic1.topic_name,
first_bids: 1,
second_bids: 0,
third_bids: 0,
total_bids: 1,
percentage_first: 100.0,
bidding_teams: [team_name.name]
}
]

# Call the method
controller.instance_variable_set(:@assignment, assignment)
expect(controller.calculate_bidding_summary_based_on_priority).to eq(expected_topic_data)
end
end


describe '#construct_users_bidding_info' do
it 'generate users bidding information hash' do
# Only members in assignment_team1 and assignment_team2 are involved in the bidding process
Expand Down
27 changes: 27 additions & 0 deletions spec/helpers/lottery_helper_spec.rb
@@ -0,0 +1,27 @@
require 'rails_helper'

# Assuming your helper module is in the helpers folder
RSpec.describe LotteryHelper, type: :helper do
# Test for low percentage range
describe '#background_color_by_percentage' do
it 'returns light red for low percentages' do
expect(helper.background_color_by_percentage(10)).to eq('background-color: #ffcccc;')
end

# Test for medium percentage range
it 'returns light orange for medium percentages' do
expect(helper.background_color_by_percentage(50)).to eq('background-color: #ffcc99;')
end

# Test for high percentage range
it 'returns light green for high percentages' do
expect(helper.background_color_by_percentage(80)).to eq('background-color: #ccffcc;')
end

# Test for percentage out of range
it 'returns no background for percentages out of range' do
expect(helper.background_color_by_percentage(101)).to eq('background-color: none;')
expect(helper.background_color_by_percentage(-1)).to eq('background-color: none;')
end
end
end

0 comments on commit f8feb79

Please sign in to comment.