diff options
| -rw-r--r-- | app/controllers/nodes_controller.rb | 12 | ||||
| -rw-r--r-- | app/controllers/page_translations_controller.rb | 98 | ||||
| -rw-r--r-- | app/models/page.rb | 21 | ||||
| -rw-r--r-- | app/views/nodes/show.html.erb | 49 | ||||
| -rw-r--r-- | app/views/page_translations/edit.html.erb | 39 | ||||
| -rw-r--r-- | app/views/page_translations/index.html.erb | 19 | ||||
| -rw-r--r-- | app/views/page_translations/show.html.erb | 27 | ||||
| -rw-r--r-- | config/routes.rb | 6 | ||||
| -rw-r--r-- | public/stylesheets/admin.css | 27 | ||||
| -rw-r--r-- | test/controllers/page_translations_controller_test.rb | 78 |
10 files changed, 366 insertions, 10 deletions
diff --git a/app/controllers/nodes_controller.rb b/app/controllers/nodes_controller.rb index c1468be..249602d 100644 --- a/app/controllers/nodes_controller.rb +++ b/app/controllers/nodes_controller.rb | |||
| @@ -16,6 +16,8 @@ class NodesController < ApplicationController | |||
| 16 | :revert | 16 | :revert |
| 17 | ] | 17 | ] |
| 18 | 18 | ||
| 19 | around_action :pin_to_default_locale, :only => [:show, :edit, :update, :autosave] | ||
| 20 | |||
| 19 | def index | 21 | def index |
| 20 | @nodes = Node.root.descendants.includes(:head, :draft) | 22 | @nodes = Node.root.descendants.includes(:head, :draft) |
| 21 | .order('id DESC') | 23 | .order('id DESC') |
| @@ -59,9 +61,9 @@ class NodesController < ApplicationController | |||
| 59 | end | 61 | end |
| 60 | 62 | ||
| 61 | def show | 63 | def show |
| 62 | node = Node.find(params[:id]) | 64 | @page = @node.draft || @node.head |
| 63 | node.wipe_draft! | 65 | @default_translation = @page.translations.find_by(:locale => I18n.default_locale) |
| 64 | @page = node.draft || node.head | 66 | @translations = @page.translation_summary |
| 65 | end | 67 | end |
| 66 | 68 | ||
| 67 | def edit | 69 | def edit |
| @@ -248,6 +250,10 @@ class NodesController < ApplicationController | |||
| 248 | end | 250 | end |
| 249 | end | 251 | end |
| 250 | 252 | ||
| 253 | def pin_to_default_locale | ||
| 254 | Globalize.with_locale(I18n.default_locale) { yield } | ||
| 255 | end | ||
| 256 | |||
| 251 | def descendant_counts_for(ordered_with_level) | 257 | def descendant_counts_for(ordered_with_level) |
| 252 | counts = Hash.new(0) | 258 | counts = Hash.new(0) |
| 253 | stack = [] # [node, level, index] | 259 | stack = [] # [node, level, index] |
diff --git a/app/controllers/page_translations_controller.rb b/app/controllers/page_translations_controller.rb new file mode 100644 index 0000000..47f01f7 --- /dev/null +++ b/app/controllers/page_translations_controller.rb | |||
| @@ -0,0 +1,98 @@ | |||
| 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, :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 | draft = ensure_editable_draft | ||
| 30 | Globalize.with_locale(@locale) { draft.update!(translation_params) } | ||
| 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 destroy | ||
| 45 | base = @node.draft || @node.head | ||
| 46 | unless base && base.translated_locales.include?(@locale) | ||
| 47 | flash[:error] = "No #{@locale.to_s.upcase} translation exists to remove." | ||
| 48 | return redirect_to node_path(@node) | ||
| 49 | end | ||
| 50 | |||
| 51 | if (base.translated_locales - [@locale]).empty? | ||
| 52 | flash[:error] = "Can't remove the only remaining translation." | ||
| 53 | return redirect_to node_path(@node) | ||
| 54 | end | ||
| 55 | |||
| 56 | draft = ensure_editable_draft | ||
| 57 | draft.translations.where(:locale => @locale).delete_all | ||
| 58 | draft.reload | ||
| 59 | |||
| 60 | flash[:notice] = "#{@locale.upcase} translation removed from the draft. Publish to make this permanent." | ||
| 61 | redirect_to node_path(@node) | ||
| 62 | rescue LockedByAnotherUser => e | ||
| 63 | flash[:error] = e.message | ||
| 64 | redirect_to node_path(@node) | ||
| 65 | end | ||
| 66 | |||
| 67 | private | ||
| 68 | |||
| 69 | def find_node | ||
| 70 | @node = Node.find(params[:node_id]) | ||
| 71 | end | ||
| 72 | |||
| 73 | # Every remaining action's :translation_locale is already | ||
| 74 | # router-constrained to non-default locales, so this should never | ||
| 75 | # actually fire in practice -- it's a deliberate second check, kept | ||
| 76 | # as a drift detector in case the route constraint and this list | ||
| 77 | # (both driven by Page.non_default_locales) ever disagree. | ||
| 78 | def find_locale | ||
| 79 | candidate = params[:translation_locale].to_s.to_sym | ||
| 80 | unless Page.non_default_locales.include?(candidate) | ||
| 81 | raise ActionController::RoutingError, "Unknown or default locale" | ||
| 82 | end | ||
| 83 | @locale = candidate | ||
| 84 | end | ||
| 85 | |||
| 86 | # Never mutates head directly. Locks first (same guard as the | ||
| 87 | # primary editor), then reuses an existing draft or creates one -- | ||
| 88 | # deliberately not going through the autosave buffer: this is a | ||
| 89 | # plain, explicit Save, not a live-typing surface. | ||
| 90 | def ensure_editable_draft | ||
| 91 | @node.lock_for_editing!(current_user) | ||
| 92 | @node.draft || @node.create_new_draft(current_user) | ||
| 93 | end | ||
| 94 | |||
| 95 | def translation_params | ||
| 96 | params.fetch(:page, {}).permit(:title, :abstract, :body) | ||
| 97 | end | ||
| 98 | end | ||
diff --git a/app/models/page.rb b/app/models/page.rb index db5b688..f796228 100644 --- a/app/models/page.rb +++ b/app/models/page.rb | |||
| @@ -87,6 +87,27 @@ class Page < ApplicationRecord | |||
| 87 | end | 87 | end |
| 88 | end | 88 | end |
| 89 | 89 | ||
| 90 | def self.non_default_locales | ||
| 91 | I18n.available_locales - [:root, I18n.default_locale] | ||
| 92 | end | ||
| 93 | |||
| 94 | # One row per non-default locale, read from the actual translation | ||
| 95 | # row -- never through the locale-dependent accessor, so a locale | ||
| 96 | # with no real translation yet reports as absent rather than quietly | ||
| 97 | # showing a fallback value borrowed from another locale. | ||
| 98 | def translation_summary | ||
| 99 | Page.non_default_locales.map do |locale| | ||
| 100 | translation = translations.find_by(:locale => locale) | ||
| 101 | { | ||
| 102 | :locale => locale, | ||
| 103 | :exists => translation.present?, | ||
| 104 | :title => translation&.title, | ||
| 105 | :updated_at => translation&.updated_at, | ||
| 106 | :outdated => translation.present? && outdated_translations?(:locale => locale) | ||
| 107 | } | ||
| 108 | end | ||
| 109 | end | ||
| 110 | |||
| 90 | def self.untranslated(options = {:locale => :de}) | 111 | def self.untranslated(options = {:locale => :de}) |
| 91 | PageTranslation.all.group_by(&:page_id).select do |k,v| | 112 | PageTranslation.all.group_by(&:page_id).select do |k,v| |
| 92 | v.size == 1 && v.map{|x| x.locale}.include?(options[:locale]) | 113 | v.size == 1 && v.map{|x| x.locale}.include?(options[:locale]) |
diff --git a/app/views/nodes/show.html.erb b/app/views/nodes/show.html.erb index 54bb38f..e6a922b 100644 --- a/app/views/nodes/show.html.erb +++ b/app/views/nodes/show.html.erb | |||
| @@ -1,6 +1,6 @@ | |||
| 1 | <% locked_by_other = @node.locked? && @node.lock_owner != current_user %> | 1 | <% locked_by_other = @node.locked? && @node.lock_owner != current_user %> |
| 2 | <div id="page_editor" class="show_node"> | 2 | <div id="page_editor" class="show_node"> |
| 3 | <h1><%= title_for_node(@node) %></h1> | 3 | <h1><%= title_for_node(@node) %> <small>(<%= I18n.default_locale.to_s.upcase %>)</small></h1> |
| 4 | <div id="content"> | 4 | <div id="content"> |
| 5 | <div class="node_description">Status</div> | 5 | <div class="node_description">Status</div> |
| 6 | <div class="node_content node_info_group node_status"> | 6 | <div class="node_content node_info_group node_status"> |
| @@ -70,6 +70,46 @@ | |||
| 70 | <% end %> | 70 | <% end %> |
| 71 | </div> | 71 | </div> |
| 72 | 72 | ||
| 73 | <div class="node_description">Translations</div> | ||
| 74 | <div class="node_content node_info_group"> | ||
| 75 | <div class="node_info_group_items"> | ||
| 76 | <div class="node_info_item"> | ||
| 77 | <span class="node_info_label"><%= I18n.default_locale.to_s.upcase %> (default)</span> | ||
| 78 | <%= @page.title %> — updated <%= @default_translation&.updated_at || @page.updated_at %> | ||
| 79 | </div> | ||
| 80 | <% @translations.each do |t| %> | ||
| 81 | <div class="node_info_item"> | ||
| 82 | <span class="node_info_label"><%= t[:locale].to_s.upcase %></span> | ||
| 83 | <div class="aligned_action_row"> | ||
| 84 | <% if t[:exists] %> | ||
| 85 | <%= link_to t[:title], node_translation_path(@node, t[:locale]) %> — updated <%= t[:updated_at] %> | ||
| 86 | <% if t[:outdated] %> | ||
| 87 | <span class="warning">may be outdated</span> | ||
| 88 | <% end %> | ||
| 89 | <% else %> | ||
| 90 | <span class="field_hint">Not yet translated.</span> | ||
| 91 | <% end %> | ||
| 92 | <% if locked_by_other %> | ||
| 93 | <span class="disabled_action"><%= t[:exists] ? "Lock + Edit" : "Create translation + Lock" %></span> | ||
| 94 | <% else %> | ||
| 95 | <%= link_to edit_node_translation_path(@node, t[:locale]), class: "action_button" do %> | ||
| 96 | <%= icon(t[:exists] ? "edit" : "language", library: "tabler", "aria-hidden": true) %> | ||
| 97 | <%= t[:exists] ? "Lock + Edit" : "Create translation + Lock" %> | ||
| 98 | <% end %> | ||
| 99 | <% if t[:exists] %> | ||
| 100 | <%= button_to node_translation_path(@node, t[:locale]), method: :delete, | ||
| 101 | form: { data: { confirm: "This cannot be undone. Continue?" }, class: 'button_to destructive' } do %> | ||
| 102 | <%= icon("trash", library: "tabler", "aria-hidden": true) %> | ||
| 103 | Destroy Translation | ||
| 104 | <% end %> | ||
| 105 | <% end %> | ||
| 106 | <% end %> | ||
| 107 | </div> | ||
| 108 | </div> | ||
| 109 | <% end %> | ||
| 110 | </div> | ||
| 111 | </div> | ||
| 112 | |||
| 73 | <div class="node_description">People</div> | 113 | <div class="node_description">People</div> |
| 74 | <div class="node_content node_info_group"> | 114 | <div class="node_content node_info_group"> |
| 75 | <div class="node_info_group_items"> | 115 | <div class="node_info_group_items"> |
| @@ -122,7 +162,7 @@ | |||
| 122 | <div class="node_info_item"> | 162 | <div class="node_info_item"> |
| 123 | <span class="node_info_label">Public Preview</span> | 163 | <span class="node_info_label">Public Preview</span> |
| 124 | <% if @node.draft.preview_token.present? %> | 164 | <% if @node.draft.preview_token.present? %> |
| 125 | <div class="preview_link_row"> | 165 | <div class="aligned_action_row"> |
| 126 | <%= link_to shared_preview_path(token: @node.draft.preview_token), shared_preview_url(token: @node.draft.preview_token), target: "_blank", rel: "noopener" %> | 166 | <%= link_to shared_preview_path(token: @node.draft.preview_token), shared_preview_url(token: @node.draft.preview_token), target: "_blank", rel: "noopener" %> |
| 127 | <%= button_to 'Revoke', revoke_shared_preview_node_path(@node), method: :put, form: { data: { confirm: "Revoke this preview link? Anyone currently using it will immediately lose access." }, class: 'button_to state_changing' } %> | 167 | <%= button_to 'Revoke', revoke_shared_preview_node_path(@node), method: :put, form: { data: { confirm: "Revoke this preview link? Anyone currently using it will immediately lose access." }, class: 'button_to state_changing' } %> |
| 128 | </div> | 168 | </div> |
| @@ -205,12 +245,11 @@ | |||
| 205 | </div> | 245 | </div> |
| 206 | <% end %> | 246 | <% end %> |
| 207 | 247 | ||
| 208 | <div class="node_description">Abstract</div> | 248 | <div class="node_description">Abstract (<%= I18n.default_locale.to_s.upcase %>)</div> |
| 209 | <div class="node_content"><%= sanitize(@page.abstract) %></div> | 249 | <div class="node_content"><%= sanitize(@page.abstract) %></div> |
| 210 | 250 | ||
| 211 | <div class="node_description">Body</div> | 251 | <div class="node_description">Body (<%= I18n.default_locale.to_s.upcase %>)</div> |
| 212 | <div class="node_content"><%= sanitize(@page.body) %></div> | 252 | <div class="node_content"><%= sanitize(@page.body) %></div> |
| 213 | <%# Assets not yet addressed - no confirmed model/association seen for this page's attached assets %> | ||
| 214 | 253 | ||
| 215 | </div> | 254 | </div> |
| 216 | </div> | 255 | </div> |
diff --git a/app/views/page_translations/edit.html.erb b/app/views/page_translations/edit.html.erb new file mode 100644 index 0000000..7371a42 --- /dev/null +++ b/app/views/page_translations/edit.html.erb | |||
| @@ -0,0 +1,39 @@ | |||
| 1 | <div class="translation_banner"> | ||
| 2 | Editing the <strong><%= @locale.to_s.upcase %></strong> translation of <strong><%= title_for_node(@node) %></strong>. | ||
| 3 | </div> | ||
| 4 | |||
| 5 | <p class="field_hint"> | ||
| 6 | Metadata such as images, tags, template, and author belong to the page | ||
| 7 | as a whole, not to a single translation — change those from the | ||
| 8 | <%= link_to 'default-locale editor', edit_node_path(@node) %>. | ||
| 9 | </p> | ||
| 10 | |||
| 11 | <div class="node_action_bar"> | ||
| 12 | <%= button_to 'Unlock + Back', unlock_node_path(@node), method: :put, | ||
| 13 | form: { class: 'button_to state_changing' }, | ||
| 14 | disabled: @node.autosave.present? %> | ||
| 15 | <%= submit_tag "Save #{@locale.to_s.upcase} translation", form: "translation_edit_form" %> | ||
| 16 | <%= submit_tag "Save + Unlock + Exit", form: "translation_edit_form" %> | ||
| 17 | <%= link_to "Preview ↗", preview_page_path(@page, :locale => @locale), target: "_blank", rel: "noopener", class: "preview_link" %> | ||
| 18 | </div> | ||
| 19 | |||
| 20 | <div id="page_editor"> | ||
| 21 | <%= form_with url: node_translation_path(@node, @locale), method: :patch, local: true, id: "translation_edit_form" do |f| %> | ||
| 22 | <div id="content"> | ||
| 23 | <div class="node_description">Title</div> | ||
| 24 | <div class="node_content"> | ||
| 25 | <%= text_field_tag "page[title]", @translation&.title %> | ||
| 26 | </div> | ||
| 27 | |||
| 28 | <div class="node_description">Abstract</div> | ||
| 29 | <div class="node_content"> | ||
| 30 | <%= text_area_tag "page[abstract]", @translation&.abstract %> | ||
| 31 | </div> | ||
| 32 | |||
| 33 | <div class="node_description">Body</div> | ||
| 34 | <div class="node_content"> | ||
| 35 | <%= text_area_tag "page[body]", @translation&.body, :class => 'with_editor' %> | ||
| 36 | </div> | ||
| 37 | </div> | ||
| 38 | <% end %> | ||
| 39 | </div> | ||
diff --git a/app/views/page_translations/index.html.erb b/app/views/page_translations/index.html.erb new file mode 100644 index 0000000..a8c4f5d --- /dev/null +++ b/app/views/page_translations/index.html.erb | |||
| @@ -0,0 +1,19 @@ | |||
| 1 | <h1>Translations — <%= title_for_node(@node) %></h1> | ||
| 2 | |||
| 3 | <div class="node_content node_info_group"> | ||
| 4 | <div class="node_info_group_items"> | ||
| 5 | <% @translations.each do |t| %> | ||
| 6 | <div class="node_info_item"> | ||
| 7 | <span class="node_info_label"><%= t[:locale].to_s.upcase %></span> | ||
| 8 | <% if t[:exists] %> | ||
| 9 | <%= t[:title] %> — updated <%= t[:updated_at] %> | ||
| 10 | <% if t[:outdated] %><span class="warning">may be outdated</span><% end %> | ||
| 11 | <%= link_to 'Edit', edit_node_translation_path(@node, t[:locale]) %> | ||
| 12 | <% else %> | ||
| 13 | <span class="field_hint">Not yet translated.</span> | ||
| 14 | <%= link_to 'Add', edit_node_translation_path(@node, t[:locale]) %> | ||
| 15 | <% end %> | ||
| 16 | </div> | ||
| 17 | <% end %> | ||
| 18 | </div> | ||
| 19 | </div> | ||
diff --git a/app/views/page_translations/show.html.erb b/app/views/page_translations/show.html.erb new file mode 100644 index 0000000..c5bd151 --- /dev/null +++ b/app/views/page_translations/show.html.erb | |||
| @@ -0,0 +1,27 @@ | |||
| 1 | <h1>Compare — <%= title_for_node(@node) %></h1> | ||
| 2 | |||
| 3 | <div class="translation_compare"> | ||
| 4 | <div class="translation_compare_column"> | ||
| 5 | <h2><%= @locale.to_s.upcase %></h2> | ||
| 6 | <div class="node_description">Title</div> | ||
| 7 | <div class="node_content"><%= @translation&.title %></div> | ||
| 8 | |||
| 9 | <div class="node_description">Abstract</div> | ||
| 10 | <div class="node_content"><%= sanitize(@translation&.abstract.to_s) %></div> | ||
| 11 | |||
| 12 | <div class="node_description">Body</div> | ||
| 13 | <div class="node_content"><%= sanitize(@translation&.body.to_s) %></div> | ||
| 14 | </div> | ||
| 15 | |||
| 16 | <div class="translation_compare_column"> | ||
| 17 | <h2><%= I18n.default_locale.to_s.upcase %> (default)</h2> | ||
| 18 | <div class="node_description">Title</div> | ||
| 19 | <div class="node_content"><%= @default_translation&.title %></div> | ||
| 20 | |||
| 21 | <div class="node_description">Abstract</div> | ||
| 22 | <div class="node_content"><%= sanitize(@default_translation&.abstract.to_s) %></div> | ||
| 23 | |||
| 24 | <div class="node_description">Body</div> | ||
| 25 | <div class="node_content"><%= sanitize(@default_translation&.body.to_s) %></div> | ||
| 26 | </div> | ||
| 27 | </div> | ||
diff --git a/config/routes.rb b/config/routes.rb index 5d61bae..38a497d 100644 --- a/config/routes.rb +++ b/config/routes.rb | |||
| @@ -55,6 +55,12 @@ Cccms::Application.routes.draw do | |||
| 55 | put :revert | 55 | put :revert |
| 56 | end | 56 | end |
| 57 | 57 | ||
| 58 | resources :translations, controller: 'page_translations', | ||
| 59 | param: :translation_locale, | ||
| 60 | constraints: { translation_locale: /en/ }, | ||
| 61 | only: [:index, :show, :edit, :update, :destroy] | ||
| 62 | |||
| 63 | |||
| 58 | resources :related_assets, only: [:create, :destroy, :update] do | 64 | resources :related_assets, only: [:create, :destroy, :update] do |
| 59 | collection do | 65 | collection do |
| 60 | get :search | 66 | get :search |
diff --git a/public/stylesheets/admin.css b/public/stylesheets/admin.css index c3c0cb4..f1f4c05 100644 --- a/public/stylesheets/admin.css +++ b/public/stylesheets/admin.css | |||
| @@ -637,6 +637,28 @@ table.revisions_table tr:hover { | |||
| 637 | margin: 0; | 637 | margin: 0; |
| 638 | } | 638 | } |
| 639 | 639 | ||
| 640 | /* ============================================================ | ||
| 641 | Translation compare view (page_translations#show) | ||
| 642 | ============================================================ */ | ||
| 643 | |||
| 644 | .translation_compare { | ||
| 645 | display: flex; | ||
| 646 | flex-direction: column; | ||
| 647 | gap: 1.5rem; | ||
| 648 | } | ||
| 649 | |||
| 650 | @media(min-width:1016px) { | ||
| 651 | .translation_compare { | ||
| 652 | flex-direction: row; | ||
| 653 | gap: 2rem; | ||
| 654 | } | ||
| 655 | |||
| 656 | .translation_compare_column { | ||
| 657 | flex: 1; | ||
| 658 | min-width: 0; | ||
| 659 | } | ||
| 660 | } | ||
| 661 | |||
| 640 | table.user_table td.user_login { | 662 | table.user_table td.user_login { |
| 641 | padding-right: 30px; | 663 | padding-right: 30px; |
| 642 | } | 664 | } |
| @@ -834,14 +856,15 @@ form.button_to button[type="submit"] { | |||
| 834 | font-weight: bold; | 856 | font-weight: bold; |
| 835 | } | 857 | } |
| 836 | 858 | ||
| 837 | .preview_link_row { | 859 | .aligned_action_row { |
| 838 | display: flex; | 860 | display: flex; |
| 839 | flex-wrap: wrap; | 861 | flex-wrap: wrap; |
| 840 | align-items: start; | 862 | align-items: start; |
| 841 | gap: 0.5rem; | 863 | gap: 0.5rem; |
| 842 | } | 864 | } |
| 843 | 865 | ||
| 844 | .preview_link_row form.button_to { | 866 | .aligned_action_row .action_button, |
| 867 | .aligned_action_row form.button_to { | ||
| 845 | margin-top: -5px; | 868 | margin-top: -5px; |
| 846 | } | 869 | } |
| 847 | 870 | ||
diff --git a/test/controllers/page_translations_controller_test.rb b/test/controllers/page_translations_controller_test.rb new file mode 100644 index 0000000..d84ac45 --- /dev/null +++ b/test/controllers/page_translations_controller_test.rb | |||
| @@ -0,0 +1,78 @@ | |||
| 1 | require 'test_helper' | ||
| 2 | |||
| 3 | class PageTranslationsControllerTest < ActionController::TestCase | ||
| 4 | test "index lists the default locale's existing translation and flags a missing one" do | ||
| 5 | login_as :quentin | ||
| 6 | node = Node.root.children.create!(:slug => "translations_index_test") | ||
| 7 | node.publish_draft! | ||
| 8 | |||
| 9 | get :index, params: { :node_id => node.id } | ||
| 10 | |||
| 11 | assert_response :success | ||
| 12 | assert_equal [:en], assigns(:translations).map { |t| t[:locale] } | ||
| 13 | assert_equal false, assigns(:translations).first[:exists] | ||
| 14 | end | ||
| 15 | |||
| 16 | test "update creates a first-time translation on a fresh draft, leaving head untouched" do | ||
| 17 | login_as :quentin | ||
| 18 | node = Node.root.children.create!(:slug => "translations_create_test") | ||
| 19 | Globalize.with_locale(:de) { node.draft.update!(:title => "Deutscher Titel") } | ||
| 20 | node.publish_draft! | ||
| 21 | |||
| 22 | patch :update, params: { :node_id => node.id, :translation_locale => "en", :page => { :title => "English Title" } } | ||
| 23 | |||
| 24 | node.reload | ||
| 25 | assert_not_nil node.draft | ||
| 26 | assert_includes node.draft.translated_locales, :en | ||
| 27 | assert_not_includes node.head.translated_locales, :en | ||
| 28 | end | ||
| 29 | |||
| 30 | test "no route exists for the default locale under the translations resource" do | ||
| 31 | login_as :quentin | ||
| 32 | node = Node.root.children.create!(:slug => "translations_route_constraint_test") | ||
| 33 | |||
| 34 | assert_raises(ActionController::UrlGenerationError) do | ||
| 35 | patch :update, params: { :node_id => node.id, :translation_locale => "de", :page => { :title => "x" } } | ||
| 36 | end | ||
| 37 | end | ||
| 38 | |||
| 39 | test "update with Save + Unlock + Exit unlocks the node and redirects to nodes#show" do | ||
| 40 | login_as :quentin | ||
| 41 | node = Node.root.children.create!(:slug => "translations_exit_test") | ||
| 42 | |||
| 43 | patch :update, params: { :node_id => node.id, :translation_locale => "en", :page => { :title => "x" }, :commit => "Save + Unlock + Exit" } | ||
| 44 | |||
| 45 | assert_nil node.reload.lock_owner | ||
| 46 | assert_redirected_to node_path(node) | ||
| 47 | end | ||
| 48 | |||
| 49 | test "update saves the translation onto the draft" do | ||
| 50 | login_as :quentin | ||
| 51 | node = Node.root.children.create!(:slug => "translations_update_test") | ||
| 52 | Globalize.with_locale(:en) { node.draft.update!(:title => "Original") } | ||
| 53 | |||
| 54 | patch :update, params: { :node_id => node.id, :translation_locale => "en", :page => { :title => "Revised" } } | ||
| 55 | |||
| 56 | assert_equal "Revised", Globalize.with_locale(:en) { node.draft.reload.title } | ||
| 57 | end | ||
| 58 | |||
| 59 | test "destroy refuses to remove the only remaining translation" do | ||
| 60 | login_as :quentin | ||
| 61 | node = Node.root.children.create!(:slug => "translations_destroy_last_test") | ||
| 62 | Globalize.with_locale(:en) { node.draft.update!(:title => "Only translation") } | ||
| 63 | node.draft.translations.where(:locale => :de).delete_all | ||
| 64 | |||
| 65 | delete :destroy, params: { :node_id => node.id, :translation_locale => "en" } | ||
| 66 | |||
| 67 | assert_equal "Can't remove the only remaining translation.", flash[:error] | ||
| 68 | end | ||
| 69 | |||
| 70 | test "destroy is a safe no-op, not a false success, when the translation doesn't exist" do | ||
| 71 | login_as :quentin | ||
| 72 | node = Node.root.children.create!(:slug => "translations_destroy_missing_test") | ||
| 73 | |||
| 74 | delete :destroy, params: { :node_id => node.id, :translation_locale => "en" } | ||
| 75 | |||
| 76 | assert_match(/No EN translation exists/, flash[:error]) | ||
| 77 | end | ||
| 78 | end | ||
