abyme 0.2.1 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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