ditty 0.7.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 (185) hide show
  1. checksums.yaml +4 -4
  2. data/.env.test +2 -0
  3. data/.gitignore +3 -0
  4. data/.pryrc +2 -0
  5. data/.rubocop.yml +24 -8
  6. data/.travis.yml +4 -8
  7. data/CNAME +1 -0
  8. data/Dockerfile +18 -0
  9. data/Gemfile.ci +0 -15
  10. data/Rakefile +5 -4
  11. data/Readme.md +24 -2
  12. data/_config.yml +1 -0
  13. data/config.ru +4 -4
  14. data/ditty.gemspec +31 -20
  15. data/docs/CNAME +1 -0
  16. data/docs/_config.yml +1 -0
  17. data/docs/index.md +34 -0
  18. data/exe/ditty +2 -0
  19. data/lib/ditty.rb +30 -4
  20. data/lib/ditty/cli.rb +38 -5
  21. data/lib/ditty/components/ditty.rb +82 -0
  22. data/lib/ditty/controllers/application_controller.rb +267 -0
  23. data/lib/ditty/controllers/{audit_logs.rb → audit_logs_controller.rb} +5 -7
  24. data/lib/ditty/controllers/{auth.rb → auth_controller.rb} +56 -32
  25. data/lib/ditty/controllers/{component.rb → component_controller.rb} +35 -24
  26. data/lib/ditty/controllers/{main.rb → main_controller.rb} +7 -7
  27. data/lib/ditty/controllers/roles_controller.rb +23 -0
  28. data/lib/ditty/controllers/user_login_traits_controller.rb +46 -0
  29. data/lib/ditty/controllers/{users.rb → users_controller.rb} +17 -20
  30. data/lib/ditty/db.rb +9 -5
  31. data/lib/ditty/emails/base.rb +48 -34
  32. data/lib/ditty/generators/crud_generator.rb +114 -0
  33. data/lib/ditty/generators/migration_generator.rb +26 -0
  34. data/lib/ditty/generators/project_generator.rb +52 -0
  35. data/lib/ditty/helpers/authentication.rb +6 -5
  36. data/lib/ditty/helpers/component.rb +11 -2
  37. data/lib/ditty/helpers/pundit.rb +24 -8
  38. data/lib/ditty/helpers/response.rb +38 -15
  39. data/lib/ditty/helpers/views.rb +48 -6
  40. data/lib/ditty/listener.rb +44 -14
  41. data/lib/ditty/memcached.rb +8 -0
  42. data/lib/ditty/middleware/accept_extension.rb +4 -2
  43. data/lib/ditty/middleware/error_catchall.rb +4 -2
  44. data/lib/ditty/models/audit_log.rb +1 -0
  45. data/lib/ditty/models/base.rb +13 -0
  46. data/lib/ditty/models/identity.rb +10 -7
  47. data/lib/ditty/models/role.rb +2 -0
  48. data/lib/ditty/models/user.rb +40 -3
  49. data/lib/ditty/models/user_login_trait.rb +17 -0
  50. data/lib/ditty/policies/audit_log_policy.rb +6 -6
  51. data/lib/ditty/policies/role_policy.rb +3 -3
  52. data/lib/ditty/policies/user_login_trait_policy.rb +45 -0
  53. data/lib/ditty/policies/user_policy.rb +3 -3
  54. data/lib/ditty/rubocop.rb +3 -0
  55. data/lib/ditty/seed.rb +2 -0
  56. data/lib/ditty/services/authentication.rb +31 -15
  57. data/lib/ditty/services/email.rb +22 -12
  58. data/lib/ditty/services/logger.rb +30 -13
  59. data/lib/ditty/services/pagination_wrapper.rb +9 -5
  60. data/lib/ditty/services/settings.rb +19 -7
  61. data/lib/ditty/tasks/ditty.rake +127 -0
  62. data/lib/ditty/tasks/omniauth-ldap.rake +43 -0
  63. data/lib/ditty/templates/.gitignore +5 -0
  64. data/lib/ditty/templates/.rspec +2 -0
  65. data/lib/ditty/templates/.rubocop.yml +7 -0
  66. data/lib/ditty/templates/Rakefile +12 -0
  67. data/lib/ditty/templates/application.rb +12 -0
  68. data/lib/ditty/templates/config.ru +37 -0
  69. data/lib/ditty/templates/controller.rb.erb +64 -0
  70. data/lib/ditty/templates/env.example +4 -0
  71. data/lib/ditty/templates/lib/project.rb.erb +5 -0
  72. data/lib/ditty/templates/migration.rb.erb +7 -0
  73. data/lib/ditty/templates/model.rb.erb +26 -0
  74. data/lib/ditty/templates/pids/.empty_directory +0 -0
  75. data/lib/ditty/templates/policy.rb.erb +48 -0
  76. data/{public → lib/ditty/templates/public}/browserconfig.xml +0 -0
  77. data/lib/ditty/templates/public/css/sb-admin-2.min.css +10 -0
  78. data/lib/ditty/templates/public/css/styles.css +13 -0
  79. data/lib/ditty/templates/public/favicon.ico +0 -0
  80. data/{public → lib/ditty/templates/public}/images/apple-icon.png +0 -0
  81. data/{public → lib/ditty/templates/public}/images/favicon-16x16.png +0 -0
  82. data/{public → lib/ditty/templates/public}/images/favicon-32x32.png +0 -0
  83. data/{public → lib/ditty/templates/public}/images/launcher-icon-1x.png +0 -0
  84. data/{public → lib/ditty/templates/public}/images/launcher-icon-2x.png +0 -0
  85. data/{public → lib/ditty/templates/public}/images/launcher-icon-4x.png +0 -0
  86. data/{public → lib/ditty/templates/public}/images/mstile-150x150.png +0 -0
  87. data/{public → lib/ditty/templates/public}/images/safari-pinned-tab.svg +0 -0
  88. data/lib/ditty/templates/public/js/sb-admin-2.min.js +7 -0
  89. data/lib/ditty/templates/public/js/scripts.js +1 -0
  90. data/{public/manifest.json → lib/ditty/templates/public/manifest.json.erb} +2 -2
  91. data/lib/ditty/templates/settings.yml.erb +19 -0
  92. data/lib/ditty/templates/sidekiq.rb +18 -0
  93. data/lib/ditty/templates/sidekiq.yml +9 -0
  94. data/lib/ditty/templates/spec_helper.rb +43 -0
  95. data/lib/ditty/templates/type.rb.erb +21 -0
  96. data/lib/ditty/templates/views/display.haml.tt +20 -0
  97. data/lib/ditty/templates/views/edit.haml.tt +10 -0
  98. data/lib/ditty/templates/views/form.haml.tt +11 -0
  99. data/lib/ditty/templates/views/index.haml.tt +29 -0
  100. data/lib/ditty/templates/views/new.haml.tt +10 -0
  101. data/lib/ditty/version.rb +1 -1
  102. data/lib/rubocop/cop/ditty/call_services_directly.rb +42 -0
  103. data/migrate/20181209_add_user_login_traits.rb +16 -0
  104. data/migrate/20181209_extend_audit_log.rb +12 -0
  105. data/migrate/20190220_add_parent_id_to_roles.rb +9 -0
  106. data/spec/ditty/api_spec.rb +51 -0
  107. data/spec/ditty/controllers/roles_spec.rb +67 -0
  108. data/spec/ditty/controllers/user_login_traits_spec.rb +72 -0
  109. data/spec/ditty/controllers/users_spec.rb +72 -0
  110. data/spec/ditty/emails/base_spec.rb +76 -0
  111. data/spec/ditty/emails/forgot_password_spec.rb +20 -0
  112. data/spec/ditty/helpers/component_spec.rb +85 -0
  113. data/spec/ditty/models/user_spec.rb +36 -0
  114. data/spec/ditty/services/email_spec.rb +36 -0
  115. data/spec/ditty/services/logger_spec.rb +68 -0
  116. data/spec/ditty/services/settings_spec.rb +63 -0
  117. data/spec/ditty_spec.rb +9 -0
  118. data/spec/factories.rb +46 -0
  119. data/spec/fixtures/logger.yml +17 -0
  120. data/spec/fixtures/section.yml +3 -0
  121. data/spec/fixtures/settings.yml +8 -0
  122. data/spec/spec_helper.rb +51 -0
  123. data/spec/support/api_shared_examples.rb +250 -0
  124. data/spec/support/crud_shared_examples.rb +145 -0
  125. data/views/403.haml +2 -0
  126. data/views/404.haml +2 -4
  127. data/views/500.haml +11 -0
  128. data/views/audit_logs/index.haml +32 -28
  129. data/views/auth/forgot_password.haml +32 -16
  130. data/views/auth/identity.haml +14 -13
  131. data/views/auth/ldap.haml +17 -0
  132. data/views/auth/login.haml +23 -17
  133. data/views/auth/register.haml +20 -18
  134. data/views/auth/register_identity.haml +27 -12
  135. data/views/auth/reset_password.haml +36 -19
  136. data/views/blank.haml +43 -0
  137. data/views/emails/forgot_password.haml +1 -1
  138. data/views/emails/layouts/action.haml +10 -6
  139. data/views/emails/layouts/alert.haml +2 -1
  140. data/views/emails/layouts/billing.haml +2 -1
  141. data/views/embedded.haml +17 -11
  142. data/views/error.haml +8 -3
  143. data/views/index.haml +1 -1
  144. data/views/layout.haml +45 -30
  145. data/views/partials/actions.haml +15 -14
  146. data/views/partials/content_tag.haml +0 -0
  147. data/views/partials/delete_form.haml +1 -1
  148. data/views/partials/filter_control.haml +2 -2
  149. data/views/partials/footer.haml +13 -5
  150. data/views/partials/form_control.haml +30 -19
  151. data/views/partials/form_tag.haml +1 -1
  152. data/views/partials/navitems.haml +42 -0
  153. data/views/partials/notifications.haml +12 -8
  154. data/views/partials/pager.haml +44 -25
  155. data/views/partials/search.haml +15 -11
  156. data/views/partials/sidebar.haml +15 -37
  157. data/views/partials/sort_ui.haml +2 -0
  158. data/views/partials/timespan_selector.haml +64 -0
  159. data/views/partials/topbar.haml +53 -0
  160. data/views/partials/user_associations.haml +32 -0
  161. data/views/quick_start.haml +23 -0
  162. data/views/roles/display.haml +27 -6
  163. data/views/roles/edit.haml +3 -3
  164. data/views/roles/form.haml +1 -0
  165. data/views/roles/index.haml +23 -14
  166. data/views/roles/new.haml +2 -2
  167. data/views/user_login_traits/display.haml +32 -0
  168. data/views/user_login_traits/edit.haml +10 -0
  169. data/views/user_login_traits/form.haml +5 -0
  170. data/views/user_login_traits/index.haml +28 -0
  171. data/views/user_login_traits/new.haml +10 -0
  172. data/views/users/display.haml +15 -16
  173. data/views/users/edit.haml +3 -3
  174. data/views/users/form.haml +0 -0
  175. data/views/users/index.haml +31 -24
  176. data/views/users/login_traits.haml +25 -0
  177. data/views/users/new.haml +2 -2
  178. data/views/users/profile.haml +17 -15
  179. data/views/users/user.haml +1 -1
  180. metadata +314 -76
  181. data/lib/ditty/components/app.rb +0 -77
  182. data/lib/ditty/controllers/application.rb +0 -175
  183. data/lib/ditty/controllers/roles.rb +0 -16
  184. data/lib/ditty/rake_tasks.rb +0 -102
  185. data/views/partials/navbar.haml +0 -23
@@ -13,8 +13,10 @@ module Ditty
13
13
  # param :count, Integer, min: 1, default: 10 # Can't do this, since count can be `all`
14
14
  def check_count
15
15
  return 10 if params[:count].nil?
16
+
16
17
  count = params[:count].to_i
17
18
  return count if count >= 1
19
+
18
20
  excp = Sinatra::Param::InvalidParameterError.new 'Parameter cannot be less than 1'
19
21
  excp.param = :count
20
22
  raise excp
@@ -31,22 +33,25 @@ module Ditty
31
33
  param :q, String
32
34
  param :page, Integer, min: 1, default: 1
33
35
  param :sort, String
34
- param :order, String, in: %w[asc desc], transform: :downcase, default: 'asc'
36
+ param :order, String, in: %w[asc desc], transform: :downcase
35
37
  # TODO: Can we dynamically validate the search / filter fields?
36
38
 
37
39
  ds = dataset
38
40
  ds = ds.dataset if ds.respond_to?(:dataset)
41
+ params[:order] ||= 'asc' if params[:sort]
39
42
  return ds if params[:count] == 'all'
43
+
40
44
  params[:count] = check_count
41
45
 
42
46
  # Account for difference between sequel paginate and will paginate
43
47
  return ds.paginate(page: params[:page], per_page: params[:count]) if ds.is_a?(Array)
48
+
44
49
  ds.paginate(params[:page], params[:count])
45
50
  end
46
51
 
47
52
  def heading(action = nil)
48
53
  @headings ||= begin
49
- heading = settings.model_class.to_s.demodulize.titleize
54
+ heading = settings.heading || settings.model_class.to_s.demodulize.singularize.titleize
50
55
  h = Hash.new(heading)
51
56
  h[:new] = "New #{heading}"
52
57
  h[:list] = pluralize heading
@@ -78,6 +83,7 @@ module Ditty
78
83
  def filters
79
84
  filter_fields.map do |filter|
80
85
  next if params[filter[:name]].blank?
86
+
81
87
  filter[:field] ||= filter[:name]
82
88
  filter[:modifier] ||= :to_s # TODO: Do this with Sinatra Param?
83
89
  filter
@@ -86,6 +92,7 @@ module Ditty
86
92
 
87
93
  def ordering
88
94
  return if params[:sort].blank?
95
+
89
96
  Sequel.send(params[:order].to_sym, params[:sort].to_sym)
90
97
  end
91
98
 
@@ -112,11 +119,13 @@ module Ditty
112
119
  assoc = filter[:field].to_s.split('.').first.to_sym
113
120
  assoc = settings.model_class.association_reflection(assoc)
114
121
  raise "Unknown association #{assoc}" if assoc.nil?
122
+
115
123
  assoc
116
124
  end
117
125
 
118
126
  def search_filters
119
127
  return [] if params[:q].blank?
128
+
120
129
  searchable_fields.map { |f| Sequel.ilike(f.to_sym, "%#{params[:q]}%") }
121
130
  end
122
131
  end
@@ -12,23 +12,39 @@ module Ditty
12
12
  super
13
13
  end
14
14
 
15
- def permitted_attributes(record, action)
16
- param_key = PolicyFinder.new(record).param_key
15
+ def permitted_attributes(record, action = nil)
17
16
  policy = policy(record)
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
+ policy.public_send(method_name)
24
+ end
23
25
 
24
- policy_fields = policy.public_send(method_name)
26
+ def permitted_parameters(record, action = nil)
27
+ param_key = PolicyFinder.new(record).param_key
28
+ policy_fields = permitted_attributes(record, action)
25
29
  request.params.fetch(param_key, {}).select do |key, _value|
26
30
  policy_fields.include? key.to_sym
27
31
  end
28
32
  end
29
33
 
34
+ def permitted_response_attributes(record, method = :values)
35
+ policy = policy(record)
36
+ response = record.send(method)
37
+
38
+ return response unless policy.respond_to? :response_attributes
39
+
40
+ policy_fields = policy.response_attributes
41
+ response.select do |key, _value|
42
+ policy_fields.include? key.to_sym
43
+ end
44
+ end
45
+
30
46
  def pundit_user
31
- current_user
47
+ current_user unless current_user&.anonymous?
32
48
  end
33
49
  end
34
50
  end
@@ -1,14 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'csv'
4
+
3
5
  module Ditty
4
6
  module Helpers
5
7
  module Response
6
- def list_response(result)
8
+ def list_response(result, view: 'index')
7
9
  respond_to do |format|
8
10
  format.html do
9
11
  actions = {}
10
12
  actions["#{base_path}/new"] = "New #{heading}" if policy(settings.model_class).create?
11
- haml :"#{view_location}/index",
13
+ haml :"#{view_location}/#{view}",
12
14
  locals: { list: result, title: heading(:list), actions: actions },
13
15
  layout: layout
14
16
  end
@@ -16,12 +18,21 @@ module Ditty
16
18
  # TODO: Add links defined by actions (New #{heading})
17
19
  total = result.respond_to?(:pagination_record_count) ? result.pagination_record_count : result.count
18
20
  json(
19
- 'items' => result.all.map(&:for_json),
21
+ 'items' => result.all.map { |e| permitted_response_attributes(e, :for_json) },
20
22
  'page' => (params['page'] || 1).to_i,
21
23
  'count' => result.count,
22
24
  'total' => total
23
25
  )
24
26
  end
27
+ format.csv do
28
+ attachment "#{base_path}.csv"
29
+ CSV.generate do |csv|
30
+ csv << result.first.for_csv.keys
31
+ result.all.each do |r|
32
+ csv << r.for_csv.values
33
+ end
34
+ end
35
+ end
25
36
  end
26
37
  end
27
38
 
@@ -29,29 +40,42 @@ module Ditty
29
40
  respond_to do |format|
30
41
  format.html do
31
42
  flash[:success] = "#{heading} Created"
32
- redirect with_layout("#{base_path}/#{entity.id}")
43
+ redirect with_layout(params[:redirect_to] || flash[:redirect_to] || "#{base_path}/#{entity.display_id}")
33
44
  end
34
45
  format.json do
35
46
  content_type :json
36
- redirect "#{base_path}/#{entity.id}", 201
47
+ redirect "#{base_path}/#{entity.display_id}", 201
37
48
  end
38
49
  end
39
50
  end
40
51
 
52
+ def actions(entity = nil)
53
+ actions = {}
54
+ actions["#{base_path}/#{entity.display_id}/edit"] = "Edit #{heading}" if entity && policy(entity).update?
55
+ actions["#{base_path}/new"] = "New #{heading}" if policy(settings.model_class).create?
56
+ actions
57
+ end
58
+
41
59
  def read_response(entity)
60
+ actions = actions(entity)
42
61
  respond_to do |format|
43
62
  format.html do
44
- actions = {}
45
- actions["#{base_path}/#{entity.id}/edit"] = "Edit #{heading}" if policy(entity).update?
46
- actions["#{base_path}/new"] = "New #{heading}" if policy(entity).create?
47
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)
48
66
  haml :"#{view_location}/display",
49
67
  locals: { entity: entity, title: title, actions: actions },
50
68
  layout: layout
51
69
  end
52
70
  format.json do
53
71
  # TODO: Add links defined by actions (Edit #{heading})
54
- json entity.for_json
72
+ json permitted_response_attributes(entity, :for_json)
73
+ end
74
+ format.csv do
75
+ CSV.generate do |csv|
76
+ csv << entity.for_csv.keys
77
+ csv << entity.for_csv.values
78
+ end
55
79
  end
56
80
  end
57
81
  end
@@ -61,11 +85,11 @@ module Ditty
61
85
  format.html do
62
86
  # TODO: Ability to customize the return path and message?
63
87
  flash[:success] = "#{heading} Updated"
64
- redirect with_layout("#{base_path}/#{entity.id}")
88
+ redirect with_layout(params[:redirect_to] || flash[:redirect_to] || "#{base_path}/#{entity.display_id}")
65
89
  end
66
90
  format.json do
67
- headers 'Location' => "#{base_path}/#{entity.id}"
68
- json body entity.for_json
91
+ content_type :json
92
+ redirect "#{base_path}/#{entity.display_id}", 200, json(entity.for_json)
69
93
  end
70
94
  end
71
95
  end
@@ -74,12 +98,11 @@ module Ditty
74
98
  respond_to do |format|
75
99
  format.html do
76
100
  flash[:success] = "#{heading} Deleted"
77
- redirect with_layout(base_path.to_s)
101
+ redirect with_layout(params[:redirect_to] || flash[:redirect_to] || back || base_path)
78
102
  end
79
103
  format.json do
80
104
  content_type :json
81
- headers 'Location' => base_path.to_s
82
- status 204
105
+ redirect base_path.to_s, 204
83
106
  end
84
107
  end
85
108
  end
@@ -7,6 +7,7 @@ module Ditty
7
7
  module Views
8
8
  def layout
9
9
  return :embedded if request.params['layout'] == 'embedded'
10
+
10
11
  :layout
11
12
  end
12
13
 
@@ -14,6 +15,7 @@ module Ditty
14
15
  uri = URI.parse(url)
15
16
  # Don't set the layout if there's none. Don't set the layout for external URIs
16
17
  return url if params['layout'].nil? || uri.host
18
+
17
19
  qs = { 'layout' => params['layout'] }.merge(uri.query ? CGI.parse(uri.query) : {})
18
20
  uri.query = Rack::Utils.build_query qs
19
21
  uri.to_s
@@ -22,9 +24,11 @@ module Ditty
22
24
  def form_control(name, model, opts = {})
23
25
  label = opts.delete(:label) || name.to_s.titlecase
24
26
  klass = opts.delete(:class) || 'form-control' unless opts[:type] == 'file'
27
+ klass = "#{klass} is-invalid" if model.errors[name]
25
28
  group = opts.delete(:group) || model.class.to_s.demodulize.underscore
26
29
  field = opts.delete(:field) || name
27
30
  default = opts.delete(:default) || nil
31
+ help_text = opts.delete(:help_text) || nil
28
32
 
29
33
  attributes = { type: 'text', id: name, name: "#{group}[#{name}]", class: klass }.merge(opts)
30
34
  haml :'partials/form_control', locals: {
@@ -34,33 +38,37 @@ module Ditty
34
38
  name: name,
35
39
  group: group,
36
40
  field: field,
37
- default: default
41
+ default: default,
42
+ help_text: help_text
38
43
  }
39
44
  end
40
45
 
41
46
  def filter_control(filter, opts = {})
42
47
  meth = "#{filter[:name]}_options".to_sym
43
48
  return unless respond_to? meth
49
+
44
50
  haml :'partials/filter_control', locals: {
45
51
  name: filter[:name],
46
52
  label: opts[:label] || filter[:name].to_s.titlecase,
47
- options: send(meth)
53
+ options: send(meth),
54
+ total_filters: opts[:filters]
48
55
  }
49
56
  end
50
57
 
51
58
  def flash_messages(key = :flash)
52
59
  return '' if flash(key).empty?
60
+
53
61
  id = (key == :flash ? 'flash' : "flash_#{key}")
54
62
  messages = flash(key).collect do |message|
55
63
  " <div class='alert alert-#{message[0]} alert-dismissable' role='alert'>#{message[1]}</div>\n"
56
64
  end
57
- "<div id='#{id}'>\n" + messages.join + '</div>'
65
+ "<div id='#{id}'>\n#{messages.join}</div>"
58
66
  end
59
67
 
60
68
  def query_string(add = {})
61
69
  qs = params.clone.merge(add)
62
70
  qs.delete('captures')
63
- Rack::Utils.build_query qs
71
+ Rack::Utils.build_query(qs.delete_if { |_k, v| v == '' })
64
72
  end
65
73
 
66
74
  def delete_form(entity, label = 'Delete')
@@ -86,14 +94,15 @@ module Ditty
86
94
  def form_tag(url, options = {}, &block)
87
95
  options[:form_verb] ||= :post
88
96
  options[:attributes] ||= {}
89
- options[:attributes] = { 'class': 'form-horizontal' }.merge options[:attributes]
97
+ options[:attributes] = { class: 'form-horizontal' }.merge options[:attributes]
90
98
  options[:url] = options[:form_verb].to_sym == :get ? url : with_layout(url)
91
99
  haml :'partials/form_tag', locals: options.merge(block: block)
92
100
  end
93
101
 
94
102
  def pagination(list, base_path, qp = {})
95
103
  return unless list.respond_to?(:pagination_record_count) || list.respond_to?(:total_entries)
96
- list = Ditty::Services::PaginationWrapper.new(list)
104
+
105
+ list = ::Ditty::Services::PaginationWrapper.new(list)
97
106
  locals = {
98
107
  first_link: "#{base_path}?" + query_string(qp.merge(page: 1)),
99
108
  next_link: list.last_page? ? '#' : "#{base_path}?" + query_string(qp.merge(page: list.next_page)),
@@ -114,6 +123,39 @@ module Ditty
114
123
  value
115
124
  end
116
125
  end
126
+
127
+ def url_for(options = nil)
128
+ return options if options.is_a? String
129
+ return request.env['HTTP_REFERER'] if options == :back && request.env['HTTP_REFERER']
130
+
131
+ raise 'Unimplemented'
132
+ end
133
+
134
+ def link_to(name = nil, options = nil, html_options = {})
135
+ html_options[:href] ||= url_for(options)
136
+
137
+ capture_haml do
138
+ haml_tag :a, name, html_options
139
+ end
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
117
159
  end
118
160
  end
119
161
  end
@@ -17,43 +17,73 @@ module Ditty
17
17
  end
18
18
 
19
19
  def method_missing(method, *args)
20
- return unless args[0].is_a?(Hash) && args[0][:target].is_a?(Sinatra::Base) && args[0][:target].settings.track_actions
20
+ unless args[0].is_a?(Hash) && args[0][:target].is_a?(Sinatra::Base) && args[0][:target].settings.track_actions
21
+ return
22
+ end
21
23
 
22
- log_action({
23
- user: args[0][:target].current_user,
24
- action: action_from(args[0][:target], method),
25
- details: args[0][:details]
26
- }.merge(args[0][:values] || {}))
24
+ log_action(
25
+ user_traits(args[0][:target]).merge(
26
+ action: action_from(args[0][:target], method),
27
+ details: args[0][:details]
28
+ ).merge(args[0][:values] || {})
29
+ )
27
30
  end
28
31
 
29
32
  def respond_to_missing?(method, _include_private = false)
30
33
  EVENTS.include? method
31
34
  end
32
35
 
36
+ def user_login(event)
37
+ log_action(
38
+ user_traits(event[:target]).merge(
39
+ action: action_from(event[:target], :user_login),
40
+ details: event[:details]
41
+ ).merge(event[:values] || {})
42
+ )
43
+
44
+ @mutex.synchronize do
45
+ UserLoginTrait.update_or_create(user_traits(event[:target]), updated_at: Time.now)
46
+ end
47
+ end
48
+
33
49
  def user_register(event)
34
50
  user = event[:values][:user]
35
- log_action({
36
- user: user,
37
- action: action_from(event[:target], :user_register),
38
- details: event[:details]
39
- }.merge(event[:values] || {}))
51
+ log_action(
52
+ user_traits(event[:target]).merge(
53
+ user_id: user.id,
54
+ action: action_from(event[:target], :user_register),
55
+ details: event[:details]
56
+ ).merge(event[:values] || {})
57
+ )
40
58
 
41
59
  # Create the SA user if none is present
42
60
  sa = Role.find_or_create(name: 'super_admin')
43
61
  return if User.where(roles: sa).count.positive?
62
+
44
63
  user.add_role sa
45
64
  end
46
65
 
47
66
  def action_from(target, method)
48
67
  return method unless method.to_s.start_with? 'component_'
49
- target.class.to_s.demodulize.underscore + '_' + method.to_s.gsub(/^component_/, '')
68
+
69
+ "#{target.class.to_s.demodulize.underscore}_#{method.to_s.gsub(/^component_/, '')}"
50
70
  end
51
71
 
52
72
  def log_action(values)
53
73
  values[:user] ||= values[:target].current_user if values[:target]
54
- @mutex.synchronize { Ditty::AuditLog.create values }
74
+ @mutex.synchronize { ::Ditty::AuditLog.create values }
75
+ end
76
+
77
+ def user_traits(target)
78
+ {
79
+ user_id: target.current_user&.id,
80
+ platform: target.browser.platform.name,
81
+ device: target.browser.device.name,
82
+ browser: target.browser.name,
83
+ ip_address: target.request.ip
84
+ }
55
85
  end
56
86
  end
57
87
  end
58
88
 
59
- Wisper.subscribe(Ditty::Listener.new) unless ENV['RACK_ENV'] == 'test'
89
+ Wisper.subscribe(::Ditty::Listener.new) unless ENV['RACK_ENV'] == 'test'