voyage 1.44.0.6 → 1.44.0.7

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