diff options
Diffstat (limited to 'app/controllers')
| -rw-r--r-- | app/controllers/admin_controller.rb | 82 | ||||
| -rw-r--r-- | app/controllers/application_controller.rb | 12 | ||||
| -rw-r--r-- | app/controllers/events_controller.rb | 23 | ||||
| -rw-r--r-- | app/controllers/menu_items_controller.rb | 2 | ||||
| -rw-r--r-- | app/controllers/nodes_controller.rb | 248 | ||||
| -rw-r--r-- | app/controllers/page_translations_controller.rb | 109 | ||||
| -rw-r--r-- | app/controllers/related_assets_controller.rb | 49 | ||||
| -rw-r--r-- | app/controllers/revisions_controller.rb | 25 | ||||
| -rw-r--r-- | app/controllers/shared_previews_controller.rb | 22 | ||||
| -rw-r--r-- | app/controllers/users_controller.rb | 2 |
10 files changed, 471 insertions, 103 deletions
diff --git a/app/controllers/admin_controller.rb b/app/controllers/admin_controller.rb index d671384..37fd78b 100644 --- a/app/controllers/admin_controller.rb +++ b/app/controllers/admin_controller.rb | |||
| @@ -1,74 +1,74 @@ | |||
| 1 | class AdminController < ApplicationController | 1 | class AdminController < ApplicationController |
| 2 | 2 | ||
| 3 | # Private | 3 | # Private |
| 4 | 4 | ||
| 5 | before_action :login_required | 5 | before_action :login_required |
| 6 | 6 | ||
| 7 | def index | 7 | def index |
| 8 | @drafts = Node.where("draft_id IS NOT NULL") | 8 | @drafts = Node.drafts_and_autosaves(current_user_id: current_user.id).limit(5) |
| 9 | .limit(50).order("updated_at desc") | 9 | @recent_changes = Node.recently_changed.limit(5) |
| 10 | 10 | end | |
| 11 | @drafts_count = Node.where("draft_id IS NOT NULL").count | ||
| 12 | |||
| 13 | @recent_changes = Node.where( | ||
| 14 | "updated_at < ? AND updated_at > ? AND parent_id IS NOT NULL", | ||
| 15 | Time.now, Time.now - 14.days | ||
| 16 | ).limit(50).order("updated_at desc") | ||
| 17 | |||
| 18 | all_nodes = Node.root.self_and_descendants | ||
| 19 | @sitemap_depth = {} | ||
| 20 | Node.each_with_level(all_nodes) do |node, level| | ||
| 21 | @sitemap_depth[node.id] = level | ||
| 22 | end | ||
| 23 | @sitemap = all_nodes.to_a.sort! { |node1,node2| node1.lft <=> node2.lft }.delete_if { |node| node.update? } | ||
| 24 | |||
| 25 | @mypages = Page.where("user_id = ? or editor_id = ?", @current_user, @current_user) | ||
| 26 | 11 | ||
| 27 | @mynodes = Node.joins(:pages) | 12 | def conventions |
| 28 | .where("pages.user_id = ? or pages.editor_id = ?", @current_user, @current_user) | 13 | @node_kinds = CccConventions::NODE_KINDS |
| 29 | .order("updated_at desc") | ||
| 30 | .uniq.first(50) | ||
| 31 | end | 14 | end |
| 32 | 15 | ||
| 33 | def search | 16 | def search |
| 34 | @results = Node.search params[:search_term], :per_page => 1000 | 17 | @results = Node.editor_search(params[:search_term]) |
| 35 | 18 | ||
| 36 | respond_to do |format| | 19 | respond_to do |format| |
| 37 | format.html do | 20 | format.html do |
| 38 | render :template => 'admin/search_results' | 21 | render :template => 'admin/search_results' |
| 39 | end | 22 | end |
| 40 | format.js do | 23 | format.js do |
| 41 | render( :json => @results.map do |node| | 24 | render( :json => @results.map do |node| |
| 42 | if node | 25 | if node |
| 43 | { :id => node.id, :title => node.title, :unique_name => node.unique_name, :node_path => node_path(node) } | 26 | { :id => node.id, :title => node.title, :unique_name => node.unique_name, :node_path => node_path(node) } |
| 44 | end | 27 | end |
| 45 | end | 28 | end |
| 46 | ) | 29 | ) |
| 47 | 30 | ||
| 48 | end | 31 | end |
| 32 | end | ||
| 33 | end | ||
| 34 | |||
| 35 | def dashboard_search | ||
| 36 | term = params[:search_term] | ||
| 37 | |||
| 38 | if term.blank? | ||
| 39 | render json: { tags: [], nodes: [] } | ||
| 40 | return | ||
| 49 | end | 41 | end |
| 42 | |||
| 43 | render json: { | ||
| 44 | tags: ActsAsTaggableOn::Tag.named_like(term).limit(5).map { |tag| | ||
| 45 | { name: tag.name, tag_path: tags_nodes_path(tags: tag.name) } | ||
| 46 | }, | ||
| 47 | nodes: Node.editor_search(term).limit(10).map { |node| | ||
| 48 | { node_id: node.id, title: node.title, unique_name: node.unique_name, node_path: node_path(node) } | ||
| 49 | } | ||
| 50 | } | ||
| 50 | end | 51 | end |
| 51 | 52 | ||
| 52 | def menu_search | 53 | def menu_search |
| 53 | if params[:search_term] == "Root" | 54 | if params[:search_term] == "Root" |
| 54 | @results = [Node.root] | 55 | @results = [Node.root] |
| 55 | else | 56 | else |
| 56 | @results = Node.search params[:search_term] | 57 | @results = Node.editor_search(params[:search_term]) |
| 57 | end | 58 | end |
| 58 | 59 | ||
| 59 | respond_to do |format| | 60 | respond_to do |format| |
| 60 | format.html do | 61 | format.html do |
| 61 | render :partial => 'admin/menu_search_results' | 62 | render :partial => 'admin/menu_search_results' |
| 62 | end | 63 | end |
| 63 | 64 | ||
| 64 | format.js do | 65 | format.js do |
| 65 | render( :json => @results.map do |node| | 66 | render( :json => @results.map do |node| |
| 66 | {:node_id => node.id, :title => node.title, :unique_name => node.unique_name} | 67 | {:node_id => node.id, :title => node.title, :unique_name => node.unique_name, :node_path => node_path(node)} |
| 67 | end | 68 | end |
| 68 | ) | 69 | ) |
| 69 | 70 | ||
| 70 | end | 71 | end |
| 71 | end | 72 | end |
| 72 | end | 73 | end |
| 73 | |||
| 74 | end | 74 | end |
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 75f92c3..cbeb40d 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb | |||
| @@ -5,6 +5,8 @@ class ApplicationController < ActionController::Base | |||
| 5 | 5 | ||
| 6 | before_action :set_locale | 6 | before_action :set_locale |
| 7 | 7 | ||
| 8 | helper_method :safe_return_to | ||
| 9 | |||
| 8 | protected | 10 | protected |
| 9 | 11 | ||
| 10 | def set_locale | 12 | def set_locale |
| @@ -18,4 +20,14 @@ class ApplicationController < ActionController::Base | |||
| 18 | def default_url_options | 20 | def default_url_options |
| 19 | { locale: I18n.locale == I18n.default_locale ? nil : I18n.locale } | 21 | { locale: I18n.locale == I18n.default_locale ? nil : I18n.locale } |
| 20 | end | 22 | end |
| 23 | |||
| 24 | def safe_return_to(url) | ||
| 25 | return events_path if url.blank? | ||
| 26 | uri = URI.parse(url) | ||
| 27 | return events_path if uri.host.present? | ||
| 28 | return events_path unless url.start_with?('/') | ||
| 29 | url | ||
| 30 | rescue URI::InvalidURIError | ||
| 31 | events_path | ||
| 32 | end | ||
| 21 | end | 33 | end |
diff --git a/app/controllers/events_controller.rb b/app/controllers/events_controller.rb index f50da3e..be3f547 100644 --- a/app/controllers/events_controller.rb +++ b/app/controllers/events_controller.rb | |||
| @@ -9,18 +9,24 @@ class EventsController < ApplicationController | |||
| 9 | # GET /events | 9 | # GET /events |
| 10 | # GET /events.xml | 10 | # GET /events.xml |
| 11 | def index | 11 | def index |
| 12 | @events = Event.all | 12 | @events = Event.order(:id) |
| 13 | 13 | ||
| 14 | respond_to do |format| | 14 | respond_to do |format| |
| 15 | format.html # index.html.erb | 15 | format.html { @events = @events.paginate(page: params[:page], per_page: 25) } |
| 16 | format.xml { render :xml => @events } | 16 | format.xml { render :xml => @events } |
| 17 | end | 17 | end |
| 18 | end | 18 | end |
| 19 | 19 | ||
| 20 | # GET /events/without_node | ||
| 21 | def without_node | ||
| 22 | @events = Event.where(node_id: nil).order(:start_time).paginate(page: params[:page], per_page: 25) | ||
| 23 | end | ||
| 24 | |||
| 20 | # GET /events/1 | 25 | # GET /events/1 |
| 21 | # GET /events/1.xml | 26 | # GET /events/1.xml |
| 22 | def show | 27 | def show |
| 23 | @event = Event.find(params[:id]) | 28 | @event = Event.find(params[:id]) |
| 29 | @return_to = params[:return_to] || events_path | ||
| 24 | 30 | ||
| 25 | respond_to do |format| | 31 | respond_to do |format| |
| 26 | format.html # show.html.erb | 32 | format.html # show.html.erb |
| @@ -31,7 +37,14 @@ class EventsController < ApplicationController | |||
| 31 | # GET /events/new | 37 | # GET /events/new |
| 32 | # GET /events/new.xml | 38 | # GET /events/new.xml |
| 33 | def new | 39 | def new |
| 34 | @event = Event.new(:node_id => params[:node_id]) | 40 | @event = Event.new( |
| 41 | node_id: params[:node_id], | ||
| 42 | tag_list: params[:tag_list] | ||
| 43 | ) | ||
| 44 | |||
| 45 | if params[:tag_list].present? && params[:auto_tag_source].present? | ||
| 46 | flash.now[:notice] = "Tag '#{params[:tag_list]}' was pre-filled because this page is tagged '#{params[:auto_tag_source]}'. You can remove it below." | ||
| 47 | end | ||
| 35 | 48 | ||
| 36 | respond_to do |format| | 49 | respond_to do |format| |
| 37 | format.html # new.html.erb | 50 | format.html # new.html.erb |
| @@ -53,7 +66,7 @@ class EventsController < ApplicationController | |||
| 53 | respond_to do |format| | 66 | respond_to do |format| |
| 54 | if @event.save | 67 | if @event.save |
| 55 | flash[:notice] = 'Event was successfully created.' | 68 | flash[:notice] = 'Event was successfully created.' |
| 56 | format.html { redirect_to(@event.node ? edit_node_path(@event.node) : edit_event_path(@event)) } | 69 | format.html { redirect_to(safe_return_to(params[:return_to] || (@event.node ? edit_node_path(@event.node) : edit_event_path(@event)))) } |
| 57 | format.xml { render :xml => @event, :status => :created, :location => @event } | 70 | format.xml { render :xml => @event, :status => :created, :location => @event } |
| 58 | else | 71 | else |
| 59 | format.html { render :action => "new" } | 72 | format.html { render :action => "new" } |
| @@ -94,6 +107,6 @@ class EventsController < ApplicationController | |||
| 94 | private | 107 | private |
| 95 | 108 | ||
| 96 | def event_params | 109 | def event_params |
| 97 | params.require(:event).permit(:start_time, :end_time, :rrule, :custom_rrule, :allday, :url, :latitude, :longitude, :node_id, :location) | 110 | params.require(:event).permit(:title, :description, :start_time, :end_time, :rrule, :allday, :url, :latitude, :longitude, :node_id, :location, :tag_list) |
| 98 | end | 111 | end |
| 99 | end | 112 | end |
diff --git a/app/controllers/menu_items_controller.rb b/app/controllers/menu_items_controller.rb index 1b1eb59..314d1ea 100644 --- a/app/controllers/menu_items_controller.rb +++ b/app/controllers/menu_items_controller.rb | |||
| @@ -14,7 +14,7 @@ class MenuItemsController < ApplicationController | |||
| 14 | end | 14 | end |
| 15 | 15 | ||
| 16 | def new | 16 | def new |
| 17 | @menu_item = MenuItem.new menu_item_params | 17 | @menu_item = MenuItem.new |
| 18 | end | 18 | end |
| 19 | 19 | ||
| 20 | def create | 20 | def create |
diff --git a/app/controllers/nodes_controller.rb b/app/controllers/nodes_controller.rb index 494887d..bff1733 100644 --- a/app/controllers/nodes_controller.rb +++ b/app/controllers/nodes_controller.rb | |||
| @@ -1,19 +1,23 @@ | |||
| 1 | class NodesController < ApplicationController | 1 | class NodesController < ApplicationController |
| 2 | 2 | ||
| 3 | # Private | 3 | # Private |
| 4 | 4 | ||
| 5 | layout 'admin' | 5 | layout 'admin' |
| 6 | 6 | ||
| 7 | before_action :login_required | 7 | before_action :login_required |
| 8 | before_action :find_node, :only => [ | 8 | before_action :find_node, :only => [ |
| 9 | :show, | 9 | :show, |
| 10 | :edit, | 10 | :edit, |
| 11 | :update, | 11 | :update, |
| 12 | :destroy, | 12 | :destroy, |
| 13 | :publish, | 13 | :publish, |
| 14 | :unlock | 14 | :unlock, |
| 15 | :autosave, | ||
| 16 | :revert | ||
| 15 | ] | 17 | ] |
| 16 | 18 | ||
| 19 | around_action :pin_to_default_locale, :only => [:show, :edit, :update, :autosave] | ||
| 20 | |||
| 17 | def index | 21 | def index |
| 18 | @nodes = Node.root.descendants.includes(:head, :draft) | 22 | @nodes = Node.root.descendants.includes(:head, :draft) |
| 19 | .order('id DESC') | 23 | .order('id DESC') |
| @@ -22,90 +26,203 @@ class NodesController < ApplicationController | |||
| 22 | 26 | ||
| 23 | def new | 27 | def new |
| 24 | @node = Node.new node_params | 28 | @node = Node.new node_params |
| 29 | @selected_kind = CccConventions::NODE_KINDS.key?(params[:kind]) ? params[:kind] : "generic" | ||
| 25 | if params.has_key?(:parent_id) | 30 | if params.has_key?(:parent_id) |
| 26 | @parent_id = params[:parent_id] | 31 | @parent_id = params[:parent_id] |
| 27 | @parent_name = Node.find(@parent_id).title | 32 | @parent_name = Node.find(@parent_id).title |
| 28 | end | 33 | end |
| 29 | end | 34 | end |
| 30 | 35 | ||
| 31 | def create | 36 | def create |
| 32 | params[:title] ||= "" | 37 | params[:title] ||= "" |
| 33 | 38 | ||
| 34 | @node = Node.new | 39 | @node = Node.new |
| 35 | @node.parent_id = find_parent | 40 | @node.parent_id = find_parent |
| 36 | @node.slug = params[:title].parameterize.to_s | 41 | @node.slug = slug_for(params[:title]) |
| 37 | 42 | ||
| 43 | config = CccConventions::NODE_KINDS[params[:kind]] | ||
| 44 | |||
| 38 | if @node.save | 45 | if @node.save |
| 39 | @node.draft.update(:title => params[:title]) | 46 | @node.draft.update(:title => params[:title]) |
| 40 | case params[:kind] | 47 | Array(config && config[:tags]).each { |t| @node.draft.tag_list.add(t) } |
| 41 | when "update" | ||
| 42 | @node.draft.tag_list.add("update") | ||
| 43 | when "press_release" | ||
| 44 | @node.draft.tag_list.add("update", "pressemitteilung") | ||
| 45 | end | ||
| 46 | @node.draft.save! | 48 | @node.draft.save! |
| 49 | |||
| 50 | @node.update!(default_template_name: config[:template]) if config && config[:template] | ||
| 51 | |||
| 47 | redirect_to(edit_node_path(@node)) | 52 | redirect_to(edit_node_path(@node)) |
| 48 | else | 53 | else |
| 54 | @selected_kind = CccConventions::NODE_KINDS.key?(params[:kind]) ? params[:kind] : "generic" | ||
| 55 | if params[:parent_id].present? | ||
| 56 | @parent_id = params[:parent_id] | ||
| 57 | @parent_name = Node.find(@parent_id).title | ||
| 58 | end | ||
| 49 | render :new | 59 | render :new |
| 50 | end | 60 | end |
| 51 | end | 61 | end |
| 52 | 62 | ||
| 53 | def show | 63 | def show |
| 54 | node = Node.find(params[:id]) | 64 | @page = @node.draft || @node.head |
| 55 | node.wipe_draft! | 65 | @translations = @page.translation_summary |
| 56 | @page = node.draft || node.head | ||
| 57 | end | 66 | end |
| 58 | 67 | ||
| 59 | def edit | 68 | def edit |
| 60 | begin | 69 | freshly_locked = @node.lock_owner != current_user |
| 61 | @draft = @node.find_or_create_draft( current_user ) | 70 | @node.lock_for_editing!( current_user ) |
| 62 | rescue LockedByAnotherUser => e | 71 | @page = @node.autosave || @node.draft || @node.head |
| 63 | flash[:error] = e.message | 72 | |
| 64 | if request.referer | 73 | if @node.autosave |
| 65 | redirect_to request.referer || node_path(@node) | 74 | flash.now[:notice] = |
| 66 | else | 75 | "This page has unsaved changes from a previous session, shown below. " \ |
| 67 | redirect_to node_path(@node) | 76 | "Save to keep them, or use \"Discard Autosave\" below to go back to the last saved version." |
| 68 | end | 77 | elsif freshly_locked |
| 78 | flash.now[:notice] = "Node locked and ready to edit" | ||
| 69 | end | 79 | end |
| 80 | rescue LockedByAnotherUser => e | ||
| 81 | flash[:error] = e.message | ||
| 82 | redirect_to(request.referer || node_path(@node)) | ||
| 70 | end | 83 | end |
| 71 | 84 | ||
| 72 | def update | 85 | def update |
| 73 | @node.update(node_params) | 86 | @node.update(node_params) |
| 74 | @draft = @node.find_or_create_draft current_user | 87 | @node.autosave!( page_params.merge(:tag_list => params[:tag_list]), current_user ) |
| 75 | @draft.tag_list = params[:tag_list] | 88 | @node.save_draft!(current_user) |
| 76 | if @draft.update( page_params ) | 89 | |
| 77 | flash[:notice] = "Draft has been saved: #{Time.now}" | 90 | flash[:notice] = "Draft saved. Publish your changes in the Status section once you're done." |
| 78 | respond_to do |format| | 91 | flash[:status_path] = node_path(@node) |
| 79 | format.html { redirect_to edit_node_path(@node) } | 92 | |
| 80 | format.js | 93 | if @node.draft.translated_locales.size > 1 |
| 94 | stale_locale = @node.draft.translated_locales.find do |locale| | ||
| 95 | @node.draft.outdated_translations?(locale: locale) | ||
| 81 | end | 96 | end |
| 97 | if stale_locale | ||
| 98 | flash[:stale_locale] = stale_locale | ||
| 99 | flash[:stale_locale_path] = edit_node_path(@node, locale: stale_locale) | ||
| 100 | end | ||
| 101 | end | ||
| 102 | |||
| 103 | if params[:commit] == "Save + Unlock + Exit" | ||
| 104 | @node.unlock! | ||
| 105 | redirect_to node_path(@node) | ||
| 106 | else | ||
| 107 | redirect_to edit_node_path(@node) | ||
| 108 | end | ||
| 109 | rescue LockedByAnotherUser => e | ||
| 110 | flash[:error] = e.message | ||
| 111 | redirect_to node_path(@node) | ||
| 112 | rescue ActiveRecord::RecordInvalid | ||
| 113 | @page = @node.autosave || @node.draft || @node.head | ||
| 114 | render :action => :edit | ||
| 115 | end | ||
| 116 | |||
| 117 | def autosave | ||
| 118 | @node.update(node_params) | ||
| 119 | @node.autosave!( page_params.merge(:tag_list => params[:tag_list]), current_user ) | ||
| 120 | head :ok | ||
| 121 | rescue LockedByAnotherUser => e | ||
| 122 | render plain: e.message, status: :locked | ||
| 123 | rescue ActiveRecord::RecordInvalid => e | ||
| 124 | render plain: e.message, status: :unprocessable_entity | ||
| 125 | rescue StandardError => e | ||
| 126 | render plain: "Autosave failed", status: :internal_server_error | ||
| 127 | end | ||
| 128 | |||
| 129 | def revert | ||
| 130 | @node.lock_for_editing!(current_user) | ||
| 131 | @node.revert!(current_user) | ||
| 132 | |||
| 133 | if params[:return_to].present? | ||
| 134 | redirect_to safe_return_to(params[:return_to]) | ||
| 135 | elsif @node.draft | ||
| 136 | redirect_to edit_node_path(@node) | ||
| 82 | else | 137 | else |
| 83 | render :action => :edit | 138 | redirect_to node_path(@node) |
| 84 | end | 139 | end |
| 140 | rescue LockedByAnotherUser => e | ||
| 141 | flash[:error] = e.message | ||
| 142 | redirect_to node_path(@node) | ||
| 85 | end | 143 | end |
| 86 | 144 | ||
| 87 | def destroy | 145 | def destroy |
| 88 | @node.destroy | 146 | @node.destroy |
| 89 | end | 147 | end |
| 90 | 148 | ||
| 91 | def publish | 149 | def publish |
| 92 | @node.publish_draft! | 150 | @node.publish_draft! |
| 93 | flash[:notice] = "Draft has been published" | 151 | flash[:notice] = "Draft has been published" |
| 94 | redirect_to node_path(@node) | 152 | redirect_to node_path(@node) |
| 95 | end | 153 | end |
| 96 | 154 | ||
| 97 | def unlock | 155 | def unlock |
| 98 | if @node.unlock! | 156 | if @node.unlock! |
| 99 | flash[:notice] = "Node unlocked" | 157 | flash[:notice] = "Node unlocked" |
| 100 | else | 158 | else |
| 101 | flash[:notice] = "Already unlocked" | 159 | flash[:notice] = "Already unlocked" |
| 102 | end | 160 | end |
| 103 | 161 | ||
| 162 | redirect_to node_path(@node) | ||
| 163 | end | ||
| 164 | |||
| 165 | def generate_shared_preview | ||
| 166 | @node = Node.find(params[:id]) | ||
| 167 | if @node.draft | ||
| 168 | @node.draft.ensure_preview_token! | ||
| 169 | flash[:notice] = "Shareable preview link created - see below." | ||
| 170 | else | ||
| 171 | flash[:notice] = "Create or edit a draft first - shared preview links are only available for pages with an active draft." | ||
| 172 | end | ||
| 173 | redirect_to node_path(@node) | ||
| 174 | end | ||
| 175 | |||
| 176 | def revoke_shared_preview | ||
| 177 | @node = Node.find(params[:id]) | ||
| 178 | @node.draft.revoke_preview_token! if @node.draft | ||
| 179 | flash[:notice] = "Shareable preview link revoked." | ||
| 104 | redirect_to node_path(@node) | 180 | redirect_to node_path(@node) |
| 105 | end | 181 | end |
| 106 | 182 | ||
| 183 | def parameterize_preview | ||
| 184 | render plain: slug_for(params[:title]) | ||
| 185 | end | ||
| 186 | |||
| 187 | # Filter functions for admin views | ||
| 188 | def drafts | ||
| 189 | @nodes = index_matching(Node.drafts_and_autosaves) | ||
| 190 | end | ||
| 191 | |||
| 192 | def recent | ||
| 193 | @nodes = index_matching(Node.recently_changed) | ||
| 194 | end | ||
| 195 | |||
| 196 | def mine | ||
| 197 | base = Node.joins(:pages) | ||
| 198 | .where("pages.user_id = ? or pages.editor_id = ?", current_user, current_user) | ||
| 199 | .distinct | ||
| 200 | @nodes = index_matching(base) | ||
| 201 | end | ||
| 202 | |||
| 203 | def chapters | ||
| 204 | @kind_keys = Array(params[:kinds]) & %w[erfa chaostreff] | ||
| 205 | @kind_keys = %w[erfa chaostreff] if @kind_keys.empty? | ||
| 206 | tags = @kind_keys.flat_map { |key| CccConventions::NODE_KINDS[key][:tags] } | ||
| 207 | @nodes = nodes_matching_tags(tags) | ||
| 208 | end | ||
| 209 | |||
| 210 | def tags | ||
| 211 | tags = params[:tags].to_s.split(',').map(&:strip).reject(&:blank?) | ||
| 212 | @nodes = nodes_matching_tags(tags) | ||
| 213 | end | ||
| 214 | |||
| 215 | def sitemap | ||
| 216 | @sitemap = Node.root.self_and_descendants_ordered_with_level | ||
| 217 | @sitemap_descendant_counts = descendant_counts_for(@sitemap) | ||
| 218 | end | ||
| 219 | |||
| 107 | private | 220 | private |
| 108 | 221 | ||
| 222 | def slug_for(title) | ||
| 223 | title.to_s.parameterize | ||
| 224 | end | ||
| 225 | |||
| 109 | def node_params | 226 | def node_params |
| 110 | params.fetch(:node, {}).permit(:slug, :parent_id, :staged_slug, :staged_parent_id) | 227 | params.fetch(:node, {}).permit(:slug, :parent_id, :staged_slug, :staged_parent_id) |
| 111 | end | 228 | end |
| @@ -113,25 +230,56 @@ class NodesController < ApplicationController | |||
| 113 | def page_params | 230 | def page_params |
| 114 | params.fetch(:page, {}).permit(:title, :abstract, :body, :template_name, :published_at, :user_id) | 231 | params.fetch(:page, {}).permit(:title, :abstract, :body, :template_name, :published_at, :user_id) |
| 115 | end | 232 | end |
| 116 | 233 | ||
| 117 | def find_node | 234 | def find_node |
| 118 | @node = Node.find(params[:id]) | 235 | @node = Node.find(params[:id]) |
| 119 | end | 236 | end |
| 120 | 237 | ||
| 121 | def find_parent | 238 | def find_parent |
| 122 | case params[:kind] | 239 | case params[:kind] |
| 123 | when "top_level" | ||
| 124 | Node.root.id | ||
| 125 | when "update" | ||
| 126 | Update.find_or_create_parent.id | ||
| 127 | when "press_release" | ||
| 128 | Update.find_or_create_parent.id | ||
| 129 | when "generic" | 240 | when "generic" |
| 130 | if params[:parent_id] && Node.find(params[:parent_id]) | 241 | if params[:parent_id] && Node.find(params[:parent_id]) |
| 131 | params[:parent_id] | 242 | params[:parent_id] |
| 132 | else | 243 | else |
| 133 | nil | 244 | nil |
| 134 | end | 245 | end |
| 246 | else | ||
| 247 | config = CccConventions::NODE_KINDS[params[:kind]] | ||
| 248 | config && config[:parent] ? config[:parent].call.id : nil | ||
| 249 | end | ||
| 250 | end | ||
| 251 | |||
| 252 | def pin_to_default_locale | ||
| 253 | Globalize.with_locale(I18n.default_locale) { yield } | ||
| 254 | end | ||
| 255 | |||
| 256 | def descendant_counts_for(ordered_with_level) | ||
| 257 | counts = Hash.new(0) | ||
| 258 | stack = [] # [node, level, index] | ||
| 259 | ordered_with_level.each_with_index do |(node, level), index| | ||
| 260 | while stack.any? && stack.last[1] >= level | ||
| 261 | ancestor_node, _ancestor_level, ancestor_index = stack.pop | ||
| 262 | counts[ancestor_node.id] = index - ancestor_index - 1 | ||
| 263 | end | ||
| 264 | stack << [node, level, index] | ||
| 265 | end | ||
| 266 | total = ordered_with_level.length | ||
| 267 | while stack.any? | ||
| 268 | ancestor_node, _ancestor_level, ancestor_index = stack.pop | ||
| 269 | counts[ancestor_node.id] = total - ancestor_index - 1 | ||
| 135 | end | 270 | end |
| 271 | counts | ||
| 272 | end | ||
| 273 | |||
| 274 | def nodes_matching_tags(tags) | ||
| 275 | matching_pages = Page.tagged_with(tags, any: true).reselect(:id) | ||
| 276 | base = Node.where(head_id: matching_pages).or(Node.where(draft_id: matching_pages)) | ||
| 277 | index_matching(base) | ||
| 278 | end | ||
| 279 | |||
| 280 | def index_matching(base_scope) | ||
| 281 | scope = base_scope.includes(:head, :draft) | ||
| 282 | scope = scope.merge(Node.editor_search(params[:q])) if params[:q].present? | ||
| 283 | scope.order("nodes.updated_at desc").paginate(page: params[:page], per_page: 25) | ||
| 136 | end | 284 | end |
| 137 | end | 285 | end |
diff --git a/app/controllers/page_translations_controller.rb b/app/controllers/page_translations_controller.rb new file mode 100644 index 0000000..9446cd2 --- /dev/null +++ b/app/controllers/page_translations_controller.rb | |||
| @@ -0,0 +1,109 @@ | |||
| 1 | class PageTranslationsController < ApplicationController | ||
| 2 | layout 'admin' | ||
| 3 | |||
| 4 | before_action :login_required | ||
| 5 | before_action :find_node | ||
| 6 | before_action :find_locale, :only => [:show, :edit, :update, :autosave, :destroy] | ||
| 7 | |||
| 8 | def index | ||
| 9 | page = @node.draft || @node.head | ||
| 10 | @translations = page ? page.translation_summary : [] | ||
| 11 | end | ||
| 12 | |||
| 13 | def show | ||
| 14 | @page = @node.draft || @node.head | ||
| 15 | @translation = @page.translations.find_by(:locale => @locale) | ||
| 16 | @default_translation = @page.translations.find_by(:locale => I18n.default_locale) | ||
| 17 | end | ||
| 18 | |||
| 19 | def edit | ||
| 20 | @node.lock_for_editing!(current_user) | ||
| 21 | @page = @node.draft || @node.head | ||
| 22 | @translation = @page.translations.find_by(:locale => @locale) | ||
| 23 | rescue LockedByAnotherUser => e | ||
| 24 | flash[:error] = e.message | ||
| 25 | redirect_to node_path(@node) | ||
| 26 | end | ||
| 27 | |||
| 28 | def update | ||
| 29 | Globalize.with_locale(@locale) { @node.autosave!(translation_params, current_user) } | ||
| 30 | @node.save_draft!(current_user) | ||
| 31 | flash[:notice] = "#{@locale.upcase} translation saved. Publish the draft to make it live." | ||
| 32 | |||
| 33 | if params[:commit] == "Save + Unlock + Exit" | ||
| 34 | @node.unlock! | ||
| 35 | redirect_to node_path(@node) | ||
| 36 | else | ||
| 37 | redirect_to edit_node_translation_path(@node, @locale) | ||
| 38 | end | ||
| 39 | rescue LockedByAnotherUser => e | ||
| 40 | flash[:error] = e.message | ||
| 41 | redirect_to node_path(@node) | ||
| 42 | end | ||
| 43 | |||
| 44 | def autosave | ||
| 45 | Globalize.with_locale(@locale) { @node.autosave!(translation_params, current_user) } | ||
| 46 | head :ok | ||
| 47 | rescue LockedByAnotherUser => e | ||
| 48 | render plain: e.message, status: :locked | ||
| 49 | rescue ActiveRecord::RecordInvalid => e | ||
| 50 | render plain: e.message, status: :unprocessable_entity | ||
| 51 | rescue StandardError => e | ||
| 52 | render plain: "Autosave failed", status: :internal_server_error | ||
| 53 | end | ||
| 54 | |||
| 55 | def destroy | ||
| 56 | base = @node.draft || @node.head | ||
| 57 | unless base && base.translated_locales.include?(@locale) | ||
| 58 | flash[:error] = "No #{@locale.to_s.upcase} translation exists to remove." | ||
| 59 | return redirect_to node_path(@node) | ||
| 60 | end | ||
| 61 | |||
| 62 | if (base.translated_locales - [@locale]).empty? | ||
| 63 | flash[:error] = "Can't remove the only remaining translation." | ||
| 64 | return redirect_to node_path(@node) | ||
| 65 | end | ||
| 66 | |||
| 67 | draft = ensure_editable_draft | ||
| 68 | draft.translations.where(:locale => @locale).delete_all | ||
| 69 | draft.reload | ||
| 70 | |||
| 71 | flash[:notice] = "#{@locale.upcase} translation removed from the draft. Publish to make this permanent." | ||
| 72 | redirect_to node_path(@node) | ||
| 73 | rescue LockedByAnotherUser => e | ||
| 74 | flash[:error] = e.message | ||
| 75 | redirect_to node_path(@node) | ||
| 76 | end | ||
| 77 | |||
| 78 | private | ||
| 79 | |||
| 80 | def find_node | ||
| 81 | @node = Node.find(params[:node_id]) | ||
| 82 | end | ||
| 83 | |||
| 84 | # Every remaining action's :translation_locale is already | ||
| 85 | # router-constrained to non-default locales, so this should never | ||
| 86 | # actually fire in practice -- it's a deliberate second check, kept | ||
| 87 | # as a drift detector in case the route constraint and this list | ||
| 88 | # (both driven by Page.non_default_locales) ever disagree. | ||
| 89 | def find_locale | ||
| 90 | candidate = params[:translation_locale].to_s.to_sym | ||
| 91 | unless Page.non_default_locales.include?(candidate) | ||
| 92 | raise ActionController::RoutingError, "Unknown or default locale" | ||
| 93 | end | ||
| 94 | @locale = candidate | ||
| 95 | end | ||
| 96 | |||
| 97 | # Never mutates head directly. Locks first (same guard as the | ||
| 98 | # primary editor), then reuses an existing draft or creates one -- | ||
| 99 | # deliberately not going through the autosave buffer: this is a | ||
| 100 | # plain, explicit Save, not a live-typing surface. | ||
| 101 | def ensure_editable_draft | ||
| 102 | @node.lock_for_editing!(current_user) | ||
| 103 | @node.draft || @node.create_new_draft(current_user) | ||
| 104 | end | ||
| 105 | |||
| 106 | def translation_params | ||
| 107 | params.fetch(:page, {}).permit(:title, :abstract, :body) | ||
| 108 | end | ||
| 109 | end | ||
diff --git a/app/controllers/related_assets_controller.rb b/app/controllers/related_assets_controller.rb new file mode 100644 index 0000000..479ebec --- /dev/null +++ b/app/controllers/related_assets_controller.rb | |||
| @@ -0,0 +1,49 @@ | |||
| 1 | class RelatedAssetsController < ApplicationController | ||
| 2 | before_action :login_required | ||
| 3 | before_action :find_node | ||
| 4 | |||
| 5 | def search | ||
| 6 | term = params[:search_term].to_s.strip | ||
| 7 | attached_ids = @node.editable_page.related_assets.pluck(:asset_id) | ||
| 8 | scope = Asset.images.where.not(id: attached_ids) | ||
| 9 | |||
| 10 | results = if term.present? | ||
| 11 | scope.where("name ILIKE ?", "%#{term}%").limit(10) | ||
| 12 | else | ||
| 13 | scope.order(created_at: :desc).limit(5) | ||
| 14 | end | ||
| 15 | |||
| 16 | render json: results.map { |a| | ||
| 17 | { id: a.id, name: a.name, thumb_url: a.upload.url(:thumb) } | ||
| 18 | } | ||
| 19 | end | ||
| 20 | |||
| 21 | def create | ||
| 22 | asset = Asset.find(params[:asset_id]) | ||
| 23 | related = @node.editable_page.related_assets.find_or_create_by!(asset: asset) | ||
| 24 | |||
| 25 | render json: { | ||
| 26 | id: related.id, | ||
| 27 | asset_id: asset.id, | ||
| 28 | name: asset.name, | ||
| 29 | thumb_url: asset.upload.url(:thumb), | ||
| 30 | url: node_related_asset_path(@node, related) | ||
| 31 | } | ||
| 32 | end | ||
| 33 | |||
| 34 | def destroy | ||
| 35 | @node.editable_page.related_assets.find(params[:id]).destroy | ||
| 36 | head :ok | ||
| 37 | end | ||
| 38 | |||
| 39 | def update | ||
| 40 | @node.editable_page.related_assets.find(params[:id]).insert_at(params[:position].to_i) | ||
| 41 | head :ok | ||
| 42 | end | ||
| 43 | |||
| 44 | private | ||
| 45 | |||
| 46 | def find_node | ||
| 47 | @node = Node.find(params[:node_id]) | ||
| 48 | end | ||
| 49 | end | ||
diff --git a/app/controllers/revisions_controller.rb b/app/controllers/revisions_controller.rb index 42d667e..0aed243 100644 --- a/app/controllers/revisions_controller.rb +++ b/app/controllers/revisions_controller.rb | |||
| @@ -13,16 +13,31 @@ class RevisionsController < ApplicationController | |||
| 13 | 13 | ||
| 14 | def diff | 14 | def diff |
| 15 | @node = Node.find(params[:node_id]) | 15 | @node = Node.find(params[:node_id]) |
| 16 | 16 | ||
| 17 | if @node.pages.length > 1 | 17 | if @node.pages.length > 1 |
| 18 | params[:start_revision] ||= @node.pages.all[-2].revision | 18 | params[:start_revision] ||= @node.pages.all[-2].revision |
| 19 | params[:end_revision] ||= @node.pages.all[-1].revision | 19 | params[:end_revision] ||= @node.pages.all[-1].revision |
| 20 | else | 20 | else |
| 21 | params[:start], params[:end] = 1, 1 | 21 | params[:start_revision], params[:end_revision] = 1, 1 |
| 22 | end | ||
| 23 | |||
| 24 | @start = @node.resolve_page_reference(params[:start_revision]) | ||
| 25 | @end = @node.resolve_page_reference(params[:end_revision]) | ||
| 26 | |||
| 27 | if @start.nil? || @end.nil? | ||
| 28 | flash[:error] = "That comparison is no longer available." | ||
| 29 | redirect_to(node_path(@node)) and return | ||
| 22 | end | 30 | end |
| 23 | 31 | ||
| 24 | @start = @node.pages.find_by_revision( params[:start_revision] ) | 32 | @locale_summary = @end.locale_diff_summary(@start) |
| 25 | @end = @node.pages.find_by_revision( params[:end_revision] ) | 33 | requested_locale = params[:locale].presence&.to_sym |
| 34 | default_locale = @locale_summary.find { |s| s[:changed] }&.dig(:locale) || I18n.default_locale | ||
| 35 | @locale = @locale_summary.any? { |s| s[:locale] == requested_locale } ? requested_locale : default_locale | ||
| 36 | |||
| 37 | @diff_view = params[:view] == "side_by_side" ? :side_by_side : :inline | ||
| 38 | @diff = @end.diff_against(@start, view: @diff_view, locale: @locale) | ||
| 39 | @available_layer_pairs = @node.available_layer_pairs | ||
| 40 | @locked_by_other = @node.locked? && @node.lock_owner != current_user | ||
| 26 | end | 41 | end |
| 27 | 42 | ||
| 28 | def show | 43 | def show |
diff --git a/app/controllers/shared_previews_controller.rb b/app/controllers/shared_previews_controller.rb new file mode 100644 index 0000000..f6fb45a --- /dev/null +++ b/app/controllers/shared_previews_controller.rb | |||
| @@ -0,0 +1,22 @@ | |||
| 1 | class SharedPreviewsController < ApplicationController | ||
| 2 | def show | ||
| 3 | @page = Page.find_by!(preview_token: params[:token]) | ||
| 4 | node = @page.node | ||
| 5 | |||
| 6 | if node | ||
| 7 | is_head = node.head_id == @page.id | ||
| 8 | is_draft = node.draft_id == @page.id | ||
| 9 | |||
| 10 | currently_public = is_head && @page.public? | ||
| 11 | superseded = !is_head && !is_draft | ||
| 12 | |||
| 13 | if superseded || currently_public | ||
| 14 | redirect_to @page.public_link | ||
| 15 | return | ||
| 16 | end | ||
| 17 | end | ||
| 18 | |||
| 19 | response.headers['X-Robots-Tag'] = 'noindex' | ||
| 20 | render template: @page.valid_template, layout: "application" | ||
| 21 | end | ||
| 22 | end | ||
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index f01691f..6572b7a 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb | |||
| @@ -15,7 +15,7 @@ class UsersController < ApplicationController | |||
| 15 | end | 15 | end |
| 16 | 16 | ||
| 17 | def new | 17 | def new |
| 18 | @user = User.new | 18 | @user = User.new(admin: params[:admin].present?) |
| 19 | end | 19 | end |
| 20 | 20 | ||
| 21 | def create | 21 | def create |
