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 collapse

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.



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

def initialize(*)
  super
  model.plugin :forme
  @params_name = model.new.forme_namespace
end

Instance Attribute Details

#params_nameObject (readonly)

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



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

def params_name
  @params_name
end

Instance Method Details

#all_rows_for(type, request) ⇒ Object

Retrieve all matching rows for this model.



140
141
142
# File 'lib/autoforme/models/sequel.rb', line 140

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.



222
223
224
225
226
227
228
229
230
231
232
233
234
# File 'lib/autoforme/models/sequel.rb', line 222

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



250
251
252
253
254
255
256
257
258
259
260
261
262
# File 'lib/autoforme/models/sequel.rb', line 250

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



242
243
244
245
246
247
# File 'lib/autoforme/models/sequel.rb', line 242

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



334
335
336
337
338
339
340
# File 'lib/autoforme/models/sequel.rb', line 334

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)


265
266
267
# File 'lib/autoforme/models/sequel.rb', line 265

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. If a block is given, only include associations where the block returns truthy.



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

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



276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
# File 'lib/autoforme/models/sequel.rb', line 276

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{|ds1, _| ds1.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.



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

def base_class
  S::Model
end

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

Return array of matching objects for the current page.



200
201
202
# File 'lib/autoforme/models/sequel.rb', line 200

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



237
238
239
# File 'lib/autoforme/models/sequel.rb', line 237

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

#default_columnsObject

Return the default columns for this model



145
146
147
148
149
150
151
152
153
154
# File 'lib/autoforme/models/sequel.rb', line 145

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!
end

#editable_mtm_association_namesObject

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



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

def editable_mtm_association_names
  association_names([:many_to_many]) do |r|
    model.method_defined?(r.add_method) && model.method_defined?(r.remove_method)
  end
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)
  "#{params_name}[#{association_key(assoc)}]"
end

#mtm_association_namesObject

Array of many to many association name strings.



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

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.



305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
# File 'lib/autoforme/models/sequel.rb', line 305

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)
          begin
            model.db.transaction(:savepoint=>true){obj.send(meth, ret)}
          rescue S::UniqueConstraintViolation
            # Already added, safe to ignore
          rescue S::ConstraintViolation
            # Old versions of sqlite3 and jdbc-sqlite3 can raise generic
            # ConstraintViolation instead of UniqueConstraintViolation
            # :nocov:
            raise unless model.db.database_type == :sqlite
            # :nocov:
          end
        end
      end
    end
  end
  ret
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.



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

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

#primary_key_value(obj) ⇒ Object

The primary key value for the given object.



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

def primary_key_value(obj)
  obj.pk
end

#save(obj) ⇒ Object

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



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

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.



169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# File 'lib/autoforme/models/sequel.rb', line 169

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 = v.to_s).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)}%"))
      else
        begin
          typecasted_value = model.db.typecast_value(column_type(c), v)
        rescue S::InvalidValue
          ds = ds.where(false)
          break
        else
          ds = ds.where(S.qualify(model.table_name, c)=>typecasted_value)
        end
      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.



159
160
161
162
163
164
165
166
# File 'lib/autoforme/models/sequel.rb', line 159

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.



343
344
345
346
347
348
349
350
351
352
353
354
# File 'lib/autoforme/models/sequel.rb', line 343

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, ref.associated_class.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



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

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