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 throughupdate_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
- #ai_model_entry_id(entry) ⇒ Object
-
#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.
-
#ai_server_model_allowlist ⇒ Object
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.
-
#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.
-
#ai_user_overrides(current_user) ⇒ Object
Per-user AI overrides (model + API key) resolved from the host app's user profile.
- #authenticate_admin!(type) ⇒ Object
- #authorization_refusal_msg(name, action) ⇒ Object
-
#authorize_klass!(klz, action) ⇒ Object
The per-template gate (template-sharing.md §6).
- #conflicting_active_klass(name, keys, klz) ⇒ Object
- #create_attachments(files, del_files, type, id, identifier, user_id) ⇒ Object
- #create_uploads(type, id, files, param_info, user_id) ⇒ Object
- #deactivate_klass(params) ⇒ Object
- #delete_klass(params) ⇒ Object
- #delete_klass_revision(params) ⇒ Object
- #delete_revision(params) ⇒ Object
-
#deleted_klasses(params) ⇒ Object
delete_klasssoft-deletes (the klasses areacts_as_paranoid), but nothing could list or restore the result, so recovery meant direct database access. - #fetch_deleted_klass(name, id) ⇒ Object
- #fetch_klass(name, id) ⇒ Object
- #fetch_properties_uploads(properties) ⇒ Object
- #fetch_repo(name, current_user) ⇒ Object
- #fetch_repo_generic_template(klass, identifier) ⇒ Object
- #fetch_repo_generic_template_list(name = false) ⇒ Object
- #generate_klass_file ⇒ Object
-
#guard_restore_conflict!(name, klz) ⇒ Object
restore!writes throughupdate_columns, so the uniqueness validations do not run, and no DB unique index backs them either. -
#guard_stale_template!(klz, properties) ⇒ Object
A template save replaces
properties_templatewholesale, 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_versionis a no-op for 'draft' andcreate_klasses_revisionis skipped), and making drafts cut revisions is not an option because cutting a revision writesproperties_release— it publishes. -
#klass_family(klz) ⇒ Object
Family string (Constants::Family) for a loaded klass record — what the legacy gate keys on.
- #klass_label(klz, keys) ⇒ Object
- #list_klass_revisions(params) ⇒ Object
- #restore_conflict_msg(name, other, keys) ⇒ Object
- #restore_klass(params) ⇒ Object
-
#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.
-
#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.findreturns nil rather than raising, so a lookup that fails costs the name and leaves the refusal itself intact. - #stale_template_msg(name, saved_at) ⇒ Object
- #update_klass_settings(params) ⇒ Object
- #update_properties_upload(element, properties, att, pa) ⇒ Object
- #update_template(params, current_user) ⇒ Object
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_allowlist ⇒ Object
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) = -> { error!('401 Unauthorized', 401) } if %w[standard_layers vocabularies].include?(type) .call unless current_user.generic_admin.values_at(*Labimotion::Constants::Family::ALL).any? else .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 (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 (klz, action) result = Labimotion::ShareResolver.(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: (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 (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]) (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]) (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]) (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_file ⇒ Object
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. (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.
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]) (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. (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 |