diff --git a/.rubocop.yml b/.rubocop.yml index 36b92b2f7f0..07a45ca67b6 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,8 +1,5 @@ inherit_from: .rubocop_basic.yml -Metrics/LineLength: - Max: 100 - Bundler/DuplicatedGem: Enabled: true diff --git a/.rubocop_basic.yml b/.rubocop_basic.yml index 7d7074e9f09..cebf55084a3 100644 --- a/.rubocop_basic.yml +++ b/.rubocop_basic.yml @@ -18,6 +18,9 @@ AllCops: Layout/IndentationConsistency: EnforcedStyle: rails +Layout/IndentationWidth: + Enabled: true + Layout/EndOfLine: EnforcedStyle: lf @@ -27,5 +30,11 @@ Layout/TrailingBlankLines: Layout/TrailingWhitespace: Enabled: true +Metrics/LineLength: + Max: 100 + RSpec/NotToNot: Enabled: true + +Style/StringLiterals: + EnforcedStyle: double_quotes diff --git a/CHANGELOG.md b/CHANGELOG.md index 955ffc300d2..3121f26705b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,30 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html) +## [0.18.1](https://github.com/consul/consul/tree/v0.18.1) (2019-01-17) + +### Added + +- **Legislation:** Legislation process homepage phase [\#3188](https://github.com/consul/consul/pull/3188) +- **Legislation:** Show documents on processes proposals phase [\#3136](https://github.com/consul/consul/pull/3136) +- **Maintenance-Refactorings:** Remove semicolons from controllers [\#3160](https://github.com/consul/consul/pull/3160) +- **Maintenance-Refactorings:** Remove before action not used [\#3167](https://github.com/consul/consul/pull/3167) +- **Maintenance-Rubocop:** Enable double quotes rubocop rule [\#3175](https://github.com/consul/consul/pull/3175) +- **Maintenance-Rubocop:** Enable line length rubocop rule [\#3165](https://github.com/consul/consul/pull/3165) +- **Maintenance-Rubocop:** Add rubocop rule to indent private methods [\#3134](https://github.com/consul/consul/pull/3134) + +### Changed + +- **Admin:** Improve CRUD budgets and content blocks [\#3173](https://github.com/consul/consul/pull/3173) +- **Design/UX:** new CRUD budgets, content blocks and heading map [\#3150](https://github.com/consul/consul/pull/3150) +- **Design/UX:** Processes key dates [\#3137](https://github.com/consul/consul/pull/3137) + +### Fixed + +- **Admin:** checks for deleted proposals [\#3154](https://github.com/consul/consul/pull/3154) +- **Admin:** Add default order for admin budget investments list [\#3151](https://github.com/consul/consul/pull/3151) +- **Budgets:** Bug Management Cannot create Budget Investment without a map location [\#3133](https://github.com/consul/consul/pull/3133) + ## [0.18.0](https://github.com/consul/consul/compare/v0.17...v0.18) (2018-12-27) ### Added @@ -47,6 +71,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - **Admin:** Improve visualization for small resolution [\#3025](https://github.com/consul/consul/pull/3025) - **Admin:** Budgets admin [\#3012](https://github.com/consul/consul/pull/3012) - **Budgets:** Budget investments social share [\#3053](https://github.com/consul/consul/pull/3053) +- **Design/UX:** Documents title [\#3131](https://github.com/consul/consul/pull/3131) - **Design/UX:** Proposal create question [\#3122](https://github.com/consul/consul/pull/3122) - **Design/UX:** Budget investments price explanation [\#3121](https://github.com/consul/consul/pull/3121) - **Design/UX:** Change CRUD for budget groups and headings [\#3106](https://github.com/consul/consul/pull/3106) diff --git a/app/assets/javascripts/map.js.coffee b/app/assets/javascripts/map.js.coffee index 241093d759f..562b035e00f 100644 --- a/app/assets/javascripts/map.js.coffee +++ b/app/assets/javascripts/map.js.coffee @@ -74,7 +74,7 @@ App.Map = openMarkerPopup = (e) -> marker = e.target - $.ajax 'investments/' + marker.options['id'] + '/json_data', + $.ajax '/investments/' + marker.options['id'] + '/json_data', type: 'GET' dataType: 'json' success: (data) -> diff --git a/app/assets/stylesheets/layout.scss b/app/assets/stylesheets/layout.scss index 14ec9e35450..88738c07257 100644 --- a/app/assets/stylesheets/layout.scss +++ b/app/assets/stylesheets/layout.scss @@ -851,7 +851,6 @@ footer { .categories a, .geozone a, .sidebar-links a, -.sidebar-map a, .tags span { background: #ececec; border-radius: rem-calc(6); @@ -882,7 +881,7 @@ footer { .sidebar-map { .map { - z-index: -1; + z-index: 0; } } diff --git a/app/assets/stylesheets/legislation_process.scss b/app/assets/stylesheets/legislation_process.scss index 0bfff8b239f..990581236c3 100644 --- a/app/assets/stylesheets/legislation_process.scss +++ b/app/assets/stylesheets/legislation_process.scss @@ -44,69 +44,100 @@ // 02. Legislation process navigation // ---------------------------------- -.legislation-process-categories { - position: relative; +.legislation-process-list { + border-bottom: 1px solid $border; +} - .legislation-process-list { - border-bottom: 1px solid $medium-gray; - margin: 0 rem-calc(16) rem-calc(16); +.key-dates { + list-style-type: none; + margin: 0 rem-calc(-10); - ul { - list-style: none; - margin: 0 auto; - margin-bottom: 0; - padding-left: 0; - } + @include breakpoint(large) { + margin: 0; + } - li { - border-bottom: 2px solid transparent; - cursor: pointer; - display: inline-block; - margin-bottom: $line-height; - margin-right: $line-height; - transition: all 0.4s; + li { + border: 1px solid $border; + display: block; + margin: rem-calc(-1) 0; + position: relative; - @include breakpoint(medium) { - margin-bottom: 0; + @include breakpoint(large down) { + + &::after { + content: '\63'; + font-family: "icons" !important; + font-size: rem-calc(24); + pointer-events: none; + position: absolute; + right: 12px; + top: 12px; } + } - &:hover, - &:active, - &:focus { - border-bottom: 2px solid $brand; + @include breakpoint(large) { + background: #fafafa; + display: inline-block; + border-bottom: 0; + border-top-left-radius: rem-calc(6); + border-top-right-radius: rem-calc(6); + margin-bottom: 0; + margin-right: $line-height / 4; + margin-top: 0; + + &:hover:not(.is-active) { + background: $highlight; } - a, - h4 { - display: block; - color: #6d6d6d; - margin-bottom: 0; + &::after { + content: ''; } } a { - &:hover, - &:active { + display: block; + padding: $line-height / 4 $line-height / 2; + + @include breakpoint(large) { + display: inline-block; + } + + &:hover { text-decoration: none; } - p { + h4 { margin-bottom: 0; - - @include breakpoint(medium) { - margin-bottom: rem-calc(16); - } } } + } - .is-active { - border-bottom: 2px solid $brand; + span { + color: $text-medium; + font-size: $small-font-size; + } - a, - h4 { - color: $brand; + .is-active { + background: $highlight; + position: relative; + + @include breakpoint(large) { + background: none; + border: 1px solid $border; + border-bottom: 0; + + &::after { + border-bottom: 1px solid #fefefe; + bottom: -1px; + left: 0; + position: absolute; + width: 100%; } } + + &::after { + content: ''; + } } } diff --git a/app/controllers/admin/budget_headings_controller.rb b/app/controllers/admin/budget_headings_controller.rb index 8143f369013..98ec7a2616a 100644 --- a/app/controllers/admin/budget_headings_controller.rb +++ b/app/controllers/admin/budget_headings_controller.rb @@ -62,7 +62,8 @@ def headings_index end def budget_heading_params - params.require(:budget_heading).permit(:name, :price, :population, :allow_custom_content, :latitude, :longitude) + params.require(:budget_heading).permit(:name, :price, :population, :allow_custom_content, + :latitude, :longitude) end end diff --git a/app/controllers/admin/budget_investments_controller.rb b/app/controllers/admin/budget_investments_controller.rb index 6797f63d0a6..60ab11d7c59 100644 --- a/app/controllers/admin/budget_investments_controller.rb +++ b/app/controllers/admin/budget_investments_controller.rb @@ -77,7 +77,7 @@ def resource_name def load_investments @investments = Budget::Investment.scoped_filter(params, @current_filter) - @investments = @investments.order_filter(params[:sort_by]) if params[:sort_by].present? + .order_filter(params[:sort_by]) @investments = @investments.page(params[:page]) unless request.format.csv? end diff --git a/app/controllers/admin/budget_phases_controller.rb b/app/controllers/admin/budget_phases_controller.rb index 8b71e5a0dba..d0eef18d8ae 100644 --- a/app/controllers/admin/budget_phases_controller.rb +++ b/app/controllers/admin/budget_phases_controller.rb @@ -2,7 +2,8 @@ class Admin::BudgetPhasesController < Admin::BaseController before_action :load_phase, only: [:edit, :update] - def edit; end + def edit + end def update if @phase.update(budget_phase_params) diff --git a/app/controllers/admin/site_customization/content_blocks_controller.rb b/app/controllers/admin/site_customization/content_blocks_controller.rb index 2fb38c01835..da54d6e6a1c 100644 --- a/app/controllers/admin/site_customization/content_blocks_controller.rb +++ b/app/controllers/admin/site_customization/content_blocks_controller.rb @@ -1,6 +1,10 @@ class Admin::SiteCustomization::ContentBlocksController < Admin::SiteCustomization::BaseController load_and_authorize_resource :content_block, class: "SiteCustomization::ContentBlock", - except: [:delete_heading_content_block, :edit_heading_content_block, :update_heading_content_block] + except: [ + :delete_heading_content_block, + :edit_heading_content_block, + :update_heading_content_block + ] def index @content_blocks = SiteCustomization::ContentBlock.order(:name, :locale) @@ -27,7 +31,11 @@ def create end def edit - @selected_content_block = (@content_block.is_a? SiteCustomization::ContentBlock) ? @content_block.name : "hcb_#{ @content_block.heading_id }" + if @content_block.is_a? SiteCustomization::ContentBlock + @selected_content_block = @content_block.name + else + @selected_content_block = "hcb_#{@content_block.heading_id}" + end end def update @@ -65,7 +73,11 @@ def delete_heading_content_block def edit_heading_content_block @content_block = Budget::ContentBlock.find(params[:id]) - @selected_content_block = (@content_block.is_a? Budget::ContentBlock) ? "hcb_#{ @content_block.heading_id }" : @content_block.heading.name + if @content_block.is_a? Budget::ContentBlock + @selected_content_block = "hcb_#{@content_block.heading_id}" + else + @selected_content_block = @content_block.heading.name + end @is_heading_content_block = true render :edit end @@ -116,7 +128,8 @@ def new_heading_content_block heading_content_block = Budget::ContentBlock.new heading_content_block.body = params[:site_customization_content_block][:body] heading_content_block.locale = params[:site_customization_content_block][:locale] - heading_content_block.heading_id = params[:site_customization_content_block][:name].sub('hcb_', '').to_i + block_heading_id = params[:site_customization_content_block][:name].sub('hcb_', '').to_i + heading_content_block.heading_id = block_heading_id heading_content_block end end diff --git a/app/controllers/budgets/ballot/lines_controller.rb b/app/controllers/budgets/ballot/lines_controller.rb index 9e693064cd7..c2e617e08f9 100644 --- a/app/controllers/budgets/ballot/lines_controller.rb +++ b/app/controllers/budgets/ballot/lines_controller.rb @@ -2,7 +2,6 @@ module Budgets module Ballot class LinesController < ApplicationController before_action :authenticate_user! - #before_action :ensure_final_voting_allowed before_action :load_budget before_action :load_ballot before_action :load_tag_cloud @@ -33,10 +32,6 @@ def destroy private - def ensure_final_voting_allowed - return head(:forbidden) unless @budget.balloting? - end - def line_params params.permit(:investment_id, :budget_id) end @@ -78,7 +73,9 @@ def load_ballot_referer def load_map @investments ||= [] - @investments_map_coordinates = MapLocation.where(investment: @investments).map(&:json_data) + @investments_map_coordinates = MapLocation.where(investment: @investments).map do |loc| + loc.json_data + end @map_location = MapLocation.load_from_heading(@heading) end diff --git a/app/controllers/budgets/investments_controller.rb b/app/controllers/budgets/investments_controller.rb index c0e91b080b8..3d86a265204 100644 --- a/app/controllers/budgets/investments_controller.rb +++ b/app/controllers/budgets/investments_controller.rb @@ -43,7 +43,9 @@ def index @investments = all_investments.page(params[:page]).per(10).for_render @investment_ids = @investments.pluck(:id) - @investments_map_coordinates = MapLocation.where(investment_id: all_investments).map { |l| l.json_data } + @investments_map_coordinates = MapLocation.where(investment: all_investments).map do |loc| + loc.json_data + end load_investment_votes(@investments) @tag_cloud = tag_cloud diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb index 5f3fd0a9f74..47130bfaf82 100644 --- a/app/controllers/graphql_controller.rb +++ b/app/controllers/graphql_controller.rb @@ -3,7 +3,8 @@ class GraphqlController < ApplicationController skip_before_action :verify_authenticity_token skip_authorization_check - class QueryStringError < StandardError; end + class QueryStringError < StandardError + end def query begin diff --git a/app/controllers/installation_controller.rb b/app/controllers/installation_controller.rb index bc0e329433e..8aef8b6596b 100644 --- a/app/controllers/installation_controller.rb +++ b/app/controllers/installation_controller.rb @@ -12,7 +12,7 @@ def details def consul_installation_details { - release: 'v0.18' + release: "v0.18.1" }.merge(features: settings_feature_flags) end diff --git a/app/controllers/management/budgets/investments_controller.rb b/app/controllers/management/budgets/investments_controller.rb index d4ecac8d40e..2e879b6e16f 100644 --- a/app/controllers/management/budgets/investments_controller.rb +++ b/app/controllers/management/budgets/investments_controller.rb @@ -23,6 +23,7 @@ def create notice = t('flash.actions.create.notice', resource_name: Budget::Investment.model_name.human, count: 1) redirect_to management_budget_investment_path(@budget, @investment), notice: notice else + load_categories render :new end end @@ -52,7 +53,8 @@ def load_investment_votes(investments) end def investment_params - params.require(:budget_investment).permit(:title, :description, :external_url, :heading_id, :tag_list, :organization_name, :location) + params.require(:budget_investment).permit(:title, :description, :external_url, :heading_id, + :tag_list, :organization_name, :location, :skip_map) end def only_verified_users diff --git a/app/helpers/admin_helper.rb b/app/helpers/admin_helper.rb index 903a1e0a333..55825aba01b 100644 --- a/app/helpers/admin_helper.rb +++ b/app/helpers/admin_helper.rb @@ -29,6 +29,10 @@ def moderated_sections "hidden_budget_investments"] end + def menu_budgets? + %w[budgets budget_groups budget_headings budget_investments].include?(controller_name) + end + def menu_budget? ["spending_proposals"].include?(controller_name) end diff --git a/app/helpers/content_blocks_helper.rb b/app/helpers/content_blocks_helper.rb index a2e968c08a4..23453219c44 100644 --- a/app/helpers/content_blocks_helper.rb +++ b/app/helpers/content_blocks_helper.rb @@ -1,6 +1,8 @@ module ContentBlocksHelper def valid_blocks - options = SiteCustomization::ContentBlock::VALID_BLOCKS.map { |key| [t("admin.site_customization.content_blocks.content_block.names.#{key}"), key] } + options = SiteCustomization::ContentBlock::VALID_BLOCKS.map do |key| + [t("admin.site_customization.content_blocks.content_block.names.#{key}"), key] + end Budget::Heading.allow_custom_content.each do |heading| options.push([heading.name, "hcb_#{heading.id}"]) end diff --git a/app/models/budget/investment.rb b/app/models/budget/investment.rb index 110da873281..1b528a9cbe4 100644 --- a/app/models/budget/investment.rb +++ b/app/models/budget/investment.rb @@ -142,6 +142,8 @@ def self.advanced_filters(params, results) def self.order_filter(sorting_param) if sorting_param.present? && SORTING_OPTIONS.include?(sorting_param) send("sort_by_#{sorting_param}") + else + order(cached_votes_up: :desc).order(id: :desc) end end diff --git a/app/models/legislation/process.rb b/app/models/legislation/process.rb index e9bfe02a469..dc880552b0e 100644 --- a/app/models/legislation/process.rb +++ b/app/models/legislation/process.rb @@ -18,8 +18,8 @@ class Legislation::Process < ActiveRecord::Base translates :homepage, touch: true include Globalizable - PHASES_AND_PUBLICATIONS = %i[draft_phase debate_phase allegations_phase proposals_phase - draft_publication result_publication].freeze + PHASES_AND_PUBLICATIONS = %i[homepage_phase draft_phase debate_phase allegations_phase + proposals_phase draft_publication result_publication].freeze has_many :draft_versions, -> { order(:id) }, class_name: 'Legislation::DraftVersion', foreign_key: 'legislation_process_id', @@ -54,6 +54,10 @@ class Legislation::Process < ActiveRecord::Base draft_end_date IS NOT NULL and (draft_start_date > ? or draft_end_date < ?))", Date.current, Date.current) } + def homepage_phase + Legislation::Process::Phase.new(start_date, end_date, homepage_enabled) + end + def draft_phase Legislation::Process::Phase.new(draft_start_date, draft_end_date, draft_phase_enabled) end @@ -80,6 +84,10 @@ def result_publication Legislation::Process::Publication.new(result_publication_date, result_publication_enabled) end + def enabled_phases? + PHASES_AND_PUBLICATIONS.any? { |process| send(process).enabled? } + end + def enabled_phases_and_publications_count PHASES_AND_PUBLICATIONS.count { |process| send(process).enabled? } end diff --git a/app/models/map_location.rb b/app/models/map_location.rb index d141c167d45..892ba8f2052 100644 --- a/app/models/map_location.rb +++ b/app/models/map_location.rb @@ -22,7 +22,7 @@ def self.load_from_heading(heading) map = new map.zoom = Budget::Heading::OSM_DISTRICT_LEVEL_ZOOM map.latitude = heading.latitude.to_f if heading.latitude.present? - map.longitude = heading.longitude.to_f if heading.latitude.present? + map.longitude = heading.longitude.to_f if heading.longitude.present? map end diff --git a/app/views/admin/_menu.html.erb b/app/views/admin/_menu.html.erb index 6e393e805c3..c6ea681720a 100644 --- a/app/views/admin/_menu.html.erb +++ b/app/views/admin/_menu.html.erb @@ -56,8 +56,7 @@ <% end %> <% if feature?(:budgets) %> -
  • "> +
  • "> <%= link_to admin_budgets_path do %> <%= t("admin.menu.budgets") %> diff --git a/app/views/admin/budget_groups/edit.html.erb b/app/views/admin/budget_groups/edit.html.erb index 4ed1ad7f976..0730a42068d 100644 --- a/app/views/admin/budget_groups/edit.html.erb +++ b/app/views/admin/budget_groups/edit.html.erb @@ -1,3 +1,3 @@ <%= render "header", action: "edit" %> -<%= render "form", path: admin_budget_group_path(@budget, @group), action: "edit" %> +<%= render "form", path: admin_budget_group_path(@budget, @group), action: "submit" %> diff --git a/app/views/admin/budget_groups/index.html.erb b/app/views/admin/budget_groups/index.html.erb index 506698fb795..3d6176312f2 100644 --- a/app/views/admin/budget_groups/index.html.erb +++ b/app/views/admin/budget_groups/index.html.erb @@ -1,3 +1,7 @@ +<%= back_link_to admin_budgets_path, t("admin.budget_groups.index.back") %> + +
    +

    <%= @budget.name %>

    <%= link_to t("admin.budget_groups.form.create"), diff --git a/app/views/admin/budget_headings/_form.html.erb b/app/views/admin/budget_headings/_form.html.erb index ebc5aa2aecf..f11ecb2306d 100644 --- a/app/views/admin/budget_headings/_form.html.erb +++ b/app/views/admin/budget_headings/_form.html.erb @@ -32,8 +32,14 @@ label: t("admin.budget_headings.form.longitude"), maxlength: 22, placeholder: "longitude" %> +

    + <%= t("admin.budget_headings.form.coordinates_info") %> +

    <%= f.check_box :allow_custom_content, label: t("admin.budget_headings.form.allow_content_block") %> +

    + <%= t("admin.budget_headings.form.content_blocks_info") %> +

    <%= f.submit t("admin.budget_headings.form.#{action}"), class: "button success" %> <% end %> diff --git a/app/views/admin/budget_headings/edit.html.erb b/app/views/admin/budget_headings/edit.html.erb index a01fa70a420..124d075107f 100644 --- a/app/views/admin/budget_headings/edit.html.erb +++ b/app/views/admin/budget_headings/edit.html.erb @@ -1,3 +1,3 @@ <%= render "header", action: "edit" %> -<%= render "form", path: admin_budget_group_heading_path(@budget, @group, @heading), action: "edit" %> +<%= render "form", path: admin_budget_group_heading_path(@budget, @group, @heading), action: "submit" %> diff --git a/app/views/admin/budget_headings/index.html.erb b/app/views/admin/budget_headings/index.html.erb index a5a2a75aff9..372c7ceb2d7 100644 --- a/app/views/admin/budget_headings/index.html.erb +++ b/app/views/admin/budget_headings/index.html.erb @@ -1,4 +1,4 @@ -<%= back_link_to admin_budget_groups_path(@budget) %> +<%= back_link_to admin_budget_groups_path(@budget), t("admin.budget_headings.index.back") %>

    <%= "#{@budget.name} / #{@group.name}" %>

    diff --git a/app/views/admin/legislation/homepages/_form.html.erb b/app/views/admin/legislation/homepages/_form.html.erb index e6b7cf2f442..aa3ee4151db 100644 --- a/app/views/admin/legislation/homepages/_form.html.erb +++ b/app/views/admin/legislation/homepages/_form.html.erb @@ -11,7 +11,7 @@ <%= f.translatable_fields do |translations_form| %> -
    +
    <%= translations_form.cktext_area :homepage, language: I18n.locale, @@ -22,7 +22,7 @@
    <% end %> -
    +
    <%= f.submit(class: "button success expanded", value: t("admin.legislation.processes.#{admin_submit_action(@process)}.submit_button")) %>
    <% end %> diff --git a/app/views/admin/site_customization/content_blocks/index.html.erb b/app/views/admin/site_customization/content_blocks/index.html.erb index ccc13bcf23b..26579fd4c0c 100644 --- a/app/views/admin/site_customization/content_blocks/index.html.erb +++ b/app/views/admin/site_customization/content_blocks/index.html.erb @@ -9,13 +9,13 @@

    <%= t("admin.site_customization.content_blocks.information") %>

    <%= t("admin.site_customization.content_blocks.about") %>

    -

    <%= t("admin.site_customization.content_blocks.top_links_html") %>

    +

    <%= t("admin.site_customization.content_blocks.html_format") %>

    -<li><a href="http://site1.com">Site 1</a></li>
    -<li><a href="http://site2.com">Site 2</a></li>
    -<li><a href="http://site3.com">Site 3</a></li>
    - -

    <%= t("admin.site_customization.content_blocks.footer_html") %>

    +

    + <%= '

  • Site 1
  • ' %>
    + <%= '
  • Site 2
  • ' %>

    + <%= '
  • Site 3
  • ' %>

    +

    <% if @content_blocks.any? || @headings_content_blocks.any? %> diff --git a/app/views/admin/stats/proposal_notifications.html.erb b/app/views/admin/stats/proposal_notifications.html.erb index 102eb0c6012..e26ac3431a8 100644 --- a/app/views/admin/stats/proposal_notifications.html.erb +++ b/app/views/admin/stats/proposal_notifications.html.erb @@ -31,7 +31,13 @@ diff --git a/app/views/budgets/investments/_content_blocks.html.erb b/app/views/budgets/investments/_content_blocks.html.erb index de86b50fe69..30f938a9421 100644 --- a/app/views/budgets/investments/_content_blocks.html.erb +++ b/app/views/budgets/investments/_content_blocks.html.erb @@ -1,7 +1,5 @@ <% if @heading.allow_custom_content %> -
    - -

    <%= notification.title %> - <%= link_to notification.proposal.title, proposal_path(notification.proposal) %> + + <% if notification.proposal.present? %> + <%= link_to notification.proposal.title, proposal_path(notification.proposal) %> + <% else %> + <%= t("admin.stats.proposal_notifications.not_available") %>
    + <% end %> +

    <%= notification.body %>