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_minoris 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 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(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.
-
#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) from the user's own settings row.
- #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(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_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) 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) = -> { 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
95 96 97 98 99 100 101 102 103 104 105 106 |
# File 'lib/labimotion/helpers/generic_helpers.rb', line 95 def (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 (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
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 (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]) (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
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]) (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_file ⇒ Object
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. (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.
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]) (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 (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 |