katapult 0.4.1 → 0.5.0

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