avo 0.2.4 → 0.4.2

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 +25 -1
  4. data/README.md +5 -3
  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 +4 -1
  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 +58 -20
  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-d405b262.css +3 -0
  49. data/public/avo-packs/css/application-d405b262.css.br +0 -0
  50. data/public/avo-packs/css/application-d405b262.css.gz +0 -0
  51. data/public/avo-packs/js/application-d7382d9da26e7470d7b2.js +3 -0
  52. data/public/avo-packs/js/{application-9a0dde96ad9918852965.js.LICENSE.txt → application-d7382d9da26e7470d7b2.js.LICENSE.txt} +0 -0
  53. data/public/avo-packs/js/application-d7382d9da26e7470d7b2.js.br +0 -0
  54. data/public/avo-packs/js/application-d7382d9da26e7470d7b2.js.gz +0 -0
  55. data/public/avo-packs/js/application-d7382d9da26e7470d7b2.js.map +1 -0
  56. data/public/avo-packs/js/application-d7382d9da26e7470d7b2.js.map.br +0 -0
  57. data/public/avo-packs/js/application-d7382d9da26e7470d7b2.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 +88 -14
  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,18 +12,28 @@ 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
+ case view
17
+ when :show
18
+ panel_name = I18n.t 'avo.resource_details', name: resource.name.downcase.upcase_first
19
+ when :edit
20
+ panel_name = I18n.t('avo.edit_item', item: resource.name.downcase).upcase_first
21
+ when :create
22
+ panel_name = I18n.t('avo.create_new_item', item: resource.name.downcase).upcase_first
23
+ end
17
24
 
18
25
  resource_with_fields = {
19
26
  id: model.id,
20
- resource_name_singular: resource.resource_name_singular,
21
- resource_name_plural: resource.resource_name_plural,
27
+ authorization: get_authorization(user, model),
28
+ singular_name: resource.singular_name,
29
+ plural_name: resource.plural_name,
22
30
  title: model[resource.title],
31
+ translation_key: resource.translation_key,
32
+ path: resource.url,
23
33
  fields: [],
24
34
  grid_fields: {},
25
35
  panels: [{
26
- name: default_panel_name,
36
+ name: panel_name,
27
37
  component: 'panel',
28
38
  }]
29
39
  }
@@ -39,9 +49,11 @@ module Avo
39
49
 
40
50
  furnished_field = field.fetch_for_resource(model, resource, view)
41
51
 
52
+ next unless field_resource_authorized field, furnished_field, user
53
+
42
54
  next if furnished_field.blank?
43
55
 
44
- furnished_field[:panel_name] = default_panel_name
56
+ furnished_field[:panel_name] = panel_name
45
57
  furnished_field[:show_on_show] = field.show_on_show
46
58
 
47
59
  if field.has_own_panel?
@@ -74,19 +86,41 @@ module Avo
74
86
 
75
87
  "/resources/#{url}"
76
88
  end
89
+
90
+ def get_authorization(user, model)
91
+ [:create, :edit, :update, :show, :destroy].map do |action|
92
+ [action, AuthorizationService::authorize_action(user, model, action)]
93
+ end.to_h
94
+ end
95
+
96
+ def field_resource_authorized(field, furnished_field, user)
97
+ if [Avo::Fields::HasManyField, Avo::Fields::HasAndBelongsToManyField].include? field.class
98
+ return true if furnished_field[:relationship_model].nil?
99
+
100
+ AuthorizationService.authorize user, furnished_field[:relationship_model].safe_constantize, Avo.configuration.authorization_methods.stringify_keys['index']
101
+ else
102
+ true
103
+ end
104
+ end
77
105
  end
78
106
 
79
107
  def name
80
108
  return @name if @name.present?
81
109
 
110
+ return I18n.t(@translation_key, count: 1).capitalize if @translation_key
111
+
82
112
  self.class.name.demodulize.titlecase
83
113
  end
84
114
 
85
- def resource_name_singular
115
+ def singular_name
116
+ return I18n.t(@translation_key, count: 1).capitalize if @translation_key
117
+
86
118
  name
87
119
  end
88
120
 
89
- def resource_name_plural
121
+ def plural_name
122
+ return I18n.t(@translation_key, count: 2).capitalize if @translation_key
123
+
90
124
  name.pluralize
91
125
  end
92
126
 
@@ -108,6 +142,10 @@ module Avo
108
142
  'id'
109
143
  end
110
144
 
145
+ def translation_key
146
+ @translation_key
147
+ end
148
+
111
149
  def model
112
150
  return @model if @model.present?
113
151
 
@@ -138,26 +176,26 @@ module Avo
138
176
  @search
139
177
  end
140
178
 
141
- def query_search(query: '', via_resource_name: , via_resource_id:)
142
- db_query = self.model
179
+ def query_search(query: '', via_resource_name: , via_resource_id:, user:)
180
+ model_class = self.model
181
+
182
+ db_query = AuthorizationService.with_policy(user, model_class)
143
183
 
144
184
  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)
185
+ related_model = App.get_resource_by_name(via_resource_name).model
186
+
187
+ db_query = related_model.find(via_resource_id).public_send(self.plural_name.downcase)
148
188
  end
149
189
 
190
+ new_query = []
191
+
150
192
  [self.search].flatten.each_with_index do |search_by, index|
151
- query_string = "text(#{search_by}) ILIKE '%#{query}%'"
193
+ new_query.push 'or' if index != 0
152
194
 
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
195
+ new_query.push "text(#{search_by}) ILIKE '%#{query}%'"
158
196
  end
159
197
 
160
- db_query.select("#{:id}, #{title} as \"name\"")
198
+ db_query.where(new_query.join(' '))
161
199
  end
162
200
 
163
201
  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