katapult 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/CHANGELOG.md +34 -12
  4. data/README.md +154 -114
  5. data/Rakefile +3 -3
  6. data/bin/katapult +34 -8
  7. data/features/application_model.feature +8 -44
  8. data/features/authenticate.feature +21 -4
  9. data/features/basics.feature +16 -89
  10. data/features/binary.feature +37 -4
  11. data/features/model.feature +40 -24
  12. data/features/navigation.feature +2 -0
  13. data/features/step_definitions/katapult_steps.rb +48 -16
  14. data/features/step_definitions/test_steps.rb +2 -0
  15. data/features/templates.feature +74 -0
  16. data/lib/generators/katapult/app_model/templates/lib/katapult/application_model.rb +2 -2
  17. data/lib/generators/katapult/basics/basics_generator.rb +12 -1
  18. data/lib/generators/katapult/basics/templates/Capfile +5 -0
  19. data/lib/generators/katapult/basics/templates/Gemfile +3 -1
  20. data/lib/generators/katapult/basics/templates/Gemfile.lock +376 -0
  21. data/lib/generators/katapult/basics/templates/app/models/application_record.rb +28 -0
  22. data/lib/generators/katapult/basics/templates/app/webpack/assets/javascripts/bootstrap.js +2 -2
  23. data/lib/generators/katapult/basics/templates/config/deploy.rb +2 -3
  24. data/lib/generators/katapult/basics/templates/config/deploy/production.rb +2 -2
  25. data/lib/generators/katapult/basics/templates/config/deploy/staging.rb +1 -1
  26. data/lib/generators/katapult/basics/templates/lib/capistrano/tasks/deploy.rake +1 -4
  27. data/lib/generators/katapult/clearance/clearance_generator.rb +13 -4
  28. data/lib/generators/katapult/clearance/templates/features/authentication.feature +3 -3
  29. data/lib/generators/katapult/model/model_generator.rb +14 -4
  30. data/lib/generators/katapult/templates/templates_generator.rb +35 -0
  31. data/lib/generators/katapult/transform/transform_generator.rb +3 -3
  32. data/lib/generators/katapult/views/views_generator.rb +1 -1
  33. data/lib/generators/katapult/web_ui/web_ui_generator.rb +1 -1
  34. data/lib/katapult/application_model.rb +7 -7
  35. data/lib/katapult/elements/attribute.rb +16 -0
  36. data/lib/katapult/elements/authentication.rb +9 -6
  37. data/lib/katapult/elements/model.rb +8 -4
  38. data/lib/katapult/elements/navigation.rb +2 -2
  39. data/lib/katapult/elements/web_ui.rb +2 -2
  40. data/lib/katapult/generator.rb +12 -2
  41. data/lib/katapult/support/generator_goodies.rb +14 -0
  42. data/lib/katapult/version.rb +1 -1
  43. data/script/update +5 -1
  44. metadata +8 -8
  45. data/features/configuration.feature +0 -24
  46. data/lib/generators/katapult/basics/templates/lib/capistrano/tasks/passenger.rake +0 -8
  47. data/lib/generators/katapult/basics/templates/lib/ext/active_record/find_by_anything.rb +0 -20
  48. data/lib/generators/katapult/basics/templates/lib/ext/active_record/these.rb +0 -7
@@ -0,0 +1,28 @@
1
+ class ApplicationRecord < ActiveRecord::Base
2
+ self.abstract_class = true
3
+
4
+ class << self
5
+ def these(arg)
6
+ where(id: arg.collect_ids)
7
+ end
8
+
9
+ def find_by_anything(identifier)
10
+ matchable_columns = columns.reject { |column| [:binary, :boolean].include?(column.type) }
11
+ query_clauses = matchable_columns.collect do |column|
12
+ qualified_column_name = "#{table_name}.#{column.name}"
13
+ is_mysql = connection.class.name =~ /mysql/i
14
+ target_type = is_mysql ? 'CHAR' : 'TEXT' # CHAR is only 1 character in PostgreSQL
15
+ column_as_string = "CAST(#{qualified_column_name} AS #{target_type})"
16
+ "#{column_as_string} = ?"
17
+ end
18
+ bindings = [identifier] * query_clauses.size
19
+ where([query_clauses.join(' OR '), *bindings]).first
20
+ end
21
+
22
+ def find_by_anything!(identifier)
23
+ find_by_anything(identifier) or raise ActiveRecord::RecordNotFound,
24
+ "No column equals #{identifier.inspect}"
25
+ end
26
+ end
27
+
28
+ end
@@ -1,8 +1,8 @@
1
- // import 'bootstrap-sass/assets/javascripts/bootstrap/transition'
1
+ import 'bootstrap-sass/assets/javascripts/bootstrap/transition'
2
2
  // import 'bootstrap-sass/assets/javascripts/bootstrap/alert'
3
3
  // import 'bootstrap-sass/assets/javascripts/bootstrap/button'
4
4
  // import 'bootstrap-sass/assets/javascripts/bootstrap/carousel'
5
- // import 'bootstrap-sass/assets/javascripts/bootstrap/collapse'
5
+ import 'bootstrap-sass/assets/javascripts/bootstrap/collapse'
6
6
  import 'bootstrap-sass/assets/javascripts/bootstrap/dropdown'
7
7
  // import 'bootstrap-sass/assets/javascripts/bootstrap/modal'
8
8
  // import 'bootstrap-sass/assets/javascripts/bootstrap/tab'
@@ -17,7 +17,7 @@ set :linked_files, %w(config/database.yml config/secrets.yml)
17
17
 
18
18
  # Default value for linked_dirs is []
19
19
  # set :linked_dirs, fetch(:linked_dirs, []).push('log', 'tmp/pids', 'tmp/cache', 'tmp/sockets', 'vendor/bundle', 'public/system')
20
- set :linked_dirs, %w(log public/system tmp/pids)
20
+ set :linked_dirs, %w(log public/system tmp/pids node_modules public/packs)
21
21
 
22
22
  # Default value for default_env is {}
23
23
  # set :default_env, { path: "/opt/ruby/bin:$PATH" }
@@ -30,7 +30,6 @@ set :ssh_options, {
30
30
  set :repo_url, 'git@code.makandra.de:makandra/<%= app_name %>.git'
31
31
 
32
32
  # set :whenever_roles, :cron
33
- # set :whenever_environment, defer { stage }
34
- # set :whenever_command, 'bundle exec whenever'
33
+ # set :whenever_identifier, ->{ "#{fetch(:application)}_#{fetch(:stage)}" }
35
34
 
36
35
  set :maintenance_template_path, 'public/maintenance.html.erb'
@@ -4,5 +4,5 @@ set :deploy_to, '/var/www/<%= app_name %>'
4
4
  set :rails_env, 'production'
5
5
  set :branch, 'production'
6
6
 
7
- # server 'one.example.com', user: 'deploy-user', roles: %w(app web cron db)
8
- # server 'two.example.com', user: 'deploy-user', roles: %w(app web)
7
+ # server 'one.example.com', user: 'deploy-user', roles: %w[app web cron db]
8
+ # server 'two.example.com', user: 'deploy-user', roles: %w[app web]
@@ -4,4 +4,4 @@ set :deploy_to, '/var/www/<%= app_name %>-staging'
4
4
  set :rails_env, 'staging'
5
5
  set :branch, ENV['DEPLOY_BRANCH'] || 'master'
6
6
 
7
- # server 'example.com', user: 'deploy-user', roles: %w(app web cron db)
7
+ # server 'example.com', user: 'deploy-user', roles: %w[app web cron db]
@@ -1,8 +1,4 @@
1
1
  namespace :deploy do
2
- desc 'Restart application'
3
- task :restart do
4
- invoke 'passenger:restart'
5
- end
6
2
 
7
3
  desc 'Show deployed revision'
8
4
  task :revision do
@@ -12,4 +8,5 @@ namespace :deploy do
12
8
  end
13
9
  end
14
10
  end
11
+
15
12
  end
@@ -124,16 +124,25 @@ resources :users do
124
124
  def add_user_factory
125
125
  factories_file = 'spec/factories/factories.rb'
126
126
 
127
- # Remove empty factory, if it exists
128
- gsub_file factories_file, " factory :user\n\n", ''
127
+ # Remove empty factory
128
+ gsub_file factories_file, "\n factory :user\n", ''
129
129
 
130
- inject_into_file factories_file, <<-'CONTENT', before: /end\n\z/
130
+ # In a second `transform` run, this might already be present
131
+ unless file_contains? factories_file, 'factory :user'
132
+ inject_into_file factories_file, <<-'CONTENT', before: /end\n\z/
131
133
  factory :user do
132
134
  sequence(:email) { |i| "user-#{ i }@example.com" }
133
135
  password 'password'
134
136
  end
135
137
 
136
- CONTENT
138
+ CONTENT
139
+ end
140
+ end
141
+
142
+ private
143
+
144
+ def user
145
+ @element.user
137
146
  end
138
147
 
139
148
  end
@@ -47,11 +47,11 @@ Feature: Everything about user authentication
47
47
 
48
48
 
49
49
  Scenario: Reset password as a signed-in user
50
- Given there is a user with the email "henry@example.com"
50
+ Given there is a user with the email "henry@example.com" and the <%= user.label_attr.name(:variable) %> "<%= user.label_attr.test_value %>"
51
51
  And I sign in as the user above
52
52
 
53
53
  When I go to the homepage
54
- And I follow "henry@example.com" within the navbar
54
+ And I follow "<%= user.label_attr.test_value %>" within the navbar
55
55
  Then I should be on the form for the user above
56
56
 
57
57
  When I fill in "Password" with "new-password"
@@ -59,7 +59,7 @@ Feature: Everything about user authentication
59
59
  Then I should be on the page for the user above
60
60
 
61
61
  When I follow "Sign out"
62
- And I fill in "Email" with "henry@example.com"
62
+ And I fill in "Email" with "<%= (user.label_attr.type == :email) ? user.label_attr.test_value : "henry@example.com" %>"
63
63
  And I fill in "Password" with "new-password"
64
64
  And I press "Sign in"
65
65
  Then I should be on the homepage
@@ -31,14 +31,24 @@ module Katapult
31
31
  end
32
32
 
33
33
  def write_factory
34
- insert_into_file 'spec/factories/factories.rb', <<-FACTORY, before: /end\n\z/
35
- factory #{ model.name(:symbol) }
34
+ factory = " factory #{ model.name(:symbol) }"
35
+ factories_file = 'spec/factories/factories.rb'
36
+ # Can happen in multiple transformation runs with authentication
37
+ return if file_contains?(factories_file, factory)
36
38
 
37
- FACTORY
39
+ factory_attrs = model.required_attrs.map do |a|
40
+ " #{ a.name(:human) } #{ a.test_value.inspect }"
41
+ end
42
+
43
+ if factory_attrs.any?
44
+ factory << " do\n#{ factory_attrs.join "\n" }\n end"
45
+ end
46
+
47
+ insert_into_file factories_file, factory + "\n\n", before: /end\n\z/
38
48
  end
39
49
 
40
50
  def generate_unit_tests
41
- Generators::ModelSpecsGenerator.new(model).invoke_all
51
+ Generators::ModelSpecsGenerator.new(model, options).invoke_all
42
52
  end
43
53
 
44
54
  no_commands do
@@ -0,0 +1,35 @@
1
+ module Katapult
2
+ class TemplatesGenerator < Rails::Generators::Base
3
+
4
+ desc 'Copy Katapult templates to the target application'
5
+ source_root File.expand_path('..', __dir__) # lib/generators/katapult
6
+
7
+ def copy_view_templates
8
+ copy_generator_templates 'views', %w[
9
+ _form.html.haml
10
+ edit.html.haml
11
+ index.html.haml
12
+ new.html.haml
13
+ show.html.haml
14
+ ]
15
+ end
16
+
17
+ def copy_controller_template
18
+ copy_generator_templates 'web_ui', 'controller.rb'
19
+ end
20
+
21
+ private
22
+
23
+ # file_list should contain paths relative the the respective generator
24
+ # template root
25
+ def copy_generator_templates(generator_name, file_list)
26
+ Array(file_list).each do |filename|
27
+ source = File.join generator_name, 'templates', filename
28
+ destination = File.join 'lib/templates/katapult', generator_name, filename
29
+
30
+ copy_file source, destination
31
+ end
32
+ end
33
+
34
+ end
35
+ end
@@ -20,7 +20,7 @@ module Katapult
20
20
  @app_model = Katapult::ApplicationModel.parse(application_model, path)
21
21
 
22
22
  say_status :render, "into #{app_name}"
23
- @app_model.render
23
+ @app_model.render options.slice(:force)
24
24
  end
25
25
 
26
26
  def write_root_route
@@ -33,9 +33,9 @@ module Katapult
33
33
  def remigrate_all_databases
34
34
  return if ENV['SKIP_MIGRATIONS'] # Used to speed up tests
35
35
 
36
- run 'rake db:drop db:create db:migrate'
36
+ rake 'db:drop db:create db:migrate'
37
37
  # See comment to Katapult::BasicsGenerator#create_databases
38
- run 'unset RAILS_ENV; rake parallel:drop parallel:create parallel:prepare'
38
+ run 'unset RAILS_ENV; bundle exec rake parallel:drop parallel:create parallel:prepare'
39
39
  end
40
40
 
41
41
  end
@@ -50,7 +50,7 @@ module Katapult
50
50
 
51
51
  def generate_integration_tests
52
52
  if web_ui.model.present?
53
- Generators::CucumberFeaturesGenerator.new(web_ui.model).invoke_all
53
+ Generators::CucumberFeaturesGenerator.new(web_ui.model, options).invoke_all
54
54
  end
55
55
  end
56
56
 
@@ -36,7 +36,7 @@ MESSAGE
36
36
  end
37
37
 
38
38
  def generate_views
39
- Generators::ViewsGenerator.new(web_ui).invoke_all
39
+ Generators::ViewsGenerator.new(web_ui, options).invoke_all
40
40
  end
41
41
 
42
42
  no_tasks do
@@ -11,7 +11,7 @@ module Katapult
11
11
 
12
12
  NotFound = Class.new(StandardError)
13
13
 
14
- attr_reader :models, :web_uis, :navigation, :authentication, :associations
14
+ attr_reader :models, :web_uis, :nav, :authentication, :associations
15
15
 
16
16
  def self.parse(application_model_string, path_to_model = '')
17
17
  new.tap do |model|
@@ -38,7 +38,7 @@ module Katapult
38
38
 
39
39
  # DSL
40
40
  def navigation(name = :main)
41
- @navigation = Navigation.new(name, application_model: self)
41
+ @nav = Navigation.new(name, application_model: self)
42
42
  end
43
43
 
44
44
  # DSL
@@ -78,13 +78,13 @@ module Katapult
78
78
  associations.select { |a| a.belongs_to == model_name }.map(&:model)
79
79
  end
80
80
 
81
- def render
81
+ def render(options = {})
82
82
  prepare_render
83
83
 
84
- models.each &:render
85
- web_uis.each &:render
86
- navigation.render if navigation
87
- authentication.render if authentication
84
+ models.each { |m| m.render(options) }
85
+ web_uis.each { |w| w.render(options) }
86
+ nav.render(options) if nav
87
+ authentication.render(options) if authentication
88
88
  end
89
89
 
90
90
  private
@@ -32,6 +32,22 @@ module Katapult
32
32
  !default.nil? and not [flag?, assignable_values].any?
33
33
  end
34
34
 
35
+ def renderable?
36
+ %i[plain_json json password].exclude? type
37
+ end
38
+
39
+ def editable?
40
+ %i[plain_json json].exclude? type
41
+ end
42
+
43
+ def required?
44
+ if assignable_values.present?
45
+ default.blank? && allow_blank.blank?
46
+ else
47
+ false
48
+ end
49
+ end
50
+
35
51
  def for_migration
36
52
  db_type = case type
37
53
  when :email, :url, :password then 'string'
@@ -7,15 +7,18 @@ module Katapult
7
7
  attr_accessor :system_email
8
8
 
9
9
  def ensure_user_model_attributes_present
10
- user_model = application_model.get_model!(name)
11
- user_attrs = user_model.attrs.map(&:name)
10
+ user_attrs = user.attrs.map(&:name)
12
11
 
13
- user_model.attr(:email) unless user_attrs.include?('email')
14
- user_model.attr(:password, type: :password, skip_db: true) unless user_attrs.include?('password')
12
+ user.attr(:email) unless user_attrs.include?('email')
13
+ user.attr(:password, type: :password, skip_db: true) unless user_attrs.include?('password')
15
14
  end
16
15
 
17
- def render
18
- Generators::ClearanceGenerator.new(self).invoke_all
16
+ def render(options = {})
17
+ Generators::ClearanceGenerator.new(self, options).invoke_all
18
+ end
19
+
20
+ def user
21
+ @user ||= application_model.get_model!(name)
19
22
  end
20
23
 
21
24
  end
@@ -48,11 +48,15 @@ module Katapult
48
48
  end
49
49
 
50
50
  def renderable_attrs
51
- attrs.reject { |a| %w[plain_json json password].include? a.type.to_s }
51
+ attrs.select &:renderable?
52
52
  end
53
53
 
54
54
  def editable_attrs
55
- attrs.reject { |a| %w[plain_json json].include? a.type.to_s }
55
+ attrs.select &:editable?
56
+ end
57
+
58
+ def required_attrs
59
+ attrs.select &:required?
56
60
  end
57
61
 
58
62
  def add_foreign_key_attrs(belongs_tos)
@@ -64,8 +68,8 @@ module Katapult
64
68
  end
65
69
  end
66
70
 
67
- def render
68
- Generators::ModelGenerator.new(self).invoke_all
71
+ def render(options = {})
72
+ Generators::ModelGenerator.new(self, options).invoke_all
69
73
  end
70
74
 
71
75
  private
@@ -19,8 +19,8 @@ module Katapult
19
19
  end
20
20
  end
21
21
 
22
- def render
23
- Generators::NavigationGenerator.new(self).invoke_all
22
+ def render(options = {})
23
+ Generators::NavigationGenerator.new(self, options).invoke_all
24
24
  end
25
25
 
26
26
  end
@@ -78,8 +78,8 @@ module Katapult
78
78
  model.name(kind)
79
79
  end
80
80
 
81
- def render
82
- Generators::WebUIGenerator.new(self).invoke_all
81
+ def render(options = {})
82
+ Generators::WebUIGenerator.new(self, options).invoke_all
83
83
  end
84
84
 
85
85
  end
@@ -9,10 +9,13 @@ module Katapult
9
9
 
10
10
  attr_accessor :element
11
11
 
12
- def initialize(element)
12
+ # @option :force (from Thor): Overwrite on conflict
13
+ def initialize(element, options = {})
13
14
  self.element = element
15
+ args = [element.name]
16
+ config = {}
14
17
 
15
- super([element.name], {}, {}) # args, opts, config
18
+ super args, options, config
16
19
  end
17
20
 
18
21
  private
@@ -22,5 +25,12 @@ module Katapult
22
25
  ERB.new(::File.binread(path), nil, '%').result(given_binding || binding)
23
26
  end
24
27
 
28
+ def generate(generator_name)
29
+ args = []
30
+ args << '--force' if options[:force]
31
+
32
+ super generator_name, *args
33
+ end
34
+
25
35
  end
26
36
  end
@@ -1,3 +1,6 @@
1
+ # This module holds methods that are shared between Katapult's element generators
2
+ # and the Rails generators it uses (e.g. BasicsGenerator)
3
+ #
1
4
  module Katapult::GeneratorGoodies
2
5
 
3
6
  def yarn(*args)
@@ -5,6 +8,11 @@ module Katapult::GeneratorGoodies
5
8
  run command
6
9
  end
7
10
 
11
+ def file_contains?(path, content)
12
+ file_content = File.read(path)
13
+ file_content.include? content
14
+ end
15
+
8
16
  private
9
17
 
10
18
  def app_name(kind = nil)
@@ -27,4 +35,10 @@ module Katapult::GeneratorGoodies
27
35
  end
28
36
  end
29
37
 
38
+ # Override Thor method
39
+ def rake(command, config = {})
40
+ command.prepend 'bundle exec rake '
41
+ run command, config
42
+ end
43
+
30
44
  end