Skip to content

Commit

Permalink
Dynamic channel (#69)
Browse files Browse the repository at this point in the history
Introduce matchmaking groups as a replacement to yaml exclusivity

This is a pretty large change and some of it was to migrate tests for classes that were touched. The primary aspects to this change include a UI piece for creating matchmaking groups and refactoring the group collection to incorporate the database groups with the yaml config groups.

Also, there was a HACK to prevent segfault in test & development with pg
See more information [here](ged/ruby-pg#538).

---------

Co-authored-by: Ali Ibrahim <[email protected]>
  • Loading branch information
tuxagon and alimi authored Feb 23, 2024
1 parent fd3dff4 commit 708eb0c
Show file tree
Hide file tree
Showing 53 changed files with 1,121 additions and 583 deletions.
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,5 @@ group :test do
gem "mocktail"
gem "rspec"
gem "rspec-rails"
gem "timecop"
end
7 changes: 7 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ GEM
net-smtp (0.3.3)
net-protocol
nio4r (2.5.8)
nokogiri (1.13.9-aarch64-linux)
racc (~> 1.4)
nokogiri (1.13.9-arm64-darwin)
racc (~> 1.4)
nokogiri (1.13.9-x86_64-darwin)
Expand Down Expand Up @@ -270,13 +272,16 @@ GEM
rubocop-performance (~> 1.19.1)
stimulus-rails (1.2.1)
railties (>= 6.0.0)
tailwindcss-rails (2.0.21-aarch64-linux)
railties (>= 6.0.0)
tailwindcss-rails (2.0.21-arm64-darwin)
railties (>= 6.0.0)
tailwindcss-rails (2.0.21-x86_64-darwin)
railties (>= 6.0.0)
tailwindcss-rails (2.0.21-x86_64-linux)
railties (>= 6.0.0)
thor (1.2.1)
timecop (0.9.8)
timeout (0.3.0)
todo_or_die (0.1.1)
turbo-rails (1.3.2)
Expand All @@ -292,6 +297,7 @@ GEM
zeitwerk (2.6.6)

PLATFORMS
aarch64-linux
arm64-darwin-20
arm64-darwin-21
arm64-darwin-22
Expand Down Expand Up @@ -322,6 +328,7 @@ DEPENDENCIES
sprockets-rails
standard
tailwindcss-rails
timecop
todo_or_die

RUBY VERSION
Expand Down
10 changes: 10 additions & 0 deletions app/assets/stylesheets/application.tailwind.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@
@tailwind components;
@tailwind utilities;

td,th {
@apply p-2;
}

.actions {
a {
@apply px-1 mx-1 border border-black;
}
}

/*
@layer components {
Expand Down
55 changes: 55 additions & 0 deletions app/controllers/matchmaking_groups_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
class MatchmakingGroupsController < ApplicationController
def index
@groups = CollectGroups.new.call.sort_by { |group| [group.readonly? ? 0 : 1, group.name] }
end

def new
@group = MatchmakingGroup.new
end

def create
if MatchmakingGroup.name_exists?(group_params[:name])
flash[:error] = "Group already exists"
else
MatchmakingGroup.create(group_params.merge(slack_user_id: @current_user.slack_user_id))
flash[:notice] = "Group created"
end

redirect_to matchmaking_groups_path
end

def edit
@group = MatchmakingGroup.find_by(id: params[:id])
redirect_to matchmaking_groups_path if @group.nil?
end

def update
@group = MatchmakingGroup.find_by(id: params[:id])

if @group.name != group_params[:name] && MatchmakingGroup.name_exists?(group_params[:name])
flash[:error] = "Other group already exists with that name"
else
@group = MatchmakingGroup.find_by(id: params[:id])
@group.update(group_params)

flash[:notice] = "Group updated"
end
redirect_to matchmaking_groups_path
end

def destroy
@group = MatchmakingGroup.find_by(id: params[:id])
@group.destroy if @group.present?
redirect_to matchmaking_groups_path
end

private

def group_params
params.require(:matchmaking_group)
.permit(:name, :slack_channel_name, :schedule, :target_size, :is_active)
.tap do |hash|
hash[:name] = hash[:name].strip
end
end
end
46 changes: 0 additions & 46 deletions app/jobs/establish_matches_for_grouping_job.rb

This file was deleted.

44 changes: 44 additions & 0 deletions app/models/matchmaking_group.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
class MatchmakingGroup < ApplicationRecord
validate :name_not_in_config
validates :name, uniqueness: true

def self.name_exists?(name)
Rails.application.config.x.matchmaking.to_h.transform_keys(&:to_s).key?(name) || exists?(name: name)
end

def active?
is_active
end

def active
is_active
end

def active=(value)
self.is_active = value
end

def channel
slack_channel_name
end

def channel=(value)
self.slack_channel_name = value
end

def size
target_size
end

def size=(value)
self.target_size = value
end

private

def name_not_in_config
if Rails.application.config.x.matchmaking.to_h.key?(name.intern)
errors.add(:name, "cannot be the same as a key in the matchmaking config")
end
end
end
27 changes: 27 additions & 0 deletions app/services/collect_groups.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
class CollectGroups
def initialize
@config = Rails.application.config.x.matchmaking
end

def call
extra_groups = MatchmakingGroup.all
readonly_groups + extra_groups
end

private

def readonly_groups
@config.to_h.map do |name, group_config|
normalized = group_config.to_h.transform_keys do |key|
next :target_size if key.intern == :size
next :is_active if key.intern == :active
next :slack_channel_name if key.intern == :channel
key
end

group = MatchmakingGroup.new(normalized.merge(name: name))
group.define_singleton_method(:readonly?) { true }
group
end
end
end
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module Mailer
class BuildsGroupingMailerMessage
class BuildGroupMailerMessage
def render(recipient:, channel:, grouping:, other_members:)
GroupingMailer.encourage_match(
recipient: recipient,
Expand Down
13 changes: 4 additions & 9 deletions app/services/matchmaking/choose_strategy.rb
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
module Matchmaking
class ChooseStrategy
def initialize(config: nil)
@config = config || Rails.application.config.x.matchmaking
end

def call(grouping)
group_config = @config.send(grouping.intern)
return nil unless group_config&.active
def call(group)
return nil unless group&.active?

return Strategies::PairByFewestEncounters.new if group_config.size == 2
return Strategies::PairByFewestEncounters.new if group.target_size == 2

Strategies::ArrangeGroupsGenetically.new(target_group_size: group_config.size)
Strategies::ArrangeGroupsGenetically.new(target_group_size: group.target_size)
end
end
end
4 changes: 2 additions & 2 deletions app/services/matchmaking/collect_scored_participants.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ def initialize
@assign_score_to_candidates = AssignScoreToCandidates.new
end

def call(participants, grouping)
def call(participants, group)
participants.reduce({}) do |memo, participant|
recent_matches = HistoricalMatch.scoreable.with_member(participant).in_grouping(grouping)
recent_matches = HistoricalMatch.scoreable.with_member(participant).in_grouping(group.name)
candidates = participants.difference([participant])

scored_candidates = @assign_score_to_candidates.call(candidates, recent_matches)
Expand Down
9 changes: 9 additions & 0 deletions app/services/matchmaking/errors/channel_not_found.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module Matchmaking
module Errors
class ChannelNotFound < StandardError
def initialize(group_name, channel_name)
super("No channel found with name '#{channel_name}' for grouping '#{group_name}'")
end
end
end
end
9 changes: 9 additions & 0 deletions app/services/matchmaking/errors/no_configured_channel.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module Matchmaking
module Errors
class NoConfiguredChannel < StandardError
def initialize(group_name)
super("No configured channel for grouping '#{group_name}'")
end
end
end
end
49 changes: 49 additions & 0 deletions app/services/matchmaking/establish_matches_for_group.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
module Matchmaking
class EstablishMatchesForGroup
def initialize
@loads_slack_channels = Slack::LoadsSlackChannels.new
@loads_slack_channel_members = Slack::LoadsSlackChannelMembers.new
@match_participants = Matchmaking::MatchParticipants.new
end

def call(group)
ensure_channel_configured(group)

channel = fetch_slack_channel(group.slack_channel_name)
ensure_channel_found(channel, group)

participants = @loads_slack_channel_members.call(channel: channel.id)

matches = @match_participants.call(participants, group)
matches.each do |match|
HistoricalMatch.create(
members: match,
grouping: group.name,
matched_on: Date.today,
pending_notifications: [
PendingNotification.create(strategy: "email"),
PendingNotification.create(strategy: "slack")
]
)
end
rescue => e
ReportsError.report(e)
end

private

def ensure_channel_configured(group)
raise Errors::NoConfiguredChannel.new(group.name) unless group.slack_channel_name
end

def ensure_channel_found(channel, group)
raise Errors::ChannelNotFound.new(group.slack_channel_name, group.name) unless channel
end

def fetch_slack_channel(channel_name)
@loads_slack_channels.call(types: "public_channel").find { |channel|
channel.name_normalized == channel_name
}
end
end
end
11 changes: 5 additions & 6 deletions app/services/matchmaking/match_participants.rb
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
module Matchmaking
class MatchParticipants
def initialize(config: nil)
def initialize
@collect_scored_participants = CollectScoredParticipants.new
@config = config || Rails.application.config.x.matchmaking
@choose_strategy = ChooseStrategy.new(config: @config)
@choose_strategy = ChooseStrategy.new
end

def call(participants, grouping)
def call(participants, group)
# We don't want to consider a group of 1 a match
return [] if participants.size < 2

scored_participants = @collect_scored_participants.call(participants, grouping)
scored_participants = @collect_scored_participants.call(participants, group)

strategy = @choose_strategy.call(grouping)
strategy = @choose_strategy.call(group)
return [] unless strategy

strategy.call(scored_participants)
Expand Down
Loading

0 comments on commit 708eb0c

Please sign in to comment.