pairer 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +7 -0
- data/README.md +134 -0
- data/Rakefile +20 -0
- data/app/assets/config/pairer_manifest.js +3 -0
- data/app/assets/images/pairer/favicon.ico +0 -0
- data/app/assets/images/pairer/sweep.png +0 -0
- data/app/assets/javascripts/pairer/application.js +47 -0
- data/app/assets/stylesheets/pairer/application.scss +82 -0
- data/app/assets/stylesheets/pairer/utility.scss +221 -0
- data/app/controllers/pairer/application_controller.rb +31 -0
- data/app/controllers/pairer/boards_controller.rb +202 -0
- data/app/controllers/pairer/sessions_controller.rb +32 -0
- data/app/helpers/pairer/application_helper.rb +4 -0
- data/app/jobs/pairer/application_job.rb +5 -0
- data/app/jobs/pairer/cleanup_boards_job.rb +9 -0
- data/app/mailers/pairer/application_mailer.rb +5 -0
- data/app/models/pairer/application_record.rb +19 -0
- data/app/models/pairer/board.rb +226 -0
- data/app/models/pairer/group.rb +45 -0
- data/app/models/pairer/person.rb +7 -0
- data/app/views/layouts/pairer/application.html.slim +53 -0
- data/app/views/layouts/pairer/application.js.erb +5 -0
- data/app/views/pairer/boards/_current_groups.html.slim +11 -0
- data/app/views/pairer/boards/_group.html.slim +17 -0
- data/app/views/pairer/boards/_group_lock_button.html.slim +4 -0
- data/app/views/pairer/boards/_person.html.slim +9 -0
- data/app/views/pairer/boards/_recently_accessed_boards.html.slim +26 -0
- data/app/views/pairer/boards/_role.html.slim +6 -0
- data/app/views/pairer/boards/_stats.html.slim +37 -0
- data/app/views/pairer/boards/create_group.js.erb +3 -0
- data/app/views/pairer/boards/create_person.js.erb +7 -0
- data/app/views/pairer/boards/delete_group.js.erb +7 -0
- data/app/views/pairer/boards/index.html.slim +28 -0
- data/app/views/pairer/boards/lock_group.js.erb +1 -0
- data/app/views/pairer/boards/lock_person.js.erb +1 -0
- data/app/views/pairer/boards/show.html.slim +240 -0
- data/app/views/pairer/boards/update_group.js.erb +0 -0
- data/app/views/pairer/exceptions/show.html.slim +8 -0
- data/app/views/pairer/sessions/sign_in.html.slim +10 -0
- data/app/views/pairer/shared/_flash.html.slim +6 -0
- data/app/views/pairer/shared/svg/_broom.html.slim +2 -0
- data/config/locales/en.yml +5 -0
- data/config/routes.rb +26 -0
- data/db/migrate/20210821001344_add_pairer_tables.rb +34 -0
- data/db/seeds.rb +0 -0
- data/lib/pairer/config.rb +38 -0
- data/lib/pairer/engine.rb +47 -0
- data/lib/pairer/version.rb +3 -0
- data/lib/pairer.rb +24 -0
- data/lib/tasks/pairer_engine_tasks.rake +4 -0
- metadata +261 -0
@@ -0,0 +1,202 @@
|
|
1
|
+
require_dependency "pairer/application_controller"
|
2
|
+
|
3
|
+
module Pairer
|
4
|
+
class BoardsController < ApplicationController
|
5
|
+
before_action do
|
6
|
+
if !signed_in?
|
7
|
+
redirect_to sign_in_path
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
before_action :get_board, except: [:index, :new, :create]
|
12
|
+
|
13
|
+
helper_method :people_by_id
|
14
|
+
|
15
|
+
def index
|
16
|
+
if params[:password].present?
|
17
|
+
@board = Pairer::Board.find_by(org_id: session[:pairer_current_org_id], password: params[:password])
|
18
|
+
|
19
|
+
if @board
|
20
|
+
session[:pairer_current_board_id] = @board.to_param
|
21
|
+
redirect_to action: :show, id: session[:pairer_current_board_id]
|
22
|
+
else
|
23
|
+
flash.now.alert = "Board not found."
|
24
|
+
|
25
|
+
render
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def create
|
31
|
+
@board = Pairer::Board.new(password: params[:password], org_id: session[:pairer_current_org_id])
|
32
|
+
|
33
|
+
if @board.save
|
34
|
+
session[:pairer_current_board_id] = @board.to_param
|
35
|
+
flash.notice = "Board created."
|
36
|
+
redirect_to(action: :show, id: @board.to_param)
|
37
|
+
else
|
38
|
+
other_board = Pairer::Board.find_by(org_id: session[:pairer_current_org_id], password: params[:password])
|
39
|
+
|
40
|
+
if other_board
|
41
|
+
session[:pairer_current_board_id] = other_board.to_param
|
42
|
+
flash.notice = "Existing board found."
|
43
|
+
redirect_to(action: :show, id: other_board.to_param)
|
44
|
+
else
|
45
|
+
flash.alert = "Board not saved. Please choose a different password."
|
46
|
+
redirect_to(action: :index)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def show
|
52
|
+
session_key = :pairer_board_access_list
|
53
|
+
|
54
|
+
access_list = session[session_key] || {}
|
55
|
+
|
56
|
+
access_list[@board.public_id] = Time.now.to_s
|
57
|
+
|
58
|
+
session[session_key] = access_list
|
59
|
+
end
|
60
|
+
|
61
|
+
def update
|
62
|
+
if params[:add_role_name]
|
63
|
+
params[:board] = {}
|
64
|
+
params[:board][:roles] = @board.roles_array + [params[:add_role_name]]
|
65
|
+
elsif params[:remove_role_name]
|
66
|
+
params[:board] = {}
|
67
|
+
params[:board][:roles] = @board.roles_array - [params[:remove_role_name]]
|
68
|
+
end
|
69
|
+
|
70
|
+
if params[:clear_board]
|
71
|
+
@board.groups.destroy_all
|
72
|
+
@board.update!(current_iteration_number: 0)
|
73
|
+
saved = true
|
74
|
+
else
|
75
|
+
saved = @board.update(params.require(:board).permit(:name, :password, :group_size, :num_iterations_to_track, roles: []))
|
76
|
+
end
|
77
|
+
|
78
|
+
if saved
|
79
|
+
if params.dig(:board, :password)
|
80
|
+
flash.notice = "Password updated."
|
81
|
+
else
|
82
|
+
flash.notice = "Board updated."
|
83
|
+
end
|
84
|
+
else
|
85
|
+
if @board.errors[:num_iterations_to_track].present?
|
86
|
+
flash.alert = "Number of Iterations to Track #{@board.errors[:num_iterations_to_track].first}"
|
87
|
+
else
|
88
|
+
flash.alert = "Board not saved."
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
redirect_to(action: :show)
|
93
|
+
end
|
94
|
+
|
95
|
+
def destroy
|
96
|
+
@board.destroy!
|
97
|
+
flash.notice = "Board deleted."
|
98
|
+
redirect_to(action: :index)
|
99
|
+
end
|
100
|
+
|
101
|
+
def shuffle
|
102
|
+
@board.shuffle!
|
103
|
+
redirect_to(action: :show)
|
104
|
+
end
|
105
|
+
|
106
|
+
def create_person
|
107
|
+
@person = @board.people.create(name: params[:name])
|
108
|
+
|
109
|
+
if request.format.js?
|
110
|
+
render
|
111
|
+
else
|
112
|
+
redirect_to(action: :show)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def lock_person
|
117
|
+
@person = @board.people.find_by!(public_id: params.require(:person_id))
|
118
|
+
|
119
|
+
@person.toggle!(:locked)
|
120
|
+
|
121
|
+
if request.format.js?
|
122
|
+
render
|
123
|
+
else
|
124
|
+
redirect_to(action: :show)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def delete_person
|
129
|
+
@person = @board.people.find_by!(public_id: params.require(:person_id))
|
130
|
+
|
131
|
+
@person.destroy!
|
132
|
+
|
133
|
+
redirect_to(action: :show)
|
134
|
+
end
|
135
|
+
|
136
|
+
def create_group
|
137
|
+
@group = @board.groups.create!(board_iteration_number: @board.current_iteration_number)
|
138
|
+
|
139
|
+
if request.format.js?
|
140
|
+
render
|
141
|
+
else
|
142
|
+
redirect_to(action: :show)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def lock_group
|
147
|
+
@group = @board.groups.find_by!(public_id: params.require(:group_id))
|
148
|
+
|
149
|
+
@group.toggle!(:locked)
|
150
|
+
|
151
|
+
if request.format.js?
|
152
|
+
render
|
153
|
+
else
|
154
|
+
redirect_to(action: :show)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def delete_group
|
159
|
+
@group = @board.groups.find_by!(public_id: params.require(:group_id))
|
160
|
+
|
161
|
+
@group.destroy!
|
162
|
+
|
163
|
+
if request.format.js?
|
164
|
+
render
|
165
|
+
else
|
166
|
+
redirect_to(action: :show)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def update_group
|
171
|
+
@group = @board.groups.find_by!(public_id: params.require(:group_id))
|
172
|
+
|
173
|
+
attrs = {
|
174
|
+
person_ids: (params[:person_ids] if params[:person_ids].present?),
|
175
|
+
roles: (params[:roles] if params[:roles].present?),
|
176
|
+
}.compact
|
177
|
+
|
178
|
+
@group.update!(attrs)
|
179
|
+
|
180
|
+
if request.format.js?
|
181
|
+
render
|
182
|
+
else
|
183
|
+
redirect_to(action: :show)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
private
|
188
|
+
|
189
|
+
def get_board
|
190
|
+
if session[:pairer_current_board_id].blank?
|
191
|
+
redirect_to(action: :index)
|
192
|
+
end
|
193
|
+
|
194
|
+
@board = Pairer::Board.find_by!(org_id: session[:pairer_current_org_id], public_id: params[:id])
|
195
|
+
end
|
196
|
+
|
197
|
+
def people_by_id
|
198
|
+
@people_by_id ||= @board.people.map{|x| [x.to_param, x] }.to_h
|
199
|
+
end
|
200
|
+
|
201
|
+
end
|
202
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require_dependency "pairer/application_controller"
|
2
|
+
|
3
|
+
module Pairer
|
4
|
+
class SessionsController < ApplicationController
|
5
|
+
|
6
|
+
def sign_in
|
7
|
+
if request.method == "GET"
|
8
|
+
if signed_in?
|
9
|
+
redirect_to boards_path
|
10
|
+
end
|
11
|
+
|
12
|
+
elsif request.method == "POST"
|
13
|
+
if Pairer.config.allowed_org_ids.include?(params[:org_id]&.downcase)
|
14
|
+
session[:pairer_current_org_id] = params[:org_id].downcase
|
15
|
+
redirect_to boards_path
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def sign_out
|
21
|
+
if !signed_in?
|
22
|
+
redirect_to action: :sign_in
|
23
|
+
else
|
24
|
+
session.delete(:pairer_current_org_id)
|
25
|
+
session.delete(:pairer_current_board_id)
|
26
|
+
flash.notice = "Signed out"
|
27
|
+
redirect_to sign_in_path
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Pairer
|
2
|
+
class ApplicationRecord < ActiveRecord::Base
|
3
|
+
self.abstract_class = true
|
4
|
+
|
5
|
+
after_create do
|
6
|
+
salt = Pairer.config.hash_id_salt
|
7
|
+
pepper = self.class.table_name
|
8
|
+
|
9
|
+
self.update_columns(public_id: Hashids.new("#{salt}_#{pepper}", 8).encode(id))
|
10
|
+
end
|
11
|
+
|
12
|
+
validates :public_id, uniqueness: {case_sensitive: true, allow_blank: true}
|
13
|
+
|
14
|
+
def to_param
|
15
|
+
try(:public_id) || id
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,226 @@
|
|
1
|
+
module Pairer
|
2
|
+
class Board < ApplicationRecord
|
3
|
+
RECENT_RESHUFFLE_DURATION = 1.minute.freeze
|
4
|
+
NUM_CANDIDATE_GROUPINGS = 5
|
5
|
+
|
6
|
+
has_many :people, class_name: "Pairer::Person", dependent: :destroy
|
7
|
+
has_many :groups, class_name: "Pairer::Group", dependent: :destroy
|
8
|
+
|
9
|
+
validates :name, presence: true
|
10
|
+
validates :org_id, presence: true, inclusion: {in: ->(x){ Pairer.config.allowed_org_ids }}
|
11
|
+
validates :password, presence: true, uniqueness: {message: "invalid password, please use a different one", scope: :org_id}
|
12
|
+
validates :current_iteration_number, presence: true, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
|
13
|
+
validates :num_iterations_to_track, presence: true, numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: ->(x){ Pairer.config.max_iterations_to_track } }
|
14
|
+
validates :group_size, presence: true, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
|
15
|
+
|
16
|
+
before_validation on: :create do
|
17
|
+
self.name ||= "New Board"
|
18
|
+
self.current_iteration_number ||= 0
|
19
|
+
self.num_iterations_to_track ||= 15
|
20
|
+
self.group_size ||= 2
|
21
|
+
end
|
22
|
+
|
23
|
+
def current_groups
|
24
|
+
groups.where(board_iteration_number: current_iteration_number)
|
25
|
+
end
|
26
|
+
|
27
|
+
def tracked_groups
|
28
|
+
groups
|
29
|
+
.order(board_iteration_number: :desc)
|
30
|
+
.where("board_iteration_number > #{current_iteration_number - num_iterations_to_track}")
|
31
|
+
end
|
32
|
+
|
33
|
+
def roles=(val)
|
34
|
+
if val.is_a?(Array)
|
35
|
+
self[:roles] = val.map{|x| x.presence&.strip }.uniq(&:downcase).compact.sort.join(";;")
|
36
|
+
else
|
37
|
+
raise "invalid behaviour"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def roles_array
|
42
|
+
self[:roles]&.split(";;") || []
|
43
|
+
end
|
44
|
+
|
45
|
+
def shuffle!
|
46
|
+
new_groups = []
|
47
|
+
|
48
|
+
prev_iteration_number = current_iteration_number
|
49
|
+
next_iteration_number = current_iteration_number + 1
|
50
|
+
|
51
|
+
available_person_ids = people.select{|x| !x.locked? }.collect(&:public_id)
|
52
|
+
available_roles = roles_array
|
53
|
+
|
54
|
+
### Build New Groups
|
55
|
+
groups.where(board_iteration_number: prev_iteration_number).each do |g|
|
56
|
+
if g.locked?
|
57
|
+
### Clone Locked Groups
|
58
|
+
|
59
|
+
new_group = g.dup
|
60
|
+
|
61
|
+
new_group.assign_attributes(
|
62
|
+
public_id: nil,
|
63
|
+
board_iteration_number: next_iteration_number,
|
64
|
+
)
|
65
|
+
|
66
|
+
new_groups << new_group
|
67
|
+
|
68
|
+
available_person_ids = (available_person_ids - g.person_ids_array)
|
69
|
+
|
70
|
+
available_roles = (available_roles - new_group.roles_array)
|
71
|
+
else
|
72
|
+
### Retain Position of Locked People within Existing Groups
|
73
|
+
|
74
|
+
group_locked_person_ids = (g.person_ids_array - available_person_ids)
|
75
|
+
|
76
|
+
if group_locked_person_ids.any?
|
77
|
+
new_group = groups.new(
|
78
|
+
board_iteration_number: next_iteration_number,
|
79
|
+
roles: [],
|
80
|
+
person_ids: group_locked_person_ids,
|
81
|
+
)
|
82
|
+
|
83
|
+
new_groups << new_group
|
84
|
+
|
85
|
+
available_person_ids = (available_person_ids - group_locked_person_ids)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
self.increment!(:current_iteration_number)
|
91
|
+
|
92
|
+
if available_person_ids.any?
|
93
|
+
pair_stats_hash = stats_hash_for_two_pairs
|
94
|
+
|
95
|
+
### Assign People to Non-Full Unlocked Groups
|
96
|
+
new_groups.select{|x| !x.locked? }.each do |g|
|
97
|
+
break if available_person_ids.empty?
|
98
|
+
|
99
|
+
num_to_add = self.group_size - g.person_ids_array.size
|
100
|
+
|
101
|
+
next if num_to_add <= 0
|
102
|
+
|
103
|
+
if available_person_ids.size < num_to_add
|
104
|
+
### Add to group whatever is left
|
105
|
+
|
106
|
+
g.person_ids = g.person_ids_array + available_person_ids
|
107
|
+
|
108
|
+
available_person_ids = []
|
109
|
+
|
110
|
+
break
|
111
|
+
end
|
112
|
+
|
113
|
+
group_size_combinations = available_person_ids.combination(num_to_add).map{|x| x + g.person_ids_array }.shuffle
|
114
|
+
|
115
|
+
### Choose group using minimum score
|
116
|
+
chosen_person_ids = group_size_combinations.min_by do |person_ids|
|
117
|
+
person_ids.combination(2).map(&:sort).sum{|k| pair_stats_hash[k] || 0 }
|
118
|
+
end
|
119
|
+
|
120
|
+
g.person_ids = (g.person_ids_array | chosen_person_ids).sort
|
121
|
+
end
|
122
|
+
|
123
|
+
### Assign People to New Groups
|
124
|
+
while available_person_ids.any? do
|
125
|
+
if available_person_ids.size <= self.group_size
|
126
|
+
### Create group using whats left
|
127
|
+
|
128
|
+
new_groups << groups.new(
|
129
|
+
board_iteration_number: next_iteration_number,
|
130
|
+
person_ids: available_person_ids,
|
131
|
+
)
|
132
|
+
|
133
|
+
available_person_ids = []
|
134
|
+
else
|
135
|
+
group_size_combinations = available_person_ids.combination(self.group_size).to_a.shuffle
|
136
|
+
|
137
|
+
### Choose group using minimum score
|
138
|
+
chosen_person_ids = group_size_combinations.min_by do |person_ids|
|
139
|
+
person_ids.combination(2).map(&:sort).sum{|k| pair_stats_hash[k] || 0 }
|
140
|
+
end
|
141
|
+
|
142
|
+
new_groups << groups.new(
|
143
|
+
board_iteration_number: next_iteration_number,
|
144
|
+
person_ids: chosen_person_ids,
|
145
|
+
)
|
146
|
+
|
147
|
+
available_person_ids = (available_person_ids - chosen_person_ids)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
### Shuffle Roles
|
152
|
+
available_roles = available_roles.shuffle
|
153
|
+
|
154
|
+
unlocked_new_groups = new_groups.select{|x| !x.locked? }
|
155
|
+
|
156
|
+
### Assign Roles to Groups
|
157
|
+
available_roles.in_groups(unlocked_new_groups.size, false).each_with_index do |roles, i|
|
158
|
+
unlocked_new_groups[i].roles = unlocked_new_groups[i].roles_array + roles
|
159
|
+
available_roles = available_roles - roles
|
160
|
+
end
|
161
|
+
|
162
|
+
### Save New Groups
|
163
|
+
new_groups.each{|x| x.save! }
|
164
|
+
end
|
165
|
+
|
166
|
+
### Delete empty groups
|
167
|
+
groups
|
168
|
+
.where(person_ids: [nil, ""])
|
169
|
+
.each{|x| x.destroy! }
|
170
|
+
|
171
|
+
### Delete outdated groups
|
172
|
+
groups
|
173
|
+
.where.not(id: tracked_groups.collect(&:id))
|
174
|
+
.each{|x| x.destroy! }
|
175
|
+
|
176
|
+
### Ensure stats do not contain bogus entries caused by re-shuffling, groups created less than x time ago are deleted upon shuffle
|
177
|
+
if !Rails.env.test? || (Rails.env.test? && ENV['DELETE_RECENT_RESHUFFLED'].to_s == "true")
|
178
|
+
groups
|
179
|
+
.where.not(id: new_groups.collect(&:id))
|
180
|
+
.where("#{Pairer::Group.table_name}.created_at >= ?", RECENT_RESHUFFLE_DURATION.ago)
|
181
|
+
.each{|x| x.destroy! }
|
182
|
+
end
|
183
|
+
|
184
|
+
### Reload groups to fix any issues with caching after creations and deletions
|
185
|
+
groups.reload
|
186
|
+
|
187
|
+
return true
|
188
|
+
end
|
189
|
+
|
190
|
+
def stats
|
191
|
+
array = []
|
192
|
+
|
193
|
+
stats_hash_for_two_pairs.sort_by{|k,count| -count }.each do |person_ids, count|
|
194
|
+
array << [person_ids, count]
|
195
|
+
end
|
196
|
+
|
197
|
+
return array
|
198
|
+
end
|
199
|
+
|
200
|
+
private
|
201
|
+
|
202
|
+
def stats_hash_for_two_pairs
|
203
|
+
h = {}
|
204
|
+
|
205
|
+
tracked_groups.each do |group|
|
206
|
+
group_person_ids = group.person_ids_array
|
207
|
+
|
208
|
+
### For combinations size, we use 2 instead of self.group_size as we are running stats on pairs, not groups
|
209
|
+
if group_person_ids.size == 1
|
210
|
+
h[group_person_ids] ||= 0
|
211
|
+
h[group_person_ids] += 1
|
212
|
+
else
|
213
|
+
combinations = group_person_ids.combination(2)
|
214
|
+
|
215
|
+
combinations.map{|x| x.sort }.each do |sorted_pair_person_ids|
|
216
|
+
h[sorted_pair_person_ids] ||= 0
|
217
|
+
h[sorted_pair_person_ids] += 1
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
return h
|
223
|
+
end
|
224
|
+
|
225
|
+
end
|
226
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Pairer
|
2
|
+
class Group < ApplicationRecord
|
3
|
+
belongs_to :board, class_name: "Pairer::Board"
|
4
|
+
|
5
|
+
validates :board_id, presence: true
|
6
|
+
validates :board_iteration_number, presence: true, numericality: { only_integer: true, minimum: 0 }
|
7
|
+
|
8
|
+
def person_ids=(val)
|
9
|
+
if val.is_a?(Array)
|
10
|
+
sanitized_array = val.map{|x| x.presence&.strip }.uniq(&:downcase).compact
|
11
|
+
|
12
|
+
if !new_record?
|
13
|
+
sanitized_array = sanitized_array.intersection(board.people.map(&:public_id)) ### This may slow the query down
|
14
|
+
end
|
15
|
+
|
16
|
+
self[:person_ids] = sanitized_array.join(";;")
|
17
|
+
else
|
18
|
+
raise "invalid behaviour"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def person_ids_array
|
23
|
+
self[:person_ids]&.split(";;") || []
|
24
|
+
end
|
25
|
+
|
26
|
+
def roles=(val)
|
27
|
+
if val.is_a?(Array)
|
28
|
+
sanitized_array = self[:roles] = val.map{|x| x.presence&.strip }.uniq(&:downcase).compact
|
29
|
+
|
30
|
+
if !new_record?
|
31
|
+
sanitized_array = sanitized_array.intersection(board.roles_array) ### This may slow the query down
|
32
|
+
end
|
33
|
+
|
34
|
+
self[:roles] = sanitized_array.join(";;")
|
35
|
+
else
|
36
|
+
raise "invalid behaviour"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def roles_array
|
41
|
+
self[:roles]&.split(";;") || []
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
- @title ||= 'Pairer'
|
2
|
+
- @description ||= "A tool to help assign working pairs"
|
3
|
+
|
4
|
+
doctype html
|
5
|
+
html
|
6
|
+
head
|
7
|
+
title = @title
|
8
|
+
|
9
|
+
= csrf_meta_tags
|
10
|
+
meta name="viewport" content="width=device-width, height=device-height, initial-scale=1.0"
|
11
|
+
|
12
|
+
= favicon_link_tag "pairer/favicon.ico"
|
13
|
+
|
14
|
+
= stylesheet_link_tag 'pairer/application', media: 'all'
|
15
|
+
|
16
|
+
script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js" referrerpolicy="no-referrer"
|
17
|
+
|
18
|
+
link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/3.2.1/css/font-awesome.min.css" integrity="sha512-IJ+BZHGlT4K43sqBGUzJ90pcxfkREDVZPZxeexRigVL8rzdw/gyJIflDahMdNzBww4k0WxpyaWpC2PLQUWmMUQ==" crossorigin="anonymous" referrerpolicy="no-referrer"
|
19
|
+
|
20
|
+
link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" integrity="sha512-SfTiTlX6kk+qitfevl/7LibUOeJWlt9rbyDn92a1DqWOw9vWG2MFoays0sgObmWazO5BQPiFucnnEAjpAB+/Sw==" crossorigin="anonymous" referrerpolicy="no-referrer"
|
21
|
+
|
22
|
+
link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.13.2/themes/base/jquery-ui.min.css" integrity="sha512-ELV+xyi8IhEApPS/pSj66+Jiw+sOT1Mqkzlh8ExXihe4zfqbWkxPRi8wptXIO9g73FSlhmquFlUOuMSoXz5IRw==" crossorigin="anonymous" referrerpolicy="no-referrer"
|
23
|
+
script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.13.2/jquery-ui.min.js" integrity="sha512-57oZ/vW8ANMjR/KQ6Be9v/+/h6bq9/l3f0Oc7vn6qMqyhvPd1cvKBRWWpzu0QoneImqr2SkmO4MSqU+RpHom3Q==" crossorigin="anonymous" referrerpolicy="no-referrer"
|
24
|
+
|
25
|
+
= javascript_include_tag 'pairer/application'
|
26
|
+
|
27
|
+
body
|
28
|
+
nav.navbar.navbar-inverse.navbar-fixed-top
|
29
|
+
.container-fluid
|
30
|
+
.navbar-header
|
31
|
+
i.fa.fa-bars.navbar-toggle.visible-sm.visible-xs style="color: white; cursor: pointer;" data-toggle="collapse" data-target="#nav" title="Show/Hide Menu"
|
32
|
+
h1.hidden = @title
|
33
|
+
a.navbar-brand href=root_path = "#{@title}"
|
34
|
+
|
35
|
+
.navbar-collapse.collapse#nav
|
36
|
+
- if signed_in?
|
37
|
+
ul.nav.navbar-nav
|
38
|
+
li class=('active' if params[:action] == "index")
|
39
|
+
a href=boards_path Find Board
|
40
|
+
- if @board
|
41
|
+
li.active
|
42
|
+
a href=board_path(@board) View Board
|
43
|
+
|
44
|
+
ul.nav.navbar-nav.navbar-right
|
45
|
+
li
|
46
|
+
a href=sign_out_path
|
47
|
+
i.fa.fa-sign-out
|
48
|
+
span.space-left.space-left Sign Out
|
49
|
+
|
50
|
+
.container-fluid
|
51
|
+
= render "pairer/shared/flash"
|
52
|
+
|
53
|
+
= yield
|
@@ -0,0 +1,11 @@
|
|
1
|
+
table.table-bordered.table.current-groups
|
2
|
+
thead
|
3
|
+
th.text-center style="width: 130px;"
|
4
|
+
= link_to shuffle_board_path(@board), class: "btn btn-success btn-sm", data: {method: :post} do
|
5
|
+
i.icon-refresh.space-right
|
6
|
+
span Shuffle
|
7
|
+
th style="width: 50%; padding-left: 25px;" People
|
8
|
+
th style="padding-left: 25px;" Roles
|
9
|
+
tbody
|
10
|
+
- @board.current_groups.each do |group|
|
11
|
+
= render "group", group: group
|
@@ -0,0 +1,17 @@
|
|
1
|
+
tr.group-row data-group-id=group.public_id
|
2
|
+
td.text-center
|
3
|
+
.space-above2
|
4
|
+
= render "group_lock_button", group: group
|
5
|
+
|
6
|
+
= link_to delete_group_board_path(@board, group_id: group), data: {method: :delete}, remote: true, class: 'btn btn-xs space-left2 group-sweep-btn', title: "Sweep" do
|
7
|
+
= render "pairer/shared/svg/broom"
|
8
|
+
|
9
|
+
td.person-list data-group-id=group.public_id data-prev-person-ids=group.person_ids_array style="padding-left: 20px;"
|
10
|
+
- group.person_ids_array.each do |person_public_id|
|
11
|
+
- person = people_by_id[person_public_id]
|
12
|
+
- if person
|
13
|
+
= render 'person', person: person
|
14
|
+
|
15
|
+
td.roles-list data-group-id=group.public_id data-prev-roles=group.roles_array style="padding-left: 20px;"
|
16
|
+
- group.roles_array.each do |role|
|
17
|
+
= render 'role', role: role
|
@@ -0,0 +1,4 @@
|
|
1
|
+
= link_to lock_group_board_path(@board, group_id: group), data: {method: :post}, remote: true, class: "btn btn-xs group-lock-btn #{group.locked? ? 'btn-highlight' : 'btn-default'}", title: "Group #{group.locked? ? "Locked" : "Unlocked"}" do
|
2
|
+
i class=(group.locked? ? 'icon-lock' : 'icon-unlock')
|
3
|
+
- if group.locked?
|
4
|
+
= " Locked"
|
@@ -0,0 +1,9 @@
|
|
1
|
+
span.person data-person-id=person.public_id
|
2
|
+
= person.name
|
3
|
+
|
4
|
+
= link_to lock_person_board_path(@board, person_id: person), data: {method: :post}, remote: true, class: "btn btn-xs space-left2 #{person.locked? ? 'btn-highlight' : 'btn-default'}", title: "#{person.locked? ? "Locked" : "Unlocked"}" do
|
5
|
+
i class=(person.locked? ? 'icon-lock' : 'icon-unlock')
|
6
|
+
|
7
|
+
span.space-left.delete
|
8
|
+
= link_to delete_person_board_path(@board, person_id: person), data: {method: :delete, confirm: "Are you sure you want to delete person '#{person.name}'?"}, title: "Delete Person", class: 'btn btn-xs btn-danger' do
|
9
|
+
i.icon-trash
|