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.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +7 -0
  3. data/README.md +134 -0
  4. data/Rakefile +20 -0
  5. data/app/assets/config/pairer_manifest.js +3 -0
  6. data/app/assets/images/pairer/favicon.ico +0 -0
  7. data/app/assets/images/pairer/sweep.png +0 -0
  8. data/app/assets/javascripts/pairer/application.js +47 -0
  9. data/app/assets/stylesheets/pairer/application.scss +82 -0
  10. data/app/assets/stylesheets/pairer/utility.scss +221 -0
  11. data/app/controllers/pairer/application_controller.rb +31 -0
  12. data/app/controllers/pairer/boards_controller.rb +202 -0
  13. data/app/controllers/pairer/sessions_controller.rb +32 -0
  14. data/app/helpers/pairer/application_helper.rb +4 -0
  15. data/app/jobs/pairer/application_job.rb +5 -0
  16. data/app/jobs/pairer/cleanup_boards_job.rb +9 -0
  17. data/app/mailers/pairer/application_mailer.rb +5 -0
  18. data/app/models/pairer/application_record.rb +19 -0
  19. data/app/models/pairer/board.rb +226 -0
  20. data/app/models/pairer/group.rb +45 -0
  21. data/app/models/pairer/person.rb +7 -0
  22. data/app/views/layouts/pairer/application.html.slim +53 -0
  23. data/app/views/layouts/pairer/application.js.erb +5 -0
  24. data/app/views/pairer/boards/_current_groups.html.slim +11 -0
  25. data/app/views/pairer/boards/_group.html.slim +17 -0
  26. data/app/views/pairer/boards/_group_lock_button.html.slim +4 -0
  27. data/app/views/pairer/boards/_person.html.slim +9 -0
  28. data/app/views/pairer/boards/_recently_accessed_boards.html.slim +26 -0
  29. data/app/views/pairer/boards/_role.html.slim +6 -0
  30. data/app/views/pairer/boards/_stats.html.slim +37 -0
  31. data/app/views/pairer/boards/create_group.js.erb +3 -0
  32. data/app/views/pairer/boards/create_person.js.erb +7 -0
  33. data/app/views/pairer/boards/delete_group.js.erb +7 -0
  34. data/app/views/pairer/boards/index.html.slim +28 -0
  35. data/app/views/pairer/boards/lock_group.js.erb +1 -0
  36. data/app/views/pairer/boards/lock_person.js.erb +1 -0
  37. data/app/views/pairer/boards/show.html.slim +240 -0
  38. data/app/views/pairer/boards/update_group.js.erb +0 -0
  39. data/app/views/pairer/exceptions/show.html.slim +8 -0
  40. data/app/views/pairer/sessions/sign_in.html.slim +10 -0
  41. data/app/views/pairer/shared/_flash.html.slim +6 -0
  42. data/app/views/pairer/shared/svg/_broom.html.slim +2 -0
  43. data/config/locales/en.yml +5 -0
  44. data/config/routes.rb +26 -0
  45. data/db/migrate/20210821001344_add_pairer_tables.rb +34 -0
  46. data/db/seeds.rb +0 -0
  47. data/lib/pairer/config.rb +38 -0
  48. data/lib/pairer/engine.rb +47 -0
  49. data/lib/pairer/version.rb +3 -0
  50. data/lib/pairer.rb +24 -0
  51. data/lib/tasks/pairer_engine_tasks.rake +4 -0
  52. 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,4 @@
1
+ module Pairer
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,5 @@
1
+ module Pairer
2
+ class ApplicationJob < ActiveJob::Base
3
+ self.queue_adapter = :async
4
+ end
5
+ end
@@ -0,0 +1,9 @@
1
+ module Pairer
2
+ class CleanupBoardsJob < ApplicationJob
3
+
4
+ def perform
5
+ Board.where.not(org_id: Pairer.config.allowed_org_ids).destroy_all
6
+ end
7
+
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ module Pairer
2
+ class ApplicationMailer < ActionMailer::Base
3
+ #default from: Pairer.from_email
4
+ end
5
+ 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,7 @@
1
+ module Pairer
2
+ class Person < ApplicationRecord
3
+ belongs_to :board, class_name: "Pairer::Board"
4
+
5
+ validates :name, presence: true, uniqueness: {scope: :board_id, case_sensitive: false}
6
+ end
7
+ 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,5 @@
1
+ $('#flash-container').html("<%= j render partial: 'pairer/shared/flash' %>");
2
+
3
+ <%= yield %>
4
+
5
+ window.init();
@@ -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