diff options
44 files changed, 910 insertions, 393 deletions
diff --git a/app/controllers/assets_controller.rb b/app/controllers/assets_controller.rb index becfe13..fbede0a 100644 --- a/app/controllers/assets_controller.rb +++ b/app/controllers/assets_controller.rb | |||
| @@ -28,6 +28,7 @@ class AssetsController < ApplicationController | |||
| 28 | # GET /assets/new.xml | 28 | # GET /assets/new.xml |
| 29 | def new | 29 | def new |
| 30 | @asset = Asset.new | 30 | @asset = Asset.new |
| 31 | @attach_node = Node.not_in_trash.find_by(:id => params[:node_id]) if params[:node_id].present? | ||
| 31 | 32 | ||
| 32 | respond_to do |format| | 33 | respond_to do |format| |
| 33 | format.html # new.html.erb | 34 | format.html # new.html.erb |
| @@ -44,10 +45,12 @@ class AssetsController < ApplicationController | |||
| 44 | # POST /assets.xml | 45 | # POST /assets.xml |
| 45 | def create | 46 | def create |
| 46 | @asset = Asset.new(asset_params) | 47 | @asset = Asset.new(asset_params) |
| 48 | attach_node = Node.not_in_trash.find_by(:id => params[:node_id]) if params[:node_id].present? | ||
| 47 | 49 | ||
| 48 | respond_to do |format| | 50 | respond_to do |format| |
| 49 | if @asset.save | 51 | if @asset.save |
| 50 | flash[:notice] = 'Asset was successfully created.' | 52 | flash[:notice] = 'Asset was successfully created.' |
| 53 | attach_to(attach_node) if attach_node | ||
| 51 | format.html { redirect_to(@asset) } | 54 | format.html { redirect_to(@asset) } |
| 52 | format.xml { render :xml => @asset, :status => :created, :location => @asset } | 55 | format.xml { render :xml => @asset, :status => :created, :location => @asset } |
| 53 | else | 56 | else |
| @@ -91,4 +94,23 @@ class AssetsController < ApplicationController | |||
| 91 | def asset_params | 94 | def asset_params |
| 92 | params.require(:asset).permit(:name, :upload, :creator, :source_url, :license_key) | 95 | params.require(:asset).permit(:name, :upload, :creator, :source_url, :license_key) |
| 93 | end | 96 | end |
| 97 | |||
| 98 | def attach_to node | ||
| 99 | result = node.attach_asset!(@asset, :user => current_user, | ||
| 100 | :headline => params[:headline].present?) | ||
| 101 | flash[:notice] = | ||
| 102 | if result[:attached].zero? | ||
| 103 | "Asset saved — it was already attached to “#{node.title}”." | ||
| 104 | else | ||
| 105 | "Asset was successfully created and attached to “#{node.title}”." | ||
| 106 | end | ||
| 107 | case result[:headline] | ||
| 108 | when :set then flash[:notice] += " It is now the page's headline." | ||
| 109 | when :kept_existing then flash[:headline_kept_path] = node_path(node) | ||
| 110 | when :not_eligible then flash[:error] = "This asset type cannot be a headline." | ||
| 111 | end | ||
| 112 | rescue LockedByAnotherUser | ||
| 113 | flash[:locked_by] = node.lock_owner&.login | ||
| 114 | flash[:locked_node_path] = node_path(node) | ||
| 115 | end | ||
| 94 | end | 116 | end |
diff --git a/app/controllers/nodes_controller.rb b/app/controllers/nodes_controller.rb index 6caa827..9c84420 100644 --- a/app/controllers/nodes_controller.rb +++ b/app/controllers/nodes_controller.rb | |||
| @@ -223,10 +223,6 @@ class NodesController < ApplicationController | |||
| 223 | @nodes = index_matching(Node.drafts_and_autosaves) | 223 | @nodes = index_matching(Node.drafts_and_autosaves) |
| 224 | end | 224 | end |
| 225 | 225 | ||
| 226 | def recent | ||
| 227 | @nodes = index_matching(Node.recently_changed) | ||
| 228 | end | ||
| 229 | |||
| 230 | def mine | 226 | def mine |
| 231 | base = Node.joins(:pages) | 227 | base = Node.joins(:pages) |
| 232 | .where("pages.user_id = ? or pages.editor_id = ?", current_user, current_user) | 228 | .where("pages.user_id = ? or pages.editor_id = ?", current_user, current_user) |
diff --git a/app/controllers/pages_controller.rb b/app/controllers/pages_controller.rb index 2d08dea..326fbd4 100644 --- a/app/controllers/pages_controller.rb +++ b/app/controllers/pages_controller.rb | |||
| @@ -6,6 +6,19 @@ class PagesController < ApplicationController | |||
| 6 | 6 | ||
| 7 | def preview | 7 | def preview |
| 8 | @page = Page.find(params[:id]) | 8 | @page = Page.find(params[:id]) |
| 9 | unless @page.node | ||
| 10 | node = Node.find_by(autosave_id: @page.id) || | ||
| 11 | Node.find_by(draft_id: @page.id) || | ||
| 12 | Node.find_by(head_id: @page.id) | ||
| 13 | @page.node = node if node | ||
| 14 | end | ||
| 15 | |||
| 16 | node ||= @page.node | ||
| 17 | if node && node.draft_id == @page.id && node.autosave | ||
| 18 | @page = node.autosave | ||
| 19 | @page.node = node | ||
| 20 | end | ||
| 21 | |||
| 9 | 22 | ||
| 10 | if @page | 23 | if @page |
| 11 | template = @page.valid_template | 24 | template = @page.valid_template |
| @@ -15,11 +28,4 @@ class PagesController < ApplicationController | |||
| 15 | ) | 28 | ) |
| 16 | end | 29 | end |
| 17 | end | 30 | end |
| 18 | |||
| 19 | def sort_images | ||
| 20 | page = Page.find(params[:id]) | ||
| 21 | page.update_assets(params[:images]) | ||
| 22 | |||
| 23 | head :ok | ||
| 24 | end | ||
| 25 | end | 31 | end |
diff --git a/app/controllers/related_assets_controller.rb b/app/controllers/related_assets_controller.rb index da82cde..ca894f2 100644 --- a/app/controllers/related_assets_controller.rb +++ b/app/controllers/related_assets_controller.rb | |||
| @@ -5,10 +5,10 @@ class RelatedAssetsController < ApplicationController | |||
| 5 | def search | 5 | def search |
| 6 | term = params[:search_term].to_s.strip | 6 | term = params[:search_term].to_s.strip |
| 7 | attached_ids = @node.editable_page.related_assets.pluck(:asset_id) | 7 | attached_ids = @node.editable_page.related_assets.pluck(:asset_id) |
| 8 | scope = Asset.images.where.not(id: attached_ids) | 8 | scope = Asset.headline_eligible.where.not(id: attached_ids) |
| 9 | 9 | ||
| 10 | results = if term.present? | 10 | results = if term.present? |
| 11 | scope.where("name ILIKE ?", "%#{term}%").limit(10) | 11 | scope.where("name ILIKE :term OR upload_file_name ILIKE :term", term: "%#{term}%").limit(10) |
| 12 | else | 12 | else |
| 13 | scope.order(created_at: :desc).limit(5) | 13 | scope.order(created_at: :desc).limit(5) |
| 14 | end | 14 | end |
| @@ -26,6 +26,7 @@ class RelatedAssetsController < ApplicationController | |||
| 26 | id: related.id, | 26 | id: related.id, |
| 27 | asset_id: asset.id, | 27 | asset_id: asset.id, |
| 28 | name: asset.name, | 28 | name: asset.name, |
| 29 | has_credit: asset.show_credit?, | ||
| 29 | thumb_url: asset.upload.url(:thumb), | 30 | thumb_url: asset.upload.url(:thumb), |
| 30 | large_url: asset.upload.url(:large), | 31 | large_url: asset.upload.url(:large), |
| 31 | original_url: asset.upload.url, | 32 | original_url: asset.upload.url, |
diff --git a/app/controllers/shared_previews_controller.rb b/app/controllers/shared_previews_controller.rb index f6fb45a..65f744d 100644 --- a/app/controllers/shared_previews_controller.rb +++ b/app/controllers/shared_previews_controller.rb | |||
| @@ -8,7 +8,7 @@ class SharedPreviewsController < ApplicationController | |||
| 8 | is_draft = node.draft_id == @page.id | 8 | is_draft = node.draft_id == @page.id |
| 9 | 9 | ||
| 10 | currently_public = is_head && @page.public? | 10 | currently_public = is_head && @page.public? |
| 11 | superseded = !is_head && !is_draft | 11 | superseded = !is_head && !is_draft |
| 12 | 12 | ||
| 13 | if superseded || currently_public | 13 | if superseded || currently_public |
| 14 | redirect_to @page.public_link | 14 | redirect_to @page.public_link |
diff --git a/app/helpers/content_helper.rb b/app/helpers/content_helper.rb index 5b67259..7a52d9c 100644 --- a/app/helpers/content_helper.rb +++ b/app/helpers/content_helper.rb | |||
| @@ -53,8 +53,8 @@ module ContentHelper | |||
| 53 | end | 53 | end |
| 54 | 54 | ||
| 55 | def headline_image | 55 | def headline_image |
| 56 | @headline_asset = @page.related_assets.find_by(headline: true)&.asset | 56 | @headline_asset = @page.headline_asset |
| 57 | render :partial => 'content/headline_image' if @headline_asset || @page.assets.images.any? | 57 | render :partial => 'content/headline_image' if @headline_asset || @page.assets.images.any? || @page.assets.pdfs.any? |
| 58 | end | 58 | end |
| 59 | 59 | ||
| 60 | # Returns the published_at attribute of a page if it is not nil, otherwise | 60 | # Returns the published_at attribute of a page if it is not nil, otherwise |
| @@ -153,7 +153,7 @@ module ContentHelper | |||
| 153 | 153 | ||
| 154 | def asset_credit(asset) | 154 | def asset_credit(asset) |
| 155 | return nil unless asset | 155 | return nil unless asset |
| 156 | return nil if asset.creator.blank? && asset.source_url.blank? && asset.license_key.blank? | 156 | return nil unless asset.has_credit? |
| 157 | 157 | ||
| 158 | license = AssetLicense.find(asset.license_key) | 158 | license = AssetLicense.find(asset.license_key) |
| 159 | 159 | ||
| @@ -173,4 +173,8 @@ module ContentHelper | |||
| 173 | full = license_text ? safe_join([attribution, license_text], ", ") : attribution | 173 | full = license_text ? safe_join([attribution, license_text], ", ") : attribution |
| 174 | safe_join([full, "."]) | 174 | safe_join([full, "."]) |
| 175 | end | 175 | end |
| 176 | |||
| 177 | def glightbox_data(image, title) | ||
| 178 | "title: #{title.to_s.tr(';', ',')};" | ||
| 179 | end | ||
| 176 | end | 180 | end |
diff --git a/app/models/asset.rb b/app/models/asset.rb index 8bea1b3..ba9c2f0 100644 --- a/app/models/asset.rb +++ b/app/models/asset.rb | |||
| @@ -1,5 +1,6 @@ | |||
| 1 | class Asset < ApplicationRecord | 1 | class Asset < ApplicationRecord |
| 2 | IMAGE_CONTENT_TYPES = ["image/gif", "image/jpeg", "image/png", "image/webp"] | 2 | IMAGE_CONTENT_TYPES = ["image/gif", "image/jpeg", "image/png", "image/webp"] |
| 3 | PDF_CONTENT_TYPE = "application/pdf" | ||
| 3 | 4 | ||
| 4 | include FileAttachment | 5 | include FileAttachment |
| 5 | 6 | ||
| @@ -9,10 +10,25 @@ class Asset < ApplicationRecord | |||
| 9 | scope :images, -> { where(:upload_content_type => IMAGE_CONTENT_TYPES) } | 10 | scope :images, -> { where(:upload_content_type => IMAGE_CONTENT_TYPES) } |
| 10 | 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"]) } |
| 11 | 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) } | ||
| 14 | |||
| 15 | scope :headline_eligible, -> { where(:upload_content_type => IMAGE_CONTENT_TYPES + [PDF_CONTENT_TYPE]) } | ||
| 12 | 16 | ||
| 13 | validates :license_key, inclusion: { in: -> { AssetLicense.keys } }, allow_blank: true | 17 | validates :license_key, inclusion: { in: -> { AssetLicense.keys } }, allow_blank: true |
| 14 | 18 | ||
| 15 | def image? | 19 | def image? |
| 16 | IMAGE_CONTENT_TYPES.include?(upload_content_type) | 20 | IMAGE_CONTENT_TYPES.include?(upload_content_type) |
| 17 | end | 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 | ||
| 18 | end | 34 | end |
diff --git a/app/models/concerns/file_attachment.rb b/app/models/concerns/file_attachment.rb index e7a33c5..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 |
| @@ -60,29 +61,41 @@ module FileAttachment | |||
| 60 | has_variant?(:medium) | 61 | has_variant?(:medium) |
| 61 | end | 62 | end |
| 62 | 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 | |||
| 63 | private | 69 | private |
| 64 | 70 | ||
| 65 | def build_upload_proxy | 71 | def build_upload_proxy |
| 66 | @upload = UploadProxy.new(self) | 72 | @upload = UploadProxy.new(self) |
| 67 | end | 73 | end |
| 68 | 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 | |||
| 69 | def process_upload | 85 | def process_upload |
| 70 | return unless @pending_upload | 86 | return unless @pending_upload |
| 71 | uploaded_file = @pending_upload | 87 | uploaded_file = @pending_upload |
| 72 | @pending_upload = nil | 88 | @pending_upload = nil |
| 73 | 89 | ||
| 74 | old_dir = Rails.root.join("public", "system", "uploads", id.to_s) | 90 | old_dir = upload_root.join(id.to_s) |
| 91 | |||
| 75 | FileUtils.rm_rf(old_dir) if Dir.exist?(old_dir) | 92 | FileUtils.rm_rf(old_dir) if Dir.exist?(old_dir) |
| 76 | 93 | ||
| 77 | original_path = file_path(:original) | 94 | original_path = file_path(:original) |
| 78 | FileUtils.mkdir_p(File.dirname(original_path)) | 95 | FileUtils.mkdir_p(File.dirname(original_path)) |
| 79 | FileUtils.cp(uploaded_file.tempfile.path, original_path) | 96 | FileUtils.cp(uploaded_file.tempfile.path, original_path) |
| 80 | 97 | ||
| 81 | if IMAGE_CONTENT_TYPES.include?(upload_content_type) | 98 | generate_all_variants(original_path) |
| 82 | generate_variants(original_path) | ||
| 83 | elsif VECTOR_CONTENT_TYPES.include?(upload_content_type) | ||
| 84 | generate_svg_variants(original_path) | ||
| 85 | end | ||
| 86 | end | 99 | end |
| 87 | 100 | ||
| 88 | def generate_variants(original_path) | 101 | def generate_variants(original_path) |
| @@ -101,16 +114,23 @@ module FileAttachment | |||
| 101 | end | 114 | end |
| 102 | end | 115 | end |
| 103 | 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 | |||
| 104 | def delete_upload_files | 127 | def delete_upload_files |
| 105 | dir = Rails.root.join("public", "system", "uploads", id.to_s) | 128 | dir = upload_root.join(id.to_s) |
| 106 | FileUtils.rm_rf(dir) if Dir.exist?(dir) | 129 | FileUtils.rm_rf(dir) if Dir.exist?(dir) |
| 107 | end | 130 | end |
| 108 | 131 | ||
| 109 | def file_path(style) | 132 | def file_path(style) |
| 110 | Rails.root.join( | 133 | upload_root.join(id.to_s, style.to_s, variant_filename(style)).to_s |
| 111 | "public", "system", "uploads", | ||
| 112 | id.to_s, style.to_s, upload_file_name | ||
| 113 | ).to_s | ||
| 114 | end | 134 | end |
| 115 | 135 | ||
| 116 | def sanitize_filename(filename) | 136 | def sanitize_filename(filename) |
| @@ -127,7 +147,7 @@ module FileAttachment | |||
| 127 | 147 | ||
| 128 | def url(style = :original) | 148 | def url(style = :original) |
| 129 | return "" if @record.upload_file_name.blank? | 149 | return "" if @record.upload_file_name.blank? |
| 130 | "/system/uploads/#{@record.id}/#{style}/#{@record.upload_file_name}" | 150 | "/system/uploads/#{@record.id}/#{style}/#{@record.variant_filename(style)}" |
| 131 | end | 151 | end |
| 132 | 152 | ||
| 133 | def content_type | 153 | def content_type |
diff --git a/app/models/node.rb b/app/models/node.rb index a440c2f..a5a40d3 100644 --- a/app/models/node.rb +++ b/app/models/node.rb | |||
| @@ -442,8 +442,65 @@ class Node < ApplicationRecord | |||
| 442 | end | 442 | end |
| 443 | end | 443 | end |
| 444 | 444 | ||
| 445 | # Attaches an asset to every current lifecycle row -- head, draft and | ||
| 446 | # autosave -- that does not already carry it. Attachments are page- | ||
| 447 | # scoped content (RelatedAsset belongs_to :page; drafts and autosaves | ||
| 448 | # are wholesale clones), so attaching to a single layer is how an | ||
| 449 | # attachment gets silently lost when another layer replaces it at | ||
| 450 | # publish or save. This is the out-of-band counterpart to the | ||
| 451 | # in-editor attach UI; it refuses when someone else holds the editing | ||
| 452 | # lock. Attaching to a head row changes the public page immediately, | ||
| 453 | # by design -- same reasoning as formalizing an already-existing | ||
| 454 | # editorial link. | ||
| 455 | # | ||
| 456 | # headline is a node-level decision: the flag is set on the newly | ||
| 457 | # created joins only when no current row has a headline yet and the | ||
| 458 | # asset is eligible; otherwise the asset is attached plain and the | ||
| 459 | # result says why, so the caller can point the editor at the star in | ||
| 460 | # the editor instead. | ||
| 461 | # | ||
| 462 | # Returns { :attached => n, :already => n, | ||
| 463 | # :headline => nil | :set | :kept_existing | :not_eligible } | ||
| 464 | def attach_asset! asset, user:, headline: false | ||
| 465 | if in_trash? || trash_node? | ||
| 466 | raise ActiveRecord::RecordInvalid.new(self), "Cannot attach assets to a node in the Trash" | ||
| 467 | end | ||
| 468 | |||
| 469 | if lock_owner && lock_owner != user | ||
| 470 | raise( | ||
| 471 | LockedByAnotherUser, | ||
| 472 | "Page is locked by another user who is working on it! " \ | ||
| 473 | "Last modification: #{(self.autosave || self.draft || self.head).updated_at.to_fs(:db)}" | ||
| 474 | ) | ||
| 475 | end | ||
| 476 | |||
| 477 | rows = [head, draft, autosave].compact | ||
| 478 | to_attach = rows.reject { |row| row.related_assets.exists?(:asset_id => asset.id) } | ||
| 479 | |||
| 480 | headline_state = | ||
| 481 | if headline && to_attach.any? | ||
| 482 | if !(asset.image? || asset.pdf?) | ||
| 483 | :not_eligible | ||
| 484 | elsif rows.any? { |row| row.headline_asset.present? } | ||
| 485 | :kept_existing | ||
| 486 | else | ||
| 487 | :set | ||
| 488 | end | ||
| 489 | end | ||
| 490 | |||
| 491 | ActiveRecord::Base.transaction do | ||
| 492 | to_attach.each do |row| | ||
| 493 | row.related_assets.create!(:asset => asset, :headline => headline_state == :set) | ||
| 494 | end | ||
| 495 | end | ||
| 496 | |||
| 497 | { :attached => to_attach.size, | ||
| 498 | :already => rows.size - to_attach.size, | ||
| 499 | :headline => headline_state } | ||
| 500 | end | ||
| 501 | |||
| 445 | def title | 502 | def title |
| 446 | head ? head.title : draft.title | 503 | editable_page&.title |
| 447 | end | 504 | end |
| 448 | 505 | ||
| 449 | def update_unique_names? | 506 | def update_unique_names? |
| @@ -533,13 +590,6 @@ class Node < ApplicationRecord | |||
| 533 | throw :abort | 590 | throw :abort |
| 534 | end | 591 | end |
| 535 | 592 | ||
| 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 | 593 | protected |
| 544 | def lock_for! current_user | 594 | def lock_for! current_user |
| 545 | self.lock_owner = current_user | 595 | self.lock_owner = current_user |
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 62000cc..8f8d49c 100644 --- a/app/models/related_asset.rb +++ b/app/models/related_asset.rb | |||
| @@ -12,6 +12,6 @@ class RelatedAsset < ApplicationRecord | |||
| 12 | 12 | ||
| 13 | def headline_only_for_images | 13 | def headline_only_for_images |
| 14 | return unless asset | 14 | return unless asset |
| 15 | errors.add(:headline, "can only be set on image assets") if headline? && !asset.image? | 15 | errors.add(:headline, "can only be set on image or PDF assets") if headline? && !(asset.image? || asset.pdf?) |
| 16 | end | 16 | end |
| 17 | end | 17 | end |
diff --git a/app/views/assets/edit.html.erb b/app/views/assets/edit.html.erb index 3ef8fea..0c1dd8f 100644 --- a/app/views/assets/edit.html.erb +++ b/app/views/assets/edit.html.erb | |||
| @@ -31,6 +31,23 @@ | |||
| 31 | ) %> | 31 | ) %> |
| 32 | </div> | 32 | </div> |
| 33 | 33 | ||
| 34 | <div class="node_description">attach to page</div> | ||
| 35 | <div class="node_content"> | ||
| 36 | <div class="restore_picker"> | ||
| 37 | <%= text_field_tag :asset_node_search_term, @attach_node&.title, | ||
| 38 | :placeholder => "Search for a page…", :autocomplete => "off" %> | ||
| 39 | <div id="asset_node_search_results" class="search_results" style="display: none"></div> | ||
| 40 | </div> | ||
| 41 | <%= hidden_field_tag :node_id, @attach_node&.id %> | ||
| 42 | <span class="field_hint">Optional — attaches this asset to that page, all pending versions included.</span> | ||
| 43 | </div> | ||
| 44 | |||
| 45 | <div class="node_description"></div> | ||
| 46 | <div class="node_content"> | ||
| 47 | <label><%= check_box_tag :headline, "1" %> attach as the page's headline</label> | ||
| 48 | <span class="field_hint">Applies only if the page has no headline yet.</span> | ||
| 49 | </div> | ||
| 50 | |||
| 34 | <div class="node_description">Actions</div> | 51 | <div class="node_description">Actions</div> |
| 35 | <div class="node_content node_info_group"> | 52 | <div class="node_content node_info_group"> |
| 36 | <div class="node_info_group_items"> | 53 | <div class="node_info_group_items"> |
diff --git a/app/views/assets/new.html.erb b/app/views/assets/new.html.erb index 2cf8865..7bded94 100644 --- a/app/views/assets/new.html.erb +++ b/app/views/assets/new.html.erb | |||
| @@ -26,6 +26,23 @@ | |||
| 26 | ) %> | 26 | ) %> |
| 27 | </div> | 27 | </div> |
| 28 | 28 | ||
| 29 | <div class="node_description">attach to page</div> | ||
| 30 | <div class="node_content"> | ||
| 31 | <div class="restore_picker"> | ||
| 32 | <%= text_field_tag :asset_node_search_term, @attach_node&.title, | ||
| 33 | :placeholder => "Search for a page…", :autocomplete => "off" %> | ||
| 34 | <div id="asset_node_search_results" class="search_results" style="display: none"></div> | ||
| 35 | </div> | ||
| 36 | <%= hidden_field_tag :node_id, @attach_node&.id %> | ||
| 37 | <span class="field_hint">Optional — attaches this asset to that page, all pending versions included.</span> | ||
| 38 | </div> | ||
| 39 | |||
| 40 | <div class="node_description"></div> | ||
| 41 | <div class="node_content"> | ||
| 42 | <label><%= check_box_tag :headline, "1" %> attach as the page's headline</label> | ||
| 43 | <span class="field_hint">Applies only if the page has no headline yet.</span> | ||
| 44 | </div> | ||
| 45 | |||
| 29 | <div class="node_description">Actions</div> | 46 | <div class="node_description">Actions</div> |
| 30 | <div class="node_content node_info_group"> | 47 | <div class="node_content node_info_group"> |
| 31 | <div class="node_info_group_items"> | 48 | <div class="node_info_group_items"> |
diff --git a/app/views/assets/show.html.erb b/app/views/assets/show.html.erb index a0e4e46..e551e35 100644 --- a/app/views/assets/show.html.erb +++ b/app/views/assets/show.html.erb | |||
| @@ -16,7 +16,6 @@ | |||
| 16 | </div> | 16 | </div> |
| 17 | </div> | 17 | </div> |
| 18 | 18 | ||
| 19 | <div class="node_description">Thumbnail</div> | ||
| 20 | <% if @asset.has_variant?(:medium) %> | 19 | <% if @asset.has_variant?(:medium) %> |
| 21 | <div class="node_description">Thumbnail</div> | 20 | <div class="node_description">Thumbnail</div> |
| 22 | <div class="node_content"><%= image_tag @asset.upload.url(:medium), style: "max-width: 300px; max-height: 300px;" %></div> | 21 | <div class="node_content"><%= image_tag @asset.upload.url(:medium), style: "max-width: 300px; max-height: 300px;" %></div> |
diff --git a/app/views/content/_asset_credits.html.erb b/app/views/content/_asset_credits.html.erb index 85739d0..5959abd 100644 --- a/app/views/content/_asset_credits.html.erb +++ b/app/views/content/_asset_credits.html.erb | |||
| @@ -1,11 +1,9 @@ | |||
| 1 | <% if @page.assets.images.any? %> | 1 | <% if @page.assets.images.any? %> |
| 2 | <div id="asset_credits"> | 2 | <div id="asset_credits"> |
| 3 | <% @page.assets.images.each do |image| %> | 3 | <% @page.assets.images.each do |image| %> |
| 4 | <% credit = asset_credit(image) %> | ||
| 5 | <% next unless credit %> | ||
| 6 | <div id="credit_for_asset_<%= image.id %>" | 4 | <div id="credit_for_asset_<%= image.id %>" |
| 7 | class="glightbox-desc<%= " headline_credit" if image == @headline_asset %>"> | 5 | class="asset_credit_block<%= " headline_credit" if image == @headline_asset %>"> |
| 8 | <%= credit %> | 6 | <%= asset_credit(image) %> |
| 9 | </div> | 7 | </div> |
| 10 | <% end %> | 8 | <% end %> |
| 11 | </div> | 9 | </div> |
diff --git a/app/views/content/_headline_image.html.erb b/app/views/content/_headline_image.html.erb index c764d22..4a3dfdc 100644 --- a/app/views/content/_headline_image.html.erb +++ b/app/views/content/_headline_image.html.erb | |||
| @@ -1,12 +1,25 @@ | |||
| 1 | <% gallery_images = @page.assets.images %> | 1 | <% gallery_images = @page.assets.images %> |
| 2 | <% image_headline = @headline_asset if @headline_asset&.image? %> | ||
| 3 | <% other_pdfs = @page.assets.pdfs.where.not(id: @headline_asset&.id) %> | ||
| 2 | 4 | ||
| 3 | <% if @headline_asset %> | 5 | <% if @headline_asset&.pdf? %> |
| 6 | <%= link_to @headline_asset.upload.url, :class => "headline_document_card", :target => "_blank", :rel => "noopener" do %> | ||
| 7 | <%= image_tag @headline_asset.upload.url(:medium), :alt => "", :class => "headline_document_card_thumb" %> | ||
| 8 | <div class="headline_document_card_info"> | ||
| 9 | <%= icon("file-text", library: "tabler", "aria-hidden": true) %> | ||
| 10 | <span class="headline_document_card_title"><%= @headline_asset.name %></span> | ||
| 11 | </div> | ||
| 12 | <% end %> | ||
| 13 | <% end %> | ||
| 14 | |||
| 15 | <% if image_headline %> | ||
| 4 | <%= link_to( | 16 | <%= link_to( |
| 5 | image_tag(@headline_asset.upload.url(:headline)), | 17 | image_tag(image_headline.upload.url(:headline)), |
| 6 | @headline_asset.upload.url, | 18 | image_headline.upload.url, |
| 7 | :class => "glightbox", | 19 | :class => "glightbox", |
| 8 | :data => { :gallery => "page-#{@page.node.id}", :title => @headline_asset.name, | 20 | :data => { :gallery => "page-#{@page.node.id}", |
| 9 | :description => "#credit_for_asset_#{@headline_asset.id}" } | 21 | :glightbox => glightbox_data(image_headline, image_headline.name), |
| 22 | :"credit-selector" => (image_headline.show_credit? ? "#credit_for_asset_#{image_headline.id}" : nil) } | ||
| 10 | ) %> | 23 | ) %> |
| 11 | <% if gallery_images.size > 1 %> | 24 | <% if gallery_images.size > 1 %> |
| 12 | <div class="right"><%= "#{gallery_images.size} #{t(:images)}" %></div> | 25 | <div class="right"><%= "#{gallery_images.size} #{t(:images)}" %></div> |
| @@ -15,14 +28,32 @@ | |||
| 15 | <%= link_to "#{gallery_images.size} #{t(:images)}, #{t(:open_gallery)}", | 28 | <%= link_to "#{gallery_images.size} #{t(:images)}, #{t(:open_gallery)}", |
| 16 | gallery_images.first.upload.url, | 29 | gallery_images.first.upload.url, |
| 17 | :class => "glightbox right", | 30 | :class => "glightbox right", |
| 18 | :data => { :gallery => "page-#{@page.node.id}", :title => gallery_images.first.name, | 31 | :data => { :gallery => "page-#{@page.node.id}", |
| 19 | :description => "#credit_for_asset_#{gallery_images.first.id}" } %> | 32 | :glightbox => glightbox_data(gallery_images.first, gallery_images.first.name), |
| 33 | :"credit-selector" => (gallery_images.first.show_credit? ? "#credit_for_asset_#{gallery_images.first.id}" : nil) } %> | ||
| 20 | <% end %> | 34 | <% end %> |
| 21 | 35 | ||
| 22 | <% gallery_images.each do |image| %> | 36 | <% gallery_images.each do |image| %> |
| 23 | <% next if image == @headline_asset %> | 37 | <% next if image == image_headline %> |
| 24 | <% next if !@headline_asset && image == gallery_images.first %> | 38 | <% next if !image_headline && image == gallery_images.first %> |
| 25 | <%= link_to "", image.upload.url, :class => "glightbox", :style => "display: none", | 39 | <%= link_to "", image.upload.url, :class => "glightbox", :style => "display: none", |
| 26 | :data => { :gallery => "page-#{@page.node.id}", :title => image.name, | 40 | :data => { :gallery => "page-#{@page.node.id}", |
| 27 | :description => "#credit_for_asset_#{image.id}" } %> | 41 | :glightbox => glightbox_data(image, image.name), |
| 42 | :"credit-selector" => (image.show_credit? ? "#credit_for_asset_#{image.id}" : nil) } %> | ||
| 43 | <% end %> | ||
| 44 | |||
| 45 | <% if other_pdfs.any? %> | ||
| 46 | <div class="related_documents"> | ||
| 47 | <div class="related_documents_label"><%= t(:related_documents) %></div> | ||
| 48 | <ul class="related_documents_list"> | ||
| 49 | <% other_pdfs.each do |pdf| %> | ||
| 50 | <li> | ||
| 51 | <%= link_to pdf.upload.url, :class => "related_document_link", :target => "_blank", :rel => "noopener" do %> | ||
| 52 | <%= icon("file-text", library: "tabler", "aria-hidden": true) %> | ||
| 53 | <span><%= pdf.name %></span> | ||
| 54 | <% end %> | ||
| 55 | </li> | ||
| 56 | <% end %> | ||
| 57 | </ul> | ||
| 58 | </div> | ||
| 28 | <% end %> | 59 | <% end %> |
diff --git a/app/views/custom/partials/_chapter.html.erb b/app/views/custom/partials/_chapter.html.erb index 47f3950..5d47679 100644 --- a/app/views/custom/partials/_chapter.html.erb +++ b/app/views/custom/partials/_chapter.html.erb | |||
| @@ -1,7 +1,7 @@ | |||
| 1 | <div class="article_partial chapter_partial" lang="<%= page.effective_lang %>"> | 1 | <div class="article_partial chapter_partial" lang="<%= page.effective_lang %>"> |
| 2 | <div class="chapter_partial_layout"> | 2 | <div class="chapter_partial_layout"> |
| 3 | <% if page.assets.images.any? %> | 3 | <% if page.headline_asset %> |
| 4 | <%= link_to_path image_tag(page.assets.images.first.upload.url(:thumb), :alt => ""), page.node.unique_name, class: "chapter_thumbnail" %> | 4 | <%= link_to_path image_tag(page.headline_asset.upload.url(:thumb), :alt => ""), page.node.unique_name, class: "chapter_thumbnail" %> |
| 5 | <% end %> | 5 | <% end %> |
| 6 | <div class="chapter_partial_content"> | 6 | <div class="chapter_partial_content"> |
| 7 | <h2 class="headline"><%= link_to_path page.title, page.node.unique_name %></h2> | 7 | <h2 class="headline"><%= link_to_path page.title, page.node.unique_name %></h2> |
diff --git a/app/views/layouts/_flash.html.erb b/app/views/layouts/_flash.html.erb new file mode 100644 index 0000000..a67fc86 --- /dev/null +++ b/app/views/layouts/_flash.html.erb | |||
| @@ -0,0 +1,24 @@ | |||
| 1 | <% if flash.any? %> | ||
| 2 | <div id="flash"> | ||
| 3 | <%= flash[:notice] %> | ||
| 4 | <% if flash[:status_path] %> | ||
| 5 | <%= link_to 'Go to Status', flash[:status_path] %> | ||
| 6 | <% end %> | ||
| 7 | <% if flash[:stale_locale_path] %> | ||
| 8 | The <%= flash[:stale_locale] %> translation may be out of date — | ||
| 9 | <%= link_to 'review it', flash[:stale_locale_path] %> too. | ||
| 10 | <% end %> | ||
| 11 | <% if flash[:locked_node_path] %> | ||
| 12 | <span class="warning">The page is locked by <%= flash[:locked_by] %> — | ||
| 13 | <%= link_to 'unlock it there', flash[:locked_node_path] %> first, | ||
| 14 | then attach from this asset's page.</span> | ||
| 15 | <% end %> | ||
| 16 | <% if flash[:headline_kept_path] %> | ||
| 17 | <span class="warning">The page's existing headline was kept — | ||
| 18 | <%= link_to 'change it there', flash[:headline_kept_path] %> if needed.</span> | ||
| 19 | <% end %> | ||
| 20 | <% if flash[:error] %> | ||
| 21 | <span id="flash_error"><%= flash[:error] %></span> | ||
| 22 | <% end %> | ||
| 23 | </div> | ||
| 24 | <% end %> | ||
diff --git a/app/views/layouts/admin.html.erb b/app/views/layouts/admin.html.erb index e220beb..89c9b55 100644 --- a/app/views/layouts/admin.html.erb +++ b/app/views/layouts/admin.html.erb | |||
| @@ -37,25 +37,10 @@ | |||
| 37 | </div> | 37 | </div> |
| 38 | </div> | 38 | </div> |
| 39 | <div class="admin_content_spacer"></div> | 39 | <div class="admin_content_spacer"></div> |
| 40 | <% if flash[:notice].present? || flash[:error].present? %> | 40 | <%= render "layouts/flash" %> |
| 41 | <div id="flash"> | ||
| 42 | <%= flash[:notice] %> | ||
| 43 | <% if flash[:status_path] %> | ||
| 44 | <%= link_to 'Go to Status', flash[:status_path] %> | ||
| 45 | <% end %> | ||
| 46 | <% if flash[:stale_locale_path] %> | ||
| 47 | The <%= flash[:stale_locale] %> translation may be out of date — | ||
| 48 | <%= link_to 'review it', flash[:stale_locale_path] %> too. | ||
| 49 | <% end %> | ||
| 50 | <% if flash[:error] %> | ||
| 51 | <span id="flash_error"><%= flash[:error] %></span> | ||
| 52 | <% end %> | ||
| 53 | </div> | ||
| 54 | <% end %> | ||
| 55 | <div id="content"> | 41 | <div id="content"> |
| 56 | <%= yield %> | 42 | <%= yield %> |
| 57 | </div> | 43 | </div> |
| 58 | |||
| 59 | <div id="results"></div> | 44 | <div id="results"></div> |
| 60 | </div> | 45 | </div> |
| 61 | </body> | 46 | </body> |
diff --git a/app/views/nodes/_recent_change_item.html.erb b/app/views/nodes/_recent_change_item.html.erb deleted file mode 100644 index 754a775..0000000 --- a/app/views/nodes/_recent_change_item.html.erb +++ /dev/null | |||
| @@ -1,9 +0,0 @@ | |||
| 1 | <li> | ||
| 2 | <div> | ||
| 3 | <%= link_to title_for_node(node), node_path(node) %> | ||
| 4 | <span class="field_hint"><%= link_to_path("#{node.unique_name} ↗", node.unique_name) %></span> | ||
| 5 | </div> | ||
| 6 | <span class="dashboard_widget_meta"> | ||
| 7 | <%= (editor = node_head_editor(node)) ? t("last_edited_by", editor: editor) : t("last_edited") %>, <%= relative_time_phrase(node.head.updated_at) %> | ||
| 8 | </span> | ||
| 9 | </li> | ||
diff --git a/app/views/nodes/edit.html.erb b/app/views/nodes/edit.html.erb index da682f0..fc1d18f 100644 --- a/app/views/nodes/edit.html.erb +++ b/app/views/nodes/edit.html.erb | |||
| @@ -33,48 +33,17 @@ | |||
| 33 | </div> | 33 | </div> |
| 34 | <% end %> | 34 | <% end %> |
| 35 | 35 | ||
| 36 | <details id="metadata_details"> | 36 | <%= fields_for @page do |d| %> |
| 37 | <summary>Metadata (slug, parent, tags, template, author, images)</summary> | 37 | <div id="edit_grid"> |
| 38 | <div id="metadata"> | ||
| 39 | <div class="node_description">Slug</div> | ||
| 40 | <div class="node_content"> | ||
| 41 | <%= f.text_field( | ||
| 42 | :staged_slug, :value => @node.staged_slug || @node.slug | ||
| 43 | ) | ||
| 44 | %> | ||
| 45 | </div> | ||
| 46 | 38 | ||
| 47 | <div class="node_description">parent</div> | 39 | <div id="main_fields"> |
| 48 | <div class="node_content"> | 40 | <div class="node_description">Title</div> |
| 49 | <%= text_field_tag :move_to_search_term, @node.parent.title rescue "" %> | 41 | <div class="node_content"><%= d.text_field :title %></div> |
| 50 | <div id="move_to_search_results" class="search_results"></div> | ||
| 51 | <%= f.hidden_field( | ||
| 52 | :staged_parent_id, | ||
| 53 | :value => @node.staged_parent_id || @node.parent_id | ||
| 54 | ) | ||
| 55 | %> | ||
| 56 | </div> | ||
| 57 | 42 | ||
| 58 | <%= fields_for @page do |d| %> | 43 | <div class="node_description">Abstract</div> |
| 59 | <div class="node_description">Tags - comma seperated</div> | 44 | <div class="node_content"><%= d.text_area :abstract %></div> |
| 60 | <div class="node_content"><%= text_field_tag :tag_list, @page.tag_list.join(', ') %></div> | ||
| 61 | 45 | ||
| 62 | <div class="node_description">Publish at</div> | 46 | <div class="node_description">Attachments</div> |
| 63 | <div class="node_content"><%= d.datetime_select :published_at, :value => @page.published_at %></div> | ||
| 64 | |||
| 65 | <div class="node_description">Template</div> | ||
| 66 | <div class="node_content"> | ||
| 67 | <%= d.select :template_name, custom_page_templates, {:prompt => 'Select Template'} %> | ||
| 68 | <span class="field_hint">Set automatically based on how this node was created - change it if needed.</span> | ||
| 69 | </div> | ||
| 70 | |||
| 71 | <div class="node_description">Author</div> | ||
| 72 | <div class="node_content"> | ||
| 73 | <%= d.select :user_id, user_list, | ||
| 74 | :selected => @page.user_id || @node.draft&.user_id || @node.head&.user_id %> | ||
| 75 | </div> | ||
| 76 | |||
| 77 | <div class="node_description">Images</div> | ||
| 78 | <div class="node_content"> | 47 | <div class="node_content"> |
| 79 | <div id="related_assets" | 48 | <div id="related_assets" |
| 80 | data-search-url="<%= search_node_related_assets_path(@node) %>" | 49 | data-search-url="<%= search_node_related_assets_path(@node) %>" |
| @@ -86,6 +55,7 @@ | |||
| 86 | data-large-url="<%= related.asset.upload.url(:large) %>" | 55 | data-large-url="<%= related.asset.upload.url(:large) %>" |
| 87 | data-original-url="<%= related.asset.upload.url %>" | 56 | data-original-url="<%= related.asset.upload.url %>" |
| 88 | data-name="<%= related.asset.name %>" | 57 | data-name="<%= related.asset.name %>" |
| 58 | data-has-credit="<%= related.asset.show_credit? %>" | ||
| 89 | data-headline="<%= related.headline? %>" | 59 | data-headline="<%= related.headline? %>" |
| 90 | class="<%= "is_headline" if related.headline? %>"> | 60 | class="<%= "is_headline" if related.headline? %>"> |
| 91 | <span class="related_asset_handle"><%= icon("grip-vertical", library: "tabler", "aria-hidden": true) %></span> | 61 | <span class="related_asset_handle"><%= icon("grip-vertical", library: "tabler", "aria-hidden": true) %></span> |
| @@ -101,7 +71,7 @@ | |||
| 101 | </li> | 71 | </li> |
| 102 | <% end %> | 72 | <% end %> |
| 103 | </ul> | 73 | </ul> |
| 104 | <p class="field_hint">The starred photo becomes this page's headline image on the public site. If none is starred, visitors get a link to browse all attached images instead.</p> | 74 | <p class="field_hint">The starred image or PDF becomes this page's headline on the public site. If none is starred, visitors get a link to browse all attached images instead.</p> |
| 105 | <%= text_field_tag nil, nil, id: "related_asset_search_term", placeholder: "Search images to attach…", autocomplete: "off" %> | 75 | <%= text_field_tag nil, nil, id: "related_asset_search_term", placeholder: "Search images to attach…", autocomplete: "off" %> |
| 106 | <div id="related_asset_search_results" class="search_results"></div> | 76 | <div id="related_asset_search_results" class="search_results"></div> |
| 107 | </div> | 77 | </div> |
| @@ -119,33 +89,73 @@ | |||
| 119 | </button> | 89 | </button> |
| 120 | </li> | 90 | </li> |
| 121 | </template> | 91 | </template> |
| 122 | |||
| 123 | </div> | 92 | </div> |
| 124 | </details> | ||
| 125 | 93 | ||
| 126 | <div id="content"> | 94 | <details id="metadata_details"> |
| 127 | <div class="node_description">Title</div> | 95 | <summary>Metadata (slug, parent, tags, template, author, images)</summary> |
| 128 | <div class="node_content"><%= d.text_field :title %></div> | 96 | <div id="metadata"> |
| 97 | <div class="node_description">Slug</div> | ||
| 98 | <div class="node_content"> | ||
| 99 | <%= f.text_field( | ||
| 100 | :staged_slug, :value => @node.staged_slug || @node.slug | ||
| 101 | ) | ||
| 102 | %> | ||
| 103 | </div> | ||
| 104 | |||
| 105 | <div class="node_description">parent</div> | ||
| 106 | <div class="node_content"> | ||
| 107 | <%= text_field_tag :move_to_search_term, @node.parent.title rescue "" %> | ||
| 108 | <p class="field_hint">Start typing to find a new parent for this node.</p> | ||
| 109 | <div id="move_to_search_results" class="search_results"></div> | ||
| 110 | <%= f.hidden_field( | ||
| 111 | :staged_parent_id, | ||
| 112 | :value => @node.staged_parent_id || @node.parent_id | ||
| 113 | ) | ||
| 114 | %> | ||
| 115 | </div> | ||
| 129 | 116 | ||
| 130 | <div class="node_description">Abstract</div> | 117 | <div class="node_description">Tags</div> |
| 131 | <div class="node_content"><%= d.text_area :abstract %></div> | 118 | <div class="node_content"> |
| 119 | <%= text_field_tag :tag_list, @page.tag_list.join(', ') %> | ||
| 120 | <span class="field_hint">Comma separated.</span> | ||
| 121 | </div> | ||
| 132 | 122 | ||
| 133 | <div class="node_description">Body</div> | 123 | <div class="node_description">Publish at</div> |
| 134 | <div class="body_toolbar_row"> | 124 | <div class="node_content"><%= d.datetime_select :published_at, :value => @page.published_at %></div> |
| 135 | <button type="button" id="preview_toggle" class="view_toggle" aria-pressed="false" aria-label="Toggle live preview" title="Toggle live preview"> | ||
| 136 | <%= icon("layout-sidebar-right", library: "tabler", "aria-hidden": true) %> <span>Live preview</span> | ||
| 137 | </button> | ||
| 138 | <button type="button" id="preview_force_render" class="view_toggle" style="display:none" aria-label="Force preview render" title="Force preview render"> | ||
| 139 | <%= icon("refresh", library: "tabler", "aria-hidden": true) %> <span>Force refresh</span> | ||
| 140 | </button> | ||
| 141 | </div> | ||
| 142 | 125 | ||
| 143 | <div id="editor_and_preview"> | 126 | <div class="node_description">Template</div> |
| 144 | <div class="preview_content"> | 127 | <div class="node_content"> |
| 145 | <div class="node_content"><%= d.text_area :body, :class => 'with_editor' %></div> | 128 | <%= d.select :template_name, custom_page_templates, {:prompt => 'Select Template'} %> |
| 129 | <span class="field_hint">Set automatically based on how this node was created - change it if needed.</span> | ||
| 130 | </div> | ||
| 131 | |||
| 132 | <div class="node_description">Author</div> | ||
| 133 | <div class="node_content"> | ||
| 134 | <%= d.select :user_id, user_list, | ||
| 135 | :selected => @page.user_id || @node.draft&.user_id || @node.head&.user_id %> | ||
| 136 | </div> | ||
| 137 | |||
| 138 | </div> | ||
| 139 | </details> | ||
| 140 | |||
| 141 | <div id="content"> | ||
| 142 | <div class="node_description">Body</div> | ||
| 143 | <div class="body_toolbar_row"> | ||
| 144 | <button type="button" id="preview_toggle" class="view_toggle" aria-pressed="false" aria-label="Toggle live preview" title="Toggle live preview"> | ||
| 145 | <%= icon("layout-sidebar-right", library: "tabler", "aria-hidden": true) %> <span>Live preview</span> | ||
| 146 | </button> | ||
| 147 | <button type="button" id="preview_force_render" class="view_toggle" style="display:none" aria-label="Force preview render" title="Force preview render"> | ||
| 148 | <%= icon("refresh", library: "tabler", "aria-hidden": true) %> <span>Force refresh</span> | ||
| 149 | </button> | ||
| 146 | </div> | 150 | </div> |
| 147 | <div id="preview_panel" style="display:none"> | 151 | |
| 148 | <iframe id="live_preview_iframe" title="Live preview" data-src="<%= preview_page_path(@page, :locale => I18n.default_locale) %>"></iframe> | 152 | <div id="editor_and_preview"> |
| 153 | <div class="preview_content"> | ||
| 154 | <div class="node_content"><%= d.text_area :body, :class => 'with_editor' %></div> | ||
| 155 | </div> | ||
| 156 | <div id="preview_panel" style="display:none"> | ||
| 157 | <iframe id="live_preview_iframe" title="Live preview" data-src="<%= preview_page_path(@page, :locale => I18n.default_locale) %>"></iframe> | ||
| 158 | </div> | ||
| 149 | </div> | 159 | </div> |
| 150 | </div> | 160 | </div> |
| 151 | </div> | 161 | </div> |
diff --git a/app/views/nodes/recent.html.erb b/app/views/nodes/recent.html.erb deleted file mode 100644 index d256253..0000000 --- a/app/views/nodes/recent.html.erb +++ /dev/null | |||
| @@ -1,7 +0,0 @@ | |||
| 1 | <h1>Recently changed</h1> | ||
| 2 | |||
| 3 | <%= will_paginate @nodes %> | ||
| 4 | <ul id="recent_changes_full_list"> | ||
| 5 | <%= render partial: "nodes/recent_change_item", collection: @nodes, as: :node %> | ||
| 6 | </ul> | ||
| 7 | <%= will_paginate @nodes %> | ||
diff --git a/app/views/nodes/show.html.erb b/app/views/nodes/show.html.erb index af05778..b3987c6 100644 --- a/app/views/nodes/show.html.erb +++ b/app/views/nodes/show.html.erb | |||
| @@ -253,16 +253,30 @@ | |||
| 253 | <% end %> | 253 | <% end %> |
| 254 | </div> | 254 | </div> |
| 255 | 255 | ||
| 256 | <% if @page.assets.images.any? %> | 256 | <div class="node_description">Attachments</div> |
| 257 | <div class="node_description">Images</div> | 257 | <div class="node_content node_info_group"> |
| 258 | <div class="node_content node_info_group"> | 258 | <% if @page.assets.any? %> |
| 259 | <% headline_asset_id = @page.headline_asset&.id %> | ||
| 259 | <ul class="thumbnail_list"> | 260 | <ul class="thumbnail_list"> |
| 260 | <% @page.assets.images.each do |asset| %> | 261 | <% @page.assets.each do |asset| %> |
| 261 | <li><%= link_to image_tag(asset.upload.url(:thumb)), asset_path(asset) %></li> | 262 | <li class="<%= "is_headline" if asset.id == headline_asset_id %>"> |
| 263 | <%= link_to image_tag(asset.upload.url(:thumb)), asset_path(asset) %> | ||
| 264 | <% if asset.id == headline_asset_id %> | ||
| 265 | <span class="headline_indicator" title="This page's headline image"> | ||
| 266 | <%= icon("star", library: "tabler", "aria-hidden": true) %> | ||
| 267 | </span> | ||
| 268 | <% end %> | ||
| 269 | </li> | ||
| 262 | <% end %> | 270 | <% end %> |
| 263 | </ul> | 271 | </ul> |
| 264 | </div> | 272 | <% end %> |
| 265 | <% end %> | 273 | <p class="add_attachments"> |
| 274 | <%= link_to new_asset_path(:node_id => @node.id), class: 'action_button' do %> | ||
| 275 | <%= icon("paperclip", library: "tabler", "aria-hidden": true) %> Upload new attachment | ||
| 276 | <% end %> | ||
| 277 | <span class="field_hint">To add or remove existing attachments, edit this node instead.</span> | ||
| 278 | </p> | ||
| 279 | </div> | ||
| 266 | 280 | ||
| 267 | <div class="node_description">Events</div> | 281 | <div class="node_description">Events</div> |
| 268 | <div class="node_content node_info_group"> | 282 | <div class="node_content node_info_group"> |
| @@ -276,7 +290,11 @@ | |||
| 276 | <% end %> | 290 | <% end %> |
| 277 | </ul> | 291 | </ul> |
| 278 | <% mapping = default_event_tag_mapping(@page) %> | 292 | <% mapping = default_event_tag_mapping(@page) %> |
| 279 | <%= link_to 'add event', new_event_path(node_id: @node.id, tag_list: mapping&.last, auto_tag_source: mapping&.first, return_to: request.path) %> | 293 | <p class="add_events"> |
| 294 | <%= link_to new_event_path(node_id: @node.id, tag_list: mapping&.last, auto_tag_source: mapping&.first, return_to: request.path), class: 'action_button' do %> | ||
| 295 | <%= icon("calendar-plus", library: "tabler", "aria-hidden": true) %> Add event | ||
| 296 | <% end %> | ||
| 297 | </p> | ||
| 280 | </div> | 298 | </div> |
| 281 | 299 | ||
| 282 | <% matches = matching_node_kinds(@node) %> | 300 | <% matches = matching_node_kinds(@node) %> |
| @@ -296,10 +314,11 @@ | |||
| 296 | <% if matches.any? %> | 314 | <% if matches.any? %> |
| 297 | <p class="add_child_links"> | 315 | <p class="add_child_links"> |
| 298 | <% matches.each_with_index do |(kind, config), index| %> | 316 | <% matches.each_with_index do |(kind, config), index| %> |
| 299 | <%= " · ".html_safe if index > 0 %> | ||
| 300 | <% link_params = { kind: kind } %> | 317 | <% link_params = { kind: kind } %> |
| 301 | <% link_params[:parent_id] = @node.id if kind == "generic" %> | 318 | <% link_params[:parent_id] = @node.id if kind == "generic" %> |
| 302 | <%= link_to "add '#{resolve_kind_text(config[:label])}' child", new_node_path(link_params) %> | 319 | <%= link_to new_node_path(link_params), class: 'action_button' do %> |
| 320 | <%= icon("text-plus", library: "tabler", "aria-hidden": true) %> Add child type <%= resolve_kind_text(config[:label]) %> | ||
| 321 | <% end %> | ||
| 303 | <% end %> | 322 | <% end %> |
| 304 | </p> | 323 | </p> |
| 305 | <% end %> | 324 | <% end %> |
diff --git a/config/locales/de.yml b/config/locales/de.yml index 21c8ebf..dd22cef 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml | |||
| @@ -130,3 +130,5 @@ de: | |||
| 130 | photo: "Foto %{name}" | 130 | photo: "Foto %{name}" |
| 131 | by: "von %{creator}" | 131 | by: "von %{creator}" |
| 132 | licensed_under: "Lizenziert unter %{license}" | 132 | licensed_under: "Lizenziert unter %{license}" |
| 133 | |||
| 134 | related_documents: "Weitere Dokumente" | ||
diff --git a/config/locales/en.yml b/config/locales/en.yml index 1afcf21..9be0152 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml | |||
| @@ -82,3 +82,5 @@ en: | |||
| 82 | photo: "Photo %{name}" | 82 | photo: "Photo %{name}" |
| 83 | by: "by %{creator}" | 83 | by: "by %{creator}" |
| 84 | licensed_under: "Licensed under %{license}" | 84 | licensed_under: "Licensed under %{license}" |
| 85 | |||
| 86 | related_documents: "Related documents" | ||
diff --git a/config/routes.rb b/config/routes.rb index 6ddf48c..6223a40 100644 --- a/config/routes.rb +++ b/config/routes.rb | |||
| @@ -30,7 +30,6 @@ Cccms::Application.routes.draw do | |||
| 30 | end | 30 | end |
| 31 | 31 | ||
| 32 | get 'pages/:id/preview', to: 'pages#preview', as: :preview_page | 32 | get 'pages/:id/preview', to: 'pages#preview', as: :preview_page |
| 33 | put 'pages/:id/sort_images', to: 'pages#sort_images', as: :sort_images_page | ||
| 34 | 33 | ||
| 35 | get 'preview/:token', to: 'shared_previews#show', as: :shared_preview | 34 | get 'preview/:token', to: 'shared_previews#show', as: :shared_preview |
| 36 | 35 | ||
| @@ -42,7 +41,6 @@ Cccms::Application.routes.draw do | |||
| 42 | get 'tags/:tags', action: :tags, as: :tags, constraints: { tags: /[^\/]+/ } | 41 | get 'tags/:tags', action: :tags, as: :tags, constraints: { tags: /[^\/]+/ } |
| 43 | get :parameterize_preview | 42 | get :parameterize_preview |
| 44 | get :drafts | 43 | get :drafts |
| 45 | get :recent | ||
| 46 | get :mine | 44 | get :mine |
| 47 | get :chapters | 45 | get :chapters |
| 48 | get :sitemap | 46 | get :sitemap |
diff --git a/doc/DESIGN_HISTORY.md b/doc/DESIGN_HISTORY.md index 5eb664b..7f411a3 100644 --- a/doc/DESIGN_HISTORY.md +++ b/doc/DESIGN_HISTORY.md | |||
| @@ -198,3 +198,39 @@ so the log can always answer, for any entry, how much to trust it. | |||
| 198 | `app/models/node.rb`. That's the authoritative, current version; this | 198 | `app/models/node.rb`. That's the authoritative, current version; this |
| 199 | entry explains why it's shaped the way it is, not what it says field | 199 | entry explains why it's shaped the way it is, not what it says field |
| 200 | by field. | 200 | by field. |
| 201 | |||
| 202 | ### Retired code | ||
| 203 | |||
| 204 | The timestamp-driven surfaces this replaced — the dashboard widget | ||
| 205 | and the nodes#recent page — are gone; admin/log is the sole | ||
| 206 | recent-activity view. | ||
| 207 | |||
| 208 | ## Attachments: images and PDFs | ||
| 209 | |||
| 210 | Originally attachments were images only; PDFs are now first-class, | ||
| 211 | and the subsystems below treat both through one mechanism. | ||
| 212 | |||
| 213 | Every attachment is an Asset joined to pages via RelatedAsset. The | ||
| 214 | starred attachment ("headline") becomes the page's face on the | ||
| 215 | public site; if none is starred, visitors get a link to browse all | ||
| 216 | attached images instead. | ||
| 217 | |||
| 218 | PDFs are headline-eligible because the standard variant set (thumb, | ||
| 219 | medium, large, headline) is generated for them as PNG rasters of the | ||
| 220 | document's first page (ImageMagick with Ghostscript); the original | ||
| 221 | remains the PDF. | ||
| 222 | |||
| 223 | Presentation differs by type at the last step only: an image | ||
| 224 | headline opens in the lightbox, a PDF headline renders as a document | ||
| 225 | card whose click delivers the document itself — a lightbox showing a | ||
| 226 | raster of page one would misrepresent what the asset is. Pages list | ||
| 227 | attached documents separately from the image gallery. | ||
| 228 | |||
| 229 | Credit display (photographer, license, attribution) applies to | ||
| 230 | images only; the vocabulary describes photographs and is deliberately | ||
| 231 | not rendered for PDFs. Admin asset search matches filenames as well | ||
| 232 | as names, since document filenames carry meaning in a way image | ||
| 233 | filenames rarely do. | ||
| 234 | |||
| 235 | The test suite runs against isolated storage and never touches the | ||
| 236 | real upload tree; keep it that way when adding asset code. | ||
diff --git a/lib/tasks/assets.rake b/lib/tasks/assets.rake index 9d4b048..e778021 100644 --- a/lib/tasks/assets.rake +++ b/lib/tasks/assets.rake | |||
| @@ -1,7 +1,9 @@ | |||
| 1 | namespace :assets do | 1 | namespace :assets do |
| 2 | desc "Regenerate ImageMagick variants for every image asset from its stored original -- rerun whenever a new style is added to FileAttachment::STYLES after assets already exist. Scope to one asset first with ASSET_ID=123." | 2 | desc "Regenerate ImageMagick variants for every image asset from its stored original -- rerun whenever a new style is added to FileAttachment::STYLES after assets already exist. Scope to one asset first with ASSET_ID=123." |
| 3 | task :regenerate_variants => :environment do | 3 | task :regenerate_variants => :environment do |
| 4 | scope = Asset.images | 4 | scope = Asset.where(upload_content_type: FileAttachment::IMAGE_CONTENT_TYPES + |
| 5 | FileAttachment::VECTOR_CONTENT_TYPES + | ||
| 6 | FileAttachment::RASTERIZED_CONTENT_TYPES) | ||
| 5 | scope = scope.where(id: ENV["ASSET_ID"]) if ENV["ASSET_ID"].present? | 7 | scope = scope.where(id: ENV["ASSET_ID"]) if ENV["ASSET_ID"].present? |
| 6 | 8 | ||
| 7 | scope.find_each do |asset| | 9 | scope.find_each do |asset| |
| @@ -12,7 +14,7 @@ namespace :assets do | |||
| 12 | next | 14 | next |
| 13 | end | 15 | end |
| 14 | 16 | ||
| 15 | asset.send(:generate_variants, original_path) | 17 | asset.send(:generate_all_variants, original_path) |
| 16 | puts "Regenerated variants for Asset##{asset.id} (#{asset.name})" | 18 | puts "Regenerated variants for Asset##{asset.id} (#{asset.name})" |
| 17 | end | 19 | end |
| 18 | end | 20 | end |
diff --git a/public/javascripts/admin_interface.js b/public/javascripts/admin_interface.js index 7fb1dc2..5514f3b 100644 --- a/public/javascripts/admin_interface.js +++ b/public/javascripts/admin_interface.js | |||
| @@ -45,10 +45,6 @@ $(document).ready(function () { | |||
| 45 | menu_item_sorter.initialize(); | 45 | menu_item_sorter.initialize(); |
| 46 | } | 46 | } |
| 47 | 47 | ||
| 48 | if ($("#metadata").length != 0) { | ||
| 49 | meta_data.initialize(); | ||
| 50 | } | ||
| 51 | |||
| 52 | if ($("#parent_search_term").length != 0) { | 48 | if ($("#parent_search_term").length != 0) { |
| 53 | parent_search.initialize_search(); | 49 | parent_search.initialize_search(); |
| 54 | } | 50 | } |
| @@ -69,6 +65,10 @@ $(document).ready(function () { | |||
| 69 | related_assets.initialize(); | 65 | related_assets.initialize(); |
| 70 | } | 66 | } |
| 71 | 67 | ||
| 68 | if ($("#asset_node_search_term").length != 0) { | ||
| 69 | asset_node_search.initialize_search(); | ||
| 70 | } | ||
| 71 | |||
| 72 | if ($("#rrule_builder").length != 0) { | 72 | if ($("#rrule_builder").length != 0) { |
| 73 | rrule_builder.initialize(); | 73 | rrule_builder.initialize(); |
| 74 | } | 74 | } |
| @@ -77,6 +77,14 @@ $(document).ready(function () { | |||
| 77 | cccms.preview.initialize(); | 77 | cccms.preview.initialize(); |
| 78 | } | 78 | } |
| 79 | 79 | ||
| 80 | var metadata_details = document.getElementById('metadata_details'); | ||
| 81 | if (metadata_details) { | ||
| 82 | var desktop_mq = window.matchMedia('(min-width: 1016px)'); | ||
| 83 | var sync_metadata = function() { metadata_details.open = desktop_mq.matches; }; | ||
| 84 | sync_metadata(); | ||
| 85 | desktop_mq.addEventListener('change', sync_metadata); | ||
| 86 | } | ||
| 87 | |||
| 80 | jQuery.ajaxSetup({ | 88 | jQuery.ajaxSetup({ |
| 81 | 'beforeSend': function(xhr) {xhr.setRequestHeader("Accept", "text/javascript");} | 89 | 'beforeSend': function(xhr) {xhr.setRequestHeader("Accept", "text/javascript");} |
| 82 | }); | 90 | }); |
| @@ -110,14 +118,6 @@ $(document).ready(function () { | |||
| 110 | }); | 118 | }); |
| 111 | 119 | ||
| 112 | 120 | ||
| 113 | meta_data = { | ||
| 114 | initialize : function() { | ||
| 115 | document.getElementById("metadata_details").addEventListener("toggle", function() { | ||
| 116 | if (this.open) image_interface.initialize(); | ||
| 117 | }); | ||
| 118 | } | ||
| 119 | }; | ||
| 120 | |||
| 121 | cccms = { | 121 | cccms = { |
| 122 | setup_autosave : function() { | 122 | setup_autosave : function() { |
| 123 | 123 | ||
| @@ -282,6 +282,7 @@ cccms = { | |||
| 282 | large: $(this).data('large-url'), | 282 | large: $(this).data('large-url'), |
| 283 | original: $(this).data('original-url'), | 283 | original: $(this).data('original-url'), |
| 284 | name: $(this).data('name'), | 284 | name: $(this).data('name'), |
| 285 | hasCredit: $(this).data('has-credit') === true, | ||
| 285 | headline: $(this).data('headline') === true | 286 | headline: $(this).data('headline') === true |
| 286 | }; | 287 | }; |
| 287 | }).get(); | 288 | }).get(); |
| @@ -320,8 +321,11 @@ cccms = { | |||
| 320 | : 'inline-image inline-image--half inline-image--' + placement; | 321 | : 'inline-image inline-image--half inline-image--' + placement; |
| 321 | 322 | ||
| 322 | var esc = cccms.inline_images.escape_attr; | 323 | var esc = cccms.inline_images.escape_attr; |
| 324 | var titleForGlightbox = (item.name || '').replace(/;/g, ','); | ||
| 325 | var glightboxOpts = 'title: ' + esc(titleForGlightbox) + | ||
| 326 | (item.hasCredit ? '; description: #credit_for_asset_' + esc(item.id) : '') + ';'; | ||
| 323 | var html = '<a href="' + esc(item.original) + '" class="glightbox" data-gallery="page-' + esc(cccms.inline_images.node_id) + '"' + | 327 | var html = '<a href="' + esc(item.original) + '" class="glightbox" data-gallery="page-' + esc(cccms.inline_images.node_id) + '"' + |
| 324 | ' data-title="' + esc(item.name) + '" data-description="#credit_for_asset_' + esc(item.id) + '">' + | 328 | ' data-glightbox="' + glightboxOpts + '">' + |
| 325 | '<img src="' + esc(item.large) + '" class="' + classes + '" alt="' + esc(item.name) + '"></a>'; | 329 | '<img src="' + esc(item.large) + '" class="' + classes + '" alt="' + esc(item.name) + '"></a>'; |
| 326 | 330 | ||
| 327 | cccms.inline_images.editor.insertContent(html); | 331 | cccms.inline_images.editor.insertContent(html); |
| @@ -360,77 +364,6 @@ menu_item_sorter = { | |||
| 360 | } | 364 | } |
| 361 | } | 365 | } |
| 362 | 366 | ||
| 363 | image_interface = { | ||
| 364 | |||
| 365 | initialize : function() { | ||
| 366 | |||
| 367 | $("#image_browser").hide(); | ||
| 368 | image_interface.initialize_sortable_image_box(); | ||
| 369 | image_interface.connect_browser_and_box(); | ||
| 370 | image_interface.set_droppable_behavior(); | ||
| 371 | image_interface.bind_image_browser_toggle(); | ||
| 372 | }, | ||
| 373 | |||
| 374 | |||
| 375 | set_droppable_behavior : function() { | ||
| 376 | $("ul#image_box").droppable({ | ||
| 377 | out : function(event, ui) { | ||
| 378 | $(ui.draggable).fadeTo("fast", 0.4); | ||
| 379 | |||
| 380 | $(ui.draggable).bind("mouseup", function() { | ||
| 381 | $(this).remove(); | ||
| 382 | }); | ||
| 383 | }, | ||
| 384 | over : function(event, ui) { | ||
| 385 | $(ui.draggable).fadeTo("fast", 1.0); | ||
| 386 | $(ui.draggable).unbind("mouseup"); | ||
| 387 | } | ||
| 388 | }); | ||
| 389 | }, | ||
| 390 | |||
| 391 | connect_browser_and_box : function() { | ||
| 392 | $("#image_browser ul li").draggable({ | ||
| 393 | connectToSortable : 'ul#image_box', | ||
| 394 | helper : 'clone', | ||
| 395 | revert : 'invalid' | ||
| 396 | }); | ||
| 397 | }, | ||
| 398 | |||
| 399 | initialize_sortable_image_box : function() { | ||
| 400 | |||
| 401 | $("ul#image_box").sortable({ | ||
| 402 | revert : true, | ||
| 403 | update : function(event, ui) { | ||
| 404 | images = $("ul#image_box").sortable("serialize", {attribute : "rel"}); | ||
| 405 | |||
| 406 | $.ajax({ | ||
| 407 | type : "POST", | ||
| 408 | url : "/pages/" + $("ul#image_box").attr("rel") + "/sort_images", | ||
| 409 | dataType : "json", | ||
| 410 | data : images + "&_method=put", | ||
| 411 | success : function() { | ||
| 412 | } | ||
| 413 | }); | ||
| 414 | } | ||
| 415 | }); | ||
| 416 | }, | ||
| 417 | |||
| 418 | bind_image_browser_toggle : function() { | ||
| 419 | $("#image_browser_toggle").bind("click", function(){ | ||
| 420 | if ($("#image_browser_toggle").attr("class") == "unselected") { | ||
| 421 | $("#image_browser_toggle").attr("class", "selected"); | ||
| 422 | $("#image_browser").show(); | ||
| 423 | } | ||
| 424 | else { | ||
| 425 | $("#image_browser_toggle").attr("class", "unselected"); | ||
| 426 | $("#image_browser").hide(); | ||
| 427 | } | ||
| 428 | |||
| 429 | return false; | ||
| 430 | }); | ||
| 431 | } | ||
| 432 | } | ||
| 433 | |||
| 434 | rrule_builder = { | 367 | rrule_builder = { |
| 435 | initialize : function() { | 368 | initialize : function() { |
| 436 | rrule_builder.try_populate_from_rrule($("#event_rrule").val()); | 369 | rrule_builder.try_populate_from_rrule($("#event_rrule").val()); |
diff --git a/public/javascripts/admin_search.js b/public/javascripts/admin_search.js index ad368cb..792849f 100644 --- a/public/javascripts/admin_search.js +++ b/public/javascripts/admin_search.js | |||
| @@ -286,3 +286,20 @@ event_search = { | |||
| 286 | }); | 286 | }); |
| 287 | } | 287 | } |
| 288 | }; | 288 | }; |
| 289 | |||
| 290 | asset_node_search = { | ||
| 291 | initialize_search : function() { | ||
| 292 | initSearchPicker({ | ||
| 293 | inputSelector: "#asset_node_search_term", | ||
| 294 | resultsSelector: "#asset_node_search_results", | ||
| 295 | onSelect: function(node) { | ||
| 296 | $("#asset_node_search_term").val(node.title); | ||
| 297 | $("#node_id").val(node.node_id); | ||
| 298 | } | ||
| 299 | }); | ||
| 300 | |||
| 301 | $("#asset_node_search_term").bind("input", function() { | ||
| 302 | if ($(this).val() === "") { $("#node_id").val(""); } | ||
| 303 | }); | ||
| 304 | } | ||
| 305 | }; | ||
diff --git a/public/javascripts/public.js b/public/javascripts/public.js index a9250ca..7f38f66 100644 --- a/public/javascripts/public.js +++ b/public/javascripts/public.js | |||
| @@ -1,6 +1,25 @@ | |||
| 1 | document.addEventListener('DOMContentLoaded', function(){ | 1 | document.addEventListener('DOMContentLoaded', function(){ |
| 2 | 2 | ||
| 3 | GLightbox({ selector: '.glightbox' }); | 3 | GLightbox({ |
| 4 | selector: '.glightbox', | ||
| 5 | afterSlideLoad: function(slideData) { | ||
| 6 | var trigger = document.querySelectorAll('.glightbox')[slideData.index]; | ||
| 7 | var selector = trigger && trigger.dataset.creditSelector; | ||
| 8 | if (!selector) return; | ||
| 9 | |||
| 10 | var source = document.querySelector(selector); | ||
| 11 | var container = slideData.slide.querySelector('.gdesc-inner'); | ||
| 12 | if (!source || !container) return; | ||
| 13 | |||
| 14 | var target = container.querySelector('.gslide-desc'); | ||
| 15 | if (!target) { | ||
| 16 | target = document.createElement('div'); | ||
| 17 | target.className = 'gslide-desc'; | ||
| 18 | container.appendChild(target); | ||
| 19 | } | ||
| 20 | target.innerHTML = source.innerHTML; | ||
| 21 | } | ||
| 22 | }); | ||
| 4 | 23 | ||
| 5 | document.getElementById("light-mode").addEventListener("change", () => { | 24 | document.getElementById("light-mode").addEventListener("change", () => { |
| 6 | if (document.getElementById("light-mode").checked) | 25 | if (document.getElementById("light-mode").checked) |
diff --git a/public/javascripts/related_assets.js b/public/javascripts/related_assets.js index cc07d86..c6c6a19 100644 --- a/public/javascripts/related_assets.js +++ b/public/javascripts/related_assets.js | |||
| @@ -87,6 +87,7 @@ related_assets = { | |||
| 87 | var item = $($("#related_asset_template").html().trim()); | 87 | var item = $($("#related_asset_template").html().trim()); |
| 88 | item.attr("data-url", related.url); | 88 | item.attr("data-url", related.url); |
| 89 | item.attr("data-asset-id", related.asset_id); | 89 | item.attr("data-asset-id", related.asset_id); |
| 90 | item.attr("data-has-credit", related.has_credit); | ||
| 90 | item.attr("data-large-url", related.large_url); | 91 | item.attr("data-large-url", related.large_url); |
| 91 | item.attr("data-original-url", related.original_url); | 92 | item.attr("data-original-url", related.original_url); |
| 92 | item.attr("data-name", related.name); | 93 | item.attr("data-name", related.name); |
diff --git a/public/stylesheets/admin.css b/public/stylesheets/admin.css index 086e73d..9f73775 100644 --- a/public/stylesheets/admin.css +++ b/public/stylesheets/admin.css | |||
| @@ -97,7 +97,8 @@ input[type=radio] { | |||
| 97 | } | 97 | } |
| 98 | 98 | ||
| 99 | #metadata, | 99 | #metadata, |
| 100 | #content { | 100 | #content, |
| 101 | #main_fields { | ||
| 101 | margin-left: 5px; | 102 | margin-left: 5px; |
| 102 | } | 103 | } |
| 103 | 104 | ||
| @@ -830,12 +831,11 @@ form.button_to button[type="submit"] { | |||
| 830 | 831 | ||
| 831 | /* Layout only -- the at-rest visibility (wavy underline) for these | 832 | /* Layout only -- the at-rest visibility (wavy underline) for these |
| 832 | links comes from the scoped rule in Base elements above. */ | 833 | links comes from the scoped rule in Base elements above. */ |
| 834 | .add_events, | ||
| 835 | .add_attachments, | ||
| 833 | .add_child_links { | 836 | .add_child_links { |
| 834 | margin-top: 0.5rem; | 837 | margin-top: 0.5rem; |
| 835 | } | 838 | margin-bottom: 0; |
| 836 | |||
| 837 | .add_child_links a { | ||
| 838 | white-space: nowrap; | ||
| 839 | } | 839 | } |
| 840 | 840 | ||
| 841 | .sitemap_node { | 841 | .sitemap_node { |
| @@ -892,6 +892,26 @@ form.button_to button[type="submit"] { | |||
| 892 | Page editor / metadata forms | 892 | Page editor / metadata forms |
| 893 | ============================================================ */ | 893 | ============================================================ */ |
| 894 | 894 | ||
| 895 | @media(min-width:1016px) { | ||
| 896 | #edit_grid { | ||
| 897 | display: grid; | ||
| 898 | grid-template-columns: 1fr 1fr; | ||
| 899 | grid-template-areas: | ||
| 900 | "main meta" | ||
| 901 | "body body"; | ||
| 902 | column-gap: 2rem; | ||
| 903 | align-items: start; | ||
| 904 | margin-left: -125px; | ||
| 905 | } | ||
| 906 | #main_fields { grid-area: main; } | ||
| 907 | #metadata_details { grid-area: meta; } | ||
| 908 | #page_editor #content { grid-area: body; } | ||
| 909 | |||
| 910 | /* Details is force-opened by JS on desktop; hide the summary only | ||
| 911 | once open, so a JS failure still leaves a working toggle. */ | ||
| 912 | #metadata_details[open] > summary { display: none; } | ||
| 913 | } | ||
| 914 | |||
| 895 | #new_node { | 915 | #new_node { |
| 896 | margin-left: -118px; | 916 | margin-left: -118px; |
| 897 | } | 917 | } |
| @@ -947,18 +967,16 @@ div#page_editor { | |||
| 947 | } | 967 | } |
| 948 | 968 | ||
| 949 | @media(min-width:1016px) { | 969 | @media(min-width:1016px) { |
| 950 | input#tag_list, | ||
| 951 | input#menu_search_term, | 970 | input#menu_search_term, |
| 952 | input#menu_item_node_id, | 971 | input#menu_item_node_id, |
| 953 | input#menu_item_path, | 972 | input#menu_item_path, |
| 954 | input#menu_item_title, | 973 | input#menu_item_title { |
| 955 | input#node_staged_slug, | ||
| 956 | input#move_to_search_term { | ||
| 957 | width: 690px; | 974 | width: 690px; |
| 958 | } | 975 | } |
| 959 | 976 | ||
| 960 | input[type=text]#page_title { | 977 | input[type=text]#page_title { |
| 961 | width: 690px; | 978 | width: 100%; |
| 979 | box-sizing: border-box; | ||
| 962 | font-size: 1rem; | 980 | font-size: 1rem; |
| 963 | padding-top: 6px; | 981 | padding-top: 6px; |
| 964 | padding-bottom: 4px; | 982 | padding-bottom: 4px; |
| @@ -968,24 +986,27 @@ div#page_editor { | |||
| 968 | } | 986 | } |
| 969 | 987 | ||
| 970 | textarea#page_abstract { | 988 | textarea#page_abstract { |
| 971 | width: 690px; | 989 | width: 100%; |
| 972 | height: 150px; | 990 | box-sizing: border-box; |
| 991 | height: 250px; | ||
| 973 | padding: 5px; | 992 | padding: 5px; |
| 974 | } | 993 | } |
| 975 | 994 | ||
| 976 | #page_editor textarea#page_body { | 995 | #page_editor textarea#page_body { |
| 977 | width: 700px; | 996 | width: 100%; |
| 997 | box-sizing: border-box; | ||
| 978 | height: 600px; | 998 | height: 600px; |
| 979 | } | 999 | } |
| 980 | 1000 | ||
| 981 | #page_editor #metadata, #page_editor #content, #admin_overview { | 1001 | #admin_overview { |
| 982 | margin-left: -125px; | 1002 | margin-left: -125px; |
| 983 | } | 1003 | } |
| 984 | 1004 | ||
| 985 | #page_editor input[type=text], | 1005 | #page_editor input[type=text], |
| 986 | #page_editor input[type=password], | 1006 | #page_editor input[type=password], |
| 987 | #page_editor textarea { | 1007 | #page_editor textarea { |
| 988 | width: 690px; | 1008 | width: 100%; |
| 1009 | box-sizing: border-box; | ||
| 989 | } | 1010 | } |
| 990 | } | 1011 | } |
| 991 | 1012 | ||
| @@ -998,18 +1019,14 @@ div#page_editor { | |||
| 998 | } | 1019 | } |
| 999 | 1020 | ||
| 1000 | #page_editor #content, | 1021 | #page_editor #content, |
| 1001 | #page_editor #metadata { | 1022 | #page_editor #metadata, |
| 1023 | #page_editor #main_fields { | ||
| 1002 | padding-right: 5px; | 1024 | padding-right: 5px; |
| 1003 | } | 1025 | } |
| 1004 | 1026 | ||
| 1005 | /* Fixed 800px width will overflow narrow viewports despite | ||
| 1006 | border-box -- every sibling rule in this block uses 100% instead. | ||
| 1007 | Worth checking on an actual phone; looks unfinished rather than | ||
| 1008 | intentional. Not changed here since it wasn't part of what you | ||
| 1009 | asked to fix. */ | ||
| 1010 | #page_editor textarea#page_body { | 1027 | #page_editor textarea#page_body { |
| 1011 | box-sizing: border-box; | 1028 | box-sizing: border-box; |
| 1012 | width: 800px; | 1029 | width: 100%; |
| 1013 | height: 50rem; | 1030 | height: 50rem; |
| 1014 | } | 1031 | } |
| 1015 | 1032 | ||
| @@ -1042,10 +1059,6 @@ div#page_editor { | |||
| 1042 | select { | 1059 | select { |
| 1043 | font-size: 1.5rem; | 1060 | font-size: 1.5rem; |
| 1044 | } | 1061 | } |
| 1045 | |||
| 1046 | #metadata ul#image_box { | ||
| 1047 | width: 100% !important; | ||
| 1048 | } | ||
| 1049 | } | 1062 | } |
| 1050 | 1063 | ||
| 1051 | /* ============================================================ | 1064 | /* ============================================================ |
| @@ -1128,25 +1141,6 @@ div#draft_list table td.actions a { | |||
| 1128 | font-size: 0.9em; | 1141 | font-size: 0.9em; |
| 1129 | } | 1142 | } |
| 1130 | 1143 | ||
| 1131 | #recent_changes_full_list { | ||
| 1132 | list-style: none; | ||
| 1133 | padding: 0; | ||
| 1134 | margin: 0; | ||
| 1135 | } | ||
| 1136 | |||
| 1137 | #recent_changes_full_list li { | ||
| 1138 | padding: 0.75rem 0; | ||
| 1139 | border-bottom: 1px solid #eee; | ||
| 1140 | } | ||
| 1141 | |||
| 1142 | #recent_changes_full_list li:last-child { | ||
| 1143 | border-bottom: none; | ||
| 1144 | } | ||
| 1145 | |||
| 1146 | #recent_changes_full_list li > div { | ||
| 1147 | margin-bottom: 0.25rem; | ||
| 1148 | } | ||
| 1149 | |||
| 1150 | /* ============================================================ | 1144 | /* ============================================================ |
| 1151 | Search widgets | 1145 | Search widgets |
| 1152 | ============================================================ */ | 1146 | ============================================================ */ |
| @@ -1264,31 +1258,6 @@ div#draft_list table td.actions a { | |||
| 1264 | Image box / browser | 1258 | Image box / browser |
| 1265 | ============================================================ */ | 1259 | ============================================================ */ |
| 1266 | 1260 | ||
| 1267 | #metadata ul#image_box { | ||
| 1268 | box-sizing: border-box; | ||
| 1269 | margin: 0; | ||
| 1270 | padding: 10px 5px 10px 5px; | ||
| 1271 | height: 100px; | ||
| 1272 | width: 702px; | ||
| 1273 | border: 1px solid #989898; | ||
| 1274 | } | ||
| 1275 | |||
| 1276 | #metadata ul#image_box li { | ||
| 1277 | float: left; | ||
| 1278 | list-style-type: none; | ||
| 1279 | margin: 5px; | ||
| 1280 | } | ||
| 1281 | |||
| 1282 | div#image_browser { | ||
| 1283 | position: absolute; | ||
| 1284 | top: 40px; | ||
| 1285 | left: 800px; | ||
| 1286 | } | ||
| 1287 | |||
| 1288 | div#image_browser ul li { | ||
| 1289 | list-style-type: none; | ||
| 1290 | } | ||
| 1291 | |||
| 1292 | .thumbnail_list { | 1261 | .thumbnail_list { |
| 1293 | display: flex; | 1262 | display: flex; |
| 1294 | flex-wrap: wrap; | 1263 | flex-wrap: wrap; |
| @@ -1316,6 +1285,30 @@ div#image_browser ul li { | |||
| 1316 | border-radius: 4px; | 1285 | border-radius: 4px; |
| 1317 | } | 1286 | } |
| 1318 | 1287 | ||
| 1288 | .thumbnail_list li { | ||
| 1289 | position: relative; | ||
| 1290 | } | ||
| 1291 | |||
| 1292 | .thumbnail_list li .headline_indicator { | ||
| 1293 | position: absolute; | ||
| 1294 | top: 4px; | ||
| 1295 | right: 4px; | ||
| 1296 | width: 1.4rem; | ||
| 1297 | height: 1.4rem; | ||
| 1298 | display: flex; | ||
| 1299 | align-items: center; | ||
| 1300 | justify-content: center; | ||
| 1301 | border-radius: 50%; | ||
| 1302 | background: rgba(255, 255, 255, 0.85); | ||
| 1303 | color: #f5b400; | ||
| 1304 | } | ||
| 1305 | |||
| 1306 | .thumbnail_list li .headline_indicator svg { | ||
| 1307 | width: 0.9rem; | ||
| 1308 | height: 0.9rem; | ||
| 1309 | fill: currentColor; | ||
| 1310 | } | ||
| 1311 | |||
| 1319 | .related_asset_handle { | 1312 | .related_asset_handle { |
| 1320 | display: flex; | 1313 | display: flex; |
| 1321 | color: #969696; | 1314 | color: #969696; |
diff --git a/public/stylesheets/ccc.css b/public/stylesheets/ccc.css index 8358d06..91f7a06 100644 --- a/public/stylesheets/ccc.css +++ b/public/stylesheets/ccc.css | |||
| @@ -727,7 +727,6 @@ div.teaser_ruler { | |||
| 727 | min-width: 0; | 727 | min-width: 0; |
| 728 | } | 728 | } |
| 729 | 729 | ||
| 730 | |||
| 731 | .inline-image--full { | 730 | .inline-image--full { |
| 732 | width: 100%; | 731 | width: 100%; |
| 733 | margin: 1rem 0; | 732 | margin: 1rem 0; |
| @@ -748,10 +747,88 @@ div.teaser_ruler { | |||
| 748 | margin-left: 1rem; | 747 | margin-left: 1rem; |
| 749 | } | 748 | } |
| 750 | 749 | ||
| 751 | #asset_credits .glightbox-desc { | 750 | #asset_credits .asset_credit_block { |
| 752 | display: none; | 751 | display: none; |
| 753 | } | 752 | } |
| 754 | 753 | ||
| 755 | #asset_credits .headline_credit { | 754 | #asset_credits .asset_credit_block.headline_credit { |
| 756 | display: block; | 755 | display: block; |
| 757 | } | 756 | } |
| 757 | |||
| 758 | #asset_credits { | ||
| 759 | border-top: dotted 1px silver; | ||
| 760 | } | ||
| 761 | |||
| 762 | #asset_credits, | ||
| 763 | .right { | ||
| 764 | font-style: italic; | ||
| 765 | font-family: Georgia; | ||
| 766 | font-size: 0.9rem; | ||
| 767 | color: color-mix(in srgb, CanvasText, #808080); | ||
| 768 | } | ||
| 769 | |||
| 770 | .glightbox-clean .gslide-description { | ||
| 771 | background: Canvas !important; | ||
| 772 | } | ||
| 773 | |||
| 774 | .glightbox-clean .gslide-title, | ||
| 775 | .glightbox-clean .gslide-desc { | ||
| 776 | color: CanvasText !important; | ||
| 777 | } | ||
| 778 | |||
| 779 | .headline_document_card { | ||
| 780 | display: flex; | ||
| 781 | gap: 1rem; | ||
| 782 | align-items: flex-start; | ||
| 783 | border: 1px solid color-mix(in srgb, CanvasText, #808080 25%); | ||
| 784 | border-radius: 6px; | ||
| 785 | padding: 1rem; | ||
| 786 | text-decoration: none; | ||
| 787 | color: inherit; | ||
| 788 | } | ||
| 789 | |||
| 790 | .headline_document_card:hover { | ||
| 791 | border-color: CanvasText; | ||
| 792 | } | ||
| 793 | |||
| 794 | .headline_document_card_thumb { | ||
| 795 | max-width: 120px; | ||
| 796 | max-height: 160px; | ||
| 797 | border: 1px solid color-mix(in srgb, CanvasText, #808080 25%); | ||
| 798 | flex-shrink: 0; | ||
| 799 | } | ||
| 800 | |||
| 801 | .headline_document_card_info { | ||
| 802 | display: flex; | ||
| 803 | align-items: center; | ||
| 804 | gap: 0.5rem; | ||
| 805 | } | ||
| 806 | |||
| 807 | .headline_document_card_title { | ||
| 808 | font-weight: bold; | ||
| 809 | } | ||
| 810 | |||
| 811 | .related_documents { | ||
| 812 | margin-top: 1rem; | ||
| 813 | } | ||
| 814 | |||
| 815 | .related_documents_label { | ||
| 816 | font-style: italic; | ||
| 817 | font-family: Georgia; | ||
| 818 | font-size: 0.9rem; | ||
| 819 | color: color-mix(in srgb, CanvasText, #808080); | ||
| 820 | margin-bottom: 0.3rem; | ||
| 821 | } | ||
| 822 | |||
| 823 | .related_documents_list { | ||
| 824 | list-style: none; | ||
| 825 | padding: 0; | ||
| 826 | margin: 0; | ||
| 827 | } | ||
| 828 | |||
| 829 | .related_document_link { | ||
| 830 | display: inline-flex; | ||
| 831 | align-items: center; | ||
| 832 | gap: 0.4rem; | ||
| 833 | text-decoration: none; | ||
| 834 | } | ||
diff --git a/test/controllers/assets_controller_test.rb b/test/controllers/assets_controller_test.rb index 5f5f6e5..f834541 100644 --- a/test/controllers/assets_controller_test.rb +++ b/test/controllers/assets_controller_test.rb | |||
| @@ -4,17 +4,16 @@ class AssetsControllerTest < ActionController::TestCase | |||
| 4 | 4 | ||
| 5 | def setup | 5 | def setup |
| 6 | login_as :quentin | 6 | login_as :quentin |
| 7 | @existing_asset_ids = Asset.pluck(:id) | ||
| 7 | end | 8 | end |
| 8 | 9 | ||
| 9 | def teardown | 10 | def teardown |
| 10 | # Clean up any files written to disk during tests | 11 | (Asset.pluck(:id) - @existing_asset_ids).each do |id| |
| 11 | Dir.glob(Rails.root.join('public', 'system', 'uploads', 'test_*')).each do |dir| | 12 | dir = Asset.upload_root.join(id.to_s) |
| 13 | raise "Refusing to delete #{dir} -- outside tmp/, Rails.env.test? may be false" unless | ||
| 14 | dir.to_s.start_with?(Rails.root.join("tmp").to_s) | ||
| 12 | FileUtils.rm_rf(dir) | 15 | FileUtils.rm_rf(dir) |
| 13 | end | 16 | end |
| 14 | # Remove uploads created for assets created during tests | ||
| 15 | Asset.where("upload_file_name IS NOT NULL").where("id > 1000000").each do |a| | ||
| 16 | FileUtils.rm_rf(Rails.root.join('public', 'system', 'uploads', a.id.to_s)) | ||
| 17 | end | ||
| 18 | end | 17 | end |
| 19 | 18 | ||
| 20 | # --- index --- | 19 | # --- index --- |
| @@ -64,15 +63,14 @@ class AssetsControllerTest < ActionController::TestCase | |||
| 64 | 63 | ||
| 65 | # original and all four variants should exist on disk | 64 | # original and all four variants should exist on disk |
| 66 | %w[original medium thumb headline large].each do |style| | 65 | %w[original medium thumb headline large].each do |style| |
| 67 | path = Rails.root.join('public', 'system', 'uploads', | 66 | path = asset.send(:file_path, style) |
| 68 | asset.id.to_s, style, 'test_image.png') | ||
| 69 | assert File.exist?(path), "Expected #{style} variant at #{path}" | 67 | assert File.exist?(path), "Expected #{style} variant at #{path}" |
| 70 | end | 68 | end |
| 71 | end | 69 | end |
| 72 | 70 | ||
| 73 | # --- create with PDF --- | 71 | # --- create with PDF --- |
| 74 | 72 | ||
| 75 | test "create asset with PDF upload generates only original" do | 73 | test "create asset with PDF upload generates rasterized variants" do |
| 76 | uploaded = Rack::Test::UploadedFile.new( | 74 | uploaded = Rack::Test::UploadedFile.new( |
| 77 | Rails.root.join('test', 'fixtures', 'files', 'test_document.pdf'), | 75 | Rails.root.join('test', 'fixtures', 'files', 'test_document.pdf'), |
| 78 | 'application/pdf' | 76 | 'application/pdf' |
| @@ -83,18 +81,14 @@ class AssetsControllerTest < ActionController::TestCase | |||
| 83 | assert_response :redirect | 81 | assert_response :redirect |
| 84 | 82 | ||
| 85 | asset = Asset.last | 83 | asset = Asset.last |
| 86 | assert_equal 'test_document.pdf', asset.upload_file_name | 84 | original_path = asset.send(:file_path, :original) |
| 87 | assert_equal 'application/pdf', asset.upload_content_type | ||
| 88 | |||
| 89 | # only original should exist, no image variants | ||
| 90 | original_path = Rails.root.join('public', 'system', 'uploads', | ||
| 91 | asset.id.to_s, 'original', 'test_document.pdf') | ||
| 92 | assert File.exist?(original_path), "Expected original at #{original_path}" | 85 | assert File.exist?(original_path), "Expected original at #{original_path}" |
| 86 | assert_equal 'test_document.pdf', File.basename(original_path) | ||
| 93 | 87 | ||
| 94 | %w[medium thumb headline large].each do |style| | 88 | %w[medium thumb headline large].each do |style| |
| 95 | path = Rails.root.join('public', 'system', 'uploads', | 89 | path = asset.send(:file_path, style) |
| 96 | asset.id.to_s, style, 'test_document.pdf') | 90 | assert File.exist?(path), "Expected a #{style} variant at #{path}" |
| 97 | assert !File.exist?(path), "Expected no #{style} variant for PDF" | 91 | assert_equal '.png', File.extname(path), "Expected #{style} variant to be a PNG, not a PDF" |
| 98 | end | 92 | end |
| 99 | end | 93 | end |
| 100 | 94 | ||
| @@ -137,7 +131,7 @@ class AssetsControllerTest < ActionController::TestCase | |||
| 137 | ) | 131 | ) |
| 138 | post :create, params: { asset: { name: 'To be deleted', upload: uploaded } } | 132 | post :create, params: { asset: { name: 'To be deleted', upload: uploaded } } |
| 139 | asset = Asset.last | 133 | asset = Asset.last |
| 140 | upload_dir = Rails.root.join('public', 'system', 'uploads', asset.id.to_s) | 134 | upload_dir = asset.send(:upload_root).join(asset.id.to_s) |
| 141 | assert Dir.exist?(upload_dir), "Upload directory should exist before destroy" | 135 | assert Dir.exist?(upload_dir), "Upload directory should exist before destroy" |
| 142 | 136 | ||
| 143 | assert_difference 'Asset.count', -1 do | 137 | assert_difference 'Asset.count', -1 do |
diff --git a/test/controllers/nodes_controller_test.rb b/test/controllers/nodes_controller_test.rb index ddc4565..b8f9278 100644 --- a/test/controllers/nodes_controller_test.rb +++ b/test/controllers/nodes_controller_test.rb | |||
| @@ -514,12 +514,6 @@ class NodesControllerTest < ActionController::TestCase | |||
| 514 | assert_includes assigns(:nodes), chaostreff_node | 514 | assert_includes assigns(:nodes), chaostreff_node |
| 515 | end | 515 | end |
| 516 | 516 | ||
| 517 | test "recent combined with a search term does not raise an ambiguous column error" do | ||
| 518 | login_as :quentin | ||
| 519 | get :recent, params: { :q => "Zombies" } | ||
| 520 | assert_response :success | ||
| 521 | end | ||
| 522 | |||
| 523 | test "drafts combined with a search term does not raise an ambiguous column error" do | 517 | test "drafts combined with a search term does not raise an ambiguous column error" do |
| 524 | login_as :quentin | 518 | login_as :quentin |
| 525 | get :drafts, params: { :q => "Zombies" } | 519 | get :drafts, params: { :q => "Zombies" } |
diff --git a/test/controllers/pages_controller_test.rb b/test/controllers/pages_controller_test.rb index 3879014..732869b 100644 --- a/test/controllers/pages_controller_test.rb +++ b/test/controllers/pages_controller_test.rb | |||
| @@ -1,5 +1,72 @@ | |||
| 1 | require 'test_helper' | 1 | require 'test_helper' |
| 2 | 2 | ||
| 3 | class PagesControllerTest < ActionController::TestCase | 3 | class PagesControllerTest < ActionController::TestCase |
| 4 | # will be removed anyway | 4 | test "preview shows the autosave when a draft and an autosave both exist" do |
| 5 | login_as :quentin | ||
| 6 | |||
| 7 | node = Node.root.children.create!(:slug => "preview_retest") | ||
| 8 | node.draft.update!(:title => "draft title") | ||
| 9 | node.publish_draft! | ||
| 10 | |||
| 11 | node.draft | ||
| 12 | node.lock_for_editing!(users(:quentin)) | ||
| 13 | node.autosave!({ :title => "draft title" }, users(:quentin)) | ||
| 14 | node.save_draft!(users(:quentin)) | ||
| 15 | node.autosave!({ :title => "autosave title" }, users(:quentin)) | ||
| 16 | |||
| 17 | get :preview, params: { :id => node.draft_id } | ||
| 18 | |||
| 19 | assert_response :success | ||
| 20 | assert_match "autosave title", response.body | ||
| 21 | end | ||
| 22 | |||
| 23 | test "preview renders a headlined image on the autosave without crashing" do | ||
| 24 | login_as :quentin | ||
| 25 | |||
| 26 | node = Node.root.children.create!(:slug => "preview_retest_with_image") | ||
| 27 | node.lock_for_editing!(users(:quentin)) | ||
| 28 | node.autosave!({ :title => "draft title" }, users(:quentin)) | ||
| 29 | node.save_draft!(users(:quentin)) | ||
| 30 | node.autosave!({ :title => "autosave title" }, users(:quentin)) | ||
| 31 | |||
| 32 | asset = Asset.create!(:name => "test", :upload_content_type => "image/png") | ||
| 33 | node.autosave.related_assets.create!(:asset_id => asset.id, :position => 1, :headline => true) | ||
| 34 | |||
| 35 | get :preview, params: { :id => node.draft_id } | ||
| 36 | |||
| 37 | assert_response :success | ||
| 38 | assert_match "autosave title", response.body | ||
| 39 | end | ||
| 40 | |||
| 41 | test "preview shows the autosave when no draft exists at all" do | ||
| 42 | login_as :quentin | ||
| 43 | |||
| 44 | node = Node.root.children.create!(:slug => "preview_retest_no_draft") | ||
| 45 | node.draft.destroy | ||
| 46 | node.update_column(:draft_id, nil) | ||
| 47 | |||
| 48 | node.lock_for_editing!(users(:quentin)) | ||
| 49 | node.autosave!({ :title => "autosave only title" }, users(:quentin)) | ||
| 50 | |||
| 51 | asset = Asset.create!(:name => "test", :upload_content_type => "image/png") | ||
| 52 | node.autosave.related_assets.create!(:asset_id => asset.id, :position => 1) | ||
| 53 | |||
| 54 | get :preview, params: { :id => node.autosave_id } | ||
| 55 | |||
| 56 | assert_response :success | ||
| 57 | assert_match "autosave only title", response.body | ||
| 58 | end | ||
| 59 | |||
| 60 | test "preview shows head normally when there is no draft or autosave" do | ||
| 61 | login_as :quentin | ||
| 62 | |||
| 63 | node = Node.root.children.create!(:slug => "preview_retest_head_only") | ||
| 64 | node.draft.update!(:title => "head title") | ||
| 65 | node.publish_draft! | ||
| 66 | |||
| 67 | get :preview, params: { :id => node.head_id } | ||
| 68 | |||
| 69 | assert_response :success | ||
| 70 | assert_match "head title", response.body | ||
| 71 | end | ||
| 5 | end | 72 | end |
diff --git a/test/controllers/related_assets_controller_test.rb b/test/controllers/related_assets_controller_test.rb index 2384adc..ced4b74 100644 --- a/test/controllers/related_assets_controller_test.rb +++ b/test/controllers/related_assets_controller_test.rb | |||
| @@ -134,4 +134,29 @@ class RelatedAssetsControllerTest < ActionController::TestCase | |||
| 134 | assert_response :success | 134 | assert_response :success |
| 135 | assert_not related.reload.headline? | 135 | assert_not related.reload.headline? |
| 136 | end | 136 | end |
| 137 | |||
| 138 | test "search includes PDF assets as headline-eligible candidates" do | ||
| 139 | login_as :quentin | ||
| 140 | node = Node.root.children.create!(:slug => "related_assets_search_pdf_test") | ||
| 141 | asset = Asset.create!(:name => "expert-opinion-searchable", :upload_content_type => "application/pdf") | ||
| 142 | |||
| 143 | get :search, params: { :node_id => node.id, :search_term => "expert-opinion-searchable" } | ||
| 144 | |||
| 145 | assert_response :success | ||
| 146 | ids = JSON.parse(response.body).map { |r| r["id"] } | ||
| 147 | assert_includes ids, asset.id | ||
| 148 | end | ||
| 149 | |||
| 150 | test "search matches by filename as well as name" do | ||
| 151 | login_as :quentin | ||
| 152 | node = Node.root.children.create!(:slug => "related_assets_search_filename_test") | ||
| 153 | asset = Asset.create!(:name => "Untitled", :upload_content_type => "application/pdf", | ||
| 154 | :upload_file_name => "Stellungnahme_Patientendaten_Schutz.pdf") | ||
| 155 | |||
| 156 | get :search, params: { :node_id => node.id, :search_term => "Patientendaten" } | ||
| 157 | |||
| 158 | assert_response :success | ||
| 159 | ids = JSON.parse(response.body).map { |r| r["id"] } | ||
| 160 | assert_includes ids, asset.id | ||
| 161 | end | ||
| 137 | end | 162 | end |
diff --git a/test/models/asset_test.rb b/test/models/asset_test.rb index d246abe..ab1cc5d 100644 --- a/test/models/asset_test.rb +++ b/test/models/asset_test.rb | |||
| @@ -41,4 +41,16 @@ class AssetTest < ActiveSupport::TestCase | |||
| 41 | test "license_key may be blank" do | 41 | test "license_key may be blank" do |
| 42 | assert Asset.new(:license_key => nil).valid? | 42 | assert Asset.new(:license_key => nil).valid? |
| 43 | end | 43 | end |
| 44 | |||
| 45 | test "pdf? is true only for application/pdf" do | ||
| 46 | assert Asset.new(:upload_content_type => "application/pdf").pdf? | ||
| 47 | assert_not Asset.new(:upload_content_type => "image/png").pdf? | ||
| 48 | end | ||
| 49 | |||
| 50 | test "show_credit? is false for a PDF even with every credit field present" do | ||
| 51 | asset = Asset.new(:name => "demo", :upload_content_type => "application/pdf", | ||
| 52 | :creator => "Jane Doe", :source_url => "https://example.org", :license_key => "cc_by_4") | ||
| 53 | assert asset.has_credit? | ||
| 54 | assert_not asset.show_credit? | ||
| 55 | end | ||
| 44 | end | 56 | end |
diff --git a/test/models/helpers/content_helper_test.rb b/test/models/helpers/content_helper_test.rb index d6c7b43..c69ef4e 100644 --- a/test/models/helpers/content_helper_test.rb +++ b/test/models/helpers/content_helper_test.rb | |||
| @@ -79,4 +79,50 @@ class ContentHelperTest < ActionView::TestCase | |||
| 79 | 79 | ||
| 80 | I18n.with_locale(:en) { assert_match t(:open_gallery), headline_image } | 80 | I18n.with_locale(:en) { assert_match t(:open_gallery), headline_image } |
| 81 | end | 81 | end |
| 82 | |||
| 83 | test "headline_image renders a document card for a PDF headline, not a lightbox image" do | ||
| 84 | node = Node.root.children.create!(:slug => "headline_image_pdf_test") | ||
| 85 | asset = Asset.create!(:name => "Expert Opinion", :upload_content_type => "application/pdf") | ||
| 86 | node.draft.assets << asset | ||
| 87 | node.draft.related_assets.find_by(:asset_id => asset.id).update!(:headline => true) | ||
| 88 | @page = node.draft | ||
| 89 | |||
| 90 | result = headline_image | ||
| 91 | |||
| 92 | assert_match "headline_document_card", result | ||
| 93 | assert_match "Expert Opinion", result | ||
| 94 | assert_no_match "data-gallery", result | ||
| 95 | end | ||
| 96 | |||
| 97 | test "headline_image lists other attached PDFs below the headline" do | ||
| 98 | node = Node.root.children.create!(:slug => "headline_image_multi_pdf_test") | ||
| 99 | headline_pdf = Asset.create!(:name => "Main Filing", :upload_content_type => "application/pdf") | ||
| 100 | other_pdf = Asset.create!(:name => "Supplementary Exhibit", :upload_content_type => "application/pdf") | ||
| 101 | node.draft.assets << headline_pdf | ||
| 102 | node.draft.assets << other_pdf | ||
| 103 | node.draft.related_assets.find_by(:asset_id => headline_pdf.id).update!(:headline => true) | ||
| 104 | @page = node.draft | ||
| 105 | |||
| 106 | result = headline_image | ||
| 107 | |||
| 108 | assert_match "Main Filing", result | ||
| 109 | assert_match "Supplementary Exhibit", result | ||
| 110 | assert_match "headline_document_card", result | ||
| 111 | assert_match "related_documents_list", result | ||
| 112 | end | ||
| 113 | |||
| 114 | test "headline_image lists attached PDFs even with no headline chosen" do | ||
| 115 | node = Node.root.children.create!(:slug => "headline_image_pdf_no_headline_test") | ||
| 116 | pdf_a = Asset.create!(:name => "Document A", :upload_content_type => "application/pdf") | ||
| 117 | pdf_b = Asset.create!(:name => "Document B", :upload_content_type => "application/pdf") | ||
| 118 | node.draft.assets << pdf_a | ||
| 119 | node.draft.assets << pdf_b | ||
| 120 | @page = node.draft | ||
| 121 | |||
| 122 | result = headline_image | ||
| 123 | |||
| 124 | assert_no_match "headline_document_card", result | ||
| 125 | assert_match "Document A", result | ||
| 126 | assert_match "Document B", result | ||
| 127 | end | ||
| 82 | end | 128 | end |
diff --git a/test/models/node_attach_asset_test.rb b/test/models/node_attach_asset_test.rb new file mode 100644 index 0000000..cb5b60f --- /dev/null +++ b/test/models/node_attach_asset_test.rb | |||
| @@ -0,0 +1,107 @@ | |||
| 1 | require "test_helper" | ||
| 2 | |||
| 3 | class NodeAttachAssetTest < ActiveSupport::TestCase | ||
| 4 | |||
| 5 | def setup | ||
| 6 | @user = users(:quentin) | ||
| 7 | @other = users(:aaron) | ||
| 8 | @node = Node.root.children.create!(:slug => "attach_asset_test") | ||
| 9 | @image = create_image_asset | ||
| 10 | end | ||
| 11 | |||
| 12 | test "attaches to a draft-only node" do | ||
| 13 | result = @node.attach_asset!(@image, :user => @user) | ||
| 14 | assert_equal 1, result[:attached] | ||
| 15 | assert_includes @node.draft.assets, @image | ||
| 16 | end | ||
| 17 | |||
| 18 | test "attaches to head when no draft is pending" do | ||
| 19 | @node.publish_draft!(@user) | ||
| 20 | result = @node.attach_asset!(@image, :user => @user) | ||
| 21 | assert_equal 1, result[:attached] | ||
| 22 | assert_includes @node.head.assets, @image | ||
| 23 | end | ||
| 24 | |||
| 25 | test "attaches to head and pending draft alike" do | ||
| 26 | @node.publish_draft!(@user) | ||
| 27 | @node.lock_for_editing!(@user) | ||
| 28 | @node.create_new_draft(@user) | ||
| 29 | result = @node.attach_asset!(@image, :user => @user) | ||
| 30 | assert_equal 2, result[:attached] | ||
| 31 | assert_includes @node.head.assets, @image | ||
| 32 | assert_includes @node.draft.assets, @image | ||
| 33 | end | ||
| 34 | |||
| 35 | test "attaches to all three lifecycle rows" do | ||
| 36 | @node.publish_draft!(@user) | ||
| 37 | @node.lock_for_editing!(@user) | ||
| 38 | @node.create_new_draft(@user) | ||
| 39 | @node.autosave!({ :title => "wip" }, @user) | ||
| 40 | result = @node.attach_asset!(@image, :user => @user) | ||
| 41 | assert_equal 3, result[:attached] | ||
| 42 | [@node.head, @node.draft, @node.autosave].each do |row| | ||
| 43 | assert_includes row.assets, @image | ||
| 44 | end | ||
| 45 | end | ||
| 46 | |||
| 47 | test "skips rows that already carry the asset" do | ||
| 48 | @node.draft.related_assets.create!(:asset => @image) | ||
| 49 | @node.publish_draft!(@user) | ||
| 50 | @node.lock_for_editing!(@user) | ||
| 51 | @node.create_new_draft(@user) | ||
| 52 | result = @node.attach_asset!(@image, :user => @user) | ||
| 53 | assert_equal 0, result[:attached] | ||
| 54 | assert_equal 2, result[:already] | ||
| 55 | assert_equal 1, @node.draft.related_assets.where(:asset_id => @image.id).count | ||
| 56 | end | ||
| 57 | |||
| 58 | test "refuses when another user holds the lock and writes nothing" do | ||
| 59 | @node.lock_for_editing!(@other) | ||
| 60 | assert_raises(LockedByAnotherUser) { @node.attach_asset!(@image, :user => @user) } | ||
| 61 | assert_empty @node.draft.assets.reload | ||
| 62 | end | ||
| 63 | |||
| 64 | test "proceeds when the attaching user holds the lock" do | ||
| 65 | @node.lock_for_editing!(@user) | ||
| 66 | result = @node.attach_asset!(@image, :user => @user) | ||
| 67 | assert_equal 1, result[:attached] | ||
| 68 | end | ||
| 69 | |||
| 70 | test "sets the headline when none exists" do | ||
| 71 | result = @node.attach_asset!(@image, :user => @user, :headline => true) | ||
| 72 | assert_equal :set, result[:headline] | ||
| 73 | assert_equal @image, @node.draft.headline_asset | ||
| 74 | end | ||
| 75 | |||
| 76 | test "keeps an existing headline and reports it" do | ||
| 77 | incumbent = create_image_asset | ||
| 78 | @node.draft.related_assets.create!(:asset => incumbent, :headline => true) | ||
| 79 | result = @node.attach_asset!(@image, :user => @user, :headline => true) | ||
| 80 | assert_equal :kept_existing, result[:headline] | ||
| 81 | assert_equal incumbent, @node.draft.reload.headline_asset | ||
| 82 | assert_includes @node.draft.assets, @image | ||
| 83 | end | ||
| 84 | |||
| 85 | test "declines the headline flag for ineligible asset types" do | ||
| 86 | plain = create_plain_asset | ||
| 87 | result = @node.attach_asset!(plain, :user => @user, :headline => true) | ||
| 88 | assert_equal :not_eligible, result[:headline] | ||
| 89 | assert_includes @node.draft.assets.reload, plain | ||
| 90 | assert_nil @node.draft.headline_asset | ||
| 91 | end | ||
| 92 | |||
| 93 | test "refuses nodes in the Trash" do | ||
| 94 | @node.trash!(@user) | ||
| 95 | assert_raises(ActiveRecord::RecordInvalid) { @node.attach_asset!(@image, :user => @user) } | ||
| 96 | end | ||
| 97 | |||
| 98 | private | ||
| 99 | |||
| 100 | def create_image_asset | ||
| 101 | Asset.create!(:name => "attach test image", :upload_content_type => "image/png") | ||
| 102 | end | ||
| 103 | |||
| 104 | def create_plain_asset | ||
| 105 | Asset.create!(:name => "attach test note", :upload_content_type => "text/plain") | ||
| 106 | end | ||
| 107 | end | ||
diff --git a/test/models/node_test.rb b/test/models/node_test.rb index ab66f81..ba38340 100644 --- a/test/models/node_test.rb +++ b/test/models/node_test.rb | |||
| @@ -268,7 +268,6 @@ class NodeTest < ActiveSupport::TestCase | |||
| 268 | assert_equal "quentin", node.head.user.login | 268 | assert_equal "quentin", node.head.user.login |
| 269 | end | 269 | end |
| 270 | 270 | ||
| 271 | |||
| 272 | test "update?" do | 271 | test "update?" do |
| 273 | Node.root.descendants.delete_all | 272 | Node.root.descendants.delete_all |
| 274 | updates = Node.root.children.create!( :slug => "updates" ) | 273 | updates = Node.root.children.create!( :slug => "updates" ) |
| @@ -359,6 +358,16 @@ class NodeTest < ActiveSupport::TestCase | |||
| 359 | assert_equal @user1, node.reload.lock_owner | 358 | assert_equal @user1, node.reload.lock_owner |
| 360 | end | 359 | end |
| 361 | 360 | ||
| 361 | test "title reads from autosave when neither draft nor head exists yet" do | ||
| 362 | node = Node.root.children.create!(:slug => "title_autosave_only_test") | ||
| 363 | node.draft.destroy | ||
| 364 | node.update_column(:draft_id, nil) | ||
| 365 | node.lock_for_editing!(users(:quentin)) | ||
| 366 | node.autosave!({ :title => "autosave-only title" }, users(:quentin)) | ||
| 367 | |||
| 368 | assert_equal "autosave-only title", node.reload.title | ||
| 369 | end | ||
| 370 | |||
| 362 | test "revert! is a safe no-op on a fresh node with only a draft" do | 371 | test "revert! is a safe no-op on a fresh node with only a draft" do |
| 363 | node = create_node_with_draft | 372 | node = create_node_with_draft |
| 364 | node.lock_for_editing!(@user1) | 373 | node.lock_for_editing!(@user1) |
| @@ -537,30 +546,6 @@ class NodeTest < ActiveSupport::TestCase | |||
| 537 | assert result.index(mine) < result.index(someone_elses_newer) | 546 | assert result.index(mine) < result.index(someone_elses_newer) |
| 538 | end | 547 | end |
| 539 | 548 | ||
| 540 | test "recently_changed includes a node whose head was recently published" do | ||
| 541 | node = Node.root.children.create!(:slug => "recent_changed_published") | ||
| 542 | find_or_create_draft(node, @user1) | ||
| 543 | node.autosave!({:title => "v1"}, @user1) | ||
| 544 | node.save_draft!(@user1) | ||
| 545 | node.publish_draft! | ||
| 546 | |||
| 547 | assert_includes Node.recently_changed, node | ||
| 548 | end | ||
| 549 | |||
| 550 | test "recently_changed excludes a node only touched by locking or unlocking after an old publish" do | ||
| 551 | node = Node.root.children.create!(:slug => "recent_changed_lock_only") | ||
| 552 | find_or_create_draft(node, @user1) | ||
| 553 | node.autosave!({:title => "v1"}, @user1) | ||
| 554 | node.save_draft!(@user1) | ||
| 555 | node.publish_draft! | ||
| 556 | node.head.update_column(:updated_at, 20.days.ago) | ||
| 557 | |||
| 558 | node.lock_for_editing!(@user1) | ||
| 559 | node.unlock! | ||
| 560 | |||
| 561 | assert_not_includes Node.recently_changed, node | ||
| 562 | end | ||
| 563 | |||
| 564 | test "autosave! carries over the current related assets to the newly created autosave row" do | 549 | test "autosave! carries over the current related assets to the newly created autosave row" do |
| 565 | node = Node.root.children.create!(:slug => "autosave_asset_carryover_test") | 550 | node = Node.root.children.create!(:slug => "autosave_asset_carryover_test") |
| 566 | user = User.find_by_login("quentin") | 551 | user = User.find_by_login("quentin") |
diff --git a/test/models/page_test.rb b/test/models/page_test.rb index 1f924f9..98a00d2 100644 --- a/test/models/page_test.rb +++ b/test/models/page_test.rb | |||
| @@ -309,11 +309,15 @@ class PageTest < ActiveSupport::TestCase | |||
| 309 | 309 | ||
| 310 | kept_asset = Asset.create!(:upload_file_name => "kept.png", :upload_content_type => "image/png", :upload_file_size => 1) | 310 | kept_asset = Asset.create!(:upload_file_name => "kept.png", :upload_content_type => "image/png", :upload_file_size => 1) |
| 311 | removed_asset = Asset.create!(:upload_file_name => "removed.pdf", :upload_content_type => "application/pdf", :upload_file_size => 1) | 311 | removed_asset = Asset.create!(:upload_file_name => "removed.pdf", :upload_content_type => "application/pdf", :upload_file_size => 1) |
| 312 | n.head.update_assets([kept_asset.id, removed_asset.id]) | 312 | n.head.related_assets.delete_all |
| 313 | n.head.related_assets.create!(:asset_id => kept_asset.id, :position => 1) | ||
| 314 | n.head.related_assets.create!(:asset_id => removed_asset.id, :position => 2) | ||
| 313 | 315 | ||
| 314 | d2 = find_or_create_draft(n, @user1) | 316 | d2 = find_or_create_draft(n, @user1) |
| 315 | added_asset = Asset.create!(:upload_file_name => "added.png", :upload_content_type => "image/png", :upload_file_size => 1) | 317 | added_asset = Asset.create!(:upload_file_name => "added.png", :upload_content_type => "image/png", :upload_file_size => 1) |
| 316 | d2.update_assets([kept_asset.id, added_asset.id]) | 318 | d2.related_assets.delete_all |
| 319 | d2.related_assets.create!(:asset_id => kept_asset.id, :position => 1) | ||
| 320 | d2.related_assets.create!(:asset_id => added_asset.id, :position => 2) | ||
| 317 | d2.save! | 321 | d2.save! |
| 318 | 322 | ||
| 319 | diff = d2.diff_against(n.head) | 323 | diff = d2.diff_against(n.head) |
diff --git a/test/models/related_asset_test.rb b/test/models/related_asset_test.rb index 710b4cc..bb86ddb 100644 --- a/test/models/related_asset_test.rb +++ b/test/models/related_asset_test.rb | |||
| @@ -11,15 +11,25 @@ class RelatedAssetTest < ActiveSupport::TestCase | |||
| 11 | assert related.valid? | 11 | assert related.valid? |
| 12 | end | 12 | end |
| 13 | 13 | ||
| 14 | test "headline cannot be set on a non-image asset" do | 14 | test "headline can be set on a PDF asset" do |
| 15 | node = Node.root.children.create!(:slug => "related_asset_headline_pdf_test") | 15 | node = Node.root.children.create!(:slug => "related_asset_headline_pdf_test") |
| 16 | asset = Asset.create!(:name => "programme", :upload_content_type => "application/pdf") | 16 | asset = Asset.create!(:name => "expert opinion", :upload_content_type => "application/pdf") |
| 17 | node.draft.assets << asset | ||
| 18 | related = node.draft.related_assets.find_by(:asset_id => asset.id) | ||
| 19 | |||
| 20 | related.headline = true | ||
| 21 | assert related.valid? | ||
| 22 | end | ||
| 23 | |||
| 24 | test "headline cannot be set on a non-image, non-PDF asset" do | ||
| 25 | node = Node.root.children.create!(:slug => "related_asset_headline_text_test") | ||
| 26 | asset = Asset.create!(:name => "programme", :upload_content_type => "text/plain") | ||
| 17 | node.draft.assets << asset | 27 | node.draft.assets << asset |
| 18 | related = node.draft.related_assets.find_by(:asset_id => asset.id) | 28 | related = node.draft.related_assets.find_by(:asset_id => asset.id) |
| 19 | 29 | ||
| 20 | related.headline = true | 30 | related.headline = true |
| 21 | assert_not related.valid? | 31 | assert_not related.valid? |
| 22 | assert_includes related.errors[:headline], "can only be set on image assets" | 32 | assert_includes related.errors[:headline], "can only be set on image or PDF assets" |
| 23 | end | 33 | end |
| 24 | 34 | ||
| 25 | test "the headline validation does not raise when asset is missing" do | 35 | test "the headline validation does not raise when asset is missing" do |
