adminable 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +94 -33
  3. data/app/assets/javascripts/adminable/scripts.js +8 -5
  4. data/app/assets/stylesheets/adminable/application.scss +15 -0
  5. data/app/controllers/adminable/application_controller.rb +0 -3
  6. data/app/controllers/adminable/resources_controller.rb +49 -24
  7. data/app/models/concerns/adminable/resource_concern.rb +15 -0
  8. data/app/views/adminable/resources/_form.html.haml +5 -9
  9. data/app/views/adminable/resources/_search.html.haml +2 -2
  10. data/app/views/adminable/resources/edit.html.haml +1 -1
  11. data/app/views/adminable/resources/form/_belongs_to.html.haml +13 -3
  12. data/app/views/adminable/resources/form/_boolean.html.haml +5 -2
  13. data/app/views/adminable/resources/form/_date.html.haml +3 -0
  14. data/app/views/adminable/resources/form/_datetime.html.haml +3 -2
  15. data/app/views/adminable/resources/form/_decimal.html.haml +3 -2
  16. data/app/views/adminable/resources/form/_float.html.haml +3 -2
  17. data/app/views/adminable/resources/form/_has_many.html.haml +12 -8
  18. data/app/views/adminable/resources/form/_integer.html.haml +3 -2
  19. data/app/views/adminable/resources/form/_string.html.haml +3 -2
  20. data/app/views/adminable/resources/form/_text.html.haml +4 -2
  21. data/app/views/adminable/resources/form/_time.html.haml +3 -0
  22. data/app/views/adminable/resources/form/_timestamp.html.haml +3 -0
  23. data/app/views/adminable/resources/index.html.haml +12 -14
  24. data/app/views/adminable/resources/index/_belongs_to.html.haml +4 -5
  25. data/app/views/adminable/resources/index/_date.html.haml +2 -0
  26. data/app/views/adminable/resources/index/_has_many.html.haml +1 -2
  27. data/app/views/adminable/resources/index/_text.html.haml +1 -1
  28. data/app/views/adminable/resources/index/_time.html.haml +2 -0
  29. data/app/views/adminable/resources/index/_timestamp.html.haml +2 -0
  30. data/app/views/adminable/shared/_label.html.haml +6 -3
  31. data/app/views/layouts/adminable/application.html.haml +3 -4
  32. data/config/locales/adminable.en.yml +4 -1
  33. data/config/locales/adminable.ru.yml +21 -0
  34. data/config/routes.rb +1 -1
  35. data/lib/adminable.rb +9 -4
  36. data/lib/adminable/attributes/association.rb +7 -14
  37. data/lib/adminable/attributes/base.rb +36 -18
  38. data/lib/adminable/attributes/collection.rb +75 -36
  39. data/lib/adminable/attributes/types/belongs_to.rb +1 -3
  40. data/lib/adminable/attributes/types/date.rb +8 -0
  41. data/lib/adminable/attributes/types/has_many.rb +0 -6
  42. data/lib/adminable/attributes/types/time.rb +8 -0
  43. data/lib/adminable/attributes/types/timestamp.rb +8 -0
  44. data/lib/adminable/configuration.rb +7 -2
  45. data/lib/adminable/engine.rb +5 -1
  46. data/lib/adminable/errors.rb +7 -0
  47. data/lib/adminable/presenters/base_presenter.rb +11 -0
  48. data/lib/adminable/presenters/entries_presenter.rb +55 -0
  49. data/lib/adminable/presenters/entry_presenter.rb +68 -0
  50. data/lib/adminable/resource.rb +9 -15
  51. data/lib/adminable/version.rb +1 -1
  52. data/lib/generators/adminable/install/templates/application_controller.rb +2 -0
  53. data/lib/generators/adminable/install_generator.rb +27 -0
  54. data/lib/generators/{cms → adminable}/resource/templates/resource_controller.rb.erb +0 -0
  55. data/lib/generators/{cms → adminable}/resource_generator.rb +0 -0
  56. metadata +77 -33
  57. data/lib/adminable/extensions/devise.rb +0 -24
@@ -1,2 +1,3 @@
1
- = render 'adminable/shared/label', f: f, attribute: attribute
2
- = f.number_field attribute.name, class: 'form-control'
1
+ = render 'adminable/shared/label', attribute: attribute
2
+ = number_field_tag "#{@resource.model.model_name.param_key}[#{attribute.name}]",
3
+ entry[attribute.name], class: 'form-control'
@@ -1,2 +1,3 @@
1
- = render 'adminable/shared/label', f: f, attribute: attribute
2
- = f.number_field attribute.name, class: 'form-control'
1
+ = render 'adminable/shared/label', attribute: attribute
2
+ = number_field_tag "#{@resource.model.model_name.param_key}[#{attribute.name}]",
3
+ entry[attribute.name], class: 'form-control'
@@ -1,9 +1,13 @@
1
- = render 'adminable/shared/label', f: f, attribute: attribute
1
+ = render 'adminable/shared/label', attribute: attribute
2
2
  = hidden_field_tag "#{@resource.model.model_name.param_key}[#{attribute.key}][]", nil
3
- #clusterizeScrollArea.clusterize-scroll
4
- #clusterizeContentArea.clusterize-content.list-group
5
- - attribute.options_for_select(entry).each do |value, key|
6
- .list-group-item.bg-faded.toggleCheckbox{ :class => ('active' if f.object.send(attribute.key).include?(key)) }
7
- = check_box_tag "#{@resource.model.model_name.param_key}[#{attribute.key}][]",
8
- key, f.object.send(attribute.key).include?(key), hidden: true
9
- = value
3
+ #clusterizeScrollArea.clusterize-scroll.associations
4
+ #clusterizeContentArea.clusterize-content
5
+ - attribute.association.all.each do |association_entry|
6
+ .association
7
+ %label.c-input.c-checkbox.m-a-0
8
+ = check_box_tag "#{@resource.model.model_name.param_key}[#{attribute.key}][]",
9
+ association_entry.id, entry.send(attribute.key).include?(association_entry.id)
10
+ %span.c-indicator
11
+ = link_to association_entry.to_name,
12
+ edit_polymorphic_path(association_entry),
13
+ target: '_blank'
@@ -1,2 +1,3 @@
1
- = render 'adminable/shared/label', f: f, attribute: attribute
2
- = f.number_field attribute.name, class: 'form-control'
1
+ = render 'adminable/shared/label', attribute: attribute
2
+ = number_field_tag "#{@resource.model.model_name.param_key}[#{attribute.name}]",
3
+ entry[attribute.name], class: 'form-control'
@@ -1,2 +1,3 @@
1
- = render 'adminable/shared/label', f: f, attribute: attribute
2
- = f.text_field attribute.name, class: 'form-control'
1
+ = render 'adminable/shared/label', attribute: attribute
2
+ = text_field_tag "#{@resource.model.model_name.param_key}[#{attribute.name}]",
3
+ entry[attribute.name], class: 'form-control'
@@ -1,2 +1,4 @@
1
- = render 'adminable/shared/label', f: f, attribute: attribute
2
- = f.text_area attribute.name, class: "form-control #{ 'wysiwyg' if attribute.wysiwyg? }", rows: 5
1
+ = render 'adminable/shared/label', attribute: attribute
2
+ = text_area_tag "#{@resource.model.model_name.param_key}[#{attribute.name}]",
3
+ entry[attribute.name],
4
+ class: "form-control #{ 'wysiwyg' if attribute.options[:wysiwyg] }", rows: 5
@@ -0,0 +1,3 @@
1
+ = render 'adminable/shared/label', attribute: attribute
2
+ = text_field_tag "#{@resource.model.model_name.param_key}[#{attribute.name}]",
3
+ entry[attribute.name], class: 'form-control'
@@ -0,0 +1,3 @@
1
+ = render 'adminable/shared/label', attribute: attribute
2
+ = text_field_tag "#{@resource.model.model_name.param_key}[#{attribute.name}]",
3
+ entry[attribute.name], class: 'form-control'
@@ -1,35 +1,33 @@
1
1
  .m-b-1.clearfix
2
2
  %h1.pull-md-left.m-b-2
3
- = @resource.model.model_name.human.pluralize
3
+ = @resource.model.model_name.human(count: 2)
4
4
  = link_to t('adminable.buttons.new', resource: @resource.model.model_name.human),
5
5
  new_polymorphic_path(@resource.model), class: 'btn btn-primary pull-md-right m-b-2'
6
6
  .row
7
- .col-md-9
7
+ %div{ :class => (@resource.attributes.search.any? ? 'col-md-9' : 'col-md-12') }
8
8
  .table-responsive.m-b-3
9
9
  %table.table.table-striped
10
10
  %thead
11
11
  %tr
12
- - @resource.attributes.index.each_value do |attribute|
13
- %th.text-nowrap{ :class => ('text-md-center' if attribute.center?) }
14
- = @resource.model.human_attribute_name(attribute.name)
12
+ - @resource.attributes.index.each do |attribute|
13
+ %th.text-nowrap{ :class => ('text-md-center' if attribute.options[:center]) }
14
+ = sort_link(@q, attribute.name, hide_indicator: true)
15
15
  %th
16
16
  %tbody
17
17
  - @entries.each do |entry|
18
18
  %tr
19
- - @resource.attributes.index.each_value do |attribute|
20
- %td{ :class => ('text-md-center' if attribute.center?) }
19
+ - @resource.attributes.index.each do |attribute|
20
+ %td{ :class => ('text-md-center' if attribute.options[:center]) }
21
21
  = render attribute.index_partial_path, entry: entry,
22
22
  attribute: attribute, value: entry[attribute.key]
23
23
  %td.text-nowrap.text-md-right
24
- = link_to t('adminable.buttons.edit'), edit_polymorphic_path(entry),
25
- class: 'label label-primary'
26
- = link_to t('adminable.buttons.delete'), polymorphic_path(entry),
27
- class: 'label label-danger', method: :delete,
28
- data: { confirm: t('adminable.ui.confirm') }
24
+ = entry.link_to_edit_small
25
+ = entry.link_to_delete_small
29
26
  - if @entries.empty?
30
27
  %tr
31
28
  %td.text-md-center.text-muted{ :colspan => @resource.attributes.index.size + 1 }
32
29
  No data available
33
- .col-md-3.hidden-sm-down
34
- = render 'search'
30
+ - if @resource.attributes.search.any?
31
+ .col-md-3.hidden-sm-down
32
+ = render 'search'
35
33
  = paginate @entries, views_prefix: 'adminable'
@@ -1,5 +1,4 @@
1
- - if value.present?
2
- - if attribute.association.klass.exists?(value)
3
- = link_to "##{ value }", edit_polymorphic_path(attribute.association.klass.find_by(id: value))
4
- - else
5
- = "##{ value }"
1
+ - if entry.public_send(attribute.name)
2
+ = Adminable::EntryPresenter.new(entry.public_send(attribute.name)).link_to_self
3
+ - else
4
+ = "##{value}"
@@ -0,0 +1,2 @@
1
+ - if value
2
+ = l value, format: '%-d %b %Y'
@@ -1,3 +1,2 @@
1
1
  - if entry
2
- - entry.send(attribute.name).each do |resource|
3
- = link_to "##{ resource.id }", edit_polymorphic_path(resource)
2
+ = Adminable::EntriesPresenter.new(entry.public_send(attribute.name)).to_s
@@ -1 +1 @@
1
- = strip_tags(value.truncate(100)).squish
1
+ = strip_tags(value).truncate(30).squish
@@ -0,0 +1,2 @@
1
+ - if value
2
+ = l value, format: '%H:%M:%S'
@@ -0,0 +1,2 @@
1
+ - if value
2
+ = l value, format: '%H:%M:%S, %-d %b %Y'
@@ -1,5 +1,8 @@
1
1
  = label_tag attribute.name do
2
2
  = @resource.model.human_attribute_name(attribute.name)
3
- - if attribute.required?
4
- %i.text-muted
5
- (required)
3
+ - if attribute.options[:required]
4
+ %span.text-muted.m-l-1
5
+ %i.fa.fa-exclamation-circle{ 'data-toggle' => :tooltip, 'data-placement' => :right, :title => 'Required' }
6
+ - if %i(belongs_to has_many).include?(attribute.type)
7
+ %span.text-muted.m-l-1.uncheck-associations
8
+ %i.fa.fa-remove{ 'data-toggle' => :tooltip, 'data-placement' => :right, :title => 'Uncheck all' }
@@ -14,7 +14,7 @@
14
14
  %body
15
15
  #collapsing-nav.collapse.p-y-1
16
16
  - Adminable::Configuration.resources.each do |resource|
17
- = link_to resource.model.model_name.human.pluralize,
17
+ = link_to resource.model.model_name.human(count: 2),
18
18
  polymorphic_path(resource.route), class: 'btn btn-link btn-block'
19
19
  .navbar.navbar-full.navbar-dark.bg-primary.m-b-3
20
20
  .hidden-md-up
@@ -27,14 +27,13 @@
27
27
  %i.fa.fa-home
28
28
  - Adminable::Configuration.resources.each do |resource|
29
29
  %li.nav-item{ :class => ('active' if current_page?(File.join('/adminable/', resource.name))) }
30
- = link_to resource.model.model_name.human.pluralize,
30
+ = link_to resource.model.model_name.human(count: 2),
31
31
  polymorphic_path(resource.route), class: 'nav-link'
32
32
  .container.p-b-3
33
33
  - if flash[:alert] || notice.present?
34
34
  .p-b-2
35
35
  - if flash[:alert]
36
- - flash[:alert].each do |alert|
37
- = render 'adminable/shared/alert', message: alert, type: 'danger'
36
+ = render 'adminable/shared/alert', message: alert, type: 'danger'
38
37
  - if notice.present?
39
38
  = render 'adminable/shared/alert', message: notice, type: 'success'
40
39
  = yield
@@ -4,13 +4,16 @@ en:
4
4
  ui:
5
5
  confirm: Are you sure?
6
6
  not_selected: Not selected
7
+ and_more: and %{size} more.
7
8
  buttons:
8
9
  submit: Submit
9
10
  back: Back
10
11
  edit: Edit
12
+ delete: Delete
11
13
  new: New %{resource}
14
+ search: Search
12
15
  titles:
13
- edit: Edit %{resource}
16
+ edit: "Edit %{resource} #%{id}"
14
17
  new: New %{resource}
15
18
  resources:
16
19
  created: "%{resource} has been successfully created."
@@ -0,0 +1,21 @@
1
+ ---
2
+ ru:
3
+ adminable:
4
+ ui:
5
+ confirm: Вы уверены?
6
+ not_selected: Не выбрано
7
+ and_more: и еще %{size}.
8
+ buttons:
9
+ submit: Сохранить
10
+ back: Назад
11
+ edit: Изменить
12
+ delete: Удалить
13
+ new: Новый %{resource}
14
+ search: Поиск
15
+ titles:
16
+ edit: "Редактирование %{resource} #%{id}"
17
+ new: Создание %{resource}
18
+ resources:
19
+ created: "%{resource} успешно создан."
20
+ updated: "%{resource} успешно обновлен."
21
+ deleted: "%{resource} успешно удален."
@@ -3,5 +3,5 @@ Adminable::Engine.routes.draw do
3
3
  resources r.route, except: :show, path: r.name, controller: r.name
4
4
  end
5
5
 
6
- root 'application#welcome'
6
+ root to: redirect(Adminable::Configuration.resources.first.name)
7
7
  end
@@ -1,10 +1,13 @@
1
1
  require 'adminable/engine'
2
2
  require 'adminable/configuration'
3
-
4
- require 'adminable/extensions/devise'
3
+ require 'adminable/errors'
5
4
 
6
5
  require 'adminable/resource'
7
6
 
7
+ require 'adminable/presenters/base_presenter'
8
+ require 'adminable/presenters/entry_presenter'
9
+ require 'adminable/presenters/entries_presenter'
10
+
8
11
  require 'adminable/attributes/collection'
9
12
  require 'adminable/attributes/association'
10
13
 
@@ -12,6 +15,7 @@ require 'adminable/attributes/base'
12
15
 
13
16
  require 'adminable/attributes/types/belongs_to'
14
17
  require 'adminable/attributes/types/boolean'
18
+ require 'adminable/attributes/types/date'
15
19
  require 'adminable/attributes/types/datetime'
16
20
  require 'adminable/attributes/types/decimal'
17
21
  require 'adminable/attributes/types/float'
@@ -19,15 +23,16 @@ require 'adminable/attributes/types/has_many'
19
23
  require 'adminable/attributes/types/integer'
20
24
  require 'adminable/attributes/types/string'
21
25
  require 'adminable/attributes/types/text'
26
+ require 'adminable/attributes/types/time'
27
+ require 'adminable/attributes/types/timestamp'
22
28
 
23
29
  require 'haml-rails'
24
30
  require 'sass-rails'
25
- require 'coffee-rails'
26
31
  require 'jquery-rails'
27
32
  require 'bootstrap'
28
33
  require 'rails-assets-tether'
29
- require 'babosa'
30
34
  require 'kaminari'
35
+ require 'ransack'
31
36
 
32
37
  module Adminable
33
38
  end
@@ -1,21 +1,14 @@
1
1
  module Adminable
2
2
  module Attributes
3
- module Association
4
- attr_reader :options_for_select, :association
3
+ class Association
4
+ attr_reader :reflection, :model, :all
5
5
 
6
- def options_for_select(entry)
7
- @association.klass.all.reject { |e| e == entry }.collect do |e|
8
- [association_option_name(e), e.id]
9
- end
6
+ # @param reflection [Object] ActiveRecord::Reflection::HasManyReflection
7
+ def initialize(reflection)
8
+ @reflection = reflection
9
+ @model = @reflection.klass
10
+ @all = Adminable::EntriesPresenter.new(@model)
10
11
  end
11
-
12
- private
13
-
14
- def association_option_name(entry)
15
- %i(email login name title id).each do |a|
16
- return entry.try(a) unless entry.try(a).nil?
17
- end
18
- end
19
12
  end
20
13
  end
21
14
  end
@@ -1,33 +1,38 @@
1
1
  module Adminable
2
2
  module Attributes
3
+ # Base class for attributes types
4
+ # @note Cannot be initialized
3
5
  class Base
4
- attr_accessor :name, :required, :show, :center, :wysiwyg
5
- attr_reader :key, :ransack_name
6
+ attr_reader :name, :options, :key, :strong_parameter, :association
6
7
 
8
+ # @param name [Symbol] attribute name e.g. `:id` or `:title`
9
+ # @param options [Hash] options, see {default_options}
7
10
  def initialize(name, options = {})
8
11
  raise 'Base class cannot be initialized' if self.class == Base
9
12
 
10
- @name = name
11
- @key = options[:key] || @name
12
- @ransack_name = "#{@name}_cont"
13
- @association = options[:association]
14
- @required = options[:required] || false
15
- @show = true
16
- @center = %w(integer boolean float decimal).include?(type)
17
- @wysiwyg = %w(text).include?(type)
18
- end
13
+ @name = name.to_sym
14
+ @options = default_options.merge(options)
19
15
 
20
- alias required? required
21
- alias show? show
22
- alias center? center
23
- alias wysiwyg? wysiwyg
16
+ @association = Adminable::Attributes::Association.new(
17
+ options[:association]
18
+ ) if options[:association]
19
+ end
24
20
 
25
- def type
26
- self.class.name.demodulize.underscore
21
+ def key
22
+ @key ||= name
27
23
  end
28
24
 
29
25
  def strong_parameter
30
- key
26
+ @strong_parameter ||= key
27
+ end
28
+
29
+ def ransack_name
30
+ @ransack_name ||= "#{name}_cont"
31
+ end
32
+
33
+ # @return [Symbol] class type e.g. `:text` for Adminable::Attributes::Text
34
+ def type
35
+ @type ||= self.class.name.demodulize.underscore.to_sym
31
36
  end
32
37
 
33
38
  def index_partial_path
@@ -37,6 +42,19 @@ module Adminable
37
42
  def form_partial_path
38
43
  "adminable/resources/form/#{type}"
39
44
  end
45
+
46
+ private
47
+
48
+ def default_options
49
+ {
50
+ index: %i(belongs_to has_many).exclude?(type),
51
+ search: false,
52
+ form: %i(id created_at updated_at).exclude?(name),
53
+ required: false,
54
+ center: %i(integer boolean float decimal).include?(type),
55
+ wysiwyg: false
56
+ }
57
+ end
40
58
  end
41
59
  end
42
60
  end
@@ -1,73 +1,112 @@
1
1
  module Adminable
2
2
  module Attributes
3
3
  class Collection
4
- attr_reader :model
5
- attr_writer :index, :form
4
+ include Enumerable
5
+ extend Forwardable
6
6
 
7
+ def_delegators :@all, :[], :<<, :each, :first, :last, :push, :unshift
8
+
9
+ attr_reader :model, :all
10
+
11
+ # @param model [Class] model from Rails application e.g. `User` or `Post`
7
12
  def initialize(model)
8
13
  @model = model
14
+ @all ||= columns + associations
9
15
  end
10
16
 
11
17
  def index
12
- @index ||= all.except(:created_at, :updated_at)
18
+ all.select { |attribute| attribute.options[:index] }
13
19
  end
14
20
 
15
21
  def form
16
- @form ||= all.except(:id, :created_at, :updated_at)
17
- end
18
-
19
- def ransack
20
- @ransack ||= columns.except(:id, :created_at, :updated_at)
21
- .select { |k, v| %w(string text).include?(v.type) }
22
- end
23
-
24
- def all
25
- [columns, belongs_to, has_many].inject(&:merge)
22
+ all.select { |attribute| attribute.options[:form] }
26
23
  end
27
24
 
28
- def association
29
- [belongs_to, has_many].inject(&:merge)
25
+ def search
26
+ all.select { |attribute| attribute.options[:search] }
30
27
  end
31
28
 
29
+ # Collects attributes from model columns
30
+ # @return [Array]
31
+ #
32
+ # rubocop:disable Metrics/MethodLength
32
33
  def columns
33
- @columns ||= {}.tap do |attribute|
34
+ @columns ||= [].tap do |attributes|
34
35
  @model.columns.reject { |a| a.name.match(/_id$/) }.each do |column|
35
- attribute[column.name] = "adminable/attributes/types/#{column.type}"
36
- .classify.constantize.new(
36
+ begin
37
+ attributes << resolve(column.type).new(
37
38
  column.name,
38
- required: attribute_required?(column.name)
39
+ required: required?(column.name)
39
40
  )
41
+ rescue Adminable::AttributeNotImplemented
42
+ next
43
+ end
40
44
  end
41
- end.symbolize_keys
45
+ end
42
46
  end
43
47
 
44
- def belongs_to
45
- @belongs_to ||= {}.tap do |attribute|
46
- @model.reflect_on_all_associations(:belongs_to).each do |association|
47
- attribute[association.name] = Adminable::Attributes::Types::BelongsTo.new(
48
+ # Collects attributes from model associations
49
+ # @return [Array]
50
+ def associations
51
+ @associations ||= [].tap do |attributes|
52
+ @model.reflect_on_all_associations.each do |association|
53
+ attributes << resolve(association.macro).new(
48
54
  association.name,
49
- required: attribute_required?(association.name),
55
+ required: required?(association.name),
50
56
  association: association
51
57
  )
52
58
  end
53
- end.symbolize_keys
59
+ end
54
60
  end
55
61
 
56
- def has_many
57
- @has_many ||= {}.tap do |attribute|
58
- @model.reflect_on_all_associations(:has_many).each do |association|
59
- attribute[association.name] = Adminable::Attributes::Types::HasMany.new(
60
- association.name,
61
- required: attribute_required?(association.name),
62
- association: association
63
- )
62
+ def configure
63
+ yield
64
+ end
65
+
66
+ # Changes options for given attribute
67
+ # @param name [Symbol] name of attribute e.g. `:title`
68
+ # @param options [Hash] options to update
69
+ def set(*args)
70
+ options = args.extract_options!
71
+ names = args
72
+
73
+ options.each do |key, value|
74
+ names.each do |name|
75
+ get(name).options[key] = value
64
76
  end
65
- end.symbolize_keys
77
+ end
78
+ end
79
+
80
+ # Finds attribute by name
81
+ # @param name [Symbol] name of attribute e.g. `:title`
82
+ # @return [Object] e.g. `Adminable::Attribute::Types::String` for `:title`
83
+ def get(name)
84
+ @all.find { |attribute| attribute.name == name } ||
85
+ raise(
86
+ Adminable::AttributeNotFound,
87
+ "couldn't find attribute with name `#{name}`"
88
+ )
89
+ end
90
+
91
+ # Adds new attribute to collection
92
+ # @param name [Symbol] name of attribute e.g. `:title`
93
+ # @param type [Symbol] type of attribute e.g. `:string`
94
+ def add(name, type, options = {})
95
+ @all << resolve(type).new(name, **options)
66
96
  end
67
97
 
68
98
  private
69
99
 
70
- def attribute_required?(name)
100
+ def resolve(type)
101
+ "adminable/attributes/types/#{type}".classify.constantize
102
+ rescue NameError
103
+ raise(
104
+ Adminable::AttributeNotImplemented,
105
+ "type `#{type}` is not supported yet."
106
+ )
107
+ end
108
+
109
+ def required?(name)
71
110
  @model.validators_on(name).any? do |validator|
72
111
  validator.class == ActiveRecord::Validations::PresenceValidator
73
112
  end