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.
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -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__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -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
@@ -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