diff options
Diffstat (limited to 'app')
37 files changed, 678 insertions, 240 deletions
diff --git a/app/controllers/assets_controller.rb b/app/controllers/assets_controller.rb index d150e06..fbede0a 100644 --- a/app/controllers/assets_controller.rb +++ b/app/controllers/assets_controller.rb | |||
| @@ -28,6 +28,7 @@ class AssetsController < ApplicationController | |||
| 28 | # GET /assets/new.xml | 28 | # GET /assets/new.xml |
| 29 | def new | 29 | def new |
| 30 | @asset = Asset.new | 30 | @asset = Asset.new |
| 31 | @attach_node = Node.not_in_trash.find_by(:id => params[:node_id]) if params[:node_id].present? | ||
| 31 | 32 | ||
| 32 | respond_to do |format| | 33 | respond_to do |format| |
| 33 | format.html # new.html.erb | 34 | format.html # new.html.erb |
| @@ -44,10 +45,12 @@ class AssetsController < ApplicationController | |||
| 44 | # POST /assets.xml | 45 | # POST /assets.xml |
| 45 | def create | 46 | def create |
| 46 | @asset = Asset.new(asset_params) | 47 | @asset = Asset.new(asset_params) |
| 48 | attach_node = Node.not_in_trash.find_by(:id => params[:node_id]) if params[:node_id].present? | ||
| 47 | 49 | ||
| 48 | respond_to do |format| | 50 | respond_to do |format| |
| 49 | if @asset.save | 51 | if @asset.save |
| 50 | flash[:notice] = 'Asset was successfully created.' | 52 | flash[:notice] = 'Asset was successfully created.' |
| 53 | attach_to(attach_node) if attach_node | ||
| 51 | format.html { redirect_to(@asset) } | 54 | format.html { redirect_to(@asset) } |
| 52 | format.xml { render :xml => @asset, :status => :created, :location => @asset } | 55 | format.xml { render :xml => @asset, :status => :created, :location => @asset } |
| 53 | else | 56 | else |
| @@ -89,6 +92,25 @@ class AssetsController < ApplicationController | |||
| 89 | private | 92 | private |
| 90 | 93 | ||
| 91 | def asset_params | 94 | def asset_params |
| 92 | params.require(:asset).permit(:name, :upload) | 95 | params.require(:asset).permit(:name, :upload, :creator, :source_url, :license_key) |
| 96 | end | ||
| 97 | |||
| 98 | def attach_to node | ||
| 99 | result = node.attach_asset!(@asset, :user => current_user, | ||
| 100 | :headline => params[:headline].present?) | ||
| 101 | flash[:notice] = | ||
| 102 | if result[:attached].zero? | ||
| 103 | "Asset saved — it was already attached to “#{node.title}”." | ||
| 104 | else | ||
| 105 | "Asset was successfully created and attached to “#{node.title}”." | ||
| 106 | end | ||
| 107 | case result[:headline] | ||
| 108 | when :set then flash[:notice] += " It is now the page's headline." | ||
| 109 | when :kept_existing then flash[:headline_kept_path] = node_path(node) | ||
| 110 | when :not_eligible then flash[:error] = "This asset type cannot be a headline." | ||
| 111 | end | ||
| 112 | rescue LockedByAnotherUser | ||
| 113 | flash[:locked_by] = node.lock_owner&.login | ||
| 114 | flash[:locked_node_path] = node_path(node) | ||
| 93 | end | 115 | end |
| 94 | end | 116 | end |
diff --git a/app/controllers/csp_reports_controller.rb b/app/controllers/csp_reports_controller.rb index a8f8edb..5a3b55e 100644 --- a/app/controllers/csp_reports_controller.rb +++ b/app/controllers/csp_reports_controller.rb | |||
| @@ -12,9 +12,9 @@ class CspReportsController < ApplicationController | |||
| 12 | if report | 12 | if report |
| 13 | directive = report["effective-directive"] || report["violated-directive"] | 13 | directive = report["effective-directive"] || report["violated-directive"] |
| 14 | at = (URI.parse(report["document-uri"]).path rescue "unparsed") | 14 | at = (URI.parse(report["document-uri"]).path rescue "unparsed") |
| 15 | Rails.logger.warn("CSP violation: #{directive} blocked=#{report['blocked-uri']} at=#{at}") | 15 | CSP_LOGGER.warn("CSP violation: #{directive} blocked=#{report['blocked-uri']} at=#{at}") |
| 16 | else | 16 | else |
| 17 | Rails.logger.warn("CSP violation: unparseable report (#{raw.to_s.bytesize} bytes)") | 17 | CSP_LOGGER.warn("CSP violation: unparseable report (#{raw.to_s.bytesize} bytes)") |
| 18 | end | 18 | end |
| 19 | 19 | ||
| 20 | head :no_content | 20 | head :no_content |
diff --git a/app/controllers/nodes_controller.rb b/app/controllers/nodes_controller.rb index 6caa827..ce8f053 100644 --- a/app/controllers/nodes_controller.rb +++ b/app/controllers/nodes_controller.rb | |||
| @@ -30,6 +30,7 @@ class NodesController < ApplicationController | |||
| 30 | @node = Node.new node_params | 30 | @node = Node.new node_params |
| 31 | @selected_kind = CccConventions::NODE_KINDS.key?(params[:kind]) ? params[:kind] : "generic" | 31 | @selected_kind = CccConventions::NODE_KINDS.key?(params[:kind]) ? params[:kind] : "generic" |
| 32 | @parent = Node.find(params[:parent_id]) if params.has_key?(:parent_id) | 32 | @parent = Node.find(params[:parent_id]) if params.has_key?(:parent_id) |
| 33 | @attach_asset = Asset.find(params[:asset_id]) if params.has_key?(:asset_id) | ||
| 33 | end | 34 | end |
| 34 | 35 | ||
| 35 | def create | 36 | def create |
| @@ -51,10 +52,19 @@ class NodesController < ApplicationController | |||
| 51 | :action => "create", | 52 | :action => "create", |
| 52 | :title => params[:title], :path => @node.unique_name) | 53 | :title => params[:title], :path => @node.unique_name) |
| 53 | 54 | ||
| 55 | if params[:asset_id].present? && (asset = Asset.find(params[:asset_id])) | ||
| 56 | result = @node.attach_asset!(asset, :user => current_user, | ||
| 57 | :headline => params[:asset_headline].present?) | ||
| 58 | flash[:notice] = "Page created with “#{asset.name}” attached." | ||
| 59 | flash[:notice] += " It is the page's headline." if result[:headline] == :set | ||
| 60 | flash[:error] = "This asset type cannot be a headline." if result[:headline] == :not_eligible | ||
| 61 | end | ||
| 62 | |||
| 54 | redirect_to(edit_node_path(@node)) | 63 | redirect_to(edit_node_path(@node)) |
| 55 | else | 64 | else |
| 56 | @selected_kind = CccConventions::NODE_KINDS.key?(params[:kind]) ? params[:kind] : "generic" | 65 | @selected_kind = CccConventions::NODE_KINDS.key?(params[:kind]) ? params[:kind] : "generic" |
| 57 | @parent = Node.find(params[:parent_id]) if params.has_key?(:parent_id) | 66 | @parent = Node.find(params[:parent_id]) if params.has_key?(:parent_id) |
| 67 | @attach_asset = Asset.find(params[:asset_id]) if params.has_key?(:asset_id) | ||
| 58 | render :new | 68 | render :new |
| 59 | end | 69 | end |
| 60 | end | 70 | end |
| @@ -77,7 +87,7 @@ class NodesController < ApplicationController | |||
| 77 | "This page has unsaved changes from a previous session, shown below. " \ | 87 | "This page has unsaved changes from a previous session, shown below. " \ |
| 78 | "Save to keep them, or use \"Discard Autosave\" below to go back to the last saved version." | 88 | "Save to keep them, or use \"Discard Autosave\" below to go back to the last saved version." |
| 79 | elsif freshly_locked | 89 | elsif freshly_locked |
| 80 | flash.now[:notice] = "Node locked and ready to edit" | 90 | flash.now[:notice] ||= "Node locked and ready to edit" |
| 81 | end | 91 | end |
| 82 | rescue LockedByAnotherUser => e | 92 | rescue LockedByAnotherUser => e |
| 83 | flash[:error] = e.message | 93 | flash[:error] = e.message |
| @@ -223,10 +233,6 @@ class NodesController < ApplicationController | |||
| 223 | @nodes = index_matching(Node.drafts_and_autosaves) | 233 | @nodes = index_matching(Node.drafts_and_autosaves) |
| 224 | end | 234 | end |
| 225 | 235 | ||
| 226 | def recent | ||
| 227 | @nodes = index_matching(Node.recently_changed) | ||
| 228 | end | ||
| 229 | |||
| 230 | def mine | 236 | def mine |
| 231 | base = Node.joins(:pages) | 237 | base = Node.joins(:pages) |
| 232 | .where("pages.user_id = ? or pages.editor_id = ?", current_user, current_user) | 238 | .where("pages.user_id = ? or pages.editor_id = ?", current_user, current_user) |
diff --git a/app/controllers/pages_controller.rb b/app/controllers/pages_controller.rb index 2d08dea..326fbd4 100644 --- a/app/controllers/pages_controller.rb +++ b/app/controllers/pages_controller.rb | |||
| @@ -6,6 +6,19 @@ class PagesController < ApplicationController | |||
| 6 | 6 | ||
| 7 | def preview | 7 | def preview |
| 8 | @page = Page.find(params[:id]) | 8 | @page = Page.find(params[:id]) |
| 9 | unless @page.node | ||
| 10 | node = Node.find_by(autosave_id: @page.id) || | ||
| 11 | Node.find_by(draft_id: @page.id) || | ||
| 12 | Node.find_by(head_id: @page.id) | ||
| 13 | @page.node = node if node | ||
| 14 | end | ||
| 15 | |||
| 16 | node ||= @page.node | ||
| 17 | if node && node.draft_id == @page.id && node.autosave | ||
| 18 | @page = node.autosave | ||
| 19 | @page.node = node | ||
| 20 | end | ||
| 21 | |||
| 9 | 22 | ||
| 10 | if @page | 23 | if @page |
| 11 | template = @page.valid_template | 24 | template = @page.valid_template |
| @@ -15,11 +28,4 @@ class PagesController < ApplicationController | |||
| 15 | ) | 28 | ) |
| 16 | end | 29 | end |
| 17 | end | 30 | end |
| 18 | |||
| 19 | def sort_images | ||
| 20 | page = Page.find(params[:id]) | ||
| 21 | page.update_assets(params[:images]) | ||
| 22 | |||
| 23 | head :ok | ||
| 24 | end | ||
| 25 | end | 31 | end |
diff --git a/app/controllers/related_assets_controller.rb b/app/controllers/related_assets_controller.rb index 5af73fc..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, |
| @@ -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..7a52d9c 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.headline_asset |
| 57 | 57 | render :partial => 'content/headline_image' if @headline_asset || @page.assets.images.any? || @page.assets.pdfs.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..73970e2 100644 --- a/app/models/asset.rb +++ b/app/models/asset.rb | |||
| @@ -1,12 +1,43 @@ | |||
| 1 | class Asset < ApplicationRecord | 1 | class Asset < ApplicationRecord |
| 2 | IMAGE_CONTENT_TYPES = ["image/gif", "image/jpeg", "image/png", "image/webp"] | ||
| 3 | PDF_CONTENT_TYPE = "application/pdf" | ||
| 2 | 4 | ||
| 3 | include FileAttachment | 5 | include FileAttachment |
| 4 | 6 | ||
| 5 | has_many :related_assets, :dependent => :destroy | 7 | has_many :related_assets, :dependent => :destroy |
| 6 | has_many :pages, :through => :related_assets | 8 | has_many :pages, :through => :related_assets |
| 7 | 9 | ||
| 8 | scope :images, -> { where(:upload_content_type => ["image/gif", "image/jpeg", "image/png", "image/webp"]) } | 10 | scope :images, -> { where(:upload_content_type => IMAGE_CONTENT_TYPES) } |
| 9 | scope :documents, -> { where(:upload_content_type => ["application/pdf", "text/plain", "text/rtf"]) } | 11 | scope :documents, -> { where(:upload_content_type => ["application/pdf", "text/plain", "text/rtf"]) } |
| 10 | scope :audio, -> { where(:upload_content_type => ["audio/mpeg", "audio/x-m4a", "audio/wav", "audio/x-wav"]) } | 12 | scope :audio, -> { where(:upload_content_type => ["audio/mpeg", "audio/x-m4a", "audio/wav", "audio/x-wav"]) } |
| 13 | scope :pdfs, -> { where(:upload_content_type => PDF_CONTENT_TYPE) } | ||
| 11 | 14 | ||
| 15 | scope :headline_eligible, -> { where(:upload_content_type => IMAGE_CONTENT_TYPES + [PDF_CONTENT_TYPE]) } | ||
| 16 | |||
| 17 | validates :license_key, inclusion: { in: -> { AssetLicense.keys } }, allow_blank: true | ||
| 18 | |||
| 19 | def image? | ||
| 20 | IMAGE_CONTENT_TYPES.include?(upload_content_type) | ||
| 21 | end | ||
| 22 | |||
| 23 | def pdf? | ||
| 24 | upload_content_type == PDF_CONTENT_TYPE | ||
| 25 | end | ||
| 26 | |||
| 27 | def has_credit? | ||
| 28 | creator.present? || source_url.present? || license_key.present? | ||
| 29 | end | ||
| 30 | |||
| 31 | def show_credit? | ||
| 32 | image? && has_credit? | ||
| 33 | end | ||
| 34 | |||
| 35 | # Nodes whose current lifecycle rows (head, draft or autosave) carry | ||
| 36 | # this asset. Historical revisions also hold RelatedAsset rows; they | ||
| 37 | # are excluded by construction here, since nothing points at them. | ||
| 38 | def attached_nodes | ||
| 39 | page_ids = related_assets.select(:page_id) | ||
| 40 | Node.where("head_id IN (:ids) OR draft_id IN (:ids) OR autosave_id IN (:ids)", | ||
| 41 | :ids => page_ids).distinct | ||
| 42 | end | ||
| 12 | end | 43 | end |
diff --git a/app/models/asset_license.rb b/app/models/asset_license.rb new file mode 100644 index 0000000..ca66d40 --- /dev/null +++ b/app/models/asset_license.rb | |||
| @@ -0,0 +1,15 @@ | |||
| 1 | class AssetLicense | ||
| 2 | Entry = Struct.new(:key, :url, :requires_attribution, :style, keyword_init: true) | ||
| 3 | |||
| 4 | DICTIONARY = YAML.load_file(Rails.root.join("config", "asset_licenses.yml")).freeze | ||
| 5 | |||
| 6 | def self.keys | ||
| 7 | DICTIONARY.keys | ||
| 8 | end | ||
| 9 | |||
| 10 | def self.find(key) | ||
| 11 | entry = DICTIONARY[key.to_s] if key.present? | ||
| 12 | return nil unless entry | ||
| 13 | Entry.new(key: key.to_s, url: entry["url"], requires_attribution: entry["requires_attribution"], style: entry["style"]) | ||
| 14 | end | ||
| 15 | end | ||
diff --git a/app/models/concerns/file_attachment.rb b/app/models/concerns/file_attachment.rb index 613a6f0..6221312 100644 --- a/app/models/concerns/file_attachment.rb +++ b/app/models/concerns/file_attachment.rb | |||
| @@ -31,6 +31,7 @@ module FileAttachment | |||
| 31 | 31 | ||
| 32 | IMAGE_CONTENT_TYPES = %w[image/jpeg image/gif image/png image/webp].freeze | 32 | IMAGE_CONTENT_TYPES = %w[image/jpeg image/gif image/png image/webp].freeze |
| 33 | VECTOR_CONTENT_TYPES = %w[image/svg+xml].freeze | 33 | VECTOR_CONTENT_TYPES = %w[image/svg+xml].freeze |
| 34 | RASTERIZED_CONTENT_TYPES = %w[application/pdf].freeze | ||
| 34 | DISPLAYABLE_AS_IMAGE = IMAGE_CONTENT_TYPES + VECTOR_CONTENT_TYPES | 35 | DISPLAYABLE_AS_IMAGE = IMAGE_CONTENT_TYPES + VECTOR_CONTENT_TYPES |
| 35 | 36 | ||
| 36 | included do | 37 | included do |
| @@ -52,29 +53,49 @@ module FileAttachment | |||
| 52 | build_upload_proxy | 53 | build_upload_proxy |
| 53 | end | 54 | end |
| 54 | 55 | ||
| 56 | def has_variant?(style) | ||
| 57 | upload_file_name.present? && File.exist?(file_path(style)) | ||
| 58 | end | ||
| 59 | |||
| 60 | def previewable? | ||
| 61 | has_variant?(:medium) | ||
| 62 | end | ||
| 63 | |||
| 64 | def variant_filename(style) | ||
| 65 | return upload_file_name if style == :original || !RASTERIZED_CONTENT_TYPES.include?(upload_content_type) | ||
| 66 | File.basename(upload_file_name, ".*") + ".png" | ||
| 67 | end | ||
| 68 | |||
| 55 | private | 69 | private |
| 56 | 70 | ||
| 57 | def build_upload_proxy | 71 | def build_upload_proxy |
| 58 | @upload = UploadProxy.new(self) | 72 | @upload = UploadProxy.new(self) |
| 59 | end | 73 | end |
| 60 | 74 | ||
| 75 | class_methods do | ||
| 76 | def upload_root | ||
| 77 | Rails.env.test? ? Rails.root.join("tmp", "test_uploads") : Rails.root.join("public", "system", "uploads") | ||
| 78 | end | ||
| 79 | end | ||
| 80 | |||
| 81 | def upload_root | ||
| 82 | self.class.upload_root | ||
| 83 | end | ||
| 84 | |||
| 61 | def process_upload | 85 | def process_upload |
| 62 | return unless @pending_upload | 86 | return unless @pending_upload |
| 63 | uploaded_file = @pending_upload | 87 | uploaded_file = @pending_upload |
| 64 | @pending_upload = nil | 88 | @pending_upload = nil |
| 65 | 89 | ||
| 66 | old_dir = Rails.root.join("public", "system", "uploads", id.to_s) | 90 | old_dir = upload_root.join(id.to_s) |
| 91 | |||
| 67 | FileUtils.rm_rf(old_dir) if Dir.exist?(old_dir) | 92 | FileUtils.rm_rf(old_dir) if Dir.exist?(old_dir) |
| 68 | 93 | ||
| 69 | original_path = file_path(:original) | 94 | original_path = file_path(:original) |
| 70 | FileUtils.mkdir_p(File.dirname(original_path)) | 95 | FileUtils.mkdir_p(File.dirname(original_path)) |
| 71 | FileUtils.cp(uploaded_file.tempfile.path, original_path) | 96 | FileUtils.cp(uploaded_file.tempfile.path, original_path) |
| 72 | 97 | ||
| 73 | if IMAGE_CONTENT_TYPES.include?(upload_content_type) | 98 | generate_all_variants(original_path) |
| 74 | generate_variants(original_path) | ||
| 75 | elsif VECTOR_CONTENT_TYPES.include?(upload_content_type) | ||
| 76 | generate_svg_variants(original_path) | ||
| 77 | end | ||
| 78 | end | 99 | end |
| 79 | 100 | ||
| 80 | def generate_variants(original_path) | 101 | def generate_variants(original_path) |
| @@ -93,20 +114,28 @@ module FileAttachment | |||
| 93 | end | 114 | end |
| 94 | end | 115 | end |
| 95 | 116 | ||
| 117 | def generate_all_variants(original_path) | ||
| 118 | if IMAGE_CONTENT_TYPES.include?(upload_content_type) | ||
| 119 | generate_variants(original_path) | ||
| 120 | elsif VECTOR_CONTENT_TYPES.include?(upload_content_type) | ||
| 121 | generate_svg_variants(original_path) | ||
| 122 | elsif RASTERIZED_CONTENT_TYPES.include?(upload_content_type) | ||
| 123 | generate_variants("#{original_path}[0]") | ||
| 124 | end | ||
| 125 | end | ||
| 126 | |||
| 96 | def delete_upload_files | 127 | def delete_upload_files |
| 97 | dir = Rails.root.join("public", "system", "uploads", id.to_s) | 128 | dir = upload_root.join(id.to_s) |
| 98 | FileUtils.rm_rf(dir) if Dir.exist?(dir) | 129 | FileUtils.rm_rf(dir) if Dir.exist?(dir) |
| 99 | end | 130 | end |
| 100 | 131 | ||
| 101 | def file_path(style) | 132 | def file_path(style) |
| 102 | Rails.root.join( | 133 | upload_root.join(id.to_s, style.to_s, variant_filename(style)).to_s |
| 103 | "public", "system", "uploads", | ||
| 104 | id.to_s, style.to_s, upload_file_name | ||
| 105 | ).to_s | ||
| 106 | end | 134 | end |
| 107 | 135 | ||
| 108 | def sanitize_filename(filename) | 136 | def sanitize_filename(filename) |
| 109 | File.basename(filename).gsub(/[^\w\.\-]/, '_') | 137 | name = File.basename(filename).unicode_normalize(:nfc) |
| 138 | name.gsub(/(?u)[^\w\.\-]/, '_') | ||
| 110 | end | 139 | end |
| 111 | 140 | ||
| 112 | # Proxy object returned by asset.upload, providing the Paperclip-compatible | 141 | # Proxy object returned by asset.upload, providing the Paperclip-compatible |
| @@ -118,7 +147,7 @@ module FileAttachment | |||
| 118 | 147 | ||
| 119 | def url(style = :original) | 148 | def url(style = :original) |
| 120 | return "" if @record.upload_file_name.blank? | 149 | return "" if @record.upload_file_name.blank? |
| 121 | "/system/uploads/#{@record.id}/#{style}/#{@record.upload_file_name}" | 150 | "/system/uploads/#{@record.id}/#{style}/#{@record.variant_filename(style)}" |
| 122 | end | 151 | end |
| 123 | 152 | ||
| 124 | def content_type | 153 | def content_type |
diff --git a/app/models/node.rb b/app/models/node.rb index a440c2f..a5a40d3 100644 --- a/app/models/node.rb +++ b/app/models/node.rb | |||
| @@ -442,8 +442,65 @@ class Node < ApplicationRecord | |||
| 442 | end | 442 | end |
| 443 | end | 443 | end |
| 444 | 444 | ||
| 445 | # Attaches an asset to every current lifecycle row -- head, draft and | ||
| 446 | # autosave -- that does not already carry it. Attachments are page- | ||
| 447 | # scoped content (RelatedAsset belongs_to :page; drafts and autosaves | ||
| 448 | # are wholesale clones), so attaching to a single layer is how an | ||
| 449 | # attachment gets silently lost when another layer replaces it at | ||
| 450 | # publish or save. This is the out-of-band counterpart to the | ||
| 451 | # in-editor attach UI; it refuses when someone else holds the editing | ||
| 452 | # lock. Attaching to a head row changes the public page immediately, | ||
| 453 | # by design -- same reasoning as formalizing an already-existing | ||
| 454 | # editorial link. | ||
| 455 | # | ||
| 456 | # headline is a node-level decision: the flag is set on the newly | ||
| 457 | # created joins only when no current row has a headline yet and the | ||
| 458 | # asset is eligible; otherwise the asset is attached plain and the | ||
| 459 | # result says why, so the caller can point the editor at the star in | ||
| 460 | # the editor instead. | ||
| 461 | # | ||
| 462 | # Returns { :attached => n, :already => n, | ||
| 463 | # :headline => nil | :set | :kept_existing | :not_eligible } | ||
| 464 | def attach_asset! asset, user:, headline: false | ||
| 465 | if in_trash? || trash_node? | ||
| 466 | raise ActiveRecord::RecordInvalid.new(self), "Cannot attach assets to a node in the Trash" | ||
| 467 | end | ||
| 468 | |||
| 469 | if lock_owner && lock_owner != user | ||
| 470 | raise( | ||
| 471 | LockedByAnotherUser, | ||
| 472 | "Page is locked by another user who is working on it! " \ | ||
| 473 | "Last modification: #{(self.autosave || self.draft || self.head).updated_at.to_fs(:db)}" | ||
| 474 | ) | ||
| 475 | end | ||
| 476 | |||
| 477 | rows = [head, draft, autosave].compact | ||
| 478 | to_attach = rows.reject { |row| row.related_assets.exists?(:asset_id => asset.id) } | ||
| 479 | |||
| 480 | headline_state = | ||
| 481 | if headline && to_attach.any? | ||
| 482 | if !(asset.image? || asset.pdf?) | ||
| 483 | :not_eligible | ||
| 484 | elsif rows.any? { |row| row.headline_asset.present? } | ||
| 485 | :kept_existing | ||
| 486 | else | ||
| 487 | :set | ||
| 488 | end | ||
| 489 | end | ||
| 490 | |||
| 491 | ActiveRecord::Base.transaction do | ||
| 492 | to_attach.each do |row| | ||
| 493 | row.related_assets.create!(:asset => asset, :headline => headline_state == :set) | ||
| 494 | end | ||
| 495 | end | ||
| 496 | |||
| 497 | { :attached => to_attach.size, | ||
| 498 | :already => rows.size - to_attach.size, | ||
| 499 | :headline => headline_state } | ||
| 500 | end | ||
| 501 | |||
| 445 | def title | 502 | def title |
| 446 | head ? head.title : draft.title | 503 | editable_page&.title |
| 447 | end | 504 | end |
| 448 | 505 | ||
| 449 | def update_unique_names? | 506 | def update_unique_names? |
| @@ -533,13 +590,6 @@ class Node < ApplicationRecord | |||
| 533 | throw :abort | 590 | throw :abort |
| 534 | end | 591 | end |
| 535 | 592 | ||
| 536 | def self.recently_changed | ||
| 537 | includes(:head).where( | ||
| 538 | "pages.updated_at < ? AND pages.updated_at > ? AND nodes.parent_id IS NOT NULL", | ||
| 539 | Time.now, Time.now - 14.days | ||
| 540 | ).order("pages.updated_at desc").references(:head) | ||
| 541 | end | ||
| 542 | |||
| 543 | protected | 593 | protected |
| 544 | def lock_for! current_user | 594 | def lock_for! current_user |
| 545 | self.lock_owner = current_user | 595 | self.lock_owner = current_user |
diff --git a/app/models/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..817e3bd 100644 --- a/app/models/page.rb +++ b/app/models/page.rb | |||
| @@ -220,7 +220,12 @@ class Page < ApplicationRecord | |||
| 220 | end | 220 | end |
| 221 | 221 | ||
| 222 | # Clone asset references | 222 | # Clone asset references |
| 223 | self.assets = page.assets | 223 | self.related_assets.delete_all |
| 224 | page.related_assets.each do |related| | ||
| 225 | self.related_assets.create!(:asset_id => related.asset_id, | ||
| 226 | :position => related.position, | ||
| 227 | :headline => related.headline) | ||
| 228 | end | ||
| 224 | 229 | ||
| 225 | self.save | 230 | self.save |
| 226 | end | 231 | end |
| @@ -287,6 +292,10 @@ class Page < ApplicationRecord | |||
| 287 | end | 292 | end |
| 288 | end | 293 | end |
| 289 | 294 | ||
| 295 | def headline_asset | ||
| 296 | related_assets.find_by(headline: true)&.asset | ||
| 297 | end | ||
| 298 | |||
| 290 | # Returns true if a page has translations where one of them is significantly | 299 | # Returns true if a page has translations where one of them is significantly |
| 291 | # older than the other. | 300 | # older than the other. |
| 292 | # Takes the I18n.default locale and a second :locale to test if the | 301 | # Takes the I18n.default locale and a second :locale to test if the |
| @@ -314,21 +323,6 @@ class Page < ApplicationRecord | |||
| 314 | end | 323 | end |
| 315 | end | 324 | end |
| 316 | 325 | ||
| 317 | def update_assets image_ids | ||
| 318 | |||
| 319 | transaction do | ||
| 320 | self.related_assets.delete_all | ||
| 321 | |||
| 322 | if image_ids | ||
| 323 | image_ids.each_with_index do |id, index| | ||
| 324 | asset = Asset.find(id) | ||
| 325 | self.related_assets.create!(:asset_id => asset.id, :position => index+1) | ||
| 326 | end | ||
| 327 | end | ||
| 328 | end | ||
| 329 | |||
| 330 | end | ||
| 331 | |||
| 332 | # Installs (or re-installs) the trigger that keeps page_translations' | 326 | # Installs (or re-installs) the trigger that keeps page_translations' |
| 333 | # search_vector in sync. Idempotent, safe to call on every boot. | 327 | # search_vector in sync. Idempotent, safe to call on every boot. |
| 334 | # search_vector is populated by a raw Postgres trigger, not anything | 328 | # search_vector is populated by a raw Postgres trigger, not anything |
diff --git a/app/models/related_asset.rb b/app/models/related_asset.rb index 8f16460..8f8d49c 100644 --- a/app/models/related_asset.rb +++ b/app/models/related_asset.rb | |||
| @@ -5,4 +5,13 @@ class RelatedAsset < ApplicationRecord | |||
| 5 | acts_as_list :scope => :page_id | 5 | acts_as_list :scope => :page_id |
| 6 | 6 | ||
| 7 | default_scope -> { order("position ASC") } | 7 | default_scope -> { order("position ASC") } |
| 8 | |||
| 9 | validate :headline_only_for_images | ||
| 10 | |||
| 11 | private | ||
| 12 | |||
| 13 | def headline_only_for_images | ||
| 14 | return unless asset | ||
| 15 | errors.add(:headline, "can only be set on image or PDF assets") if headline? && !(asset.image? || asset.pdf?) | ||
| 16 | end | ||
| 8 | end | 17 | end |
diff --git a/app/views/assets/edit.html.erb b/app/views/assets/edit.html.erb index f198600..0c1dd8f 100644 --- a/app/views/assets/edit.html.erb +++ b/app/views/assets/edit.html.erb | |||
| @@ -1,24 +1,61 @@ | |||
| 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> | ||
| 18 | |||
| 19 | <div class="node_description"><%= f.label :creator %></div> | ||
| 20 | <div class="node_content"><%= f.text_field :creator %></div> | ||
| 13 | 21 | ||
| 14 | <p> | 22 | <div class="node_description"><%= f.label :source_url, "Source URL" %></div> |
| 15 | <label>Replace file:</label><br> | 23 | <div class="node_content"><%= f.text_field :source_url %></div> |
| 16 | <%= f.file_field :upload %> | ||
| 17 | </p> | ||
| 18 | 24 | ||
| 19 | <p> | 25 | <div class="node_description"><%= f.label :license_key, "License" %></div> |
| 20 | <%= f.submit 'Update' %> | 26 | <div class="node_content"> |
| 21 | </p> | 27 | <%= f.select :license_key, |
| 22 | <% end %> | 28 | options_for_select( |
| 29 | [["— none —", ""]] + AssetLicense.keys.map { |key| [t("asset_licenses.#{key}"), key] }, | ||
| 30 | @asset.license_key | ||
| 31 | ) %> | ||
| 32 | </div> | ||
| 23 | 33 | ||
| 24 | <%= link_to 'Show', @asset %> | <%= link_to 'Back', assets_path %> | 34 | <div class="node_description">attach to page</div> |
| 35 | <div class="node_content"> | ||
| 36 | <div class="restore_picker"> | ||
| 37 | <%= text_field_tag :asset_node_search_term, @attach_node&.title, | ||
| 38 | :placeholder => "Search for a page…", :autocomplete => "off" %> | ||
| 39 | <div id="asset_node_search_results" class="search_results" style="display: none"></div> | ||
| 40 | </div> | ||
| 41 | <%= hidden_field_tag :node_id, @attach_node&.id %> | ||
| 42 | <span class="field_hint">Optional — attaches this asset to that page, all pending versions included.</span> | ||
| 43 | </div> | ||
| 44 | |||
| 45 | <div class="node_description"></div> | ||
| 46 | <div class="node_content"> | ||
| 47 | <label><%= check_box_tag :headline, "1" %> attach as the page's headline</label> | ||
| 48 | <span class="field_hint">Applies only if the page has no headline yet.</span> | ||
| 49 | </div> | ||
| 50 | |||
| 51 | <div class="node_description">Actions</div> | ||
| 52 | <div class="node_content node_info_group"> | ||
| 53 | <div class="node_info_group_items"> | ||
| 54 | <div class="node_info_item"><%= f.submit 'Update' %></div> | ||
| 55 | <div class="node_info_item"><%= link_to 'Show', @asset %></div> | ||
| 56 | <div class="node_info_item"><%= link_to 'Back', assets_path %></div> | ||
| 57 | </div> | ||
| 58 | </div> | ||
| 59 | </div> | ||
| 60 | <% end %> | ||
| 61 | </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..7bded94 100644 --- a/app/views/assets/new.html.erb +++ b/app/views/assets/new.html.erb | |||
| @@ -1,17 +1,55 @@ | |||
| 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">attach to page</div> | ||
| 30 | <div class="node_content"> | ||
| 31 | <div class="restore_picker"> | ||
| 32 | <%= text_field_tag :asset_node_search_term, @attach_node&.title, | ||
| 33 | :placeholder => "Search for a page…", :autocomplete => "off" %> | ||
| 34 | <div id="asset_node_search_results" class="search_results" style="display: none"></div> | ||
| 35 | </div> | ||
| 36 | <%= hidden_field_tag :node_id, @attach_node&.id %> | ||
| 37 | <span class="field_hint">Optional — attaches this asset to that page, all pending versions included.</span> | ||
| 38 | </div> | ||
| 39 | |||
| 40 | <div class="node_description"></div> | ||
| 41 | <div class="node_content"> | ||
| 42 | <label><%= check_box_tag :headline, "1" %> attach as the page's headline</label> | ||
| 43 | <span class="field_hint">Applies only if the page has no headline yet.</span> | ||
| 44 | </div> | ||
| 45 | |||
| 46 | <div class="node_description">Actions</div> | ||
| 47 | <div class="node_content node_info_group"> | ||
| 48 | <div class="node_info_group_items"> | ||
| 49 | <div class="node_info_item"><%= f.submit 'Create' %></div> | ||
| 50 | <div class="node_info_item"><%= link_to 'Back', assets_path %></div> | ||
| 51 | </div> | ||
| 52 | </div> | ||
| 53 | </div> | ||
| 54 | <% end %> | ||
| 55 | </div> | ||
diff --git a/app/views/assets/show.html.erb b/app/views/assets/show.html.erb index 5717dd9..2723821 100644 --- a/app/views/assets/show.html.erb +++ b/app/views/assets/show.html.erb | |||
| @@ -16,11 +16,67 @@ | |||
| 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 %> | ||
| 23 | |||
| 24 | <div class="node_description">Attached to</div> | ||
| 25 | <div class="node_content"> | ||
| 26 | <% nodes = @asset.attached_nodes %> | ||
| 27 | <% if nodes.any? %> | ||
| 28 | <ul class="search_results_list"> | ||
| 29 | <% nodes.each do |node| %> | ||
| 30 | <li> | ||
| 31 | <%= link_to node.title, node_path(node) %> | ||
| 32 | <% if node.editable_page&.headline_asset == @asset %> | ||
| 33 | <%= icon("star", library: "tabler", "aria-hidden": true) %> | ||
| 34 | <% end %> | ||
| 35 | <% unless node.head && node.head.related_assets.exists?(:asset_id => @asset.id) %> | ||
| 36 | <span class="field_hint">pending versions only</span> | ||
| 37 | <% end %> | ||
| 38 | </li> | ||
| 39 | <% end %> | ||
| 40 | </ul> | ||
| 41 | <% else %> | ||
| 42 | — not attached to any page — | ||
| 43 | <% end %> | ||
| 44 | <%= link_to new_node_path(:asset_id => @asset.id), :class => "action_button" do %> | ||
| 45 | <%= icon("file-plus", library: "tabler", "aria-hidden": true) %> New page with this attachment | ||
| 46 | <% end %> | ||
| 47 | </div> | ||
| 21 | 48 | ||
| 22 | <div class="node_description">Public Path</div> | 49 | <div class="node_description">Public Path</div> |
| 23 | <div class="node_content"><%= @asset.upload.url.sub(/\?\d+$/, "") %></div> | 50 | <div class="node_content"> |
| 51 | <% public_path = @asset.upload.url.sub(/\?\d+$/, "") %> | ||
| 52 | <%= link_to public_path, public_path, target: "_blank", rel: "noopener" %> | ||
| 53 | <button type="button" class="action_button copy_button" | ||
| 54 | data-copy-url="<%= request.base_url + public_path %>"> | ||
| 55 | <%= icon("copy", library: "tabler", "aria-hidden": true) %> | ||
| 56 | <span class="copy_button_label">Copy URL</span> | ||
| 57 | </button> | ||
| 58 | </div> | ||
| 59 | |||
| 60 | <div class="node_description">Creator</div> | ||
| 61 | <div class="node_content"><%= @asset.creator.presence || "—" %></div> | ||
| 62 | |||
| 63 | <div class="node_description">Source</div> | ||
| 64 | <div class="node_content"> | ||
| 65 | <% if @asset.source_url.present? %> | ||
| 66 | <%= link_to @asset.source_url, @asset.source_url %> | ||
| 67 | <% else %> | ||
| 68 | — | ||
| 69 | <% end %> | ||
| 70 | </div> | ||
| 71 | |||
| 72 | <div class="node_description">License</div> | ||
| 73 | <div class="node_content"> | ||
| 74 | <% if (license = AssetLicense.find(@asset.license_key)) %> | ||
| 75 | <%= t("asset_licenses.#{license.key}") %> | ||
| 76 | <% else %> | ||
| 77 | — | ||
| 78 | <% end %> | ||
| 79 | </div> | ||
| 24 | 80 | ||
| 25 | <div class="node_description">Content Type</div> | 81 | <div class="node_description">Content Type</div> |
| 26 | <div class="node_content"><%= @asset.upload.content_type %></div> | 82 | <div class="node_content"><%= @asset.upload.content_type %></div> |
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..4a3dfdc 100644 --- a/app/views/content/_headline_image.html.erb +++ b/app/views/content/_headline_image.html.erb | |||
| @@ -1,16 +1,59 @@ | |||
| 1 | <%= link_to( | 1 | <% gallery_images = @page.assets.images %> |
| 2 | image_tag(@images[0].upload.url(:headline)), | 2 | <% image_headline = @headline_asset if @headline_asset&.image? %> |
| 3 | @images[0].upload.url, | 3 | <% other_pdfs = @page.assets.pdfs.where.not(id: @headline_asset&.id) %> |
| 4 | :class => "glightbox", | ||
| 5 | :data => { :gallery => "page-#{@page.node.id}" } | ||
| 6 | ) %> | ||
| 7 | 4 | ||
| 8 | <% if 1 < @images.length %> | 5 | <% if @headline_asset&.pdf? %> |
| 9 | <div class="right"> | 6 | <%= link_to @headline_asset.upload.url, :class => "headline_document_card", :target => "_blank", :rel => "noopener" do %> |
| 10 | <%= "#{@images.length} #{t(:images)}" %> | 7 | <%= image_tag @headline_asset.upload.url(:medium), :alt => "", :class => "headline_document_card_thumb" %> |
| 11 | </div> | 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 %> | ||
| 16 | <%= link_to( | ||
| 17 | image_tag(image_headline.upload.url(:headline)), | ||
| 18 | image_headline.upload.url, | ||
| 19 | :class => "glightbox", | ||
| 20 | :data => { :gallery => "page-#{@page.node.id}", | ||
| 21 | :glightbox => glightbox_data(image_headline, image_headline.name), | ||
| 22 | :"credit-selector" => (image_headline.show_credit? ? "#credit_for_asset_#{image_headline.id}" : nil) } | ||
| 23 | ) %> | ||
| 24 | <% if gallery_images.size > 1 %> | ||
| 25 | <div class="right"><%= "#{gallery_images.size} #{t(:images)}" %></div> | ||
| 26 | <% end %> | ||
| 27 | <% elsif gallery_images.any? %> | ||
| 28 | <%= link_to "#{gallery_images.size} #{t(:images)}, #{t(:open_gallery)}", | ||
| 29 | gallery_images.first.upload.url, | ||
| 30 | :class => "glightbox right", | ||
| 31 | :data => { :gallery => "page-#{@page.node.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) } %> | ||
| 12 | <% end %> | 34 | <% end %> |
| 13 | 35 | ||
| 14 | <% @images[1..-1].each do |image| %> | 36 | <% gallery_images.each do |image| %> |
| 15 | <%= link_to "", image.upload.url, :class => "glightbox", :style => "display: none", :data => { :gallery => "page-#{@page.node.id}" } %> | 37 | <% next if image == image_headline %> |
| 38 | <% next if !image_headline && image == gallery_images.first %> | ||
| 39 | <%= link_to "", image.upload.url, :class => "glightbox", :style => "display: none", | ||
| 40 | :data => { :gallery => "page-#{@page.node.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> | ||
| 16 | <% end %> | 59 | <% 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/custom/partials/_chapter.html.erb b/app/views/custom/partials/_chapter.html.erb index 47f3950..5d47679 100644 --- a/app/views/custom/partials/_chapter.html.erb +++ b/app/views/custom/partials/_chapter.html.erb | |||
| @@ -1,7 +1,7 @@ | |||
| 1 | <div class="article_partial chapter_partial" lang="<%= page.effective_lang %>"> | 1 | <div class="article_partial chapter_partial" lang="<%= page.effective_lang %>"> |
| 2 | <div class="chapter_partial_layout"> | 2 | <div class="chapter_partial_layout"> |
| 3 | <% if page.assets.images.any? %> | 3 | <% if page.headline_asset %> |
| 4 | <%= link_to_path image_tag(page.assets.images.first.upload.url(:thumb), :alt => ""), page.node.unique_name, class: "chapter_thumbnail" %> | 4 | <%= link_to_path image_tag(page.headline_asset.upload.url(:thumb), :alt => ""), page.node.unique_name, class: "chapter_thumbnail" %> |
| 5 | <% end %> | 5 | <% end %> |
| 6 | <div class="chapter_partial_content"> | 6 | <div class="chapter_partial_content"> |
| 7 | <h2 class="headline"><%= link_to_path page.title, page.node.unique_name %></h2> | 7 | <h2 class="headline"><%= link_to_path page.title, page.node.unique_name %></h2> |
diff --git a/app/views/layouts/_flash.html.erb b/app/views/layouts/_flash.html.erb new file mode 100644 index 0000000..a67fc86 --- /dev/null +++ b/app/views/layouts/_flash.html.erb | |||
| @@ -0,0 +1,24 @@ | |||
| 1 | <% if flash.any? %> | ||
| 2 | <div id="flash"> | ||
| 3 | <%= flash[:notice] %> | ||
| 4 | <% if flash[:status_path] %> | ||
| 5 | <%= link_to 'Go to Status', flash[:status_path] %> | ||
| 6 | <% end %> | ||
| 7 | <% if flash[:stale_locale_path] %> | ||
| 8 | The <%= flash[:stale_locale] %> translation may be out of date — | ||
| 9 | <%= link_to 'review it', flash[:stale_locale_path] %> too. | ||
| 10 | <% end %> | ||
| 11 | <% if flash[:locked_node_path] %> | ||
| 12 | <span class="warning">The page is locked by <%= flash[:locked_by] %> — | ||
| 13 | <%= link_to 'unlock it there', flash[:locked_node_path] %> first, | ||
| 14 | then attach from this asset's page.</span> | ||
| 15 | <% end %> | ||
| 16 | <% if flash[:headline_kept_path] %> | ||
| 17 | <span class="warning">The page's existing headline was kept — | ||
| 18 | <%= link_to 'change it there', flash[:headline_kept_path] %> if needed.</span> | ||
| 19 | <% end %> | ||
| 20 | <% if flash[:error] %> | ||
| 21 | <span id="flash_error"><%= flash[:error] %></span> | ||
| 22 | <% end %> | ||
| 23 | </div> | ||
| 24 | <% end %> | ||
diff --git a/app/views/layouts/admin.html.erb b/app/views/layouts/admin.html.erb index e220beb..89c9b55 100644 --- a/app/views/layouts/admin.html.erb +++ b/app/views/layouts/admin.html.erb | |||
| @@ -37,25 +37,10 @@ | |||
| 37 | </div> | 37 | </div> |
| 38 | </div> | 38 | </div> |
| 39 | <div class="admin_content_spacer"></div> | 39 | <div class="admin_content_spacer"></div> |
| 40 | <% if flash[:notice].present? || flash[:error].present? %> | 40 | <%= render "layouts/flash" %> |
| 41 | <div id="flash"> | ||
| 42 | <%= flash[:notice] %> | ||
| 43 | <% if flash[:status_path] %> | ||
| 44 | <%= link_to 'Go to Status', flash[:status_path] %> | ||
| 45 | <% end %> | ||
| 46 | <% if flash[:stale_locale_path] %> | ||
| 47 | The <%= flash[:stale_locale] %> translation may be out of date — | ||
| 48 | <%= link_to 'review it', flash[:stale_locale_path] %> too. | ||
| 49 | <% end %> | ||
| 50 | <% if flash[:error] %> | ||
| 51 | <span id="flash_error"><%= flash[:error] %></span> | ||
| 52 | <% end %> | ||
| 53 | </div> | ||
| 54 | <% end %> | ||
| 55 | <div id="content"> | 41 | <div id="content"> |
| 56 | <%= yield %> | 42 | <%= yield %> |
| 57 | </div> | 43 | </div> |
| 58 | |||
| 59 | <div id="results"></div> | 44 | <div id="results"></div> |
| 60 | </div> | 45 | </div> |
| 61 | </body> | 46 | </body> |
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index ca867ab..f981fc1 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb | |||
| @@ -62,12 +62,10 @@ | |||
| 62 | <div id="center_column"> | 62 | <div id="center_column"> |
| 63 | <%= yield :layout %> | 63 | <%= yield :layout %> |
| 64 | <div id="footer"> | 64 | <div id="footer"> |
| 65 | <br /> | ||
| 66 | <p style="text-align: center"> | 65 | <p style="text-align: center"> |
| 67 | <%= link_to "Impressum", content_path("imprint") %> | 66 | <%= link_to "Impressum", content_path("imprint") %> |
| 68 | <%= link_to "Datenschutz", content_path("datenschutz") %> | 67 | <%= link_to "Datenschutz", content_path("datenschutz") %> |
| 69 | <%= language_selector %> | 68 | <%= language_selector %> |
| 70 | <!-- %= link_to t(:sponsors), content_path("sponsors") % --> | ||
| 71 | </p> | 69 | </p> |
| 72 | </div> | 70 | </div> |
| 73 | </div> | 71 | </div> |
diff --git a/app/views/layouts/events.html.erb b/app/views/layouts/events.html.erb deleted file mode 100644 index 93a6c0c..0000000 --- a/app/views/layouts/events.html.erb +++ /dev/null | |||
| @@ -1,17 +0,0 @@ | |||
| 1 | <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" | ||
| 2 | "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> | ||
| 3 | |||
| 4 | <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> | ||
| 5 | <head> | ||
| 6 | <meta http-equiv="content-type" content="text/html;charset=UTF-8" /> | ||
| 7 | <title>Events: <%= controller.action_name %></title> | ||
| 8 | <%= stylesheet_link_tag 'scaffold' %> | ||
| 9 | </head> | ||
| 10 | <body> | ||
| 11 | |||
| 12 | <p style="color: green"><%= flash[:notice] %></p> | ||
| 13 | |||
| 14 | <%= yield %> | ||
| 15 | |||
| 16 | </body> | ||
| 17 | </html> | ||
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/_recent_change_item.html.erb b/app/views/nodes/_recent_change_item.html.erb deleted file mode 100644 index 754a775..0000000 --- a/app/views/nodes/_recent_change_item.html.erb +++ /dev/null | |||
| @@ -1,9 +0,0 @@ | |||
| 1 | <li> | ||
| 2 | <div> | ||
| 3 | <%= link_to title_for_node(node), node_path(node) %> | ||
| 4 | <span class="field_hint"><%= link_to_path("#{node.unique_name} ↗", node.unique_name) %></span> | ||
| 5 | </div> | ||
| 6 | <span class="dashboard_widget_meta"> | ||
| 7 | <%= (editor = node_head_editor(node)) ? t("last_edited_by", editor: editor) : t("last_edited") %>, <%= relative_time_phrase(node.head.updated_at) %> | ||
| 8 | </span> | ||
| 9 | </li> | ||
diff --git a/app/views/nodes/edit.html.erb b/app/views/nodes/edit.html.erb index 25f15c2..fc1d18f 100644 --- a/app/views/nodes/edit.html.erb +++ b/app/views/nodes/edit.html.erb | |||
| @@ -33,48 +33,17 @@ | |||
| 33 | </div> | 33 | </div> |
| 34 | <% end %> | 34 | <% end %> |
| 35 | 35 | ||
| 36 | <details id="metadata_details"> | 36 | <%= fields_for @page do |d| %> |
| 37 | <summary>Metadata (slug, parent, tags, template, author, images)</summary> | 37 | <div id="edit_grid"> |
| 38 | <div id="metadata"> | ||
| 39 | <div class="node_description">Slug</div> | ||
| 40 | <div class="node_content"> | ||
| 41 | <%= f.text_field( | ||
| 42 | :staged_slug, :value => @node.staged_slug || @node.slug | ||
| 43 | ) | ||
| 44 | %> | ||
| 45 | </div> | ||
| 46 | |||
| 47 | <div class="node_description">parent</div> | ||
| 48 | <div class="node_content"> | ||
| 49 | <%= text_field_tag :move_to_search_term, @node.parent.title rescue "" %> | ||
| 50 | <div id="move_to_search_results" class="search_results"></div> | ||
| 51 | <%= f.hidden_field( | ||
| 52 | :staged_parent_id, | ||
| 53 | :value => @node.staged_parent_id || @node.parent_id | ||
| 54 | ) | ||
| 55 | %> | ||
| 56 | </div> | ||
| 57 | |||
| 58 | <%= fields_for @page do |d| %> | ||
| 59 | <div class="node_description">Tags - comma seperated</div> | ||
| 60 | <div class="node_content"><%= text_field_tag :tag_list, @page.tag_list.join(', ') %></div> | ||
| 61 | |||
| 62 | <div class="node_description">Publish at</div> | ||
| 63 | <div class="node_content"><%= d.datetime_select :published_at, :value => @page.published_at %></div> | ||
| 64 | 38 | ||
| 65 | <div class="node_description">Template</div> | 39 | <div id="main_fields"> |
| 66 | <div class="node_content"> | 40 | <div class="node_description">Title</div> |
| 67 | <%= d.select :template_name, custom_page_templates, {:prompt => 'Select Template'} %> | 41 | <div class="node_content"><%= d.text_field :title %></div> |
| 68 | <span class="field_hint">Set automatically based on how this node was created - change it if needed.</span> | ||
| 69 | </div> | ||
| 70 | 42 | ||
| 71 | <div class="node_description">Author</div> | 43 | <div class="node_description">Abstract</div> |
| 72 | <div class="node_content"> | 44 | <div class="node_content"><%= d.text_area :abstract %></div> |
| 73 | <%= d.select :user_id, user_list, | ||
| 74 | :selected => @page.user_id || @node.draft&.user_id || @node.head&.user_id %> | ||
| 75 | </div> | ||
| 76 | 45 | ||
| 77 | <div class="node_description">Images</div> | 46 | <div class="node_description">Attachments</div> |
| 78 | <div class="node_content"> | 47 | <div class="node_content"> |
| 79 | <div id="related_assets" | 48 | <div id="related_assets" |
| 80 | data-search-url="<%= search_node_related_assets_path(@node) %>" | 49 | data-search-url="<%= search_node_related_assets_path(@node) %>" |
| @@ -82,17 +51,27 @@ | |||
| 82 | <ul id="related_asset_list" class="thumbnail_list"> | 51 | <ul id="related_asset_list" class="thumbnail_list"> |
| 83 | <% @page.related_assets.includes(:asset).each do |related| %> | 52 | <% @page.related_assets.includes(:asset).each do |related| %> |
| 84 | <li data-url="<%= node_related_asset_path(@node, related) %>" | 53 | <li data-url="<%= node_related_asset_path(@node, related) %>" |
| 54 | data-asset-id="<%= related.asset.id %>" | ||
| 85 | data-large-url="<%= related.asset.upload.url(:large) %>" | 55 | data-large-url="<%= related.asset.upload.url(:large) %>" |
| 86 | data-original-url="<%= related.asset.upload.url %>" | 56 | data-original-url="<%= related.asset.upload.url %>" |
| 87 | data-name="<%= related.asset.name %>"> | 57 | data-name="<%= related.asset.name %>" |
| 58 | data-has-credit="<%= related.asset.show_credit? %>" | ||
| 59 | data-headline="<%= related.headline? %>" | ||
| 60 | class="<%= "is_headline" if related.headline? %>"> | ||
| 88 | <span class="related_asset_handle"><%= icon("grip-vertical", library: "tabler", "aria-hidden": true) %></span> | 61 | <span class="related_asset_handle"><%= icon("grip-vertical", library: "tabler", "aria-hidden": true) %></span> |
| 89 | <%= image_tag related.asset.upload.url(:thumb) %> | 62 | <%= image_tag related.asset.upload.url(:thumb) %> |
| 63 | <button type="button" class="related_asset_set_headline" | ||
| 64 | aria-pressed="<%= related.headline? %>" aria-label="Set as headline image" | ||
| 65 | title="Use this photo as the page's headline image"> | ||
| 66 | <%= icon("star", library: "tabler", "aria-hidden": true) %> | ||
| 67 | </button> | ||
| 90 | <button type="button" class="related_asset_remove" aria-label="Remove image"> | 68 | <button type="button" class="related_asset_remove" aria-label="Remove image"> |
| 91 | <%= icon("x", library: "tabler", "aria-hidden": true) %> | 69 | <%= icon("x", library: "tabler", "aria-hidden": true) %> |
| 92 | </button> | 70 | </button> |
| 93 | </li> | 71 | </li> |
| 94 | <% end %> | 72 | <% end %> |
| 95 | </ul> | 73 | </ul> |
| 74 | <p class="field_hint">The starred image or PDF becomes this page's headline on the public site. If none is starred, visitors get a link to browse all attached images instead.</p> | ||
| 96 | <%= text_field_tag nil, nil, id: "related_asset_search_term", placeholder: "Search images to attach…", autocomplete: "off" %> | 75 | <%= text_field_tag nil, nil, id: "related_asset_search_term", placeholder: "Search images to attach…", autocomplete: "off" %> |
| 97 | <div id="related_asset_search_results" class="search_results"></div> | 76 | <div id="related_asset_search_results" class="search_results"></div> |
| 98 | </div> | 77 | </div> |
| @@ -102,38 +81,81 @@ | |||
| 102 | <li> | 81 | <li> |
| 103 | <span class="related_asset_handle"><%= icon("grip-vertical", library: "tabler", "aria-hidden": true) %></span> | 82 | <span class="related_asset_handle"><%= icon("grip-vertical", library: "tabler", "aria-hidden": true) %></span> |
| 104 | <img src=""> | 83 | <img src=""> |
| 84 | <button type="button" class="related_asset_set_headline" aria-pressed="false" aria-label="Set as headline image"> | ||
| 85 | <%= icon("star", library: "tabler", "aria-hidden": true) %> | ||
| 86 | </button> | ||
| 105 | <button type="button" class="related_asset_remove" aria-label="Remove image"> | 87 | <button type="button" class="related_asset_remove" aria-label="Remove image"> |
| 106 | <%= icon("x", library: "tabler", "aria-hidden": true) %> | 88 | <%= icon("x", library: "tabler", "aria-hidden": true) %> |
| 107 | </button> | 89 | </button> |
| 108 | </li> | 90 | </li> |
| 109 | </template> | 91 | </template> |
| 110 | |||
| 111 | </div> | 92 | </div> |
| 112 | </details> | ||
| 113 | 93 | ||
| 114 | <div id="content"> | 94 | <details id="metadata_details"> |
| 115 | <div class="node_description">Title</div> | 95 | <summary>Metadata (slug, parent, tags, template, author, images)</summary> |
| 116 | <div class="node_content"><%= d.text_field :title %></div> | 96 | <div id="metadata"> |
| 97 | <div class="node_description">Slug</div> | ||
| 98 | <div class="node_content"> | ||
| 99 | <%= f.text_field( | ||
| 100 | :staged_slug, :value => @node.staged_slug || @node.slug | ||
| 101 | ) | ||
| 102 | %> | ||
| 103 | </div> | ||
| 117 | 104 | ||
| 118 | <div class="node_description">Abstract</div> | 105 | <div class="node_description">parent</div> |
| 119 | <div class="node_content"><%= d.text_area :abstract %></div> | 106 | <div class="node_content"> |
| 107 | <%= text_field_tag :move_to_search_term, @node.parent.title rescue "" %> | ||
| 108 | <p class="field_hint">Start typing to find a new parent for this node.</p> | ||
| 109 | <div id="move_to_search_results" class="search_results"></div> | ||
| 110 | <%= f.hidden_field( | ||
| 111 | :staged_parent_id, | ||
| 112 | :value => @node.staged_parent_id || @node.parent_id | ||
| 113 | ) | ||
| 114 | %> | ||
| 115 | </div> | ||
| 120 | 116 | ||
| 121 | <div class="node_description">Body</div> | 117 | <div class="node_description">Tags</div> |
| 122 | <div class="body_toolbar_row"> | 118 | <div class="node_content"> |
| 123 | <button type="button" id="preview_toggle" class="view_toggle" aria-pressed="false" aria-label="Toggle live preview" title="Toggle live preview"> | 119 | <%= text_field_tag :tag_list, @page.tag_list.join(', ') %> |
| 124 | <%= icon("layout-sidebar-right", library: "tabler", "aria-hidden": true) %> <span>Live preview</span> | 120 | <span class="field_hint">Comma separated.</span> |
| 125 | </button> | 121 | </div> |
| 126 | <button type="button" id="preview_force_render" class="view_toggle" style="display:none" aria-label="Force preview render" title="Force preview render"> | 122 | |
| 127 | <%= icon("refresh", library: "tabler", "aria-hidden": true) %> <span>Force refresh</span> | 123 | <div class="node_description">Publish at</div> |
| 128 | </button> | 124 | <div class="node_content"><%= d.datetime_select :published_at, :value => @page.published_at %></div> |
| 129 | </div> | 125 | |
| 126 | <div class="node_description">Template</div> | ||
| 127 | <div class="node_content"> | ||
| 128 | <%= d.select :template_name, custom_page_templates, {:prompt => 'Select Template'} %> | ||
| 129 | <span class="field_hint">Set automatically based on how this node was created - change it if needed.</span> | ||
| 130 | </div> | ||
| 130 | 131 | ||
| 131 | <div id="editor_and_preview"> | 132 | <div class="node_description">Author</div> |
| 132 | <div class="preview_content"> | 133 | <div class="node_content"> |
| 133 | <div class="node_content"><%= d.text_area :body, :class => 'with_editor' %></div> | 134 | <%= d.select :user_id, user_list, |
| 135 | :selected => @page.user_id || @node.draft&.user_id || @node.head&.user_id %> | ||
| 136 | </div> | ||
| 137 | |||
| 138 | </div> | ||
| 139 | </details> | ||
| 140 | |||
| 141 | <div id="content"> | ||
| 142 | <div class="node_description">Body</div> | ||
| 143 | <div class="body_toolbar_row"> | ||
| 144 | <button type="button" id="preview_toggle" class="view_toggle" aria-pressed="false" aria-label="Toggle live preview" title="Toggle live preview"> | ||
| 145 | <%= icon("layout-sidebar-right", library: "tabler", "aria-hidden": true) %> <span>Live preview</span> | ||
| 146 | </button> | ||
| 147 | <button type="button" id="preview_force_render" class="view_toggle" style="display:none" aria-label="Force preview render" title="Force preview render"> | ||
| 148 | <%= icon("refresh", library: "tabler", "aria-hidden": true) %> <span>Force refresh</span> | ||
| 149 | </button> | ||
| 134 | </div> | 150 | </div> |
| 135 | <div id="preview_panel" style="display:none"> | 151 | |
| 136 | <iframe id="live_preview_iframe" title="Live preview" data-src="<%= preview_page_path(@page, :locale => I18n.default_locale) %>"></iframe> | 152 | <div id="editor_and_preview"> |
| 153 | <div class="preview_content"> | ||
| 154 | <div class="node_content"><%= d.text_area :body, :class => 'with_editor' %></div> | ||
| 155 | </div> | ||
| 156 | <div id="preview_panel" style="display:none"> | ||
| 157 | <iframe id="live_preview_iframe" title="Live preview" data-src="<%= preview_page_path(@page, :locale => I18n.default_locale) %>"></iframe> | ||
| 158 | </div> | ||
| 137 | </div> | 159 | </div> |
| 138 | </div> | 160 | </div> |
| 139 | </div> | 161 | </div> |
diff --git a/app/views/nodes/new.html.erb b/app/views/nodes/new.html.erb index 890d46e..1303e9e 100644 --- a/app/views/nodes/new.html.erb +++ b/app/views/nodes/new.html.erb | |||
| @@ -41,10 +41,31 @@ | |||
| 41 | <div class="node_description">Resulting path</div> | 41 | <div class="node_description">Resulting path</div> |
| 42 | <div class="node_content"> | 42 | <div class="node_content"> |
| 43 | <span id="resulting_path">—</span> | 43 | <span id="resulting_path">—</span> |
| 44 | <button type="button" id="copy_resulting_path" class="unselected">copy</button> | 44 | <button type="button" id="copy_resulting_path" class="unselected copy_button" data-copy-target="#resulting_path"> |
| 45 | <span class="copy_button_label">copy</span> | ||
| 46 | </button> | ||
| 47 | |||
| 45 | <span class="field_hint">This preview updates as you type. The final path can still be changed afterward by editing the title (for the last segment) or moving the node to a different parent (for everything before it).</span> | 48 | <span class="field_hint">This preview updates as you type. The final path can still be changed afterward by editing the title (for the last segment) or moving the node to a different parent (for everything before it).</span> |
| 46 | </div> | 49 | </div> |
| 47 | 50 | ||
| 51 | <% if @attach_asset %> | ||
| 52 | <div class="node_description">attachment</div> | ||
| 53 | <div class="node_content"> | ||
| 54 | <ul class="thumbnail_list"> | ||
| 55 | <li> | ||
| 56 | <% if @attach_asset.has_variant?(:thumb) %> | ||
| 57 | <%= image_tag @attach_asset.upload.url(:thumb) %> | ||
| 58 | <% end %> | ||
| 59 | <%= link_to @attach_asset.name, asset_path(@attach_asset), | ||
| 60 | :target => "_blank", :rel => "noopener" %> | ||
| 61 | </li> | ||
| 62 | </ul> | ||
| 63 | <span class="field_hint">This asset will automatically be attached to the new page.</span> | ||
| 64 | <label><%= check_box_tag :asset_headline, "1" %> as the page's headline</label> | ||
| 65 | <%= hidden_field_tag :asset_id, @attach_asset.id %> | ||
| 66 | </div> | ||
| 67 | <% end %> | ||
| 68 | |||
| 48 | <div class="node_description"></div> | 69 | <div class="node_description"></div> |
| 49 | <div class="node_content"><%= submit_tag "Create" %></div> | 70 | <div class="node_content"><%= submit_tag "Create" %></div> |
| 50 | 71 | ||
diff --git a/app/views/nodes/recent.html.erb b/app/views/nodes/recent.html.erb deleted file mode 100644 index d256253..0000000 --- a/app/views/nodes/recent.html.erb +++ /dev/null | |||
| @@ -1,7 +0,0 @@ | |||
| 1 | <h1>Recently changed</h1> | ||
| 2 | |||
| 3 | <%= will_paginate @nodes %> | ||
| 4 | <ul id="recent_changes_full_list"> | ||
| 5 | <%= render partial: "nodes/recent_change_item", collection: @nodes, as: :node %> | ||
| 6 | </ul> | ||
| 7 | <%= will_paginate @nodes %> | ||
diff --git a/app/views/nodes/show.html.erb b/app/views/nodes/show.html.erb index af05778..b3987c6 100644 --- a/app/views/nodes/show.html.erb +++ b/app/views/nodes/show.html.erb | |||
| @@ -253,16 +253,30 @@ | |||
| 253 | <% end %> | 253 | <% end %> |
| 254 | </div> | 254 | </div> |
| 255 | 255 | ||
| 256 | <% if @page.assets.images.any? %> | 256 | <div class="node_description">Attachments</div> |
| 257 | <div class="node_description">Images</div> | 257 | <div class="node_content node_info_group"> |
| 258 | <div class="node_content node_info_group"> | 258 | <% if @page.assets.any? %> |
| 259 | <% headline_asset_id = @page.headline_asset&.id %> | ||
| 259 | <ul class="thumbnail_list"> | 260 | <ul class="thumbnail_list"> |
| 260 | <% @page.assets.images.each do |asset| %> | 261 | <% @page.assets.each do |asset| %> |
| 261 | <li><%= link_to image_tag(asset.upload.url(:thumb)), asset_path(asset) %></li> | 262 | <li class="<%= "is_headline" if asset.id == headline_asset_id %>"> |
| 263 | <%= link_to image_tag(asset.upload.url(:thumb)), asset_path(asset) %> | ||
| 264 | <% if asset.id == headline_asset_id %> | ||
| 265 | <span class="headline_indicator" title="This page's headline image"> | ||
| 266 | <%= icon("star", library: "tabler", "aria-hidden": true) %> | ||
| 267 | </span> | ||
| 268 | <% end %> | ||
| 269 | </li> | ||
| 262 | <% end %> | 270 | <% end %> |
| 263 | </ul> | 271 | </ul> |
| 264 | </div> | 272 | <% end %> |
| 265 | <% end %> | 273 | <p class="add_attachments"> |
| 274 | <%= link_to new_asset_path(:node_id => @node.id), class: 'action_button' do %> | ||
| 275 | <%= icon("paperclip", library: "tabler", "aria-hidden": true) %> Upload new attachment | ||
| 276 | <% end %> | ||
| 277 | <span class="field_hint">To add or remove existing attachments, edit this node instead.</span> | ||
| 278 | </p> | ||
| 279 | </div> | ||
| 266 | 280 | ||
| 267 | <div class="node_description">Events</div> | 281 | <div class="node_description">Events</div> |
| 268 | <div class="node_content node_info_group"> | 282 | <div class="node_content node_info_group"> |
| @@ -276,7 +290,11 @@ | |||
| 276 | <% end %> | 290 | <% end %> |
| 277 | </ul> | 291 | </ul> |
| 278 | <% mapping = default_event_tag_mapping(@page) %> | 292 | <% mapping = default_event_tag_mapping(@page) %> |
| 279 | <%= link_to 'add event', new_event_path(node_id: @node.id, tag_list: mapping&.last, auto_tag_source: mapping&.first, return_to: request.path) %> | 293 | <p class="add_events"> |
| 294 | <%= link_to new_event_path(node_id: @node.id, tag_list: mapping&.last, auto_tag_source: mapping&.first, return_to: request.path), class: 'action_button' do %> | ||
| 295 | <%= icon("calendar-plus", library: "tabler", "aria-hidden": true) %> Add event | ||
| 296 | <% end %> | ||
| 297 | </p> | ||
| 280 | </div> | 298 | </div> |
| 281 | 299 | ||
| 282 | <% matches = matching_node_kinds(@node) %> | 300 | <% matches = matching_node_kinds(@node) %> |
| @@ -296,10 +314,11 @@ | |||
| 296 | <% if matches.any? %> | 314 | <% if matches.any? %> |
| 297 | <p class="add_child_links"> | 315 | <p class="add_child_links"> |
| 298 | <% matches.each_with_index do |(kind, config), index| %> | 316 | <% matches.each_with_index do |(kind, config), index| %> |
| 299 | <%= " · ".html_safe if index > 0 %> | ||
| 300 | <% link_params = { kind: kind } %> | 317 | <% link_params = { kind: kind } %> |
| 301 | <% link_params[:parent_id] = @node.id if kind == "generic" %> | 318 | <% link_params[:parent_id] = @node.id if kind == "generic" %> |
| 302 | <%= link_to "add '#{resolve_kind_text(config[:label])}' child", new_node_path(link_params) %> | 319 | <%= link_to new_node_path(link_params), class: 'action_button' do %> |
| 320 | <%= icon("text-plus", library: "tabler", "aria-hidden": true) %> Add child type <%= resolve_kind_text(config[:label]) %> | ||
| 321 | <% end %> | ||
| 303 | <% end %> | 322 | <% end %> |
| 304 | </p> | 323 | </p> |
| 305 | <% end %> | 324 | <% end %> |
