pairer 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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