diff options
44 files changed, 721 insertions, 238 deletions
diff --git a/app/controllers/assets_controller.rb b/app/controllers/assets_controller.rb index d150e06..becfe13 100644 --- a/app/controllers/assets_controller.rb +++ b/app/controllers/assets_controller.rb | |||
| @@ -89,6 +89,6 @@ class AssetsController < ApplicationController | |||
| 89 | private | 89 | private |
| 90 | 90 | ||
| 91 | def asset_params | 91 | def asset_params |
| 92 | params.require(:asset).permit(:name, :upload) | 92 | params.require(:asset).permit(:name, :upload, :creator, :source_url, :license_key) |
| 93 | end | 93 | end |
| 94 | end | 94 | end |
diff --git a/app/controllers/pages_controller.rb b/app/controllers/pages_controller.rb index 2d08dea..0b7e98f 100644 --- a/app/controllers/pages_controller.rb +++ b/app/controllers/pages_controller.rb | |||
| @@ -15,11 +15,4 @@ class PagesController < ApplicationController | |||
| 15 | ) | 15 | ) |
| 16 | end | 16 | end |
| 17 | end | 17 | 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 | 18 | end |
diff --git a/app/controllers/related_assets_controller.rb b/app/controllers/related_assets_controller.rb index 5af73fc..2fefaf7 100644 --- a/app/controllers/related_assets_controller.rb +++ b/app/controllers/related_assets_controller.rb | |||
| @@ -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.has_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, |
| @@ -39,7 +40,17 @@ class RelatedAssetsController < ApplicationController | |||
| 39 | end | 40 | end |
| 40 | 41 | ||
| 41 | def update | 42 | def update |
| 42 | @node.editable_page.related_assets.find(params[:id]).insert_at(params[:position].to_i) | 43 | related = @node.editable_page.related_assets.find(params[:id]) |
| 44 | |||
| 45 | if params.key?(:headline) | ||
| 46 | RelatedAsset.transaction do | ||
| 47 | @node.editable_page.related_assets.update_all(headline: false) | ||
| 48 | related.update!(headline: true) if params[:headline] == "true" | ||
| 49 | end | ||
| 50 | else | ||
| 51 | related.insert_at(params[:position].to_i) | ||
| 52 | end | ||
| 53 | |||
| 43 | head :ok | 54 | head :ok |
| 44 | end | 55 | end |
| 45 | 56 | ||
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 5810966..b3c6bad 100644 --- a/app/helpers/content_helper.rb +++ b/app/helpers/content_helper.rb | |||
| @@ -53,11 +53,8 @@ module ContentHelper | |||
| 53 | end | 53 | end |
| 54 | 54 | ||
| 55 | def headline_image | 55 | def headline_image |
| 56 | @images = @page.assets.images | 56 | @headline_asset = @page.related_assets.find_by(headline: true)&.asset |
| 57 | 57 | render :partial => 'content/headline_image' if @headline_asset || @page.assets.images.any? | |
| 58 | unless @images.empty? | ||
| 59 | render :partial => 'content/headline_image' | ||
| 60 | end | ||
| 61 | end | 58 | end |
| 62 | 59 | ||
| 63 | # 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 |
| @@ -154,4 +151,30 @@ module ContentHelper | |||
| 154 | ) | 151 | ) |
| 155 | end | 152 | end |
| 156 | 153 | ||
| 154 | def asset_credit(asset) | ||
| 155 | return nil unless asset | ||
| 156 | return nil unless asset.has_credit? | ||
| 157 | |||
| 158 | license = AssetLicense.find(asset.license_key) | ||
| 159 | |||
| 160 | photo_label = t("asset_credits.photo", name: asset.name) | ||
| 161 | photo = asset.source_url.present? ? link_to(photo_label, asset.source_url) : photo_label | ||
| 162 | |||
| 163 | attribution_parts = [photo] | ||
| 164 | attribution_parts << t("asset_credits.by", creator: asset.creator) if asset.creator.present? | ||
| 165 | attribution = safe_join(attribution_parts, " ") | ||
| 166 | |||
| 167 | license_text = if license | ||
| 168 | name = t("asset_licenses.#{license.key}", default: license.key) | ||
| 169 | phrase = license.style == "license" ? t("asset_credits.licensed_under", license: name) : name | ||
| 170 | license.url.present? ? link_to(phrase, license.url) : phrase | ||
| 171 | end | ||
| 172 | |||
| 173 | full = license_text ? safe_join([attribution, license_text], ", ") : attribution | ||
| 174 | safe_join([full, "."]) | ||
| 175 | end | ||
| 176 | |||
| 177 | def glightbox_data(image, title) | ||
| 178 | "title: #{title.to_s.tr(';', ',')};" | ||
| 179 | end | ||
| 157 | end | 180 | end |
diff --git a/app/models/asset.rb b/app/models/asset.rb index aca0ee8..4a295d8 100644 --- a/app/models/asset.rb +++ b/app/models/asset.rb | |||
| @@ -1,12 +1,22 @@ | |||
| 1 | class Asset < ApplicationRecord | 1 | class Asset < ApplicationRecord |
| 2 | IMAGE_CONTENT_TYPES = ["image/gif", "image/jpeg", "image/png", "image/webp"] | ||
| 2 | 3 | ||
| 3 | include FileAttachment | 4 | include FileAttachment |
| 4 | 5 | ||
| 5 | has_many :related_assets, :dependent => :destroy | 6 | has_many :related_assets, :dependent => :destroy |
| 6 | has_many :pages, :through => :related_assets | 7 | has_many :pages, :through => :related_assets |
| 7 | 8 | ||
| 8 | scope :images, -> { where(:upload_content_type => ["image/gif", "image/jpeg", "image/png", "image/webp"]) } | 9 | scope :images, -> { where(:upload_content_type => IMAGE_CONTENT_TYPES) } |
| 9 | scope :documents, -> { where(:upload_content_type => ["application/pdf", "text/plain", "text/rtf"]) } | 10 | scope :documents, -> { where(:upload_content_type => ["application/pdf", "text/plain", "text/rtf"]) } |
| 10 | scope :audio, -> { where(:upload_content_type => ["audio/mpeg", "audio/x-m4a", "audio/wav", "audio/x-wav"]) } | 11 | scope :audio, -> { where(:upload_content_type => ["audio/mpeg", "audio/x-m4a", "audio/wav", "audio/x-wav"]) } |
| 11 | 12 | ||
| 13 | validates :license_key, inclusion: { in: -> { AssetLicense.keys } }, allow_blank: true | ||
| 14 | |||
| 15 | def image? | ||
| 16 | IMAGE_CONTENT_TYPES.include?(upload_content_type) | ||
| 17 | end | ||
| 18 | |||
| 19 | def has_credit? | ||
| 20 | creator.present? || source_url.present? || license_key.present? | ||
| 21 | end | ||
| 12 | end | 22 | end |
diff --git a/app/models/asset_license.rb b/app/models/asset_license.rb new file mode 100644 index 0000000..ca66d40 --- /dev/null +++ b/app/models/asset_license.rb | |||
| @@ -0,0 +1,15 @@ | |||
| 1 | class AssetLicense | ||
| 2 | Entry = Struct.new(:key, :url, :requires_attribution, :style, keyword_init: true) | ||
| 3 | |||
| 4 | DICTIONARY = YAML.load_file(Rails.root.join("config", "asset_licenses.yml")).freeze | ||
| 5 | |||
| 6 | def self.keys | ||
| 7 | DICTIONARY.keys | ||
| 8 | end | ||
| 9 | |||
| 10 | def self.find(key) | ||
| 11 | entry = DICTIONARY[key.to_s] if key.present? | ||
| 12 | return nil unless entry | ||
| 13 | Entry.new(key: key.to_s, url: entry["url"], requires_attribution: entry["requires_attribution"], style: entry["style"]) | ||
| 14 | end | ||
| 15 | end | ||
diff --git a/app/models/concerns/file_attachment.rb b/app/models/concerns/file_attachment.rb index 3c09456..e7a33c5 100644 --- a/app/models/concerns/file_attachment.rb +++ b/app/models/concerns/file_attachment.rb | |||
| @@ -52,6 +52,14 @@ module FileAttachment | |||
| 52 | build_upload_proxy | 52 | build_upload_proxy |
| 53 | end | 53 | end |
| 54 | 54 | ||
| 55 | def has_variant?(style) | ||
| 56 | upload_file_name.present? && File.exist?(file_path(style)) | ||
| 57 | end | ||
| 58 | |||
| 59 | def previewable? | ||
| 60 | has_variant?(:medium) | ||
| 61 | end | ||
| 62 | |||
| 55 | private | 63 | private |
| 56 | 64 | ||
| 57 | def build_upload_proxy | 65 | def build_upload_proxy |
diff --git a/app/models/node_action.rb b/app/models/node_action.rb index 13bd5ba..63a99ae 100644 --- a/app/models/node_action.rb +++ b/app/models/node_action.rb | |||
| @@ -166,4 +166,12 @@ class NodeAction < ApplicationRecord | |||
| 166 | def subject_name | 166 | def subject_name |
| 167 | metadata["human_readable_node_name"] || node&.unique_name || "deleted node" | 167 | metadata["human_readable_node_name"] || node&.unique_name || "deleted node" |
| 168 | end | 168 | end |
| 169 | |||
| 170 | def diff_link_params | ||
| 171 | prev = NodeAction.where(node_id: node_id, action: "publish") | ||
| 172 | .where("id < ?", id) | ||
| 173 | .order(id: :desc).first | ||
| 174 | return nil unless prev&.page && page | ||
| 175 | { start_revision: prev.page.revision, end_revision: page.revision } | ||
| 176 | end | ||
| 169 | end | 177 | end |
diff --git a/app/models/page.rb b/app/models/page.rb index f33d88d..c1498fc 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 |
| @@ -314,21 +319,6 @@ class Page < ApplicationRecord | |||
| 314 | end | 319 | end |
| 315 | end | 320 | end |
| 316 | 321 | ||
| 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' | 322 | # Installs (or re-installs) the trigger that keeps page_translations' |
| 333 | # search_vector in sync. Idempotent, safe to call on every boot. | 323 | # search_vector in sync. Idempotent, safe to call on every boot. |
| 334 | # search_vector is populated by a raw Postgres trigger, not anything | 324 | # search_vector is populated by a raw Postgres trigger, not anything |
diff --git a/app/models/related_asset.rb b/app/models/related_asset.rb index 8f16460..62000cc 100644 --- a/app/models/related_asset.rb +++ b/app/models/related_asset.rb | |||
| @@ -5,4 +5,13 @@ class RelatedAsset < ApplicationRecord | |||
| 5 | acts_as_list :scope => :page_id | 5 | acts_as_list :scope => :page_id |
| 6 | 6 | ||
| 7 | default_scope -> { order("position ASC") } | 7 | default_scope -> { order("position ASC") } |
| 8 | |||
| 9 | validate :headline_only_for_images | ||
| 10 | |||
| 11 | private | ||
| 12 | |||
| 13 | def headline_only_for_images | ||
| 14 | return unless asset | ||
| 15 | errors.add(:headline, "can only be set on image assets") if headline? && !asset.image? | ||
| 16 | end | ||
| 8 | end | 17 | end |
diff --git a/app/views/assets/edit.html.erb b/app/views/assets/edit.html.erb index f198600..3ef8fea 100644 --- a/app/views/assets/edit.html.erb +++ b/app/views/assets/edit.html.erb | |||
| @@ -1,24 +1,44 @@ | |||
| 1 | <h1>Editing asset</h1> | 1 | <div id="page_editor"> |
| 2 | <h1>Editing asset</h1> | ||
| 2 | 3 | ||
| 3 | <%= form_for(@asset, html: { multipart: true }) do |f| %> | 4 | <%= form_for(@asset, html: { multipart: true }) do |f| %> |
| 4 | <%= form_error_messages(f) %> | 5 | <%= form_error_messages(f) %> |
| 5 | 6 | ||
| 6 | <% if @asset.upload.present? %> | 7 | <div id="content"> |
| 7 | <p> | 8 | <div class="node_description"><%= f.label :name %></div> |
| 8 | <strong>Current file:</strong> | 9 | <div class="node_content"><%= f.text_field :name %></div> |
| 9 | <%= @asset.upload.url %> | 10 | |
| 10 | (<%= number_to_human_size(@asset.upload.size) %>) | 11 | <% if @asset.upload.present? %> |
| 11 | </p> | 12 | <div class="node_description">Current File</div> |
| 12 | <% end %> | 13 | <div class="node_content"><%= @asset.upload.url %> (<%= number_to_human_size(@asset.upload.size) %>)</div> |
| 14 | <% end %> | ||
| 15 | |||
| 16 | <div class="node_description"><%= f.label :upload, "Replace file" %></div> | ||
| 17 | <div class="node_content"><%= f.file_field :upload %></div> | ||
| 13 | 18 | ||
| 14 | <p> | 19 | <div class="node_description"><%= f.label :creator %></div> |
| 15 | <label>Replace file:</label><br> | 20 | <div class="node_content"><%= f.text_field :creator %></div> |
| 16 | <%= f.file_field :upload %> | ||
| 17 | </p> | ||
| 18 | 21 | ||
| 19 | <p> | 22 | <div class="node_description"><%= f.label :source_url, "Source URL" %></div> |
| 20 | <%= f.submit 'Update' %> | 23 | <div class="node_content"><%= f.text_field :source_url %></div> |
| 21 | </p> | ||
| 22 | <% end %> | ||
| 23 | 24 | ||
| 24 | <%= link_to 'Show', @asset %> | <%= link_to 'Back', assets_path %> | 25 | <div class="node_description"><%= f.label :license_key, "License" %></div> |
| 26 | <div class="node_content"> | ||
| 27 | <%= f.select :license_key, | ||
| 28 | options_for_select( | ||
| 29 | [["— none —", ""]] + AssetLicense.keys.map { |key| [t("asset_licenses.#{key}"), key] }, | ||
| 30 | @asset.license_key | ||
| 31 | ) %> | ||
| 32 | </div> | ||
| 33 | |||
| 34 | <div class="node_description">Actions</div> | ||
| 35 | <div class="node_content node_info_group"> | ||
| 36 | <div class="node_info_group_items"> | ||
| 37 | <div class="node_info_item"><%= f.submit 'Update' %></div> | ||
| 38 | <div class="node_info_item"><%= link_to 'Show', @asset %></div> | ||
| 39 | <div class="node_info_item"><%= link_to 'Back', assets_path %></div> | ||
| 40 | </div> | ||
| 41 | </div> | ||
| 42 | </div> | ||
| 43 | <% end %> | ||
| 44 | </div> | ||
diff --git a/app/views/assets/index.html.erb b/app/views/assets/index.html.erb index 8c35561..ff1300a 100644 --- a/app/views/assets/index.html.erb +++ b/app/views/assets/index.html.erb | |||
| @@ -17,7 +17,7 @@ | |||
| 17 | </tr> | 17 | </tr> |
| 18 | <% @assets.each do |asset| %> | 18 | <% @assets.each do |asset| %> |
| 19 | <tr> | 19 | <tr> |
| 20 | <td><%= image_tag asset.upload.url(:thumb), style: "max-width: 100px; max-height: 100px;" %></td> | 20 | <td><% if asset.has_variant?(:thumb) %><%= image_tag asset.upload.url(:thumb), style: "max-width: 100px; max-height: 100px;" %><% end %></td> |
| 21 | <td><%= link_to asset.name, asset.upload.url %></td> | 21 | <td><%= link_to asset.name, asset.upload.url %></td> |
| 22 | <td><%= asset.upload.content_type %></td> | 22 | <td><%= asset.upload.content_type %></td> |
| 23 | <td><%= link_to 'Show', asset %></td> | 23 | <td><%= link_to 'Show', asset %></td> |
diff --git a/app/views/assets/new.html.erb b/app/views/assets/new.html.erb index 6c1310a..2cf8865 100644 --- a/app/views/assets/new.html.erb +++ b/app/views/assets/new.html.erb | |||
| @@ -1,17 +1,38 @@ | |||
| 1 | <h1>New asset</h1> | 1 | <div id="page_editor"> |
| 2 | <h1>New asset</h1> | ||
| 2 | 3 | ||
| 3 | <%= form_for(@asset, :html => { :multipart => true }) do |f| %> | 4 | <%= form_for(@asset, :html => { :multipart => true }) do |f| %> |
| 4 | <%= form_error_messages(f) %> | 5 | <%= form_error_messages(f) %> |
| 5 | |||
| 6 | <p> | ||
| 7 | <%= f.label :name %><br /> | ||
| 8 | <%= f.text_field :name %> | ||
| 9 | </p> | ||
| 10 | 6 | ||
| 11 | <p> | 7 | <div id="content"> |
| 12 | <%= f.file_field :upload %> | 8 | <div class="node_description"><%= f.label :name %></div> |
| 13 | <%= f.submit 'Create' %> | 9 | <div class="node_content"><%= f.text_field :name %></div> |
| 14 | </p> | ||
| 15 | <% end %> | ||
| 16 | 10 | ||
| 17 | <%= link_to 'Back', assets_path %> | 11 | <div class="node_description"><%= f.label :upload, "File" %></div> |
| 12 | <div class="node_content"><%= f.file_field :upload %></div> | ||
| 13 | |||
| 14 | <div class="node_description"><%= f.label :creator %></div> | ||
| 15 | <div class="node_content"><%= f.text_field :creator %></div> | ||
| 16 | |||
| 17 | <div class="node_description"><%= f.label :source_url, "Source URL" %></div> | ||
| 18 | <div class="node_content"><%= f.text_field :source_url %></div> | ||
| 19 | |||
| 20 | <div class="node_description"><%= f.label :license_key, "License" %></div> | ||
| 21 | <div class="node_content"> | ||
| 22 | <%= f.select :license_key, | ||
| 23 | options_for_select( | ||
| 24 | [["— none —", ""]] + AssetLicense.keys.map { |key| [t("asset_licenses.#{key}"), key] }, | ||
| 25 | @asset.license_key | ||
| 26 | ) %> | ||
| 27 | </div> | ||
| 28 | |||
| 29 | <div class="node_description">Actions</div> | ||
| 30 | <div class="node_content node_info_group"> | ||
| 31 | <div class="node_info_group_items"> | ||
| 32 | <div class="node_info_item"><%= f.submit 'Create' %></div> | ||
| 33 | <div class="node_info_item"><%= link_to 'Back', assets_path %></div> | ||
| 34 | </div> | ||
| 35 | </div> | ||
| 36 | </div> | ||
| 37 | <% end %> | ||
| 38 | </div> | ||
diff --git a/app/views/assets/show.html.erb b/app/views/assets/show.html.erb index ff00883..e551e35 100644 --- a/app/views/assets/show.html.erb +++ b/app/views/assets/show.html.erb | |||
| @@ -16,8 +16,10 @@ | |||
| 16 | </div> | 16 | </div> |
| 17 | </div> | 17 | </div> |
| 18 | 18 | ||
| 19 | <div class="node_description">Thumbnail</div> | 19 | <% if @asset.has_variant?(:medium) %> |
| 20 | <div class="node_content"><%= image_tag @asset.upload.url(:medium), style: "max-width: 300px; max-height: 300px;" %></div> | 20 | <div class="node_description">Thumbnail</div> |
| 21 | <div class="node_content"><%= image_tag @asset.upload.url(:medium), style: "max-width: 300px; max-height: 300px;" %></div> | ||
| 22 | <% end %> | ||
| 21 | 23 | ||
| 22 | <div class="node_description">Public Path</div> | 24 | <div class="node_description">Public Path</div> |
| 23 | <div class="node_content"> | 25 | <div class="node_content"> |
| @@ -30,6 +32,27 @@ | |||
| 30 | </button> | 32 | </button> |
| 31 | </div> | 33 | </div> |
| 32 | 34 | ||
| 35 | <div class="node_description">Creator</div> | ||
| 36 | <div class="node_content"><%= @asset.creator.presence || "—" %></div> | ||
| 37 | |||
| 38 | <div class="node_description">Source</div> | ||
| 39 | <div class="node_content"> | ||
| 40 | <% if @asset.source_url.present? %> | ||
| 41 | <%= link_to @asset.source_url, @asset.source_url %> | ||
| 42 | <% else %> | ||
| 43 | — | ||
| 44 | <% end %> | ||
| 45 | </div> | ||
| 46 | |||
| 47 | <div class="node_description">License</div> | ||
| 48 | <div class="node_content"> | ||
| 49 | <% if (license = AssetLicense.find(@asset.license_key)) %> | ||
| 50 | <%= t("asset_licenses.#{license.key}") %> | ||
| 51 | <% else %> | ||
| 52 | — | ||
| 53 | <% end %> | ||
| 54 | </div> | ||
| 55 | |||
| 33 | <div class="node_description">Content Type</div> | 56 | <div class="node_description">Content Type</div> |
| 34 | <div class="node_content"><%= @asset.upload.content_type %></div> | 57 | <div class="node_content"><%= @asset.upload.content_type %></div> |
| 35 | 58 | ||
diff --git a/app/views/content/_asset_credits.html.erb b/app/views/content/_asset_credits.html.erb new file mode 100644 index 0000000..5959abd --- /dev/null +++ b/app/views/content/_asset_credits.html.erb | |||
| @@ -0,0 +1,10 @@ | |||
| 1 | <% if @page.assets.images.any? %> | ||
| 2 | <div id="asset_credits"> | ||
| 3 | <% @page.assets.images.each do |image| %> | ||
| 4 | <div id="credit_for_asset_<%= image.id %>" | ||
| 5 | class="asset_credit_block<%= " headline_credit" if image == @headline_asset %>"> | ||
| 6 | <%= asset_credit(image) %> | ||
| 7 | </div> | ||
| 8 | <% end %> | ||
| 9 | </div> | ||
| 10 | <% end %> | ||
diff --git a/app/views/content/_headline_image.html.erb b/app/views/content/_headline_image.html.erb index 243be40..e7b821b 100644 --- a/app/views/content/_headline_image.html.erb +++ b/app/views/content/_headline_image.html.erb | |||
| @@ -1,16 +1,31 @@ | |||
| 1 | <%= link_to( | 1 | <% gallery_images = @page.assets.images %> |
| 2 | image_tag(@images[0].upload.url(:headline)), | ||
| 3 | @images[0].upload.url, | ||
| 4 | :class => "glightbox", | ||
| 5 | :data => { :gallery => "page-#{@page.node.id}" } | ||
| 6 | ) %> | ||
| 7 | 2 | ||
| 8 | <% if 1 < @images.length %> | 3 | <% if @headline_asset %> |
| 9 | <div class="right"> | 4 | <%= link_to( |
| 10 | <%= "#{@images.length} #{t(:images)}" %> | 5 | image_tag(@headline_asset.upload.url(:headline)), |
| 11 | </div> | 6 | @headline_asset.upload.url, |
| 7 | :class => "glightbox", | ||
| 8 | :data => { :gallery => "page-#{@page.node.id}", | ||
| 9 | :glightbox => glightbox_data(@headline_asset, @headline_asset.name), | ||
| 10 | :"credit-selector" => (@headline_asset.has_credit? ? "#credit_for_asset_#{@headline_asset.id}" : nil) } | ||
| 11 | ) %> | ||
| 12 | <% if gallery_images.size > 1 %> | ||
| 13 | <div class="right"><%= "#{gallery_images.size} #{t(:images)}" %></div> | ||
| 14 | <% end %> | ||
| 15 | <% elsif gallery_images.any? %> | ||
| 16 | <%= link_to "#{gallery_images.size} #{t(:images)}, #{t(:open_gallery)}", | ||
| 17 | gallery_images.first.upload.url, | ||
| 18 | :class => "glightbox right", | ||
| 19 | :data => { :gallery => "page-#{@page.node.id}", | ||
| 20 | :glightbox => glightbox_data(gallery_images.first, gallery_images.first.name), | ||
| 21 | :"credit-selector" => (gallery_images.first.has_credit? ? "#credit_for_asset_#{gallery_images.first.id}" : nil) } %> | ||
| 12 | <% end %> | 22 | <% end %> |
| 13 | 23 | ||
| 14 | <% @images[1..-1].each do |image| %> | 24 | <% gallery_images.each do |image| %> |
| 15 | <%= link_to "", image.upload.url, :class => "glightbox", :style => "display: none", :data => { :gallery => "page-#{@page.node.id}" } %> | 25 | <% next if image == @headline_asset %> |
| 26 | <% next if !@headline_asset && image == gallery_images.first %> | ||
| 27 | <%= link_to "", image.upload.url, :class => "glightbox", :style => "display: none", | ||
| 28 | :data => { :gallery => "page-#{@page.node.id}", | ||
| 29 | :glightbox => glightbox_data(image, image.name), | ||
| 30 | :"credit-selector" => (image.has_credit? ? "#credit_for_asset_#{image.id}" : nil) } %> | ||
| 16 | <% end %> | 31 | <% end %> |
diff --git a/app/views/custom/page_templates/public/chapter_detail.html.erb b/app/views/custom/page_templates/public/chapter_detail.html.erb index c4c2598..18449b8 100644 --- a/app/views/custom/page_templates/public/chapter_detail.html.erb +++ b/app/views/custom/page_templates/public/chapter_detail.html.erb | |||
| @@ -31,4 +31,5 @@ | |||
| 31 | </ul> | 31 | </ul> |
| 32 | </div> | 32 | </div> |
| 33 | <% end %> | 33 | <% end %> |
| 34 | <%= render partial: 'content/asset_credits' %> | ||
| 34 | </div> | 35 | </div> |
diff --git a/app/views/custom/page_templates/public/no_date_and_author.html.erb b/app/views/custom/page_templates/public/no_date_and_author.html.erb index fa39e54..ec7e850 100644 --- a/app/views/custom/page_templates/public/no_date_and_author.html.erb +++ b/app/views/custom/page_templates/public/no_date_and_author.html.erb | |||
| @@ -3,6 +3,7 @@ | |||
| 3 | <p><%= sanitize( @page.abstract ) %></p> | 3 | <p><%= sanitize( @page.abstract ) %></p> |
| 4 | <div id="headline_image"><%= headline_image %></div> | 4 | <div id="headline_image"><%= headline_image %></div> |
| 5 | <%= aggregate?(@page.body) %> | 5 | <%= aggregate?(@page.body) %> |
| 6 | <%= render partial: 'content/asset_credits' %> | ||
| 6 | </div> | 7 | </div> |
| 7 | 8 | ||
| 8 | <%= will_paginate(@content_collection) if @content_collection %> | 9 | <%= will_paginate(@content_collection) if @content_collection %> |
diff --git a/app/views/custom/page_templates/public/no_title_abstract_date_and_author.html.erb b/app/views/custom/page_templates/public/no_title_abstract_date_and_author.html.erb index 64e2a7c..c486684 100644 --- a/app/views/custom/page_templates/public/no_title_abstract_date_and_author.html.erb +++ b/app/views/custom/page_templates/public/no_title_abstract_date_and_author.html.erb | |||
| @@ -1,6 +1,7 @@ | |||
| 1 | <div class="article" lang="<%= @page.effective_lang %>"> | 1 | <div class="article" lang="<%= @page.effective_lang %>"> |
| 2 | <div id="headline_image"><%= headline_image %></div> | 2 | <div id="headline_image"><%= headline_image %></div> |
| 3 | <%= aggregate?(@page.body) %> | 3 | <%= aggregate?(@page.body) %> |
| 4 | <%= render partial: 'content/asset_credits' %> | ||
| 4 | </div> | 5 | </div> |
| 5 | 6 | ||
| 6 | <%= will_paginate(@content_collection) if @content_collection %> | 7 | <%= will_paginate(@content_collection) if @content_collection %> |
diff --git a/app/views/custom/page_templates/public/standard_template.html.erb b/app/views/custom/page_templates/public/standard_template.html.erb index bbc8cd0..e1e2eb0 100644 --- a/app/views/custom/page_templates/public/standard_template.html.erb +++ b/app/views/custom/page_templates/public/standard_template.html.erb | |||
| @@ -3,6 +3,7 @@ | |||
| 3 | <div class="abstract"><p><%= sanitize( @page.abstract )%></p></div> | 3 | <div class="abstract"><p><%= sanitize( @page.abstract )%></p></div> |
| 4 | <div id="headline_image"><%= headline_image %></div> | 4 | <div id="headline_image"><%= headline_image %></div> |
| 5 | <div class="body"><%= aggregate?(@page.body) %></div> | 5 | <div class="body"><%= aggregate?(@page.body) %></div> |
| 6 | <%= render partial: 'content/asset_credits' %> | ||
| 6 | </div> | 7 | </div> |
| 7 | 8 | ||
| 8 | <%= will_paginate(@content_collection) if @content_collection %> | 9 | <%= will_paginate(@content_collection) if @content_collection %> |
diff --git a/app/views/custom/page_templates/public/title_only.html.erb b/app/views/custom/page_templates/public/title_only.html.erb index 42fe9a6..d706e9d 100644 --- a/app/views/custom/page_templates/public/title_only.html.erb +++ b/app/views/custom/page_templates/public/title_only.html.erb | |||
| @@ -2,6 +2,7 @@ | |||
| 2 | <h2 class="headline"><%= @page.title %></h2> | 2 | <h2 class="headline"><%= @page.title %></h2> |
| 3 | <div id="headline_image"><%= headline_image %></div> | 3 | <div id="headline_image"><%= headline_image %></div> |
| 4 | <%= aggregate?(@page.body) %> | 4 | <%= aggregate?(@page.body) %> |
| 5 | <%= render partial: 'content/asset_credits' %> | ||
| 5 | </div> | 6 | </div> |
| 6 | 7 | ||
| 7 | <%= will_paginate(@content_collection) if @content_collection %> | 8 | <%= will_paginate(@content_collection) if @content_collection %> |
diff --git a/app/views/custom/page_templates/public/update.html.erb b/app/views/custom/page_templates/public/update.html.erb index d5995ac..0ff1bb2 100644 --- a/app/views/custom/page_templates/public/update.html.erb +++ b/app/views/custom/page_templates/public/update.html.erb | |||
| @@ -4,6 +4,7 @@ | |||
| 4 | <div class="abstract"><p><%= sanitize( @page.abstract )%></p></div> | 4 | <div class="abstract"><p><%= sanitize( @page.abstract )%></p></div> |
| 5 | <div id="headline_image"><%= headline_image %></div> | 5 | <div id="headline_image"><%= headline_image %></div> |
| 6 | <div class="body"><%= aggregate?(@page.body) %></div> | 6 | <div class="body"><%= aggregate?(@page.body) %></div> |
| 7 | <%= render partial: 'content/asset_credits' %> | ||
| 7 | </div> | 8 | </div> |
| 8 | 9 | ||
| 9 | <%= will_paginate(@content_collection) if @content_collection %> | 10 | <%= will_paginate(@content_collection) if @content_collection %> |
diff --git a/app/views/node_actions/_change_details.html.erb b/app/views/node_actions/_change_details.html.erb index 2583e8b..066d0f3 100644 --- a/app/views/node_actions/_change_details.html.erb +++ b/app/views/node_actions/_change_details.html.erb | |||
| @@ -5,9 +5,13 @@ | |||
| 5 | <tr> | 5 | <tr> |
| 6 | <th><%= I18n.default_locale.to_s.upcase %></th> | 6 | <th><%= I18n.default_locale.to_s.upcase %></th> |
| 7 | <td> | 7 | <td> |
| 8 | <%= safe_join(default_items, ", ") %> | 8 | <%= safe_join(default_items, tag.br) %> |
| 9 | <% if action_entry.page && action_entry.node %> | 9 | <% if action_entry.page && action_entry.node %> |
| 10 | <%= link_to t("node_actions.view_revision"), node_revision_path(action_entry.node, action_entry.page) %> | 10 | <% if (diff_params = action_entry.diff_link_params) %> |
| 11 | <%= link_to t("node_actions.view_diff"), diff_node_revisions_path(action_entry.node, diff_params) %> | ||
| 12 | <% else %> | ||
| 13 | <%= link_to t("node_actions.view_revision"), node_revision_path(action_entry.node, action_entry.page) %> | ||
| 14 | <% end %> | ||
| 11 | <% end %> | 15 | <% end %> |
| 12 | </td> | 16 | </td> |
| 13 | </tr> | 17 | </tr> |
| @@ -16,9 +20,13 @@ | |||
| 16 | <tr> | 20 | <tr> |
| 17 | <th><%= locale.upcase %></th> | 21 | <th><%= locale.upcase %></th> |
| 18 | <td> | 22 | <td> |
| 19 | <%= safe_join(translation_changes(diff), ", ") %> | 23 | <%= safe_join(translation_changes(diff), tag.br) %> |
| 20 | <% if action_entry.page && action_entry.node %> | 24 | <% if action_entry.page && action_entry.node %> |
| 21 | <%= link_to t("node_actions.view_revision"), node_revision_path(action_entry.node, action_entry.page, :locale => locale) %> | 25 | <% if (diff_params = action_entry.diff_link_params) %> |
| 26 | <%= link_to t("node_actions.view_diff"), diff_node_revisions_path(action_entry.node, diff_params.merge(:locale => locale)) %> | ||
| 27 | <% else %> | ||
| 28 | <%= link_to t("node_actions.view_revision"), node_revision_path(action_entry.node, action_entry.page, :locale => locale) %> | ||
| 29 | <% end %> | ||
| 22 | <% end %> | 30 | <% end %> |
| 23 | </td> | 31 | </td> |
| 24 | </tr> | 32 | </tr> |
diff --git a/app/views/nodes/edit.html.erb b/app/views/nodes/edit.html.erb index 25f15c2..07b42d4 100644 --- a/app/views/nodes/edit.html.erb +++ b/app/views/nodes/edit.html.erb | |||
| @@ -82,17 +82,27 @@ | |||
| 82 | <ul id="related_asset_list" class="thumbnail_list"> | 82 | <ul id="related_asset_list" class="thumbnail_list"> |
| 83 | <% @page.related_assets.includes(:asset).each do |related| %> | 83 | <% @page.related_assets.includes(:asset).each do |related| %> |
| 84 | <li data-url="<%= node_related_asset_path(@node, related) %>" | 84 | <li data-url="<%= node_related_asset_path(@node, related) %>" |
| 85 | data-asset-id="<%= related.asset.id %>" | ||
| 85 | data-large-url="<%= related.asset.upload.url(:large) %>" | 86 | data-large-url="<%= related.asset.upload.url(:large) %>" |
| 86 | data-original-url="<%= related.asset.upload.url %>" | 87 | data-original-url="<%= related.asset.upload.url %>" |
| 87 | data-name="<%= related.asset.name %>"> | 88 | data-name="<%= related.asset.name %>" |
| 89 | data-has-credit="<%= related.asset.has_credit? %>" | ||
| 90 | data-headline="<%= related.headline? %>" | ||
| 91 | class="<%= "is_headline" if related.headline? %>"> | ||
| 88 | <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> |
| 89 | <%= image_tag related.asset.upload.url(:thumb) %> | 93 | <%= image_tag related.asset.upload.url(:thumb) %> |
| 94 | <button type="button" class="related_asset_set_headline" | ||
| 95 | aria-pressed="<%= related.headline? %>" aria-label="Set as headline image" | ||
| 96 | title="Use this photo as the page's headline image"> | ||
| 97 | <%= icon("star", library: "tabler", "aria-hidden": true) %> | ||
| 98 | </button> | ||
| 90 | <button type="button" class="related_asset_remove" aria-label="Remove image"> | 99 | <button type="button" class="related_asset_remove" aria-label="Remove image"> |
| 91 | <%= icon("x", library: "tabler", "aria-hidden": true) %> | 100 | <%= icon("x", library: "tabler", "aria-hidden": true) %> |
| 92 | </button> | 101 | </button> |
| 93 | </li> | 102 | </li> |
| 94 | <% end %> | 103 | <% end %> |
| 95 | </ul> | 104 | </ul> |
| 105 | <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> | ||
| 96 | <%= 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" %> |
| 97 | <div id="related_asset_search_results" class="search_results"></div> | 107 | <div id="related_asset_search_results" class="search_results"></div> |
| 98 | </div> | 108 | </div> |
| @@ -102,6 +112,9 @@ | |||
| 102 | <li> | 112 | <li> |
| 103 | <span class="related_asset_handle"><%= icon("grip-vertical", library: "tabler", "aria-hidden": true) %></span> | 113 | <span class="related_asset_handle"><%= icon("grip-vertical", library: "tabler", "aria-hidden": true) %></span> |
| 104 | <img src=""> | 114 | <img src=""> |
| 115 | <button type="button" class="related_asset_set_headline" aria-pressed="false" aria-label="Set as headline image"> | ||
| 116 | <%= icon("star", library: "tabler", "aria-hidden": true) %> | ||
| 117 | </button> | ||
| 105 | <button type="button" class="related_asset_remove" aria-label="Remove image"> | 118 | <button type="button" class="related_asset_remove" aria-label="Remove image"> |
| 106 | <%= icon("x", library: "tabler", "aria-hidden": true) %> | 119 | <%= icon("x", library: "tabler", "aria-hidden": true) %> |
| 107 | </button> | 120 | </button> |
diff --git a/app/views/nodes/show.html.erb b/app/views/nodes/show.html.erb index af05778..a7be3c6 100644 --- a/app/views/nodes/show.html.erb +++ b/app/views/nodes/show.html.erb | |||
| @@ -254,11 +254,19 @@ | |||
| 254 | </div> | 254 | </div> |
| 255 | 255 | ||
| 256 | <% if @page.assets.images.any? %> | 256 | <% if @page.assets.images.any? %> |
| 257 | <% headline_asset_id = @page.related_assets.find_by(headline: true)&.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.images.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/asset_licenses.yml b/config/asset_licenses.yml new file mode 100644 index 0000000..5cf377f --- /dev/null +++ b/config/asset_licenses.yml | |||
| @@ -0,0 +1,25 @@ | |||
| 1 | # config/asset_licenses.yml | ||
| 2 | cc0: | ||
| 3 | url: "https://creativecommons.org/publicdomain/zero/1.0/" | ||
| 4 | requires_attribution: false | ||
| 5 | style: license | ||
| 6 | cc_by_4: | ||
| 7 | url: "https://creativecommons.org/licenses/by/4.0/" | ||
| 8 | requires_attribution: true | ||
| 9 | style: license | ||
| 10 | cc_by_sa_4: | ||
| 11 | url: "https://creativecommons.org/licenses/by-sa/4.0/" | ||
| 12 | requires_attribution: true | ||
| 13 | style: license | ||
| 14 | public_domain: | ||
| 15 | url: | ||
| 16 | requires_attribution: false | ||
| 17 | style: note | ||
| 18 | used_with_permission: | ||
| 19 | url: | ||
| 20 | requires_attribution: false | ||
| 21 | style: note | ||
| 22 | own_work: | ||
| 23 | url: | ||
| 24 | requires_attribution: false | ||
| 25 | style: note | ||
diff --git a/config/locales/de.yml b/config/locales/de.yml index f64f6cd..21c8ebf 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml | |||
| @@ -116,3 +116,17 @@ de: | |||
| 116 | revision_created: "angelegt am %{date} von %{actor}" | 116 | revision_created: "angelegt am %{date} von %{actor}" |
| 117 | revision_published: "veröffentlicht am %{date} von %{actor}" | 117 | revision_published: "veröffentlicht am %{date} von %{actor}" |
| 118 | revision_restored: "wiederhergestellt am %{date} von %{actor}" | 118 | revision_restored: "wiederhergestellt am %{date} von %{actor}" |
| 119 | |||
| 120 | open_gallery: "Gallerie anzeigen" | ||
| 121 | asset_licenses: | ||
| 122 | cc0: "CC0" | ||
| 123 | cc_by_4: "CC BY 4.0" | ||
| 124 | cc_by_sa_4: "CC BY-SA 4.0" | ||
| 125 | public_domain: "Gemeinfrei" | ||
| 126 | used_with_permission: "Mit Genehmigung verwendet" | ||
| 127 | own_work: "Eigenes Werk" | ||
| 128 | |||
| 129 | asset_credits: | ||
| 130 | photo: "Foto %{name}" | ||
| 131 | by: "von %{creator}" | ||
| 132 | licensed_under: "Lizenziert unter %{license}" | ||
diff --git a/config/locales/en.yml b/config/locales/en.yml index 8a8acc1..1afcf21 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml | |||
| @@ -68,3 +68,17 @@ en: | |||
| 68 | revision_created: "created %{date} by %{actor}" | 68 | revision_created: "created %{date} by %{actor}" |
| 69 | revision_published: "published %{date} by %{actor}" | 69 | revision_published: "published %{date} by %{actor}" |
| 70 | revision_restored: "restored %{date} by %{actor}" | 70 | revision_restored: "restored %{date} by %{actor}" |
| 71 | |||
| 72 | open_gallery: "Open gallery" | ||
| 73 | asset_licenses: | ||
| 74 | cc0: "CC0" | ||
| 75 | cc_by_4: "CC BY 4.0" | ||
| 76 | cc_by_sa_4: "CC BY-SA 4.0" | ||
| 77 | public_domain: "Public domain" | ||
| 78 | used_with_permission: "Used with permission" | ||
| 79 | own_work: "Own work" | ||
| 80 | |||
| 81 | asset_credits: | ||
| 82 | photo: "Photo %{name}" | ||
| 83 | by: "by %{creator}" | ||
| 84 | licensed_under: "Licensed under %{license}" | ||
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/db/migrate/20260720115941_add_credit_fields_to_assets.rb b/db/migrate/20260720115941_add_credit_fields_to_assets.rb new file mode 100644 index 0000000..638d894 --- /dev/null +++ b/db/migrate/20260720115941_add_credit_fields_to_assets.rb | |||
| @@ -0,0 +1,7 @@ | |||
| 1 | class AddCreditFieldsToAssets < ActiveRecord::Migration[8.1] | ||
| 2 | def change | ||
| 3 | add_column :assets, :license_key, :string | ||
| 4 | add_column :assets, :creator, :string | ||
| 5 | add_column :assets, :source_url, :string | ||
| 6 | end | ||
| 7 | end | ||
diff --git a/db/migrate/20260720120917_add_headline_to_related_assets.rb b/db/migrate/20260720120917_add_headline_to_related_assets.rb new file mode 100644 index 0000000..c77e1ef --- /dev/null +++ b/db/migrate/20260720120917_add_headline_to_related_assets.rb | |||
| @@ -0,0 +1,7 @@ | |||
| 1 | class AddHeadlineToRelatedAssets < ActiveRecord::Migration[8.1] | ||
| 2 | def change | ||
| 3 | add_column :related_assets, :headline, :boolean, null: false, default: false | ||
| 4 | add_index :related_assets, :page_id, unique: true, where: "headline = true", | ||
| 5 | name: "index_related_assets_on_page_id_headline_uniqueness" | ||
| 6 | end | ||
| 7 | end | ||
diff --git a/lib/tasks/assets.rake b/lib/tasks/assets.rake index 563dfad..9d4b048 100644 --- a/lib/tasks/assets.rake +++ b/lib/tasks/assets.rake | |||
| @@ -16,4 +16,24 @@ namespace :assets do | |||
| 16 | puts "Regenerated variants for Asset##{asset.id} (#{asset.name})" | 16 | puts "Regenerated variants for Asset##{asset.id} (#{asset.name})" |
| 17 | end | 17 | end |
| 18 | end | 18 | end |
| 19 | |||
| 20 | desc "One-shot: flag each live page's current first-by-position image as its headline" | ||
| 21 | task backfill_headline: :environment do | ||
| 22 | count = 0 | ||
| 23 | page_ids = (Node.where.not(head_id: nil).pluck(:head_id) + | ||
| 24 | Node.where.not(draft_id: nil).pluck(:draft_id)).uniq | ||
| 25 | |||
| 26 | Page.where(id: page_ids).find_each do |page| | ||
| 27 | next if page.related_assets.where(headline: true).exists? | ||
| 28 | |||
| 29 | first_image = page.related_assets.joins(:asset).merge(Asset.images).order(:position).first | ||
| 30 | next unless first_image | ||
| 31 | |||
| 32 | first_image.update!(headline: true) | ||
| 33 | count += 1 | ||
| 34 | end | ||
| 35 | |||
| 36 | puts "Flagged #{count} pages' current first image as headline." | ||
| 37 | end | ||
| 38 | |||
| 19 | end | 39 | end |
diff --git a/public/javascripts/admin_interface.js b/public/javascripts/admin_interface.js index e9f96c2..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 | ||
| 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 = { | 109 | cccms = { |
| 122 | setup_autosave : function() { | 110 | setup_autosave : function() { |
| 123 | 111 | ||
| @@ -277,10 +265,13 @@ cccms = { | |||
| 277 | 265 | ||
| 278 | var items = $('#related_asset_list li').map(function() { | 266 | var items = $('#related_asset_list li').map(function() { |
| 279 | return { | 267 | return { |
| 268 | id: $(this).data('asset-id'), | ||
| 280 | thumb: $(this).find('img').attr('src'), | 269 | thumb: $(this).find('img').attr('src'), |
| 281 | large: $(this).data('large-url'), | 270 | large: $(this).data('large-url'), |
| 282 | original: $(this).data('original-url'), | 271 | original: $(this).data('original-url'), |
| 283 | name: $(this).data('name') | 272 | name: $(this).data('name'), |
| 273 | hasCredit: $(this).data('has-credit') === true, | ||
| 274 | headline: $(this).data('headline') === true | ||
| 284 | }; | 275 | }; |
| 285 | }).get(); | 276 | }).get(); |
| 286 | cccms.inline_images.items = items; | 277 | cccms.inline_images.items = items; |
| @@ -293,7 +284,7 @@ cccms = { | |||
| 293 | items.forEach(function(item, i) { | 284 | items.forEach(function(item, i) { |
| 294 | var wrapper = $('<div class="inline_image_picker_item"></div>'); | 285 | var wrapper = $('<div class="inline_image_picker_item"></div>'); |
| 295 | wrapper.append($('<img>').attr('src', item.thumb).attr('data-index', i)); | 286 | wrapper.append($('<img>').attr('src', item.thumb).attr('data-index', i)); |
| 296 | if (i === 0) { | 287 | if (item.headline) { |
| 297 | wrapper.append($('<span>', { "class": "inline_image_picker_headline_badge" }).text("Headline")); | 288 | wrapper.append($('<span>', { "class": "inline_image_picker_headline_badge" }).text("Headline")); |
| 298 | } | 289 | } |
| 299 | grid.append(wrapper); | 290 | grid.append(wrapper); |
| @@ -317,8 +308,13 @@ cccms = { | |||
| 317 | ? 'inline-image inline-image--full' | 308 | ? 'inline-image inline-image--full' |
| 318 | : 'inline-image inline-image--half inline-image--' + placement; | 309 | : 'inline-image inline-image--half inline-image--' + placement; |
| 319 | 310 | ||
| 320 | var html = '<a href="' + item.original + '" class="glightbox" data-gallery="page-' + cccms.inline_images.node_id + '">' + | 311 | var esc = cccms.inline_images.escape_attr; |
| 321 | '<img src="' + item.large + '" class="' + classes + '" alt="' + cccms.inline_images.escape_attr(item.name) + '"></a>'; | 312 | var titleForGlightbox = (item.name || '').replace(/;/g, ','); |
| 313 | var glightboxOpts = 'title: ' + esc(titleForGlightbox) + | ||
| 314 | (item.hasCredit ? '; description: #credit_for_asset_' + esc(item.id) : '') + ';'; | ||
| 315 | var html = '<a href="' + esc(item.original) + '" class="glightbox" data-gallery="page-' + esc(cccms.inline_images.node_id) + '"' + | ||
| 316 | ' data-glightbox="' + glightboxOpts + '">' + | ||
| 317 | '<img src="' + esc(item.large) + '" class="' + classes + '" alt="' + esc(item.name) + '"></a>'; | ||
| 322 | 318 | ||
| 323 | cccms.inline_images.editor.insertContent(html); | 319 | cccms.inline_images.editor.insertContent(html); |
| 324 | $('#inline_image_picker').hide(); | 320 | $('#inline_image_picker').hide(); |
| @@ -356,77 +352,6 @@ menu_item_sorter = { | |||
| 356 | } | 352 | } |
| 357 | } | 353 | } |
| 358 | 354 | ||
| 359 | image_interface = { | ||
| 360 | |||
| 361 | initialize : function() { | ||
| 362 | |||
| 363 | $("#image_browser").hide(); | ||
| 364 | image_interface.initialize_sortable_image_box(); | ||
| 365 | image_interface.connect_browser_and_box(); | ||
| 366 | image_interface.set_droppable_behavior(); | ||
| 367 | image_interface.bind_image_browser_toggle(); | ||
| 368 | }, | ||
| 369 | |||
| 370 | |||
| 371 | set_droppable_behavior : function() { | ||
| 372 | $("ul#image_box").droppable({ | ||
| 373 | out : function(event, ui) { | ||
| 374 | $(ui.draggable).fadeTo("fast", 0.4); | ||
| 375 | |||
| 376 | $(ui.draggable).bind("mouseup", function() { | ||
| 377 | $(this).remove(); | ||
| 378 | }); | ||
| 379 | }, | ||
| 380 | over : function(event, ui) { | ||
| 381 | $(ui.draggable).fadeTo("fast", 1.0); | ||
| 382 | $(ui.draggable).unbind("mouseup"); | ||
| 383 | } | ||
| 384 | }); | ||
| 385 | }, | ||
| 386 | |||
| 387 | connect_browser_and_box : function() { | ||
| 388 | $("#image_browser ul li").draggable({ | ||
| 389 | connectToSortable : 'ul#image_box', | ||
| 390 | helper : 'clone', | ||
| 391 | revert : 'invalid' | ||
| 392 | }); | ||
| 393 | }, | ||
| 394 | |||
| 395 | initialize_sortable_image_box : function() { | ||
| 396 | |||
| 397 | $("ul#image_box").sortable({ | ||
| 398 | revert : true, | ||
| 399 | update : function(event, ui) { | ||
| 400 | images = $("ul#image_box").sortable("serialize", {attribute : "rel"}); | ||
| 401 | |||
| 402 | $.ajax({ | ||
| 403 | type : "POST", | ||
| 404 | url : "/pages/" + $("ul#image_box").attr("rel") + "/sort_images", | ||
| 405 | dataType : "json", | ||
| 406 | data : images + "&_method=put", | ||
| 407 | success : function() { | ||
| 408 | } | ||
| 409 | }); | ||
| 410 | } | ||
| 411 | }); | ||
| 412 | }, | ||
| 413 | |||
| 414 | bind_image_browser_toggle : function() { | ||
| 415 | $("#image_browser_toggle").bind("click", function(){ | ||
| 416 | if ($("#image_browser_toggle").attr("class") == "unselected") { | ||
| 417 | $("#image_browser_toggle").attr("class", "selected"); | ||
| 418 | $("#image_browser").show(); | ||
| 419 | } | ||
| 420 | else { | ||
| 421 | $("#image_browser_toggle").attr("class", "unselected"); | ||
| 422 | $("#image_browser").hide(); | ||
| 423 | } | ||
| 424 | |||
| 425 | return false; | ||
| 426 | }); | ||
| 427 | } | ||
| 428 | } | ||
| 429 | |||
| 430 | rrule_builder = { | 355 | rrule_builder = { |
| 431 | initialize : function() { | 356 | initialize : function() { |
| 432 | 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 @@ | |||
| 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 3340fa7..c6c6a19 100644 --- a/public/javascripts/related_assets.js +++ b/public/javascripts/related_assets.js | |||
| @@ -5,8 +5,6 @@ related_assets = { | |||
| 5 | var createUrl = container.data("create-url"); | 5 | var createUrl = container.data("create-url"); |
| 6 | var list = $("#related_asset_list"); | 6 | var list = $("#related_asset_list"); |
| 7 | 7 | ||
| 8 | related_assets.update_headline_badge(list); | ||
| 9 | |||
| 10 | initSearchPicker({ | 8 | initSearchPicker({ |
| 11 | inputSelector: "#related_asset_search_term", | 9 | inputSelector: "#related_asset_search_term", |
| 12 | resultsSelector: "#related_asset_search_results", | 10 | resultsSelector: "#related_asset_search_results", |
| @@ -44,7 +42,6 @@ related_assets = { | |||
| 44 | url: ui.item.data("url"), | 42 | url: ui.item.data("url"), |
| 45 | data: "position=" + newPosition | 43 | data: "position=" + newPosition |
| 46 | }); | 44 | }); |
| 47 | related_assets.update_headline_badge(list); | ||
| 48 | } | 45 | } |
| 49 | }); | 46 | }); |
| 50 | 47 | ||
| @@ -55,21 +52,29 @@ related_assets = { | |||
| 55 | url: item.data("url"), | 52 | url: item.data("url"), |
| 56 | success: function() { | 53 | success: function() { |
| 57 | item.remove(); | 54 | item.remove(); |
| 58 | related_assets.update_headline_badge(list); | ||
| 59 | } | 55 | } |
| 60 | }); | 56 | }); |
| 61 | }); | 57 | }); |
| 62 | }, | ||
| 63 | 58 | ||
| 64 | update_headline_badge: function(list) { | 59 | list.on("click", ".related_asset_set_headline", function() { |
| 65 | list.find(".related_asset_headline_badge").remove(); | 60 | var button = $(this); |
| 66 | var first = list.children("li").first(); | 61 | var item = button.closest("li"); |
| 67 | if (first.length) { | 62 | var makeHeadline = button.attr("aria-pressed") !== "true"; |
| 68 | $("<span>", { | 63 | |
| 69 | "class": "related_asset_headline_badge", | 64 | $.ajax({ |
| 70 | "title": "Shown automatically as this page's headline image" | 65 | type: "PATCH", |
| 71 | }).text("Headline").insertBefore(first.find(".related_asset_remove")); | 66 | url: item.data("url"), |
| 72 | } | 67 | data: "headline=" + makeHeadline, |
| 68 | success: function() { | ||
| 69 | list.children("li").removeClass("is_headline") | ||
| 70 | .find(".related_asset_set_headline").attr("aria-pressed", "false"); | ||
| 71 | if (makeHeadline) { | ||
| 72 | item.addClass("is_headline"); | ||
| 73 | button.attr("aria-pressed", "true"); | ||
| 74 | } | ||
| 75 | } | ||
| 76 | }); | ||
| 77 | }); | ||
| 73 | }, | 78 | }, |
| 74 | 79 | ||
| 75 | attach: function(assetId, createUrl, list) { | 80 | attach: function(assetId, createUrl, list) { |
| @@ -81,13 +86,14 @@ related_assets = { | |||
| 81 | success: function(related) { | 86 | success: function(related) { |
| 82 | var item = $($("#related_asset_template").html().trim()); | 87 | var item = $($("#related_asset_template").html().trim()); |
| 83 | item.attr("data-url", related.url); | 88 | item.attr("data-url", related.url); |
| 89 | item.attr("data-asset-id", related.asset_id); | ||
| 90 | item.attr("data-has-credit", related.has_credit); | ||
| 84 | item.attr("data-large-url", related.large_url); | 91 | item.attr("data-large-url", related.large_url); |
| 85 | item.attr("data-original-url", related.original_url); | 92 | item.attr("data-original-url", related.original_url); |
| 86 | item.attr("data-name", related.name); | 93 | item.attr("data-name", related.name); |
| 87 | item.find("img").attr("src", related.thumb_url); | 94 | item.find("img").attr("src", related.thumb_url); |
| 88 | list.append(item); | 95 | list.append(item); |
| 89 | list.sortable("refresh"); | 96 | list.sortable("refresh"); |
| 90 | related_assets.update_headline_badge(list); | ||
| 91 | $("#related_asset_search_term").val(""); | 97 | $("#related_asset_search_term").val(""); |
| 92 | $("#related_asset_search_results").slideUp().empty(); | 98 | $("#related_asset_search_results").slideUp().empty(); |
| 93 | } | 99 | } |
diff --git a/public/stylesheets/admin.css b/public/stylesheets/admin.css index 0e11747..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 | |||
| 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 { | 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; |
| @@ -1327,15 +1322,6 @@ div#image_browser ul li { | |||
| 1327 | height: 1rem; | 1322 | height: 1rem; |
| 1328 | } | 1323 | } |
| 1329 | 1324 | ||
| 1330 | .related_asset_headline_badge { | ||
| 1331 | background: #fff3cd; | ||
| 1332 | color: #6b5900; | ||
| 1333 | border-radius: 999px; | ||
| 1334 | padding: 0.1rem 0.6rem; | ||
| 1335 | font-size: 0.75em; | ||
| 1336 | white-space: nowrap; | ||
| 1337 | } | ||
| 1338 | |||
| 1339 | .related_asset_remove { | 1325 | .related_asset_remove { |
| 1340 | display: flex; | 1326 | display: flex; |
| 1341 | align-items: center; | 1327 | align-items: center; |
| @@ -1372,6 +1358,36 @@ div#image_browser ul li { | |||
| 1372 | border-radius: 4px; | 1358 | border-radius: 4px; |
| 1373 | } | 1359 | } |
| 1374 | 1360 | ||
| 1361 | .related_asset_set_headline { | ||
| 1362 | display: flex; | ||
| 1363 | align-items: center; | ||
| 1364 | justify-content: center; | ||
| 1365 | background: none; | ||
| 1366 | border: none; | ||
| 1367 | padding: 2px; | ||
| 1368 | cursor: pointer; | ||
| 1369 | color: #969696; | ||
| 1370 | } | ||
| 1371 | |||
| 1372 | .related_asset_set_headline svg { | ||
| 1373 | width: 0.9rem; | ||
| 1374 | height: 0.9rem; | ||
| 1375 | } | ||
| 1376 | |||
| 1377 | .related_asset_set_headline:hover, | ||
| 1378 | .related_asset_set_headline[aria-pressed="true"] { | ||
| 1379 | color: #f5b400; | ||
| 1380 | } | ||
| 1381 | |||
| 1382 | .related_asset_set_headline[aria-pressed="true"] svg { | ||
| 1383 | fill: currentColor; | ||
| 1384 | } | ||
| 1385 | |||
| 1386 | .thumbnail_list li.is_headline { | ||
| 1387 | border-color: #fff3cd; | ||
| 1388 | background-color: #fffdf5; | ||
| 1389 | } | ||
| 1390 | |||
| 1375 | /* ============================================================ | 1391 | /* ============================================================ |
| 1376 | Live edit preview | 1392 | Live edit preview |
| 1377 | ============================================================ */ | 1393 | ============================================================ */ |
diff --git a/public/stylesheets/ccc.css b/public/stylesheets/ccc.css index a07cc4d..9654594 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; |
| @@ -747,3 +746,32 @@ div.teaser_ruler { | |||
| 747 | float: right; | 746 | float: right; |
| 748 | margin-left: 1rem; | 747 | margin-left: 1rem; |
| 749 | } | 748 | } |
| 749 | |||
| 750 | #asset_credits .asset_credit_block { | ||
| 751 | display: none; | ||
| 752 | } | ||
| 753 | |||
| 754 | #asset_credits .asset_credit_block.headline_credit { | ||
| 755 | display: block; | ||
| 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 | } | ||
diff --git a/test/controllers/related_assets_controller_test.rb b/test/controllers/related_assets_controller_test.rb index 9d20cbb..2384adc 100644 --- a/test/controllers/related_assets_controller_test.rb +++ b/test/controllers/related_assets_controller_test.rb | |||
| @@ -88,4 +88,50 @@ class RelatedAssetsControllerTest < ActionController::TestCase | |||
| 88 | ordered_asset_ids = node.draft.reload.related_assets.map(&:asset_id) | 88 | ordered_asset_ids = node.draft.reload.related_assets.map(&:asset_id) |
| 89 | assert_equal [second.id, first.id], ordered_asset_ids | 89 | assert_equal [second.id, first.id], ordered_asset_ids |
| 90 | end | 90 | end |
| 91 | |||
| 92 | test "update sets the headline flag" do | ||
| 93 | login_as :quentin | ||
| 94 | node = Node.root.children.create!(:slug => "related_assets_headline_test") | ||
| 95 | asset = Asset.create!(:name => "headline-photo", :upload_content_type => "image/png") | ||
| 96 | node.draft.assets << asset | ||
| 97 | related = node.draft.related_assets.find_by(:asset_id => asset.id) | ||
| 98 | |||
| 99 | patch :update, params: { :node_id => node.id, :id => related.id, :headline => "true" } | ||
| 100 | |||
| 101 | assert_response :success | ||
| 102 | assert related.reload.headline? | ||
| 103 | end | ||
| 104 | |||
| 105 | test "update with headline=true clears any previous headline on the same page" do | ||
| 106 | login_as :quentin | ||
| 107 | node = Node.root.children.create!(:slug => "related_assets_headline_swap_test") | ||
| 108 | first = Asset.create!(:name => "first-headline", :upload_content_type => "image/png") | ||
| 109 | second = Asset.create!(:name => "second-headline", :upload_content_type => "image/png") | ||
| 110 | node.draft.assets << first | ||
| 111 | node.draft.assets << second | ||
| 112 | |||
| 113 | first_related = node.draft.related_assets.find_by(:asset_id => first.id) | ||
| 114 | second_related = node.draft.related_assets.find_by(:asset_id => second.id) | ||
| 115 | first_related.update!(:headline => true) | ||
| 116 | |||
| 117 | patch :update, params: { :node_id => node.id, :id => second_related.id, :headline => "true" } | ||
| 118 | |||
| 119 | assert_response :success | ||
| 120 | assert_not first_related.reload.headline? | ||
| 121 | assert second_related.reload.headline? | ||
| 122 | end | ||
| 123 | |||
| 124 | test "update with headline=false clears the headline" do | ||
| 125 | login_as :quentin | ||
| 126 | node = Node.root.children.create!(:slug => "related_assets_headline_unset_test") | ||
| 127 | asset = Asset.create!(:name => "unset-headline", :upload_content_type => "image/png") | ||
| 128 | node.draft.assets << asset | ||
| 129 | related = node.draft.related_assets.find_by(:asset_id => asset.id) | ||
| 130 | related.update!(:headline => true) | ||
| 131 | |||
| 132 | patch :update, params: { :node_id => node.id, :id => related.id, :headline => "false" } | ||
| 133 | |||
| 134 | assert_response :success | ||
| 135 | assert_not related.reload.headline? | ||
| 136 | end | ||
| 91 | end | 137 | end |
diff --git a/test/models/asset_license_test.rb b/test/models/asset_license_test.rb new file mode 100644 index 0000000..9f2d04f --- /dev/null +++ b/test/models/asset_license_test.rb | |||
| @@ -0,0 +1,31 @@ | |||
| 1 | require 'test_helper' | ||
| 2 | |||
| 3 | class AssetLicenseTest < ActiveSupport::TestCase | ||
| 4 | test "find returns a populated entry for a known key" do | ||
| 5 | entry = AssetLicense.find("cc_by_4") | ||
| 6 | |||
| 7 | assert_equal "cc_by_4", entry.key | ||
| 8 | assert_equal "https://creativecommons.org/licenses/by/4.0/", entry.url | ||
| 9 | assert entry.requires_attribution | ||
| 10 | assert_equal "license", entry.style | ||
| 11 | end | ||
| 12 | |||
| 13 | test "find returns nil for an unknown or blank key" do | ||
| 14 | assert_nil AssetLicense.find("not_a_real_license") | ||
| 15 | assert_nil AssetLicense.find(nil) | ||
| 16 | assert_nil AssetLicense.find("") | ||
| 17 | end | ||
| 18 | |||
| 19 | test "keys includes every dictionary entry" do | ||
| 20 | assert_includes AssetLicense.keys, "cc0" | ||
| 21 | assert_includes AssetLicense.keys, "own_work" | ||
| 22 | end | ||
| 23 | |||
| 24 | test "a note-style entry has no attribution requirement and no url" do | ||
| 25 | entry = AssetLicense.find("own_work") | ||
| 26 | |||
| 27 | assert_not entry.requires_attribution | ||
| 28 | assert_nil entry.url | ||
| 29 | assert_equal "note", entry.style | ||
| 30 | end | ||
| 31 | end | ||
diff --git a/test/models/asset_test.rb b/test/models/asset_test.rb index a1041e4..d246abe 100644 --- a/test/models/asset_test.rb +++ b/test/models/asset_test.rb | |||
| @@ -19,5 +19,26 @@ class AssetTest < ActiveSupport::TestCase | |||
| 19 | assert_equal 0, draft.assets.length | 19 | assert_equal 0, draft.assets.length |
| 20 | assert_equal 0, RelatedAsset.count | 20 | assert_equal 0, RelatedAsset.count |
| 21 | end | 21 | end |
| 22 | 22 | ||
| 23 | test "image? is true for supported image content types" do | ||
| 24 | assert Asset.new(:upload_content_type => "image/png").image? | ||
| 25 | assert Asset.new(:upload_content_type => "image/jpeg").image? | ||
| 26 | end | ||
| 27 | |||
| 28 | test "image? is false for non-image content types" do | ||
| 29 | assert_not Asset.new(:upload_content_type => "application/pdf").image? | ||
| 30 | assert_not Asset.new(:upload_content_type => nil).image? | ||
| 31 | end | ||
| 32 | |||
| 33 | test "license_key must be a known dictionary key" do | ||
| 34 | asset = Asset.new(:license_key => "not_a_real_license") | ||
| 35 | I18n.with_locale(:en) do | ||
| 36 | assert_not asset.valid? | ||
| 37 | assert_includes asset.errors[:license_key], "is not included in the list" | ||
| 38 | end | ||
| 39 | end | ||
| 40 | |||
| 41 | test "license_key may be blank" do | ||
| 42 | assert Asset.new(:license_key => nil).valid? | ||
| 43 | end | ||
| 23 | end | 44 | end |
diff --git a/test/models/helpers/content_helper_test.rb b/test/models/helpers/content_helper_test.rb index a7ed478..d6c7b43 100644 --- a/test/models/helpers/content_helper_test.rb +++ b/test/models/helpers/content_helper_test.rb | |||
| @@ -2,7 +2,81 @@ require 'test_helper' | |||
| 2 | 2 | ||
| 3 | class ContentHelperTest < ActionView::TestCase | 3 | class ContentHelperTest < ActionView::TestCase |
| 4 | test "weekday_abbr delegates through the current I18n locale" do | 4 | test "weekday_abbr delegates through the current I18n locale" do |
| 5 | I18n.locale = :de | 5 | I18n.with_locale(:de) do |
| 6 | assert_equal "Mo", weekday_abbr(Time.parse("2026-07-06")) | 6 | assert_equal "Mo", weekday_abbr(Time.parse("2026-07-06")) |
| 7 | end | ||
| 8 | end | ||
| 9 | |||
| 10 | test "asset_credit returns nil for a nil asset" do | ||
| 11 | assert_nil asset_credit(nil) | ||
| 12 | end | ||
| 13 | |||
| 14 | test "asset_credit returns nil when creator, source, and license are all blank" do | ||
| 15 | assert_nil asset_credit(Asset.new(:name => "blank")) | ||
| 16 | end | ||
| 17 | |||
| 18 | test "asset_credit renders creator, source link, and license link when all are present" do | ||
| 19 | asset = Asset.new(:name => "demo", :creator => "Jane Doe", | ||
| 20 | :source_url => "https://example.org/photo", :license_key => "cc_by_4") | ||
| 21 | |||
| 22 | result = I18n.with_locale(:en) { asset_credit(asset) } | ||
| 23 | |||
| 24 | assert_match %r{<a href="https://example.org/photo">Photo demo</a>}, result | ||
| 25 | assert_match "by Jane Doe", result | ||
| 26 | assert_match %r{<a href="https://creativecommons.org/licenses/by/4.0/">Licensed under CC BY 4.0</a>}, result | ||
| 27 | end | ||
| 28 | |||
| 29 | test "asset_credit falls back to plain text when source_url is blank" do | ||
| 30 | asset = Asset.new(:name => "demo", :creator => "Jane Doe", :license_key => "cc_by_4") | ||
| 31 | assert_no_match %r{<a href="[^"]*">Photo demo</a>}, I18n.with_locale(:en) { asset_credit(asset) } | ||
| 32 | end | ||
| 33 | |||
| 34 | test "asset_credit omits the 'by' clause when creator is blank" do | ||
| 35 | asset = Asset.new(:name => "demo", :source_url => "https://example.org/photo", :license_key => "cc_by_4") | ||
| 36 | assert_no_match "by ", I18n.with_locale(:en) { asset_credit(asset) } | ||
| 37 | end | ||
| 38 | |||
| 39 | test "a note-style license renders with no 'Licensed under' prefix and no link" do | ||
| 40 | asset = Asset.new(:name => "demo", :creator => "CCC", :license_key => "own_work") | ||
| 41 | result = I18n.with_locale(:en) { asset_credit(asset) } | ||
| 42 | |||
| 43 | assert_match "Own work", result | ||
| 44 | assert_no_match "Licensed under", result | ||
| 45 | assert_no_match "<a", result | ||
| 46 | end | ||
| 47 | |||
| 48 | test "asset_credit degrades gracefully when the license_key is no longer in the dictionary" do | ||
| 49 | asset = Asset.new(:name => "demo", :creator => "Jane Doe") | ||
| 50 | asset.license_key = "a_retired_key" | ||
| 51 | |||
| 52 | result = I18n.with_locale(:en) { asset_credit(asset) } | ||
| 53 | |||
| 54 | assert_match "Photo demo by Jane Doe", result | ||
| 55 | assert_no_match "Licensed under", result | ||
| 56 | end | ||
| 57 | |||
| 58 | test "headline_image renders nothing when no images are attached" do | ||
| 59 | node = Node.root.children.create!(:slug => "headline_image_empty_test") | ||
| 60 | @page = node.draft | ||
| 61 | assert_nil headline_image | ||
| 62 | end | ||
| 63 | |||
| 64 | test "headline_image renders the flagged headline asset" do | ||
| 65 | node = Node.root.children.create!(:slug => "headline_image_flagged_test") | ||
| 66 | asset = Asset.create!(:name => "flagged", :upload_content_type => "image/png") | ||
| 67 | node.draft.assets << asset | ||
| 68 | node.draft.related_assets.find_by(:asset_id => asset.id).update!(:headline => true) | ||
| 69 | @page = node.draft | ||
| 70 | |||
| 71 | assert_match "glightbox", headline_image | ||
| 72 | end | ||
| 73 | |||
| 74 | test "headline_image falls back to a gallery-open caption when no headline is flagged" do | ||
| 75 | node = Node.root.children.create!(:slug => "headline_image_no_flag_test") | ||
| 76 | asset = Asset.create!(:name => "unflagged", :upload_content_type => "image/png") | ||
| 77 | node.draft.assets << asset | ||
| 78 | @page = node.draft | ||
| 79 | |||
| 80 | I18n.with_locale(:en) { assert_match t(:open_gallery), headline_image } | ||
| 7 | end | 81 | end |
| 8 | end | 82 | end |
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 a739e6b..710b4cc 100644 --- a/test/models/related_asset_test.rb +++ b/test/models/related_asset_test.rb | |||
| @@ -1,8 +1,44 @@ | |||
| 1 | require 'test_helper' | 1 | require 'test_helper' |
| 2 | 2 | ||
| 3 | class RelatedImageTest < ActiveSupport::TestCase | 3 | class RelatedAssetTest < ActiveSupport::TestCase |
| 4 | # Replace this with your real tests. | 4 | test "headline can be set on an image asset" do |
| 5 | test "the truth" do | 5 | node = Node.root.children.create!(:slug => "related_asset_headline_image_test") |
| 6 | assert true | 6 | asset = Asset.create!(:name => "photo", :upload_content_type => "image/png") |
| 7 | node.draft.assets << asset | ||
| 8 | related = node.draft.related_assets.find_by(:asset_id => asset.id) | ||
| 9 | |||
| 10 | related.headline = true | ||
| 11 | assert related.valid? | ||
| 12 | end | ||
| 13 | |||
| 14 | test "headline cannot be set on a non-image asset" do | ||
| 15 | node = Node.root.children.create!(:slug => "related_asset_headline_pdf_test") | ||
| 16 | asset = Asset.create!(:name => "programme", :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_not related.valid? | ||
| 22 | assert_includes related.errors[:headline], "can only be set on image assets" | ||
| 23 | end | ||
| 24 | |||
| 25 | test "the headline validation does not raise when asset is missing" do | ||
| 26 | related = RelatedAsset.new(:headline => true) | ||
| 27 | assert_not related.valid? | ||
| 28 | end | ||
| 29 | |||
| 30 | test "at most one headline per page is enforced at the database level" do | ||
| 31 | node = Node.root.children.create!(:slug => "related_asset_headline_uniqueness_test") | ||
| 32 | first = Asset.create!(:name => "first", :upload_content_type => "image/png") | ||
| 33 | second = Asset.create!(:name => "second", :upload_content_type => "image/png") | ||
| 34 | node.draft.assets << first | ||
| 35 | node.draft.assets << second | ||
| 36 | |||
| 37 | node.draft.related_assets.find_by(:asset_id => first.id).update!(:headline => true) | ||
| 38 | second_related = node.draft.related_assets.find_by(:asset_id => second.id) | ||
| 39 | |||
| 40 | assert_raises(ActiveRecord::RecordNotUnique) do | ||
| 41 | RelatedAsset.where(:id => second_related.id).update_all(:headline => true) | ||
| 42 | end | ||
| 7 | end | 43 | end |
| 8 | end | 44 | end |
