Module: Labimotion::GenericHelpers

Extended by:
Grape::API::Helpers
Defined in:
lib/labimotion/helpers/generic_helpers.rb

Overview

Generic Helpers

Constant Summary collapse

STALE_TEMPLATE_MC =

Message code for "somebody else saved this template while you were editing it".

'sc09'
RESTORE_CONFLICT_MC =

Message code for "an active template already holds the identity you are restoring".

'sc10'
AUTHORIZATION_REFUSAL_MC =

Message code for "this template is maintained by somebody else" — the 403 a share-gated action answers with. Deliberately not the legacy 401: nobody here is unauthorised, they are asking about a colleague's template (template-sharing.md §3).

'sc11'
OWNER_ROW_MC =

Message code for "the owner row cannot be removed or downgraded — transfer instead".

'sc12'
ACCESS_ALREADY_HELD_MC =

Message code for "you already hold access to this template, so there is nothing to request". The 409 request_access answers a caller whose share row is already at viewer/editor/owner — a stale grid, not a mistake worth an error banner.

'sc13'
ACTION_PHRASE =

How a refusal names the action it refused, and which actions are owner-only for the wording. Kept as plain symbols here rather than reading ShareResolver::REQUIRED_LEVEL — the wording must not chain-load the ActiveRecord model in a process that only wanted the helpers. ShareResolver stays the enforcement authority.

{
  read: 'view', write: 'edit', release: 'release',
  deactivate: 'activate or deactivate', destroy: 'delete', manage: 'manage sharing for'
}.freeze
OWNER_ONLY_ACTIONS =
%i[release deactivate destroy manage].freeze
KLASS_IDENTITY =

What makes a klass unique among the active rows. These are model validations only — there is no DB unique index behind any of them — and restore! writes through update_columns, so nothing re-checks them on the way back in.

{
  'ElementKlass' => %i[name],
  'SegmentKlass' => %i[label element_klass_id],
  'DatasetKlass' => %i[ols_term_id]
}.freeze
KLASS_IDENTITY_NOUN =

How the refusal names the identity that is taken.

{
  'ElementKlass' => 'name',
  'SegmentKlass' => 'label',
  'DatasetKlass' => 'ontology term'
}.freeze
RESTORE_RECOVERY_WINDOW =

Seconds, symmetric around the deleted template's own deleted_at, within which a soft-deleted child counts as "deleted together with it" (see Paranoia#restore!). Wide enough to cover a slow cascade over a template with many elements, narrow enough that rows binned on their own, on another day, stay binned.

300

Instance Method Summary collapse

Instance Method Details

#ai_model_entry_id(entry) ⇒ Object



168
169
170
171
172
# File 'lib/labimotion/helpers/generic_helpers.rb', line 168

def ai_model_entry_id(entry)
  return entry.to_s unless entry.is_a?(Hash)

  (entry[:id] || entry['id']).to_s
end

#ai_provider_overrides(profile, api_key) ⇒ Object

Personal provider endpoint (base_url/api_path) — honored only ALONGSIDE a personal key, so the shared server key is never sent to a user-supplied URL. The base_url itself is SSRF-validated later, in AiTemplate.



134
135
136
137
138
139
140
141
# File 'lib/labimotion/helpers/generic_helpers.rb', line 134

def ai_provider_overrides(profile, api_key)
  return {} if api_key.blank?

  {
    base_url: (profile.labimotion_ai_base_url if profile.respond_to?(:labimotion_ai_base_url)),
    api_path: (profile.labimotion_ai_api_path if profile.respond_to?(:labimotion_ai_api_path))
  }
end

#ai_server_model_allowlistObject

Model ids the server permits on the SHARED key: the admin pick-list (:models) plus the server default (:model / KI_TOOLBOX_MODEL), read from the host config the same way Labimotion::AiTemplate does. Empty when the admin configured neither — the caller then skips the clamp (fail open), matching the gem's decoupled, non-blocking handling of overrides.



159
160
161
162
163
164
165
166
# File 'lib/labimotion/helpers/generic_helpers.rb', line 159

def ai_server_model_allowlist
  cfg = Rails.configuration.labimotion_ai || {}
  list = cfg[:models].is_a?(Array) ? cfg[:models] : []
  ids = list.map { |entry| ai_model_entry_id(entry) }
  (ids << cfg[:model].to_s << ENV['KI_TOOLBOX_MODEL'].to_s).reject(&:blank?).uniq
rescue StandardError
  []
end

#ai_shared_key_model(model) ⇒ Object

On the SHARED server key (user has no personal key), keep the user's model only if the admin allowed it; otherwise return nil so AiTemplate falls back to the server default. Returns the model unchanged when no allowlist is configured (fail open). Guards an off-list model being billed to the shared key.



147
148
149
150
151
152
# File 'lib/labimotion/helpers/generic_helpers.rb', line 147

def ai_shared_key_model(model)
  return model if model.blank?

  allow = ai_server_model_allowlist
  allow.any? && !allow.include?(model.to_s) ? nil : model
end

#ai_user_overrides(current_user) ⇒ Object

Per-user AI overrides (model + API key) resolved from the host app's user profile. Returns {} when the host doesn't expose them, so AiTemplate falls back to the server-wide yml/ENV configuration. Guarded with respond_to? to keep the gem decoupled from the host's Profile implementation. Shared by the dataset / element / segment AI helpers.



117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/labimotion/helpers/generic_helpers.rb', line 117

def ai_user_overrides(current_user)
  profile = current_user.respond_to?(:profile) ? current_user.profile : nil
  return {} if profile.nil?

  model   = (profile.labimotion_ai_model if profile.respond_to?(:labimotion_ai_model))
  api_key = (profile.labimotion_ai_api_key if profile.respond_to?(:labimotion_ai_api_key))
  model   = ai_shared_key_model(model) if api_key.blank?

  { model: model, api_key: api_key }.merge(ai_provider_overrides(profile, api_key)).compact
rescue StandardError => e
  Labimotion.log_exception(e, current_user)
  {}
end

#authenticate_admin!(type) ⇒ Object



60
61
62
63
64
65
66
67
# File 'lib/labimotion/helpers/generic_helpers.rb', line 60

def authenticate_admin!(type)
  unauthorized = -> { error!('401 Unauthorized', 401) }
  if %w[standard_layers vocabularies].include?(type)
    unauthorized.call unless current_user.generic_admin.values_at(*Labimotion::Constants::Family::ALL).any?
  else
    unauthorized.call unless current_user.generic_admin[type]
  end
end

#authorization_refusal_msg(name, action) ⇒ Object



92
93
94
95
96
97
98
99
100
101
# File 'lib/labimotion/helpers/generic_helpers.rb', line 92

def authorization_refusal_msg(name, action)
  requirement = if OWNER_ONLY_ACTIONS.include?(action)
                  "Only the owner can #{ACTION_PHRASE[action]} this template."
                elsif action == :read
                  "You need shared access to #{ACTION_PHRASE[action]} this template."
                else
                  "You need edit access to #{ACTION_PHRASE[action]} this template."
                end
  "This template is maintained by #{name}. #{requirement}"
end

#authorize_klass!(klz, action) ⇒ Object

The per-template gate (template-sharing.md §6). Takes the record, not a family string: resolution needs that klass's share rows and, for a SegmentKlass, its parent element's. Owner-less templates (and hosts that have not migrated) fall back to the legacy designer-wide gate — permanent behaviour, not a transition. A refusal is a 403 naming the owner, so the person refused knows who to ask, never the legacy bare 401.



80
81
82
83
84
85
86
87
88
89
90
# File 'lib/labimotion/helpers/generic_helpers.rb', line 80

def authorize_klass!(klz, action)
  result = Labimotion::ShareResolver.authorize(klz, current_user, action)
  return if result == :ok
  return authenticate_admin!(klass_family(klz)) if result == :legacy

  owner = Labimotion::OwnerResolver.find(result.owner_id)
  name = owner.respond_to?(:name) && owner.name.present? ? owner.name : 'another user'
  error!({ mc: AUTHORIZATION_REFUSAL_MC,
           msg: authorization_refusal_msg(name, action),
           owner: { id: result.owner_id, name: name == 'another user' ? nil : name } }, 403)
end

#conflicting_active_klass(name, keys, klz) ⇒ Object



323
324
325
326
327
328
329
330
331
332
# File 'lib/labimotion/helpers/generic_helpers.rb', line 323

def conflicting_active_klass(name, keys, klz)
  return nil if keys.nil?

  identity = keys.to_h { |key| [key, klz.public_send(key)] }
  return nil if identity[keys.first].blank?

  # The default scope of a paranoid model excludes deleted rows, so this can only match an
  # active one — never the row being restored.
  "Labimotion::#{name}".constantize.where(identity).first
end

#create_attachments(files, del_files, type, id, identifier, user_id) ⇒ Object



479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
# File 'lib/labimotion/helpers/generic_helpers.rb', line 479

def create_attachments(files, del_files, type, id, identifier, user_id)
  attach_ary = []
  (files || []).each_with_index do |file, index|
    next unless (tempfile = file[:tempfile])

    att = Attachment.new(
      bucket: file[:container_id],
      filename: file[:filename],
      con_state: Labimotion::ConState::NONE,
      file_path: file[:tempfile],
      created_by: user_id,
      created_for: user_id,
      identifier: identifier[index],
      content_type: file[:type],
      attachable_type: type,
      attachable_id: id,
    )
    begin
      att.save!
      attach_ary.push(att.id)
    ensure
      tempfile.close
      tempfile.unlink
    end
  end
  unless (del_files || []).empty?
    Attachment.where('id IN (?) AND attachable_type = (?)', del_files.map!(&:to_i), type).update_all(attachable_id: nil)
  end
  attach_ary
rescue StandardError => e
  Labimotion.log_exception(e)
  raise e
end

#create_uploads(type, id, files, param_info, user_id) ⇒ Object



426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
# File 'lib/labimotion/helpers/generic_helpers.rb', line 426

def create_uploads(type, id, files, param_info, user_id)
  return if files.nil? || param_info.nil? || files.empty? || param_info.empty?

  attach_ary = []
  map_info = JSON.parse(param_info)
  map_info&.keys&.each do |key|
    next if map_info[key]['files'].empty?

    if type == Labimotion::Prop::SEGMENT
      element = Labimotion::Segment.find_by(element_id: id, segment_klass_id: key)
    elsif type == Labimotion::Prop::ELEMENT
      element = Labimotion::Element.find_by(id: id)
    end
    next if element.nil?

    uploads = fetch_properties_uploads(element.properties)

    map_info[key]['files'].each do |fobj|
      file = (files || []).select { |ff| ff['filename'] == fobj['uid'] }&.first
      pa = uploads.select { |ss| ss[:uid] == file[:filename] }&.first || nil
      next unless (tempfile = file[:tempfile])

      a = Attachment.new(
        bucket: file[:container_id],
        filename: fobj['filename'],
        con_state: Labimotion::ConState::NONE,
        file_path: file[:tempfile],
        created_by: user_id,
        created_for: user_id,
        content_type: file[:type],
        attachable_type: map_info[key]['type'],
        attachable_id: element.id,
      )
      begin
        a.save!

        update_properties_upload(element, element.properties, a, pa)
        attach_ary.push(a.id)
      ensure
        tempfile.close
        tempfile.unlink
      end
    end
    element.send("#{type.downcase}s_revisions")&.last&.destroy!
    element.save!
  end
  attach_ary
rescue StandardError => e
  Labimotion.log_exception(e, current_user)
  error!('Error while uploading files.', 500)
  raise e
end

#deactivate_klass(params) ⇒ Object



174
175
176
177
178
179
180
181
182
183
# File 'lib/labimotion/helpers/generic_helpers.rb', line 174

def deactivate_klass(params)
  klz = fetch_klass(params[:klass], params[:id])
  authorize_klass!(klz, :deactivate)
  klz&.update!(is_active: params[:is_active])
  generate_klass_file unless klz.class.name != 'Labimotion::ElementKlass'
  klz
rescue StandardError => e
  Labimotion.log_exception(e, current_user)
  raise e
end

#delete_klass(params) ⇒ Object



195
196
197
198
199
200
201
202
203
204
# File 'lib/labimotion/helpers/generic_helpers.rb', line 195

def delete_klass(params)
  klz = fetch_klass(params[:klass], params[:id])
  authorize_klass!(klz, :destroy)
  klz&.destroy!
  generate_klass_file unless klz.class.name != 'Labimotion::ElementKlass'
  status 201
rescue StandardError => e
  Labimotion.log_exception(e, current_user)
  raise e
end

#delete_klass_revision(params) ⇒ Object



353
354
355
356
357
358
359
360
361
362
363
364
# File 'lib/labimotion/helpers/generic_helpers.rb', line 353

def delete_klass_revision(params)
  revision = "Labimotion::#{params[:klass]}esRevision".constantize.find(params[:id])
  klass = "Labimotion::#{params[:klass]}".constantize.find_by(id: params[:klass_id]) unless revision.nil?
  error!('Revision is invalid.', 404) if revision.nil?
  error!('Revision is invalid.', 404) unless revision_of_klass?(revision, klass, params[:klass])
  authorize_klass!(klass, :destroy)
  error!('Can not delete the active revision.', 405) if revision.uuid == klass.uuid
  revision.destroy!
rescue StandardError => e
  Labimotion.log_exception(e, current_user)
  raise e
end

#delete_revision(params) ⇒ Object



366
367
368
369
370
371
372
373
374
375
# File 'lib/labimotion/helpers/generic_helpers.rb', line 366

def delete_revision(params)
  revision = "Labimotion::#{params[:klass]}sRevision".constantize.find(params[:id])
  element = "Labimotion::#{params[:klass]}".constantize.find_by(id: params[:element_id]) unless revision.nil?
  error!('Revision is invalid.', 404) if revision.nil?
  error!('Can not delete the active revision.', 405) if revision.uuid == element.uuid
  revision&.destroy!
rescue StandardError => e
  Labimotion.log_exception(e, current_user)
  raise e
end

#deleted_klasses(params) ⇒ Object

delete_klass soft-deletes (the klasses are acts_as_paranoid), but nothing could list or restore the result, so recovery meant direct database access.



277
278
279
280
281
282
# File 'lib/labimotion/helpers/generic_helpers.rb', line 277

def deleted_klasses(params)
  "Labimotion::#{params[:klass]}".constantize.only_deleted.order(deleted_at: :desc)
rescue StandardError => e
  Labimotion.log_exception(e, current_user)
  raise e
end

#fetch_deleted_klass(name, id) ⇒ Object



302
303
304
305
306
# File 'lib/labimotion/helpers/generic_helpers.rb', line 302

def fetch_deleted_klass(name, id)
  klz = "Labimotion::#{name}".constantize.only_deleted.find_by(id: id)
  error!("#{name.gsub(/(Klass)/, '')} is not in the deleted list. Please refresh.", 404) if klz.nil?
  klz
end

#fetch_klass(name, id) ⇒ Object



103
104
105
106
107
108
109
110
# File 'lib/labimotion/helpers/generic_helpers.rb', line 103

def fetch_klass(name, id)
  klz = "Labimotion::#{name}".constantize.find_by(id: id)
  error!("#{name.gsub(/(Klass)/, '')} is invalid. Please re-select.", 500) if klz.nil?
  klz
rescue StandardError => e
  Labimotion.log_exception(e, current_user)
  raise e
end

#fetch_properties_uploads(properties) ⇒ Object



396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
# File 'lib/labimotion/helpers/generic_helpers.rb', line 396

def fetch_properties_uploads(properties)
  uploads = []
  properties[Labimotion::Prop::LAYERS].keys.each do |key|
    layer = properties[Labimotion::Prop::LAYERS][key]
    field_uploads = layer[Labimotion::Prop::FIELDS].select { |ss| ss['type'] == Labimotion::FieldType::UPLOAD }
    field_uploads.each do |field|
      ((field['value'] && field['value']['files']) || []).each do |file|
        uploads.push({ layer: key, field: field['field'], uid: file['uid'], filename: file['filename'] })
      end
    end
  end
  uploads
rescue StandardError => e
  Labimotion.log_exception(e, current_user)
  raise e
end

#fetch_repo(name, current_user) ⇒ Object



521
522
523
524
525
526
527
528
529
530
531
532
533
534
# File 'lib/labimotion/helpers/generic_helpers.rb', line 521

def fetch_repo(name, current_user)
  # current_klasses = "Labimotion::#{name}".constantize.where.not(identifier: nil)&.pluck(:identifier) || []
  response = Labimotion::TemplateHub.list(name)
  # if response && response['list'].present? && response['list'].length.positive?
    # filter_list = response['list']&.reject do |ds|
    #   current_klasses.include?(ds['identifier'])
    # end || []
  # end
  # filter_list || []
  (response && response['list']) || []
rescue StandardError => e
  Labimotion.log_exception(e, current_user)
  { error: 'Cannot connect to Chemotion Repository' }
end

#fetch_repo_generic_template(klass, identifier) ⇒ Object



513
514
515
# File 'lib/labimotion/helpers/generic_helpers.rb', line 513

def fetch_repo_generic_template(klass, identifier)
  Chemotion::Generic::Fetch::Template.exec(API::TARGET, klass, identifier)
end

#fetch_repo_generic_template_list(name = false) ⇒ Object



517
518
519
# File 'lib/labimotion/helpers/generic_helpers.rb', line 517

def fetch_repo_generic_template_list(name = false)
  Chemotion::Generic::Fetch::Template.list(API::TARGET, name)
end

#generate_klass_fileObject



387
388
389
390
391
392
393
394
# File 'lib/labimotion/helpers/generic_helpers.rb', line 387

def generate_klass_file
  klass_names_file = Labimotion::KLASSES_JSON # Rails.root.join('app/packs/klasses.json')
  klasses = Labimotion::ElementKlass.where(is_active: true)&.pluck(:name) || []
  File.write(klass_names_file, klasses)
rescue StandardError => e
  Labimotion.log_exception(e, current_user)
  raise e
end

#guard_restore_conflict!(name, klz) ⇒ Object

restore! writes through update_columns, so the uniqueness validations do not run, and no DB unique index backs them either. Restoring onto a taken identity would leave two active rows with the same one, and the ten find_by(name:) call sites in the gem — two of which pick the properties_release that renders real elements — would then resolve it arbitrarily. Refuse instead, and let a human decide which row keeps the name.



313
314
315
316
317
318
319
320
321
# File 'lib/labimotion/helpers/generic_helpers.rb', line 313

def guard_restore_conflict!(name, klz)
  keys = KLASS_IDENTITY[name]
  other = conflicting_active_klass(name, keys, klz)
  return if other.nil?

  error!({ mc: RESTORE_CONFLICT_MC,
           msg: restore_conflict_msg(name, other, keys),
           blocked_by: { id: other.id, label: klass_label(other, keys) } }, 409)
end

#guard_stale_template!(klz, properties) ⇒ Object

A template save replaces properties_template wholesale, so two designers holding the same template open used to overwrite each other with no warning and nothing to recover from: drafts are never snapshotted (Utils.next_version is a no-op for 'draft' and create_klasses_revision is skipped), and making drafts cut revisions is not an option because cutting a revision writes properties_release — it publishes.

update_template already mints a fresh uuid into the template on every save, so the stored uuid is a ready-made optimistic-locking token: the client posts back the uuid it was handed, and a difference means somebody saved in between.

Blank on either side means we cannot tell — a template stored before this guard existed, or a client that dropped the field — and a template with no stored uuid must not start refusing saves.



247
248
249
250
251
252
253
# File 'lib/labimotion/helpers/generic_helpers.rb', line 247

def guard_stale_template!(klz, properties)
  held = klz.properties_template.is_a?(Hash) ? klz.properties_template['uuid'] : nil
  sent = properties.is_a?(Hash) ? properties['uuid'] : nil
  return if held.blank? || sent.blank? || held == sent

  error!(stale_template_error(klz), 409)
end

#klass_family(klz) ⇒ Object

Family string (Constants::Family) for a loaded klass record — what the legacy gate keys on.



71
72
73
# File 'lib/labimotion/helpers/generic_helpers.rb', line 71

def klass_family(klz)
  Labimotion::Constants::Family::FAMILY_OF[klz.class.name.split('::').last]
end

#klass_label(klz, keys) ⇒ Object



339
340
341
342
# File 'lib/labimotion/helpers/generic_helpers.rb', line 339

def klass_label(klz, keys)
  label = klz.respond_to?(:label) ? klz.label : nil
  label.blank? ? klz.public_send(keys.first).to_s : label
end

#list_klass_revisions(params) ⇒ Object



377
378
379
380
381
382
383
384
# File 'lib/labimotion/helpers/generic_helpers.rb', line 377

def list_klass_revisions(params)
  klass = "Labimotion::#{params[:klass]}".constantize.find_by(id: params[:id])
  list = klass.send("#{params[:klass].underscore}es_revisions") unless klass.nil?
  list&.order(released_at: :desc)&.limit(params[:limit])
rescue StandardError => e
  Labimotion.log_exception(e, current_user)
  raise e
end

#restore_conflict_msg(name, other, keys) ⇒ Object



334
335
336
337
# File 'lib/labimotion/helpers/generic_helpers.rb', line 334

def restore_conflict_msg(name, other, keys)
  "Cannot restore: the active #{name.gsub(/(Klass)/, '')} \"#{klass_label(other, keys)}\" " \
    "(id #{other.id}) already uses this #{KLASS_IDENTITY_NOUN[name]}."
end

#restore_klass(params) ⇒ Object



284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
# File 'lib/labimotion/helpers/generic_helpers.rb', line 284

def restore_klass(params)
  klz = fetch_deleted_klass(params[:klass], params[:id])
  # Restore is delete's inverse and carries the same cascade into elements, so it takes
  # the same :destroy level. Shares survive deletion, so the owner row is still there to
  # answer this.
  authorize_klass!(klz, :destroy)
  guard_restore_conflict!(params[:klass], klz)
  # `recursive` is what brings the template's elements/segments back with it; without it
  # paranoia restores the klass row alone and every child stays deleted. The window keeps
  # that cascade to the children deleted *with* the template.
  klz.restore!(recursive: true, recovery_window: RESTORE_RECOVERY_WINDOW)
  generate_klass_file if params[:klass] == 'ElementKlass'
  klz.reload
rescue StandardError => e
  Labimotion.log_exception(e, current_user)
  raise e
end

#revision_of_klass?(revision, klass, klass_name) ⇒ Boolean

The revision must belong to the klass the caller named: the klass row is what authorize_klass! (and the active-revision guard) runs on, so accepting a mismatched pair would let ownership of one template authorize deleting — even the active — revisions of another.

Returns:

  • (Boolean)


348
349
350
351
# File 'lib/labimotion/helpers/generic_helpers.rb', line 348

def revision_of_klass?(revision, klass, klass_name)
  foreign_key = "#{klass_name.sub('Klass', '_klass').downcase}_id"
  !klass.nil? && revision.public_send(foreign_key) == klass.id
end

#stale_template_error(klz) ⇒ Object

Naming the other designer is what turns a refusal into something a user can act on, but it is decoration: OwnerResolver.find returns nil rather than raising, so a lookup that fails costs the name and leaves the refusal itself intact.



258
259
260
261
262
263
264
265
266
# File 'lib/labimotion/helpers/generic_helpers.rb', line 258

def stale_template_error(klz)
  saver = Labimotion::OwnerResolver.find(klz.updated_by)
  name = saver.respond_to?(:name) ? saver.name : nil
  name = nil if name.blank?
  { mc: STALE_TEMPLATE_MC,
    msg: stale_template_msg(name, klz.updated_at),
    saved_by: { id: klz.updated_by, name: name },
    saved_at: klz.updated_at.blank? ? nil : klz.updated_at.iso8601 }
end

#stale_template_msg(name, saved_at) ⇒ Object



268
269
270
271
272
273
# File 'lib/labimotion/helpers/generic_helpers.rb', line 268

def stale_template_msg(name, saved_at)
  who = name.blank? ? 'Another user' : name
  moment = saved_at.blank? ? 'after you opened it' : "at #{saved_at.strftime('%Y-%m-%d %H:%M %Z')}"
  "#{who} saved this template #{moment}, so your copy is out of date; " \
    'reload the template and re-apply your changes.'
end

#update_klass_settings(params) ⇒ Object



185
186
187
188
189
190
191
192
193
# File 'lib/labimotion/helpers/generic_helpers.rb', line 185

def update_klass_settings(params)
  klz = fetch_klass(params[:klass], params[:id])
  authorize_klass!(klz, :write)
  klz&.update!(settings: (klz.settings || {}).merge(params[:settings] || {}))
  klz
rescue StandardError => e
  Labimotion.log_exception(e, current_user)
  raise e
end

#update_properties_upload(element, properties, att, pa) ⇒ Object



413
414
415
416
417
418
419
420
421
422
423
424
# File 'lib/labimotion/helpers/generic_helpers.rb', line 413

def update_properties_upload(element, properties, att, pa)
  return if pa.nil?

  idx = properties[Labimotion::Prop::LAYERS][pa[:layer]][Labimotion::Prop::FIELDS].index { |fl| fl['field'] == pa[:field] }
  fidx = properties[Labimotion::Prop::LAYERS][pa[:layer]][Labimotion::Prop::FIELDS][idx]['value']['files'].index { |fi| fi['uid'] == pa[:uid] }
  properties[Labimotion::Prop::LAYERS][pa[:layer]][Labimotion::Prop::FIELDS][idx]['value']['files'][fidx]['aid'] = att.id
  properties[Labimotion::Prop::LAYERS][pa[:layer]][Labimotion::Prop::FIELDS][idx]['value']['files'][fidx]['uid'] = att.identifier
  element.update_columns(properties: properties)
rescue StandardError => e
  Labimotion.log_exception(e, current_user)
  raise e
end

#update_template(params, current_user) ⇒ Object



206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
# File 'lib/labimotion/helpers/generic_helpers.rb', line 206

def update_template(params, current_user)
  klz = fetch_klass(params[:klass], params[:id])
  # The split that makes `editor` mean "drafting only": a draft save needs :write, while
  # anything else bumps the version and cuts a revision — publishing, which is :release.
  # Authorization before the stale check: who may act comes before whether the copy is fresh.
  authorize_klass!(klz, params[:release] == 'draft' ? :write : :release)
  guard_stale_template!(klz, params[:properties_template])
  uuid = SecureRandom.uuid
  properties = params[:properties_template]
  properties['uuid'] = uuid
  klz.version = Labimotion::Utils.next_version(params[:release], klz.version)
  properties['version'] = klz.version
  properties['pkg'] = Labimotion::Utils.pkg(params['pkg'] || (klz.properties_template && klz.properties_template['pkg']))
  properties['klass'] = klz.class.name.split('::').last
  properties['identifier'] = klz.identifier
  properties.delete('eln') if properties['eln'].present?
  klz.updated_by = current_user.id
  klz.properties_template = properties
  klz. = params[:metadata] || {}
  klz.save!
  klz.reload
  klz.create_klasses_revision(current_user) if params[:release] != 'draft'
  klz
rescue StandardError => e
  Labimotion.log_exception(e, current_user)
  raise e
end