Class: AutoForme::Models::Sequel

Inherits:
AutoForme::Model show all
Defined in:
lib/autoforme/models/sequel.rb

Overview

Sequel specific model class for AutoForme

Constant Summary collapse

S =

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

::Sequel
SUPPORTED_ASSOCIATION_TYPES =

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

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

Constants inherited from AutoForme::Model

AutoForme::Model::AUTOCOMPLETE_TYPES, AutoForme::Model::DEFAULT_LIMIT, AutoForme::Model::DEFAULT_SUPPORTED_ACTIONS, AutoForme::Model::DEFAULT_TABLE_CLASS

Instance Attribute Summary

Attributes inherited from AutoForme::Model

#framework, #model, #opts

Instance Method Summary collapse

Methods inherited from AutoForme::Model

#associated_model_class, #associated_object_display_name, #association_links_for, #autocomplete_options_for, #before_action_hook, #class_name, #column_options_for, #column_value, #columns_for, #default_object_display_name, #destroy, #display_name_for, #eager_for, #eager_graph_for, #filter_for, for, #form_attributes_for, #form_options_for, #hook, #inline_mtm_assocs, #lazy_load_association_links?, #limit_for, #link, #mtm_association_select_options, #new, #object_display_name, #order_for, #page_footer_for, #page_header_for, #redirect_for, #select_options, #supported_action?, #supported_mtm_edit?, #supported_mtm_update?, #table_class_for

Methods included from OptsAttributes

#opts_attribute

Constructor Details

#initializeSequel

Make sure the forme plugin is loaded into the model.



12
13
14
15
# File 'lib/autoforme/models/sequel.rb', line 12

def initialize(*)
  super
  @model.plugin :forme
end

Instance Method Details

#all_rows_for(type, request) ⇒ Object

Retrieve all matching rows for this model.



132
133
134
# File 'lib/autoforme/models/sequel.rb', line 132

def all_rows_for(type, request)
  all_dataset_for(type, request).all
end

#apply_associated_eager(type, request, ds) ⇒ Object

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.



205
206
207
208
209
210
211
212
213
214
215
216
217
# File 'lib/autoforme/models/sequel.rb', line 205

def apply_associated_eager(type, request, ds)
  columns_for(type, request).each do |col|
    if association?(col)
      if model = associated_model_class(col)
        eager = model.eager_for(:association, request) || model.eager_graph_for(:association, request)
        ds = ds.eager(col=>eager)
      else
        ds = ds.eager(col)
      end
    end
  end
  ds
end

#apply_dataset_options(type, request, ds) ⇒ Object

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



233
234
235
236
237
238
239
240
241
242
243
244
245
# File 'lib/autoforme/models/sequel.rb', line 233

def apply_dataset_options(type, request, ds)
  ds = apply_filter(type, request, ds)
  if order = order_for(type, request)
    ds = ds.order(*order)
  end
  if eager = eager_for(type, request)
    ds = ds.eager(eager)
  end
  if eager_graph = eager_graph_for(type, request)
    ds = ds.eager_graph(eager_graph)
  end
  ds
end

#apply_filter(type, request, ds) ⇒ Object

Apply the model’s filter to the given dataset



225
226
227
228
229
230
# File 'lib/autoforme/models/sequel.rb', line 225

def apply_filter(type, request, ds)
  if filter = filter_for
    ds = filter.call(ds, type, request)
  end
  ds
end

#associated_class(assoc) ⇒ Object

The associated class for the given association



68
69
70
# File 'lib/autoforme/models/sequel.rb', line 68

def associated_class(assoc)
  model.association_reflection(assoc).associated_class
end

#associated_mtm_objects(request, assoc, obj) ⇒ Object

The currently associated many to many objects for the association



307
308
309
310
311
312
313
# File 'lib/autoforme/models/sequel.rb', line 307

def associated_mtm_objects(request, assoc, obj)
  ds = obj.send("#{assoc}_dataset")
  if assoc_class = associated_model_class(assoc)
    ds = assoc_class.apply_dataset_options(:association, request, ds)
  end
  ds
end

#associated_new_column_values(obj, assoc) ⇒ Object

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



94
95
96
97
# File 'lib/autoforme/models/sequel.rb', line 94

def associated_new_column_values(obj, assoc)
  ref = model.association_reflection(assoc)
  ref[:keys].zip(ref[:primary_keys].map{|k| obj.send(k)})
end

#association?(column) ⇒ Boolean

Whether the column represents an association.

Returns:

  • (Boolean)


58
59
60
61
62
63
64
65
# File 'lib/autoforme/models/sequel.rb', line 58

def association?(column)
  case column
  when String
    model.associations.map{|x| x.to_s}.include?(column)
  else
    model.association_reflection(column)
  end
end

#association_autocomplete?(assoc, request) ⇒ Boolean

Whether to autocomplete for the given association.

Returns:

  • (Boolean)


248
249
250
# File 'lib/autoforme/models/sequel.rb', line 248

def association_autocomplete?(assoc, request)
  (c = associated_model_class(assoc)) && c.autocomplete_options_for(:association, request)
end

#association_key(assoc) ⇒ Object

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



88
89
90
# File 'lib/autoforme/models/sequel.rb', line 88

def association_key(assoc)
  model.association_reflection(assoc)[:key]
end

#association_names(types = SUPPORTED_ASSOCIATION_TYPES) ⇒ Object

Array of association name strings for given association types



105
106
107
# File 'lib/autoforme/models/sequel.rb', line 105

def association_names(types=SUPPORTED_ASSOCIATION_TYPES)
  model.all_association_reflections.select{|r| types.include?(r[:type])}.map{|r| r[:name]}.sort_by{|n| n.to_s}
end

#association_type(assoc) ⇒ Object

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.



76
77
78
79
80
81
82
83
84
85
# File 'lib/autoforme/models/sequel.rb', line 76

def association_type(assoc)
  case model.association_reflection(assoc)[:type]
  when :many_to_one, :one_to_one
    :one
  when :one_to_many
    :new
  when :many_to_many
    :edit
  end
end

#autocomplete(opts = {}) ⇒ Object

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)



259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
# File 'lib/autoforme/models/sequel.rb', line 259

def autocomplete(opts={})
  type, request, assoc, query, exclude = opts.values_at(:type, :request, :association, :query, :exclude)
  if assoc
    if exclude && association_type(assoc) == :edit
      ref = model.association_reflection(assoc)
      block = lambda do |ds|
        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]))
      end
    end
    return associated_model_class(assoc).autocomplete(opts.merge(:type=>:association, :association=>nil), &block)
  end
  opts = autocomplete_options_for(type, request)
  callback_opts = {:type=>type, :request=>request, :query=>query}
  ds = all_dataset_for(type, request)
  ds = opts[:callback].call(ds, callback_opts) if opts[:callback]
  display = opts[:display] || S.qualify(model.table_name, :name)
  display = display.call(callback_opts) if display.respond_to?(:call)
  limit = opts[:limit] || 10
  limit = limit.call(callback_opts) if limit.respond_to?(:call)
  opts[:filter] ||= lambda{|ds, opts| ds.where(S.ilike(display, "%#{ds.escape_like(query)}%"))}
  ds = opts[:filter].call(ds, callback_opts)
  ds = ds.select(S.join([S.qualify(model.table_name, model.primary_key), display], ' - ').as(:v)).
    limit(limit)
  ds = yield ds if block_given?
  ds.map(:v)
end

#base_classObject

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



18
19
20
# File 'lib/autoforme/models/sequel.rb', line 18

def base_class
  S::Model
end

#browse(type, request) ⇒ Object

Return array of matching objects for the current page.



185
186
187
# File 'lib/autoforme/models/sequel.rb', line 185

def browse(type, request)
  paginate(type, request, apply_associated_eager(:browse, request, all_dataset_for(type, request)))
end

#column_type(column) ⇒ Object

The schema type for the column



220
221
222
# File 'lib/autoforme/models/sequel.rb', line 220

def column_type(column)
  (sch = model.db_schema[column]) && sch[:type]
end

#default_columnsObject

Return the default columns for this model



137
138
139
140
141
142
143
144
145
146
# File 'lib/autoforme/models/sequel.rb', line 137

def default_columns
  columns = model.columns - Array(model.primary_key)
  model.all_association_reflections.each do |reflection|
    next unless reflection[:type] == :many_to_one
    if i = columns.index(reflection[:key])
      columns[i] = reflection[:name]
    end
  end
  columns.sort_by{|s| s.to_s}
end

#form_param_name(assoc) ⇒ Object

The name of the form param for the given association.



28
29
30
# File 'lib/autoforme/models/sequel.rb', line 28

def form_param_name(assoc)
  "#{model.send(:underscore, model.name)}[#{association_key(assoc)}]"
end

#mtm_association_namesObject

Array of many to many association name strings.



100
101
102
# File 'lib/autoforme/models/sequel.rb', line 100

def mtm_association_names
  association_names([:many_to_many])
end

#mtm_update(request, assoc, obj, add, remove) ⇒ Object

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



288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
# File 'lib/autoforme/models/sequel.rb', line 288

def mtm_update(request, assoc, obj, add, remove)
  ref = model.association_reflection(assoc)
  assoc_class = associated_model_class(assoc)
  ret = nil
  model.db.transaction do
    [[add, ref.add_method], [remove, ref.remove_method]].each do |ids, meth|
      if ids
        ids.each do |id|
          next if id.to_s.empty?
          ret = assoc_class ? assoc_class.with_pk(:association, request, id) : obj.send(:_apply_association_options, ref, ref.associated_class.dataset.clone).with_pk!(id)
          obj.send(meth, ret)
        end
      end
    end
  end
  ret
end

#new_searchObject

A completely empty search object, with no defaults.



23
24
25
# File 'lib/autoforme/models/sequel.rb', line 23

def new_search
  @model.call({})
end

#paginate(type, request, ds) ⇒ Object

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.



191
192
193
194
195
196
197
198
199
200
201
# File 'lib/autoforme/models/sequel.rb', line 191

def paginate(type, request, ds)
  limit = limit_for(type, request)
  offset = ((request.id.to_i||1)-1) * limit
  objs = ds.limit(limit+1, (offset if offset > 0)).all
  next_page = false
  if objs.length > limit
    next_page = true
    objs.pop
  end
  [next_page, objs]
end

#params_nameObject

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



122
123
124
# File 'lib/autoforme/models/sequel.rb', line 122

def params_name
  @model.send(:underscore, @model.name)
end

#primary_key_value(obj) ⇒ Object

The primary key value for the given object.



116
117
118
# File 'lib/autoforme/models/sequel.rb', line 116

def primary_key_value(obj)
  obj.pk
end

#save(obj) ⇒ Object

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



110
111
112
113
# File 'lib/autoforme/models/sequel.rb', line 110

def save(obj)
  obj.raise_on_save_failure = false
  obj.save
end

#search_results(type, request) ⇒ Object

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



161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
# File 'lib/autoforme/models/sequel.rb', line 161

def search_results(type, request)
  params = request.params
  ds = apply_associated_eager(:search, request, all_dataset_for(type, request))
  columns_for(:search_form, request).each do |c|
    if (v = params[c.to_s]) && !v.empty?
      if association?(c)
        ref = model.association_reflection(c)
        ads = ref.associated_dataset
        if model_class = associated_model_class(c)
          ads = model_class.apply_filter(:association, request, ads)
        end
        primary_key = S.qualify(ref.associated_class.table_name, ref.primary_key)
        ds = ds.where(S.qualify(model.table_name, ref[:key])=>ads.where(primary_key=>v).select(primary_key))
      elsif column_type(c) == :string
        ds = ds.where(S.ilike(S.qualify(model.table_name, c), "%#{ds.escape_like(v.to_s)}%"))
      else
        ds = ds.where(S.qualify(model.table_name, c)=>v.to_s)
      end
    end
  end
  paginate(type, request, ds)
end

#session_value(column) ⇒ Object

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.



151
152
153
154
155
156
157
158
# File 'lib/autoforme/models/sequel.rb', line 151

def session_value(column)
  filter do |ds, type, req|
    ds.where(S.qualify(model.table_name, column)=>req.session[column])
  end
  before_create do |obj, req|
    obj.send("#{column}=", req.session[column])
  end
end

#set_fields(obj, type, request, params) ⇒ Object

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



33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/autoforme/models/sequel.rb', line 33

def set_fields(obj, type, request, params)
  columns_for(type, request).each do |col|
    column = col

    if association?(col)
      ref = model.association_reflection(col)
      ds = ref.associated_dataset
      if model_class = associated_model_class(col)
        ds = model_class.apply_filter(:association, request, ds)
      end

      v = params[ref[:key].to_s]
      v = nil if v.to_s.strip == ''
      if v
        v = ds.first!(S.qualify(ds.model.table_name, ref.primary_key)=>v)
      end
    else
      v = params[col.to_s]
    end

    obj.send("#{column}=", v)
  end
end

#unassociated_mtm_objects(request, assoc, obj) ⇒ Object

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



316
317
318
319
320
321
322
323
324
325
326
327
# File 'lib/autoforme/models/sequel.rb', line 316

def unassociated_mtm_objects(request, assoc, obj)
  ref = model.association_reflection(assoc)
  assoc_class = associated_model_class(assoc)
  lambda do |ds|
    subquery = model.db.from(ref[:join_table]).
      select(ref.qualified_right_key).
      where(ref.qualified_left_key=>obj.pk)
    ds = ds.exclude(S.qualify(ref.associated_class.table_name, model.primary_key)=>subquery)
    ds = assoc_class.apply_dataset_options(:association, request, ds) if assoc_class
    ds
  end
end

#with_pk(type, request, pk) ⇒ Object

Retrieve underlying model instance with matching primary key



127
128
129
# File 'lib/autoforme/models/sequel.rb', line 127

def with_pk(type, request, pk)
  dataset_for(type, request).with_pk!(pk)
end