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',
  release_minor: 'release a minor version of',
  deactivate: 'activate or deactivate', destroy: 'delete', manage: 'manage sharing for'
}.freeze
OWNER_ONLY_ACTIONS =

release_minor is deliberately NOT here: a maintainer holds it too, so its refusal must not claim "only the owner can" — it gets its own maintainer wording below.

%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(settings, 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.



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

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

  { base_url: settings.base_url, api_path: settings.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) from the user's own settings row. Returns {} when nothing is stored, so AiTemplate falls back to the server-wide yml/ENV configuration. Shared by the dataset / element / segment AI helpers.



121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/labimotion/helpers/generic_helpers.rb', line 121

def ai_user_overrides(current_user)
  return {} if current_user.nil?

  settings = Labimotion::UserAiSettings.for(current_user)
  api_key = settings.api_key
  model   = api_key.blank? ? ai_shared_key_model(settings.model) : settings.model

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

#authenticate_admin!(type) ⇒ Object



63
64
65
66
67
68
69
70
# File 'lib/labimotion/helpers/generic_helpers.rb', line 63

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



95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/labimotion/helpers/generic_helpers.rb', line 95

def authorization_refusal_msg(name, action)
  requirement = if OWNER_ONLY_ACTIONS.include?(action)
                  "Only the owner can #{ACTION_PHRASE[action]} this template."
                elsif action == :release_minor
                  "You need maintainer access to #{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.



83
84
85
86
87
88
89
90
91
92
93
# File 'lib/labimotion/helpers/generic_helpers.rb', line 83

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



331
332
333
334
335
336
337
338
339
340
# File 'lib/labimotion/helpers/generic_helpers.rb', line 331

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



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
512
513
514
515
516
517
518
519
# File 'lib/labimotion/helpers/generic_helpers.rb', line 487

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



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
478
479
480
481
482
483
484
485
# File 'lib/labimotion/helpers/generic_helpers.rb', line 434

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



361
362
363
364
365
366
367
368
369
370
371
372
# File 'lib/labimotion/helpers/generic_helpers.rb', line 361

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



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

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.



285
286
287
288
289
290
# File 'lib/labimotion/helpers/generic_helpers.rb', line 285

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



310
311
312
313
314
# File 'lib/labimotion/helpers/generic_helpers.rb', line 310

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



108
109
110
111
112
113
114
115
# File 'lib/labimotion/helpers/generic_helpers.rb', line 108

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



404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
# File 'lib/labimotion/helpers/generic_helpers.rb', line 404

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



529
530
531
532
533
534
535
536
537
538
539
540
541
542
# File 'lib/labimotion/helpers/generic_helpers.rb', line 529

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



521
522
523
# File 'lib/labimotion/helpers/generic_helpers.rb', line 521

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



525
526
527
# File 'lib/labimotion/helpers/generic_helpers.rb', line 525

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

#generate_klass_fileObject



395
396
397
398
399
400
401
402
# File 'lib/labimotion/helpers/generic_helpers.rb', line 395

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.



321
322
323
324
325
326
327
328
329
# File 'lib/labimotion/helpers/generic_helpers.rb', line 321

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.



255
256
257
258
259
260
261
# File 'lib/labimotion/helpers/generic_helpers.rb', line 255

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.



74
75
76
# File 'lib/labimotion/helpers/generic_helpers.rb', line 74

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

#klass_label(klz, keys) ⇒ Object



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

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



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

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



342
343
344
345
# File 'lib/labimotion/helpers/generic_helpers.rb', line 342

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



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

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)


356
357
358
359
# File 'lib/labimotion/helpers/generic_helpers.rb', line 356

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.



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

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



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

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



421
422
423
424
425
426
427
428
429
430
431
432
# File 'lib/labimotion/helpers/generic_helpers.rb', line 421

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
233
234
235
236
237
238
239
240
# 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" and `maintainer` mean "drafting
  # plus minor releases": a draft save needs :write; a minor release bumps the version
  # and cuts a revision, which a maintainer may do (:release_minor); a major release —
  # and any release value nobody recognises — stays owner-only (:release), so an unknown
  # value fails toward the stricter gate.
  # Authorization before the stale check: who may act comes before whether the copy is fresh.
  release_action = case params[:release]
                   when 'draft' then :write
                   when 'minor' then :release_minor
                   else :release
                   end
  authorize_klass!(klz, release_action)
  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