ditty 0.9.1 → 0.10.1

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 (105) hide show
  1. checksums.yaml +4 -4
  2. data/.env.test +2 -0
  3. data/.gitignore +1 -0
  4. data/.rubocop.yml +23 -4
  5. data/.travis.yml +2 -7
  6. data/Gemfile.ci +0 -15
  7. data/ditty.gemspec +19 -15
  8. data/lib/ditty.rb +2 -2
  9. data/lib/ditty/cli.rb +5 -0
  10. data/lib/ditty/components/ditty.rb +4 -7
  11. data/lib/ditty/controllers/application_controller.rb +6 -5
  12. data/lib/ditty/controllers/audit_logs_controller.rb +2 -0
  13. data/lib/ditty/controllers/auth_controller.rb +5 -2
  14. data/lib/ditty/controllers/component_controller.rb +3 -3
  15. data/lib/ditty/controllers/user_login_traits_controller.rb +28 -1
  16. data/lib/ditty/controllers/users_controller.rb +3 -2
  17. data/lib/ditty/db.rb +4 -3
  18. data/lib/ditty/emails/base.rb +32 -30
  19. data/lib/ditty/generators/crud_generator.rb +51 -41
  20. data/lib/ditty/generators/project_generator.rb +1 -0
  21. data/lib/ditty/helpers/pundit.rb +4 -4
  22. data/lib/ditty/helpers/response.rb +6 -11
  23. data/lib/ditty/helpers/views.rb +21 -3
  24. data/lib/ditty/listener.rb +1 -1
  25. data/lib/ditty/models/base.rb +5 -0
  26. data/lib/ditty/models/identity.rb +7 -7
  27. data/lib/ditty/models/user.rb +9 -1
  28. data/lib/ditty/policies/user_policy.rb +1 -1
  29. data/lib/ditty/services/authentication.rb +19 -9
  30. data/lib/ditty/services/email.rb +13 -13
  31. data/lib/ditty/services/logger.rb +26 -20
  32. data/lib/ditty/services/pagination_wrapper.rb +7 -5
  33. data/lib/ditty/services/settings.rb +7 -6
  34. data/lib/ditty/tasks/ditty.rake +2 -1
  35. data/lib/ditty/templates/application.rb +1 -1
  36. data/lib/ditty/templates/config.ru +2 -2
  37. data/lib/ditty/templates/controller.rb.erb +7 -1
  38. data/{public → lib/ditty/templates/public}/browserconfig.xml +0 -0
  39. data/{public → lib/ditty/templates/public}/css/styles.css +0 -0
  40. data/lib/ditty/templates/public/favicon.ico +0 -0
  41. data/{public → lib/ditty/templates/public}/images/apple-icon.png +0 -0
  42. data/{public → lib/ditty/templates/public}/images/favicon-16x16.png +0 -0
  43. data/{public → lib/ditty/templates/public}/images/favicon-32x32.png +0 -0
  44. data/{public → lib/ditty/templates/public}/images/launcher-icon-1x.png +0 -0
  45. data/{public → lib/ditty/templates/public}/images/launcher-icon-2x.png +0 -0
  46. data/{public → lib/ditty/templates/public}/images/launcher-icon-4x.png +0 -0
  47. data/{public → lib/ditty/templates/public}/images/mstile-150x150.png +0 -0
  48. data/{public → lib/ditty/templates/public}/images/safari-pinned-tab.svg +0 -0
  49. data/{public → lib/ditty/templates/public}/js/scripts.js +0 -0
  50. data/{public/manifest.json → lib/ditty/templates/public/manifest.json.erb} +2 -2
  51. data/lib/ditty/templates/settings.yml.erb +1 -0
  52. data/lib/ditty/templates/spec_helper.rb +1 -1
  53. data/lib/ditty/templates/views/display.haml.tt +1 -1
  54. data/lib/ditty/templates/views/edit.haml.tt +1 -1
  55. data/lib/ditty/templates/views/index.haml.tt +1 -1
  56. data/lib/ditty/templates/views/new.haml.tt +1 -1
  57. data/lib/ditty/version.rb +1 -1
  58. data/spec/ditty/api_spec.rb +1 -1
  59. data/spec/ditty/emails/base_spec.rb +3 -3
  60. data/spec/ditty/emails/forgot_password_spec.rb +3 -2
  61. data/spec/ditty/models/user_spec.rb +3 -3
  62. data/spec/ditty/services/logger_spec.rb +7 -6
  63. data/spec/ditty/services/settings_spec.rb +2 -2
  64. data/spec/factories.rb +4 -4
  65. data/spec/spec_helper.rb +5 -1
  66. data/views/403.haml +1 -1
  67. data/views/500.haml +11 -0
  68. data/views/audit_logs/index.haml +12 -11
  69. data/views/auth/forgot_password.haml +29 -24
  70. data/views/auth/ldap.haml +1 -1
  71. data/views/auth/login.haml +3 -2
  72. data/views/auth/register.haml +3 -2
  73. data/views/auth/reset_password.haml +36 -19
  74. data/views/blank.haml +1 -0
  75. data/views/embedded.haml +17 -11
  76. data/views/layout.haml +16 -8
  77. data/views/partials/actions.haml +15 -14
  78. data/views/partials/filter_control.haml +1 -1
  79. data/views/partials/footer.haml +10 -2
  80. data/views/partials/form_tag.haml +1 -1
  81. data/views/partials/navitems.haml +25 -27
  82. data/views/partials/pager.haml +44 -25
  83. data/views/partials/search.haml +14 -9
  84. data/views/partials/sidebar.haml +2 -2
  85. data/views/partials/sort_ui.haml +2 -0
  86. data/views/partials/timespan_selector.haml +64 -0
  87. data/views/partials/topbar.haml +0 -15
  88. data/views/partials/user_associations.haml +32 -0
  89. data/views/quick_start.haml +23 -0
  90. data/views/roles/display.haml +3 -3
  91. data/views/roles/edit.haml +1 -1
  92. data/views/roles/index.haml +2 -2
  93. data/views/roles/new.haml +1 -1
  94. data/views/user_login_traits/display.haml +1 -1
  95. data/views/user_login_traits/edit.haml +1 -1
  96. data/views/user_login_traits/index.haml +23 -25
  97. data/views/user_login_traits/new.haml +1 -1
  98. data/views/users/display.haml +5 -5
  99. data/views/users/edit.haml +1 -1
  100. data/views/users/index.haml +5 -5
  101. data/views/users/login_traits.haml +2 -2
  102. data/views/users/new.haml +1 -1
  103. data/views/users/profile.haml +4 -4
  104. data/views/users/user.haml +1 -1
  105. metadata +116 -54
@@ -31,6 +31,8 @@ module Ditty
31
31
  def create_model
32
32
  filename = File.join("lib/#{folder}/models", "#{model_name.underscore}.rb")
33
33
  template '../templates/model.rb.erb', filename
34
+ rescue StandardError => e
35
+ puts "Could not generate model for #{model_name}: #{e.message}"
34
36
  end
35
37
 
36
38
  def create_controller
@@ -38,16 +40,22 @@ module Ditty
38
40
  template '../templates/controller.rb.erb', filename
39
41
  # TODO: Insert the route into the component file
40
42
  # insert_into_file 'config.ru', "use #{class_name}\n", after: 'run ApplicationController\n'
43
+ rescue StandardError => e
44
+ puts "Could not generate controller for #{model_name}: #{e.message}"
41
45
  end
42
46
 
43
47
  def create_policy
44
48
  filename = File.join("lib/#{folder}/policies", "#{policy_name.underscore}.rb")
45
49
  template '../templates/policy.rb.erb', filename
50
+ rescue StandardError => e
51
+ puts "Could not generate policy for #{model_name}: #{e.message}"
46
52
  end
47
53
 
48
54
  def create_type
49
55
  filename = File.join("lib/#{folder}/types", "#{model_name.underscore}_type.rb")
50
56
  template '../templates/type.rb.erb', filename
57
+ rescue StandardError => e
58
+ puts "Could not generate type for #{model_name}: #{e.message}"
51
59
  end
52
60
 
53
61
  def create_views
@@ -58,47 +66,49 @@ module Ditty
58
66
 
59
67
  private
60
68
 
61
- def meta_columns
62
- %i[id guid slug created_at updated_at]
63
- end
64
-
65
- def columns
66
- require "#{folder}/models/#{model_name.underscore}"
67
- name.constantize.columns
68
- rescue StandardError
69
- []
70
- end
71
-
72
- def schema
73
- require "#{folder}/models/#{model_name.underscore}"
74
- name.constantize.db_schema
75
- rescue StandardError
76
- []
77
- end
78
-
79
- def many_to_ones
80
- DB.foreign_key_list(model_name.underscore.pluralize)
81
- end
82
-
83
- def name_column(table)
84
- candidates = DB.schema(table.to_sym).to_h.keys - DB.foreign_key_list(table.to_sym).map { |e| e[:columns] }.flatten
85
- (candidates - meta_columns).first
86
- end
87
-
88
- def graphql_types
89
- @graphql_types ||= Hash.new('String').merge(
90
- integer: 'Integer',
91
- boolean: 'Boolean',
92
- datetime: 'GraphQL::Types::ISO8601DateTime'
93
- )
94
- end
95
-
96
- def input_types
97
- @input_types ||= Hash.new('text').merge(
98
- integer: 'number',
99
- datetime: 'date'
100
- )
101
- end
69
+ def meta_columns
70
+ %i[id guid slug created_at updated_at]
71
+ end
72
+
73
+ def columns
74
+ require "#{folder}/models/#{model_name.underscore}"
75
+ name.constantize.columns
76
+ rescue StandardError
77
+ []
78
+ end
79
+
80
+ def schema
81
+ require "#{folder}/models/#{model_name.underscore}"
82
+ name.constantize.db_schema
83
+ rescue StandardError
84
+ []
85
+ end
86
+
87
+ def many_to_ones
88
+ DB.foreign_key_list(model_name.underscore.pluralize)
89
+ end
90
+
91
+ def name_column(table)
92
+ candidates = DB.schema(table.to_sym).to_h.keys - DB.foreign_key_list(table.to_sym).map do |e|
93
+ e[:columns]
94
+ end.flatten
95
+ (candidates - meta_columns).first
96
+ end
97
+
98
+ def graphql_types
99
+ @graphql_types ||= Hash.new('String').merge(
100
+ integer: 'Integer',
101
+ boolean: 'Boolean',
102
+ datetime: 'GraphQL::Types::ISO8601DateTime'
103
+ )
104
+ end
105
+
106
+ def input_types
107
+ @input_types ||= Hash.new('text').merge(
108
+ integer: 'number',
109
+ datetime: 'date'
110
+ )
111
+ end
102
112
  end
103
113
  end
104
114
  end
@@ -27,6 +27,7 @@ module Ditty
27
27
  directory 'logs'
28
28
  directory 'pids'
29
29
  directory 'public'
30
+ directory '../../../views', 'views'
30
31
  copy_file '.gitignore', './.gitignore'
31
32
  copy_file 'env.example', './.env'
32
33
  copy_file '.rubocop.yml', './.rubocop.yml'
@@ -16,10 +16,10 @@ module Ditty
16
16
  policy = policy(record)
17
17
  action ||= record.new? ? :create : :update
18
18
  method_name = if policy.respond_to?("permitted_attributes_for_#{action}")
19
- "permitted_attributes_for_#{action}"
20
- else
21
- 'permitted_attributes'
22
- end
19
+ "permitted_attributes_for_#{action}"
20
+ else
21
+ 'permitted_attributes'
22
+ end
23
23
  policy.public_send(method_name)
24
24
  end
25
25
 
@@ -2,8 +2,6 @@
2
2
 
3
3
  require 'csv'
4
4
 
5
- require 'csv'
6
-
7
5
  module Ditty
8
6
  module Helpers
9
7
  module Response
@@ -27,6 +25,7 @@ module Ditty
27
25
  )
28
26
  end
29
27
  format.csv do
28
+ attachment "#{base_path}.csv"
30
29
  CSV.generate do |csv|
31
30
  csv << result.first.for_csv.keys
32
31
  result.all.each do |r|
@@ -41,7 +40,7 @@ module Ditty
41
40
  respond_to do |format|
42
41
  format.html do
43
42
  flash[:success] = "#{heading} Created"
44
- redirect with_layout(flash[:redirect_to] || "#{base_path}/#{entity.display_id}")
43
+ redirect with_layout(params[:redirect_to] || flash[:redirect_to] || "#{base_path}/#{entity.display_id}")
45
44
  end
46
45
  format.json do
47
46
  content_type :json
@@ -62,6 +61,8 @@ module Ditty
62
61
  respond_to do |format|
63
62
  format.html do
64
63
  title = heading(:read) + (entity.respond_to?(:name) ? ": #{entity.name}" : '')
64
+ last_modified entity.updated_at if entity.respond_to?(:updated_at)
65
+ etag entity.etag if entity.respond_to?(:etag)
65
66
  haml :"#{view_location}/display",
66
67
  locals: { entity: entity, title: title, actions: actions },
67
68
  layout: layout
@@ -76,12 +77,6 @@ module Ditty
76
77
  csv << entity.for_csv.values
77
78
  end
78
79
  end
79
- format.csv do
80
- CSV.generate do |csv|
81
- csv << entity.for_csv.keys
82
- csv << entity.for_csv.values
83
- end
84
- end
85
80
  end
86
81
  end
87
82
 
@@ -90,7 +85,7 @@ module Ditty
90
85
  format.html do
91
86
  # TODO: Ability to customize the return path and message?
92
87
  flash[:success] = "#{heading} Updated"
93
- redirect with_layout(flash[:redirect_to] || back || "#{base_path}/#{entity.display_id}")
88
+ redirect with_layout(params[:redirect_to] || flash[:redirect_to] || "#{base_path}/#{entity.display_id}")
94
89
  end
95
90
  format.json do
96
91
  content_type :json
@@ -103,7 +98,7 @@ module Ditty
103
98
  respond_to do |format|
104
99
  format.html do
105
100
  flash[:success] = "#{heading} Deleted"
106
- redirect with_layout(flash[:redirect_to] || back || base_path)
101
+ redirect with_layout(params[:redirect_to] || flash[:redirect_to] || back || base_path)
107
102
  end
108
103
  format.json do
109
104
  content_type :json
@@ -62,13 +62,13 @@ module Ditty
62
62
  messages = flash(key).collect do |message|
63
63
  " <div class='alert alert-#{message[0]} alert-dismissable' role='alert'>#{message[1]}</div>\n"
64
64
  end
65
- "<div id='#{id}'>\n" + messages.join + '</div>'
65
+ "<div id='#{id}'>\n#{messages.join}</div>"
66
66
  end
67
67
 
68
68
  def query_string(add = {})
69
69
  qs = params.clone.merge(add)
70
70
  qs.delete('captures')
71
- Rack::Utils.build_query qs
71
+ Rack::Utils.build_query(qs.delete_if { |_k, v| v == '' })
72
72
  end
73
73
 
74
74
  def delete_form(entity, label = 'Delete')
@@ -94,7 +94,7 @@ module Ditty
94
94
  def form_tag(url, options = {}, &block)
95
95
  options[:form_verb] ||= :post
96
96
  options[:attributes] ||= {}
97
- options[:attributes] = { 'class': 'form-horizontal' }.merge options[:attributes]
97
+ options[:attributes] = { class: 'form-horizontal' }.merge options[:attributes]
98
98
  options[:url] = options[:form_verb].to_sym == :get ? url : with_layout(url)
99
99
  haml :'partials/form_tag', locals: options.merge(block: block)
100
100
  end
@@ -138,6 +138,24 @@ module Ditty
138
138
  haml_tag :a, name, html_options
139
139
  end
140
140
  end
141
+
142
+ def sort_ui(field)
143
+ haml :'partials/sort_ui', locals: { field: field }
144
+ end
145
+
146
+ def sort_query(field)
147
+ query_string(
148
+ order: params[:sort] == field.to_s && params[:order] == 'asc' ? 'desc' : 'asc',
149
+ sort: field,
150
+ )
151
+ end
152
+
153
+ def sort_icon(field)
154
+ return 'fa-sort' unless params[:sort] == field.to_s
155
+ return 'fa-sort-up' if params[:order] == 'asc'
156
+
157
+ 'fa-sort-down'
158
+ end
141
159
  end
142
160
  end
143
161
  end
@@ -66,7 +66,7 @@ module Ditty
66
66
  def action_from(target, method)
67
67
  return method unless method.to_s.start_with? 'component_'
68
68
 
69
- target.class.to_s.demodulize.underscore + '_' + method.to_s.gsub(/^component_/, '')
69
+ "#{target.class.to_s.demodulize.underscore}_#{method.to_s.gsub(/^component_/, '')}"
70
70
  end
71
71
 
72
72
  def log_action(values)
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'digest/sha2'
3
4
  require 'sequel'
4
5
 
5
6
  module Ditty
@@ -12,6 +13,10 @@ module Ditty
12
13
  self[:slug] || self[:guid] || self[:id]
13
14
  end
14
15
 
16
+ def etag
17
+ Digest::SHA2.hexdigest values.to_json
18
+ end
19
+
15
20
  alias for_csv for_json
16
21
  end
17
22
  end
@@ -55,7 +55,7 @@ module Ditty
55
55
  # 1 Special Character
56
56
  # 1 Number
57
57
  # At least 8 characters
58
- %r[\A(?=.*[A-Z])(?=.*[a-z])(?=.*[!@#&$*)(}{%^=_+|\\:";'<>,.\-\/?\[\]])(?=.*[0-9]).{8,}\Z],
58
+ %r[\A(?=.*[A-Z])(?=.*[a-z])(?=.*[!@#&$*)(}{%^=_+|\\:";'<>,.\-/?\[\]])(?=.*[0-9]).{8,}\Z],
59
59
  :password,
60
60
  message: 'is not strong enough'
61
61
  )
@@ -72,12 +72,12 @@ module Ditty
72
72
 
73
73
  private
74
74
 
75
- def encrypt_password
76
- self.crypted_password = ::BCrypt::Password.create(password)
77
- end
75
+ def encrypt_password
76
+ self.crypted_password = ::BCrypt::Password.create(password)
77
+ end
78
78
 
79
- def password_required
80
- crypted_password.blank? || !password.blank?
81
- end
79
+ def password_required
80
+ crypted_password.blank? || !password.blank?
81
+ end
82
82
  end
83
83
  end
@@ -40,7 +40,9 @@ module Ditty
40
40
  end
41
41
 
42
42
  def respond_to_missing?(name, _include_private = false)
43
- name[-1] == '?'
43
+ return true if name[-1] == '?'
44
+
45
+ super
44
46
  end
45
47
 
46
48
  def gravatar
@@ -57,6 +59,12 @@ module Ditty
57
59
  validates_format(/\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i, :email)
58
60
  end
59
61
 
62
+ def before_save
63
+ super
64
+ self.name = nil if name.blank?
65
+ self.surname = nil if surname.blank?
66
+ end
67
+
60
68
  # Add the basic roles and identity
61
69
  def after_create
62
70
  super
@@ -26,7 +26,7 @@ module Ditty
26
26
  end
27
27
 
28
28
  def delete?
29
- create?
29
+ create? && record&.super_admin? == false
30
30
  end
31
31
 
32
32
  def permitted_attributes
@@ -3,7 +3,6 @@
3
3
  require 'ditty/controllers/application_controller'
4
4
  require 'ditty/services/settings'
5
5
  require 'ditty/services/logger'
6
- require 'backports/2.4.0/hash/compact'
7
6
 
8
7
  require 'omniauth'
9
8
  OmniAuth.config.logger = ::Ditty::Services::Logger
@@ -23,26 +22,36 @@ module Ditty
23
22
  end
24
23
 
25
24
  def providers
26
- config.compact.keys
25
+ config.compact.keys.select { |e| config[e][:available] && config[e][:enabled] != false }
27
26
  end
28
27
 
29
28
  def setup
30
- providers.each do |provider|
31
- req = config.dig(provider, :require) || "omniauth/#{provider}"
32
- begin
33
- require req
29
+ config.compact.each_key do |provider|
30
+ ::Ditty::Services::Logger.debug "Loading authentication provider #{provider}"
31
+ req = if config.dig(provider, :require)
32
+ [config[provider][:require]]
33
+ else
34
+ ["omniauth/#{provider}", "omniauth-#{provider}"]
35
+ end
36
+ req.find do |e|
37
+ require e
38
+ config[provider][:available] = true
39
+ true
34
40
  rescue LoadError
35
- require "omniauth-#{provider}"
41
+ ::Ditty::Services::Logger.warn "Could not load authentication provider #{provider} using #{e}"
42
+ config[provider][:available] = false
43
+ false
36
44
  end
37
45
  end
38
46
  end
39
47
 
40
48
  def config
41
- default.merge ::Ditty::Services::Settings.values(:authentication) || {}
49
+ @config ||= default.merge(::Ditty::Services::Settings.values(:authentication) || {})
42
50
  end
43
51
 
44
52
  def provides?(provider)
45
- providers.include? provider.to_sym
53
+ provider = provider.to_sym
54
+ providers.include?(provider) && config[provider][:available] && config.dig(provider, :enabled) != false
46
55
  end
47
56
 
48
57
  def default
@@ -50,6 +59,7 @@ module Ditty
50
59
  require 'ditty/controllers/auth_controller'
51
60
  {
52
61
  identity: {
62
+ available: true,
53
63
  arguments: [
54
64
  {
55
65
  fields: [:username],
@@ -34,21 +34,21 @@ module Ditty
34
34
 
35
35
  private
36
36
 
37
- def config
38
- @config ||= default.merge ::Ditty::Services::Settings.values(:email) || {}
39
- end
37
+ def config
38
+ @config ||= default.merge ::Ditty::Services::Settings.values(:email) || {}
39
+ end
40
40
 
41
- def default
42
- {
43
- delivery_method: :logger,
44
- logger: ::Ditty::Services::Logger
45
- }
46
- end
41
+ def default
42
+ {
43
+ delivery_method: :logger,
44
+ logger: ::Ditty::Services::Logger
45
+ }
46
+ end
47
47
 
48
- def from_symbol(email, options)
49
- require "ditty/emails/#{email}"
50
- constantize("Ditty::Emails::#{classify(email)}").new(options)
51
- end
48
+ def from_symbol(email, options)
49
+ require "ditty/emails/#{email}"
50
+ constantize("Ditty::Emails::#{classify(email)}").new(options)
51
+ end
52
52
  end
53
53
  end
54
54
  end