avo 0.2.3 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of avo might be problematic. Click here for more details.

Files changed (87) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +52 -48
  3. data/Gemfile.lock +26 -2
  4. data/README.md +30 -9
  5. data/app/controllers/avo/application_controller.rb +34 -1
  6. data/app/controllers/avo/filters_controller.rb +19 -0
  7. data/app/controllers/avo/relations_controller.rb +34 -0
  8. data/app/controllers/avo/resource_overview_controller.rb +15 -7
  9. data/app/controllers/avo/resources_controller.rb +71 -145
  10. data/app/controllers/avo/search_controller.rb +55 -0
  11. data/app/helpers/avo/application_helper.rb +6 -2
  12. data/app/views/layouts/avo/_javascript.html.erb +3 -0
  13. data/app/views/layouts/avo/_translations.html.erb +5 -0
  14. data/app/views/layouts/avo/application.html.erb +40 -18
  15. data/avo.gemspec +5 -2
  16. data/config/credentials.yml.enc +1 -0
  17. data/config/routes.rb +11 -7
  18. data/lib/avo.rb +4 -0
  19. data/lib/avo/app/action.rb +8 -5
  20. data/lib/avo/app/app.rb +35 -28
  21. data/lib/avo/app/fields/belongs_to.rb +2 -2
  22. data/lib/avo/app/fields/code_field.rb +2 -0
  23. data/lib/avo/app/fields/country_field.rb +1 -1
  24. data/lib/avo/app/fields/field.rb +3 -1
  25. data/lib/avo/app/fields/field_extensions/visible_in_different_views.rb +4 -0
  26. data/lib/avo/app/fields/has_and_belongs_to_many.rb +1 -0
  27. data/lib/avo/app/fields/has_many.rb +1 -0
  28. data/lib/avo/app/fields/has_one.rb +2 -2
  29. data/lib/avo/app/fields/id_field.rb +4 -4
  30. data/lib/avo/app/fields/markdown_field.rb +27 -0
  31. data/lib/avo/app/fields/password_field.rb +3 -1
  32. data/lib/avo/app/fields/select_field.rb +1 -1
  33. data/lib/avo/app/licensing/community_license.rb +4 -0
  34. data/lib/avo/app/licensing/hq.rb +86 -0
  35. data/lib/avo/app/licensing/license.rb +48 -0
  36. data/lib/avo/app/licensing/license_manager.rb +25 -0
  37. data/lib/avo/app/licensing/null_license.rb +12 -0
  38. data/lib/avo/app/licensing/pro_license.rb +9 -0
  39. data/lib/avo/app/resource.rb +49 -18
  40. data/lib/avo/app/services/authorization_service.rb +40 -0
  41. data/lib/avo/configuration.rb +28 -2
  42. data/lib/avo/engine.rb +7 -7
  43. data/lib/avo/version.rb +1 -1
  44. data/lib/generators/avo/install_generator.rb +2 -1
  45. data/lib/generators/avo/templates/{initializer.rb → initializer/avo.rb} +2 -0
  46. data/lib/generators/avo/templates/locales/avo.en.yml +60 -0
  47. data/lib/generators/avo/templates/views/_scripts.html.erb +0 -0
  48. data/public/avo-packs/css/application-2f609d81.css +3 -0
  49. data/public/avo-packs/css/application-2f609d81.css.br +0 -0
  50. data/public/avo-packs/css/application-2f609d81.css.gz +0 -0
  51. data/public/avo-packs/js/application-84e2d573c3c15df1fb7b.js +3 -0
  52. data/public/avo-packs/js/{application-9a0dde96ad9918852965.js.LICENSE.txt → application-84e2d573c3c15df1fb7b.js.LICENSE.txt} +0 -0
  53. data/public/avo-packs/js/application-84e2d573c3c15df1fb7b.js.br +0 -0
  54. data/public/avo-packs/js/application-84e2d573c3c15df1fb7b.js.gz +0 -0
  55. data/public/avo-packs/js/application-84e2d573c3c15df1fb7b.js.map +1 -0
  56. data/public/avo-packs/js/application-84e2d573c3c15df1fb7b.js.map.br +0 -0
  57. data/public/avo-packs/js/application-84e2d573c3c15df1fb7b.js.map.gz +0 -0
  58. data/public/avo-packs/manifest.json +13 -6
  59. data/public/avo-packs/manifest.json.br +0 -0
  60. data/public/avo-packs/manifest.json.gz +0 -0
  61. data/public/avo-packs/media/font/fontello-068ca2b3.ttf +0 -0
  62. data/public/avo-packs/media/font/fontello-068ca2b3.ttf.br +0 -0
  63. data/public/avo-packs/media/font/fontello-068ca2b3.ttf.gz +0 -0
  64. data/public/avo-packs/media/font/fontello-8d4a4e6f.woff2 +0 -0
  65. data/public/avo-packs/media/font/fontello-9354499c.svg +72 -0
  66. data/public/avo-packs/media/font/fontello-9354499c.svg.br +0 -0
  67. data/public/avo-packs/media/font/fontello-9354499c.svg.gz +0 -0
  68. data/public/avo-packs/media/font/fontello-a782baa8.woff +0 -0
  69. data/public/avo-packs/media/font/fontello-e73a0647.eot +0 -0
  70. data/public/avo-packs/media/font/fontello-e73a0647.eot.br +0 -0
  71. data/public/avo-packs/media/font/fontello-e73a0647.eot.gz +0 -0
  72. data/public/avo-packs/media/svgs/arrow-circle-right-1ad1e15ec9a7aa54b67d126566a5aa2d.svg +1 -0
  73. data/public/avo-packs/media/svgs/arrow-circle-right-1ad1e15ec9a7aa54b67d126566a5aa2d.svg.br +0 -0
  74. data/public/avo-packs/media/svgs/arrow-circle-right-1ad1e15ec9a7aa54b67d126566a5aa2d.svg.gz +0 -0
  75. data/public/avo-packs/media/svgs/exclamation-8d1c0baa390a8df9bb52176011eb5892.svg +1 -0
  76. data/public/avo-packs/media/svgs/exclamation-8d1c0baa390a8df9bb52176011eb5892.svg.br +0 -0
  77. data/public/avo-packs/media/svgs/exclamation-8d1c0baa390a8df9bb52176011eb5892.svg.gz +0 -0
  78. metadata +90 -22
  79. data/public/avo-packs/css/application-5dc4dd78.css +0 -3
  80. data/public/avo-packs/css/application-5dc4dd78.css.br +0 -0
  81. data/public/avo-packs/css/application-5dc4dd78.css.gz +0 -0
  82. data/public/avo-packs/js/application-9a0dde96ad9918852965.js +0 -3
  83. data/public/avo-packs/js/application-9a0dde96ad9918852965.js.br +0 -0
  84. data/public/avo-packs/js/application-9a0dde96ad9918852965.js.gz +0 -0
  85. data/public/avo-packs/js/application-9a0dde96ad9918852965.js.map +0 -1
  86. data/public/avo-packs/js/application-9a0dde96ad9918852965.js.map.br +0 -0
  87. data/public/avo-packs/js/application-9a0dde96ad9918852965.js.map.gz +0 -0
@@ -27,6 +27,7 @@ module Avo
27
27
  fields[:relation_class] = target_resource.class.to_s
28
28
  fields[:path] = target_resource.url
29
29
  fields[:relationship] = :has_and_belongs_to_many
30
+ fields[:relationship_model] = target_resource.model.name
30
31
 
31
32
  fields
32
33
  end
@@ -27,6 +27,7 @@ module Avo
27
27
  fields[:relation_class] = target_resource.class.to_s
28
28
  fields[:path] = target_resource.url
29
29
  fields[:relationship] = :has_many
30
+ fields[:relationship_model] = target_resource.model.name
30
31
 
31
32
  fields
32
33
  end
@@ -13,7 +13,7 @@ module Avo
13
13
 
14
14
  hide_on :create
15
15
 
16
- @placeholder = 'Choose an option'
16
+ @placeholder = I18n.t 'avo.choose_an_option'
17
17
 
18
18
  @relation_method = name.to_s.parameterize.underscore
19
19
  end
@@ -43,7 +43,7 @@ module Avo
43
43
  end
44
44
  end
45
45
 
46
- fields[:resource_name_plural] = target_resource.resource_name_plural
46
+ fields[:plural_name] = target_resource.plural_name
47
47
 
48
48
  fields
49
49
  end
@@ -1,14 +1,14 @@
1
1
  module Avo
2
2
  module Fields
3
3
  class IdField < Field
4
- DEFAULT_VALUE = 'id'
5
-
6
4
  def initialize(name, **args, &block)
5
+ default_value = 'id'
6
+
7
7
  if name.nil?
8
- @name = name = DEFAULT_VALUE
8
+ @name = name = default_value
9
9
  elsif !name.is_a? String and !name.is_a? Symbol
10
10
  args_copy = name
11
- @name = name = DEFAULT_VALUE
11
+ @name = name = default_value
12
12
  args = args_copy
13
13
  end
14
14
 
@@ -0,0 +1,27 @@
1
+ require_relative 'field'
2
+
3
+ module Avo
4
+ module Fields
5
+ class MarkdownField < Field
6
+ def initialize(name, **args, &block)
7
+ @defaults = {
8
+ component: 'markdown-field',
9
+ }
10
+
11
+ super(name, **args, &block)
12
+
13
+ hide_on :index
14
+
15
+ @always_show = args[:always_show].present? ? args[:always_show] : false
16
+ @height = args[:height].present? ? args[:height].to_s : 'auto'
17
+ end
18
+
19
+ def hydrate_field(fields, model, resource, view)
20
+ {
21
+ always_show: @always_show,
22
+ height: @height
23
+ }
24
+ end
25
+ end
26
+ end
27
+ end
@@ -8,9 +8,11 @@ module Avo
8
8
  component: 'password-field',
9
9
  }
10
10
 
11
+ show_on :forms
12
+
11
13
  super(name, **args, &block)
12
14
 
13
- only_on :forms
15
+ hide_on [:index, :show]
14
16
  end
15
17
  end
16
18
  end
@@ -12,7 +12,7 @@ module Avo
12
12
  @options = args[:options].present? ? args[:options] : {}
13
13
  @enum = args[:enum].present? ? args[:enum] : nil
14
14
  @display_value = args[:display_value].present? ? args[:display_value] : false
15
- @placeholder = args[:placeholder].present? ? args[:placeholder].to_s : 'Choose an option'
15
+ @placeholder = args[:placeholder].present? ? args[:placeholder].to_s : I18n.t('avo.choose_an_option')
16
16
  end
17
17
 
18
18
  def hydrate_field(fields, model, resource, view)
@@ -0,0 +1,4 @@
1
+ module Avo
2
+ class CommunityLicense < License
3
+ end
4
+ end
@@ -0,0 +1,86 @@
1
+ module Avo
2
+ class HQ
3
+ attr_accessor :current_request
4
+
5
+ ENDPOINT = 'https://avohq.io/api/v1/licenses/check'
6
+ CACHE_KEY = 'avo.hq.response'
7
+ REQUEST_TIMEOUT = 5 # seconds
8
+
9
+ def initialize(current_request)
10
+ @current_request = current_request
11
+ @cache_store = Avo::App.cache_store
12
+ end
13
+
14
+ def response
15
+ @hq_response or request
16
+ end
17
+
18
+ private
19
+ def request
20
+ return cached_response if has_cached_response
21
+
22
+ begin
23
+ perform_and_cache_request
24
+ rescue HTTParty::Error => exception
25
+ cache_and_return_error 'HTTP client error.', exception.message
26
+ rescue Net::OpenTimeout => exception
27
+ cache_and_return_error 'Request timeout.', exception.message
28
+ rescue SocketError => exception
29
+ cache_and_return_error 'Connection error.', exception.message
30
+ end
31
+ end
32
+
33
+ def perform_and_cache_request
34
+ hq_response = perform_request
35
+
36
+ return cache_and_return_error 'Avo HQ Internal server error.', hq_response.body if hq_response.code == 500
37
+
38
+ cache_response 1.hour.to_i, hq_response.parsed_response if hq_response.code == 200
39
+ end
40
+
41
+ def cache_response(time, response)
42
+ response.merge!(
43
+ expiry: time,
44
+ **payload,
45
+ ).stringify_keys!
46
+
47
+ @cache_store.write(CACHE_KEY, response, expires_in: time)
48
+
49
+ @hq_response = response
50
+
51
+ response
52
+ end
53
+
54
+ def perform_request
55
+ puts 'Performing request to avohq.io API to check license availability.'.inspect if Rails.env.development?
56
+
57
+ HTTParty.post ENDPOINT, body: payload.to_json, headers: { 'Content-type': 'application/json' }, timeout: REQUEST_TIMEOUT
58
+ end
59
+
60
+ def payload
61
+ {
62
+ license: Avo.configuration.license,
63
+ license_key: Avo.configuration.license_key,
64
+ avo_version: Avo::VERSION,
65
+ rails_version: Rails::VERSION::STRING,
66
+ ruby_version: RUBY_VERSION,
67
+ environment: Rails.env,
68
+ ip: current_request.ip,
69
+ host: current_request.host,
70
+ port: current_request.port,
71
+ }
72
+ end
73
+
74
+ def cache_and_return_error(error, exception_message = '')
75
+ cache_response 5.minutes.to_i, { error: error, exception_message: exception_message }.stringify_keys
76
+ end
77
+
78
+ def has_cached_response
79
+ @cache_store.exist? CACHE_KEY
80
+ end
81
+
82
+ def cached_response
83
+ @cache_store.read CACHE_KEY
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,48 @@
1
+ module Avo
2
+ class License
3
+ attr_accessor :id
4
+ attr_accessor :response
5
+ attr_accessor :valid
6
+
7
+ def initialize(response)
8
+ @response = response
9
+ @id = response['id']
10
+ @valid = response['valid']
11
+ end
12
+
13
+ def valid?
14
+ valid
15
+ end
16
+
17
+ def invalid?
18
+ !valid?
19
+ end
20
+
21
+ def pro?
22
+ id == 'pro'
23
+ end
24
+
25
+ def error
26
+ @response['error']
27
+ end
28
+
29
+ def properties
30
+ @response.slice 'valid', 'id', 'error'
31
+ end
32
+
33
+ def abilities
34
+ []
35
+ end
36
+
37
+ def can(ability)
38
+ abilities.include? ability
39
+ end
40
+
41
+ def cant(ability)
42
+ !can ability
43
+ end
44
+
45
+ alias_method :has, :can
46
+ alias_method :lacks, :cant
47
+ end
48
+ end
@@ -0,0 +1,25 @@
1
+ require_relative 'license'
2
+ require_relative 'community_license'
3
+ require_relative 'pro_license'
4
+ require_relative 'null_license'
5
+
6
+ module Avo
7
+ class LicenseManager
8
+ def initialize(hq_response)
9
+ @hq_response = hq_response
10
+ end
11
+
12
+ def license
13
+ return NullLicense.new if Rails.env.test? and ENV['RUN_WITH_NULL_LICENSE'] == '1'
14
+
15
+ case @hq_response['id']
16
+ when 'community'
17
+ CommunityLicense.new @hq_response
18
+ when 'pro'
19
+ ProLicense.new @hq_response
20
+ else
21
+ NullLicense.new @hq_response
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,12 @@
1
+ module Avo
2
+ class NullLicense < License
3
+ def initialize(response = nil)
4
+ response ||= {
5
+ id: 'community',
6
+ valid: true,
7
+ }
8
+
9
+ super(response)
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,9 @@
1
+ module Avo
2
+ class ProLicense < License
3
+ def abilities
4
+ [
5
+ :authorization,
6
+ ]
7
+ end
8
+ end
9
+ end
@@ -12,14 +12,17 @@ module Avo
12
12
  attr_reader :default_view_type
13
13
 
14
14
  class << self
15
- def hydrate_resource(model, resource, view = :index)
16
- default_panel_name = "#{resource.name} details"
15
+ def hydrate_resource(model:, resource:, view: :index, user:)
16
+ default_panel_name = I18n.t 'avo.resource_details', name: resource.name
17
17
 
18
18
  resource_with_fields = {
19
19
  id: model.id,
20
- resource_name_singular: resource.resource_name_singular,
21
- resource_name_plural: resource.resource_name_plural,
20
+ authorization: get_authorization(user, model),
21
+ singular_name: resource.singular_name,
22
+ plural_name: resource.plural_name,
22
23
  title: model[resource.title],
24
+ translation_key: resource.translation_key,
25
+ path: resource.url,
23
26
  fields: [],
24
27
  grid_fields: {},
25
28
  panels: [{
@@ -39,6 +42,8 @@ module Avo
39
42
 
40
43
  furnished_field = field.fetch_for_resource(model, resource, view)
41
44
 
45
+ next unless field_resource_authorized field, furnished_field, user
46
+
42
47
  next if furnished_field.blank?
43
48
 
44
49
  furnished_field[:panel_name] = default_panel_name
@@ -74,19 +79,41 @@ module Avo
74
79
 
75
80
  "/resources/#{url}"
76
81
  end
82
+
83
+ def get_authorization(user, model)
84
+ [:create, :edit, :update, :show, :destroy].map do |action|
85
+ [action, AuthorizationService::authorize_action(user, model, action)]
86
+ end.to_h
87
+ end
88
+
89
+ def field_resource_authorized(field, furnished_field, user)
90
+ if [Avo::Fields::HasManyField, Avo::Fields::HasAndBelongsToManyField].include? field.class
91
+ return true if furnished_field[:relationship_model].nil?
92
+
93
+ AuthorizationService.authorize user, furnished_field[:relationship_model].safe_constantize, Avo.configuration.authorization_methods.stringify_keys['index']
94
+ else
95
+ true
96
+ end
97
+ end
77
98
  end
78
99
 
79
100
  def name
80
101
  return @name if @name.present?
81
102
 
103
+ return I18n.t(@translation_key, count: 1).capitalize if @translation_key
104
+
82
105
  self.class.name.demodulize.titlecase
83
106
  end
84
107
 
85
- def resource_name_singular
108
+ def singular_name
109
+ return I18n.t(@translation_key, count: 1).capitalize if @translation_key
110
+
86
111
  name
87
112
  end
88
113
 
89
- def resource_name_plural
114
+ def plural_name
115
+ return I18n.t(@translation_key, count: 2).capitalize if @translation_key
116
+
90
117
  name.pluralize
91
118
  end
92
119
 
@@ -108,6 +135,10 @@ module Avo
108
135
  'id'
109
136
  end
110
137
 
138
+ def translation_key
139
+ @translation_key
140
+ end
141
+
111
142
  def model
112
143
  return @model if @model.present?
113
144
 
@@ -138,26 +169,26 @@ module Avo
138
169
  @search
139
170
  end
140
171
 
141
- def query_search(query: '', via_resource_name: , via_resource_id:)
142
- db_query = self.model
172
+ def query_search(query: '', via_resource_name: , via_resource_id:, user:)
173
+ model_class = self.model
174
+
175
+ db_query = AuthorizationService.with_policy(user, model_class)
143
176
 
144
177
  if via_resource_name.present?
145
- related_resource = App.get_resource_by_name(via_resource_name)
146
- related_model = related_resource.model
147
- db_query = related_model.find(via_resource_id).public_send(self.resource_name_plural.downcase)
178
+ related_model = App.get_resource_by_name(via_resource_name).model
179
+
180
+ db_query = related_model.find(via_resource_id).public_send(self.plural_name.downcase)
148
181
  end
149
182
 
183
+ new_query = []
184
+
150
185
  [self.search].flatten.each_with_index do |search_by, index|
151
- query_string = "text(#{search_by}) ILIKE '%#{query}%'"
186
+ new_query.push 'or' if index != 0
152
187
 
153
- if index == 0
154
- db_query = db_query.where query_string
155
- else
156
- db_query = db_query.or(self.model.where query_string)
157
- end
188
+ new_query.push "text(#{search_by}) ILIKE '%#{query}%'"
158
189
  end
159
190
 
160
- db_query.select("#{:id}, #{title} as \"name\"")
191
+ db_query.where(new_query.join(' '))
161
192
  end
162
193
 
163
194
  def model
@@ -0,0 +1,40 @@
1
+ module Avo
2
+ class AuthorizationService
3
+ class << self
4
+ def authorize(user, record, action)
5
+ return true if skip_authorization
6
+
7
+ begin
8
+ if Pundit.policy user, record
9
+ Pundit.authorize user, record, action
10
+ end
11
+ true
12
+ rescue Pundit::NotAuthorizedError => error
13
+ false
14
+ end
15
+ end
16
+
17
+ def authorize_action(user, record, action)
18
+ action = Avo.configuration.authorization_methods.stringify_keys[action.to_s]
19
+
20
+ return true if action.nil?
21
+
22
+ authorize user, record, action
23
+ end
24
+
25
+ def with_policy(user, model)
26
+ return model if skip_authorization
27
+
28
+ begin
29
+ Pundit.policy_scope! user, model
30
+ rescue => exception
31
+ model
32
+ end
33
+ end
34
+
35
+ def skip_authorization
36
+ Avo::App.license.lacks :authorization
37
+ end
38
+ end
39
+ end
40
+ end