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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/CHANGELOG.md +34 -12
- data/README.md +154 -114
- data/Rakefile +3 -3
- data/bin/katapult +34 -8
- data/features/application_model.feature +8 -44
- data/features/authenticate.feature +21 -4
- data/features/basics.feature +16 -89
- data/features/binary.feature +37 -4
- data/features/model.feature +40 -24
- data/features/navigation.feature +2 -0
- data/features/step_definitions/katapult_steps.rb +48 -16
- data/features/step_definitions/test_steps.rb +2 -0
- data/features/templates.feature +74 -0
- data/lib/generators/katapult/app_model/templates/lib/katapult/application_model.rb +2 -2
- data/lib/generators/katapult/basics/basics_generator.rb +12 -1
- data/lib/generators/katapult/basics/templates/Capfile +5 -0
- data/lib/generators/katapult/basics/templates/Gemfile +3 -1
- data/lib/generators/katapult/basics/templates/Gemfile.lock +376 -0
- data/lib/generators/katapult/basics/templates/app/models/application_record.rb +28 -0
- data/lib/generators/katapult/basics/templates/app/webpack/assets/javascripts/bootstrap.js +2 -2
- data/lib/generators/katapult/basics/templates/config/deploy.rb +2 -3
- data/lib/generators/katapult/basics/templates/config/deploy/production.rb +2 -2
- data/lib/generators/katapult/basics/templates/config/deploy/staging.rb +1 -1
- data/lib/generators/katapult/basics/templates/lib/capistrano/tasks/deploy.rake +1 -4
- data/lib/generators/katapult/clearance/clearance_generator.rb +13 -4
- data/lib/generators/katapult/clearance/templates/features/authentication.feature +3 -3
- data/lib/generators/katapult/model/model_generator.rb +14 -4
- data/lib/generators/katapult/templates/templates_generator.rb +35 -0
- data/lib/generators/katapult/transform/transform_generator.rb +3 -3
- data/lib/generators/katapult/views/views_generator.rb +1 -1
- data/lib/generators/katapult/web_ui/web_ui_generator.rb +1 -1
- data/lib/katapult/application_model.rb +7 -7
- data/lib/katapult/elements/attribute.rb +16 -0
- data/lib/katapult/elements/authentication.rb +9 -6
- data/lib/katapult/elements/model.rb +8 -4
- data/lib/katapult/elements/navigation.rb +2 -2
- data/lib/katapult/elements/web_ui.rb +2 -2
- data/lib/katapult/generator.rb +12 -2
- data/lib/katapult/support/generator_goodies.rb +14 -0
- data/lib/katapult/version.rb +1 -1
- data/script/update +5 -1
- metadata +8 -8
- data/features/configuration.feature +0 -24
- data/lib/generators/katapult/basics/templates/lib/capistrano/tasks/passenger.rake +0 -8
- data/lib/generators/katapult/basics/templates/lib/ext/active_record/find_by_anything.rb +0 -20
- 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
|
-
|
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
|
-
|
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 :
|
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
|
8
|
-
# server 'two.example.com', user: 'deploy-user', roles: %w
|
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
|
7
|
+
# server 'example.com', user: 'deploy-user', roles: %w[app web cron db]
|
@@ -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
|
128
|
-
gsub_file factories_file, " factory :user\n
|
127
|
+
# Remove empty factory
|
128
|
+
gsub_file factories_file, "\n factory :user\n", ''
|
129
129
|
|
130
|
-
|
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
|
-
|
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 "
|
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
|
-
|
35
|
-
|
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
|
-
|
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
|
-
|
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
|
@@ -11,7 +11,7 @@ module Katapult
|
|
11
11
|
|
12
12
|
NotFound = Class.new(StandardError)
|
13
13
|
|
14
|
-
attr_reader :models, :web_uis, :
|
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
|
-
@
|
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
|
85
|
-
web_uis.each
|
86
|
-
|
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
|
-
|
11
|
-
user_attrs = user_model.attrs.map(&:name)
|
10
|
+
user_attrs = user.attrs.map(&:name)
|
12
11
|
|
13
|
-
|
14
|
-
|
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.
|
51
|
+
attrs.select &:renderable?
|
52
52
|
end
|
53
53
|
|
54
54
|
def editable_attrs
|
55
|
-
attrs.
|
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
|
data/lib/katapult/generator.rb
CHANGED
@@ -9,10 +9,13 @@ module Katapult
|
|
9
9
|
|
10
10
|
attr_accessor :element
|
11
11
|
|
12
|
-
|
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
|
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
|