diff --git a/decidim-core/app/cells/decidim/diff/show.erb b/decidim-core/app/cells/decidim/diff/show.erb
index ef3d22f96d9a7..d38d236d11fd3 100644
--- a/decidim-core/app/cells/decidim/diff/show.erb
+++ b/decidim-core/app/cells/decidim/diff/show.erb
@@ -1,8 +1,50 @@
-<%= render :diff_mode_dropdown %>
-<%= render :diff_mode_html %>
+
+
+
+ -
+
+
+ <%= link_to "#diff-text", class: "button button--nomargin button--sc hollow" do %>
+ <%= t("versions.tabs.text") %>
+ <% end %>
+
+
+ -
+
+
+ <%= link_to "#diff-source", class: "button button--nomargin button--sc hollow" do %>
+ <%= t("versions.tabs.source") %>
+ <% end %>
+
+
+ -
+
+
+ <%= link_to "#diff-preview", class: "button button--nomargin button--sc hollow" do %>
+ <%= t("versions.tabs.preview") %>
+ <% end %>
+
+
+
+
+
+
+
+
+ <%= render :diff_mode_dropdown %>
+ <% diff_data.each do |data| %>
+ <%= attribute(data, :text) %>
+ <% end %>
+
+
+ <%= render :diff_mode_dropdown %>
+ <% diff_data.each do |data| %>
+ <%= attribute(data, :html) %>
+ <% end %>
+
+
+ <%= preview %>
+
+
-
- <% diff_data.each do |data| %>
- <%= attribute(data) %>
- <% end %>
diff --git a/decidim-core/app/cells/decidim/diff_cell.rb b/decidim-core/app/cells/decidim/diff_cell.rb
index 63cc301707da0..b2d75b12aead2 100644
--- a/decidim-core/app/cells/decidim/diff_cell.rb
+++ b/decidim-core/app/cells/decidim/diff_cell.rb
@@ -1,15 +1,15 @@
# frozen_string_literal: true
-require "decidim/diffy_extension"
-
module Decidim
# This cell renders the diff between `:old_data` and `:new_data`.
class DiffCell < Decidim::ViewModel
include Cell::ViewModel::Partial
+ include ::HtmlToPlainText # from the premailer gem
+ include LanguageChooserHelper
include LayoutHelper
- def attribute(data)
- render locals: { data: data }
+ def attribute(data, format)
+ render locals: { data: data, format: format }
end
def diff_unified(data, format)
@@ -22,12 +22,6 @@ def diff_split(data, direction, format)
private
- # Adds a unique ID prefix for the attribute div IDs to avoid duplicate IDs
- # in the DOM.
- def attribute_diff_id(id)
- "#{SecureRandom.uuid}_#{id}"
- end
-
# A PaperTrail::Version.
def current_version
model
@@ -38,6 +32,11 @@ def item
current_version.item
end
+ # preview (if associated item allows it)
+ def preview
+ diff_renderer.preview
+ end
+
# DiffRenderer class for the current_version's item; falls back to `BaseDiffRenderer`.
def diff_renderer_class
if current_version.item_type.deconstantize == "Decidim"
@@ -63,17 +62,32 @@ def diff_data
diff_renderer.diff.values
end
+ def available_locales_for(data)
+ locales = { I18n.locale.to_s => true }
+
+ locales.merge! valid_locale_keys(data[:old_value]) if data[:old_value].is_a?(Hash)
+ locales.merge! valid_locale_keys(data[:new_value]) if data[:new_value].is_a?(Hash)
+
+ locales.filter { |k| I18n.locale_available?(k) }
+ end
+
+ def valid_locale_keys(input)
+ locales = input.transform_values(&:present?)
+ locales.merge!(input["machine_translations"].transform_values(&:present?)) if input["machine_translations"].is_a?(Hash)
+ locales
+ end
+
# Outputs the diff as HTML with inline highlighting of the character
# changes between lines.
#
# Returns an HTML-safe string.
- def output_unified_diff(data, format)
+ def output_unified_diff(data, format, locale)
Diffy::Diff.new(
- data[:old_value].to_s,
- data[:new_value].to_s,
+ old_new_values(data, format, locale)[0],
+ old_new_values(data, format, locale)[1],
allow_empty_diff: false,
include_plus_and_minus_in_html: true
- ).to_s(format)
+ ).to_s(:html)
end
# Outputs the diff as HTML with side-by-side changes between lines.
@@ -81,16 +95,37 @@ def output_unified_diff(data, format)
# The left side represents deletions while the right side represents insertions.
#
# Returns an HTML-safe string.
- def output_split_diff(data, direction, format)
+ def output_split_diff(data, direction, format, locale)
Diffy::SplitDiff.new(
- data[:old_value].to_s,
- data[:new_value].to_s,
+ old_new_values(data, format, locale)[0],
+ old_new_values(data, format, locale)[1],
allow_empty_diff: false,
- format: format,
+ format: :html,
include_plus_and_minus_in_html: true
).send(direction)
end
+ def old_new_values(data, format, locale)
+ original_translations = data[:old_value].try(:filter) { |key, value| value.present? && key != "machine_translations" }
+ [
+ value_from_locale(data[:old_value], format, locale),
+ value_from_locale(data[:new_value], format, locale, original_translations)
+ ]
+ end
+
+ def value_from_locale(value, format, locale, skip_machine_keys = {})
+ text = value.is_a?(Hash) ? find_locale_value(value, locale, skip_machine_keys) : value
+
+ text = text.first if text.is_a?(Array)
+ # return text.to_s if format == :html || text.blank?
+
+ convert_to_text(text.to_s.dup, 100)
+ end
+
+ def find_locale_value(input, locale, skip_machine_keys = {})
+ input.dig(locale).presence || skip_machine_keys[locale].presence || input.dig("machine_translations", locale)
+ end
+
# Gives the option to view HTML unescaped for better user experience.
# Official means created from admin (where rich text editor is enabled).
def show_html_view_dropdown?
diff --git a/decidim-core/app/commands/decidim/update_account.rb b/decidim-core/app/commands/decidim/update_account.rb
index 807af1398ab6e..507f7c5a86109 100644
--- a/decidim-core/app/commands/decidim/update_account.rb
+++ b/decidim-core/app/commands/decidim/update_account.rb
@@ -38,6 +38,7 @@ def update_personal_data
@user.email = @form.email
@user.personal_url = @form.personal_url
@user.about = @form.about
+ @user.time_zone = @form.time_zone
end
def update_avatar
diff --git a/decidim-core/app/controllers/concerns/decidim/use_organization_time_zone.rb b/decidim-core/app/controllers/concerns/decidim/use_organization_time_zone.rb
index 41d7ea3d8b548..13ba69e616c3a 100644
--- a/decidim-core/app/controllers/concerns/decidim/use_organization_time_zone.rb
+++ b/decidim-core/app/controllers/concerns/decidim/use_organization_time_zone.rb
@@ -25,7 +25,7 @@ def use_organization_time_zone(&action)
#
# Returns a String.
def organization_time_zone
- @organization_time_zone ||= current_organization.time_zone
+ @organization_time_zone ||= current_user&.time_zone.presence || current_organization.time_zone
end
end
end
diff --git a/decidim-core/app/forms/decidim/account_form.rb b/decidim-core/app/forms/decidim/account_form.rb
index 4388b2c7e3801..4d5ce6658a40b 100644
--- a/decidim-core/app/forms/decidim/account_form.rb
+++ b/decidim-core/app/forms/decidim/account_form.rb
@@ -18,6 +18,7 @@ class AccountForm < Form
attribute :remove_avatar, Boolean, default: false
attribute :personal_url
attribute :about
+ attribute :time_zone
validates :name, presence: true, format: { with: Decidim::User::REGEXP_NAME }
validates :email, presence: true, "valid_email_2/email": { disposable: true }
@@ -28,6 +29,7 @@ class AccountForm < Form
validates :password, password: { name: :name, email: :email, username: :nickname }, if: -> { password.present? }
validates :password_confirmation, presence: true, if: :password_present
validates :avatar, passthru: { to: Decidim::User }
+ validates :time_zone, time_zone: true, if: -> { time_zone.present? }
validate :unique_email
validate :unique_nickname
diff --git a/decidim-core/app/models/decidim/user.rb b/decidim-core/app/models/decidim/user.rb
index db6734b06c0fc..09e5568faf563 100644
--- a/decidim-core/app/models/decidim/user.rb
+++ b/decidim-core/app/models/decidim/user.rb
@@ -45,6 +45,7 @@ def self.all
validates :tos_agreement, acceptance: true, allow_nil: false, on: :create
validates :tos_agreement, acceptance: true, if: :user_invited?
validates :email, :nickname, uniqueness: { scope: :organization }, unless: -> { deleted? || managed? || nickname.blank? }
+ validates :time_zone, time_zone: true, if: -> { time_zone.present? }
validate :all_roles_are_valid
diff --git a/decidim-core/app/packs/src/decidim/diff_mode_dropdown.js b/decidim-core/app/packs/src/decidim/diff_mode_dropdown.js
index 04ac309f50165..8fd7e5f87d213 100644
--- a/decidim-core/app/packs/src/decidim/diff_mode_dropdown.js
+++ b/decidim-core/app/packs/src/decidim/diff_mode_dropdown.js
@@ -3,8 +3,10 @@ $(() => {
$(document).on("click", ".diff-view-by a.diff-view-mode", (event) => {
event.preventDefault();
- const $target = $(event.target)
- let type = "escaped";
+ const $target = $(event.target);
+ const $container = $target.closest(".tabs-panel");
+ const $unified = $container.find(".diff_view_unified")
+ const $split = $container.find(".diff_view_split")
const $selected = $target.parents(".is-dropdown-submenu-parent").find("#diff-view-selected");
if ($selected.text().trim() === $target.text().trim()) {
return;
@@ -12,38 +14,13 @@ $(() => {
$selected.text($target.text());
- if ($target.attr("id") === "diff-view-unified") {
- if ($(".row.diff_view_split_escaped").hasClass("hide")) {
- type = "unescaped";
- }
-
- $allDiffViews.addClass("hide");
- $(`.row.diff_view_unified_${type}`).removeClass("hide");
- }
- if ($target.attr("id") === "diff-view-split") {
- if ($(".row.diff_view_unified_escaped").hasClass("hide")) {
- type = "unescaped";
- }
-
- $allDiffViews.addClass("hide");
- $(`.row.diff_view_split_${type}`).removeClass("hide");
- }
- })
-
- $(document).on("click", ".diff-view-by a.diff-view-html", (event) => {
- event.preventDefault();
- const $target = $(event.target);
- $target.parents(".is-dropdown-submenu-parent").find("#diff-view-html-selected").text($target.text());
- const $visibleDiffViewsId = $allDiffViews.not(".hide").first().attr("id").split("_").slice(1, -1).join("_");
- const $visibleDiffViews = $allDiffViews.filter(`[id*=${$visibleDiffViewsId}]`)
-
- if ($target.attr("id") === "diff-view-escaped-html") {
- $visibleDiffViews.filter("[id$=_unescaped]").addClass("hide");
- $visibleDiffViews.filter("[id$=_escaped]").removeClass("hide");
+ if ($target.hasClass("diff-view-unified")) {
+ $split.addClass("hide");
+ $unified.removeClass("hide");
}
- if ($target.attr("id") === "diff-view-unescaped-html") {
- $visibleDiffViews.filter("[id$=_escaped]").addClass("hide");
- $visibleDiffViews.filter("[id$=_unescaped]").removeClass("hide");
+ if ($target.hasClass("diff-view-split")) {
+ $unified.addClass("hide");
+ $split.removeClass("hide");
}
})
-});
+});
\ No newline at end of file
diff --git a/decidim-core/app/packs/stylesheets/decidim/modules/_versions.scss b/decidim-core/app/packs/stylesheets/decidim/modules/_versions.scss
index 1219712295e9a..308feba12fc21 100644
--- a/decidim-core/app/packs/stylesheets/decidim/modules/_versions.scss
+++ b/decidim-core/app/packs/stylesheets/decidim/modules/_versions.scss
@@ -1,3 +1,55 @@
+.versions-diff{
+ .versions-selector{
+ border-bottom: $border;
+ padding-bottom: $global-margin * .5;
+ color: $muted;
+ font-size: rem-calc(19);
+ display: flex;
+ align-items: center;
+
+ //Override foundation
+ .tabs{
+ width: 100%;
+
+ @include flexgap(.5rem);
+ }
+ }
+
+ //Override foundation
+ .tabs,
+ .tabs-content{
+ background: transparent;
+ }
+
+ //Override foundation
+ .tabs-title{
+ > a{
+ padding: 0;
+
+ &:hover{
+ background: transparent;
+ }
+ }
+
+ > a[aria-selected='true']{
+ background: transparent;
+ }
+
+ &:not(.is-active) .button{
+ opacity: .4;
+ }
+ }
+
+ .diff-preview{
+ margin: 1em 0;
+
+ .heading3{
+ font-size: 1.5em;
+ margin-bottom: 1em;
+ }
+ }
+}
+
.diff-direction-label{
display: block;
font-size: 85%;
@@ -13,7 +65,6 @@
padding: 0;
width: 100%;
background: transparent;
- min-height: 2.7rem;
}
del,
@@ -28,19 +79,23 @@
text-decoration: none;
}
- del strong{
+ .del strong,
+ .del strong *{
font-weight: normal;
- background: scale-color($color-removal, $lightness: -8%);
+ background: scale-color($color-removal, $lightness: -10%);
+ display: inline;
}
- ins strong{
+ .ins strong,
+ .ins strong *{
font-weight: normal;
- background: scale-color($color-addition, $lightness: -8%);
+ background: scale-color($color-addition, $lightness: -10%);
+ display: inline;
}
li{
position: relative;
- padding: .5rem 1rem .5rem 1.5rem;
+ padding: .1rem 1rem .1rem 1.5rem;
margin: 0;
&.ins,
@@ -48,18 +103,18 @@
.symbol{
position: absolute;
left: .5rem;
- top: .5rem;
+ top: .1rem;
width: 1rem;
}
}
&.ins{
- background: $color-addition;
+ background: scale-color($color-addition, $lightness: 30%, $saturation: 30%);
color: scale-color($color-addition, $lightness: -75%, $saturation: -75%);
}
&.del{
- background: $color-removal;
+ background: scale-color($color-removal, $lightness: 30%, $saturation: 30%);
color: scale-color($color-removal, $lightness: -75%, $saturation: -75%);
}
@@ -71,4 +126,4 @@
background: none repeat scroll 0 0 gray;
}
}
-}
+}
\ No newline at end of file
diff --git a/decidim-core/app/services/decidim/base_diff_renderer.rb b/decidim-core/app/services/decidim/base_diff_renderer.rb
index cc3997410e53b..049fc068e43d8 100644
--- a/decidim-core/app/services/decidim/base_diff_renderer.rb
+++ b/decidim-core/app/services/decidim/base_diff_renderer.rb
@@ -28,6 +28,11 @@ def diff
end
end
+ # implement if item can be previewed
+ def preview
+ nil
+ end
+
private
attr_reader :version
diff --git a/decidim-core/app/views/decidim/account/show.html.erb b/decidim-core/app/views/decidim/account/show.html.erb
index 14207f164c0cd..e1e7abcc5cd37 100644
--- a/decidim-core/app/views/decidim/account/show.html.erb
+++ b/decidim-core/app/views/decidim/account/show.html.erb
@@ -25,6 +25,9 @@
) %>
<%= t(".available_locales_helper") %>
+ <%= f.time_zone_select :time_zone, nil, default: organization_time_zone %>
+
<%= t(".time_zone_helper") %>
+
<% if @account.errors[:password].any? || @account.errors[:password_confirmation].any? %>
<%= render partial: "password_fields", locals: { form: f } %>
<% else %>
diff --git a/decidim-core/config/locales/en.yml b/decidim-core/config/locales/en.yml
index 6362fdad07288..35c100747641a 100644
--- a/decidim-core/config/locales/en.yml
+++ b/decidim-core/config/locales/en.yml
@@ -139,6 +139,7 @@ en:
show:
available_locales_helper: Choose the language you want to use to browse and receive notifications in Decidim
change_password: Change password
+ time_zone_helper: Use your personal time zone to display dates in your local time when you are logged in
update_account: Update account
update:
error: There was a problem updating your account.
@@ -1733,6 +1734,10 @@ en:
short: "%d/%m/%Y %H:%M"
time_of_day: "%H:%M"
versions:
+ tabs:
+ text: Text diff
+ source: Source diff
+ preview: Preview
directions:
left: Deletions
right: Additions
diff --git a/decidim-core/db/migrate/20220823094517_add_time_zone_to_users.rb b/decidim-core/db/migrate/20220823094517_add_time_zone_to_users.rb
new file mode 100644
index 0000000000000..a3728c50a047a
--- /dev/null
+++ b/decidim-core/db/migrate/20220823094517_add_time_zone_to_users.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddTimeZoneToUsers < ActiveRecord::Migration[6.0]
+ def change
+ add_column :decidim_users, :time_zone, :string
+ end
+end
diff --git a/decidim-core/lib/decidim/diffy_extension.rb b/decidim-core/lib/decidim/diffy_extension.rb
deleted file mode 100644
index 9f29facda60a0..0000000000000
--- a/decidim-core/lib/decidim/diffy_extension.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-# frozen_string_literal: true
-
-module Decidim
- # Extending Diffy gem to accomodate the needs of app/cells/decidim/diff_cell.rb
- module DiffyExtension
- # HtmlFormatter that returns basic html output (no inline highlighting)
- # and does not escape HTML tags.
- class UnescapedHtmlFormatter < Diffy::HtmlFormatter
- # We exclude the tags `del` and `ins` so the diffy styling does not apply.
- TAGS = (UserInputScrubber.new.tags.to_a - %w(del ins)).freeze
-
- def to_s
- str = wrap_lines(@diff.map { |line| wrap_line(line) })
- ActionView::Base.new(ActionView::LookupContext.new(nil), {}, nil).sanitize(str, tags: TAGS)
- end
- end
-
- # Adding a new method to Diffy::Format so we can pass the
- # `:unescaped_html` option when calling Diffy::Diff#to_s.
- Diffy::Format.module_eval do
- def unescaped_html
- UnescapedHtmlFormatter.new(self, options).to_s
- end
- end
-
- # The private "split" method SplitDiff needs to be overriden to take into
- # account the new :unescaped_html format, and the fact that the tags
- #
are not there anymore
- Diffy::SplitDiff.module_eval do
- private
-
- def split
- return [split_left, split_right] unless @format == :unescaped_html
-
- [unescaped_split_left, unescaped_split_right]
- end
-
- def unescaped_split_left
- @diff.gsub(%r{([\s\S]*?)}, "")
- end
-
- def unescaped_split_right
- @diff.gsub(%r{([\s\S]*?)}, "")
- end
- end
- end
-end
diff --git a/decidim-core/spec/commands/decidim/update_account_spec.rb b/decidim-core/spec/commands/decidim/update_account_spec.rb
index 889e259210dbe..895024da94790 100644
--- a/decidim-core/spec/commands/decidim/update_account_spec.rb
+++ b/decidim-core/spec/commands/decidim/update_account_spec.rb
@@ -6,6 +6,7 @@ module Decidim
describe UpdateAccount do
let(:command) { described_class.new(user, form) }
let(:user) { create(:user, :confirmed) }
+ let(:time_zone) { "UTC" }
let(:data) do
{
name: user.name,
@@ -17,7 +18,8 @@ module Decidim
remove_avatar: nil,
personal_url: "https://example.org",
about: "This is a description of me",
- locale: "es"
+ locale: "es",
+ time_zone: time_zone
}
end
@@ -32,7 +34,8 @@ module Decidim
remove_avatar: data[:remove_avatar],
personal_url: data[:personal_url],
about: data[:about],
- locale: data[:locale]
+ locale: data[:locale],
+ time_zone: data[:time_zone]
).with_context(current_organization: user.organization, current_user: user)
end
@@ -47,6 +50,14 @@ module Decidim
expect { command.call }.to broadcast(:invalid)
expect(user.reload.name).to eq(old_name)
end
+
+ context "when timezone is invalid" do
+ let(:time_zone) { "giberish" }
+
+ it "returns invalid" do
+ expect { command.call }.to broadcast(:invalid)
+ end
+ end
end
context "when valid" do
@@ -77,6 +88,22 @@ module Decidim
expect(user.reload.locale).to eq("es")
end
+ context "when timezone is defined" do
+ it "updates the time zone" do
+ expect { command.call }.to broadcast(:ok)
+ expect(user.reload.time_zone).to eq("UTC")
+ end
+ end
+
+ context "when timezone is not defined" do
+ let(:time_zone) { "" }
+
+ it "updates the time zone" do
+ expect { command.call }.to broadcast(:ok)
+ expect(user.reload.time_zone).to eq("")
+ end
+ end
+
describe "updating the email" do
before do
form.email = "new@email.com"
diff --git a/decidim-core/spec/controllers/concerns/use_organization_time_zone_spec.rb b/decidim-core/spec/controllers/concerns/use_organization_time_zone_spec.rb
index d548998a53bd2..20aab8ae23181 100644
--- a/decidim-core/spec/controllers/concerns/use_organization_time_zone_spec.rb
+++ b/decidim-core/spec/controllers/concerns/use_organization_time_zone_spec.rb
@@ -7,9 +7,12 @@ module Decidim
let(:utc_time_zone) { "UTC" }
let(:alt_time_zone) { "Hawaii" }
let(:organization) { create(:organization, time_zone: time_zone) }
+ let(:user) { create :user, :confirmed, organization: organization, time_zone: user_time_zone }
+ let(:user_time_zone) { "" }
before do
request.env["decidim.current_organization"] = organization
+ allow(controller).to receive(:current_user) { user }
end
context "when time zone is UTC" do
@@ -85,5 +88,42 @@ module Decidim
end
end
end
+
+ context "when time zone is defined by the user" do
+ let(:time_zone) { utc_time_zone }
+ let(:user_time_zone) { "London" }
+
+ it "controller uses London" do
+ expect(controller.organization_time_zone).to eq("London")
+ end
+
+ it "Time uses UTC zone within the controller scope" do
+ controller.use_organization_time_zone do
+ expect(Time.zone.name).to eq("London")
+ end
+ end
+
+ it "Time uses Rails timezone outside the controller scope" do
+ expect(Time.zone.name).to eq("UTC")
+ end
+ end
+
+ context "when user's time zone in not present" do
+ let(:time_zone) { utc_time_zone }
+ let(:user_time_zone) { "" }
+
+ it "controller uses time zone of organization" do
+ expect(controller.organization_time_zone).to eq(utc_time_zone)
+ end
+ end
+
+ context "when user is not present" do
+ let(:time_zone) { utc_time_zone }
+ let(:user) { nil }
+
+ it "controller uses time zone of organization" do
+ expect(controller.organization_time_zone).to eq(utc_time_zone)
+ end
+ end
end
end
diff --git a/decidim-core/spec/forms/account_form_spec.rb b/decidim-core/spec/forms/account_form_spec.rb
index a10d8e8d3dc2c..79e3c57595085 100644
--- a/decidim-core/spec/forms/account_form_spec.rb
+++ b/decidim-core/spec/forms/account_form_spec.rb
@@ -15,7 +15,8 @@ module Decidim
remove_avatar: remove_avatar,
personal_url: personal_url,
about: about,
- locale: "es"
+ locale: "es",
+ time_zone: time_zone
).with_context(
current_organization: organization,
current_user: user
@@ -34,6 +35,7 @@ module Decidim
let(:remove_avatar) { false }
let(:personal_url) { "http://example.org" }
let(:about) { "This is a description about me" }
+ let(:time_zone) { "UTC" }
context "with correct data" do
it "is valid" do
@@ -172,5 +174,23 @@ module Decidim
end
end
end
+
+ describe "time_zone" do
+ context "when an empty time_zone" do
+ let(:time_zone) { "" }
+
+ it "is invalid" do
+ expect(subject).to be_valid
+ end
+ end
+
+ context "when time_zone has more 255 chars" do
+ let(:time_zone) { [*("A".."Z")].sample(256).join }
+
+ it "is invalid" do
+ expect(subject).not_to be_valid
+ end
+ end
+ end
end
end
diff --git a/decidim-proposals/app/helpers/decidim/proposals/application_helper.rb b/decidim-proposals/app/helpers/decidim/proposals/application_helper.rb
index 1c3badce0dca3..c21dac4cf9d0c 100644
--- a/decidim-proposals/app/helpers/decidim/proposals/application_helper.rb
+++ b/decidim-proposals/app/helpers/decidim/proposals/application_helper.rb
@@ -8,6 +8,7 @@ module ApplicationHelper
include Decidim::Comments::CommentsHelper
include PaginateHelper
include ProposalVotesHelper
+ include ProposalPresenterHelper
include ::Decidim::EndorsableHelper
include ::Decidim::FollowableHelper
include Decidim::MapHelper
diff --git a/decidim-proposals/app/helpers/decidim/proposals/proposal_presenter_helper.rb b/decidim-proposals/app/helpers/decidim/proposals/proposal_presenter_helper.rb
new file mode 100644
index 0000000000000..a5f34f319fbd0
--- /dev/null
+++ b/decidim-proposals/app/helpers/decidim/proposals/proposal_presenter_helper.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module Decidim
+ module Proposals
+ module ProposalPresenterHelper
+ include Decidim::ApplicationHelper
+
+ def not_from_collaborative_draft(proposal)
+ proposal.linked_resources(:proposals, "created_from_collaborative_draft").empty?
+ end
+
+ def not_from_participatory_text(proposal)
+ proposal.participatory_text_level.nil?
+ end
+
+ # If the proposal is official or the rich text editor is enabled on the
+ # frontend, the proposal body is considered as safe content; that's unless
+ # the proposal comes from a collaborative_draft or a participatory_text.
+ def safe_content?
+ rich_text_editor_in_public_views? && not_from_collaborative_draft(@proposal) ||
+ (@proposal.official? || @proposal.official_meeting?) && not_from_participatory_text(@proposal)
+ end
+
+ def render_proposal_title(proposal)
+ Decidim::Proposals::ProposalPresenter.new(proposal).title(links: true, html_escape: true)
+ end
+
+ # If the content is safe, HTML tags are sanitized, otherwise, they are stripped.
+ def render_proposal_body(proposal)
+ Decidim::ContentProcessor.render(render_sanitized_content(proposal, :body), "div")
+ end
+ end
+ end
+end
diff --git a/decidim-proposals/app/services/decidim/proposals/diff_renderer.rb b/decidim-proposals/app/services/decidim/proposals/diff_renderer.rb
index aa607cf606bd1..9ca60f57dddfc 100644
--- a/decidim-proposals/app/services/decidim/proposals/diff_renderer.rb
+++ b/decidim-proposals/app/services/decidim/proposals/diff_renderer.rb
@@ -3,6 +3,19 @@
module Decidim
module Proposals
class DiffRenderer < BaseDiffRenderer
+ include ActionView::Helpers::TextHelper
+ include ActionView::Helpers::TagHelper
+ include ProposalPresenterHelper
+ include SanitizeHelper
+
+ delegate :organization, to: :proposal, prefix: :current
+
+ def preview
+ title = content_tag(:h3, render_proposal_title(proposal), class: "heading3")
+ body = content_tag(:div, render_proposal_body(proposal), class: "body")
+ content_tag(:div, "#{title}#{body}".html_safe, class: "diff-preview diff-proposal")
+ end
+
private
# Lists which attributes will be diffable and how they should be rendered.