summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/controllers/nodes_controller.rb12
-rw-r--r--app/controllers/page_translations_controller.rb98
-rw-r--r--app/models/page.rb21
-rw-r--r--app/views/nodes/show.html.erb49
-rw-r--r--app/views/page_translations/edit.html.erb39
-rw-r--r--app/views/page_translations/index.html.erb19
-rw-r--r--app/views/page_translations/show.html.erb27
7 files changed, 257 insertions, 8 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 @@
1class 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
98end
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>