abyme 0.2.1 → 0.5.0

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.
data/Rakefile CHANGED
@@ -1,6 +1,51 @@
1
- require "bundler/gem_tasks"
1
+ # require "bundler/gem_tasks"
2
2
  require "rspec/core/rake_task"
3
+ # require 'rails/dummy/tasks'
3
4
 
4
5
  RSpec::Core::RakeTask.new(:spec)
5
-
6
6
  task :default => :spec
7
+
8
+ # APP_RAKEFILE = File.expand_path("spec/dummy/Rakefile", __dir__)
9
+ # load 'rails/tasks/engine.rake'
10
+ # load 'rails/tasks/statistics.rake'
11
+
12
+ # Bundler::GemHelper.install_tasks
13
+
14
+ # begin
15
+ # require 'bundler/setup'
16
+ # rescue LoadError
17
+ # puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
18
+ # end
19
+
20
+ # require 'rdoc/task'
21
+
22
+ # RDoc::Task.new(:rdoc) do |rdoc|
23
+ # rdoc.rdoc_dir = 'rdoc'
24
+ # rdoc.title = 'Abyme'
25
+ # rdoc.options << '--line-numbers'
26
+ # rdoc.rdoc_files.include('README.md')
27
+ # rdoc.rdoc_files.include('lib/**/*.rb')
28
+ # end
29
+
30
+ begin
31
+ require 'bundler/setup'
32
+ rescue LoadError
33
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
34
+ end
35
+
36
+ require 'rdoc/task'
37
+
38
+ RDoc::Task.new(:rdoc) do |rdoc|
39
+ rdoc.rdoc_dir = 'rdoc'
40
+ rdoc.title = 'SampleEngineWithRspecAndCucumber'
41
+ rdoc.options << '--line-numbers'
42
+ rdoc.rdoc_files.include('README.rdoc')
43
+ rdoc.rdoc_files.include('lib/**/*.rb')
44
+ end
45
+
46
+ APP_RAKEFILE = File.expand_path("spec/dummy/Rakefile", __dir__)
47
+
48
+ load 'rails/tasks/engine.rake'
49
+ load 'rails/tasks/statistics.rake'
50
+
51
+ Bundler::GemHelper.install_tasks
data/abyme.gemspec CHANGED
@@ -20,7 +20,7 @@ Gem::Specification.new do |spec|
20
20
  # Specify which files should be added to the gem when it is released.
21
21
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
22
22
  spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
23
- `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features|node_modules)/}) }
23
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
24
24
  end
25
25
  spec.bindir = "exe"
26
26
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
@@ -28,5 +28,21 @@ Gem::Specification.new do |spec|
28
28
 
29
29
  spec.add_development_dependency "bundler", "~> 2.0"
30
30
  spec.add_development_dependency "rake", "~> 13.0"
31
- spec.add_development_dependency "rspec", "~> 3.0"
31
+ # Tests
32
+ spec.add_development_dependency "rspec-rails"
33
+ spec.add_development_dependency 'rails-controller-testing'
34
+ spec.add_development_dependency 'database_cleaner-active_record'
35
+ spec.add_development_dependency 'capybara'
36
+ spec.add_development_dependency 'webdrivers'
37
+ spec.add_development_dependency "generator_spec"
38
+
39
+ # Dummy app
40
+ spec.add_development_dependency "sqlite3"
41
+ spec.add_development_dependency 'rails'
42
+ spec.add_development_dependency 'pry-rails'
43
+ spec.add_development_dependency 'web-console'
44
+
45
+ spec.add_development_dependency 'puma'
46
+ spec.add_development_dependency 'simplecov'
47
+ spec.add_development_dependency 'simplecov-lcov'
32
48
  end
data/bin/rails ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+ # This command will automatically be run when you run "rails" with Rails 4 gems installed from the root of your application.
3
+
4
+ ENGINE_ROOT = File.expand_path('..', __dir__)
5
+ ENGINE_PATH = File.expand_path('../lib/abyme/engine', __dir__)
6
+
7
+ # Set up gems listed in the Gemfile.
8
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
9
+ require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
10
+
11
+ require 'rails/all'
12
+ require 'rails/engine/commands'
@@ -1,27 +1,62 @@
1
1
  import { Controller } from 'stimulus';
2
2
 
3
3
  export default class extends Controller {
4
- static targets = ['template', 'associations', 'fields', 'newFields'];
4
+ // static targets = ['template', 'associations', 'fields', 'newFields'];
5
+ // Some applications don't compile correctly with the usual static syntax.
6
+ // Thus implementing targets with standard getters below
7
+
8
+ static get targets() {
9
+ return ['template', 'associations', 'fields', 'newFields'];
10
+ }
5
11
 
6
12
  connect() {
13
+ console.log("Abyme Connected")
14
+
7
15
  if (this.count) {
8
- this.addDefaultAssociations();
16
+ // If data-count is present,
17
+ // add n default fields on page load
18
+
19
+ this.add_default_associations();
9
20
  }
10
21
  }
11
22
 
23
+ // return the value of the data-count attribute
24
+
12
25
  get count() {
13
26
  return this.element.dataset.minCount || 0;
14
27
  }
15
28
 
29
+ // return the value of the data-position attribute
30
+ // if there is no position specified set end as default
31
+
16
32
  get position() {
17
33
  return this.associationsTarget.dataset.abymePosition === 'end' ? 'beforeend' : 'afterbegin';
18
34
  }
19
35
 
36
+ // ADD_ASSOCIATION
37
+
38
+ // this function is call whenever a click occurs
39
+ // on the element with the click->abyme#add_association
40
+ // <button> element by default
41
+
42
+ // if a data-count is present the add_association
43
+ // will be call without an event so we have to check
44
+ // this case
45
+
46
+ // check for limit reached
47
+ // dispatch an event if the limit is reached
48
+
49
+ // - call the function build_html that take care
50
+ // for building the correct html to be inserted in the DOM
51
+ // - dispatch an event before insert
52
+ // - insert html into the dom
53
+ // - dispatch an event after insert
54
+
20
55
  add_association(event) {
21
56
  if (event) {
22
57
  event.preventDefault();
23
58
  }
24
- // check for limit reached
59
+
25
60
  if (this.element.dataset.limit && this.limit_check()) {
26
61
  this.create_event('limit-reached')
27
62
  return false
@@ -33,8 +68,21 @@ export default class extends Controller {
33
68
  this.create_event('after-add');
34
69
  }
35
70
 
71
+ // REMOVE_ASSOCIATION
72
+
73
+ // this function is call whenever a click occurs
74
+ // on the element with the click->abyme#remove_association
75
+ // <button> element by default
76
+
77
+ // - call the function mark_for_destroy that takes care
78
+ // of marking the element for destruction and hiding it
79
+ // - dispatch an event before mark & hide
80
+ // - mark for descrution + hide the element
81
+ // - dispatch an event after mark and hide
82
+
36
83
  remove_association(event) {
37
84
  event.preventDefault();
85
+
38
86
  this.create_event('before-remove');
39
87
  this.mark_for_destroy(event);
40
88
  this.create_event('after-remove');
@@ -42,6 +90,12 @@ export default class extends Controller {
42
90
 
43
91
  // LIFECYCLE EVENTS RELATED
44
92
 
93
+ // CREATE_EVENT
94
+
95
+ // take a stage (String) => before-add, after-add...
96
+ // create a new custom event
97
+ // and dispatch at at the controller level
98
+
45
99
  create_event(stage, html = null) {
46
100
  const event = new CustomEvent(`abyme:${stage}`, { detail: {controller: this, content: html} });
47
101
  this.element.dispatchEvent(event);
@@ -69,9 +123,14 @@ export default class extends Controller {
69
123
  abymeAfterRemove(event) {
70
124
  }
71
125
 
72
- // UTILITIES
126
+ // BUILD HTML
127
+
128
+ // takes the html template and substitutes the sub-string
129
+ // NEW_RECORD for a generated timestamp
130
+ // then if there is a sub template in the html (multiple nested level)
131
+ // set all the sub timestamps back as NEW_RECORD
132
+ // finally returns the html
73
133
 
74
- // build html
75
134
  build_html() {
76
135
  let html = this.templateTarget.innerHTML.replace(
77
136
  /NEW_RECORD/g,
@@ -88,8 +147,15 @@ export default class extends Controller {
88
147
 
89
148
  return html;
90
149
  }
91
-
92
- // mark association for destroy
150
+
151
+ // MARK_FOR_DESTROY
152
+
153
+ // mark association for destruction
154
+ // get the closest abyme--fields from the remove_association button
155
+ // set the _destroy input value as 1
156
+ // hide the element
157
+ // add the class of abyme--marked-for-destroy to the element
158
+
93
159
  mark_for_destroy(event) {
94
160
  let item = event.target.closest('.abyme--fields');
95
161
  item.querySelector("input[name*='_destroy']").value = 1;
@@ -97,20 +163,29 @@ export default class extends Controller {
97
163
  item.classList.add('abyme--marked-for-destroy')
98
164
  }
99
165
 
100
- // check if associations limit is reached
166
+
167
+ // LIMIT_CHECK
168
+
169
+ // Check if associations limit is reached
170
+ // based on newFieldsTargets only
171
+ // persisted fields are ignored
172
+
101
173
  limit_check() {
102
174
  return (this.newFieldsTargets
103
175
  .filter(item => !item.classList.contains('abyme--marked-for-destroy'))).length
104
176
  >= parseInt(this.element.dataset.limit)
105
177
  }
106
178
 
107
- // Add default blank associations at page load
108
- async addDefaultAssociations() {
179
+ // ADD_DEFAULT_ASSOCIATION
180
+
181
+ // Add n default blank associations at page load
182
+ // call sleep function to ensure uniqueness of timestamp
183
+
184
+ async add_default_associations() {
109
185
  let i = 0
110
186
  while (i < this.count) {
111
187
  this.add_association()
112
188
  i++
113
- // Sleep function to ensure uniqueness of timestamp
114
189
  await this.sleep(1);
115
190
  }
116
191
  }
data/lib/abyme.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  require "abyme/version"
2
2
  require 'abyme/view_helpers'
3
3
  require 'abyme/engine'
4
+ require 'abyme/controller'
5
+ require 'abyme/action_view_extensions/builder'
4
6
 
5
7
  module Abyme
6
8
  class Error < StandardError; end
@@ -2,31 +2,42 @@ module Abyme
2
2
  class AbymeBuilder < ActionView::Base
3
3
  include ActionView
4
4
 
5
- def initialize(association:, form:, lookup_context:, partial:, &block)
5
+ # If a block is given to the #abymize helper
6
+ # it will instanciate a new AbymeBuilder
7
+ # and pass to it the association name (Symbol)
8
+ # the form object, lookup_context optionaly a partial path
9
+ # then yield itself to the block
10
+
11
+ def initialize(association:, form:, context:, partial:, &block)
6
12
  @association = association
7
13
  @form = form
8
- @lookup_context = lookup_context
14
+ @context = context
15
+ @lookup_context = context.lookup_context
9
16
  @partial = partial
10
17
  yield(self) if block_given?
11
18
  end
19
+
20
+ # RECORDS
21
+
22
+ # calls the #persisted_records_for helper method
23
+ # passing association, form and options to it
12
24
 
13
25
  def records(options = {})
14
26
  persisted_records_for(@association, @form, options) do |fields_for_association|
15
- render_association_partial(fields_for_association, options)
27
+ render_association_partial(@association, fields_for_association, @partial, @context)
16
28
  end
17
29
  end
30
+
31
+ # NEW_RECORDS
32
+
33
+ # calls the #new_records_for helper method
34
+ # passing association, form and options to it
18
35
 
19
36
  def new_records(options = {}, &block)
20
37
  new_records_for(@association, @form, options) do |fields_for_association|
21
- render_association_partial(fields_for_association, options)
38
+ render_association_partial(@association, fields_for_association, @partial, @context)
22
39
  end
23
40
  end
24
41
 
25
- private
26
-
27
- def render_association_partial(fields, options)
28
- partial = @partial || options[:partial] || "abyme/#{@association.to_s.singularize}_fields"
29
- ActionController::Base.render(partial: partial, locals: { f: fields })
30
- end
31
42
  end
32
43
  end
@@ -0,0 +1,15 @@
1
+ module Abyme
2
+ module ActionViewExtensions
3
+ module Builder
4
+ def abyme_for(association, options = {}, &block)
5
+ @template.abyme_for(association, self, options, &block)
6
+ end
7
+ end
8
+ end
9
+ end
10
+
11
+ module ActionView::Helpers
12
+ class FormBuilder
13
+ include Abyme::ActionViewExtensions::Builder
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ module Abyme
2
+ module Controller
3
+ def abyme_attributes
4
+ return [] if resource_class.nil?
5
+
6
+ resource_class.abyme_attributes
7
+ end
8
+
9
+ private
10
+
11
+ def resource_class
12
+ self.class.name.match(/(.*)(Controller)/)[1].singularize.safe_constantize
13
+ end
14
+ end
15
+ end
data/lib/abyme/engine.rb CHANGED
@@ -4,9 +4,11 @@ module Abyme
4
4
 
5
5
  config.after_initialize do
6
6
  ActiveSupport.on_load :action_view do
7
- # ActionView::Base.send :include, Abyme::ViewHelpers
8
7
  include Abyme::ViewHelpers
9
8
  end
9
+ ActiveSupport.on_load :action_controller do
10
+ include Abyme::Controller
11
+ end
10
12
  end
11
13
  end
12
14
  end
data/lib/abyme/model.rb CHANGED
@@ -1,11 +1,85 @@
1
1
  module Abyme
2
2
  module Model
3
- extend ActiveSupport::Concern
4
-
5
- class_methods do
6
- def abyme_for(association, options = {})
3
+ module ClassMethods
4
+ def abymize(association, permit: nil, reject: nil, **options)
7
5
  default_options = {reject_if: :all_blank, allow_destroy: true}
8
- accepts_nested_attributes_for association, default_options.merge(options)
6
+ nested_attributes_options = default_options.merge(options)
7
+ accepts_nested_attributes_for association, nested_attributes_options
8
+ # Save allow_destroy value for this model/association for later
9
+ save_destroy_option(association, nested_attributes_options[:allow_destroy])
10
+ Abyme::Model.permit_attributes(self.name, association, permit || reject, permit.present?) if permit.present? || reject.present?
11
+ end
12
+
13
+ def abyme_attributes
14
+ Abyme::Model.instance_variable_get(:@permitted_attributes)[self.name]
15
+ end
16
+
17
+ private
18
+
19
+ def save_destroy_option(association, value)
20
+ Abyme::Model.instance_variable_get(:@allow_destroy)[self.name][association] = value
21
+ end
22
+ end
23
+
24
+ @permitted_attributes ||= {}
25
+ @allow_destroy ||= {}
26
+
27
+ attr_accessor :allow_destroy
28
+ attr_reader :permitted_attributes
29
+
30
+ def self.permit_attributes(class_name, association, attributes, permit)
31
+ @permitted_attributes[class_name]["#{association}_attributes".to_sym] = AttributesBuilder.new(class_name, association, attributes, permit)
32
+ .build_attributes
33
+ end
34
+
35
+ def self.included(klass)
36
+ @permitted_attributes[klass.name] ||= {}
37
+ @allow_destroy[klass.name] ||= {}
38
+ klass.extend ClassMethods
39
+ end
40
+
41
+ class AttributesBuilder
42
+ def initialize(model, association, attributes, permit = true)
43
+ @model = model
44
+ @association = association
45
+ @attributes_list = attributes
46
+ @permit = permit
47
+ @association_class = @association.to_s.classify.constantize
48
+ end
49
+
50
+ def build_attributes
51
+ nested_attributes = @association_class.abyme_attributes if @association_class.respond_to? :abyme_attributes
52
+ authorized_attributes = build_default_attributes
53
+ if @permit && @attributes_list == :all_attributes
54
+ authorized_attributes = build_all_attributes(authorized_attributes, nested_attributes)
55
+ elsif @permit
56
+ @attributes_list << nested_attributes unless (nested_attributes.blank? || @attributes_list.include?(nested_attributes))
57
+ authorized_attributes += @attributes_list
58
+ else
59
+ authorized_attributes = build_all_attributes(authorized_attributes, nested_attributes)
60
+ authorized_attributes -= @attributes_list
61
+ end
62
+ authorized_attributes
63
+ end
64
+
65
+ def destroy_allowed?
66
+ Abyme::Model.instance_variable_get(:@allow_destroy).dig(@model, @association)
67
+ end
68
+
69
+ def add_all_attributes
70
+ @association_class.column_names.map(&:to_sym).reject { |attr| [:id, :created_at, :updated_at].include?(attr) }
71
+ end
72
+
73
+ def build_all_attributes(authorized_attributes, nested_attributes)
74
+ authorized_attributes += add_all_attributes
75
+ authorized_attributes << nested_attributes unless (nested_attributes.blank? || authorized_attributes.include?(nested_attributes))
76
+ authorized_attributes
77
+ end
78
+
79
+ def build_default_attributes
80
+ attributes = [:id]
81
+ attributes << :_destroy if destroy_allowed?
82
+ attributes
9
83
  end
10
84
  end
11
85
  end