fragmentary 0.1.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 +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.travis.yml +7 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +611 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/fragmentary.gemspec +31 -0
- data/lib/fragmentary.rb +11 -0
- data/lib/fragmentary/dispatcher.rb +19 -0
- data/lib/fragmentary/fragment.rb +549 -0
- data/lib/fragmentary/fragments_helper.rb +62 -0
- data/lib/fragmentary/handler.rb +31 -0
- data/lib/fragmentary/publisher.rb +82 -0
- data/lib/fragmentary/request.rb +30 -0
- data/lib/fragmentary/request_queue.rb +139 -0
- data/lib/fragmentary/subscriber.rb +36 -0
- data/lib/fragmentary/subscription.rb +85 -0
- data/lib/fragmentary/user_session.rb +53 -0
- data/lib/fragmentary/version.rb +3 -0
- data/lib/fragmentary/widget.rb +56 -0
- data/lib/fragmentary/widget_parser.rb +46 -0
- metadata +158 -0
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "fragmentary"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/fragmentary.gemspec
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "fragmentary/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "fragmentary"
|
8
|
+
spec.version = Fragmentary::VERSION
|
9
|
+
spec.authors = ["Mark Thomson"]
|
10
|
+
spec.email = ["mark.thomson@persuasivethinking.com"]
|
11
|
+
|
12
|
+
spec.summary = "Fragment modeling and caching for Rails"
|
13
|
+
spec.description = "Fragment caching for Rails with arbitrarily complex data dependencies"
|
14
|
+
spec.homepage = "https://github.com/MarkMT/fragmentary"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
# Specify which files should be added to the gem when it is released.
|
18
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
19
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
20
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
21
|
+
end
|
22
|
+
spec.bindir = "exe"
|
23
|
+
spec.require_paths = ["lib"]
|
24
|
+
|
25
|
+
spec.add_runtime_dependency "rails", ">= 4.0.0", "< 5"
|
26
|
+
spec.add_runtime_dependency "delayed_job_active_record", "~> 4.1"
|
27
|
+
spec.add_runtime_dependency "wisper-activerecord", "~> 1.0"
|
28
|
+
spec.add_development_dependency "bundler", "~> 1.17"
|
29
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
30
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
31
|
+
end
|
data/lib/fragmentary.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'fragmentary/version'
|
2
|
+
require 'fragmentary/fragments_helper'
|
3
|
+
require 'fragmentary/subscriber'
|
4
|
+
require 'fragmentary/request_queue'
|
5
|
+
require 'fragmentary/request'
|
6
|
+
require 'fragmentary/fragment'
|
7
|
+
require 'fragmentary/handler'
|
8
|
+
require 'fragmentary/user_session'
|
9
|
+
require 'fragmentary/widget_parser'
|
10
|
+
require 'fragmentary/widget'
|
11
|
+
require 'fragmentary/publisher'
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Fragmentary
|
2
|
+
|
3
|
+
class Dispatcher
|
4
|
+
def initialize(tasks)
|
5
|
+
@tasks = tasks
|
6
|
+
end
|
7
|
+
|
8
|
+
def perform
|
9
|
+
@tasks.each do |task|
|
10
|
+
Rails.logger.info "***** Dispatching task for handler class #{task.class.name}"
|
11
|
+
task.call
|
12
|
+
end
|
13
|
+
RequestQueue.all.each do |queue|
|
14
|
+
queue.start
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
@@ -0,0 +1,549 @@
|
|
1
|
+
module Fragmentary
|
2
|
+
|
3
|
+
module Fragment
|
4
|
+
|
5
|
+
def self.base_class
|
6
|
+
@base_class
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.included(base)
|
10
|
+
|
11
|
+
@base_class = base
|
12
|
+
|
13
|
+
base.class_eval do
|
14
|
+
include ActionView::Helpers::CacheHelper
|
15
|
+
|
16
|
+
belongs_to :parent, :class_name => name
|
17
|
+
belongs_to :root, :class_name => name
|
18
|
+
has_many :children, :class_name => name, :foreign_key => :parent_id, :dependent => :destroy
|
19
|
+
belongs_to :user
|
20
|
+
|
21
|
+
# Don't touch the parent when we create the child - the child was created by
|
22
|
+
# renderng the parent, which occured because the parent was touched, thus
|
23
|
+
# triggering the current request. Touching it again would result in a
|
24
|
+
# redundant duplicate request.
|
25
|
+
after_commit :touch_parent, :on => [:update, :destroy]
|
26
|
+
|
27
|
+
attr_accessible :parent_id, :root_id, :record_id, :user_id, :user_type, :key
|
28
|
+
|
29
|
+
attr_accessor :indexed_children
|
30
|
+
|
31
|
+
validate :root_id, :presence => true
|
32
|
+
|
33
|
+
cache_timestamp_format = :usec # Not be needed for Rails 5, which uses :usec by default.
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
base.instance_eval do
|
38
|
+
class << self; attr_writer :record_type, :key_name; end
|
39
|
+
end
|
40
|
+
|
41
|
+
base.extend ClassMethods
|
42
|
+
|
43
|
+
ActionView::Base.send :include, FragmentsHelper
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
# Class Methods
|
48
|
+
# -------------
|
49
|
+
module ClassMethods
|
50
|
+
|
51
|
+
def root(options)
|
52
|
+
if options[:type].constantize.requestable?
|
53
|
+
klass, search_attributes, options = base_class.attributes(options)
|
54
|
+
fragment = klass.where(search_attributes).includes(:children).first_or_initialize(options); fragment.save if fragment.new_record?
|
55
|
+
fragment.set_indexed_children if fragment.child_search_key
|
56
|
+
fragment
|
57
|
+
else
|
58
|
+
raise RangeError, "#{options[:type]} is not a root fragment class"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Each fragment record is unique by type and parent_id (which is nil for a root_fragment) and for some types also by
|
63
|
+
# record_id (i.e. for root fragments for pages associated with particular AR records and for child fragments that
|
64
|
+
# appear in a list) user_type (e.g. "admin", "signed_in", "signed_out") and user_id (for fragments that include
|
65
|
+
# user-specific content).
|
66
|
+
def attributes(options)
|
67
|
+
klass = options.delete(:type).constantize
|
68
|
+
|
69
|
+
# Augment the options with the user_type and user_id in case they are needed below
|
70
|
+
options.reverse_merge!(:user_type => klass.user_type(user = options.delete(:user)), :user_id => user.try(:id))
|
71
|
+
|
72
|
+
# Collect the attributes to be used when searching for an existing fragment. Fragments are unique by these values.
|
73
|
+
search_attributes = {}
|
74
|
+
|
75
|
+
parent_id = options.delete(:parent_id)
|
76
|
+
search_attributes.merge!(:parent_id => parent_id) if parent_id
|
77
|
+
|
78
|
+
[:record_id, :user_id, :user_type, :key].each do |attribute_name|
|
79
|
+
if klass.needs?(attribute_name)
|
80
|
+
option_name = (attribute_name == :key and klass.key_name) ? klass.key_name : attribute_name
|
81
|
+
attribute = options.delete(option_name) {puts caller(0); raise ArgumentError, "Fragment type #{klass} needs a #{option_name.to_s}"}
|
82
|
+
attribute = attribute.try :to_s if attribute_name == :key
|
83
|
+
search_attributes.merge!(attribute_name => attribute)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# If :user_id or :user_name aren't required, don't include them when we create a new fragment record.
|
88
|
+
options.delete(:user_id); options.delete(:user_type)
|
89
|
+
|
90
|
+
return klass, search_attributes, options
|
91
|
+
end
|
92
|
+
|
93
|
+
def cache_store
|
94
|
+
@@cache_store ||= Rails.application.config.action_controller.cache_store
|
95
|
+
end
|
96
|
+
|
97
|
+
def existing(options)
|
98
|
+
options.merge!(:type => name) unless self == base_class
|
99
|
+
raise ArgumentError, "A 'type' attribute is needed in order to retrieve a fragment" unless options[:type]
|
100
|
+
klass, search_attributes, options = base_class.attributes(options)
|
101
|
+
# We merge options because it may include :record_id, which may be needed for uniqueness even
|
102
|
+
# for classes that don't 'need_record_id' if the parent_id isn't available.
|
103
|
+
fragment = klass.where(search_attributes.merge(options)).includes(:children).first
|
104
|
+
# Unlike Fragment.root and Fragment#child we don't instantiate a record if none is found,
|
105
|
+
# so fragment may be nil.
|
106
|
+
fragment.try :set_indexed_children if fragment.try :child_search_key
|
107
|
+
fragment
|
108
|
+
end
|
109
|
+
|
110
|
+
def fragment_type
|
111
|
+
self
|
112
|
+
end
|
113
|
+
|
114
|
+
def request_queues
|
115
|
+
@@request_queues ||= Hash.new do |hash, user_type|
|
116
|
+
hash[user_type] = RequestQueue.new(user_type)
|
117
|
+
end
|
118
|
+
if self == base_class
|
119
|
+
@@request_queues
|
120
|
+
else
|
121
|
+
return nil unless new.requestable?
|
122
|
+
user_types.each_with_object({}){|user_type, queues| queues[user_type] = @@request_queues[user_type]}
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def remove_queued_request(user:, record_id:)
|
127
|
+
request_queues[user_type(user)].remove_path(request_path(record_id))
|
128
|
+
end
|
129
|
+
|
130
|
+
def subscriber
|
131
|
+
@subscriber ||= Subscriber.new(self)
|
132
|
+
end
|
133
|
+
|
134
|
+
def needs?(attribute_name)
|
135
|
+
attribute_name = attribute_name.to_s if attribute_name.is_a? Symbol
|
136
|
+
raise ArgumentError unless attribute_name.is_a? String
|
137
|
+
send :"needs_#{attribute_name.to_s}?"
|
138
|
+
end
|
139
|
+
|
140
|
+
# If a class declares 'needs_user_id', a user_id value must be provided in the attributes hash in order to either
|
141
|
+
# create or retrieve a Fragment of that class. A user_id is needed for example when caching user-specific content
|
142
|
+
# such as a user profile. When the fragment is instantiated using FragmentsHelper methods 'cache_fragment' or
|
143
|
+
# 'CacheBuilder.cache_child', a :user option is added to the options hash automatically from the value of 'current_user'.
|
144
|
+
# The user_id is extracted from this option in Fragment.find_or_create.
|
145
|
+
def needs_user_id
|
146
|
+
self.extend NeedsUserId
|
147
|
+
end
|
148
|
+
|
149
|
+
# If a class declares 'needs_user_type', a user_type value must be provided in the attributes hash in order to either
|
150
|
+
# create or retrieve a Fragment of that class. A user_type is needed to distinguish between fragments that are rendered
|
151
|
+
# differently depending on the type of user, e.g. to distinguish between content seen by signed in users and those not
|
152
|
+
# signed in. When the fragment is instantiated using FragmentsHelper methods 'cache_fragment' or 'CacheBuilder.cache_child',
|
153
|
+
# a :user option is added to the options hash automatically from the value of 'current_user'. The user_type is extracted
|
154
|
+
# from this option in Fragment.find_or_create.
|
155
|
+
def needs_user_type
|
156
|
+
self.extend NeedsUserType
|
157
|
+
end
|
158
|
+
|
159
|
+
def needs_key(options = {})
|
160
|
+
extend NeedsKey
|
161
|
+
if name = options.delete(:name) || options.delete(:key_name)
|
162
|
+
self.key_name = name.to_sym
|
163
|
+
define_method(key_name) {send(:key)}
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def key_name
|
168
|
+
@key_name ||= nil
|
169
|
+
end
|
170
|
+
|
171
|
+
# If a class declares 'needs_record_id', a record_id value must be provided in the attributes hash in order to either
|
172
|
+
# create or retrieve a Fragment of that class. Ordinarily a record_id is passed automatically from a parent fragment
|
173
|
+
# to its child. However if the child fragment class is declared with 'needs_record_id' the parent's record_id is not
|
174
|
+
# passed on and must be provided explicitly, typically for Fragment classes that represent items in a list that
|
175
|
+
# each correspond to a particular record of some ActiveRecord class. In these cases the record_id should be provided
|
176
|
+
# explicitly in the call to cache_fragment (for a root fragment) or cache_child (for a child fragment).
|
177
|
+
def needs_record_id(options = {})
|
178
|
+
self.extend NeedsRecordId
|
179
|
+
if record_type = options.delete(:record_type) || options.delete(:type)
|
180
|
+
set_record_type(record_type)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def record_type
|
185
|
+
raise ArgumentError, "The #{self.name} class has no record_type" unless @record_type
|
186
|
+
@record_type
|
187
|
+
end
|
188
|
+
|
189
|
+
# A subclass of a class declared with 'needs_record_id' will not have a record_type unless set explicitly, which can be done
|
190
|
+
# using the following method.
|
191
|
+
def set_record_type(type)
|
192
|
+
if needs_record_id?
|
193
|
+
self.record_type = type
|
194
|
+
if record_type_subscription = subscriber.subscriptions[record_type]
|
195
|
+
# Set a callback on the eigenclass of an individual subscription to clean up client fragments
|
196
|
+
# corresponding to a destroyed AR record. Note that this assumes that ALL fragments of a class
|
197
|
+
# that calls this method should be removed if those fragments have a record_id matching the id
|
198
|
+
# of the destroyed AR record. Also note that the call 'subscriber.subscriptions' above ensures that
|
199
|
+
# the subscription exists even if the particular fragment subclass doesn't explicitly subscribe
|
200
|
+
# to the record_type AR class. And note that if the fragment subclass does subscribe to the
|
201
|
+
# record_type class, the callback doesn't affect the execution of any delete handler defined
|
202
|
+
# by the fragment.
|
203
|
+
class << record_type_subscription
|
204
|
+
set_callback :after_destroy, :after, ->{subscriber.client.remove_fragments_for_record(record.id)}
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
def record
|
209
|
+
record_type.constantize.find(record_id)
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
def remove_fragments_for_record(record_id)
|
215
|
+
where(:record_id => record_id).each(&:destroy)
|
216
|
+
end
|
217
|
+
|
218
|
+
def needs_record_id?
|
219
|
+
false
|
220
|
+
end
|
221
|
+
|
222
|
+
def needs_user_id?
|
223
|
+
false
|
224
|
+
end
|
225
|
+
|
226
|
+
def user_types
|
227
|
+
['signed_in']
|
228
|
+
end
|
229
|
+
|
230
|
+
# This default definition can be overridden by sub-classes as required
|
231
|
+
# (typically in root fragment classes by calling needs_user_type).
|
232
|
+
def user_type(user)
|
233
|
+
user ? "signed_in" : "signed_out"
|
234
|
+
end
|
235
|
+
|
236
|
+
def needs_user_type?
|
237
|
+
false
|
238
|
+
end
|
239
|
+
|
240
|
+
def needs_key?
|
241
|
+
false
|
242
|
+
end
|
243
|
+
|
244
|
+
# Note that fragments matching the specified attributes won't always exist, e.g. if the page they are to appear on
|
245
|
+
# hasn't yet been requested, e.g. an assumption created on an article page won't necessarily have been rendered on the
|
246
|
+
# opinion analysis page.
|
247
|
+
def touch_fragments_for_record(record_id)
|
248
|
+
fragments_for_record(record_id).each(&:touch)
|
249
|
+
end
|
250
|
+
|
251
|
+
def fragments_for_record(record_id)
|
252
|
+
self.where(:record_id => record_id)
|
253
|
+
end
|
254
|
+
|
255
|
+
def subscribe_to(publisher, &block)
|
256
|
+
subscriber.subscribe_to(publisher, block)
|
257
|
+
end
|
258
|
+
|
259
|
+
def child_search_key
|
260
|
+
nil
|
261
|
+
end
|
262
|
+
|
263
|
+
def queue_request(*args)
|
264
|
+
puts " queue request for #{self.name}: #{args.inspect}"
|
265
|
+
if r = request(*args)
|
266
|
+
request_queues.each{|key, queue| queue << r}
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
def requestable?
|
271
|
+
respond_to? :request_path
|
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
|
281
|
+
end
|
282
|
+
|
283
|
+
# The instance method 'request_method' is defined in terms of this.
|
284
|
+
def request_method
|
285
|
+
:get
|
286
|
+
end
|
287
|
+
|
288
|
+
def request_parameters(*args)
|
289
|
+
nil
|
290
|
+
end
|
291
|
+
|
292
|
+
# The instance method 'request_options' is defined in terms of this.
|
293
|
+
def request_options
|
294
|
+
nil
|
295
|
+
end
|
296
|
+
|
297
|
+
# This method defines the handler for the creation of new list items. The method takes:
|
298
|
+
# - members: a symbol representing the association class whose records define membership
|
299
|
+
# of the list,
|
300
|
+
# - list_record: an association that when applied to a membership record identifies the record_id
|
301
|
+
# associated with the list itself. This can be specified in the form of a symbol representing
|
302
|
+
# a method to be applied to the membership association or a proc that takes the membership
|
303
|
+
# association as an argument.
|
304
|
+
def acts_as_list_fragment(members:, list_record:, **options)
|
305
|
+
# The name of the association that defines elements of the list
|
306
|
+
@members = members.to_s.singularize
|
307
|
+
# And the corresponding class
|
308
|
+
@membership_class = @members.classify.constantize
|
309
|
+
# A method (in the form of a symbol) or proc that returns the id of the record that identifies
|
310
|
+
# the list fragment instance for a given member.
|
311
|
+
@list_record = list_record
|
312
|
+
|
313
|
+
# Identifies the record_ids of list fragments associated with a specific membership association.
|
314
|
+
# This method will be called from the block passed to 'subscribe_to' below, which is executed
|
315
|
+
# against the Subscriber, but sends missing methods back to its client, which is this class.
|
316
|
+
# A ListFragment is not declared with 'needs_record_id'; by default it receives its record_id
|
317
|
+
# from its parent fragment.
|
318
|
+
def list_record(association)
|
319
|
+
if @list_record.is_a? Symbol
|
320
|
+
association.send @list_record
|
321
|
+
elsif @list_record.is_a? Proc
|
322
|
+
@list_record.call(association)
|
323
|
+
end
|
324
|
+
end
|
325
|
+
|
326
|
+
if options.delete(:delay) == true
|
327
|
+
# Note that the following assumes that @list_record is a symbol
|
328
|
+
instance_eval <<-HEREDOC
|
329
|
+
class #{self.name}::Create#{@membership_class}Handler < Fragmentary::Handler
|
330
|
+
def call
|
331
|
+
association = @args
|
332
|
+
#{self.name}.touch_fragments_for_record(association[:#{@list_record.to_s}])
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
subscribe_to #{@membership_class} do
|
337
|
+
def create_#{@members}_successful(association)
|
338
|
+
#{self.name}::Create#{@membership_class}Handler.create(association.to_h)
|
339
|
+
end
|
340
|
+
end
|
341
|
+
HEREDOC
|
342
|
+
else
|
343
|
+
instance_eval <<-HEREDOC
|
344
|
+
subscribe_to #{@membership_class} do
|
345
|
+
def create_#{@members}_successful(association)
|
346
|
+
touch_fragments_for_record(list_record(association))
|
347
|
+
end
|
348
|
+
end
|
349
|
+
HEREDOC
|
350
|
+
end
|
351
|
+
|
352
|
+
instance_eval <<-HEREDOC
|
353
|
+
def self.child_search_key
|
354
|
+
:record_id
|
355
|
+
end
|
356
|
+
HEREDOC
|
357
|
+
end
|
358
|
+
|
359
|
+
end # ClassMethods
|
360
|
+
|
361
|
+
|
362
|
+
# Instance Methods
|
363
|
+
# ----------------
|
364
|
+
|
365
|
+
def child_search_key
|
366
|
+
self.class.child_search_key
|
367
|
+
end
|
368
|
+
|
369
|
+
def set_indexed_children
|
370
|
+
return unless child_search_key
|
371
|
+
obj = Hash.new {|h, indx| h[indx] = []}
|
372
|
+
@indexed_children = children.each_with_object(obj) {|child, collection| collection[child.send(child_search_key)] << child }
|
373
|
+
end
|
374
|
+
|
375
|
+
def existing_child(options)
|
376
|
+
child(options.merge(:existing => true))
|
377
|
+
end
|
378
|
+
|
379
|
+
# Note that this method can be called in two different contexts. One is as part of rendering the parent fragment,
|
380
|
+
# which means that the parent was obtained using either Fragment.root or a previous invocation of this method.
|
381
|
+
# In this case, the children will have already been loaded and indexed. The second is when the child is being
|
382
|
+
# rendered on its own, e.g. inserted by ajax into a parent that is already on the page. In this case the
|
383
|
+
# children won't have already been loaded or indexed.
|
384
|
+
def child(options)
|
385
|
+
begin
|
386
|
+
existing = options.delete(:existing)
|
387
|
+
# root_id and parent_id are passed from parent to child. For all except root fragments, root_id is stored explicitly.
|
388
|
+
derived_options = {:root_id => root_id || id}
|
389
|
+
# record_id is passed from parent to child unless it is required to be provided explicitly.
|
390
|
+
derived_options.merge!(:record_id => record_id) unless options[:type].constantize.needs_record_id?
|
391
|
+
klass, search_attributes, options = Fragment.base_class.attributes(options.reverse_merge(derived_options))
|
392
|
+
|
393
|
+
# Try to find the child within the children loaded previously
|
394
|
+
select_attributes = search_attributes.merge(:type => klass.name)
|
395
|
+
if child_search_key and keyed_children = indexed_children.try(:[], select_attributes[child_search_key])
|
396
|
+
# If the key was found we don't need to include it in the attributes used for the final selection
|
397
|
+
select_attributes.delete(child_search_key)
|
398
|
+
end
|
399
|
+
|
400
|
+
# If there isn't a key or there isn't set of previously indexed_children (e.g. the child is being rendered
|
401
|
+
# on its own), we just revert to the regular children association.
|
402
|
+
fragment = (keyed_children || children).to_a.find{|child| select_attributes.all?{|key, value| child.send(key) == value}}
|
403
|
+
|
404
|
+
# If we didn't find an existing child, create a new record unless only an existing record was requested
|
405
|
+
unless fragment or existing
|
406
|
+
fragment = klass.new(search_attributes.merge(options))
|
407
|
+
children << fragment # Saves the fragment and sets the parent_id attribute
|
408
|
+
end
|
409
|
+
|
410
|
+
# Load the grandchildren, so they'll each be available later. Index them if a search key is available.
|
411
|
+
if fragment
|
412
|
+
fragment_children = fragment.children
|
413
|
+
fragment.set_indexed_children if fragment.child_search_key
|
414
|
+
end
|
415
|
+
|
416
|
+
fragment
|
417
|
+
rescue => e
|
418
|
+
Rails.logger.error e.message + "\n " + e.backtrace.join("\n ")
|
419
|
+
end
|
420
|
+
end
|
421
|
+
|
422
|
+
# If this fragment's class needs a record_id, it will also have a record_type. If not, we copy the record_id from
|
423
|
+
# the parent, if it has one.
|
424
|
+
def record_type
|
425
|
+
@record_type ||= self.class.needs_record_id? ? self.class.record_type : self.parent.record_type
|
426
|
+
end
|
427
|
+
|
428
|
+
# Though each fragment is typically associated with a particular user_type, touching a root fragment will send
|
429
|
+
# page requests for the path associated with the fragment to queues for all relevant user_types for this fragment class.
|
430
|
+
def request_queues
|
431
|
+
self.class.request_queues
|
432
|
+
end
|
433
|
+
|
434
|
+
def cache_store
|
435
|
+
self.class.cache_store
|
436
|
+
end
|
437
|
+
|
438
|
+
def touch(*args, no_request: false)
|
439
|
+
request_queues.each{|key, queue| queue << request} if request && !no_request
|
440
|
+
super(*args)
|
441
|
+
end
|
442
|
+
|
443
|
+
def destroy(options = {})
|
444
|
+
options.delete(:delete_matches) ? delete_matched_cache : delete_cache
|
445
|
+
super()
|
446
|
+
end
|
447
|
+
|
448
|
+
def delete_matched_cache
|
449
|
+
cache_store.delete_matched(Regexp.new("#{self.class.model_name.cache_key}/#{id}"))
|
450
|
+
end
|
451
|
+
|
452
|
+
def delete_cache
|
453
|
+
cache_store.delete(ActiveSupport::Cache.expand_cache_key(self, 'views'))
|
454
|
+
end
|
455
|
+
|
456
|
+
def touch_tree(no_request: false)
|
457
|
+
children.each{|child| child.touch_tree(:no_request => no_request)}
|
458
|
+
# If there are children, we'll have already touched this fragment in the process of touching them.
|
459
|
+
touch(:no_request => no_request) unless children.any?
|
460
|
+
end
|
461
|
+
|
462
|
+
def touch_or_destroy
|
463
|
+
puts " touch_or_destroy #{self.class.name} #{id}"
|
464
|
+
if cache_exist?
|
465
|
+
children.each(&:touch_or_destroy)
|
466
|
+
touch(:no_request => true) unless children.any?
|
467
|
+
else
|
468
|
+
destroy # will also destroy all children because of :dependent => :destroy
|
469
|
+
end
|
470
|
+
end
|
471
|
+
|
472
|
+
def cache_exist?
|
473
|
+
# expand_cache_key calls cache_key and prepends "views/"
|
474
|
+
cache_store.exist?(ActiveSupport::Cache.expand_cache_key(self, 'views'))
|
475
|
+
end
|
476
|
+
|
477
|
+
# Request-related methods...
|
478
|
+
# Note: subclasses that define request_path need to also define self.request_path and should define
|
479
|
+
# the instance method in terms of the class method. Likewise, those that define request_parameters
|
480
|
+
# also need to defined self.request_parameters and define the instance method in terms of the class method.
|
481
|
+
# Subclasses generally don't need to define request_method or request_options, but may need to define
|
482
|
+
# self.request_options. The instance method version request_options is defined in terms of the class method
|
483
|
+
# below.
|
484
|
+
#
|
485
|
+
# Also... subclasses that define request_path also need to define self.request, but not the instance method
|
486
|
+
# request since that is defined below in terms of its constituent request arguments. The reason is that the
|
487
|
+
# class method self.request generally takes a parameter (e.g. a record_id or a key), and this is used in
|
488
|
+
# different ways depending on the class, whereas the instance method takes the same form regardless of the class.
|
489
|
+
def request_method
|
490
|
+
self.class.request_method
|
491
|
+
end
|
492
|
+
|
493
|
+
def request_parameters
|
494
|
+
self.class.request_parameters # -> nil
|
495
|
+
end
|
496
|
+
|
497
|
+
def request_options
|
498
|
+
self.class.request_options
|
499
|
+
end
|
500
|
+
|
501
|
+
def requestable?
|
502
|
+
@requestable ||= respond_to? :request_path
|
503
|
+
end
|
504
|
+
|
505
|
+
# Returns a Request object that can be used to send a server request for the fragment content
|
506
|
+
def request
|
507
|
+
requestable? ? @request ||= Request.new(request_method, request_path, request_parameters, request_options) : nil
|
508
|
+
end
|
509
|
+
|
510
|
+
private
|
511
|
+
def touch_parent
|
512
|
+
parent.try :touch unless previous_changes["memo"]
|
513
|
+
end
|
514
|
+
|
515
|
+
module NeedsRecordId
|
516
|
+
# needs_record_id means we don't inherit the record_id from the parent
|
517
|
+
def needs_record_id?
|
518
|
+
true
|
519
|
+
end
|
520
|
+
end
|
521
|
+
|
522
|
+
module NeedsUserId
|
523
|
+
def needs_user_id?
|
524
|
+
true
|
525
|
+
end
|
526
|
+
end
|
527
|
+
|
528
|
+
module NeedsUserType
|
529
|
+
def needs_user_type?
|
530
|
+
true
|
531
|
+
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
|
+
end
|
541
|
+
|
542
|
+
module NeedsKey
|
543
|
+
def needs_key?
|
544
|
+
true
|
545
|
+
end
|
546
|
+
end
|
547
|
+
end
|
548
|
+
|
549
|
+
end
|