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, AutoForme::Model::VALID_CONSTANT_NAME_REGEXP

Instance Attribute Summary

Attributes inherited from AutoForme::Model

#framework, #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, #edit_html_for, #filter_for, for, #form_attributes_for, #form_options_for, #hook, #inline_mtm_assocs, #lazy_load_association_links?, #limit_for, #link, #model, #mtm_association_select_options, #new, #object_display_name, #order_for, #page_footer_for, #page_header_for, #redirect_for, #select_options, #show_html_for, #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.



14
15
16
17
# File 'lib/autoforme/models/sequel.rb', line 14

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

Instance Method Details

#all_rows_for(type, request) ⇒ Object

Retrieve all matching rows for this model.



134
135
136
# File 'lib/autoforme/models/sequel.rb', line 134

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.



209
210
211
212
213
214
215
216
217
218
219
220
221
# File 'lib/autoforme/models/sequel.rb', line 209

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



237
238
239
240
241
242
243
244
245
246
247
248
249
# File 'lib/autoforme/models/sequel.rb', line 237

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



229
230
231
232
233
234
# File 'lib/autoforme/models/sequel.rb', line 229

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



70
71
72
# File 'lib/autoforme/models/sequel.rb', line 70

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



311
312
313
314
315
316
317
# File 'lib/autoforme/models/sequel.rb', line 311

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



96
97
98
99
# File 'lib/autoforme/models/sequel.rb', line 96

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)


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

def association?(column)
  case column
  when String
    model.associations.map(&: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)


252
253
254
# File 'lib/autoforme/models/sequel.rb', line 252

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.



90
91
92
# File 'lib/autoforme/models/sequel.rb', line 90

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



107
108
109
# File 'lib/autoforme/models/sequel.rb', line 107

def association_names(types=SUPPORTED_ASSOCIATION_TYPES)
  model.all_association_reflections.select{|r| types.include?(r[:type])}.map{|r| r[:name]}.sort_by(&: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.



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

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)



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

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.



20
21
22
# File 'lib/autoforme/models/sequel.rb', line 20

def base_class
  S::Model
end

#browse(type, request, opts = {}) ⇒ Object

Return array of matching objects for the current page.



187
188
189
# File 'lib/autoforme/models/sequel.rb', line 187

def browse(type, request, opts={})
  paginate(type, request, apply_associated_eager(:browse, request, all_dataset_for(type, request)), opts)
end

#column_type(column) ⇒ Object

The schema type for the column



224
225
226
# File 'lib/autoforme/models/sequel.rb', line 224

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

#default_columnsObject

Return the default columns for this model



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

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(&:to_s)
end

#form_param_name(assoc) ⇒ Object

The name of the form param for the given association.



30
31
32
# File 'lib/autoforme/models/sequel.rb', line 30

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.



102
103
104
# File 'lib/autoforme/models/sequel.rb', line 102

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.



292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
# File 'lib/autoforme/models/sequel.rb', line 292

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.



25
26
27
# File 'lib/autoforme/models/sequel.rb', line 25

def new_search
  model.call({})
end

#paginate(type, request, ds, opts = {}) ⇒ 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.



193
194
195
196
197
198
199
200
201
202
203
204
205
# File 'lib/autoforme/models/sequel.rb', line 193

def paginate(type, request, ds, opts={})
  return ds.all if opts[:all_results]
  limit = limit_for(type, request)
  %r{\/(\d+)\z} =~ request.env['PATH_INFO']
  offset = (($1||1).to_i - 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.



124
125
126
# File 'lib/autoforme/models/sequel.rb', line 124

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

#primary_key_value(obj) ⇒ Object

The primary key value for the given object.



118
119
120
# File 'lib/autoforme/models/sequel.rb', line 118

def primary_key_value(obj)
  obj.pk
end

#save(obj) ⇒ Object

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



112
113
114
115
# File 'lib/autoforme/models/sequel.rb', line 112

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

#search_results(type, request, opts = {}) ⇒ Object

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



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

def search_results(type, request, opts={})
  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, opts)
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.



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

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.



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

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.



320
321
322
323
324
325
326
327
328
329
330
331
# File 'lib/autoforme/models/sequel.rb', line 320

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



129
130
131
# File 'lib/autoforme/models/sequel.rb', line 129

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