Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ GEM
benchmark (0.5.0)
bigdecimal (3.3.1)
bindex (0.8.1)
bootsnap (1.25.0)
bootsnap (1.26.0)
msgpack (~> 1.5)
builder (3.3.0)
bullet (8.2.0)
Expand Down Expand Up @@ -419,7 +419,7 @@ GEM
base64 (>= 0.1.0)
logger (>= 1.6.0)
rack (>= 3.0.0, < 4)
rack-proxy (1.0.2)
rack-proxy (2.0.0)
rack (>= 2.0, < 4)
rack-session (2.1.2)
base64 (>= 0.1.0)
Expand Down Expand Up @@ -758,7 +758,7 @@ CHECKSUMS
benchmark (0.5.0) sha256=465df122341aedcb81a2a24b4d3bd19b6c67c1530713fd533f3ff034e419236c
bigdecimal (3.3.1) sha256=eaa01e228be54c4f9f53bf3cc34fe3d5e845c31963e7fcc5bedb05a4e7d52218
bindex (0.8.1) sha256=7b1ecc9dc539ed8bccfc8cb4d2732046227b09d6f37582ff12e50a5047ceb17e
bootsnap (1.25.0) sha256=41059e7d0f9cb4023a33465d095f64b913fc9d1b808d6524c307da945fbcffcf
bootsnap (1.26.0) sha256=ca96237015e6cd74a02963d5821cf00ac5ea134653b323e8cd6d702a7718bf1b
builder (3.3.0) sha256=497918d2f9dca528fdca4b88d84e4ef4387256d984b8154e9d5d3fe5a9c8835f
bullet (8.2.0) sha256=921ed8cde81c939e4eda041345f44f0d0d72c6fbb63aec07d11e5bf653bbe473
bundler (4.0.17) sha256=214e21431b5665dd2f99df8a5511c6b151d7a72e8015c8b38f8b775b61cbb6c1
Expand Down Expand Up @@ -890,7 +890,7 @@ CHECKSUMS
racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
rack (3.2.7) sha256=93e13e1c24f93556671d85d2d79fa228c3485815c50d7e2f265b5330c6528fb7
rack-protection (4.2.1) sha256=cf6e2842df8c55f5e4d1a4be015e603e19e9bc3a7178bae58949ccbb58558bac
rack-proxy (1.0.2) sha256=9f57da746be62ec3fed46646dbae2d50f7f4ddc39307d693affff9ffd431865b
rack-proxy (2.0.0) sha256=4f1d435d82afe93bc916d1226df8be307c1b808551f0ecdb56e0b668fd5756e6
rack-session (2.1.2) sha256=595434f8c0c3473ae7d7ac56ecda6cc6dfd9d37c0b2b5255330aa1576967ffe8
rack-test (2.2.0) sha256=005a36692c306ac0b4a9350355ee080fd09ddef1148a5f8b2ac636c720f5c463
rackup (2.3.1) sha256=6c79c26753778e90983761d677a48937ee3192b3ffef6bc963c0950f94688868
Expand Down
1 change: 1 addition & 0 deletions app/controllers/home_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ def endpoints
activateProjectUrl: activate_project_path,
diversityCheckUrl: check_diversity_score_path,
groupsUrl: groups_path( id: '' ),
suggestGroupsUrl: suggest_groups_path( id: '' ),
diversityRescoreGroup: rescore_group_path( id: '' ),
diversityRescoreGroups: rescore_groups_path( id: '' )
}
Expand Down
134 changes: 106 additions & 28 deletions app/controllers/projects_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ class ProjectsController < ApplicationController
include LtiGradable

before_action :set_project, only: %i[show edit update destroy activate
rescore_group rescore_groups]
rescore_group rescore_groups suggest_groups]
before_action :check_editor, except: %i[rescore_group rescore_groups
show index get_groups
set_groups]
Expand Down Expand Up @@ -139,24 +139,31 @@ def set_groups
group_hash = {}
params[:groups].each_value do | g |
group = nil
if g[:id].positive?
group = project.groups.find_by id: g[:id]
group_id = g[:id].to_i
if group_id.positive?
group = project.groups.find_by id: group_id
group.name = g[:name]
else
group = project.groups.build( name: g[:name] )
end
group.users = []
group_hash[g[:id]] = group
group_hash[group_id] = group
end
params[:students].each_value do | s |
student = project.rosters.find_by( user_id: s[:id] ).user
group = group_hash[s[:group_id]]
group = group_hash[s[:group_id].to_i]
group.users << student unless group.nil?
end

begin
ActiveRecord::Base.transaction do
group_hash.each_value( &:save! )
group_hash.each_value do | group |
group.calc_diversity_score
group.save!
end
current_group_ids = group_hash.values.map( &:id ).compact
groups_to_remove = Group.where( project: ).where.not( id: current_group_ids )
groups_to_remove.destroy_all
end
rescue StandardError
# Post back a JSON error
Expand All @@ -176,29 +183,43 @@ def get_groups
get_groups_helper project:
end

def get_groups_helper( project:, message: nil )
students = {}
project.rosters.enrolled.each do | roster |
student = roster.user
students[ student.id ] = {
id: student.id,
first_name: student.first_name,
last_name: student.last_name,
email: student.email
}
end
# Builds a recommendation preview for the Groups tab without persisting it.
#
# This controller action is the bridge between the course roster and
# Group.suggest_optimal_groups. It returns the suggested groups, preview-only
# student assignments, and aggregate metrics that the React UI shows before
# the instructor accepts or rejects the proposal.
def suggest_groups
students = @project.rosters.enrolled.includes(
user: [
:emails, :gender, :primary_language, :cip_code,
{ home_state: :home_country }, { reactions: :narrative }
]
).map( &:user )
suggestion = Group.suggest_optimal_groups(
users: students,
target_group_size: params[:target_group_size],
target_group_count: params[:target_group_count]
)
students_payload = build_students_payload @project
suggested_students_payload = students_payload.deep_dup
groups_payload = build_suggested_groups_payload( suggestion, suggested_students_payload )

groups = {}
project.groups.each do | group |
groups[group.id] = {
id: group.id,
name: group.name,
diversity: group.diversity_score
render json: {
groups: groups_payload,
students: suggested_students_payload,
summary: {
diversity_score_standard_deviation: suggestion[:diversity_score_standard_deviation],
average_diversity_score: suggestion[:average_diversity_score],
average_faultline_strength: suggestion[:average_faultline_strength],
max_faultline_strength: suggestion[:max_faultline_strength]
}
group.users.each do | user |
students[user.id][ :group_id ] = group.id
end
end
}
end

def get_groups_helper( project:, message: nil )
students = build_students_payload project
groups = build_groups_payload( project, students )

respond_to do | format |
format.json do
Expand All @@ -212,7 +233,7 @@ def get_groups_helper( project:, message: nil )
end

def remove_group
group = Group.find( params[:group_id] )
group = Group.find_by( id: params[:group_id] )
group&.delete
redirect_to @project
end
Expand Down Expand Up @@ -267,6 +288,63 @@ def activate

private

# Serializes enrolled roster members for the groups-management JSON payloads.
def build_students_payload( project )
students = {}
project.rosters.enrolled.each do | roster |
student = roster.user
students[ student.id ] = {
id: student.id,
first_name: student.first_name,
last_name: student.last_name,
email: student.email
}
end
students
end

# Serializes persisted project groups and annotates the passed-in students hash
# with each member's saved group assignment.
def build_groups_payload( project, students )
groups = {}
project.groups.each do | group |
groups[group.id] = {
id: group.id,
name: group.name,
diversity: group.diversity_score,
faultline: group.calc_faultline_strength,
member_count: group.users.count
}
group.users.each do | user |
students[user.id][ :group_id ] = group.id
end
end
groups
end

# Serializes a recommendation preview and annotates the passed-in students hash
# with preview-only suggested group assignments.
#
# The returned structure mirrors build_groups_payload so the UI can preview a
# recommendation and later submit it through the existing set_groups action.
def build_suggested_groups_payload( suggestion, students )
groups = {}
suggestion.fetch( :groups, [] ).each_with_index do | suggested_group, index |
group_id = -( index + 1 )
groups[group_id] = {
id: group_id,
name: suggested_group[:name],
diversity: suggested_group[:diversity_score],
faultline: suggested_group[:faultline_strength],
member_count: suggested_group[:users].count
}
suggested_group[:users].each do | user |
students[user.id][ :group_id ] = group_id
end
end
groups
end

# Use callbacks to share common setup or constraints between actions.
def set_project
if params[:id].blank? || 'new' == params[:id]
Expand Down
1 change: 1 addition & 0 deletions app/javascript/components/projects/ProjectDataAdmin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,7 @@ export default function ProjectDataAdmin(props: ProjectDataAdminProps) {
<ProjectGroups
projectId={projectId}
groupsUrl={endpoints.groupsUrl}
suggestGroupsUrl={endpoints.suggestGroupsUrl}
diversityCheckUrl={endpoints.diversityCheckUrl}
diversityRescoreGroup={endpoints.diversityRescoreGroup}
diversityRescoreGroups={endpoints.diversityRescoreGroups}
Expand Down
Loading