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