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 %> + + + + +
+
+ <%= text_field_tag :url, '', + "aria-describedby": "related_content_help_text", + placeholder: t("related_content.placeholder", url: setting['url']) %> + + <%= hidden_field_tag :relationable_klass, relationable.class.name %> + <%= hidden_field_tag :relationable_id, relationable.id %> +
+ +
+ <%= submit_tag t("related_content.submit"), class: "button" %> +
+
+ <% 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 @@ + 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 @@ + + +<%= 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