voyage 1.44.0.6 → 1.44.0.7

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f0eaaf85bbe8db4e1ad19731f0cd98f8ad1fb2db
4
- data.tar.gz: 04a3370b1789e18342f71442848498ef5bd19641
3
+ metadata.gz: f3069f490551d031bd77d95c0f11fafd36608a7c
4
+ data.tar.gz: 2cf4fe7cc391b8a195be17da2d72bf849d216cd1
5
5
  SHA512:
6
- metadata.gz: 92f26dd7993995eb9a232811218299db77fc4dfe0bd028c73617e7a1b465a330926daefc743c2d524347f04ce8dd78c12d534402f9a773b238a03e5c699a17d6
7
- data.tar.gz: 158df60d57f4a7aaf4933b0593bb45f7e3c9c5bc522f7b372d4697bf7ff76cab91109c17028b01f7e9d133aa8b3e03ed011644e2b2b0bc1e61942f212184f436
6
+ metadata.gz: aa91712c84e2ef27758b693e36ef471788611f97c99454285a2662bd596fae2fa7f24142bcf622f05b74a46b1ac3c8d8e8519bb6ffb8ff99eea752a1c7229201
7
+ data.tar.gz: c4812939c3c07301063aaf17baaa4cfee6af9c38b052baad26334f283df7077478c1600978cbfe4f30f580b2cb767c9ff9d6178bf6abeb16853a79f1e03a362f
data/TODO.md ADDED
@@ -0,0 +1,4 @@
1
+ - [ ] Install, then run Rstrip gem with a config file. I have one in home directory.
2
+
3
+ INTERCOM_SECURE_MODE_SECRET_KEY
4
+ SEGMENT_KEY
data/lib/voyage/README.md CHANGED
@@ -20,3 +20,29 @@ Everything else is a new file we want to add.
20
20
  ## Testing
21
21
 
22
22
  Test that the new generator works, manually for now. It'd be awesome to get some [aruba](https://github.com/cucumber/aruba) tests going to test the various command line options / generated app permutations that are possible. For example, with and without devise, which templating language we should use, etc.
23
+
24
+ ## Pushing a new release
25
+
26
+ * Bump the version file: `lib/voyage/version.rb`
27
+
28
+ VERSION = '1.44.0.6'.freeze
29
+
30
+ * Tag the current commits on master BEFORE squashing (in case we want to refer to that diff history). Add a good commit message with what was done.
31
+
32
+ git tag -a 1.44.0.6-voyage
33
+
34
+ * Squash all new commits (assumed 3 here) into the 2 main commits (for a total of 5)
35
+
36
+ git rebase -i HEAD~5
37
+
38
+ * Force push the changes to master
39
+
40
+ git push --force-with-lease --no-verify
41
+
42
+ * Build the gem
43
+
44
+ gem build suspenders.gemspec
45
+
46
+ * Publish to rubygems
47
+
48
+ gem push voyage-1.44.0.6.gem
@@ -17,20 +17,24 @@ module Suspenders
17
17
 
18
18
  def update_gemset_in_gemfile
19
19
  replace_in_file 'Gemfile', '#ruby-gemset', "#ruby-gemset=#{app_name}"
20
+
21
+ # Remove commented out lines from template
22
+ gsub_file('Gemfile', /^\s{2}\n/, '')
20
23
  end
21
24
 
22
25
  def use_slim
23
26
  if @@accept_defaults || agree?('Would you like to use slim? (Y/n)')
24
27
  @@use_slim = true
25
28
  run 'gem install html2slim'
26
- update_application_layout_for_slim
29
+ update_application_rb_for_slim
27
30
  else
28
31
  @@use_slim = false
32
+ gsub_file('Gemfile', /^gem 'slim-rails'\n/, '')
29
33
  end
30
34
  end
31
35
 
32
36
  def update_application_layout_for_slim
33
- find = <<-RUBY.gsub(/^ {6}/, '')
37
+ find = <<-RUBY.gsub(/^ {4}/, '')
34
38
  <%#
35
39
  Configure default and controller-, and view-specific titles in
36
40
  config/locales/en.yml. For more see:
@@ -38,10 +42,10 @@ module Suspenders
38
42
  %>
39
43
  RUBY
40
44
 
41
- replace = <<-RUBY.gsub(/^ {6}/, '')
42
- <% # Configure default and controller-, and view-specific titles in
43
- # config/locales/en.yml. For more see:
44
- # https://github.com/calebthompson/title#usage %>
45
+ replace = <<-RUBY.gsub(/^ {8}/, '')
46
+ <% # Configure default and controller-, and view-specific titles in
47
+ # config/locales/en.yml. For more see:
48
+ # https://github.com/calebthompson/title#usage %>
45
49
  RUBY
46
50
 
47
51
  replace_in_file 'app/views/layouts/application.html.erb', find, replace
@@ -49,9 +53,6 @@ module Suspenders
49
53
  inside('lib') do # arbitrary, run in context of newly generated app
50
54
  run "erb2slim '../app/views/layouts' '../app/views/layouts'"
51
55
  run "erb2slim -d '../app/views/layouts'"
52
-
53
- run "erb2slim '../app/views/application' '../app/views/application'"
54
- run "erb2slim -d '../app/views/application'"
55
56
  end
56
57
 
57
58
  # strip trailing space after closing "> in application layout before
@@ -60,32 +61,23 @@ module Suspenders
60
61
 
61
62
  find = <<-RUBY.gsub(/^ {6}/, '')
62
63
  | <body class="
64
+ = devise_controller? ? 'devise' : 'application'
63
65
  = body_class
64
66
  | ">
65
67
  RUBY
66
68
 
67
69
  replace = <<-RUBY.gsub(/^ {6}/, '')
68
- body class="\#{body_class}"
70
+ body class="\#{devise_controller? ? 'devise' : 'application'} \#{body_class}"
69
71
  RUBY
70
72
 
71
73
  replace_in_file 'app/views/layouts/application.html.slim', find, replace
74
+ end
72
75
 
73
- find = <<-RUBY.gsub(/^ {6}/, '')
74
- = render "flashes"
75
- = yield
76
- = render "javascript"
77
- = render "css_overrides"
78
- RUBY
79
-
80
- # Bump renders in to be nested within body
81
- replace = <<-RUBY.gsub(/^ {6}/, '')
82
- = render "flashes"
83
- = yield
84
- = render "javascript"
85
- = render "css_overrides"
86
- RUBY
87
-
88
- replace_in_file 'app/views/layouts/application.html.slim', find, replace
76
+ def update_application_rb_for_slim
77
+ inject_into_file "config/application.rb", after: " g.fixture_replacement :factory_girl, dir: 'spec/factories'\n" do <<-'RUBY'.gsub(/^ {2}/, '')
78
+ g.template_engine :slim
79
+ RUBY
80
+ end
89
81
  end
90
82
 
91
83
  # ------------
@@ -93,17 +85,20 @@ module Suspenders
93
85
  # ------------
94
86
  def install_devise
95
87
  if @@accept_defaults || agree?('Would you like to install Devise? (Y/n)')
88
+ @@use_devise = true
96
89
  bundle_command 'exec rails generate devise:install'
97
90
 
98
91
  if @@accept_defaults || agree?("Would you like to add first_name and last_name to the devise model? (Y/n)")
99
92
  adding_first_and_last_name = true
100
93
 
101
- bundle_command "exec rails generate resource user first_name:string last_name:string"
94
+ bundle_command "exec rails generate resource user first_name:string last_name:string uuid:string"
102
95
 
103
96
  replace_in_file 'spec/factories/users.rb',
104
97
  'first_name "MyString"', 'first_name { Faker::Name.first_name }'
105
98
  replace_in_file 'spec/factories/users.rb',
106
99
  'last_name "MyString"', 'last_name { Faker::Name.last_name }'
100
+ replace_in_file 'spec/factories/users.rb',
101
+ 'uuid "MyString"', 'uuid { SecureRandom.uuid }'
107
102
  end
108
103
 
109
104
  bundle_command "exec rails generate devise user"
@@ -118,12 +113,14 @@ module Suspenders
118
113
 
119
114
  customize_devise_views if adding_first_and_last_name
120
115
  customize_application_controller_for_devise(adding_first_and_last_name)
116
+ add_devise_registrations_controller
121
117
  customize_resource_controller_for_devise(adding_first_and_last_name)
122
- add_views_for_devise_resource(adding_first_and_last_name)
118
+ add_admin_views_for_devise_resource(adding_first_and_last_name)
119
+ add_analytics_initializer
123
120
  authorize_devise_resource_for_index_action
124
121
  add_canard_roles_to_devise_resource
125
122
  update_devise_initializer
126
- add_sign_in_and_sign_out_routes_for_devise
123
+ add_custom_routes_for_devise
127
124
  customize_user_factory(adding_first_and_last_name)
128
125
  generate_seeder_templates(using_devise: true)
129
126
  customize_user_spec
@@ -153,13 +150,67 @@ module Suspenders
153
150
  end
154
151
 
155
152
  def customize_application_controller_for_devise(adding_first_and_last_name)
153
+ inject_into_file 'app/controllers/application_controller.rb', before: "class ApplicationController < ActionController::Base" do <<-RUBY.gsub(/^ {8}/, '')
154
+ # rubocop:disable Metrics/ClassLength, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/LineLength
155
+ RUBY
156
+ end
157
+
156
158
  inject_into_file 'app/controllers/application_controller.rb', after: " protect_from_forgery with: :exception" do <<-RUBY.gsub(/^ {6}/, '')
157
159
 
160
+ check_authorization unless: :devise_or_pages_controller?
161
+ impersonates :user
162
+
158
163
  before_action :configure_permitted_parameters, if: :devise_controller?
164
+ before_action :authenticate_user!, unless: -> { is_a?(HighVoltage::PagesController) }
165
+ before_action :add_layout_name_to_gon
166
+ before_action :detect_device_type
167
+
168
+ rescue_from CanCan::AccessDenied do |exception|
169
+ redirect_to root_path, alert: exception.message
170
+ end
171
+
172
+ # Example Traditional Event: analytics_track(user, 'Created Widget', { widget_name: 'foo' })
173
+ # Example Page View: analytics_track(user, 'Page Viewed', { page_name: 'Terms and Conditions', url: '/terms' })
174
+ #
175
+ # NOTE: setup some defaults that we want to track on every event mixpanel_track
176
+ # NOTE: the identify step happens on every page load to keep intercom.io and mixpanel people up to date
177
+ def analytics_track(user, event_name, options = {})
178
+ return if user.tester?
179
+
180
+ sanitized_options = sanitize_hash_javascript(options)
181
+
182
+ segment_attributes = {
183
+ user_id: user.uuid,
184
+ event: event_name,
185
+ properties: {
186
+ browser: "\#{browser.name rescue 'unknown'}",
187
+ browser_id: "\#{browser.id rescue 'unknown'}",
188
+ browser_version: "\#{browser.version rescue 'unknown'}",
189
+ platform: "\#{browser.platform rescue 'unknown'}",
190
+ roles: "\#{user.roles.map(&:to_s).join(',') rescue ''}",
191
+ rails_env: Rails.env.to_s,
192
+ }.merge(sanitized_options),
193
+ }
194
+
195
+ Analytics.track(segment_attributes)
196
+ end
159
197
 
160
198
  protected
161
199
 
162
- # rubocop:disable Metrics/MethodLength
200
+ def devise_or_pages_controller?
201
+ devise_controller? || is_a?(HighVoltage::PagesController)
202
+ end
203
+
204
+ def sanitize_hash_javascript(hash)
205
+ hash.deep_stringify_keys
206
+ .deep_transform_keys { |k| sanitize_javascript(k) }
207
+ .transform_values { |v| sanitize_javascript(v) }
208
+ end
209
+
210
+ def sanitize_javascript(value)
211
+ value.is_a?(String) ? ActionView::Base.new.escape_javascript(value) : value
212
+ end
213
+
163
214
  def configure_permitted_parameters
164
215
  devise_parameter_sanitizer.permit(
165
216
  :sign_up,
@@ -192,33 +243,79 @@ module Suspenders
192
243
  ],
193
244
  )
194
245
  end
195
- # rubocop:enable Metrics/MethodLength
246
+
247
+ def add_layout_name_to_gon
248
+ gon.layout =
249
+ case devise_controller?
250
+ when true
251
+ 'devise'
252
+ else
253
+ 'application'
254
+ end
255
+ end
256
+
257
+ def detect_device_type
258
+ request.variant =
259
+ case request.user_agent
260
+ when /iPad/i
261
+ :tablet
262
+ when /iPhone/i
263
+ :phone
264
+ when /Android/i && /mobile/i
265
+ :phone
266
+ when /Android/i
267
+ :tablet
268
+ when /Windows Phone/i
269
+ :phone
270
+ end
271
+ end
196
272
  RUBY
197
273
  end
198
274
  end
199
275
 
276
+ def add_devise_registrations_controller
277
+ template '../templates/devise_registrations_controller.rb',
278
+ 'app/controllers/devise_customizations/registrations_controller.rb'
279
+ end
280
+
281
+
282
+ def add_analytics_initializer
283
+ template '../templates/analytics_ruby_initializer.rb', 'config/initializers/analytics_ruby.rb'
284
+ template '../templates/analytics_alias.html.erb.erb', 'app/views/users/analytics_alias.html.erb'
285
+ end
286
+
200
287
  def customize_resource_controller_for_devise(adding_first_and_last_name)
201
288
  bundle_command 'exec rails generate controller users'
202
289
  run 'rm spec/controllers/users_controller_spec.rb'
203
290
 
204
- inject_into_class "app/controllers/users_controller.rb", "UsersController" do <<-RUBY.gsub(/^ {6}/, '')
291
+ inject_into_class 'app/controllers/users_controller.rb', 'UsersController' do <<-RUBY.gsub(/^ {6}/, '')
205
292
  # https://github.com/CanCanCommunity/cancancan/wiki/authorizing-controller-actions
206
- load_and_authorize_resource only: [:index, :show]
207
- RUBY
208
- end
293
+ # load_and_authorize_resource only: []
294
+ skip_authorization_check only: [:analytics_alias]
209
295
 
210
- unless adding_first_and_last_name
211
- inject_into_file 'config/routes.rb', after: ' devise_for :users' do <<-RUBY.gsub(/^ {8}/, '')
212
- \n
213
- resources :users
214
- RUBY
296
+ def analytics_alias
297
+ # view file has JS that will identify the anonymous user through segment
298
+ # after registration via "after devise registration path"
215
299
  end
300
+ RUBY
216
301
  end
217
302
  end
218
303
 
219
- def add_views_for_devise_resource(adding_first_and_last_name)
304
+ def add_admin_views_for_devise_resource(adding_first_and_last_name)
220
305
  config = { adding_first_and_last_name: adding_first_and_last_name }
221
- template '../templates/users_index.html.slim.erb', 'app/views/users/index.html.slim', config
306
+ template '../templates/users_index.html.erb', 'app/views/admin/users/index.html.erb', config
307
+
308
+ if @@use_slim
309
+ inside('lib') do # arbitrary, run in context of newly generated app
310
+ run "erb2slim '../app/views/users' '../app/views/users'"
311
+ run "erb2slim -d '../app/views/users'"
312
+
313
+ run "erb2slim '../app/views/admin/users' '../app/views/admin/users'"
314
+ run "erb2slim -d '../app/views/admin/users'"
315
+ end
316
+ end
317
+
318
+ template '../templates/admin_users_controller.rb', 'app/controllers/admin/users_controller.rb'
222
319
  end
223
320
 
224
321
  def authorize_devise_resource_for_index_action
@@ -229,6 +326,25 @@ module Suspenders
229
326
  replace_in_file "spec/abilities/#{resource_name}_spec.rb", "require 'cancan/matchers'", "require_relative '../support/matchers/custom_cancan'"
230
327
  end
231
328
 
329
+ find = <<-RUBY.gsub(/^ {4}/, '')
330
+ it { is_expected.to be_able_to(:manage, user) }
331
+ RUBY
332
+ replace = <<-RUBY.gsub(/^ {4}/, '')
333
+ it { is_expected.to be_able_to(:manage, acting_user) }
334
+ it { is_expected.to_not be_able_to(:manage, user) }
335
+ RUBY
336
+ replace_in_file 'spec/abilities/users_spec.rb', find, replace
337
+
338
+ find = <<-RUBY.gsub(/^ {6}/, '')
339
+ can [:manage], User
340
+ RUBY
341
+ replace = <<-RUBY.gsub(/^ {6}/, '')
342
+ can [:manage], User do |u|
343
+ u == user
344
+ end
345
+ RUBY
346
+ replace_in_file 'app/abilities/users.rb', find, replace
347
+
232
348
  generate 'migration add_roles_mask_to_users roles_mask:integer'
233
349
  template '../templates/custom_cancan_matchers.rb', 'spec/support/matchers/custom_cancan.rb'
234
350
  end
@@ -236,11 +352,42 @@ module Suspenders
236
352
  def add_canard_roles_to_devise_resource
237
353
  inject_into_file 'app/models/user.rb', before: /^end/ do <<-RUBY.gsub(/^ {6}/, '')
238
354
 
355
+ before_create :generate_uuid
356
+
239
357
  # Permissions cascade/inherit through the roles listed below. The order of
240
358
  # this list is important, it should progress from least to most privelage
241
359
  ROLES = [:admin].freeze
242
360
  acts_as_user roles: ROLES
243
361
  roles ROLES
362
+
363
+ validates :email,
364
+ presence: true,
365
+ format: /\\A[-a-z0-9_+\\.]+\\@([-a-z0-9]+\\.)+[a-z0-9]{2,8}\\z/i,
366
+ uniqueness: true
367
+
368
+ # NOTE: these password validations won't run if the user has an invite token
369
+ validates :password,
370
+ presence: true,
371
+ length: { within: 8..72 },
372
+ confirmation: true,
373
+ on: :create
374
+ validates :password_confirmation,
375
+ presence: true,
376
+ on: :create
377
+
378
+ def tester?
379
+ (email =~ /(example.com|headway.io)$/).present?
380
+ end
381
+
382
+ private
383
+
384
+ def generate_uuid
385
+ loop do
386
+ uuid = SecureRandom.uuid
387
+ self.uuid = uuid
388
+ break unless User.exists?(uuid: uuid)
389
+ end
390
+ end
244
391
  RUBY
245
392
  end
246
393
  end
@@ -254,23 +401,53 @@ module Suspenders
254
401
  "config.mailer_sender = 'user@example.com'"
255
402
  end
256
403
 
257
- def add_sign_in_and_sign_out_routes_for_devise
258
- inject_into_file 'config/routes.rb', before: /^end/ do <<-RUBY.gsub(/^ {6}/, '')
404
+ def add_custom_routes_for_devise
405
+ find = <<-RUBY.gsub(/^ {6}/, '')
406
+ devise_for :users
407
+ resources :users
408
+ RUBY
409
+
410
+ replace = <<-RUBY.gsub(/^ {6}/, '')
411
+ devise_for :users, controllers: {
412
+ registrations: 'devise_customizations/registrations',
413
+ }
414
+
415
+ resources :users do
416
+ member do
417
+ get 'analytics_alias'
418
+ end
419
+ end
420
+
421
+ namespace :admin do
422
+ resources :users do
423
+ member do
424
+ get 'impersonate'
425
+ end
426
+
427
+ collection do
428
+ get 'stop_impersonating'
429
+ end
430
+ end
431
+ end
432
+
259
433
  authenticated :user do
260
434
  # root to: 'dashboard#show', as: :authenticated_root
435
+ root to: 'high_voltage/pages#show', id: 'welcome', as: :authenticated_root
261
436
  end
262
437
 
263
438
  devise_scope :user do
264
439
  get 'sign-in', to: 'devise/sessions#new'
265
440
  get 'sign-out', to: 'devise/sessions#destroy'
266
441
  end
267
- RUBY
268
- end
442
+ RUBY
443
+
444
+ replace_in_file 'config/routes.rb', find, replace
269
445
  end
270
446
 
271
447
  def customize_user_factory(adding_first_and_last_name)
272
448
  inject_into_file 'spec/factories/users.rb', before: /^ end/ do <<-'RUBY'.gsub(/^ {4}/, '')
273
- password 'password'
449
+ password 'asdfjkl123'
450
+ password_confirmation 'asdfjkl123'
274
451
  sequence(:email) { |n| "user_#{n}@example.com" }
275
452
 
276
453
  trait :admin do
@@ -311,6 +488,26 @@ module Suspenders
311
488
  end
312
489
  end
313
490
  end
491
+
492
+ describe 'validations' do
493
+ it { is_expected.to validate_presence_of(:email) }
494
+ it { is_expected.to validate_presence_of(:password) }
495
+ it { is_expected.to validate_presence_of(:password_confirmation) }
496
+ end
497
+
498
+ context '#tester?' do
499
+ ['example.com', 'headway.io'].each do |domain|
500
+ it "an email including the \#{domain} domain is a tester" do
501
+ user = build(:user, email: "asdf@\#{domain}")
502
+ expect(user.tester?).to eq(true)
503
+ end
504
+ end
505
+
506
+ it 'an email including the gmail.com domain is NOT a tester' do
507
+ user = build(:user, email: 'asdf@gmail.com')
508
+ expect(user.tester?).to eq(false)
509
+ end
510
+ end
314
511
  RUBY
315
512
 
316
513
  replace_in_file 'spec/models/user_spec.rb', find, replace
@@ -318,11 +515,21 @@ module Suspenders
318
515
 
319
516
  def customize_application_js
320
517
  template '../templates/application.js', 'app/assets/javascripts/application.js', force: true
518
+
519
+ inject_into_file 'app/views/application/_javascript.html.erb', after: '<%= render "analytics" %>' do <<-RUBY.gsub(/^ {8}/, '')
520
+
521
+ <%= render "analytics_identify" %>
522
+ RUBY
523
+ end
321
524
  end
322
525
 
323
526
  def require_files_in_lib
324
- create_file 'config/initializers/require_files_in_lib.rb',
325
- "Dir[File.join(Rails.root, 'lib', '**', '*.rb')].each { |l| require l }\n"
527
+ create_file 'config/initializers/require_files_in_lib.rb' do <<-RUBY.gsub(/^ {8}/, '')
528
+ # rubocop:disable Rails/FilePath
529
+ Dir[File.join(Rails.root, 'lib', '**', '*.rb')].each { |l| require l }
530
+ # rubocop:enable Rails/FilePath
531
+ RUBY
532
+ end
326
533
  end
327
534
 
328
535
  def generate_ruby_version_and_gemset
@@ -352,6 +559,7 @@ module Suspenders
352
559
  end
353
560
  end
354
561
 
562
+
355
563
  # --------
356
564
  # TEMP FIX
357
565
  # https://github.com/thoughtbot/bourbon/issues/993
@@ -372,55 +580,31 @@ module Suspenders
372
580
  # -------------------------
373
581
  def generate_refills
374
582
  if @@accept_defaults || agree?('Would you like to install default Refill components? (Y/n)')
583
+ @@add_refills = true
584
+
375
585
  bundle_command 'exec rails generate refills:import navigation'
376
586
  bundle_command 'exec rails generate refills:import footer'
377
587
 
378
- convert_refill_views if @@use_slim
379
- add_refills_to_layout
588
+ add_admin_links_to_navigation
589
+
380
590
  add_refills_to_stylesheets
591
+ else
592
+ @@add_refills = false
381
593
  end
382
594
  end
383
595
 
384
- def convert_refill_views
385
- inside('lib') do # arbitrary, run in context of newly generated app
386
- run "erb2slim '../app/views/refills' '../app/views/refills'"
387
- run "erb2slim -d '../app/views/refills'"
388
- end
596
+ def add_admin_links_to_navigation
597
+ return unless @@use_devise
598
+ inject_into_file 'app/views/refills/_navigation.html.erb', after: ' <li class="nav-link"><a href="javascript:void(0)">Contact</a></li>' do <<-RUBY
389
599
 
390
- find = <<-RUBY.gsub(/^ {2}/, '')
391
- | <div class="flash-
392
- = key
393
- | ">
394
- = value
395
- RUBY
396
-
397
- replace = <<-RUBY.gsub(/^ {2}/, '')
398
- div class="flash-\#{key}"
399
- = value
400
- RUBY
401
-
402
- replace_in_file 'app/views/application/_flashes.html.slim', find, replace
403
- end
404
-
405
- def add_refills_to_layout
406
- if @@use_slim
407
- inject_into_file 'app/views/layouts/application.html.slim', before: ' = yield' do <<-RUBY.gsub(/^ {8}/, '')
408
- = render 'refills/navigation'
409
- RUBY
410
- end
411
- inject_into_file 'app/views/layouts/application.html.slim', before: ' = render "javascript"' do <<-RUBY.gsub(/^ {8}/, '')
412
- = render 'refills/footer'
413
- RUBY
414
- end
415
- else
416
- inject_into_file 'app/views/layouts/application.html.erb', before: ' <%= yield %>' do <<-RUBY.gsub(/^ {8}/, '')
417
- <%= render 'refills/navigation' %>
418
- RUBY
419
- end
420
- inject_into_file 'app/views/layouts/application.html.erb', before: ' <%= render "javascript" %>' do <<-RUBY.gsub(/^ {8}/, '')
421
- <%= render 'refills/footer' %>
422
- RUBY
423
- end
600
+ <% if current_user && true_user.admin? %>
601
+ <% if current_user != true_user %>
602
+ <li class='nav-link'><%= link_to 'Stop Impersonating', stop_impersonating_admin_users_path %></li>
603
+ <% else %>
604
+ <li class="nav-link"><a href="/admin/users">Admin</a></li>
605
+ <% end %>
606
+ <% end %>
607
+ RUBY
424
608
  end
425
609
  end
426
610
 
@@ -451,6 +635,16 @@ module Suspenders
451
635
  end
452
636
 
453
637
  template "../templates/rails_helper.rb.erb", "spec/rails_helper.rb", force: true
638
+
639
+ %w(test development).each do |environment|
640
+ inject_into_file "config/environments/#{environment}.rb", after: /^end/ do <<-RUBY.gsub(/^ {10}/, '')
641
+
642
+ # NOTE: console can use create(:factory_name), or build(:factory_name) without
643
+ # needing to use FactoryGirl.create(:factory_name).
644
+ include FactoryGirl::Syntax::Methods
645
+ RUBY
646
+ end
647
+ end
454
648
  end
455
649
 
456
650
  def add_rubocop_config
@@ -507,6 +701,7 @@ module Suspenders
507
701
  ###############################
508
702
  def configure_generators
509
703
  config = <<-RUBY.gsub(/^ {4}/, '')
704
+
510
705
  config.generators do |g|
511
706
  g.helper false
512
707
  g.javascript_engine false
@@ -516,8 +711,8 @@ module Suspenders
516
711
  g.test_framework :rspec
517
712
  g.view_specs false
518
713
  g.fixture_replacement :factory_girl, dir: 'spec/factories'
519
- g.template_engine :slim
520
714
  end
715
+
521
716
  RUBY
522
717
 
523
718
  inject_into_class 'config/application.rb', 'Application', config
@@ -527,6 +722,13 @@ module Suspenders
527
722
  create_file '.ruby-version', "#{Voyage::RUBY_VERSION}\n"
528
723
  end
529
724
 
725
+ def overwrite_application_layout
726
+ template '../templates/voyage_layout.html.erb.erb', 'app/views/layouts/application.html.erb', force: true, add_refills: @@add_refills
727
+ update_application_layout_for_slim if @@use_slim
728
+
729
+ template '../templates/analytics_identify.html.erb.erb', 'app/views/application/_analytics_identify.html.erb', force: true
730
+ end
731
+
530
732
  # --------------------------------
531
733
  # setup_test_environment overrides
532
734
  # --------------------------------
@@ -33,6 +33,7 @@ module Suspenders
33
33
  invoke :add_high_voltage_static_pages
34
34
  invoke :downgrade_neat_1_8_so_refills_media_mixin_works # this should be temporary until they get refills re-written to take advantage of Neat 2.0
35
35
  invoke :generate_refills
36
+ invoke :overwrite_application_layout
36
37
  invoke :generate_test_environment
37
38
  invoke :update_test_environment
38
39
  invoke :add_rubocop_config
@@ -90,6 +91,10 @@ module Suspenders
90
91
  build :generate_refills
91
92
  end
92
93
 
94
+ def overwrite_application_layout
95
+ build :overwrite_application_layout
96
+ end
97
+
93
98
  def generate_test_environment
94
99
  build :generate_test_environment
95
100
  end
@@ -13,7 +13,8 @@ gem "normalize-rails", "~> 3.0.0"
13
13
  gem "pg"
14
14
  gem "puma"
15
15
  gem "rack-canonical-host"
16
- gem "rails", "<%= Voyage::RAILS_VERSION %>"
16
+ # gem "rails", "<%= Voyage::RAILS_VERSION %>"
17
+ gem 'rails', git: 'https://github.com/rails/rails.git', branch: '5-0-stable' # OMG Fix deprecation warnings...
17
18
  gem "recipient_interceptor"
18
19
  gem "sass-rails", "~> 5.0"
19
20
  gem "simple_form"
@@ -24,39 +25,47 @@ gem "title"
24
25
  gem "uglifier"
25
26
 
26
27
  # Customizations
27
- gem 'responders' # respond to json/html/js more easily in controllers
28
- gem 'slim-rails' # templating
29
28
  gem 'font-awesome-rails'
30
- gem 'dynamic_form' # for custom messages without the database column
31
- gem 'nested_form_fields' # Dynamically add and remove nested has_many association fields in a Ruby on Rails form
32
- gem 'devise'
29
+ gem 'slim-rails'
33
30
 
34
- gem 'turbolinks'
31
+ # Javascript Tweaks
32
+ gem 'gon' # pass variables betwween rails and javascript. Several examples in the application_controller.rb
35
33
  gem 'jquery-turbolinks'
36
34
  gem 'jquery-ui-rails'
37
35
  gem 'nprogress-rails' # Show request progress when a link is clicked
38
- gem 'gon' # pass variables betwween rails and javascript. Several examples in the application_controller.rb
36
+ gem 'responders' # respond to json/html/js more easily in controllers
37
+ gem 'turbolinks'
39
38
 
40
- gem 'cancancan' # authorization library
39
+ # Authentication / Authorization
41
40
  gem 'canard', git: 'https://github.com/jondkinney/canard.git', branch: 'feature/fixed-generators-and-rails-5' # ties into cancancan, adds roles for the user
41
+ gem 'cancancan' # authorization library
42
+ gem 'devise'
43
+ gem 'pretender' # impersonate users as an admin
42
44
 
45
+ # Database Tweaks
46
+ gem 'dynamic_form' # for custom messages without the database column
43
47
  gem 'friendly_id' # slugs in the url auto-generated
44
- gem 'paper_trail'
48
+ gem 'nested_form_fields' # Dynamically add and remove nested has_many association fields in a Ruby on Rails form
49
+ # gem 'nondestructive_migrations' # data migrations go here, not in regular ActiveRecord migrations
50
+ gem 'nondestructive_migrations', git: 'https://github.com/mfazekas/nondestructive_migrations.git', branch: 'fix-orm-warning'
51
+ gem 'paper_trail' # version active record models and soft-delete by default
45
52
  gem 'settingslogic' # yaml settings (project wide, non-editable), this is implemented with the model Settings.rb
46
53
 
47
- # Want these here for debugging on production
48
- gem 'pry-byebug' # stepwise debugging inside pry
49
- gem 'pry-rails' # better REPL than irb
50
- gem 'pry-awesome_print' # make pry output legible
51
- gem 'pry-remote'
52
-
53
- gem 'nondestructive_migrations'
54
+ # User Uploads
54
55
  gem 'carrierwave'
55
- gem 'mini_magick'
56
56
  gem 'fog'
57
+ gem 'mini_magick'
58
+
59
+ # Cron Jobs
57
60
  gem 'whenever', require: false # provides a clear syntax for writing and deploying cron jobs
58
61
  gem 'whenever-web'
59
62
 
63
+ # Debugging (need at top level if we want pry-remote to work on a deployed prod server)
64
+ gem 'pry-awesome_print' # make pry output legible
65
+ gem 'pry-byebug' # stepwise debugging inside pry
66
+ gem 'pry-rails' # better REPL than irb
67
+ gem 'pry-remote' # Production debugging
68
+
60
69
  group :development do
61
70
  gem "listen"
62
71
  gem "spring"
@@ -65,13 +74,13 @@ group :development do
65
74
 
66
75
  # Customizations
67
76
  gem 'annotate' # annotate models automatically when rake db:migrate is called
68
- gem 'rails-erd' # auto gen ERD Diagram of models in the app on rake db:migrate
69
- gem 'zenflow', git: 'https://github.com/zencoder/zenflow.git'
70
77
  gem 'better_errors' # A better error page for rails when a local 500 (or similar) is thrown
71
78
  gem 'binding_of_caller' # REPL in better_errors to debug in the browser at the point at which it failed
72
- gem 'meta_request' # for chrome rails console plugin found here: https://chrome.google.com/webstore/detail/railspanel/gjpfobpafnhjhbajcjgccbbdofdckggg?hl=en-US
73
79
  gem 'bitters', '~> 1.3'
80
+ gem 'meta_request' # for chrome rails console plugin found here: https://chrome.google.com/webstore/detail/railspanel/gjpfobpafnhjhbajcjgccbbdofdckggg?hl=en-US
81
+ gem 'rails-erd' # auto gen ERD Diagram of models in the app on rake db:migrate
74
82
  gem 'redcarpet' # used to render the readme inside a static welcome page from the high_voltage gem
83
+ gem 'zenflow', git: 'https://github.com/zencoder/zenflow.git'
75
84
  end
76
85
 
77
86
  group :development, :test do
@@ -86,11 +95,9 @@ group :development, :test do
86
95
 
87
96
  # Customizations
88
97
  gem 'faker' # provides auto generated names for factories, can be customized
89
-
98
+ gem 'letter_opener' # auto-open emails when they're sent
90
99
  gem 'rubocop'
91
100
  gem 'rubocop-rspec', require: false
92
-
93
- gem 'letter_opener' # auto-open emails when they're sent
94
101
  end
95
102
 
96
103
  group :development, :staging do
@@ -109,12 +116,15 @@ group :test do
109
116
 
110
117
  # Customizations
111
118
  gem 'cadre' # highlights code coverage in vim
112
-
113
- gem 'capybara'
114
- gem 'poltergeist'
115
- gem 'selenium-webdriver' # brew install chromedriver
119
+ gem 'capybara' # DSL for finding elements on a page during integration testing
120
+ gem 'poltergeist' # Headless browser, used in integration tests
121
+ gem 'selenium-webdriver' # `brew install chromedriver`, used for acceptance tests in an actual browser.
116
122
  end
117
123
 
118
124
  group :staging, :production do
119
125
  gem "rack-timeout"
120
126
  end
127
+
128
+ group :production do
129
+ gem 'analytics-ruby', '~> 2.2.2', require: 'segment/analytics'
130
+ end
@@ -7,22 +7,27 @@ with the necessary dependencies to run and test this app:
7
7
 
8
8
  % ./bin/setup
9
9
 
10
- It assumes you have a machine equipped with Ruby, Postgres, etc. If not, set up
11
- your machine with [this script].
10
+ It assumes you have a machine equipped with Ruby, Postgres, etc.
12
11
 
13
- [this script]: https://github.com/thoughtbot/laptop
12
+ ## Seeded Data
14
13
 
15
- After setting up, you can run the application using [Heroku Local]:
14
+ Assuming that devise was opted in to, a user and admin have been seeded for you:
16
15
 
17
- % heroku local
16
+ admin@example.com -> asdfjkl123
17
+ user_1@example.com -> asdfjkl123
18
18
 
19
- [Heroku Local]: https://devcenter.heroku.com/articles/heroku-local
19
+ ## Static Pages
20
20
 
21
- ## Guidelines
21
+ The HighVoltage gem is used.
22
22
 
23
- Use the following guides for getting things done, programming well, and
24
- programming in style.
23
+ ## Authorization
25
24
 
26
- * [Protocol](http://github.com/thoughtbot/guides/blob/master/protocol)
27
- * [Best Practices](http://github.com/thoughtbot/guides/blob/master/best-practices)
28
- * [Style](http://github.com/thoughtbot/guides/blob/master/style)
25
+ This repo uses CanCanCan and Canard to authorize user action. `ApplicationController` defines the `check_authorization` method for non-devise non-high voltage controllers. If you want to skip authorization for a specific method in a controller, the following method can be used: `skip_authorization_check`
26
+
27
+ ## Rubocop
28
+
29
+ Take a look at the `.rubocop.yml` file to see what styles are being enforced.
30
+
31
+ ## Annotations
32
+
33
+ Model & spec files are automatically annotated each time `rake db:migrate` is run.
@@ -0,0 +1,40 @@
1
+ module Admin
2
+ class UsersController < ApplicationController
3
+ before_action :require_admin!, except: [:stop_impersonating]
4
+ skip_authorization_check
5
+
6
+ def index
7
+ @users = User.all
8
+ end
9
+
10
+ def impersonate
11
+ user = User.find(params[:id])
12
+ track_impersonation(user, 'Start')
13
+ impersonate_user(user)
14
+ redirect_to root_path
15
+ end
16
+
17
+ def stop_impersonating
18
+ track_impersonation(current_user, 'Stop')
19
+ stop_impersonating_user
20
+ redirect_to admin_users_path
21
+ end
22
+
23
+ private
24
+
25
+ def require_admin!
26
+ txt = 'You must be an admin to perform that action'
27
+ redirect_to root_path, notice: txt unless current_user.admin?
28
+ end
29
+
30
+ def track_impersonation(user, status)
31
+ analytics_track(
32
+ true_user,
33
+ "Impersonation #{status}",
34
+ impersonated_user_id: user.id,
35
+ impersonated_user_email: user.email,
36
+ impersonated_by_email: true_user.email,
37
+ )
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,18 @@
1
+ <%% goto = Rails.application.routes.url_helpers.authenticated_root_path %>
2
+
3
+ <%% content_for :javascript do %>
4
+ <script type='text/javascript'>
5
+ $(document).ready(function() {
6
+ console.log('Aliased: 1st');
7
+
8
+ if (typeof analytics != 'undefined') {
9
+ analytics.alias('<%%= current_user.uuid %>');
10
+ }
11
+
12
+ console.log("Redirect to '<%%= goto %>' after alias & identify");
13
+
14
+ window.location.replace('<%%= goto %>');
15
+ });
16
+ </script>
17
+ <%% end %>
18
+
@@ -0,0 +1,42 @@
1
+ <%% if current_user %>
2
+ <%% address = current_user.try(:address) %>
3
+ <%% address_attrs = {} %>
4
+ <%% address_attrs.merge!(
5
+ city: (address.city rescue 'unknown'),
6
+ country: 'United States',
7
+ postal_code: (address.zip rescue 'unknown'),
8
+ state: (address.state.name rescue 'unknown'),
9
+ street: ("#{address.line1} #{address.line2}" rescue 'unknown'),
10
+ ) if address.present? %>
11
+
12
+ <script type='text/javascript'>
13
+ $(document).ready(function() {
14
+ console.log('Identified: 2nd');
15
+
16
+ var analytics_attrs = {
17
+ email: "<%%= current_user.email %>",
18
+ first_name: "<%%= escape_javascript(current_user.first_name) %>",
19
+ last_name: "<%%= escape_javascript(current_user.last_name) %>",
20
+ address: "<%%= raw controller.send(:sanitize_hash_javascript, address_attrs).to_json %>",
21
+ created_at: "<%%= current_user.created_at.iso8601 %>",
22
+ roles: "<%%= current_user.roles.map(&:to_s).join(',') %>",
23
+ rails_env: "<%%= Rails.env.to_s %>",
24
+ }
25
+
26
+ console.log(analytics_attrs);
27
+
28
+ if (typeof analytics != 'undefined') {
29
+ analytics.identify(
30
+ "<%%= current_user.uuid %>",
31
+ analytics_attrs, {
32
+ integrations: {
33
+ Intercom : {
34
+ user_hash: "<%%= OpenSSL::HMAC.hexdigest('sha256', ENV['INTERCOM_SECURE_MODE_SECRET_KEY'].to_s, current_user.uuid.to_s) %>",
35
+ }
36
+ }
37
+ }
38
+ );
39
+ }
40
+ });
41
+ </script>
42
+ <%% end %>
@@ -0,0 +1,15 @@
1
+ if Rails.env.production?
2
+ require 'segment/analytics'
3
+
4
+ Analytics = Segment::Analytics.new(
5
+ write_key: ENV['SEGMENT_ANALYTICS_RUBY_KEY'],
6
+ on_error: proc { |_status, msg| Rails.logger.info msg },
7
+ )
8
+ else
9
+ Analytics = Struct.new('Analytics') do
10
+ def self.track(_segment_attributes)
11
+ end
12
+ def self.identify(_segment_attributes)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,17 @@
1
+ module DeviseCustomizations
2
+ class RegistrationsController < Devise::RegistrationsController
3
+ def create
4
+ super
5
+ end
6
+
7
+ protected
8
+
9
+ def after_sign_up_path_for(resource)
10
+ if user_signed_in?
11
+ analytics_alias_user_path(resource)
12
+ else
13
+ new_user_session_path(resource)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -27,10 +27,19 @@ AllCops:
27
27
  - '.+'
28
28
  Exclude:
29
29
  - 'db/**/*'
30
- - 'config/**/*'
30
+ - 'config/*'
31
+ - 'config/environments/*'
32
+ - 'config/locales/*'
33
+ - 'config/initializers/devise.rb'
34
+ - 'config/initializers/simple_form.rb'
35
+ - 'config/initializers/new_framework_defaults.rb'
36
+ - 'config/initializers/assets.rb'
37
+ - 'config/initializers/backtrace_silencers.rb'
38
+ - 'config/initializers/rack_mini_profiler.rb'
39
+ - 'config/initializers/wrap_parameters.rb'
31
40
  - 'bin/**/*'
32
41
  Include:
33
- - '**/*.pryrc'
42
+ - 'Gemfile'
34
43
  - '.simplecov'
35
44
  - 'config/initializers/*'
36
45
 
@@ -18,8 +18,10 @@ module Seeder
18
18
  # Remove all users
19
19
  User.destroy_all
20
20
 
21
+ create(:user)
22
+
21
23
  # list user traits from factory_girl here
22
- %w(admin another_role).each do |name|
24
+ %w(admin).each do |name|
23
25
  create(:user, name.to_sym)
24
26
  end
25
27
  end
@@ -14,7 +14,7 @@ puts
14
14
 
15
15
  dev_tables_to_seed =
16
16
  %w(
17
- <%= config[:using_devise] ? 'admin_user' : 'factory_name' %>
17
+ <%= config[:using_devise] ? 'all_users' : 'factory_name' %>
18
18
  )
19
19
 
20
20
  production_tables_to_seed =
@@ -0,0 +1,28 @@
1
+ <h1>Listing Users</h1>
2
+
3
+ <table>
4
+ <thead>
5
+ <tr><% if config[:adding_first_and_last_name] %>
6
+ <th>First Name</th>
7
+ <th>Last Name</th><% end %>
8
+ <th>Email</th>
9
+ <th></th>
10
+ <th></th>
11
+ <th></th>
12
+ </tr>
13
+ </thead>
14
+ <tbody>
15
+ <%% @users.each do |user| %>
16
+ <tr><% if config[:adding_first_and_last_name] %>
17
+ <td><%%= user.first_name %></td>
18
+ <td><%%= user.last_name %></td><% end %>
19
+ <td><%%= link_to user.email, impersonate_admin_user_path(user) %></td>
20
+ <td><%%= link_to 'Show', user %></td>
21
+ <td><%%= link_to 'Edit', edit_user_path(user) %></td>
22
+ <td><%%= link_to 'Destroy', user, data: { confirm: 'Are you sure?' }, method: :delete %></td>
23
+ </tr>
24
+ <%% end %>
25
+ </tbody>
26
+ </table>
27
+
28
+ <br />
@@ -0,0 +1,43 @@
1
+ <!DOCTYPE html>
2
+ <html lang="<%= I18n.locale %>">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="ROBOTS" content="NOODP" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
7
+ <meta name="apple-mobile-web-app-capable" content="yes" />
8
+ <meta name="mobile-web-app-capable" content="yes" />
9
+ <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
10
+ <%%#
11
+ Configure default and controller-, and view-specific titles in
12
+ config/locales/en.yml. For more see:
13
+ https://github.com/calebthompson/title#usage
14
+ %>
15
+ <title><%%= title %></title>
16
+ <%%= stylesheet_link_tag :application, media: "all" %>
17
+ <link rel='apple-touch-icon' href='/assets/apple-touch-icon.png' />
18
+ <%%= csrf_meta_tags %>
19
+ <%%= favicon_link_tag 'favicon.ico' %>
20
+ <%%# Add gon here for gon use inside the compiled js before the page is fully loaded %>
21
+ <%%= Gon::Base.render_data %>
22
+ </head>
23
+ <body class="<%%= devise_controller? ? 'devise' : 'application' %> <%%= body_class %>">
24
+ <%%# Add gon here for turbolinks benefit %>
25
+ <%%= Gon::Base.render_data %>
26
+ <%%= render 'flashes' -%>
27
+ <%% if devise_controller? %>
28
+ <% if config[:add_refills] %><%%= render 'refills/navigation' %><% end %>
29
+ <%%= yield %>
30
+ <%% else %>
31
+ <div class='grid-offset'>
32
+ <div class='work-area'>
33
+ <% if config[:add_refills] %><%%= render 'refills/navigation' %><% end %>
34
+ <%%= yield %>
35
+ </div>
36
+ </div>
37
+ <%% end %>
38
+
39
+ <% if config[:add_refills] %><%%= render 'refills/footer' %><% end %>
40
+ <%%= render 'javascript' %>
41
+ <%%= render 'css_overrides' %>
42
+ </body>
43
+ </html>
@@ -5,5 +5,5 @@ module Voyage
5
5
  .read("#{File.dirname(__FILE__)}/../../.ruby-version")
6
6
  .strip
7
7
  .freeze
8
- VERSION = '1.44.0.6'.freeze
8
+ VERSION = '1.44.0.7'.freeze
9
9
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: voyage
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.44.0.6
4
+ version: 1.44.0.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - thoughtbot, headway
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-02-12 00:00:00.000000000 Z
11
+ date: 2017-02-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bitters
@@ -87,6 +87,7 @@ files:
87
87
  - README.md
88
88
  - RELEASING.md
89
89
  - Rakefile
90
+ - TODO.md
90
91
  - USAGE
91
92
  - bin/rake
92
93
  - bin/rspec
@@ -108,17 +109,23 @@ files:
108
109
  - lib/voyage/templates/Gemfile.erb
109
110
  - lib/voyage/templates/README.md.erb
110
111
  - lib/voyage/templates/about.html.erb
112
+ - lib/voyage/templates/admin_users_controller.rb
113
+ - lib/voyage/templates/analytics_alias.html.erb.erb
114
+ - lib/voyage/templates/analytics_identify.html.erb.erb
115
+ - lib/voyage/templates/analytics_ruby_initializer.rb
111
116
  - lib/voyage/templates/application.js
112
117
  - lib/voyage/templates/auto_annotate_models.rake
113
118
  - lib/voyage/templates/config_locales_en.yml.erb
114
119
  - lib/voyage/templates/controller_helpers.rb
115
120
  - lib/voyage/templates/custom_cancan_matchers.rb
121
+ - lib/voyage/templates/devise_registrations_controller.rb
116
122
  - lib/voyage/templates/rails_helper.rb.erb
117
123
  - lib/voyage/templates/rubocop.yml
118
124
  - lib/voyage/templates/seeder.rb.erb
119
125
  - lib/voyage/templates/seeds.rb.erb
120
126
  - lib/voyage/templates/simplecov.rb
121
- - lib/voyage/templates/users_index.html.slim.erb
127
+ - lib/voyage/templates/users_index.html.erb
128
+ - lib/voyage/templates/voyage_layout.html.erb.erb
122
129
  - lib/voyage/templates/welcome.html.erb
123
130
  - lib/voyage/version.rb
124
131
  - spec/adapters/heroku_spec.rb
@@ -1,25 +0,0 @@
1
- h1 Listing Users
2
-
3
- table
4
- thead
5
- tr<% if config[:adding_first_and_last_name] %>
6
- th First Name
7
- th Last Name<% end %>
8
- th Email
9
- th
10
- th
11
- th
12
-
13
- tbody
14
- - @users.each do |user|
15
- tr<% if config[:adding_first_and_last_name] %>
16
- td= user.first_name
17
- td= user.last_name<% end %>
18
- td= user.email
19
- td= link_to 'Show', user
20
- td= link_to 'Edit', edit_user_path(user)
21
- td= link_to 'Destroy', user, data: { confirm: 'Are you sure?' }, method: :delete
22
-
23
- br
24
-
25
- = link_to 'New User', signup_path