diff options
| author | erdgeist <erdgeist@erdgeist.org> | 2026-07-21 23:17:56 +0200 |
|---|---|---|
| committer | erdgeist <erdgeist@erdgeist.org> | 2026-07-21 23:17:56 +0200 |
| commit | 8f3f58e3010fa7c923b77b7d2050175890b58f9b (patch) | |
| tree | ece76f69129b8b6c3c59b0b71f69e9fc59950d30 | |
| parent | 18114a978b7c037da5194c48f860435e261f9a0b (diff) | |
Render PDF headlines as document cards, not lightbox images
A starred PDF previously ran through the same crop-and-lightbox path
a photo does -- exactly the awkward treatment explicit headline
designation was meant to avoid. _headline_image.html.erb now branches
on @headline_asset.pdf?: a PDF renders as a linked card (a :medium
thumbnail, a file icon, its name), no gallery participation at all.
The existing image-headline and gallery-fallback logic is otherwise
unchanged, now scoped to an image-specific headline only.
Other attached PDFs -- headlined or not -- list below as plain links,
same reasoning as why non-headline photos still get a gallery
trigger: an attached document shouldn't go invisible just because
nothing's been starred yet.
headline_image's own render guard needed widening to cover a page
with PDFs attached but no images and no headline at all -- the one
case none of the existing conditions accounted for.
| -rw-r--r-- | app/helpers/content_helper.rb | 2 | ||||
| -rw-r--r-- | app/models/asset.rb | 2 | ||||
| -rw-r--r-- | app/views/content/_headline_image.html.erb | 42 | ||||
| -rw-r--r-- | config/locales/de.yml | 2 | ||||
| -rw-r--r-- | config/locales/en.yml | 2 | ||||
| -rw-r--r-- | public/stylesheets/ccc.css | 57 | ||||
| -rw-r--r-- | test/models/helpers/content_helper_test.rb | 46 |
7 files changed, 145 insertions, 8 deletions
diff --git a/app/helpers/content_helper.rb b/app/helpers/content_helper.rb index 459d9e0..7a52d9c 100644 --- a/app/helpers/content_helper.rb +++ b/app/helpers/content_helper.rb | |||
| @@ -54,7 +54,7 @@ module ContentHelper | |||
| 54 | 54 | ||
| 55 | def headline_image | 55 | def headline_image |
| 56 | @headline_asset = @page.headline_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 |
diff --git a/app/models/asset.rb b/app/models/asset.rb index 9feefa9..ba9c2f0 100644 --- a/app/models/asset.rb +++ b/app/models/asset.rb | |||
| @@ -10,6 +10,8 @@ class Asset < ApplicationRecord | |||
| 10 | scope :images, -> { where(:upload_content_type => IMAGE_CONTENT_TYPES) } | 10 | scope :images, -> { where(:upload_content_type => IMAGE_CONTENT_TYPES) } |
| 11 | 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"]) } |
| 12 | 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 | |||
| 13 | scope :headline_eligible, -> { where(:upload_content_type => IMAGE_CONTENT_TYPES + [PDF_CONTENT_TYPE]) } | 15 | scope :headline_eligible, -> { where(:upload_content_type => IMAGE_CONTENT_TYPES + [PDF_CONTENT_TYPE]) } |
| 14 | 16 | ||
| 15 | validates :license_key, inclusion: { in: -> { AssetLicense.keys } }, allow_blank: true | 17 | validates :license_key, inclusion: { in: -> { AssetLicense.keys } }, allow_blank: true |
diff --git a/app/views/content/_headline_image.html.erb b/app/views/content/_headline_image.html.erb index 13a4ac7..4a3dfdc 100644 --- a/app/views/content/_headline_image.html.erb +++ b/app/views/content/_headline_image.html.erb | |||
| @@ -1,13 +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}", | 20 | :data => { :gallery => "page-#{@page.node.id}", |
| 9 | :glightbox => glightbox_data(@headline_asset, @headline_asset.name), | 21 | :glightbox => glightbox_data(image_headline, image_headline.name), |
| 10 | :"credit-selector" => (@headline_asset.show_credit? ? "#credit_for_asset_#{@headline_asset.id}" : nil) } | 22 | :"credit-selector" => (image_headline.show_credit? ? "#credit_for_asset_#{image_headline.id}" : nil) } |
| 11 | ) %> | 23 | ) %> |
| 12 | <% if gallery_images.size > 1 %> | 24 | <% if gallery_images.size > 1 %> |
| 13 | <div class="right"><%= "#{gallery_images.size} #{t(:images)}" %></div> | 25 | <div class="right"><%= "#{gallery_images.size} #{t(:images)}" %></div> |
| @@ -22,10 +34,26 @@ | |||
| 22 | <% end %> | 34 | <% end %> |
| 23 | 35 | ||
| 24 | <% gallery_images.each do |image| %> | 36 | <% gallery_images.each do |image| %> |
| 25 | <% next if image == @headline_asset %> | 37 | <% next if image == image_headline %> |
| 26 | <% next if !@headline_asset && image == gallery_images.first %> | 38 | <% next if !image_headline && image == gallery_images.first %> |
| 27 | <%= link_to "", image.upload.url, :class => "glightbox", :style => "display: none", | 39 | <%= link_to "", image.upload.url, :class => "glightbox", :style => "display: none", |
| 28 | :data => { :gallery => "page-#{@page.node.id}", | 40 | :data => { :gallery => "page-#{@page.node.id}", |
| 29 | :glightbox => glightbox_data(image, image.name), | 41 | :glightbox => glightbox_data(image, image.name), |
| 30 | :"credit-selector" => (image.show_credit? ? "#credit_for_asset_#{image.id}" : nil) } %> | 42 | :"credit-selector" => (image.show_credit? ? "#credit_for_asset_#{image.id}" : nil) } %> |
| 31 | <% end %> | 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> | ||
| 59 | <% 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/public/stylesheets/ccc.css b/public/stylesheets/ccc.css index 9654594..91f7a06 100644 --- a/public/stylesheets/ccc.css +++ b/public/stylesheets/ccc.css | |||
| @@ -775,3 +775,60 @@ div.teaser_ruler { | |||
| 775 | .glightbox-clean .gslide-desc { | 775 | .glightbox-clean .gslide-desc { |
| 776 | color: CanvasText !important; | 776 | color: CanvasText !important; |
| 777 | } | 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/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 |
