Skip to content

Commit 708eb0c

Browse files
tuxagonalimi
andauthored
Dynamic channel (#69)
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]>
1 parent fd3dff4 commit 708eb0c

File tree

53 files changed

+1121
-583
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+1121
-583
lines changed

Gemfile

+1
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,5 @@ group :test do
3737
gem "mocktail"
3838
gem "rspec"
3939
gem "rspec-rails"
40+
gem "timecop"
4041
end

Gemfile.lock

+7
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,8 @@ GEM
141141
net-smtp (0.3.3)
142142
net-protocol
143143
nio4r (2.5.8)
144+
nokogiri (1.13.9-aarch64-linux)
145+
racc (~> 1.4)
144146
nokogiri (1.13.9-arm64-darwin)
145147
racc (~> 1.4)
146148
nokogiri (1.13.9-x86_64-darwin)
@@ -270,13 +272,16 @@ GEM
270272
rubocop-performance (~> 1.19.1)
271273
stimulus-rails (1.2.1)
272274
railties (>= 6.0.0)
275+
tailwindcss-rails (2.0.21-aarch64-linux)
276+
railties (>= 6.0.0)
273277
tailwindcss-rails (2.0.21-arm64-darwin)
274278
railties (>= 6.0.0)
275279
tailwindcss-rails (2.0.21-x86_64-darwin)
276280
railties (>= 6.0.0)
277281
tailwindcss-rails (2.0.21-x86_64-linux)
278282
railties (>= 6.0.0)
279283
thor (1.2.1)
284+
timecop (0.9.8)
280285
timeout (0.3.0)
281286
todo_or_die (0.1.1)
282287
turbo-rails (1.3.2)
@@ -292,6 +297,7 @@ GEM
292297
zeitwerk (2.6.6)
293298

294299
PLATFORMS
300+
aarch64-linux
295301
arm64-darwin-20
296302
arm64-darwin-21
297303
arm64-darwin-22
@@ -322,6 +328,7 @@ DEPENDENCIES
322328
sprockets-rails
323329
standard
324330
tailwindcss-rails
331+
timecop
325332
todo_or_die
326333

327334
RUBY VERSION

app/assets/stylesheets/application.tailwind.css

+10
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22
@tailwind components;
33
@tailwind utilities;
44

5+
td,th {
6+
@apply p-2;
7+
}
8+
9+
.actions {
10+
a {
11+
@apply px-1 mx-1 border border-black;
12+
}
13+
}
14+
515
/*
616
717
@layer components {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
class MatchmakingGroupsController < ApplicationController
2+
def index
3+
@groups = CollectGroups.new.call.sort_by { |group| [group.readonly? ? 0 : 1, group.name] }
4+
end
5+
6+
def new
7+
@group = MatchmakingGroup.new
8+
end
9+
10+
def create
11+
if MatchmakingGroup.name_exists?(group_params[:name])
12+
flash[:error] = "Group already exists"
13+
else
14+
MatchmakingGroup.create(group_params.merge(slack_user_id: @current_user.slack_user_id))
15+
flash[:notice] = "Group created"
16+
end
17+
18+
redirect_to matchmaking_groups_path
19+
end
20+
21+
def edit
22+
@group = MatchmakingGroup.find_by(id: params[:id])
23+
redirect_to matchmaking_groups_path if @group.nil?
24+
end
25+
26+
def update
27+
@group = MatchmakingGroup.find_by(id: params[:id])
28+
29+
if @group.name != group_params[:name] && MatchmakingGroup.name_exists?(group_params[:name])
30+
flash[:error] = "Other group already exists with that name"
31+
else
32+
@group = MatchmakingGroup.find_by(id: params[:id])
33+
@group.update(group_params)
34+
35+
flash[:notice] = "Group updated"
36+
end
37+
redirect_to matchmaking_groups_path
38+
end
39+
40+
def destroy
41+
@group = MatchmakingGroup.find_by(id: params[:id])
42+
@group.destroy if @group.present?
43+
redirect_to matchmaking_groups_path
44+
end
45+
46+
private
47+
48+
def group_params
49+
params.require(:matchmaking_group)
50+
.permit(:name, :slack_channel_name, :schedule, :target_size, :is_active)
51+
.tap do |hash|
52+
hash[:name] = hash[:name].strip
53+
end
54+
end
55+
end

app/jobs/establish_matches_for_grouping_job.rb

-46
This file was deleted.

app/models/matchmaking_group.rb

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
class MatchmakingGroup < ApplicationRecord
2+
validate :name_not_in_config
3+
validates :name, uniqueness: true
4+
5+
def self.name_exists?(name)
6+
Rails.application.config.x.matchmaking.to_h.transform_keys(&:to_s).key?(name) || exists?(name: name)
7+
end
8+
9+
def active?
10+
is_active
11+
end
12+
13+
def active
14+
is_active
15+
end
16+
17+
def active=(value)
18+
self.is_active = value
19+
end
20+
21+
def channel
22+
slack_channel_name
23+
end
24+
25+
def channel=(value)
26+
self.slack_channel_name = value
27+
end
28+
29+
def size
30+
target_size
31+
end
32+
33+
def size=(value)
34+
self.target_size = value
35+
end
36+
37+
private
38+
39+
def name_not_in_config
40+
if Rails.application.config.x.matchmaking.to_h.key?(name.intern)
41+
errors.add(:name, "cannot be the same as a key in the matchmaking config")
42+
end
43+
end
44+
end

app/services/collect_groups.rb

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
class CollectGroups
2+
def initialize
3+
@config = Rails.application.config.x.matchmaking
4+
end
5+
6+
def call
7+
extra_groups = MatchmakingGroup.all
8+
readonly_groups + extra_groups
9+
end
10+
11+
private
12+
13+
def readonly_groups
14+
@config.to_h.map do |name, group_config|
15+
normalized = group_config.to_h.transform_keys do |key|
16+
next :target_size if key.intern == :size
17+
next :is_active if key.intern == :active
18+
next :slack_channel_name if key.intern == :channel
19+
key
20+
end
21+
22+
group = MatchmakingGroup.new(normalized.merge(name: name))
23+
group.define_singleton_method(:readonly?) { true }
24+
group
25+
end
26+
end
27+
end

app/services/mailer/builds_grouping_mailer_message.rb app/services/mailer/build_group_mailer_message.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
module Mailer
2-
class BuildsGroupingMailerMessage
2+
class BuildGroupMailerMessage
33
def render(recipient:, channel:, grouping:, other_members:)
44
GroupingMailer.encourage_match(
55
recipient: recipient,
+4-9
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,11 @@
11
module Matchmaking
22
class ChooseStrategy
3-
def initialize(config: nil)
4-
@config = config || Rails.application.config.x.matchmaking
5-
end
6-
7-
def call(grouping)
8-
group_config = @config.send(grouping.intern)
9-
return nil unless group_config&.active
3+
def call(group)
4+
return nil unless group&.active?
105

11-
return Strategies::PairByFewestEncounters.new if group_config.size == 2
6+
return Strategies::PairByFewestEncounters.new if group.target_size == 2
127

13-
Strategies::ArrangeGroupsGenetically.new(target_group_size: group_config.size)
8+
Strategies::ArrangeGroupsGenetically.new(target_group_size: group.target_size)
149
end
1510
end
1611
end

app/services/matchmaking/collect_scored_participants.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ def initialize
44
@assign_score_to_candidates = AssignScoreToCandidates.new
55
end
66

7-
def call(participants, grouping)
7+
def call(participants, group)
88
participants.reduce({}) do |memo, participant|
9-
recent_matches = HistoricalMatch.scoreable.with_member(participant).in_grouping(grouping)
9+
recent_matches = HistoricalMatch.scoreable.with_member(participant).in_grouping(group.name)
1010
candidates = participants.difference([participant])
1111

1212
scored_candidates = @assign_score_to_candidates.call(candidates, recent_matches)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
module Matchmaking
2+
module Errors
3+
class ChannelNotFound < StandardError
4+
def initialize(group_name, channel_name)
5+
super("No channel found with name '#{channel_name}' for grouping '#{group_name}'")
6+
end
7+
end
8+
end
9+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
module Matchmaking
2+
module Errors
3+
class NoConfiguredChannel < StandardError
4+
def initialize(group_name)
5+
super("No configured channel for grouping '#{group_name}'")
6+
end
7+
end
8+
end
9+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
module Matchmaking
2+
class EstablishMatchesForGroup
3+
def initialize
4+
@loads_slack_channels = Slack::LoadsSlackChannels.new
5+
@loads_slack_channel_members = Slack::LoadsSlackChannelMembers.new
6+
@match_participants = Matchmaking::MatchParticipants.new
7+
end
8+
9+
def call(group)
10+
ensure_channel_configured(group)
11+
12+
channel = fetch_slack_channel(group.slack_channel_name)
13+
ensure_channel_found(channel, group)
14+
15+
participants = @loads_slack_channel_members.call(channel: channel.id)
16+
17+
matches = @match_participants.call(participants, group)
18+
matches.each do |match|
19+
HistoricalMatch.create(
20+
members: match,
21+
grouping: group.name,
22+
matched_on: Date.today,
23+
pending_notifications: [
24+
PendingNotification.create(strategy: "email"),
25+
PendingNotification.create(strategy: "slack")
26+
]
27+
)
28+
end
29+
rescue => e
30+
ReportsError.report(e)
31+
end
32+
33+
private
34+
35+
def ensure_channel_configured(group)
36+
raise Errors::NoConfiguredChannel.new(group.name) unless group.slack_channel_name
37+
end
38+
39+
def ensure_channel_found(channel, group)
40+
raise Errors::ChannelNotFound.new(group.slack_channel_name, group.name) unless channel
41+
end
42+
43+
def fetch_slack_channel(channel_name)
44+
@loads_slack_channels.call(types: "public_channel").find { |channel|
45+
channel.name_normalized == channel_name
46+
}
47+
end
48+
end
49+
end

app/services/matchmaking/match_participants.rb

+5-6
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
11
module Matchmaking
22
class MatchParticipants
3-
def initialize(config: nil)
3+
def initialize
44
@collect_scored_participants = CollectScoredParticipants.new
5-
@config = config || Rails.application.config.x.matchmaking
6-
@choose_strategy = ChooseStrategy.new(config: @config)
5+
@choose_strategy = ChooseStrategy.new
76
end
87

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

13-
scored_participants = @collect_scored_participants.call(participants, grouping)
12+
scored_participants = @collect_scored_participants.call(participants, group)
1413

15-
strategy = @choose_strategy.call(grouping)
14+
strategy = @choose_strategy.call(group)
1615
return [] unless strategy
1716

1817
strategy.call(scored_participants)

0 commit comments

Comments
 (0)