diff --git a/app/assets/stylesheets/layout.scss b/app/assets/stylesheets/layout.scss
index 665e6b532..d8763591a 100644
--- a/app/assets/stylesheets/layout.scss
+++ b/app/assets/stylesheets/layout.scss
@@ -20,6 +20,7 @@
// 18. Banners
// 19. Recommended Section Home
// 20. Documents
+// 21. Related content
//
// 01. Global styles
@@ -2391,3 +2392,56 @@ table {
background: #fafafa;
border-bottom: 1px solid #eee;
}
+
+// 21. Related content
+// -------------------
+
+.related-content {
+ border-top: 1px solid $border;
+
+ h2 {
+ font-size: rem-calc(24);
+
+ span {
+ color: #4f4f4f;
+ font-weight: normal;
+ }
+ }
+}
+
+.add-related-content {
+ display: block;
+
+ @include breakpoint(medium) {
+ float: right;
+ }
+}
+
+.related-content-list {
+ list-style-type: none;
+ margin-left: 0;
+
+ li {
+ border-bottom: 1px solid $border;
+ padding: $line-height / 4;
+
+ &:first-child {
+ border-top: 1px solid $border;
+ }
+ }
+
+ h3 {
+ font-size: $base-font-size;
+ font-weight: normal;
+ }
+
+ span {
+ color: #4f4f4f;
+ font-size: rem-calc(12);
+ text-transform: uppercase;
+ }
+
+ .flag {
+ margin-top: $line-height / 2;
+ }
+}
diff --git a/app/controllers/management/proposals_controller.rb b/app/controllers/management/proposals_controller.rb
index 54616c05b..2101996af 100644
--- a/app/controllers/management/proposals_controller.rb
+++ b/app/controllers/management/proposals_controller.rb
@@ -14,6 +14,8 @@ class Management::ProposalsController < Management::BaseController
def show
super
@notifications = @proposal.notifications
+ @related_contents = Kaminari.paginate_array(@proposal.relationed_contents).page(params[:page]).per(5)
+
redirect_to management_proposal_path(@proposal), status: :moved_permanently if request.path != management_proposal_path(@proposal)
end
diff --git a/app/controllers/proposals_controller.rb b/app/controllers/proposals_controller.rb
index bfd8baeb0..1f065db06 100644
--- a/app/controllers/proposals_controller.rb
+++ b/app/controllers/proposals_controller.rb
@@ -22,6 +22,8 @@ class ProposalsController < ApplicationController
def show
super
@notifications = @proposal.notifications
+ @related_contents = Kaminari.paginate_array(@proposal.relationed_contents).page(params[:page]).per(5)
+
redirect_to proposal_path(@proposal), status: :moved_permanently if request.path != proposal_path(@proposal)
end
diff --git a/app/controllers/related_contents_controller.rb b/app/controllers/related_contents_controller.rb
new file mode 100644
index 000000000..06bfaadc7
--- /dev/null
+++ b/app/controllers/related_contents_controller.rb
@@ -0,0 +1,42 @@
+class RelatedContentsController < ApplicationController
+ VALID_URL = /#{Setting['url']}\/.*\/.*/
+
+ skip_authorization_check
+
+ def create
+ if relationable_object && related_object
+ @relationable.relate_content(@related)
+
+ flash[:success] = t('related_content.success')
+ else
+ flash[:error] = t('related_content.error', url: Setting['url'])
+ end
+
+ redirect_to @relationable
+ end
+
+ private
+
+ def valid_url?
+ params[:url].match(VALID_URL)
+ end
+
+ def relationable_object
+ @relationable = (params[:relationable_klass].singularize.camelize.constantize).find_by_id(params[:relationable_id])
+ end
+
+ def related_object
+ begin
+ if valid_url?
+ url = params[:url]
+
+ related_klass = url.match(/\/(#{RelatedContent::RELATIONABLE_MODELS.join("|")})\//)[0].gsub("/", "")
+ related_id = url.match(/\/[0-9]+/)[0].gsub("/", "")
+
+ @related = (related_klass.singularize.camelize.constantize).find_by_id(related_id)
+ end
+ rescue
+ nil
+ end
+ end
+end
diff --git a/app/models/related_content.rb b/app/models/related_content.rb
index ab72aefe3..fa41a57ad 100644
--- a/app/models/related_content.rb
+++ b/app/models/related_content.rb
@@ -1,5 +1,6 @@
class RelatedContent < ActiveRecord::Base
RELATED_CONTENTS_REPORT_THRESHOLD = Setting['related_contents_report_threshold'].to_i
+ RELATIONABLE_MODELS = %w{proposals debates}
belongs_to :parent_relationable, polymorphic: true
belongs_to :child_relationable, polymorphic: true
diff --git a/app/views/proposals/show.html.erb b/app/views/proposals/show.html.erb
index aa8b0aee3..d028a3f7f 100644
--- a/app/views/proposals/show.html.erb
+++ b/app/views/proposals/show.html.erb
@@ -108,6 +108,8 @@
<%= render 'shared/geozone', geozonable: @proposal %>
+ <%= render 'relationable/related_content', relationable: @proposal %>
+
<%= render 'proposals/actions', proposal: @proposal %>
diff --git a/app/views/relationable/_form.html.erb b/app/views/relationable/_form.html.erb
new file mode 100644
index 000000000..930c839a2
--- /dev/null
+++ b/app/views/relationable/_form.html.erb
@@ -0,0 +1,22 @@
+<%= form_tag related_contents_path, method: :post, id: "related_content", class: "hide", "data-toggler": ".hide" do %>
+
+
+
+ <%= t("related_content.help", models: t('related_content.content_title').values.to_sentence, org: setting['org_name']) %>
+
+
+
+ <% end %>
diff --git a/app/views/relationable/_related_content.html.erb b/app/views/relationable/_related_content.html.erb
new file mode 100644
index 000000000..19435fec2
--- /dev/null
+++ b/app/views/relationable/_related_content.html.erb
@@ -0,0 +1,18 @@
+
+
+
+
+ <%= render 'relationable/form', relationable: relationable %>
+
+ <%= render 'relationable/related_list', relationable: relationable %>
+
+
diff --git a/app/views/relationable/_related_list.html.erb b/app/views/relationable/_related_list.html.erb
new file mode 100644
index 000000000..08fc931df
--- /dev/null
+++ b/app/views/relationable/_related_list.html.erb
@@ -0,0 +1,16 @@
+
+ <% @related_contents.each do |related| %>
+ -
+
+
+
+
+ <%= t("related_content.content_title.#{related.class.name.downcase}") %>
+
+ <%= link_to related.title, eval("#{related.class.name.downcase}_path(related)") %>
+
+
+ <% end %>
+
+
+<%= paginate @related_contents %>
diff --git a/config/locales/en/general.yml b/config/locales/en/general.yml
index ad5cc30e3..f7dd1e6cd 100644
--- a/config/locales/en/general.yml
+++ b/config/locales/en/general.yml
@@ -810,4 +810,16 @@ en:
user_permission_votes: Participate on final voting
invisible_captcha:
sentence_for_humans: "If you are human, ignore this field"
- timestamp_error_message: "Sorry, that was too quick! Please resubmit."
\ No newline at end of file
+ timestamp_error_message: "Sorry, that was too quick! Please resubmit."
+ related_content:
+ title: "Related content"
+ add: "Add related content"
+ label: "Link to related content"
+ placeholder: "%{url}"
+ help: "You can add links of %{models} inside of %{org}."
+ submit: "Add"
+ error: "Link not valid. Remember to start with %{url}."
+ success: "You added a new related content"
+ content_title:
+ proposal: "Proposal"
+ debate: "Debate"
diff --git a/config/locales/es/general.yml b/config/locales/es/general.yml
index e455e1770..f444c7423 100644
--- a/config/locales/es/general.yml
+++ b/config/locales/es/general.yml
@@ -808,3 +808,15 @@ es:
invisible_captcha:
sentence_for_humans: "Si eres humano, por favor ignora este campo"
timestamp_error_message: "Eso ha sido demasiado rápido. Por favor, reenvía el formulario."
+ related_content:
+ title: "Contenido relacionado"
+ add: "Añadir contenido relacionado"
+ label: "Enlace a contenido relacionado"
+ placeholder: "%{url}"
+ help: "Puedes introducir cualquier enlace de %{models} que esté dentro de %{org}."
+ submit: "Añadir"
+ error: "Enlace no válido. Recuerda que debe empezar por %{url}."
+ success: "Has añadido un nuevo contenido relacionado"
+ content_title:
+ proposal: "Propuesta"
+ debate: "Debate"
diff --git a/config/routes.rb b/config/routes.rb
index 31d811293..429e5a862 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -462,6 +462,8 @@ Rails.application.routes.draw do
root to: "dashboard#index"
end
+ resources :related_contents, only: [:create]
+
# GraphQL
get '/graphql', to: 'graphql#query'
post '/graphql', to: 'graphql#query'
diff --git a/spec/features/proposals_spec.rb b/spec/features/proposals_spec.rb
index 12d4b7bce..36d5015b5 100644
--- a/spec/features/proposals_spec.rb
+++ b/spec/features/proposals_spec.rb
@@ -142,6 +142,73 @@ feature 'Proposals' do
visit proposal_path(proposal)
expect(page).not_to have_content "Access the community"
end
+
+ scenario 'related contents are listed' do
+ proposal1 = create(:proposal)
+ proposal2 = create(:proposal)
+ related_content = create(:related_content, parent_relationable: proposal1, child_relationable: proposal2)
+
+ visit proposal_path(proposal1)
+ within("#related-content-list") do
+ expect(page).to have_content(proposal2.title)
+ end
+
+ visit proposal_path(proposal2)
+ within("#related-content-list") do
+ expect(page).to have_content(proposal1.title)
+ end
+ end
+
+ scenario 'related contents can be added' do
+ proposal1 = create(:proposal)
+ proposal2 = create(:proposal)
+ debate1 = create(:debate)
+
+ visit proposal_path(proposal1)
+
+ expect(page).to have_selector('#related_content', visible: false)
+ click_on("Add related content")
+ expect(page).to have_selector('#related_content', visible: true)
+
+ within("#related_content") do
+ fill_in 'url', with: "#{Setting['url']}/proposals/#{proposal2.to_param}"
+ click_button "Add"
+ end
+
+ within("#related-content-list") do
+ expect(page).to have_content(proposal2.title)
+ end
+
+ visit proposal_path(proposal2)
+
+ within("#related-content-list") do
+ expect(page).to have_content(proposal1.title)
+ end
+
+ within("#related_content") do
+ fill_in 'url', with: "#{Setting['url']}/debates/#{debate1.to_param}"
+ click_button "Add"
+ end
+
+ within("#related-content-list") do
+ expect(page).to have_content(debate1.title)
+ end
+ end
+
+ scenario 'if related content URL is invalid returns error' do
+ proposal1 = create(:proposal)
+
+ visit proposal_path(proposal1)
+
+ click_on("Add related content")
+
+ within("#related_content") do
+ fill_in 'url', with: "http://invalidurl.com"
+ click_button "Add"
+ end
+
+ expect(page).to have_content("Link not valid. Remember to start with #{Setting[:url]}.")
+ end
end
context "Embedded video" do