diff options
Diffstat (limited to 'app/models')
| -rw-r--r-- | app/models/action_participant.rb | 4 | ||||
| -rw-r--r-- | app/models/asset.rb | 33 | ||||
| -rw-r--r-- | app/models/asset_license.rb | 15 | ||||
| -rw-r--r-- | app/models/concerns/file_attachment.rb | 55 | ||||
| -rw-r--r-- | app/models/node.rb | 89 | ||||
| -rw-r--r-- | app/models/node_action.rb | 27 | ||||
| -rw-r--r-- | app/models/page.rb | 26 | ||||
| -rw-r--r-- | app/models/related_asset.rb | 9 |
8 files changed, 208 insertions, 50 deletions
diff --git a/app/models/action_participant.rb b/app/models/action_participant.rb new file mode 100644 index 0000000..f179a48 --- /dev/null +++ b/app/models/action_participant.rb | |||
| @@ -0,0 +1,4 @@ | |||
| 1 | class ActionParticipant < ApplicationRecord | ||
| 2 | belongs_to :node_action | ||
| 3 | belongs_to :subject, :polymorphic => true | ||
| 4 | end | ||
diff --git a/app/models/asset.rb b/app/models/asset.rb index aca0ee8..73970e2 100644 --- a/app/models/asset.rb +++ b/app/models/asset.rb | |||
| @@ -1,12 +1,43 @@ | |||
| 1 | class Asset < ApplicationRecord | 1 | class Asset < ApplicationRecord |
| 2 | IMAGE_CONTENT_TYPES = ["image/gif", "image/jpeg", "image/png", "image/webp"] | ||
| 3 | PDF_CONTENT_TYPE = "application/pdf" | ||
| 2 | 4 | ||
| 3 | include FileAttachment | 5 | include FileAttachment |
| 4 | 6 | ||
| 5 | has_many :related_assets, :dependent => :destroy | 7 | has_many :related_assets, :dependent => :destroy |
| 6 | has_many :pages, :through => :related_assets | 8 | has_many :pages, :through => :related_assets |
| 7 | 9 | ||
| 8 | scope :images, -> { where(:upload_content_type => ["image/gif", "image/jpeg", "image/png", "image/webp"]) } | 10 | scope :images, -> { where(:upload_content_type => IMAGE_CONTENT_TYPES) } |
| 9 | scope :documents, -> { where(:upload_content_type => ["application/pdf", "text/plain", "text/rtf"]) } | 11 | scope :documents, -> { where(:upload_content_type => ["application/pdf", "text/plain", "text/rtf"]) } |
| 10 | scope :audio, -> { where(:upload_content_type => ["audio/mpeg", "audio/x-m4a", "audio/wav", "audio/x-wav"]) } | 12 | scope :audio, -> { where(:upload_content_type => ["audio/mpeg", "audio/x-m4a", "audio/wav", "audio/x-wav"]) } |
| 13 | scope :pdfs, -> { where(:upload_content_type => PDF_CONTENT_TYPE) } | ||
| 11 | 14 | ||
| 15 | scope :headline_eligible, -> { where(:upload_content_type => IMAGE_CONTENT_TYPES + [PDF_CONTENT_TYPE]) } | ||
| 16 | |||
| 17 | validates :license_key, inclusion: { in: -> { AssetLicense.keys } }, allow_blank: true | ||
| 18 | |||
| 19 | def image? | ||
| 20 | IMAGE_CONTENT_TYPES.include?(upload_content_type) | ||
| 21 | end | ||
| 22 | |||
| 23 | def pdf? | ||
| 24 | upload_content_type == PDF_CONTENT_TYPE | ||
| 25 | end | ||
| 26 | |||
| 27 | def has_credit? | ||
| 28 | creator.present? || source_url.present? || license_key.present? | ||
| 29 | end | ||
| 30 | |||
| 31 | def show_credit? | ||
| 32 | image? && has_credit? | ||
| 33 | end | ||
| 34 | |||
| 35 | # Nodes whose current lifecycle rows (head, draft or autosave) carry | ||
| 36 | # this asset. Historical revisions also hold RelatedAsset rows; they | ||
| 37 | # are excluded by construction here, since nothing points at them. | ||
| 38 | def attached_nodes | ||
| 39 | page_ids = related_assets.select(:page_id) | ||
| 40 | Node.where("head_id IN (:ids) OR draft_id IN (:ids) OR autosave_id IN (:ids)", | ||
| 41 | :ids => page_ids).distinct | ||
| 42 | end | ||
| 12 | end | 43 | end |
diff --git a/app/models/asset_license.rb b/app/models/asset_license.rb new file mode 100644 index 0000000..ca66d40 --- /dev/null +++ b/app/models/asset_license.rb | |||
| @@ -0,0 +1,15 @@ | |||
| 1 | class AssetLicense | ||
| 2 | Entry = Struct.new(:key, :url, :requires_attribution, :style, keyword_init: true) | ||
| 3 | |||
| 4 | DICTIONARY = YAML.load_file(Rails.root.join("config", "asset_licenses.yml")).freeze | ||
| 5 | |||
| 6 | def self.keys | ||
| 7 | DICTIONARY.keys | ||
| 8 | end | ||
| 9 | |||
| 10 | def self.find(key) | ||
| 11 | entry = DICTIONARY[key.to_s] if key.present? | ||
| 12 | return nil unless entry | ||
| 13 | Entry.new(key: key.to_s, url: entry["url"], requires_attribution: entry["requires_attribution"], style: entry["style"]) | ||
| 14 | end | ||
| 15 | end | ||
diff --git a/app/models/concerns/file_attachment.rb b/app/models/concerns/file_attachment.rb index 613a6f0..6221312 100644 --- a/app/models/concerns/file_attachment.rb +++ b/app/models/concerns/file_attachment.rb | |||
| @@ -31,6 +31,7 @@ module FileAttachment | |||
| 31 | 31 | ||
| 32 | IMAGE_CONTENT_TYPES = %w[image/jpeg image/gif image/png image/webp].freeze | 32 | IMAGE_CONTENT_TYPES = %w[image/jpeg image/gif image/png image/webp].freeze |
| 33 | VECTOR_CONTENT_TYPES = %w[image/svg+xml].freeze | 33 | VECTOR_CONTENT_TYPES = %w[image/svg+xml].freeze |
| 34 | RASTERIZED_CONTENT_TYPES = %w[application/pdf].freeze | ||
| 34 | DISPLAYABLE_AS_IMAGE = IMAGE_CONTENT_TYPES + VECTOR_CONTENT_TYPES | 35 | DISPLAYABLE_AS_IMAGE = IMAGE_CONTENT_TYPES + VECTOR_CONTENT_TYPES |
| 35 | 36 | ||
| 36 | included do | 37 | included do |
| @@ -52,29 +53,49 @@ module FileAttachment | |||
| 52 | build_upload_proxy | 53 | build_upload_proxy |
| 53 | end | 54 | end |
| 54 | 55 | ||
| 56 | def has_variant?(style) | ||
| 57 | upload_file_name.present? && File.exist?(file_path(style)) | ||
| 58 | end | ||
| 59 | |||
| 60 | def previewable? | ||
| 61 | has_variant?(:medium) | ||
| 62 | end | ||
| 63 | |||
| 64 | def variant_filename(style) | ||
| 65 | return upload_file_name if style == :original || !RASTERIZED_CONTENT_TYPES.include?(upload_content_type) | ||
| 66 | File.basename(upload_file_name, ".*") + ".png" | ||
| 67 | end | ||
| 68 | |||
| 55 | private | 69 | private |
| 56 | 70 | ||
| 57 | def build_upload_proxy | 71 | def build_upload_proxy |
| 58 | @upload = UploadProxy.new(self) | 72 | @upload = UploadProxy.new(self) |
| 59 | end | 73 | end |
| 60 | 74 | ||
| 75 | class_methods do | ||
| 76 | def upload_root | ||
| 77 | Rails.env.test? ? Rails.root.join("tmp", "test_uploads") : Rails.root.join("public", "system", "uploads") | ||
| 78 | end | ||
| 79 | end | ||
| 80 | |||
| 81 | def upload_root | ||
| 82 | self.class.upload_root | ||
| 83 | end | ||
| 84 | |||
| 61 | def process_upload | 85 | def process_upload |
| 62 | return unless @pending_upload | 86 | return unless @pending_upload |
| 63 | uploaded_file = @pending_upload | 87 | uploaded_file = @pending_upload |
| 64 | @pending_upload = nil | 88 | @pending_upload = nil |
| 65 | 89 | ||
| 66 | old_dir = Rails.root.join("public", "system", "uploads", id.to_s) | 90 | old_dir = upload_root.join(id.to_s) |
| 91 | |||
| 67 | FileUtils.rm_rf(old_dir) if Dir.exist?(old_dir) | 92 | FileUtils.rm_rf(old_dir) if Dir.exist?(old_dir) |
| 68 | 93 | ||
| 69 | original_path = file_path(:original) | 94 | original_path = file_path(:original) |
| 70 | FileUtils.mkdir_p(File.dirname(original_path)) | 95 | FileUtils.mkdir_p(File.dirname(original_path)) |
| 71 | FileUtils.cp(uploaded_file.tempfile.path, original_path) | 96 | FileUtils.cp(uploaded_file.tempfile.path, original_path) |
| 72 | 97 | ||
| 73 | if IMAGE_CONTENT_TYPES.include?(upload_content_type) | 98 | generate_all_variants(original_path) |
| 74 | generate_variants(original_path) | ||
| 75 | elsif VECTOR_CONTENT_TYPES.include?(upload_content_type) | ||
| 76 | generate_svg_variants(original_path) | ||
| 77 | end | ||
| 78 | end | 99 | end |
| 79 | 100 | ||
| 80 | def generate_variants(original_path) | 101 | def generate_variants(original_path) |
| @@ -93,20 +114,28 @@ module FileAttachment | |||
| 93 | end | 114 | end |
| 94 | end | 115 | end |
| 95 | 116 | ||
| 117 | def generate_all_variants(original_path) | ||
| 118 | if IMAGE_CONTENT_TYPES.include?(upload_content_type) | ||
| 119 | generate_variants(original_path) | ||
| 120 | elsif VECTOR_CONTENT_TYPES.include?(upload_content_type) | ||
| 121 | generate_svg_variants(original_path) | ||
| 122 | elsif RASTERIZED_CONTENT_TYPES.include?(upload_content_type) | ||
| 123 | generate_variants("#{original_path}[0]") | ||
| 124 | end | ||
| 125 | end | ||
| 126 | |||
| 96 | def delete_upload_files | 127 | def delete_upload_files |
| 97 | dir = Rails.root.join("public", "system", "uploads", id.to_s) | 128 | dir = upload_root.join(id.to_s) |
| 98 | FileUtils.rm_rf(dir) if Dir.exist?(dir) | 129 | FileUtils.rm_rf(dir) if Dir.exist?(dir) |
| 99 | end | 130 | end |
| 100 | 131 | ||
| 101 | def file_path(style) | 132 | def file_path(style) |
| 102 | Rails.root.join( | 133 | upload_root.join(id.to_s, style.to_s, variant_filename(style)).to_s |
| 103 | "public", "system", "uploads", | ||
| 104 | id.to_s, style.to_s, upload_file_name | ||
| 105 | ).to_s | ||
| 106 | end | 134 | end |
| 107 | 135 | ||
| 108 | def sanitize_filename(filename) | 136 | def sanitize_filename(filename) |
| 109 | File.basename(filename).gsub(/[^\w\.\-]/, '_') | 137 | name = File.basename(filename).unicode_normalize(:nfc) |
| 138 | name.gsub(/(?u)[^\w\.\-]/, '_') | ||
| 110 | end | 139 | end |
| 111 | 140 | ||
| 112 | # Proxy object returned by asset.upload, providing the Paperclip-compatible | 141 | # Proxy object returned by asset.upload, providing the Paperclip-compatible |
| @@ -118,7 +147,7 @@ module FileAttachment | |||
| 118 | 147 | ||
| 119 | def url(style = :original) | 148 | def url(style = :original) |
| 120 | return "" if @record.upload_file_name.blank? | 149 | return "" if @record.upload_file_name.blank? |
| 121 | "/system/uploads/#{@record.id}/#{style}/#{@record.upload_file_name}" | 150 | "/system/uploads/#{@record.id}/#{style}/#{@record.variant_filename(style)}" |
| 122 | end | 151 | end |
| 123 | 152 | ||
| 124 | def content_type | 153 | def content_type |
diff --git a/app/models/node.rb b/app/models/node.rb index a440c2f..7a93e79 100644 --- a/app/models/node.rb +++ b/app/models/node.rb | |||
| @@ -4,7 +4,14 @@ class Node < ApplicationRecord | |||
| 4 | 4 | ||
| 5 | # Associations | 5 | # Associations |
| 6 | has_many :pages, -> { order("revision ASC") }, :dependent => :destroy | 6 | has_many :pages, -> { order("revision ASC") }, :dependent => :destroy |
| 7 | |||
| 8 | # Entries where this node is the primary subject (fast-path column). | ||
| 9 | # For a *complete* history -- including subtree trash/destroy entries | ||
| 10 | # recorded at an ancestor -- use participated_actions instead. | ||
| 7 | has_many :node_actions, :dependent => :nullify | 11 | has_many :node_actions, :dependent => :nullify |
| 12 | has_many :action_participations, :class_name => "ActionParticipant", :as => :subject | ||
| 13 | has_many :participated_actions, :through => :action_participations, :source => :node_action | ||
| 14 | |||
| 8 | belongs_to :head, :class_name => "Page", :foreign_key => :head_id, optional: true | 15 | belongs_to :head, :class_name => "Page", :foreign_key => :head_id, optional: true |
| 9 | belongs_to :draft, :class_name => "Page", :foreign_key => :draft_id, optional: true | 16 | belongs_to :draft, :class_name => "Page", :foreign_key => :draft_id, optional: true |
| 10 | # Autosave pages carry no node_id, so has_many :pages does not cover | 17 | # Autosave pages carry no node_id, so has_many :pages does not cover |
| @@ -312,14 +319,14 @@ class Node < ApplicationRecord | |||
| 312 | was_published = head_id.present? | 319 | was_published = head_id.present? |
| 313 | final_published_at = head&.published_at | 320 | final_published_at = head&.published_at |
| 314 | 321 | ||
| 315 | demoted = 0 | 322 | subtree = [self] + descendants.to_a |
| 316 | ([self] + descendants.to_a).each do |node| | 323 | demoted_nodes = subtree.select do |node| |
| 317 | next unless node.head_id | 324 | next false unless node.head_id |
| 318 | former = node.head | 325 | former = node.head |
| 319 | node.head = nil | 326 | node.head = nil |
| 320 | node.draft_id = former.id if node.draft_id.nil? | 327 | node.draft_id = former.id if node.draft_id.nil? |
| 321 | node.save! | 328 | node.save! |
| 322 | demoted += 1 | 329 | true |
| 323 | end | 330 | end |
| 324 | 331 | ||
| 325 | self.reload | 332 | self.reload |
| @@ -332,9 +339,10 @@ class Node < ApplicationRecord | |||
| 332 | metadata = { :path => { "from" => path_before, "to" => unique_name } } | 339 | metadata = { :path => { "from" => path_before, "to" => unique_name } } |
| 333 | metadata[:was_published] = true if was_published | 340 | metadata[:was_published] = true if was_published |
| 334 | metadata[:final_published_at] = final_published_at.iso8601 if final_published_at | 341 | metadata[:final_published_at] = final_published_at.iso8601 if final_published_at |
| 335 | metadata[:demoted_heads] = demoted if demoted > 0 | 342 | metadata[:demoted_heads] = demoted_nodes.size if demoted_nodes.any? |
| 336 | 343 | ||
| 337 | NodeAction.record!(:node => self, :user => current_user, :action => "trash", **metadata) | 344 | NodeAction.record!(:participants => subtree, :user => current_user, |
| 345 | :action => "trash", **metadata) | ||
| 338 | self | 346 | self |
| 339 | end | 347 | end |
| 340 | end | 348 | end |
| @@ -382,7 +390,8 @@ class Node < ApplicationRecord | |||
| 382 | metadata = { :path => unique_name } | 390 | metadata = { :path => unique_name } |
| 383 | metadata[:destroyed_descendants] = doomed.size - 1 if doomed.size > 1 | 391 | metadata[:destroyed_descendants] = doomed.size - 1 if doomed.size > 1 |
| 384 | 392 | ||
| 385 | NodeAction.record!(:node => self, :user => current_user, :action => "destroy", **metadata) | 393 | NodeAction.record!(:participants => doomed, :user => current_user, |
| 394 | :action => "destroy", **metadata) | ||
| 386 | doomed.each(&:destroy!) | 395 | doomed.each(&:destroy!) |
| 387 | end | 396 | end |
| 388 | end | 397 | end |
| @@ -442,8 +451,65 @@ class Node < ApplicationRecord | |||
| 442 | end | 451 | end |
| 443 | end | 452 | end |
| 444 | 453 | ||
| 454 | # Attaches an asset to every current lifecycle row -- head, draft and | ||
| 455 | # autosave -- that does not already carry it. Attachments are page- | ||
| 456 | # scoped content (RelatedAsset belongs_to :page; drafts and autosaves | ||
| 457 | # are wholesale clones), so attaching to a single layer is how an | ||
| 458 | # attachment gets silently lost when another layer replaces it at | ||
| 459 | # publish or save. This is the out-of-band counterpart to the | ||
| 460 | # in-editor attach UI; it refuses when someone else holds the editing | ||
| 461 | # lock. Attaching to a head row changes the public page immediately, | ||
| 462 | # by design -- same reasoning as formalizing an already-existing | ||
| 463 | # editorial link. | ||
| 464 | # | ||
| 465 | # headline is a node-level decision: the flag is set on the newly | ||
| 466 | # created joins only when no current row has a headline yet and the | ||
| 467 | # asset is eligible; otherwise the asset is attached plain and the | ||
| 468 | # result says why, so the caller can point the editor at the star in | ||
| 469 | # the editor instead. | ||
| 470 | # | ||
| 471 | # Returns { :attached => n, :already => n, | ||
| 472 | # :headline => nil | :set | :kept_existing | :not_eligible } | ||
| 473 | def attach_asset! asset, user:, headline: false | ||
| 474 | if in_trash? || trash_node? | ||
| 475 | raise ActiveRecord::RecordInvalid.new(self), "Cannot attach assets to a node in the Trash" | ||
| 476 | end | ||
| 477 | |||
| 478 | if lock_owner && lock_owner != user | ||
| 479 | raise( | ||
| 480 | LockedByAnotherUser, | ||
| 481 | "Page is locked by another user who is working on it! " \ | ||
| 482 | "Last modification: #{(self.autosave || self.draft || self.head).updated_at.to_fs(:db)}" | ||
| 483 | ) | ||
| 484 | end | ||
| 485 | |||
| 486 | rows = [head, draft, autosave].compact | ||
| 487 | to_attach = rows.reject { |row| row.related_assets.exists?(:asset_id => asset.id) } | ||
| 488 | |||
| 489 | headline_state = | ||
| 490 | if headline && to_attach.any? | ||
| 491 | if !(asset.image? || asset.pdf?) | ||
| 492 | :not_eligible | ||
| 493 | elsif rows.any? { |row| row.headline_asset.present? } | ||
| 494 | :kept_existing | ||
| 495 | else | ||
| 496 | :set | ||
| 497 | end | ||
| 498 | end | ||
| 499 | |||
| 500 | ActiveRecord::Base.transaction do | ||
| 501 | to_attach.each do |row| | ||
| 502 | row.related_assets.create!(:asset => asset, :headline => headline_state == :set) | ||
| 503 | end | ||
| 504 | end | ||
| 505 | |||
| 506 | { :attached => to_attach.size, | ||
| 507 | :already => rows.size - to_attach.size, | ||
| 508 | :headline => headline_state } | ||
| 509 | end | ||
| 510 | |||
| 445 | def title | 511 | def title |
| 446 | head ? head.title : draft.title | 512 | editable_page&.title |
| 447 | end | 513 | end |
| 448 | 514 | ||
| 449 | def update_unique_names? | 515 | def update_unique_names? |
| @@ -533,13 +599,6 @@ class Node < ApplicationRecord | |||
| 533 | throw :abort | 599 | throw :abort |
| 534 | end | 600 | end |
| 535 | 601 | ||
| 536 | def self.recently_changed | ||
| 537 | includes(:head).where( | ||
| 538 | "pages.updated_at < ? AND pages.updated_at > ? AND nodes.parent_id IS NOT NULL", | ||
| 539 | Time.now, Time.now - 14.days | ||
| 540 | ).order("pages.updated_at desc").references(:head) | ||
| 541 | end | ||
| 542 | |||
| 543 | protected | 602 | protected |
| 544 | def lock_for! current_user | 603 | def lock_for! current_user |
| 545 | self.lock_owner = current_user | 604 | self.lock_owner = current_user |
diff --git a/app/models/node_action.rb b/app/models/node_action.rb index 13bd5ba..8a3dd8b 100644 --- a/app/models/node_action.rb +++ b/app/models/node_action.rb | |||
| @@ -3,6 +3,8 @@ class NodeAction < ApplicationRecord | |||
| 3 | belongs_to :page, optional: true | 3 | belongs_to :page, optional: true |
| 4 | belongs_to :user, optional: true | 4 | belongs_to :user, optional: true |
| 5 | 5 | ||
| 6 | has_many :action_participants, :dependent => :destroy | ||
| 7 | |||
| 6 | validates :action, presence: true | 8 | validates :action, presence: true |
| 7 | validates :occurred_at, presence: true | 9 | validates :occurred_at, presence: true |
| 8 | 10 | ||
| @@ -81,10 +83,15 @@ class NodeAction < ApplicationRecord | |||
| 81 | # This log records; it does not undo. No IP, session, or user | 83 | # This log records; it does not undo. No IP, session, or user |
| 82 | # agent, ever. Success only. | 84 | # agent, ever. Success only. |
| 83 | 85 | ||
| 84 | def self.record!(node:, action:, user: nil, page: nil, locale: nil, | 86 | def self.record!(node: nil, participants: [], action:, user: nil, page: nil, |
| 85 | occurred_at: nil, inferred_from: nil, **extra) | 87 | locale: nil, occurred_at: nil, inferred_from: nil, **extra) |
| 88 | participants = participants.presence || [node].compact | ||
| 89 | raise ArgumentError, "NodeAction.record! needs at least one participant" if participants.empty? | ||
| 90 | |||
| 91 | primary_node = node || (participants.first if participants.first.is_a?(Node)) | ||
| 92 | |||
| 86 | create!( | 93 | create!( |
| 87 | :node => node, | 94 | :node => primary_node, |
| 88 | :page => page, | 95 | :page => page, |
| 89 | :user => user, | 96 | :user => user, |
| 90 | :action => action, | 97 | :action => action, |
| @@ -94,10 +101,12 @@ class NodeAction < ApplicationRecord | |||
| 94 | :metadata => { | 101 | :metadata => { |
| 95 | "username" => user&.login, | 102 | "username" => user&.login, |
| 96 | "human_readable_node_name" => Globalize.with_locale(I18n.default_locale) { | 103 | "human_readable_node_name" => Globalize.with_locale(I18n.default_locale) { |
| 97 | node&.head&.title || node&.draft&.title | 104 | primary_node&.head&.title || primary_node&.draft&.title |
| 98 | }, | 105 | }, |
| 99 | }.merge(extra.stringify_keys) | 106 | }.merge(extra.stringify_keys) |
| 100 | ) | 107 | ).tap do |na| |
| 108 | participants.each { |subject| na.action_participants.create!(:subject => subject) } | ||
| 109 | end | ||
| 101 | end | 110 | end |
| 102 | 111 | ||
| 103 | # Computes the publish-entry diff between an outgoing head and the | 112 | # Computes the publish-entry diff between an outgoing head and the |
| @@ -166,4 +175,12 @@ class NodeAction < ApplicationRecord | |||
| 166 | def subject_name | 175 | def subject_name |
| 167 | metadata["human_readable_node_name"] || node&.unique_name || "deleted node" | 176 | metadata["human_readable_node_name"] || node&.unique_name || "deleted node" |
| 168 | end | 177 | end |
| 178 | |||
| 179 | def diff_link_params | ||
| 180 | prev = NodeAction.where(node_id: node_id, action: "publish") | ||
| 181 | .where("id < ?", id) | ||
| 182 | .order(id: :desc).first | ||
| 183 | return nil unless prev&.page && page | ||
| 184 | { start_revision: prev.page.revision, end_revision: page.revision } | ||
| 185 | end | ||
| 169 | end | 186 | end |
diff --git a/app/models/page.rb b/app/models/page.rb index f33d88d..817e3bd 100644 --- a/app/models/page.rb +++ b/app/models/page.rb | |||
| @@ -220,7 +220,12 @@ class Page < ApplicationRecord | |||
| 220 | end | 220 | end |
| 221 | 221 | ||
| 222 | # Clone asset references | 222 | # Clone asset references |
| 223 | self.assets = page.assets | 223 | self.related_assets.delete_all |
| 224 | page.related_assets.each do |related| | ||
| 225 | self.related_assets.create!(:asset_id => related.asset_id, | ||
| 226 | :position => related.position, | ||
| 227 | :headline => related.headline) | ||
| 228 | end | ||
| 224 | 229 | ||
| 225 | self.save | 230 | self.save |
| 226 | end | 231 | end |
| @@ -287,6 +292,10 @@ class Page < ApplicationRecord | |||
| 287 | end | 292 | end |
| 288 | end | 293 | end |
| 289 | 294 | ||
| 295 | def headline_asset | ||
| 296 | related_assets.find_by(headline: true)&.asset | ||
| 297 | end | ||
| 298 | |||
| 290 | # Returns true if a page has translations where one of them is significantly | 299 | # Returns true if a page has translations where one of them is significantly |
| 291 | # older than the other. | 300 | # older than the other. |
| 292 | # Takes the I18n.default locale and a second :locale to test if the | 301 | # Takes the I18n.default locale and a second :locale to test if the |
| @@ -314,21 +323,6 @@ class Page < ApplicationRecord | |||
| 314 | end | 323 | end |
| 315 | end | 324 | end |
| 316 | 325 | ||
| 317 | def update_assets image_ids | ||
| 318 | |||
| 319 | transaction do | ||
| 320 | self.related_assets.delete_all | ||
| 321 | |||
| 322 | if image_ids | ||
| 323 | image_ids.each_with_index do |id, index| | ||
| 324 | asset = Asset.find(id) | ||
| 325 | self.related_assets.create!(:asset_id => asset.id, :position => index+1) | ||
| 326 | end | ||
| 327 | end | ||
| 328 | end | ||
| 329 | |||
| 330 | end | ||
| 331 | |||
| 332 | # Installs (or re-installs) the trigger that keeps page_translations' | 326 | # Installs (or re-installs) the trigger that keeps page_translations' |
| 333 | # search_vector in sync. Idempotent, safe to call on every boot. | 327 | # search_vector in sync. Idempotent, safe to call on every boot. |
| 334 | # search_vector is populated by a raw Postgres trigger, not anything | 328 | # search_vector is populated by a raw Postgres trigger, not anything |
diff --git a/app/models/related_asset.rb b/app/models/related_asset.rb index 8f16460..8f8d49c 100644 --- a/app/models/related_asset.rb +++ b/app/models/related_asset.rb | |||
| @@ -5,4 +5,13 @@ class RelatedAsset < ApplicationRecord | |||
| 5 | acts_as_list :scope => :page_id | 5 | acts_as_list :scope => :page_id |
| 6 | 6 | ||
| 7 | default_scope -> { order("position ASC") } | 7 | default_scope -> { order("position ASC") } |
| 8 | |||
| 9 | validate :headline_only_for_images | ||
| 10 | |||
| 11 | private | ||
| 12 | |||
| 13 | def headline_only_for_images | ||
| 14 | return unless asset | ||
| 15 | errors.add(:headline, "can only be set on image or PDF assets") if headline? && !(asset.image? || asset.pdf?) | ||
| 16 | end | ||
| 8 | end | 17 | end |
