fragmentary 0.2.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9495373cf7b72c44963e4bedd39960bf08d81d90
4
- data.tar.gz: 85c2c4605899af9dd9a9907f395c3eb70685fdc5
3
+ metadata.gz: ce71afcd11d78930295c8839ee9519b2bb504f40
4
+ data.tar.gz: 354eee986e23c365e958807f3024ebb571e6b7e8
5
5
  SHA512:
6
- metadata.gz: bec32e4de3fbc9427d359c93872f4edf964d1d43dc96d160f38067d15fe2288e84034eaedef54f06d5986c80672dc2d645c819c4149806b36d45a4f1b0966480
7
- data.tar.gz: cb92de732842410aa5a1126214ff3feb9ccf19a377cbf4f9ef4fb5abc6c2b886efa660796cb0b1d99f0244a3c2d697783a582287fb038009f43b6e19277847f9
6
+ metadata.gz: cecfed68ceed46c1d174c5f967769ae4c6bc9608f09675e3d693958c931be25e8adaf58c497755197549e83959756a14e67e4cf50fd52cd58ac9b1e5cfe31c6e
7
+ data.tar.gz: fb4fca48abf783d5fd7faa24147b4c55164444341c21bdf936107562cf112fef55a183f34855bf42076bf8bc3a747ece32f1d61d818bd3e507b7411610163d3c
@@ -1,3 +1,10 @@
1
+ ### 0.2.1
2
+ - Fixes a syntax bug in validating a fragment's root_id
3
+ - Adds README documentation on the use of methods for explicitly retreving user-dependent fragments.
4
+ - Adds README documentation on the needs_user_id declaration
5
+ - Deprecates the :template parameter in FragmentsHelper#fragment_builder
6
+ - Adds minor performance enhancements
7
+
1
8
  ### 0.2.0
2
9
  - Makes the following configurable:
3
10
  - current_user_method - called on current template to obtain the current authenticated user.
data/README.md CHANGED
@@ -110,7 +110,7 @@ class Product < ActiveRecord::Base
110
110
  end
111
111
  ```
112
112
 
113
- Create a Fragment subclass (e.g. in app/models/fragments.rb or elsewhere) for each distinct fragment type that your views contain. If a particular fragment type needs to be unique by the id of some application data record, specify the class of the application model as shown in the example below.
113
+ Create a `Fragment` subclass (e.g. in app/models/fragments.rb or elsewhere) for each distinct fragment type that your views contain. If a particular fragment type needs to be unique by the id of some application data record, specify the class of the application model as shown in the example below.
114
114
  ```
115
115
  class ProductTemplate < Fragment
116
116
  needs_record_id :type => 'Product'
@@ -122,7 +122,7 @@ Here `needs_record_id` indicates that there is a separate `ProductTemplate` frag
122
122
 
123
123
  We've used this, for example, for fragment types representing different kinds of list items that have certain generic characteristics but are used in different contexts to represent different kinds of content.
124
124
 
125
- Within the body of the fragment class definition, for each application model whose records the content of the fragment depends upon, use the `subscribe_to` method with a block containing method definitions to handle create, update and destroy events on your application data, typically to touch the fragment records affected by the application data change. The names of these methods follow the form used in the wisper-activerecord gem, i.e. `create_<model_name>_successful`, `update_<model_name>_successful` and `destroy_<model_name>_successful`, each taking a single argument representing the application data record that has changed.
125
+ Within the body of the fragment subclass definition, for each application model whose records the content of the fragment depends upon, use the `subscribe_to` method with a block containing method definitions to handle create, update and destroy events on your application data, typically to touch the fragment records affected by the application data change. The names of these methods follow the form used in the wisper-activerecord gem, i.e. `create_<model_name>_successful`, `update_<model_name>_successful` and `destroy_<model_name>_successful`, each taking a single argument representing the application data record that has changed.
126
126
 
127
127
  Within the body of each method you define within the `subscribe_to` block, you can retrieve and touch the fragment records affected by the change in application data. The method `touch_fragments_for_record` can be used for convenience. That method takes an individual application data record or record_id or an array of either. So, for example, if product listings include the names of all the categories the products belong to, with those categories being represented by a separate ActiveRecord model, and the wording of a category name changes, you could handle that as follows.
128
128
  ```
@@ -140,7 +140,7 @@ The effect of this will be to expire the product template fragment for every pro
140
140
 
141
141
  When implementing the method definitions within the `subscribe_to` block, note that the block will be executed against an instance of a separate `Fragmentary::Subscriber` class that acts on behalf of the particular `Fragment` subclass we are defining (so the methods we define within the block are actually defined on that subscriber object). However, _all other_ methods called on the `Subscriber` object, including those called from within the methods we define in the block, are delegated by `method_missing` to the fragment subclass. So in the example, `touch_fragments_for_record` called from within `update_product_category_successful` represents `ProductTemplate.touch_fragments_for_record`. This method is defined by the `Fragment` class, so is available to all fragment subclasses.
142
142
 
143
- Note also that for fragment subclasses that declare `needs_record_id`, there is no need define a `destroy_<model_name>_successful` method simply to remove a fragment whose `record_id` matches the `id` of a <model_name> application record that is destroyed, e.g. to destroy a `ProductTemplate` whose `record_id` matches the `id` of a destroyed `Product` object. Fragmentary handles this clean-up automatically. This is not to say, however, that 'destroy' handlers are never needed at all. The destruction of an application data record will often require other fragments to be touched.
143
+ Note also that for fragment subclasses that declare `needs_record_id`, there is no need to define a `destroy_<model_name>_successful` method simply to remove a fragment whose `record_id` matches the `id` of a <model_name> application record that is destroyed, e.g. to destroy a `ProductTemplate` whose `record_id` matches the `id` of a destroyed `Product` object. Fragmentary handles this clean-up automatically. This is not to say, however, that 'destroy' handlers are never needed at all. The destruction of an application data record will often require other fragments to be touched.
144
144
 
145
145
  ### View Setup
146
146
 
@@ -157,7 +157,7 @@ A 'root' fragment is one that has no parent. In the template in which a root fra
157
157
 
158
158
  #### Nested Fragments
159
159
 
160
- The variable `fragment` that is yielded to the block above is an object of class `Fragmentary::FragmentsHelper::CacheBuilder`, which contains both the actual ActiveRecord fragment record found or created by `cache_fragment` *and* the current template as instance variables. Apart from its constructor and accessors, the class has one instance method, `cache_child` which can be used to define a child fragment nested within the first. The method is used _within_ a block of content defined by `cache_fragment` and much like `cache_fragment` it takes a hash of options that uniquely identify the child fragment. Also like `cache_fragment` it yields another `CacheBuilder` object and wraps a block containing the content of the child fragment to be cached.
160
+ The variable `fragment` that is yielded to the block above is an object of class `Fragmentary::FragmentsHelper::CacheBuilder`, which contains both the actual ActiveRecord fragment record found or created by `cache_fragment` *and* the current template as instance variables. The class has one public instance method defined, `cache_child`, which can be used to define a child fragment nested within the first. The method is used _within_ a block of content defined by `cache_fragment` and much like `cache_fragment` it takes a hash of options that uniquely identify the child fragment. Also like `cache_fragment` it yields another `CacheBuilder` object and wraps a block containing the content of the child fragment to be cached.
161
161
  ```
162
162
  <% cache_fragment :type => 'ProductTemplate', :record_id => @product.id do |fragment| %>
163
163
  <% fragment.cache_child :type => 'StoresAvailable' do |child_fragment| %>
@@ -310,9 +310,16 @@ Whether you specify the user type mapping in Fragmentary's configuration or in a
310
310
 
311
311
 
312
312
  #### Per-User Customization
313
- While storing different versions of fragments is a workable solution for different groups of users, sometimes content needs to be customized for individual users. Doing this at any realistic scale would likely introduce significant cost and performance challenges. To address this we provide a way for user-specific content (in general any content) to be inserted into a cached fragment _after_ it has been retrieved from the cache through the use of a special placeholder string in a view template.
313
+ While storing different versions of fragments is a workable solution for different groups of users, sometimes content needs to be customized for individual users. It is _possible_ to accomplish this by defining a fragment subclass with the declaration `needs_user_id`:
314
+ ```
315
+ class MyFragment < Fragment
316
+ needs_user_id
317
+ ...
318
+ end
319
+ ```
320
+ You will also need to include a `user_id` column in the migration used to create your fragments database table. The `needs_user_id` declaration will result in separate fragments being created for each individual user. As in the case of `needs_user_type`, the `user_id` parameter is automatically extracted from your configured (or default) `current_user_method`.
314
321
 
315
- To accomplish this, two classes are defined, `Fragmentary::Widget` and a subclass `Fragmentary::UserWidget`. Instances of these classes represent chunks of content that will be inserted into a fragment after it has been either rendered and stored or retrieved from the cache. For each type of insertable content you wish your application to support, define a specific subclass of one of these classes (`UserWidget` is preferred if the content is to be user-specific) with two instance methods:
322
+ However this approach is _not_ generally recommended as doing so at any realistic scale is likely to introduce significant cost and performance challenges. Instead, we provide a way for user-specific content (in general any content) to be inserted into a cached fragment _after_ it has been retrieved from the cache through the use of a special placeholder string in a view template. To accomplish this, two classes are defined, `Fragmentary::Widget` and a subclass `Fragmentary::UserWidget`. Instances of these classes represent chunks of content that will be inserted into a fragment after it has been either rendered and stored or retrieved from the cache. For each type of insertable content you wish your application to support, define a specific subclass of one of these classes (`UserWidget` is preferred if the content is to be user-specific) with two instance methods:
316
323
  - `pattern`, which returns a `Regexp` that will be used to detect a placeholder you will use in your template at the point where you wish the inserted content to be placed. The placeholder consists of a string matching the regular expression you specify wrapped in special delimiter characters `%{...}`.
317
324
  - `content`, which returns the string that will be inserted in place of the placeholder.
318
325
 
@@ -412,7 +419,15 @@ Of course if you have to render the content in order to determine whether it nee
412
419
  <% end %>
413
420
  ```
414
421
 
415
- Note the use above of the method `Fragment.root` to retrieve the fragment after caching has occurred. The method takes the same parameters as `cache_fragment` and returns a matching fragment (calling the method after caching guarantees that it will exist since caching instantiates the fragment if it doesn't already exist). We have to retrieve the fragment explicitly since the variable `fragment` is local to the block to which it is yielded. Also note that although `fragment` is actually a `CacheBuilder` object, that class uses `method_missing` to pass any methods, such as `update_attribute`, to the underlying fragment.
422
+ Note that we update the fragment's `memo` attribute by calling `update_attribute` on the `fragment` object. Although this is actually a `CacheBuilder` object, that class passes any methods such as `update_attribute` to the underlying fragment.
423
+
424
+ Also note the use above of the method `Fragment.root` to retrieve the fragment matching the supplied parameters after caching has occurred. We have to retrieve the fragment explicitly since the variable `fragment` is local to the `cache_fragment` block to which it is yielded. The method takes the same parameters as `cache_fragment` with one exception: At present, if your fragment class declares `needs_user_type` or `needs_user_id`, you need to explicitly pass either the respective parameter or the current user object (this may change in a future release). e.g.
425
+
426
+ ```
427
+ ...
428
+ <% root_fragment = Fragment.root(:type => 'ProductTemplate', :record_id => @product.id, :user => current_user).memo == required_memo_value %>
429
+ ...
430
+ ```
416
431
 
417
432
  The process for child fragments is similar, except that an instance method Fragment#child can be called on the parent fragment in order to retrieve the fragment to be tested. e.g.
418
433
 
@@ -430,7 +445,7 @@ The process for child fragments is similar, except that an instance method Fragm
430
445
  <% end %>
431
446
  ```
432
447
 
433
- In both of these examples, the fragment to be tested is retrieved after caching. If relying on side-effects doesn't make you too queasy, you can alternatively retrieve it before caching, in which case the fragment can be passed to `cache_fragment` or `cache_child` as appropriate instead of the set of fragment attributes we used before. This avoids the need for `cache_fragment` or `cache_child` to retrieve the fragment internally. e.g.
448
+ In both of these examples, the fragment to be tested is retrieved after caching. If relying on side-effects doesn't make you too queasy, you can alternatively retrieve it before caching, in which case the retrieved fragment can be passed to `cache_fragment` or `cache_child` as appropriate instead of the set of fragment attributes we used before. This avoids the need for `cache_fragment` or `cache_child` to retrieve the fragment themselves internally, e.g.
434
449
  ```
435
450
  <% root_fragment = Fragment.root(:type => 'ProductTemplate', :record_id => @product.id) %>
436
451
  <% conditional_content = capture do %>
@@ -456,7 +471,7 @@ Note that both `Fragment.root` and `Fragment#child` will instantiate a matching
456
471
  <% end %>
457
472
 
458
473
  <% if child.memo == required_memo_value %>
459
- <%= conditional_content %>
474
+ <%= conditional_content %>
460
475
  <% end %>
461
476
 
462
477
  <% else %>
@@ -471,7 +486,7 @@ Note that both `Fragment.root` and `Fragment#child` will instantiate a matching
471
486
 
472
487
  When application data that a cached fragment depends upon changes, i.e. as a result of a POST, PATCH or DELETE request, the `subscribe_to` declarations in your `Fragment` subclass definitions ensure that the `updated_at` attribute of any existing fragment records affected will be updated. Then on subsequent browser requests, the cached content itself will be refreshed.
473
488
 
474
- As part of this process, new fragment records may be created as *children* of an existing fragment if the existing fragment contains newly added list items. A new *root* fragment, on the other hand, will be created for any root fragment class defined with `needs_record_id` if an application data record of the associated `record_type` is created, as soon as a browser request is made to the new page (or new content in the case of an AJAX request) containing that fragment.
489
+ As part of this process, new fragment records may be created as *children* of an existing fragment if the existing fragment contains newly added list items. A new *root* fragment, on the other hand, will be created for any root fragment class defined with `needs_record_id` if an application data record of the associated `record_type` is created, as soon as a browser request is made for the new content (often a new page) containing that fragment.
475
490
 
476
491
  Sometimes, however, it is desirable to avoid having to wait for an explicit request from a user in order to update the cache or to create new cached content. To deal with this, Fragmentary provides a mechanism to automatically create or refresh cached content preemptively, essentially as soon as a change in application data occurs. Rails' `ActionDispatch::Integration::Session` [class](https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/testing/integration.rb) provides an interface that allows requests to be sent directly to the application programmatically, without generating any external network traffic. Fragmentary uses this interface to automatically send requests needed to update the cache whenever changes in application data occur.
477
492
 
@@ -487,7 +502,7 @@ end
487
502
  ```
488
503
  Since nested child fragments automatically touch their parent fragments when they themselves are updated, internal requests can be initiated by an update to application data that affects a fragment anywhere within a page. Only the root fragment needs to define the request path.
489
504
 
490
- The request that is generated will be sent to the application at the `request_path` specified, but it may also include additional request parameters and options. To send HTTP request parameters with the request, the Fragment subclass should define an additional instance method `request_parameters` that returns a hash of named parameters. To send an XMLHttpRequest, a class should define an instance method `request_options` that returns the hash `{:xhr => true}`.
505
+ The request that is generated will be sent to the application at the `request_path` specified, but it may also include additional request parameters and options. To send HTTP request parameters with the request, the `Fragment` subclass should define an additional instance method `request_parameters` that returns a hash of named parameters. To send an XMLHttpRequest, a class should define an instance method `request_options` that returns the hash `{:xhr => true}`.
491
506
 
492
507
  In the case of a root fragment that *does not* yet exist, i.e. for a `Fragment` subclass defined with `needs_record_id` and an associated `record_type` when a new application record of that type is first created, a request will be generated in order to create the new fragment automatically if the subclass has a *class* method `request_path` defined that takes the `id` of the newly created application record and returns a string representing the path to which the request should be sent. For example:
493
508
  ```
@@ -515,7 +530,7 @@ The set of user types that an individual `Fragment` subclass is expected to supp
515
530
 
516
531
  Configuration of the `user_types` method is discussed in the next section. Fragmentary uses the types returned by this method to identify the request queues that each internal application request needs to be added to when instances of each particular `Fragment` subclass are updated. `Fragment` subclasses inherit both class and instance methods `request_queues` that return a hash of queues keyed by each of the specific user type strings that that specific subclass supports.
517
532
 
518
- The request queue for any given user type is shared across all Fragment subclasses within the application. So for example, a `ProductTemplate` fragment may have two different request queues, `ProductTemplate.request_queues["admin"]` and `ProductTemplate.request_queues["signed_in"]`. If a `StoreTemplate` fragment has one request queue `StoreTemplate.request_queues["signed_in"]`, the `"signed_in"` queues for both classes represent the same object.
533
+ The request queue for any given user type is shared across all `Fragment` subclasses within the application. So for example, a `ProductTemplate` fragment may have two different request queues, `ProductTemplate.request_queues["admin"]` and `ProductTemplate.request_queues["signed_in"]`. If a `StoreTemplate` fragment has one request queue `StoreTemplate.request_queues["signed_in"]`, the `"signed_in"` queues for both classes represent the same object.
519
534
 
520
535
  #### Configuring Internal Request Users
521
536
 
@@ -591,7 +606,7 @@ MyFragment.queue_request(request)
591
606
 
592
607
  Off-loading internal application requests to an asynchronous process as noted in the previous section means they can occur without delaying the server's response to the user's initial external request. However, in a complex application there may also be some overhead just in the process of updating fragment records that is not critical in order to send a response back to the user. For example, some application data changes may require a large number of fragments to be updated on pages other than the one the user is going to be sent or redirected to right away. In this case, we may wish to offload the task of updating these fragments to an asynchronous process as well.
593
608
 
594
- Fragmentary accomplishes this by means of the `Fragmentary::Handler` class. An individual task you wish to be handled asynchronously is created by defining a subclass of `Fragmentary::Handler`, typically within the scope of the Fragment subclass in which you wish to use it. The `Handler` subclass you define requires a single instance method `call`, which defines the task to be performed.
609
+ Fragmentary accomplishes this by means of the `Fragmentary::Handler` class. An individual task you wish to be handled asynchronously is created by defining a subclass of `Fragmentary::Handler`, typically within the scope of the `Fragment` subclass in which you wish to use it. The `Handler` subclass you define requires a single instance method `call`, which defines the task to be performed.
595
610
 
596
611
  To use the handler, typically within a fragment's `subscribe_to` declaration, call the inherited class method `create`, passing it a hash of named arguments. Those arguments can then be accessed within your `call` method as the instance variable `@args`. Consider our earlier example in which each product template contains a list of stores in which the product is available. Suppose an administrator fixes a typographical error in the name of a store. That correction needs to propagate to every `AvailableStore` fragment for every product that is available in that store. We can do this using a Handler as follows:
597
612
  ```
@@ -601,7 +616,7 @@ class AvailableStore < Fragment
601
616
  class UpdateStoreHandler < Fragmentary::Handler
602
617
  def call
603
618
  product_stores = ProductStore.where(:store_id => @args[:id])
604
- touch_fragment_for_record(product_stores)
619
+ touch_fragments_for_record(product_stores)
605
620
  end
606
621
  end
607
622
 
@@ -649,7 +664,7 @@ end
649
664
 
650
665
  ### Updating Fragments Explicitly Within a Controller
651
666
 
652
- The typical usage scenario for Fragmentary is for fragment records to be updated by the methods defined in the `subscribe_to` blocks you create in your fragment subclasses, in response to user requests that modify the application data. This involves very little coupling between your application's controllers and models and the fragment caching process. Models need only include the `Fragmentary::Publisher` module to ensure that `Fragment` subclasses can subscribe to the application data events that trigger fragment updates. Your `ApplicationController` only needs to ensure that any queued internal requests and asynchronous fragment update tasks are dispatched (if you choose to use either of those features) before sending a response to the user's browser. Application models and controllers generally have no need to access the Fragment class API.
667
+ The typical usage scenario for Fragmentary is for fragment records to be updated by the methods defined in the `subscribe_to` blocks you create in your fragment subclasses, in response to user requests that modify the application data. This involves very little coupling between your application's controllers and models and the fragment caching process. Models need only include the `Fragmentary::Publisher` module to ensure that `Fragment` subclasses can subscribe to the application data events that trigger fragment updates. Your `ApplicationController` only needs to ensure that any queued internal requests and asynchronous fragment update tasks are dispatched (if you choose to use either of those features) before sending a response to the user's browser. Application models and controllers generally have no need to access the `Fragment` class API.
653
668
 
654
669
  Ocassionally, however, a situation may arise in which it is useful to touch fragments directly from a controller. For example, an application data event may occur that requires fragments to be updated on a large number of different pages within your site. This is the kind of scenario in which you might choose to offload fragment updating to a `Fragmentary::Handler` and execute the task asynchronously as described in the previous section. However, it may also be that *one* of those pages is the one that is going to be sent back to the user's browser immediately in response to the current request. You can't put off touching the affected fragment on that one page until the asynchronous process executes because then the user won't see the updated content. In a scenario like this, you also can't detect within the `subscribe_to` block in your fragment subclass definition which one fragment needs to be touched immediately rather than asynchronously, since the fragment class has no knowledge of the internal state of the controller.
655
670
 
@@ -710,9 +725,9 @@ However, it's possible for the partial to contain a cached fragment that happens
710
725
  </ul>
711
726
  <% end %>
712
727
  ```
713
- In the Javascript case, however, since the template only renders the new list item and not the list as a whole, there is no `cache_fragment` method invoked in order to yield the `CacheBuilder` object, and so we have to construct it explicitly. To do this, Fragmentary provides a helper method `fragment_builder` that takes an options hash containing the parameters that define the fragment (the same ones passed to `cache_fragment`) plus the current template and returns the `CacheBuilder` object that the partial needs.
728
+ In the Javascript case, however, since the template only renders the new list item and not the list as a whole, there is no `cache_fragment` method invoked in order to yield the `CacheBuilder` object, and so we have to construct it explicitly. To do this, Fragmentary provides a helper method `fragment_builder` that takes an options hash containing the parameters that define the fragment (the same ones passed to `cache_fragment`) and returns the `CacheBuilder` object that the partial needs.
714
729
  ```
715
- <% parent_fragment = fragment_builder(:type => 'ProductList', :template => self) %>
730
+ <% parent_fragment = fragment_builder(:type => 'ProductList') %>
716
731
  $('ul.product_list').append('<%= j(render 'product/summary', :product => @product,
717
732
  :parent_fragment => parent_fragment) %>')
718
733
  ```
@@ -723,7 +738,7 @@ The second challenge in dealing with child fragments rendered without the contex
723
738
 
724
739
  To address this, `cache_child` can take an additional boolean option, `:insert_widgets` that can be used to force the insertion of widgets into the child. Typically a local variable containing the value of this option would be passed to the partial in which `cache_child` appears. In the Javascript template:
725
740
  ```
726
- <% parent_fragment = fragment_builder(:type => 'ProductList', :template => self) %>
741
+ <% parent_fragment = fragment_builder(:type => 'ProductList') %>
727
742
  $('ul.product_list').append('<%= j(render 'product/summary', :product => @product,
728
743
  :parent_fragment => parent_fragment,
729
744
  :insert_widgets => true) %>')
@@ -28,9 +28,9 @@ module Fragmentary
28
28
 
29
29
  attr_accessor :indexed_children
30
30
 
31
- validate :root_id, :presence => true
31
+ validates :root_id, :presence => true
32
32
 
33
- cache_timestamp_format = :usec # Probably not needed for Rails 5, which uses :usec by default.
33
+ self.cache_timestamp_format = :usec # Probably not needed for Rails 5, which uses :usec by default.
34
34
 
35
35
  end
36
36
 
@@ -49,9 +49,13 @@ module Fragmentary
49
49
  module ClassMethods
50
50
 
51
51
  def root(options)
52
- klass, search_attributes, options = base_class.attributes(options)
53
- fragment = klass.where(search_attributes).includes(:children).first_or_initialize(options); fragment.save if fragment.new_record?
54
- fragment.set_indexed_children if fragment.child_search_key
52
+ if fragment = options[:fragment]
53
+ raise ArgumentError, "You passed Fragment #{fragment.id} to Fragment.root, but it's a child of Fragment #{fragment.parent_id}" if fragment.parent_id
54
+ else
55
+ klass, search_attributes, options = base_class.attributes(options)
56
+ fragment = klass.where(search_attributes).includes(:children).first_or_initialize(options); fragment.save if fragment.new_record?
57
+ fragment.set_indexed_children if fragment.child_search_key
58
+ end
55
59
  fragment
56
60
  end
57
61
 
@@ -90,16 +94,21 @@ module Fragmentary
90
94
  @@cache_store ||= Rails.application.config.action_controller.cache_store
91
95
  end
92
96
 
97
+ # ToDo: combine this with Fragment.root
93
98
  def existing(options)
94
- options.merge!(:type => name) unless self == base_class
95
- raise ArgumentError, "A 'type' attribute is needed in order to retrieve a fragment" unless options[:type]
96
- klass, search_attributes, options = base_class.attributes(options)
97
- # We merge options because it may include :record_id, which may be needed for uniqueness even
98
- # for classes that don't 'need_record_id' if the parent_id isn't available.
99
- fragment = klass.where(search_attributes.merge(options)).includes(:children).first
100
- # Unlike Fragment.root and Fragment#child we don't instantiate a record if none is found,
101
- # so fragment may be nil.
102
- fragment.try :set_indexed_children if fragment.try :child_search_key
99
+ if fragment = options[:fragment]
100
+ raise ArgumentError, "You passed Fragment #{fragment.id} to Fragment.existing, but it's a child of Fragment #{fragment.parent_id}" if fragment.parent_id
101
+ else
102
+ options.merge!(:type => name) unless self == base_class
103
+ raise ArgumentError, "A 'type' attribute is needed in order to retrieve a fragment" unless options[:type]
104
+ klass, search_attributes, options = base_class.attributes(options)
105
+ # We merge options because it may include :record_id, which may be needed for uniqueness even
106
+ # for classes that don't 'need_record_id' if the parent_id isn't available.
107
+ fragment = klass.where(search_attributes.merge(options)).includes(:children).first
108
+ # Unlike Fragment.root and Fragment#child we don't instantiate a record if none is found,
109
+ # so fragment may be nil.
110
+ fragment.try :set_indexed_children if fragment.try :child_search_key
111
+ end
103
112
  fragment
104
113
  end
105
114
 
@@ -262,7 +271,7 @@ module Fragmentary
262
271
  # hasn't yet been requested, e.g. an assumption created on an article page won't necessarily have been rendered on the
263
272
  # opinion analysis page.
264
273
  def touch_fragments_for_record(record_id)
265
- fragments_for_record(record_id).each(&:touch)
274
+ fragments_for_record(record_id).includes({:parent => :parent}).each(&:touch)
266
275
  end
267
276
 
268
277
  def fragments_for_record(record_id)
@@ -389,7 +398,10 @@ module Fragmentary
389
398
  # rendered on its own, e.g. inserted by ajax into a parent that is already on the page. In this case the
390
399
  # children won't have already been loaded or indexed.
391
400
  def child(options)
392
- begin
401
+ if child = options[:child]
402
+ raise ArgumentError, "You passed a child fragment to a parent it's not a child of." unless child.parent_id == self.id
403
+ child
404
+ else
393
405
  existing = options.delete(:existing)
394
406
  # root_id and parent_id are passed from parent to child. For all except root fragments, root_id is stored explicitly.
395
407
  derived_options = {:root_id => root_id || id}
@@ -419,10 +431,7 @@ module Fragmentary
419
431
  fragment_children = fragment.children
420
432
  fragment.set_indexed_children if fragment.child_search_key
421
433
  end
422
-
423
434
  fragment
424
- rescue => e
425
- Rails.logger.error e.message + "\n " + e.backtrace.join("\n ")
426
435
  end
427
436
  end
428
437
 
@@ -466,10 +475,13 @@ module Fragmentary
466
475
  touch(:no_request => no_request) unless children.any?
467
476
  end
468
477
 
478
+ # Touch this fragment and all descendants that have entries in the cache. Destroy any that
479
+ # don't have cache entries.
469
480
  def touch_or_destroy
470
481
  puts " touch_or_destroy #{self.class.name} #{id}"
471
482
  if cache_exist?
472
483
  children.each(&:touch_or_destroy)
484
+ # if there are children, this will be touched automatically once they are.
473
485
  touch(:no_request => true) unless children.any?
474
486
  else
475
487
  destroy # will also destroy all children because of :dependent => :destroy
@@ -2,56 +2,52 @@ module Fragmentary
2
2
 
3
3
  module FragmentsHelper
4
4
 
5
- def cache_fragment(options)
6
- no_cache = options.delete(:no_cache)
7
- options.reverse_merge!(:user => Template.new(self).current_user)
8
- fragment = options.delete(:fragment) || Fragmentary::Fragment.base_class.root(options)
9
- builder = CacheBuilder.new(fragment, template = self)
10
- unless no_cache
11
- cache fragment, :skip_digest => true do
12
- yield(builder)
13
- end
14
- else
15
- yield(builder)
16
- end
17
- self.output_buffer = WidgetParser.new(self).parse_buffer
5
+ def cache_fragment(options, &block)
6
+ CacheBuilder.new(self).cache_fragment(options, &block)
18
7
  end
19
8
 
20
9
  def fragment_builder(options)
21
- template = options.delete(:template)
10
+ # the template option is deprecated but avoids breaking prior usage
11
+ template = options.delete(:template) || self
22
12
  options.reverse_merge!(:user => Template.new(template).current_user)
23
- CacheBuilder.new(Fragmentary::Fragment.base_class.existing(options), template)
13
+ CacheBuilder.new(template, Fragmentary::Fragment.base_class.existing(options))
24
14
  end
25
15
 
26
16
  class CacheBuilder
27
17
  include ::ActionView::Helpers::CacheHelper
28
18
  include ::ActionView::Helpers::TextHelper
29
19
 
30
- attr_accessor :fragment, :template
20
+ attr_reader :fragment
31
21
 
32
- def initialize(fragment, template)
22
+ def initialize(template, fragment = nil)
33
23
  @fragment = fragment
34
24
  @template = template
35
25
  end
36
26
 
37
- def cache_child(options)
27
+ def cache_fragment(options, &block)
38
28
  no_cache = options.delete(:no_cache)
39
29
  insert_widgets = options.delete(:insert_widgets)
40
- options.reverse_merge!(:user => Template.new(template).current_user)
41
- child = options.delete(:child) || fragment.child(options)
42
- builder = CacheBuilder.new(child, template)
30
+ options.reverse_merge!(:user => Template.new(@template).current_user)
31
+ # If the CacheBuilder was instantiated with an existing fragment, next_fragment is its child;
32
+ # otherwise it is the root fragment specified by the options provided.
33
+ next_fragment = @fragment.try(:child, options) || Fragmentary::Fragment.base_class.root(options)
34
+ builder = CacheBuilder.new(@template, next_fragment)
43
35
  unless no_cache
44
- template.cache child, :skip_digest => true do
36
+ @template.cache next_fragment, :skip_digest => true do
45
37
  yield(builder)
46
38
  end
47
39
  else
48
40
  yield(builder)
49
41
  end
50
- template.output_buffer = WidgetParser.new(template).parse_buffer if insert_widgets
42
+ @template.output_buffer = WidgetParser.new(@template).parse_buffer if (!@fragment || insert_widgets)
51
43
  end
52
44
 
45
+ alias cache_child cache_fragment
46
+
47
+ private
48
+
53
49
  def method_missing(method, *args)
54
- fragment.send(method, *args)
50
+ @fragment.send(method, *args)
55
51
  end
56
52
 
57
53
  end
@@ -61,7 +57,6 @@ module Fragmentary
61
57
 
62
58
  # Just a wrapper to allow us to call a configurable current_user_method on the template
63
59
  class Template
64
- attr_reader :template
65
60
 
66
61
  def initialize(template)
67
62
  @template = template
@@ -69,8 +64,8 @@ module Fragmentary
69
64
 
70
65
  def current_user
71
66
  return nil unless methd = Fragmentary.current_user_method
72
- if template.respond_to? methd
73
- template.send methd
67
+ if @template.respond_to? methd
68
+ @template.send methd
74
69
  else
75
70
  raise NoMethodError, "The current_user_method '#{methd.to_s}' specified doesn't exist"
76
71
  end
@@ -33,7 +33,7 @@ module Fragmentary
33
33
  end
34
34
 
35
35
  def after_update_broadcast
36
- broadcast(:after_update, self)
36
+ broadcast(:after_update, self) if self.previous_changes.any?
37
37
  end
38
38
 
39
39
  def after_destroy_broadcast
@@ -1,3 +1,3 @@
1
1
  module Fragmentary
2
- VERSION = "0.2.0"
2
+ VERSION = "0.2.1"
3
3
  end
@@ -1,7 +1,7 @@
1
1
  module Fragmentary
2
2
 
3
3
  class Widget
4
- attr_reader :template, :key, :match
4
+ attr_reader :template, :match
5
5
 
6
6
  def self.inherited subclass
7
7
  super if defined? super
@@ -18,7 +18,6 @@ module Fragmentary
18
18
 
19
19
  def initialize(template, key)
20
20
  @template = template
21
- @key = key
22
21
  @match = key.match(pattern)
23
22
  end
24
23
 
@@ -31,7 +31,7 @@ module Fragmentary
31
31
  widget._content
32
32
  end
33
33
  else
34
- "Oops! Widget not found."
34
+ "Oops! Widget for #{widget_key} not found."
35
35
  end
36
36
  end
37
37
  # The gsub replaces instances of '%' that aren't part of widget specifications with '%%', preventing
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fragmentary
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mark Thomson
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-01-01 00:00:00.000000000 Z
11
+ date: 2020-07-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails