class AutoForme::Models::Sequel

  1. lib/autoforme/models/sequel.rb
Superclass: Model

Sequel specific model class for AutoForme

Constants

S = ::Sequel  

Short reference to top level Sequel module, for easily calling methods

SUPPORTED_ASSOCIATION_TYPES = [:many_to_one, :one_to_one, :one_to_many, :many_to_many]  

What association types to recognize. Other association types are ignored.

Attributes

params_name [R]

The namespace for form parameter names for this model, needs to match the ones automatically used by Forme.

Public Class methods

new(*)

Make sure the forme plugin is loaded into the model.

[show source]
   # File lib/autoforme/models/sequel.rb
18 def initialize(*)
19   super
20   model.plugin :forme
21   @params_name = model.new.forme_namespace
22 end

Public Instance methods

all_rows_for(type, request)

Retrieve all matching rows for this model.

[show source]
    # File lib/autoforme/models/sequel.rb
146 def all_rows_for(type, request)
147   all_dataset_for(type, request).all
148 end
apply_associated_eager(type, request, ds)

On the browse/search results pages, in addition to eager loading based on the current model’s eager loading config, also eager load based on the associated models config.

[show source]
    # File lib/autoforme/models/sequel.rb
249 def apply_associated_eager(type, request, ds)
250   columns_for(type, request).each do |col|
251     if association?(col)
252       if model = associated_model_class(col)
253         eager = model.eager_for(:association, request) || model.eager_graph_for(:association, request)
254         ds = ds.eager(col=>eager)
255       else
256         ds = ds.eager(col)
257       end
258     end
259   end
260   ds
261 end
apply_dataset_options(type, request, ds)

Apply the model’s filter, eager, and order to the given dataset

[show source]
    # File lib/autoforme/models/sequel.rb
277 def apply_dataset_options(type, request, ds)
278   ds = apply_filter(type, request, ds)
279   if order = order_for(type, request)
280     ds = ds.order(*order)
281   end
282   if eager = eager_for(type, request)
283     ds = ds.eager(eager)
284   end
285   if eager_graph = eager_graph_for(type, request)
286     ds = ds.eager_graph(eager_graph)
287   end
288   ds
289 end
apply_filter(type, request, ds)

Apply the model’s filter to the given dataset

[show source]
    # File lib/autoforme/models/sequel.rb
269 def apply_filter(type, request, ds)
270   if filter = filter_for
271     ds = filter.call(ds, type, request)
272   end
273   ds
274 end
associated_class(assoc)

The associated class for the given association

[show source]
   # File lib/autoforme/models/sequel.rb
70 def associated_class(assoc)
71   model.association_reflection(assoc).associated_class
72 end
associated_mtm_objects(request, assoc, obj)

The currently associated many to many objects for the association

[show source]
    # File lib/autoforme/models/sequel.rb
361 def associated_mtm_objects(request, assoc, obj)
362   ds = obj.send("#{assoc}_dataset")
363   if assoc_class = associated_model_class(assoc)
364     ds = assoc_class.apply_dataset_options(:association, request, ds)
365   end
366   ds
367 end
associated_new_column_values(obj, assoc)

An array of pairs mapping foreign keys in associated class to primary key value of current object

[show source]
   # File lib/autoforme/models/sequel.rb
96 def associated_new_column_values(obj, assoc)
97   ref = model.association_reflection(assoc)
98   ref[:keys].zip(ref[:primary_keys].map{|k| obj.send(k)})
99 end
association?(column)

Whether the column represents an association.

[show source]
   # File lib/autoforme/models/sequel.rb
60 def association?(column)
61   case column
62   when String
63     model.associations.map(&:to_s).include?(column)
64   else
65     model.association_reflection(column)
66   end
67 end
association_autocomplete?(assoc, request)

Whether to autocomplete for the given association.

[show source]
    # File lib/autoforme/models/sequel.rb
292 def association_autocomplete?(assoc, request)
293   (c = associated_model_class(assoc)) && c.autocomplete_options_for(:association, request)
294 end
association_key(assoc)

The foreign key column for the given many to one association.

[show source]
   # File lib/autoforme/models/sequel.rb
90 def association_key(assoc)
91   model.association_reflection(assoc)[:key]
92 end
association_names(types=SUPPORTED_ASSOCIATION_TYPES)

Array of association name strings for given association types. If a block is given, only include associations where the block returns truthy.

[show source]
    # File lib/autoforme/models/sequel.rb
116 def association_names(types=SUPPORTED_ASSOCIATION_TYPES)
117   model.all_association_reflections.
118     select{|r| types.include?(r[:type]) && (!block_given? || yield(r))}.
119     map{|r| r[:name]}.
120     sort_by(&:to_s)
121 end
association_type(assoc)

A short type for the association, either :one for a singular association, :new for an association where you can create new objects, or :edit for association where you can add/remove members from the association.

[show source]
   # File lib/autoforme/models/sequel.rb
78 def association_type(assoc)
79   case model.association_reflection(assoc)[:type]
80   when :many_to_one, :one_to_one
81     :one
82   when :one_to_many
83     :new
84   when :many_to_many
85     :edit
86   end
87 end
autocomplete(opts={})

Return array of autocompletion strings for the request. Options:

:type

Action type symbol

:request

AutoForme::Request instance

:association

Association symbol

:query

Query string submitted by the user

:exclude

Primary key value of current model, excluding already associated values (used when editing many to many associations)

[show source]
    # File lib/autoforme/models/sequel.rb
303 def autocomplete(opts={})
304   type, request, assoc, query, exclude = opts.values_at(:type, :request, :association, :query, :exclude)
305   if assoc
306     if exclude && association_type(assoc) == :edit
307       ref = model.association_reflection(assoc)
308       block = lambda do |ds|
309         ds.exclude(S.qualify(ref.associated_class.table_name, ref.right_primary_key)=>model.db.from(ref[:join_table]).where(ref[:left_key]=>exclude).select(ref[:right_key]))
310       end
311     end
312     return associated_model_class(assoc).autocomplete(opts.merge(:type=>:association, :association=>nil), &block)
313   end
314   opts = autocomplete_options_for(type, request)
315   callback_opts = {:type=>type, :request=>request, :query=>query}
316   ds = all_dataset_for(type, request)
317   ds = opts[:callback].call(ds, callback_opts) if opts[:callback]
318   display = opts[:display] || S.qualify(model.table_name, :name)
319   display = display.call(callback_opts) if display.respond_to?(:call)
320   limit = opts[:limit] || 10
321   limit = limit.call(callback_opts) if limit.respond_to?(:call)
322   opts[:filter] ||= lambda{|ds1, _| ds1.where(S.ilike(display, "%#{ds.escape_like(query)}%"))}
323   ds = opts[:filter].call(ds, callback_opts)
324   ds = ds.select(S.join([S.qualify(model.table_name, model.primary_key), display], ' - ').as(:v)).
325     limit(limit)
326   ds = yield ds if block_given?
327   ds.map(:v)
328 end
base_class()

The base class for the underlying model, ::Sequel::Model.

[show source]
   # File lib/autoforme/models/sequel.rb
25 def base_class
26   S::Model
27 end
browse(type, request, opts={})

Return array of matching objects for the current page.

[show source]
    # File lib/autoforme/models/sequel.rb
208 def browse(type, request, opts={})
209   paginate(type, request, apply_associated_eager(:browse, request, all_dataset_for(type, request)), opts)
210 end
column_type(column)

The schema type for the column

[show source]
    # File lib/autoforme/models/sequel.rb
264 def column_type(column)
265   (sch = model.db_schema[column]) && sch[:type]
266 end
default_columns()

Return the default columns for this model

[show source]
    # File lib/autoforme/models/sequel.rb
151 def default_columns
152   columns = model.columns - Array(model.primary_key)
153   model.all_association_reflections.each do |reflection|
154     next unless reflection[:type] == :many_to_one
155     if i = columns.index(reflection[:key])
156       columns[i] = reflection[:name]
157     end
158   end
159   columns.sort!
160 end
editable_mtm_association_names()

Array of many to many association name strings for editable many to many associations.

[show source]
    # File lib/autoforme/models/sequel.rb
103 def editable_mtm_association_names
104   association_names([:many_to_many]) do |r|
105     model.method_defined?(r.add_method) && model.method_defined?(r.remove_method)
106   end
107 end
form_param_name(assoc)

The name of the form param for the given association.

[show source]
   # File lib/autoforme/models/sequel.rb
30 def form_param_name(assoc)
31   "#{params_name}[#{association_key(assoc)}]"
32 end
mtm_association_names()

Array of many to many association name strings.

[show source]
    # File lib/autoforme/models/sequel.rb
110 def mtm_association_names
111   association_names([:many_to_many])
112 end
mtm_update(request, assoc, obj, add, remove)

Update the many to many association. add and remove should be arrays of primary key values of associated objects to add to the association.

[show source]
    # File lib/autoforme/models/sequel.rb
332 def mtm_update(request, assoc, obj, add, remove)
333   ref = model.association_reflection(assoc)
334   assoc_class = associated_model_class(assoc)
335   ret = nil
336   model.db.transaction do
337     [[add, ref.add_method], [remove, ref.remove_method]].each do |ids, meth|
338       if ids
339         ids.each do |id|
340           next if id.to_s.empty?
341           ret = assoc_class ? assoc_class.with_pk(:association, request, id) : obj.send(:_apply_association_options, ref, ref.associated_class.dataset.clone).with_pk!(id)
342           begin
343             model.db.transaction(:savepoint=>true){obj.send(meth, ret)}
344           rescue S::UniqueConstraintViolation
345             # Already added, safe to ignore
346           rescue S::ConstraintViolation
347             # Old versions of sqlite3 and jdbc-sqlite3 can raise generic
348             # ConstraintViolation instead of UniqueConstraintViolation
349             # :nocov:
350             raise unless model.db.database_type == :sqlite
351             # :nocov:
352           end
353         end
354       end
355     end
356   end
357   ret
358 end
paginate(type, request, ds, opts={})

Do very simple pagination, by selecting one more object than necessary, and noting if there is a next page by seeing if more objects are returned than the limit.

[show source]
    # File lib/autoforme/models/sequel.rb
214 def paginate(type, request, ds, opts={})
215   return ds.all if opts[:all_results]
216   limit = limit_for(type, request)
217   ds = ds.limit(limit+1)
218 
219   if pagination_strategy_for(type, request) == :filter
220     order_cols = ds.send(:hash_key_symbols, Array(order_for(type, request)).map{|c| c.is_a?(S::SQL::OrderedExpression) ? c.expression : c})
221     after = Array(request.params["_after"])
222     if order_cols.length == after.length
223       begin
224         after = order_cols.zip(after).map{|c, v| typecast_value(c, v)}
225       rescue S::InvalidValue
226         # ignore pagination, assume first page
227       else
228         ds = ds.where(ds.send(:ignore_values_preceding, {}){after})
229       end
230     end
231   else # offset strategy, the default
232     %r{\/(\d+)\z} =~ request.env['PATH_INFO']
233     offset = (($1||1).to_i - 1) * limit
234     ds = ds.offset(offset) if offset > 0
235   end
236 
237   objs = ds.all
238   next_page = order_cols && []
239   if objs.length > limit
240     objs.pop
241     last_obj = objs[-1]
242     next_page = after ? order_cols.map{|c| last_obj.send(c)} : true
243   end
244   [next_page, objs]
245 end
primary_key_value(obj)

The primary key value for the given object.

[show source]
    # File lib/autoforme/models/sequel.rb
130 def primary_key_value(obj)
131   obj.pk
132 end
save(obj)

Save the object, returning the object if successful, or nil if not.

[show source]
    # File lib/autoforme/models/sequel.rb
124 def save(obj)
125   obj.raise_on_save_failure = false
126   obj.save
127 end
search_results(type, request, opts={})

Returning array of matching objects for the current search page using the given parameters.

[show source]
    # File lib/autoforme/models/sequel.rb
175 def search_results(type, request, opts={})
176   params = request.params
177   ds = apply_associated_eager(:search, request, all_dataset_for(type, request))
178   columns_for(:search_form, request).each do |c|
179     if (v = params[c.to_s]) && !(v = v.to_s).empty?
180       if filtered_ds = column_search_filter_for(ds, c, v, request)
181         ds = filtered_ds
182       elsif association?(c)
183         ref = model.association_reflection(c)
184         ads = ref.associated_dataset
185         if model_class = associated_model_class(c)
186           ads = model_class.apply_filter(:association, request, ads)
187         end
188         primary_key = S.qualify(ref.associated_class.table_name, ref.primary_key)
189         ds = ds.where(S.qualify(model.table_name, ref[:key])=>ads.where(primary_key=>v).select(primary_key))
190       elsif column_type(c) == :string
191         ds = ds.where(S.ilike(S.qualify(model.table_name, c), "%#{ds.escape_like(v)}%"))
192       else
193         begin
194           typecasted_value = typecast_value(c, v)
195         rescue S::InvalidValue
196           ds = ds.where(false)
197           break
198         else
199           ds = ds.where(S.qualify(model.table_name, c)=>typecasted_value)
200         end
201       end
202     end
203   end
204   paginate(type, request, ds, opts)
205 end
session_value(column)

Add a filter restricting access to only rows where the column name matching the session value. Also add a before_create hook that sets the column value to the session value.

[show source]
    # File lib/autoforme/models/sequel.rb
165 def session_value(column)
166   filter do |ds, type, req|
167     ds.where(S.qualify(model.table_name, column)=>req.session[column])
168   end
169   before_create do |obj, req|
170     obj.send("#{column}=", req.session[column])
171   end
172 end
set_fields(obj, type, request, params)

Set the fields for the given action type to the object based on the request params.

[show source]
   # File lib/autoforme/models/sequel.rb
35 def set_fields(obj, type, request, params)
36   columns_for(type, request).each do |col|
37     column = col
38 
39     if association?(col)
40       ref = model.association_reflection(col)
41       ds = ref.associated_dataset
42       if model_class = associated_model_class(col)
43         ds = model_class.apply_filter(:association, request, ds)
44       end
45 
46       v = params[ref[:key].to_s]
47       v = nil if v.to_s.strip == ''
48       if v
49         v = ds.first!(S.qualify(ds.model.table_name, ref.primary_key)=>v)
50       end
51     else
52       v = params[col.to_s]
53     end
54 
55     obj.send("#{column}=", v)
56   end
57 end
unassociated_mtm_objects(request, assoc, obj)

All objects in the associated table that are not currently associated to the given object.

[show source]
    # File lib/autoforme/models/sequel.rb
370 def unassociated_mtm_objects(request, assoc, obj)
371   ref = model.association_reflection(assoc)
372   assoc_class = associated_model_class(assoc)
373   lambda do |ds|
374     subquery = model.db.from(ref[:join_table]).
375       select(ref.qualified_right_key).
376       where(ref.qualified_left_key=>obj.pk)
377     ds = ds.exclude(S.qualify(ref.associated_class.table_name, ref.associated_class.primary_key)=>subquery)
378     ds = assoc_class.apply_dataset_options(:association, request, ds) if assoc_class
379     ds
380   end
381 end
with_pk(type, request, pk)

Retrieve underlying model instance with matching primary key

[show source]
    # File lib/autoforme/models/sequel.rb
135 def with_pk(type, request, pk)
136   begin
137     pk = typecast_value(model.primary_key, pk)
138   rescue S::InvalidValue
139     raise S::NoMatchingRow
140   end
141 
142   dataset_for(type, request).with_pk!(pk)
143 end