fragmentary 0.1.0 → 0.2.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/README.md +209 -56
- data/lib/fragmentary.rb +10 -0
- data/lib/fragmentary/config.rb +50 -0
- data/lib/fragmentary/fragment.rb +36 -37
- data/lib/fragmentary/fragments_helper.rb +22 -4
- data/lib/fragmentary/request_queue.rb +32 -40
- data/lib/fragmentary/subscriber.rb +3 -1
- data/lib/fragmentary/user_session.rb +44 -18
- data/lib/fragmentary/version.rb +1 -1
- data/lib/fragmentary/widget.rb +5 -1
- data/lib/fragmentary/widget_parser.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9495373cf7b72c44963e4bedd39960bf08d81d90
|
4
|
+
data.tar.gz: 85c2c4605899af9dd9a9907f395c3eb70685fdc5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bec32e4de3fbc9427d359c93872f4edf964d1d43dc96d160f38067d15fe2288e84034eaedef54f06d5986c80672dc2d645c819c4149806b36d45a4f1b0966480
|
7
|
+
data.tar.gz: cb92de732842410aa5a1126214ff3feb9ccf19a377cbf4f9ef4fb5abc6c2b886efa660796cb0b1d99f0244a3c2d697783a582287fb038009f43b6e19277847f9
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
### 0.2.0
|
2
|
+
- Makes the following configurable:
|
3
|
+
- current_user_method - called on current template to obtain the current authenticated user.
|
4
|
+
- user_type_mapping - returns user_type for current user, configurable globally and per fragment class
|
5
|
+
- session_users for background requests, configurable globally and per fragment class
|
6
|
+
- sign-in/sign-out paths for background requests
|
7
|
+
- For root fragments with `needs_record_id`, a declared `record_type` and a defined class method `request_path`, automatically generates internal requests to the specified path when application records of the associated type are created (extracts
|
8
|
+
behavior that was previously handled in the application).
|
9
|
+
- Makes Fragment.requestable? reflect only the existence of a class :request_path
|
10
|
+
- Modifies interface for Fragment.queue_request and Fragment.remove_queued_request
|
11
|
+
- Adds event handlers to subscribers via an anonymous module so that subclasses can access them via `super`
|
data/README.md
CHANGED
@@ -6,7 +6,7 @@ Fragmentary augments the fragment caching capabilities of Ruby on Rails to suppo
|
|
6
6
|
* post-cache insertion of user-specific content
|
7
7
|
* automatic refreshing of cached content when application data changes, without an external client request
|
8
8
|
|
9
|
-
**Note**: Fragmentary has been extracted from [Persuasive Thinking](http://persuasivethinking.com) where it is currently in active use.
|
9
|
+
**Note**: Fragmentary has been extracted from [Persuasive Thinking](http://persuasivethinking.com) where it is currently in active use. See [Integration Issues](https://github.com/MarkMT/fragmentary/blob/master/README.md#integration-issues) for details of issues that should be considered when using it elsewhere.
|
10
10
|
|
11
11
|
## Background
|
12
12
|
In simple cases, Rails' native support for fragment caching assumes that a fragment's content is a representation of a specific application data record. The content is stored in the cache with a key value derived from the `updated_at` attribute of that record. If any attributes of the record change, the cached entry automatically expires and on the next browser request for that content the fragment is re-rendered using the current data. In the view, the `cache` helper is used to specify the record used to determine the key and define the content to be rendered within the fragment, e.g.:
|
@@ -39,13 +39,13 @@ It is certainly possible to construct more complex keys from multiple records an
|
|
39
39
|
A further limitation is that the rendering and storage of cache content relies on an explicit request being received from a user's browser, meaning that at least one user experiences a response time that doesn't benefit from caching every time a relevant change in data occurs. Nested fragments can mitigate this problem for pre-existing pages, since only part of the response need be re-built, but we are still left with the challenge of how to implement nesting in the case of more complex data associations.
|
40
40
|
|
41
41
|
## Fragmentary - General Approach
|
42
|
-
Fragmentary uses a database table and corresponding ActiveRecord model that are separate from your application data specifically to represent view fragments. Records in this table serve only as metadata, recording the type of content each fragment contains, where it is located, and when it was last updated.
|
42
|
+
Fragmentary uses a database table and corresponding ActiveRecord model that are separate from your application data specifically to represent view fragments. Records in this table serve only as metadata, recording the type of content each fragment contains, where it is located, and when it was last updated. These records play the same role with respect to caching as an application data record in Rails' native approach, i.e. internally a fragment record is passed to Rails' `cache` method in the same way that `product` was in the earlier example, and the cache key is derived from the fragment record's `updated_at` attribute. A publish-subscribe mechanism is used to automatically update this timestamp whenever any application data affecting the content of a fragment is added, modified or destroyed, causing the fragment's cached content to be expired.
|
43
43
|
|
44
44
|
To support fragment nesting, each fragment record also includes a `parent_id` attribute pointing to its immediate parent, or containing, fragment. Whenever the `updated_at` attribute on an inner fragment changes (initially as a result of a change in some application data the fragment is associated with), as well as expiring the cached content for that fragment, the parent fragment is automatically touched, thus expiring the containing cache as well. This process continues up through all successive ancestors, ensuring that the whole page (or part thereof in the case of some AJAX requests) is refreshed.
|
45
45
|
|
46
46
|
Rather than use Rails' native `cache` helper directly, Fragmentary provides some new helper methods that hide some of the necessary internals involved in working with an explicit fragment model. Additional features include the ability to cache separate versions of fragments for different types of users and the ability to insert user-specific content after cached content is retrieved from the cache store. Fragmentary also supports automatic background updating of cached content within seconds of application data changing, avoiding the need for a user to visit a page in order to update the cache.
|
47
47
|
|
48
|
-
Plainly, Fragmentary is more complex to use than Rails' native approach to fragment caching, but it provides much greater flexibility. It was initially developed for an application in which this flexibility was essential in order to achieve acceptable page load times for views derived from relatively complex data models and where data can be changed from multiple page contexts and affect rendered content in multiple ways in multiple contexts.
|
48
|
+
Plainly, Fragmentary is more complex to use than Rails' native approach to fragment caching, but it provides much greater flexibility. It was initially developed for an application in which this flexibility was essential in order to achieve acceptable page load times for views derived from relatively complex data models and where data can be changed from multiple page contexts and can affect rendered content in multiple ways in multiple contexts.
|
49
49
|
|
50
50
|
## Installation
|
51
51
|
|
@@ -116,7 +116,7 @@ class ProductTemplate < Fragment
|
|
116
116
|
needs_record_id :type => 'Product'
|
117
117
|
end
|
118
118
|
```
|
119
|
-
If you need to define further subclasses of your initial subclass, you can if necessary declare `needs_record_id` on the latter without providing a type and specify the type separately on the individual subclasses using:
|
119
|
+
Here `needs_record_id` indicates that there is a separate `ProductTemplate` fragment associated with each `Product` record. If you need to define further subclasses of your initial subclass, you can if necessary declare `needs_record_id` on the latter without providing a type and specify the type separately on the individual subclasses using:
|
120
120
|
|
121
121
|
`set_record_type 'SomeModelName'`
|
122
122
|
|
@@ -124,7 +124,7 @@ We've used this, for example, for fragment types representing different kinds of
|
|
124
124
|
|
125
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.
|
126
126
|
|
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
|
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
|
```
|
129
129
|
class ProductTemplate < Fragment
|
130
130
|
needs_record_id :type => 'Product'
|
@@ -138,9 +138,9 @@ end
|
|
138
138
|
```
|
139
139
|
The effect of this will be to expire the product template fragment for every product contained within the affected category.
|
140
140
|
|
141
|
-
When implementing the method definitions within the `subscribe_to` block, note that the block will be executed against an instance of a separate `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.
|
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 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
|
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.
|
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. 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.
|
161
161
|
```
|
162
162
|
<% cache_fragment :type => 'ProductTemplate', :record_id => @product.id do |fragment| %>
|
163
163
|
<% fragment.cache_child :type => 'StoresAvailable' do |child_fragment| %>
|
@@ -168,17 +168,17 @@ The variable `fragment` that is yielded to the block above is an object of class
|
|
168
168
|
```
|
169
169
|
Within the body of the child fragment you can continue to define further nested fragments using `child_fragment.cache_child` etc, as long as appropriate fragment subclasses are defined.
|
170
170
|
|
171
|
-
Internally, the main difference between `CacheBuilder#cache_child` and `cache_fragment` is that in the former, if an existing fragment matching the options provided is not found and a new record needs to be created, the method will automatically set the `parent_id` attribute of the new child fragment to the id of its parent
|
171
|
+
Internally, the main difference between `CacheBuilder#cache_child` and `cache_fragment` is that in the former, if an existing fragment matching the options provided is not found and a new record needs to be created, the method will automatically set the `parent_id` attribute of the new child fragment to the id of its parent. This makes it possible for future changes to the child fragment's `updated_at` attribute to trigger similar updates to its parent.
|
172
172
|
|
173
|
-
Also note that if the parent fragment's class has been defined with `needs_record_id` but the child fragment's class has _not_, the parent's `record_id`
|
173
|
+
Also note that if the parent fragment's class has been defined with `needs_record_id` but the child fragment's class has _not_, `cache_child` will automatically copy the parent's `record_id` to the child, i.e. the `record_id` propagates down the nesting tree until it reaches a fragment whose class declares `needs_record_id`, at which point it must be provided explicitly in the call to `cache_child`.
|
174
174
|
|
175
175
|
#### Lists
|
176
176
|
|
177
177
|
Special consideration is needed in the case of fragments that represent lists of variable length. Specifically, we need to be able to properly handle the creation of new list items. Suppose for example, we have a list of stores in which a product is available and a new store needs to be added to the list. In terms of application data, the availability of a product in a particular store might be represented by a `ProductStore` join model. Adding a new store involves creating a new record of this class. This record effectively acts as a 'list membership' association.
|
178
178
|
|
179
|
-
There are two fragment types involved in caching the list, one for the list as a whole and another for individual items within it. We might represent the list with a fragment of type `StoresAvailable` and individual stores in the list with fragments of type `AvailableStore`, each a child of the list fragment. Both of these fragment types are associated via their respective `record_id` attributes with corresponding application data records. The `StoresAvailable` list fragment is associated with a `Product` record (the product whose availability we are displaying) and each `AvailableStore` fragment is associated with a list membership `ProductStore` record.
|
179
|
+
There are two fragment types involved in caching the list, one for the list as a whole and another for individual items within it. We might represent the list with a fragment of type `StoresAvailable` and individual stores in the list with fragments of type `AvailableStore`, each of those a child of the list fragment. Both of these fragment types are associated via their respective `record_id` attributes with corresponding application data records. The `StoresAvailable` list fragment is associated with a `Product` record (the product whose availability we are displaying) and each `AvailableStore` fragment is associated with a list membership `ProductStore` record.
|
180
180
|
|
181
|
-
We need to ensure that when a new `ProductStore` record is created, the corresponding `AvailableStore` fragment is also created _and_ that the containing `StoresAvailable` fragment is updated as well. We can't simply rely on the creation of the `AvailableStore` to automatically trigger an update to `StoresAvailable` in the way we do when we _update_ a child fragment, because the former
|
181
|
+
We need to ensure that when a new `ProductStore` record is created, the corresponding `AvailableStore` fragment is also created _and_ that the containing `StoresAvailable` fragment is updated as well. We can't simply rely on the creation of the `AvailableStore` to automatically trigger an update to `StoresAvailable` in the way we do when we _update_ a child fragment, because the former _doesn't exist_ until the latter is refreshed.
|
182
182
|
|
183
183
|
To address this situation, as a convenience Fragmentary provides a class method `acts_as_list_fragment` that is used when defining the fragment class for the list as a whole, e.g.
|
184
184
|
```
|
@@ -188,7 +188,7 @@ end
|
|
188
188
|
```
|
189
189
|
`acts_as_list_fragment` takes two named arguments:
|
190
190
|
* `members` is the name of the list membership association in snake case, or tableized, form. The creation of an association of that type triggers the addition of a new item to the list. In the example, it is the creation of a new `product_store` that results in a new item being added to the list. The effect of declaring `acts_as_list_fragment` is to ensure that when that membership association is created, the list fragment it is being added to is touched, expiring the cache so that on the next request the list will be re-rendered, which has the effect of creating the required new `AvailableStore` fragment. Note that the value of the `members` argument should match the record type of the list item fragments. So in the example, the `AvailableStore` class should be defined with `needs_record_id, :type => ProductStore` (We recognize there's some implied redundancy here that could be problematic; some adjustment may be made in the future).
|
191
|
-
* `list_record` is either the name of a method (represented by a symbol) or a `Proc` that defines how to obtain the record_id (or the record itself; either will work) associated with the list fragment from a given membership association. If the value is a method name, the list record is found by calling that method on the membership association. In the example, the membership association is a `ProductStore` record, say `product_store`. The list is represented by a `StoresAvailable` fragment whose `record_id` points to a `Product` record. We can get that record simply by calling `product_store.product`, so the `list_record` parameter passed to `acts_as_list_fragment` is just the method `:product` (`:product_id` would also work). However sometimes a simple method like this is insufficient and a `Proc` may be used instead. In this case the newly created membership association is passed as a parameter to the `Proc` and we can implement whatever functional relationship is necessary to obtain the list record. In this simple example, if we wanted (for no good reason) to use a `Proc`, it would look like `->(product_store){product_store.product}`.
|
191
|
+
* `list_record` is either the name of a method (represented by a symbol) or a `Proc` that defines how to obtain the record_id (or the record itself; either will work) associated with the list fragment from a given membership association. If the value is a method name, the list record is found by calling that method on the membership association. In the example, the membership association is a `ProductStore` record, say `product_store`. The list is represented by a `StoresAvailable` fragment whose `record_id` points to a `Product` record. We can get that `Product` record simply by calling `product_store.product`, so the `list_record` parameter passed to `acts_as_list_fragment` is just the method `:product` (`:product_id` would also work). However sometimes a simple method like this is insufficient and a `Proc` may be used instead. In this case the newly created membership association is passed as a parameter to the `Proc` and we can implement whatever functional relationship is necessary to obtain the list record. In this simple example, if we wanted (for no good reason) to use a `Proc`, it would look like `->(product_store){product_store.product}`.
|
192
192
|
|
193
193
|
Note that in the example, the specified `list_record` method, `:product`, returns a single record for a given `product_store` membership association. However this isn't necessarily always the case. The method or `Proc` may also return an array of records or record_ids.
|
194
194
|
|
@@ -229,7 +229,7 @@ class StoresAvailable < Fragment
|
|
229
229
|
end
|
230
230
|
```
|
231
231
|
|
232
|
-
In fact there can be cases where it is actually necessary to take an explicit approach like this. Using `acts_as_list_fragment` assumes that we can identify the list fragments to be touched by identifying their associated `record_id`s (this is the point of the method's `list_record` parameter). However, we have seen a situation where the set of list fragments that needed to be touched required a complex inner join between the `fragments` table and multiple application data tables,
|
232
|
+
In fact there can be cases where it is actually necessary to take an explicit approach like this. Using `acts_as_list_fragment` assumes that we can identify the list fragments to be touched by identifying their associated `record_id`s (this is the point of the method's `list_record` parameter). However, we have seen a situation where the set of list fragments that needed to be touched required a complex inner join between the `fragments` table and multiple application data tables, and this produced the list fragments to be touched directly rather than a set of associated record_ids.
|
233
233
|
|
234
234
|
### Accessing Fragments by Arbitrary Application Attributes
|
235
235
|
|
@@ -259,7 +259,7 @@ Internally, the `key` attribute is a string, but the value of the custom option
|
|
259
259
|
#### Customizing Based on User Type
|
260
260
|
In the context of a website that identifies users by some form of authentication, often the content served by the site needs to be different for different groups of users. A common example is the case where administrative users see special privileged information or capabilities on a page that are not available to regular users. Similarly, a signed-in user may see a different version of a page from a user who is not signed in.
|
261
261
|
|
262
|
-
In the context of caching, this means that separate versions of some content may need to be stored in the cache for each distinct group of users. Fragmentary supports this by means of the `user_type` attribute on the `Fragment` model. This is a string identifying which type of user the corresponding cached content is for. Typical examples would be `"admin"`, `"signed_in"` and `"signed_out"`.
|
262
|
+
In the context of caching, this means that separate versions of some content may need to be stored in the cache for each distinct group of users. Fragmentary supports this by means of the `user_type` attribute on the `Fragment` model. This is a string identifying which type of user the corresponding cached content is intended for. Typical examples would be `"admin"`, `"signed_in"` and `"signed_out"`.
|
263
263
|
|
264
264
|
The fragment subclass definition for a fragment representing content that needs to be customized by user type should include the `needs_user_type` declaration:
|
265
265
|
```
|
@@ -268,7 +268,7 @@ class MyFragment < Fragment
|
|
268
268
|
...
|
269
269
|
end
|
270
270
|
```
|
271
|
-
|
271
|
+
For a fragment subclass like this, when you define an individual fragment in the view, Fragmentary needs to be able to initially create and then subsequently retrieve the fragment record using the correct `user_type` value. One way to do this is to pass a user type to either the `cache_fragment` or `cache_child` method explicitly. This presumes that in the case of authenticated users, a user object is available in the template and that you can derive a corresponding `user_type` string for that object. For example, if you use the [Devise gem]( https://github.com/plataformatec/devise) to provide authentication, by default an authenticated user is represented by the method `current_user`. Since the method returns `nil` if the user is not signed in, the `user_type` passed to `cache_fragment` could be provided as follows (assuming here that a method `is_an_admin?` is defined for your user model),
|
272
272
|
```
|
273
273
|
<% user_type = current_user ? (current_user.is_an_admin? ? "admin" : "signed_in") : "signed_out" %>
|
274
274
|
<% cache_fragment :type => 'MyFragment', :user_type => user_type do %>
|
@@ -277,20 +277,36 @@ When you define a fragment in the view, Fragmentary needs to be able to initiall
|
|
277
277
|
```
|
278
278
|
This will store the `user_type` string in the fragment record when it is first created and then use that string to retrieve the fragment record on subsequent requests.
|
279
279
|
|
280
|
-
However
|
280
|
+
However specifying the `user_type` explicitly like this every time you insert a fragment is actually not necessary. Fragmentary allows you to pre-configure both the method used in your template to obtain the user object and the method used to map that object to the `user_type` string. That string will then be automatically inserted into the fragment specification implicitly whenever you use `cache_fragment` or `cache_child` with a fragment `type` that references a class defined with `needs_user_type`. So in the template you don't need to provide any additional parameters:
|
281
281
|
```
|
282
282
|
<% cache_fragment :type => 'MyFragment' do %>
|
283
283
|
...
|
284
284
|
<% end %>
|
285
285
|
```
|
286
286
|
|
287
|
-
|
287
|
+
To configure Fragmentary to use this approach, create a file, say `fragmentary.rb`, in `config/initializers` and add something like the following:
|
288
288
|
```
|
289
|
-
|
290
|
-
|
289
|
+
Fragmentary.setup do |config|
|
290
|
+
config.current_user_method = :current_user
|
291
|
+
config.default_user_type_mapping = ->(user) {
|
292
|
+
user ? (user.is_an_admin? ? "admin" : "signed_in") : "signed_out"
|
293
|
+
}
|
291
294
|
end
|
292
295
|
```
|
293
|
-
|
296
|
+
The variable `config` yielded to the `setup` block above represents `Fragmentary.config` which is an instance of class `Fragmentary::Config`. The value assigned to `config.current_user_method` is a symbol representing the method Fragmentary will call on the current template to obtain the current user object. It is up to you to ensure that the method exists for your application. The default value is `:current_user` (so you wouldn't actually need to specify the value used in the example above).
|
297
|
+
|
298
|
+
The value assigned to `config.default_user_type_mapping` is a `Proc` to which Fragmentary will pass whatever user object is returned by `config.current_user_method`. When called, the method returns the required `user_type` string.
|
299
|
+
|
300
|
+
As well as configuring a `default_user_type_mapping` as shown above, it is also possible to specify a mapping on a per-class basis when declaring `needs_user_type`:
|
301
|
+
```
|
302
|
+
class MyFragment < Fragment
|
303
|
+
needs_user_type
|
304
|
+
:user_type_mapping => -> (user) {
|
305
|
+
user ? (user.paid_subscriber? ? "paid" : "unpaid") : nil
|
306
|
+
}
|
307
|
+
end
|
308
|
+
```
|
309
|
+
Whether you specify the user type mapping in Fragmentary's configuration or in a class declaration, declaring `needs_user_type` results in a class method `MyFragment.user_type(user)` being added to your fragment subclass.
|
294
310
|
|
295
311
|
|
296
312
|
#### Per-User Customization
|
@@ -322,7 +338,7 @@ and in your template insert that widget content by writing,
|
|
322
338
|
<% end %>
|
323
339
|
```
|
324
340
|
|
325
|
-
The point to note is that the specific soup in the example is _not_ stored in the cache; it is inserted only after the cached content is retrieved by `cache_fragment` (or alternatively `cache_child`).
|
341
|
+
The point to note is that the specific soup in the example is _not_ stored in the cache; it is inserted only after the cached content is retrieved by `cache_fragment` (or alternatively `cache_child`). Once you've defined the `Widget` subclass, inserting the placeholder into your template is all you have to do to make use of the widget. Fragmentary takes care of detecting the placeholder and inserting the widget's non-cached content into the content retrieved from the cache.
|
326
342
|
|
327
343
|
You can also pass variable data into your widget by including parenthesized capture groups within the widget's regular expression pattern. For example, with a pattern like this,
|
328
344
|
```
|
@@ -368,14 +384,14 @@ and insert it into a template with,
|
|
368
384
|
<p><%= "%{welcome_message}" %></p>
|
369
385
|
```
|
370
386
|
Notice that in this case we subclassed `UserWidget` instead of `Widget`. The only differences between the two are that:
|
371
|
-
- as a convenience, `UserWidget` provides a read accessor `current_user` to save you having to write `template.
|
372
|
-
- if `current_user` is nil
|
387
|
+
- as a convenience, `UserWidget` provides a read accessor `current_user` to save you having to write `template.send(Fragmentary.config.current_user_method)`.
|
388
|
+
- if `current_user` is nil because no user is signed in, `content` will return an empty string.
|
373
389
|
|
374
390
|
### The 'memo' Attribute and Conditional Content
|
375
391
|
|
376
392
|
With the exception of `updated_at`, the attributes of the fragment model we have discussed, `type`, `record_id`, `parent_id`, `key`, and `user_type`, are all designed for one purpose: to uniquely identify a fragment record. Occasionally, however, we may need to attach additional information to a fragment record, e.g. regarding the content contained within the fragment.
|
377
393
|
|
378
|
-
Suppose you wish the inclusion of a piece of cached content in your template to be conditional on the result of some computation that occurs _within_ the process of rendering of that content.
|
394
|
+
Suppose you wish the inclusion of a piece of cached content in your template to be conditional on the result of some computation that occurs _within_ the process of rendering of that content. This could arise, for example, if database records needed for the fragment are retrieved within the body of the fragment (in order to avoid having to access the database when a cached version of the fragment is available), and the fragment is either shown or hidden selectively, depending on whether some variable derived from that data matches another parameter, say a user input.
|
379
395
|
|
380
396
|
Of course if you have to render the content in order to determine whether it needs to be included, you defeat the purpose of caching. However, the fragment `memo` attribute included in the database migration suggested earlier can be used to address this. The process involves three parts:
|
381
397
|
* wrap the `cache_fragment` or `cache_child` method that defines the fragment content with a Rails `capture` block and assign the result to a local variable.
|
@@ -396,7 +412,7 @@ Of course if you have to render the content in order to determine whether it nee
|
|
396
412
|
<% end %>
|
397
413
|
```
|
398
414
|
|
399
|
-
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
|
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.
|
400
416
|
|
401
417
|
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.
|
402
418
|
|
@@ -418,7 +434,7 @@ In both of these examples, the fragment to be tested is retrieved after caching.
|
|
418
434
|
```
|
419
435
|
<% root_fragment = Fragment.root(:type => 'ProductTemplate', :record_id => @product.id) %>
|
420
436
|
<% conditional_content = capture do %>
|
421
|
-
<% cache_fragment :fragment =>
|
437
|
+
<% cache_fragment :fragment => root_fragment do |fragment| %>
|
422
438
|
<%# Content to be cached goes here %>
|
423
439
|
<%# ... %>
|
424
440
|
<% fragment.update_attribute(:memo, computed_memo_value) %>
|
@@ -449,39 +465,97 @@ Note that both `Fragment.root` and `Fragment#child` will instantiate a matching
|
|
449
465
|
<% end %>
|
450
466
|
```
|
451
467
|
|
452
|
-
###
|
468
|
+
### Updating Cached Content Automatically
|
453
469
|
|
454
470
|
#### Internal Application Requests
|
455
471
|
|
456
|
-
When application data that a cached fragment depends upon changes,
|
472
|
+
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
|
+
|
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.
|
457
475
|
|
458
|
-
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 without generating any external network traffic. Fragmentary uses this interface to automatically send requests
|
476
|
+
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.
|
459
477
|
|
460
|
-
|
478
|
+
Creating these requests is handled slightly differently depending on whether they are designed to update content associated with an existing root fragment or to create content for a new root fragment that does not yet exist. In the case of an existing root fragment, if the fragment's class has a `request_path` *instance* method defined, a request will be sent to the application at that path (represented in the form of a string) whenever `touch` is called on the fragment record (in general, the request can be suppressed by passing `:no_request => true` if required). You simply need to define the `request_path` method in any individual `Fragment` subclass that you wish to generate requests for. For example:
|
479
|
+
```
|
480
|
+
class ProductTemplate < Fragment
|
481
|
+
needs_record_id :type => 'Product'
|
482
|
+
|
483
|
+
def request_path
|
484
|
+
"/product/#{record_id}"
|
485
|
+
end
|
486
|
+
end
|
487
|
+
```
|
488
|
+
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
|
+
|
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}`.
|
491
|
+
|
492
|
+
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
|
+
```
|
494
|
+
class ProductTemplate < Fragment
|
495
|
+
needs_record_id :type => 'Product'
|
496
|
+
|
497
|
+
def self.request_path(record_id)
|
498
|
+
"/product/#{record_id}"
|
499
|
+
end
|
500
|
+
|
501
|
+
def request_path
|
502
|
+
self.class.request_path(record_id)
|
503
|
+
end
|
504
|
+
```
|
505
|
+
|
506
|
+
So in this example, any time a new `Product` record is created, Fragmentary will send a request to the page for that new product, resulting in the corresponding `ProductTemplate` fragment being created. As shown here, in cases like this we generally choose to define the instance method `request_path` in terms of the corresponding class method.
|
461
507
|
|
462
508
|
#### Request Queues
|
463
509
|
|
464
|
-
A single external
|
510
|
+
A single external HTTP POST, PATCH or DELETE request can cause changes to application data affecting multiple fragments on multiple pages. One external request can therefore lead to multiple internal application requests. In addition, considering that different versions of some cached fragments exist for different user types, in order to ensure that all affected versions get refreshed we may need to send each internal request multiple times in the context of several different user sessions, each representing a different user type.
|
465
511
|
|
466
|
-
To achieve this, during
|
512
|
+
To achieve this, during handling of the initial external request in which changes in application data propagate to affected fragment records via the `subscribe_to` declarations in your fragment class definitions, multiple instances of class `Fragmentary::RequestQueue`, each corresponding to a different user type, are used to store a collection of `Fragmentary::Request` objects representing the internal requests generated during that process.
|
467
513
|
|
468
|
-
The set of user types
|
514
|
+
The set of user types that an individual `Fragment` subclass is expected to support is available via a configurable class method `user_types`, which returns an array of user type strings (Note this is different from the class method `user_type` discussed earlier that returns a single user type for a specific current user).
|
469
515
|
|
470
|
-
Fragmentary uses the types returned by this method to identify the request queues that
|
516
|
+
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.
|
471
517
|
|
472
|
-
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 `
|
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.
|
473
519
|
|
474
|
-
|
520
|
+
#### Configuring Internal Request Users
|
521
|
+
|
522
|
+
The user types each subclass supports can be configured in two ways. If all of the fragments in your application that declare `need_user_type` always use the same set of user types, you can configure these by setting `Fragmentary.config.session_users` in your `initializers/fragementary.rb` file:
|
475
523
|
|
476
|
-
In the current implementation, dummy users that require authentication are identified by email and password. In our application code we define a class method `User.test_user` that creates or retrieves a dummy user object containing read accessors for these two parameters. The method takes a string name and one named option indicating whether the user is an admin:
|
477
524
|
```
|
478
|
-
|
525
|
+
Fragmentary.setup do |config|
|
479
526
|
...
|
527
|
+
config.session_users = {
|
528
|
+
'signed_in' => {:credentials => {:user => {:email => 'bob@example.com', :password => 'bobs_secret'},
|
529
|
+
'admin' => {:credentials => {:user => {:email => 'alice@example.com', :password => 'alices_secret'}
|
530
|
+
}
|
531
|
+
config.get_sign_in_path = '/users/sign_in'
|
532
|
+
config.post_sign_in_path = '/users/sign_in'
|
533
|
+
config.sign_out_path = '/users/sign_out'
|
534
|
+
end
|
535
|
+
```
|
536
|
+
|
537
|
+
The value assigned to `session_users` is a hash whose keys are each a required `user_type` string, with values containing a hash of credentials needed to sign in as a stereotypical user for that type, i.e. the HTTP request parameters that need to be sent with a POST request to sign in. If authentication is not required, the value should be an empty hash. If you prefer not to put credentials in the configuration file directly, you can alternatively specify a `Proc` that returns the required hash; the `Proc` will be executed when sign-in actually occurs. We have used this, for example, to retrieve randomized single-use credentials from our User model for specific test users that have only read-access to the site.
|
538
|
+
|
539
|
+
As shown in the example, we also need to assign sign-in and sign-out paths, with separate sign-in paths for GET and POST requests. The GET path is the address from which a user would retrieve the sign-in form. Fragmentary sends a GET request to this address first in order to retrieve the CSRF token that needs to be submitted with the user credentials in a POST request when signing in.
|
540
|
+
|
541
|
+
The configuration defined using `Fragmentary.setup` as shown above will be used as the default for any fragment subclass defined using `needs_user_type`; for those subclasses the class method `user_types` will return an array of the `user_type` strings so defined. However, if you need a specific subclass to support a different set of user types, you can configure that by passing additional options to `needs_user_type` in the class definition.
|
542
|
+
|
543
|
+
```
|
544
|
+
class MyFragment < Fragment
|
545
|
+
needs_user_type
|
546
|
+
:session_users => ['admin',
|
547
|
+
'paid' => {:credentials => ...},
|
548
|
+
'unpaid' => {:credentials => ...}]
|
480
549
|
end
|
481
550
|
```
|
482
|
-
The method assigns to the user a dummy email address that is unique by name together with a random password for each new session. A singleton accessor method `password` is defined on the object to make the temporary password available to the UserSession object so that it's able to sign the user in before sending the queued requests.
|
483
551
|
|
484
|
-
|
552
|
+
The value of the `:session_users` option (you can also use `:user_types` or just `:types` instead) is an array containing either strings of user types that are defined elsewhere, e.g. using `Fragmentary.setup` (e.g. 'admin' in the example) or hashes of new session user definitions that include their required sign-in credentials.
|
553
|
+
|
554
|
+
#### Sending Queued Requests
|
555
|
+
|
556
|
+
A class method `Fragmentary::RequestQueue.all` returns an array of all request queues the application uses. The requests stored within a given `RequestQueue` can be sent to the application by calling the instance method `RequestQueue#send`. Calling `send` instantiates a `Fragmentary::UserSession` object representing a browser session for the particular type of user the queue is handling. For sessions representing user types that need to be authenticated, instantiating the `UserSession` will sign in to the application using the credentials configured for the particular `user_type`.
|
557
|
+
|
558
|
+
To send all requests once processing of each external browser request has been completed, add a method such as the following to your `ApplicationController` class and call it using a controller `after_filter`, e.g. for create, update and destroy actions:
|
485
559
|
```
|
486
560
|
def send_queued_requests
|
487
561
|
delay = 0.seconds
|
@@ -490,9 +564,32 @@ end
|
|
490
564
|
```
|
491
565
|
The `send` method takes two optional named arguments, `delay` and `between`. If neither are present, all requests held in the queue are sent immediately. If either are present, sending of requests is off-loaded to an asynchronous process using the [Delayed::Job gem](https://github.com/collectiveidea/delayed_job) and scheduled according to the parameters provided: `delay` represents the delay before the queue begins sending requests and `between` represents the interval between individual requests in the queue being sent. In the example above, we choose to delay the sending of requests from each queue by 10 seconds each. You may customize as appropriate.
|
492
566
|
|
567
|
+
#### Queuing Requests Explicitly
|
568
|
+
|
569
|
+
In the cases we've described so far, internal requests are added to request queues automatically, either as a result of updating an existing root fragment or because a new application record has been created that requires a new root fragment with a class declaration `needs_record_id` to be created. There are, however, sometimes cases in which you may wish to create `Fragmentary::Request` objects yourself and add them to the appropriate queues explicitly. This could arise, for example, with a fragment class that declares `needs_key`. If a change in application data results in a value of the 'key' that didn't previously exist, you may wish, in a `subscribe_to` block within your fragment subclass definition, to create an internal request explicitly that will result in new cached content being generated for that new 'key' value.
|
570
|
+
|
571
|
+
The process is straightforward. First instantiate a request by calling `Fragmentary::Request.new`. This method takes two required parameters and two optional parameters:
|
572
|
+
|
573
|
+
```
|
574
|
+
request = Fragmentary::Request.new(method, path, parameters, options)
|
575
|
+
```
|
576
|
+
|
577
|
+
`method` is a string, usually 'get', but in general 'post', 'patch', 'put' and 'delete' are also acceptable values.
|
578
|
+
|
579
|
+
`path` is a string representing the path you wish the request to be sent to.
|
580
|
+
|
581
|
+
`parameters` is an optional hash containing named HTTP request parameters to be sent with the request.
|
582
|
+
|
583
|
+
`options` is an optional hash `{:xhr => true}` if the request is to be sent as an `XMLHttpRequest`.
|
584
|
+
|
585
|
+
Once the request is instantiated, you can add it to all of the queues required by the fragment's subclass simply by calling the class method `queue_request`:
|
586
|
+
```
|
587
|
+
MyFragment.queue_request(request)
|
588
|
+
```
|
589
|
+
|
493
590
|
### Asynchronous Fragment Updating
|
494
591
|
|
495
|
-
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,
|
592
|
+
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.
|
496
593
|
|
497
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.
|
498
595
|
|
@@ -515,11 +612,15 @@ class AvailableStore < Fragment
|
|
515
612
|
end
|
516
613
|
end
|
517
614
|
```
|
518
|
-
While a `Handler` subclass definition defines a task to be run asynchronously and calling the class method `create` within a `subscribe_to` block causes the task to be instantiated, we still have to explicitly dispatch the task to an asynchronous process, which we again (currently) do using Delayed::Job.
|
615
|
+
While a `Handler` subclass definition defines a task to be run asynchronously, and calling the class method `create` within a `subscribe_to` block causes the task to be instantiated, we still have to explicitly dispatch the task to an asynchronous process, which we again (currently) do using Delayed::Job.
|
519
616
|
|
520
|
-
To facilitate this we use the `Fragmentary::Dispatcher` class. A dispatcher object is instantiated with an array containing all existing handlers, retrieved using the class method `Fragmentary::Handler.all`.
|
617
|
+
To facilitate this we use the `Fragmentary::Dispatcher` class. A dispatcher object is instantiated with an array containing all existing handlers, retrieved using the class method `Fragmentary::Handler.all`.
|
618
|
+
```
|
619
|
+
dispatcher = Fragmentary::Dispatcher.new(Fragmentary::Handler.all)
|
620
|
+
```
|
621
|
+
The `Dispatcher` class defines an instance method `perform`, which invokes `call` on all the handlers provided when the dispatcher is instantiated. Delayed::Job uses the `perform` method to define the job to be run asynchronously, which is accomplished by passing the dispatcher object to `Delayed::Job.enqueue`:
|
521
622
|
```
|
522
|
-
Delayed::Job.enqueue(
|
623
|
+
Delayed::Job.enqueue(dispatcher)
|
523
624
|
```
|
524
625
|
As in the case of our queued background requests, we enqueue the dispatcher in an `ApplicationController` `after_filter`. In fact we can combine both the sending of background requests and the asynchronous fragment updating task in one method:
|
525
626
|
```
|
@@ -528,25 +629,78 @@ class ApplicationController < ActionController::Base
|
|
528
629
|
after_filter :handle_asynchronous_tasks
|
529
630
|
|
530
631
|
def handle_asynchronous_tasks
|
531
|
-
send_queued_requests
|
632
|
+
send_queued_requests # as defined earlier
|
532
633
|
Delayed::Job.enqueue(Fragmentary::Dispatcher.new(Fragmentary::Handler.all))
|
533
634
|
Fragmentary::Handler.clear
|
534
635
|
end
|
535
636
|
|
536
637
|
end
|
537
638
|
```
|
538
|
-
Note that updating fragments in an asynchronous process like this will itself generate internal application requests beyond
|
639
|
+
Note that updating fragments in an asynchronous process like this will itself generate internal application requests beyond those generated in the course of handling the user's initial request. The `Dispatcher` takes care of sending these additional requests.
|
640
|
+
|
641
|
+
#### Updating Lists Asynchronously
|
642
|
+
|
643
|
+
As discussed earlier, if a fragment class is declared with `acts_as_list_fragment`, fragments of that class will be automatically touched whenever a new list item is created. If you want this process to take place asynchronously, simply pass the option `:delay => true` to `acts_as_list_fragment`, e.g.:
|
644
|
+
```
|
645
|
+
class StoresAvailable < Fragment
|
646
|
+
acts_as_list_fragment :members => :product_stores, :list_record => :product, :delay => true
|
647
|
+
end
|
648
|
+
```
|
649
|
+
|
650
|
+
### Updating Fragments Explicitly Within a Controller
|
651
|
+
|
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.
|
653
|
+
|
654
|
+
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
|
+
|
656
|
+
A possible solution in this situation may be to touch the fragment representing the content that is about to be returned to the user *in the controller*. We're *not* convinced that this is good practice, but if you decide you need to, you can use the class method `Fragment.existing` to obtain the required fragment in the controller and call `touch` on it explicitly. If the fragment subclass involved is one that supports multiple user types, you will want to ensure that just the fragment corresponding to the current user's `user_type` is touched, which you can do by passing the current user object to `existing`. Internally, `existing` will map the user object to the corresponding `user_type` using the `user_type_mapping` you have configured in order to select the correct fragment. In addition, when you touch the fragment in this context, you should pass `:no_request => true` so that it doesn't generate an internal request to update the cache since responding to the user's actual request is going to accomplish this automatically.
|
657
|
+
|
658
|
+
```
|
659
|
+
class ProductStoresController < ApplicationController
|
660
|
+
def create
|
661
|
+
...
|
662
|
+
@product_store.save
|
663
|
+
fragment = ProductTemplate.existing(:record_id => @product_store.product_id, :user => current_user)
|
664
|
+
fragment.touch(:no_request => true)
|
665
|
+
end
|
666
|
+
end
|
667
|
+
```
|
668
|
+
|
669
|
+
### Removing Queued Requests in the Controller
|
670
|
+
|
671
|
+
Another scenario that occasionally arises is when internal requests created in the course of executing methods defined in a `subscribe_to` block include the path to the same page that the controller is going to render anyway in the normal course of responding to the user's current browser request. In effect, that one internal request is redundant. Although it generally won't cause any real harm to send a redundant request (remember the internal request is usually dispatched asynchronously, so it won't interfere with the synchronous response), redundancy is redundancy and you may wish to avoid it.
|
672
|
+
|
673
|
+
In this case, it is possible within the controller to remove a request that has already been queued, after the application data has been created, updated or destroyed, by using the class method `Fragment.remove_queued_request`. The method takes two named parameters, the path of the request to be removed and the current user object, with the latter allowing it to remove the request from the correct queue, e.g.
|
674
|
+
|
675
|
+
```
|
676
|
+
class ProductsController < ApplicationController
|
677
|
+
def create
|
678
|
+
...
|
679
|
+
if @product.save
|
680
|
+
respond_to do |format|
|
681
|
+
format.html do
|
682
|
+
ProductTemplate.remove_queued_request(:request_path => "/products/#{@product.id}", :user => current_user)
|
683
|
+
redirect_to @product
|
684
|
+
end
|
685
|
+
format.js
|
686
|
+
end
|
687
|
+
else
|
688
|
+
...
|
689
|
+
end
|
690
|
+
end
|
691
|
+
end
|
692
|
+
```
|
539
693
|
|
540
694
|
### Handling AJAX Requests
|
541
695
|
|
542
696
|
#### Providing a Receiver for Calls to 'cache_child'
|
543
|
-
There are a couple of special issues to consider when using Fragmentary with templates designed to respond to partial page requests. An example would be a Javascript template used to insert content into a previously loaded page, such as when dynamically adding a new item to an existing list.
|
697
|
+
There are a couple of special issues to consider when using Fragmentary with templates designed to respond to partial page requests. An example would be a Javascript template used to insert content into a previously loaded page, such as when dynamically adding a new item to an existing list. The typical approach in this scenario is to use embedded Ruby (ERB) in the Javascript template to render a partial containing the required content, escape the resulting string for Javascript and insert it into the list using jQuery's `append` method, e.g.
|
544
698
|
|
545
699
|
```
|
546
700
|
$('ul.product_list').append('<%= j(render 'product/summary', :product => @product) %>')
|
547
701
|
```
|
548
702
|
|
549
|
-
However, it's possible for the partial to contain a cached fragment that happens to be a child of a parent containing the list as a whole.
|
703
|
+
However, it's possible for the partial to contain a cached fragment that happens to be a child of a parent containing the list as a whole. In this case, when the entire page containing the original list is first rendered and the parent fragment is retrieved using say `cache_fragment` (if it happens to be a root fragment), a `CacheBuilder` object is yielded to the block that renders the partial, and this object is passed to the partial to act as the receiver for `cache_child`.
|
550
704
|
```
|
551
705
|
<% cache_fragment :type =>`ProductList` do |parent_fragment| %>
|
552
706
|
<ul class='product_list'>
|
@@ -556,7 +710,7 @@ However, it's possible for the partial to contain a cached fragment that happens
|
|
556
710
|
</ul>
|
557
711
|
<% end %>
|
558
712
|
```
|
559
|
-
In the Javascript case, however, there is no `cache_fragment` method 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.
|
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.
|
560
714
|
```
|
561
715
|
<% parent_fragment = fragment_builder(:type => 'ProductList', :template => self) %>
|
562
716
|
$('ul.product_list').append('<%= j(render 'product/summary', :product => @product,
|
@@ -591,12 +745,11 @@ It is possible to define a fragment without actually storing its content in the
|
|
591
745
|
|
592
746
|
## Integration Issues
|
593
747
|
|
594
|
-
|
595
|
-
1. Fragmentary
|
596
|
-
|
597
|
-
|
598
|
-
1. Fragmentary
|
599
|
-
1. Fragmentary uses the [Delayed::Job gem](https://github.com/collectiveidea/delayed_job) to execute background tasks asynchronously. Other alternatives exist within the Rails ecosystem, and in Rails 5.x it may make sense use [Active Job](https://guides.rubyonrails.org/active_job_basics.html) as an abstraction layer.
|
748
|
+
There are some aspects of this pre-release version of Fragmentary that reflect the application context in which it was originally developed and may need adjustment before deployment elsewhere. Note in particular the following:
|
749
|
+
1. Fragmentary was created in the context of a Rails 4.x application (for perfectly sound reasons! :)). There should be only minor adjustment required for use within a Rails 5.x application, but two specific issues we are aware of are the following:
|
750
|
+
- Rails 5.x changes the API for `ActionDispatch::Integration::Session` and now requires that HTTP request parameters be passed as a named parameter `:params`, rather than an unnamed hash in Rails 4.x. This affects the method `to_proc` in class `Fragmentary::Request` and the methods `sign_in` and `sign_out` in class `Fragmentary::UserSession`.
|
751
|
+
- In 'lib/fragmentary/fragment.rb', we set `cache_timestamp_format = :usec` to overcome a timestamp resolution problem when using caching with Postgres under Rails 4.x. We believe that this problem has been solved in Rails 5.x, so this setting will not be necessary. See https://github.com/rails/rails/issues/21815.
|
752
|
+
1. Fragmentary uses the [Delayed::Job gem](https://github.com/collectiveidea/delayed_job) to execute background tasks asynchronously. Other alternatives exist within the Rails ecosystem, and in Rails 5.x it will probably make sense to use [Active Job](https://guides.rubyonrails.org/active_job_basics.html) as an abstraction layer.
|
600
753
|
|
601
754
|
## Contributing
|
602
755
|
|
data/lib/fragmentary.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'fragmentary/version'
|
2
|
+
require 'fragmentary/config'
|
2
3
|
require 'fragmentary/fragments_helper'
|
3
4
|
require 'fragmentary/subscriber'
|
4
5
|
require 'fragmentary/request_queue'
|
@@ -9,3 +10,12 @@ require 'fragmentary/user_session'
|
|
9
10
|
require 'fragmentary/widget_parser'
|
10
11
|
require 'fragmentary/widget'
|
11
12
|
require 'fragmentary/publisher'
|
13
|
+
|
14
|
+
module Fragmentary
|
15
|
+
def self.config
|
16
|
+
@config ||= Fragmentary::Config.instance
|
17
|
+
yield @config if block_given?
|
18
|
+
@config
|
19
|
+
end
|
20
|
+
class << self; alias setup config; end
|
21
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Fragmentary
|
2
|
+
|
3
|
+
class Config
|
4
|
+
include Singleton
|
5
|
+
attr_accessor :current_user_method, :get_sign_in_path, :post_sign_in_path,
|
6
|
+
:sign_out_path, :users, :default_user_type_mapping, :session_users
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
# default
|
10
|
+
@current_user_method = :current_user
|
11
|
+
end
|
12
|
+
|
13
|
+
def session_users=(session_users)
|
14
|
+
raise "config.session_users must be a Hash" unless session_users.is_a?(Hash)
|
15
|
+
Fragmentary.parse_session_users(session_users)
|
16
|
+
@session_users = session_users
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.current_user_method
|
21
|
+
self.config.current_user_method
|
22
|
+
end
|
23
|
+
|
24
|
+
# Parse a class-specific set of session_user options
|
25
|
+
# session_users can be an array of session_user keys, a hash of session_user definitions or an array
|
26
|
+
# containing a mixture of both. The method should return an array of keys. If session_users is an
|
27
|
+
# array, elements representing existing SessionUser objects should be included in the returned array.
|
28
|
+
# Non-hash elements that don't represent existing SessionUser objects should raise an exception. Array
|
29
|
+
# elements that are hashes should be parsed to create new SessionUser objects. Raise an exception if
|
30
|
+
# any attempt to redefine an existing user_type.
|
31
|
+
def self.parse_session_users(session_users = nil)
|
32
|
+
return nil unless session_users
|
33
|
+
if session_users.is_a?(Array)
|
34
|
+
# Fun fact: can't use 'each_with_object' here because 'acc += parse_session_users(v)' assigns a
|
35
|
+
# different object to 'acc', while 'each_with_object' passes the *same* object to the block on
|
36
|
+
# each iteration.
|
37
|
+
session_users.inject([]) do |acc, v|
|
38
|
+
if v.is_a?(Hash)
|
39
|
+
acc + parse_session_users(v)
|
40
|
+
else
|
41
|
+
acc << v
|
42
|
+
end
|
43
|
+
end
|
44
|
+
elsif session_users.is_a?(Hash)
|
45
|
+
session_users.each_with_object([]) do |(k,v), acc|
|
46
|
+
acc << k if user = SessionUser.new(k,v)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
data/lib/fragmentary/fragment.rb
CHANGED
@@ -30,7 +30,7 @@ module Fragmentary
|
|
30
30
|
|
31
31
|
validate :root_id, :presence => true
|
32
32
|
|
33
|
-
cache_timestamp_format = :usec #
|
33
|
+
cache_timestamp_format = :usec # Probably not needed for Rails 5, which uses :usec by default.
|
34
34
|
|
35
35
|
end
|
36
36
|
|
@@ -49,14 +49,10 @@ module Fragmentary
|
|
49
49
|
module ClassMethods
|
50
50
|
|
51
51
|
def root(options)
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
fragment
|
57
|
-
else
|
58
|
-
raise RangeError, "#{options[:type]} is not a root fragment class"
|
59
|
-
end
|
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
|
55
|
+
fragment
|
60
56
|
end
|
61
57
|
|
62
58
|
# Each fragment record is unique by type and parent_id (which is nil for a root_fragment) and for some types also by
|
@@ -118,13 +114,13 @@ module Fragmentary
|
|
118
114
|
if self == base_class
|
119
115
|
@@request_queues
|
120
116
|
else
|
121
|
-
return nil unless new.requestable?
|
117
|
+
return nil unless (requestable? or new.requestable?)
|
122
118
|
user_types.each_with_object({}){|user_type, queues| queues[user_type] = @@request_queues[user_type]}
|
123
119
|
end
|
124
120
|
end
|
125
121
|
|
126
|
-
def remove_queued_request(user:,
|
127
|
-
request_queues[user_type(user)].remove_path(request_path
|
122
|
+
def remove_queued_request(user:, request_path:)
|
123
|
+
request_queues[user_type(user)].remove_path(request_path)
|
128
124
|
end
|
129
125
|
|
130
126
|
def subscriber
|
@@ -152,8 +148,18 @@ module Fragmentary
|
|
152
148
|
# signed in. When the fragment is instantiated using FragmentsHelper methods 'cache_fragment' or 'CacheBuilder.cache_child',
|
153
149
|
# a :user option is added to the options hash automatically from the value of 'current_user'. The user_type is extracted
|
154
150
|
# from this option in Fragment.find_or_create.
|
155
|
-
def needs_user_type
|
151
|
+
def needs_user_type(options = {})
|
156
152
|
self.extend NeedsUserType
|
153
|
+
instance_eval do
|
154
|
+
@user_type_mapping = options[:user_type_mapping]
|
155
|
+
def self.user_type(user)
|
156
|
+
(@user_type_mapping || Fragmentary.config.default_user_type_mapping).try(:call, user)
|
157
|
+
end
|
158
|
+
@user_types = Fragmentary.parse_session_users(options[:session_users] || options[:types] || options[:user_types])
|
159
|
+
def self.user_types
|
160
|
+
@user_types || Fragmentary.config.session_users.keys
|
161
|
+
end
|
162
|
+
end
|
157
163
|
end
|
158
164
|
|
159
165
|
def needs_key(options = {})
|
@@ -205,9 +211,20 @@ module Fragmentary
|
|
205
211
|
end
|
206
212
|
end
|
207
213
|
|
208
|
-
|
209
|
-
record_type.constantize
|
214
|
+
if requestable?
|
215
|
+
record_class = record_type.constantize
|
216
|
+
instance_eval <<-HEREDOC
|
217
|
+
subscribe_to #{record_class} do
|
218
|
+
def create_#{record_class.model_name.param_key}_successful(record)
|
219
|
+
request = Fragmentary::Request.new(request_method, request_path(record.id),
|
220
|
+
request_parameters(record.id), request_options)
|
221
|
+
queue_request(request)
|
222
|
+
end
|
223
|
+
end
|
224
|
+
HEREDOC
|
210
225
|
end
|
226
|
+
|
227
|
+
define_method(:record){record_type.constantize.find(record_id)}
|
211
228
|
end
|
212
229
|
end
|
213
230
|
|
@@ -260,24 +277,14 @@ module Fragmentary
|
|
260
277
|
nil
|
261
278
|
end
|
262
279
|
|
263
|
-
def queue_request(
|
264
|
-
|
265
|
-
|
266
|
-
request_queues.each{|key, queue| queue << r}
|
280
|
+
def queue_request(request=nil)
|
281
|
+
if request
|
282
|
+
request_queues.each{|key, queue| queue << request}
|
267
283
|
end
|
268
284
|
end
|
269
285
|
|
270
286
|
def requestable?
|
271
|
-
respond_to?
|
272
|
-
end
|
273
|
-
|
274
|
-
# Subclasses that define a class method self.request_path also need to override this method
|
275
|
-
def request(*args)
|
276
|
-
if respond_to? :request_path
|
277
|
-
raise "You can't call Fragment.request for a subclass that defines 'request_path'. #{name} needs its own request implementation."
|
278
|
-
else
|
279
|
-
raise "There is no 'request' class method defined for the #{name} class."
|
280
|
-
end
|
287
|
+
respond_to?(:request_path)
|
281
288
|
end
|
282
289
|
|
283
290
|
# The instance method 'request_method' is defined in terms of this.
|
@@ -529,14 +536,6 @@ module Fragmentary
|
|
529
536
|
def needs_user_type?
|
530
537
|
true
|
531
538
|
end
|
532
|
-
|
533
|
-
def user_types
|
534
|
-
['admin', 'signed_in']
|
535
|
-
end
|
536
|
-
|
537
|
-
def user_type(user)
|
538
|
-
user ? (user.is_an_admin? ? "admin" : "signed_in") : "signed_out"
|
539
|
-
end
|
540
539
|
end
|
541
540
|
|
542
541
|
module NeedsKey
|
@@ -4,7 +4,7 @@ module Fragmentary
|
|
4
4
|
|
5
5
|
def cache_fragment(options)
|
6
6
|
no_cache = options.delete(:no_cache)
|
7
|
-
options.reverse_merge!(:user =>
|
7
|
+
options.reverse_merge!(:user => Template.new(self).current_user)
|
8
8
|
fragment = options.delete(:fragment) || Fragmentary::Fragment.base_class.root(options)
|
9
9
|
builder = CacheBuilder.new(fragment, template = self)
|
10
10
|
unless no_cache
|
@@ -19,11 +19,10 @@ module Fragmentary
|
|
19
19
|
|
20
20
|
def fragment_builder(options)
|
21
21
|
template = options.delete(:template)
|
22
|
-
options.reverse_merge!(:user =>
|
22
|
+
options.reverse_merge!(:user => Template.new(template).current_user)
|
23
23
|
CacheBuilder.new(Fragmentary::Fragment.base_class.existing(options), template)
|
24
24
|
end
|
25
25
|
|
26
|
-
|
27
26
|
class CacheBuilder
|
28
27
|
include ::ActionView::Helpers::CacheHelper
|
29
28
|
include ::ActionView::Helpers::TextHelper
|
@@ -38,7 +37,7 @@ module Fragmentary
|
|
38
37
|
def cache_child(options)
|
39
38
|
no_cache = options.delete(:no_cache)
|
40
39
|
insert_widgets = options.delete(:insert_widgets)
|
41
|
-
options.reverse_merge!(:user => template
|
40
|
+
options.reverse_merge!(:user => Template.new(template).current_user)
|
42
41
|
child = options.delete(:child) || fragment.child(options)
|
43
42
|
builder = CacheBuilder.new(child, template)
|
44
43
|
unless no_cache
|
@@ -59,4 +58,23 @@ module Fragmentary
|
|
59
58
|
|
60
59
|
end
|
61
60
|
|
61
|
+
|
62
|
+
# Just a wrapper to allow us to call a configurable current_user_method on the template
|
63
|
+
class Template
|
64
|
+
attr_reader :template
|
65
|
+
|
66
|
+
def initialize(template)
|
67
|
+
@template = template
|
68
|
+
end
|
69
|
+
|
70
|
+
def current_user
|
71
|
+
return nil unless methd = Fragmentary.current_user_method
|
72
|
+
if template.respond_to? methd
|
73
|
+
template.send methd
|
74
|
+
else
|
75
|
+
raise NoMethodError, "The current_user_method '#{methd.to_s}' specified doesn't exist"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
62
80
|
end
|
@@ -4,10 +4,8 @@ module Fragmentary
|
|
4
4
|
|
5
5
|
class RequestQueue
|
6
6
|
|
7
|
-
@@all = []
|
8
|
-
|
9
7
|
def self.all
|
10
|
-
@@all
|
8
|
+
@@all ||= []
|
11
9
|
end
|
12
10
|
|
13
11
|
attr_reader :requests, :user_type, :sender
|
@@ -16,7 +14,7 @@ module Fragmentary
|
|
16
14
|
@user_type = user_type
|
17
15
|
@requests = []
|
18
16
|
@sender = Sender.new(self)
|
19
|
-
|
17
|
+
self.class.all << self
|
20
18
|
end
|
21
19
|
|
22
20
|
def <<(request)
|
@@ -30,21 +28,6 @@ module Fragmentary
|
|
30
28
|
@requests.size
|
31
29
|
end
|
32
30
|
|
33
|
-
def session
|
34
|
-
@session ||= new_session
|
35
|
-
end
|
36
|
-
|
37
|
-
def new_session
|
38
|
-
case user_type
|
39
|
-
when 'signed_in'
|
40
|
-
UserSession.new('Bob')
|
41
|
-
when 'admin'
|
42
|
-
UserSession.new('Alice', :admin => true)
|
43
|
-
else
|
44
|
-
UserSession.new
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
31
|
def next_request
|
49
32
|
@requests.shift
|
50
33
|
end
|
@@ -53,10 +36,6 @@ module Fragmentary
|
|
53
36
|
@requests = []
|
54
37
|
end
|
55
38
|
|
56
|
-
def clear_session
|
57
|
-
@session = nil
|
58
|
-
end
|
59
|
-
|
60
39
|
def remove_path(path)
|
61
40
|
requests.delete_if{|r| r.path == path}
|
62
41
|
end
|
@@ -82,22 +61,6 @@ module Fragmentary
|
|
82
61
|
@queue = queue
|
83
62
|
end
|
84
63
|
|
85
|
-
def next_request
|
86
|
-
queue.next_request.to_proc
|
87
|
-
end
|
88
|
-
|
89
|
-
def send_next_request
|
90
|
-
if queue.size > 0
|
91
|
-
queue.session.instance_exec(&(next_request))
|
92
|
-
end
|
93
|
-
end
|
94
|
-
|
95
|
-
def send_all_requests
|
96
|
-
while queue.size > 0
|
97
|
-
send_next_request
|
98
|
-
end
|
99
|
-
end
|
100
|
-
|
101
64
|
# Send all requests, either directly or by schedule
|
102
65
|
def start(delay: nil, between: nil)
|
103
66
|
Rails.logger.info "\n***** Processing request queue for user_type '#{queue.user_type}'\n"
|
@@ -123,9 +86,25 @@ module Fragmentary
|
|
123
86
|
|
124
87
|
private
|
125
88
|
|
89
|
+
def next_request
|
90
|
+
queue.next_request.to_proc
|
91
|
+
end
|
92
|
+
|
93
|
+
def send_next_request
|
94
|
+
if queue.size > 0
|
95
|
+
session.instance_exec(&(next_request))
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def send_all_requests
|
100
|
+
while queue.size > 0
|
101
|
+
send_next_request
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
126
105
|
def schedule_requests(delay=0.seconds)
|
127
106
|
if queue.size > 0
|
128
|
-
|
107
|
+
clear_session
|
129
108
|
Delayed::Job.transaction do
|
130
109
|
self.class.jobs.destroy_all
|
131
110
|
Delayed::Job.enqueue self, :run_at => delay.from_now
|
@@ -133,6 +112,19 @@ module Fragmentary
|
|
133
112
|
end
|
134
113
|
end
|
135
114
|
|
115
|
+
def session
|
116
|
+
@session ||= new_session
|
117
|
+
end
|
118
|
+
|
119
|
+
def new_session
|
120
|
+
session_user = Fragmentary::SessionUser.fetch(queue.user_type)
|
121
|
+
UserSession.new(session_user)
|
122
|
+
end
|
123
|
+
|
124
|
+
def clear_session
|
125
|
+
@session = nil
|
126
|
+
end
|
127
|
+
|
136
128
|
end
|
137
129
|
|
138
130
|
end
|
@@ -6,48 +6,74 @@ module Fragmentary
|
|
6
6
|
|
7
7
|
include Rails::ConsoleMethods
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
def initialize(*user, &block)
|
9
|
+
def initialize(user, &block)
|
12
10
|
# app is from Rails::ConsoleMethods. It returns an object ActionDispatch::Integration::Session.new(Rails.application)
|
13
11
|
# with some extensions. See https://github.com/rails/rails/blob/master/railties/lib/rails/console/app.rb
|
14
12
|
# The session object has instance methods get, post etc.
|
15
13
|
# See https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/testing/integration.rb
|
16
14
|
@session = app
|
17
|
-
sign_in if @
|
15
|
+
sign_in if @credentials = session_credentials(user)
|
18
16
|
instance_eval(&block) if block_given?
|
19
17
|
end
|
20
18
|
|
19
|
+
def session_credentials(user)
|
20
|
+
credentials = user.try(:credentials)
|
21
|
+
credentials.is_a?(Proc) ? credentials.call : credentials
|
22
|
+
end
|
23
|
+
|
21
24
|
def method_missing(method, *args)
|
22
|
-
session.send(method, *args)
|
25
|
+
@session.send(method, *args)
|
23
26
|
end
|
24
27
|
|
25
28
|
def sign_out
|
26
|
-
post
|
29
|
+
post Fragmentary.config.sign_out_path, {:_method => 'delete', :authenticity_token => request.session[:_csrf_token]}
|
27
30
|
end
|
28
31
|
|
29
32
|
def sign_in
|
30
|
-
get
|
33
|
+
get Fragmentary.config.get_sign_in_path # necessary in order to get the csrf token
|
31
34
|
# NOTE: In Rails 5, params is changed to a named argument, i.e. :params => {...}. Will need to be changed.
|
32
|
-
post
|
33
|
-
|
34
|
-
if session.redirect?
|
35
|
+
post Fragmentary.config.post_sign_in_path, @credentials.merge(:authenticity_token => request.session[:_csrf_token])
|
36
|
+
if @session.redirect?
|
35
37
|
follow_redirect!
|
36
38
|
else
|
37
|
-
raise "Sign in failed
|
39
|
+
raise "Sign in failed with credentials #{@credentials.inspect}"
|
38
40
|
end
|
39
41
|
end
|
40
42
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
43
|
+
end
|
44
|
+
|
45
|
+
class SessionUser
|
46
|
+
|
47
|
+
def self.all
|
48
|
+
@@all ||= Hash.new
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.fetch(key)
|
52
|
+
all[key]
|
53
|
+
end
|
54
|
+
|
55
|
+
def initialize(user_type, options={})
|
56
|
+
if user = self.class.fetch(user_type)
|
57
|
+
if user.options != options
|
58
|
+
raise RangeError, "You can't redefine an existing SessionUser object: #{user_type.inspect}"
|
59
|
+
else
|
60
|
+
user
|
61
|
+
end
|
62
|
+
else
|
63
|
+
@user_type = user_type
|
64
|
+
@options = options
|
65
|
+
self.class.all.merge!({user_type => self})
|
48
66
|
end
|
49
67
|
end
|
50
68
|
|
69
|
+
def credentials
|
70
|
+
options[:credentials]
|
71
|
+
end
|
72
|
+
|
73
|
+
protected
|
74
|
+
def options
|
75
|
+
@options
|
76
|
+
end
|
51
77
|
end
|
52
78
|
|
53
79
|
end
|
data/lib/fragmentary/version.rb
CHANGED
data/lib/fragmentary/widget.rb
CHANGED
@@ -30,6 +30,8 @@ module Fragmentary
|
|
30
30
|
match ? content : nil
|
31
31
|
end
|
32
32
|
|
33
|
+
private
|
34
|
+
|
33
35
|
def content
|
34
36
|
"Undefined Widget"
|
35
37
|
end
|
@@ -41,13 +43,15 @@ module Fragmentary
|
|
41
43
|
|
42
44
|
def initialize(template, key)
|
43
45
|
super
|
44
|
-
@current_user =
|
46
|
+
@current_user = Fragmentary::Template.new(template).current_user
|
45
47
|
end
|
46
48
|
|
47
49
|
def _content
|
48
50
|
match ? user_content : nil
|
49
51
|
end
|
50
52
|
|
53
|
+
private
|
54
|
+
|
51
55
|
def user_content
|
52
56
|
current_user ? content : ""
|
53
57
|
end
|
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.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mark Thomson
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2019-01-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -110,6 +110,7 @@ files:
|
|
110
110
|
- ".gitignore"
|
111
111
|
- ".rspec"
|
112
112
|
- ".travis.yml"
|
113
|
+
- CHANGELOG.md
|
113
114
|
- Gemfile
|
114
115
|
- LICENSE.txt
|
115
116
|
- README.md
|
@@ -118,6 +119,7 @@ files:
|
|
118
119
|
- bin/setup
|
119
120
|
- fragmentary.gemspec
|
120
121
|
- lib/fragmentary.rb
|
122
|
+
- lib/fragmentary/config.rb
|
121
123
|
- lib/fragmentary/dispatcher.rb
|
122
124
|
- lib/fragmentary/fragment.rb
|
123
125
|
- lib/fragmentary/fragments_helper.rb
|