summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/controllers/pages_controller.rb20
-rw-r--r--app/controllers/related_assets_controller.rb5
-rw-r--r--app/controllers/shared_previews_controller.rb2
-rw-r--r--app/helpers/content_helper.rb10
-rw-r--r--app/models/asset.rb16
-rw-r--r--app/models/concerns/file_attachment.rb44
-rw-r--r--app/models/page.rb26
-rw-r--r--app/models/related_asset.rb2
-rw-r--r--app/views/assets/show.html.erb1
-rw-r--r--app/views/content/_asset_credits.html.erb6
-rw-r--r--app/views/content/_headline_image.html.erb53
-rw-r--r--app/views/custom/partials/_chapter.html.erb4
-rw-r--r--app/views/nodes/edit.html.erb3
-rw-r--r--app/views/nodes/show.html.erb14
-rw-r--r--config/locales/de.yml2
-rw-r--r--config/locales/en.yml2
-rw-r--r--config/routes.rb1
-rw-r--r--lib/tasks/assets.rake6
-rw-r--r--public/javascripts/admin_interface.js89
-rw-r--r--public/javascripts/public.js21
-rw-r--r--public/javascripts/related_assets.js1
-rw-r--r--public/stylesheets/admin.css53
-rw-r--r--public/stylesheets/ccc.css83
-rw-r--r--test/controllers/assets_controller_test.rb32
-rw-r--r--test/controllers/pages_controller_test.rb69
-rw-r--r--test/controllers/related_assets_controller_test.rb25
-rw-r--r--test/models/asset_test.rb12
-rw-r--r--test/models/helpers/content_helper_test.rb46
-rw-r--r--test/models/page_test.rb8
-rw-r--r--test/models/related_asset_test.rb16
30 files changed, 463 insertions, 209 deletions
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
25end 31end
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
176end 180end
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 @@
1class Asset < ApplicationRecord 1class 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
18end 34end
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/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
17end 17end
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/nodes/edit.html.erb b/app/views/nodes/edit.html.erb
index da682f0..12dc000 100644
--- a/app/views/nodes/edit.html.erb
+++ b/app/views/nodes/edit.html.erb
@@ -86,6 +86,7 @@
86 data-large-url="<%= related.asset.upload.url(:large) %>" 86 data-large-url="<%= related.asset.upload.url(:large) %>"
87 data-original-url="<%= related.asset.upload.url %>" 87 data-original-url="<%= related.asset.upload.url %>"
88 data-name="<%= related.asset.name %>" 88 data-name="<%= related.asset.name %>"
89 data-has-credit="<%= related.asset.show_credit? %>"
89 data-headline="<%= related.headline? %>" 90 data-headline="<%= related.headline? %>"
90 class="<%= "is_headline" if related.headline? %>"> 91 class="<%= "is_headline" if related.headline? %>">
91 <span class="related_asset_handle"><%= icon("grip-vertical", library: "tabler", "aria-hidden": true) %></span> 92 <span class="related_asset_handle"><%= icon("grip-vertical", library: "tabler", "aria-hidden": true) %></span>
@@ -101,7 +102,7 @@
101 </li> 102 </li>
102 <% end %> 103 <% end %>
103 </ul> 104 </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> 105 <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" %> 106 <%= 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> 107 <div id="related_asset_search_results" class="search_results"></div>
107 </div> 108 </div>
diff --git a/app/views/nodes/show.html.erb b/app/views/nodes/show.html.erb
index af05778..adf79dd 100644
--- a/app/views/nodes/show.html.erb
+++ b/app/views/nodes/show.html.erb
@@ -253,12 +253,20 @@
253 <% end %> 253 <% end %>
254 </div> 254 </div>
255 255
256 <% if @page.assets.images.any? %> 256 <% if @page.assets.any? %>
257 <% headline_asset_id = @page.headline_asset&.id %>
257 <div class="node_description">Images</div> 258 <div class="node_description">Images</div>
258 <div class="node_content node_info_group"> 259 <div class="node_content node_info_group">
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 </div>
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..20602f7 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
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 @@
1namespace :assets do 1namespace :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..f6d915f 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 }
@@ -110,14 +106,6 @@ $(document).ready(function () {
110}); 106});
111 107
112 108
113meta_data = {
114 initialize : function() {
115 document.getElementById("metadata_details").addEventListener("toggle", function() {
116 if (this.open) image_interface.initialize();
117 });
118 }
119};
120
121cccms = { 109cccms = {
122 setup_autosave : function() { 110 setup_autosave : function() {
123 111
@@ -282,6 +270,7 @@ cccms = {
282 large: $(this).data('large-url'), 270 large: $(this).data('large-url'),
283 original: $(this).data('original-url'), 271 original: $(this).data('original-url'),
284 name: $(this).data('name'), 272 name: $(this).data('name'),
273 hasCredit: $(this).data('has-credit') === true,
285 headline: $(this).data('headline') === true 274 headline: $(this).data('headline') === true
286 }; 275 };
287 }).get(); 276 }).get();
@@ -320,8 +309,11 @@ cccms = {
320 : 'inline-image inline-image--half inline-image--' + placement; 309 : 'inline-image inline-image--half inline-image--' + placement;
321 310
322 var esc = cccms.inline_images.escape_attr; 311 var esc = cccms.inline_images.escape_attr;
312 var titleForGlightbox = (item.name || '').replace(/;/g, ',');
313 var glightboxOpts = 'title: ' + esc(titleForGlightbox) +
314 (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) + '"' + 315 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) + '">' + 316 ' data-glightbox="' + glightboxOpts + '">' +
325 '<img src="' + esc(item.large) + '" class="' + classes + '" alt="' + esc(item.name) + '"></a>'; 317 '<img src="' + esc(item.large) + '" class="' + classes + '" alt="' + esc(item.name) + '"></a>';
326 318
327 cccms.inline_images.editor.insertContent(html); 319 cccms.inline_images.editor.insertContent(html);
@@ -360,77 +352,6 @@ menu_item_sorter = {
360 } 352 }
361} 353}
362 354
363image_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
434rrule_builder = { 355rrule_builder = {
435 initialize : function() { 356 initialize : function() {
436 rrule_builder.try_populate_from_rrule($("#event_rrule").val()); 357 rrule_builder.try_populate_from_rrule($("#event_rrule").val());
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 @@
1document.addEventListener('DOMContentLoaded', function(){ 1document.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..ef6cb11 100644
--- a/public/stylesheets/admin.css
+++ b/public/stylesheets/admin.css
@@ -1042,10 +1042,6 @@ div#page_editor {
1042 select { 1042 select {
1043 font-size: 1.5rem; 1043 font-size: 1.5rem;
1044 } 1044 }
1045
1046 #metadata ul#image_box {
1047 width: 100% !important;
1048 }
1049} 1045}
1050 1046
1051/* ============================================================ 1047/* ============================================================
@@ -1264,31 +1260,6 @@ div#draft_list table td.actions a {
1264 Image box / browser 1260 Image box / browser
1265 ============================================================ */ 1261 ============================================================ */
1266 1262
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
1282div#image_browser {
1283 position: absolute;
1284 top: 40px;
1285 left: 800px;
1286}
1287
1288div#image_browser ul li {
1289 list-style-type: none;
1290}
1291
1292.thumbnail_list { 1263.thumbnail_list {
1293 display: flex; 1264 display: flex;
1294 flex-wrap: wrap; 1265 flex-wrap: wrap;
@@ -1316,6 +1287,30 @@ div#image_browser ul li {
1316 border-radius: 4px; 1287 border-radius: 4px;
1317} 1288}
1318 1289
1290.thumbnail_list li {
1291 position: relative;
1292}
1293
1294.thumbnail_list li .headline_indicator {
1295 position: absolute;
1296 top: 4px;
1297 right: 4px;
1298 width: 1.4rem;
1299 height: 1.4rem;
1300 display: flex;
1301 align-items: center;
1302 justify-content: center;
1303 border-radius: 50%;
1304 background: rgba(255, 255, 255, 0.85);
1305 color: #f5b400;
1306}
1307
1308.thumbnail_list li .headline_indicator svg {
1309 width: 0.9rem;
1310 height: 0.9rem;
1311 fill: currentColor;
1312}
1313
1319.related_asset_handle { 1314.related_asset_handle {
1320 display: flex; 1315 display: flex;
1321 color: #969696; 1316 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/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 @@
1require 'test_helper' 1require 'test_helper'
2 2
3class PagesControllerTest < ActionController::TestCase 3class 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
5end 72end
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
137end 162end
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
44end 56end
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
82end 128end
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