diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index a9e44d249..44258bc7a 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -79,6 +79,7 @@ //= require send_newsletter_alert //= require managers //= require globalize +//= require send_admin_notification_alert var initialize_modules = function() { App.Comments.initialize(); @@ -124,6 +125,7 @@ var initialize_modules = function() { App.SendNewsletterAlert.initialize(); App.Managers.initialize(); App.Globalize.initialize(); + App.SendAdminNotificationAlert.initialize(); }; $(function(){ diff --git a/app/assets/javascripts/send_admin_notification_alert.js.coffee b/app/assets/javascripts/send_admin_notification_alert.js.coffee new file mode 100644 index 000000000..8c1c928e5 --- /dev/null +++ b/app/assets/javascripts/send_admin_notification_alert.js.coffee @@ -0,0 +1,4 @@ +App.SendAdminNotificationAlert = + initialize: -> + $('#js-send-admin_notification-alert').on 'click', -> + confirm(this.dataset.alert); diff --git a/app/controllers/admin/admin_notifications_controller.rb b/app/controllers/admin/admin_notifications_controller.rb new file mode 100644 index 000000000..1dd03038a --- /dev/null +++ b/app/controllers/admin/admin_notifications_controller.rb @@ -0,0 +1,67 @@ +class Admin::AdminNotificationsController < Admin::BaseController + + def index + @admin_notifications = AdminNotification.all + end + + def show + @admin_notification = AdminNotification.find(params[:id]) + end + + def new + @admin_notification = AdminNotification.new + end + + def create + @admin_notification = AdminNotification.new(admin_notification_params) + + if @admin_notification.save + notice = t("admin.admin_notifications.create_success") + redirect_to [:admin, @admin_notification], notice: notice + else + render :new + end + end + + def edit + @admin_notification = AdminNotification.find(params[:id]) + end + + def update + @admin_notification = AdminNotification.find(params[:id]) + + if @admin_notification.update(admin_notification_params) + notice = t("admin.admin_notifications.update_success") + redirect_to [:admin, @admin_notification], notice: notice + else + render :edit + end + end + + def destroy + @admin_notification = AdminNotification.find(params[:id]) + @admin_notification.destroy + + notice = t("admin.admin_notifications.delete_success") + redirect_to admin_admin_notifications_path, notice: notice + end + + def deliver + @admin_notification = AdminNotification.find(params[:id]) + + if @admin_notification.valid? + @admin_notification.deliver + flash[:notice] = t("admin.admin_notifications.send_success") + else + flash[:error] = t("admin.segment_recipient.invalid_recipients_segment") + end + + redirect_to [:admin, @admin_notification] + end + + private + + def admin_notification_params + params.require(:admin_notification).permit(:title, :body, :link, :segment_recipient) + end +end diff --git a/app/controllers/admin/budget_investments_controller.rb b/app/controllers/admin/budget_investments_controller.rb index 0258c23a6..043c06394 100644 --- a/app/controllers/admin/budget_investments_controller.rb +++ b/app/controllers/admin/budget_investments_controller.rb @@ -75,17 +75,9 @@ class Admin::BudgetInvestmentsController < Admin::BaseController resource_model.parameterize('_') end - def sort_by(params) - if params.present? && Budget::Investment::SORTING_OPTIONS.include?(params) - "#{params == 'supports' ? 'cached_votes_up' : params} ASC" - else - "cached_votes_up DESC, created_at DESC" - end - end - def load_investments @investments = Budget::Investment.scoped_filter(params, @current_filter) - .order(sort_by(params[:sort_by])) + @investments = @investments.order_filter(params[:sort_by]) if params[:sort_by].present? @investments = @investments.page(params[:page]) unless request.format.csv? end diff --git a/app/controllers/admin/signature_sheets_controller.rb b/app/controllers/admin/signature_sheets_controller.rb index 60299c5a6..4a6777695 100644 --- a/app/controllers/admin/signature_sheets_controller.rb +++ b/app/controllers/admin/signature_sheets_controller.rb @@ -1,7 +1,7 @@ class Admin::SignatureSheetsController < Admin::BaseController def index - @signature_sheets = SignatureSheet.all + @signature_sheets = SignatureSheet.all.order(created_at: :desc) end def new @@ -29,4 +29,4 @@ class Admin::SignatureSheetsController < Admin::BaseController params.require(:signature_sheet).permit(:signable_type, :signable_id, :document_numbers) end -end \ No newline at end of file +end diff --git a/app/controllers/admin/system_emails_controller.rb b/app/controllers/admin/system_emails_controller.rb new file mode 100644 index 000000000..626680f43 --- /dev/null +++ b/app/controllers/admin/system_emails_controller.rb @@ -0,0 +1,37 @@ +class Admin::SystemEmailsController < Admin::BaseController + + before_action :load_system_email, only: [:view, :preview_pending] + + def index + @system_emails = { + proposal_notification_digest: %w(view preview_pending) + } + end + + def view + case @system_email + when "proposal_notification_digest" + @notifications = Notification.where(notifiable_type: "ProposalNotification").limit(2) + @subject = t('mailers.proposal_notification_digest.title', org_name: Setting['org_name']) + end + end + + def preview_pending + case @system_email + when "proposal_notification_digest" + @previews = ProposalNotification.where(id: unsent_proposal_notifications_ids) + .page(params[:page]) + end + end + + private + + def load_system_email + @system_email = params[:system_email_id] + end + + def unsent_proposal_notifications_ids + Notification.where(notifiable_type: "ProposalNotification", emailed_at: nil) + .group(:notifiable_id).count.keys + end +end diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb index 588f2ed6b..ee6878502 100644 --- a/app/controllers/notifications_controller.rb +++ b/app/controllers/notifications_controller.rb @@ -44,7 +44,11 @@ class NotificationsController < ApplicationController when "Topic" community_topic_path @notification.linkable_resource.community, @notification.linkable_resource else - url_for @notification.linkable_resource + if @notification.linkable_resource.is_a?(AdminNotification) + @notification.linkable_resource.link || notifications_path + else + url_for @notification.linkable_resource + end end end diff --git a/app/models/abilities/administrator.rb b/app/models/abilities/administrator.rb index 6f8a70b5e..e8bb005d4 100644 --- a/app/models/abilities/administrator.rb +++ b/app/models/abilities/administrator.rb @@ -50,8 +50,7 @@ module Abilities can :manage, Annotation can [:read, :update, :valuate, :destroy, :summary], SpendingProposal - - can [:index, :read, :new, :create, :update, :destroy, :calculate_winners, :read_results], Budget + can [:index, :read, :new, :create, :update, :destroy, :calculate_winners], Budget can [:read, :create, :update, :destroy], Budget::Group can [:read, :create, :update, :destroy], Budget::Heading can [:hide, :update, :toggle_selection], Budget::Investment diff --git a/app/models/admin_notification.rb b/app/models/admin_notification.rb new file mode 100644 index 000000000..eccd90910 --- /dev/null +++ b/app/models/admin_notification.rb @@ -0,0 +1,44 @@ +class AdminNotification < ActiveRecord::Base + include Notifiable + + validates :title, presence: true + validates :body, presence: true + validates :segment_recipient, presence: true + validate :validate_segment_recipient + + before_validation :complete_link_url + + def list_of_recipients + UserSegments.send(segment_recipient) if valid_segment_recipient? + end + + def valid_segment_recipient? + segment_recipient && UserSegments.respond_to?(segment_recipient) + end + + def draft? + sent_at.nil? + end + + def list_of_recipients_count + list_of_recipients.try(:count) || 0 + end + + def deliver + list_of_recipients.each { |user| Notification.add(user, self) } + self.update(sent_at: Time.current, recipients_count: list_of_recipients.count) + end + + private + + def validate_segment_recipient + errors.add(:segment_recipient, :invalid) unless valid_segment_recipient? + end + + def complete_link_url + return unless link.present? + unless self.link[/\Ahttp:\/\//] || self.link[/\Ahttps:\/\//] + self.link = "http://#{self.link}" + end + end +end diff --git a/app/models/budget/investment.rb b/app/models/budget/investment.rb index 1e2d3887c..d69a0d64a 100644 --- a/app/models/budget/investment.rb +++ b/app/models/budget/investment.rb @@ -57,6 +57,10 @@ class Budget scope :sort_by_price, -> { reorder(price: :desc, confidence_score: :desc, id: :desc) } scope :sort_by_random, ->(seed) { reorder("budget_investments.id % #{seed.to_f.nonzero? ? seed.to_f : 1}, budget_investments.id") } + scope :sort_by_id, -> { order("id DESC") } + scope :sort_by_title, -> { order("title ASC") } + scope :sort_by_supports, -> { order("cached_votes_up DESC") } + scope :valuation_open, -> { where(valuation_finished: false) } scope :without_admin, -> { valuation_open.where(administrator_id: nil) } scope :without_valuator, -> { valuation_open.where(valuator_assignments_count: 0) } @@ -133,6 +137,24 @@ class Budget results.where("budget_investments.id IN (?)", ids) end + def self.order_filter(sorting_param) + if sorting_param.present? && SORTING_OPTIONS.include?(sorting_param) + send("sort_by_#{sorting_param}") + end + end + + def self.limit_results(budget, params, results) + max_per_heading = params[:max_per_heading].to_i + return results if max_per_heading <= 0 + + ids = [] + budget.headings.pluck(:id).each do |hid| + ids += Investment.where(heading_id: hid).order(confidence_score: :desc).limit(max_per_heading).pluck(:id) + end + + results.where("budget_investments.id IN (?)", ids) + end + def self.search_by_title_or_id(title_or_id, results) if title_or_id =~ /^[0-9]+$/ results.where(id: title_or_id) diff --git a/app/models/budget/investment/exporter.rb b/app/models/budget/investment/exporter.rb index 03900fd7e..c7e4ce461 100644 --- a/app/models/budget/investment/exporter.rb +++ b/app/models/budget/investment/exporter.rb @@ -16,16 +16,18 @@ class Budget::Investment::Exporter def headers [ - I18n.t("admin.budget_investments.index.table_id"), - I18n.t("admin.budget_investments.index.table_title"), - I18n.t("admin.budget_investments.index.table_supports"), - I18n.t("admin.budget_investments.index.table_admin"), - I18n.t("admin.budget_investments.index.table_valuator"), - I18n.t("admin.budget_investments.index.table_valuation_group"), - I18n.t("admin.budget_investments.index.table_geozone"), - I18n.t("admin.budget_investments.index.table_feasibility"), - I18n.t("admin.budget_investments.index.table_valuation_finished"), - I18n.t("admin.budget_investments.index.table_selection") + I18n.t("admin.budget_investments.index.list.id"), + I18n.t("admin.budget_investments.index.list.title"), + I18n.t("admin.budget_investments.index.list.supports"), + I18n.t("admin.budget_investments.index.list.admin"), + I18n.t("admin.budget_investments.index.list.valuator"), + I18n.t("admin.budget_investments.index.list.valuation_group"), + I18n.t("admin.budget_investments.index.list.geozone"), + I18n.t("admin.budget_investments.index.list.feasibility"), + I18n.t("admin.budget_investments.index.list.valuation_finished"), + I18n.t("admin.budget_investments.index.list.selected"), + I18n.t("admin.budget_investments.index.list.visible_to_valuators"), + I18n.t("admin.budget_investments.index.list.author_username") ] end @@ -40,7 +42,9 @@ class Budget::Investment::Exporter investment.heading.name, price(investment), investment.valuation_finished? ? I18n.t('shared.yes') : I18n.t('shared.no'), - investment.selected? ? I18n.t('shared.yes') : I18n.t('shared.no') + investment.selected? ? I18n.t('shared.yes') : I18n.t('shared.no'), + investment.visible_to_valuators? ? I18n.t('shared.yes') : I18n.t('shared.no'), + investment.author.username ] end diff --git a/app/models/notification.rb b/app/models/notification.rb index 33bf7701c..dacedb762 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -53,9 +53,19 @@ class Notification < ActiveRecord::Base "proposal_notification" when "Comment" "replies_to" + when "AdminNotification" + nil else "comments_on" end end -end \ No newline at end of file + def link + if notifiable.is_a?(AdminNotification) && notifiable.link.blank? + nil + else + self + end + end + +end diff --git a/app/views/admin/_menu.html.erb b/app/views/admin/_menu.html.erb index b830acb46..ffec03232 100644 --- a/app/views/admin/_menu.html.erb +++ b/app/views/admin/_menu.html.erb @@ -79,17 +79,23 @@ <% end %> - <% messages_sections = %w(newsletters emails_download) %> + <% messages_sections = %w(newsletters emails_download admin_notifications system_emails) %> <% messages_menu_active = messages_sections.include?(controller_name) %>
  • > - <%= t("admin.menu.emails") %> + <%= t("admin.menu.messaging_users") %> - diff --git a/app/views/admin/activity/show.html.erb b/app/views/admin/activity/show.html.erb index dd4cee125..33f4dc6f1 100644 --- a/app/views/admin/activity/show.html.erb +++ b/app/views/admin/activity/show.html.erb @@ -33,12 +33,18 @@ <%= activity.actionable.username %> (<%= activity.actionable.email %>) <% when "Comment" %> <%= activity.actionable.body %> + <% when "Newsletter" %> + <%= activity.actionable.subject %> +
    + <%= activity.actionable.body %> + <% when "ProposalNotification" %> + <%= activity.actionable.title %> +
    + <%= activity.actionable.body %> <% else %> <%= activity.actionable.title %>
    -
    - <%= activity.actionable.description %> -
    + <%= activity.actionable.description %> <% end %> <%= activity.user.name %> (<%= activity.user.email %>) diff --git a/app/views/admin/admin_notifications/_form.html.erb b/app/views/admin/admin_notifications/_form.html.erb new file mode 100644 index 000000000..4053bb66d --- /dev/null +++ b/app/views/admin/admin_notifications/_form.html.erb @@ -0,0 +1,13 @@ +<%= form_for [:admin, @admin_notification] do |f| %> + <%= render 'shared/errors', resource: @admin_notification %> + + <%= f.select :segment_recipient, options_for_select(user_segments_options, + @admin_notification[:segment_recipient]) %> + <%= f.text_field :title %> + <%= f.text_field :link %> + <%= f.text_area :body %> + +
    + <%= f.submit class: "button success" %> +
    +<% end %> diff --git a/app/views/admin/admin_notifications/edit.html.erb b/app/views/admin/admin_notifications/edit.html.erb new file mode 100644 index 000000000..cb14f0a0b --- /dev/null +++ b/app/views/admin/admin_notifications/edit.html.erb @@ -0,0 +1,4 @@ +<%= back_link_to %> +

    <%= t("admin.admin_notifications.edit.section_title") %>

    + +<%= render "form" %> diff --git a/app/views/admin/admin_notifications/index.html.erb b/app/views/admin/admin_notifications/index.html.erb new file mode 100644 index 000000000..d16ee476e --- /dev/null +++ b/app/views/admin/admin_notifications/index.html.erb @@ -0,0 +1,56 @@ +

    <%= t("admin.admin_notifications.index.section_title") %>

    +<%= link_to t("admin.admin_notifications.index.new_notification"), new_admin_admin_notification_path, + class: "button float-right" %> + +<% if @admin_notifications.any? %> + + + + + + + + + + + <% @admin_notifications.order(created_at: :desc).each do |admin_notification| %> + + + + + + + <% end %> + +
    <%= t("admin.admin_notifications.index.title") %><%= t("admin.admin_notifications.index.segment_recipient") %><%= t("admin.admin_notifications.index.sent") %><%= t("admin.admin_notifications.index.actions") %>
    + <%= admin_notification.title %> + + <%= segment_name(admin_notification.segment_recipient) %> + + <% if admin_notification.draft? %> + <%= t("admin.admin_notifications.index.draft") %> + <% else %> + <%= l admin_notification.sent_at.to_date %> + <% end %> + + <% if admin_notification.draft? %> + <%= link_to t("admin.admin_notifications.index.edit"), + edit_admin_admin_notification_path(admin_notification), + method: :get, class: "button hollow" %> + <%= link_to t("admin.admin_notifications.index.delete"), + admin_admin_notification_path(admin_notification), + method: :delete, class: "button hollow alert" %> + <%= link_to t("admin.admin_notifications.index.preview"), + admin_admin_notification_path(admin_notification), + class: "button" %> + <% else %> + <%= link_to t("admin.admin_notifications.index.view"), + admin_admin_notification_path(admin_notification), + class: "button" %> + <% end %> +
    +<% else %> +
    + <%= t("admin.admin_notifications.index.empty_notifications") %> +
    +<% end %> diff --git a/app/views/admin/admin_notifications/new.html.erb b/app/views/admin/admin_notifications/new.html.erb new file mode 100644 index 000000000..69bf2a80b --- /dev/null +++ b/app/views/admin/admin_notifications/new.html.erb @@ -0,0 +1,4 @@ +<%= back_link_to %> +

    <%= t("admin.admin_notifications.new.section_title") %>

    + +<%= render "form" %> diff --git a/app/views/admin/admin_notifications/show.html.erb b/app/views/admin/admin_notifications/show.html.erb new file mode 100644 index 000000000..92c0e37fc --- /dev/null +++ b/app/views/admin/admin_notifications/show.html.erb @@ -0,0 +1,77 @@ +<%= back_link_to admin_admin_notifications_path %> + +

    <%= t("admin.admin_notifications.show.section_title") %>

    + +
    +
    +
    +
    + <%= t("admin.admin_notifications.show.sent_at") %>
    + <% if @admin_notification.draft? %> + <%= t("admin.admin_notifications.index.draft") %> + <% else %> + <%= l(@admin_notification.sent_at.to_date) %> + <% end %> +
    +
    + <%= t("admin.admin_notifications.show.title") %>
    + <%= @admin_notification.title %> +
    +
    +
    + +
    + <%= t("admin.admin_notifications.show.body") %>
    + <%= @admin_notification.body %> +
    +
    + <%= t("admin.admin_notifications.show.link") %>
    + <%= @admin_notification.link %> +
    +
    +
    +
    + <%= t("admin.admin_notifications.show.segment_recipient") %>
    + <%= segment_name(@admin_notification.segment_recipient) %> + <% if @admin_notification.draft? %> + <%= t("admin.admin_notifications.show.will_get_notified", + n: @admin_notification.list_of_recipients_count) %> + <% else %> + <%= t("admin.admin_notifications.show.got_notified", + n: @admin_notification.recipients_count) %> + <% end %> +
    +
    +
    + +

    + <% if @admin_notification.draft? %> + <%= t("admin.admin_notifications.show.preview_guide") %> + <% else %> + <%= t("admin.admin_notifications.show.sent_guide") %> + <% end %> +

    +
    +
    +
      +
    • + <% locals = { notification: nil, + title: @admin_notification.title, + body: @admin_notification.body, + timestamp: Time.current } %> + <% link_text = render partial: '/notifications/notification_body', locals: locals %> + <%= link_to_if @admin_notification.link.present?, link_text, @admin_notification.link %> +
    • +
    +
    +
    +
    +<% if @admin_notification.draft? && @admin_notification.valid_segment_recipient? %> + <%= link_to t("admin.admin_notifications.show.send"), + deliver_admin_admin_notification_path(@admin_notification), + "data-alert": t("admin.admin_notifications.show.send_alert", + n: @admin_notification.list_of_recipients_count), + method: :post, + id: "js-send-admin_notification-alert", + class: "button success" %> +<% end %> diff --git a/app/views/admin/budget_investments/_investments.html.erb b/app/views/admin/budget_investments/_investments.html.erb index db10e2210..a3a512366 100644 --- a/app/views/admin/budget_investments/_investments.html.erb +++ b/app/views/admin/budget_investments/_investments.html.erb @@ -35,21 +35,21 @@ - - - - + + + + - - - - - + + + + + <% if params[:filter] == "selected" %> - + <% end %> diff --git a/app/views/admin/signature_sheets/index.html.erb b/app/views/admin/signature_sheets/index.html.erb index 74241b5b0..be35d8b6b 100644 --- a/app/views/admin/signature_sheets/index.html.erb +++ b/app/views/admin/signature_sheets/index.html.erb @@ -19,7 +19,7 @@ <%= signature_sheet.author.name %> <% end %> diff --git a/app/views/admin/signature_sheets/show.html.erb b/app/views/admin/signature_sheets/show.html.erb index 0c2536b34..126e363ba 100644 --- a/app/views/admin/signature_sheets/show.html.erb +++ b/app/views/admin/signature_sheets/show.html.erb @@ -2,7 +2,7 @@
    <%= t("admin.signature_sheets.show.created_at") %> - <%= l(@signature_sheet.created_at, format: :short) %> + <%= l(@signature_sheet.created_at, format: :long) %>  •  <%= t("admin.signature_sheets.show.author") %> <%= @signature_sheet.author.name %> @@ -41,4 +41,4 @@
    <%= t("admin.signature_sheets.show.loading") %>
    -<% end %> \ No newline at end of file +<% end %> diff --git a/app/views/admin/system_emails/index.html.erb b/app/views/admin/system_emails/index.html.erb new file mode 100644 index 000000000..4d135d615 --- /dev/null +++ b/app/views/admin/system_emails/index.html.erb @@ -0,0 +1,34 @@ +

    <%= t("admin.menu.system_emails") %>

    + +
    <%= t("admin.budget_investments.index.table_id") %><%= t("admin.budget_investments.index.table_title") %><%= t("admin.budget_investments.index.table_supports") %><%= t("admin.budget_investments.index.table_admin") %><%= t("admin.budget_investments.index.list.id") %><%= t("admin.budget_investments.index.list.title") %><%= t("admin.budget_investments.index.list.supports") %><%= t("admin.budget_investments.index.list.admin") %> - <%= t("admin.budget_investments.index.table_valuation_group") %> - <%= t("admin.budget_investments.index.table_valuator") %> + <%= t("admin.budget_investments.index.list.valuation_group") %> + <%= t("admin.budget_investments.index.list.valuator") %> <%= t("admin.budget_investments.index.table_geozone") %><%= t("admin.budget_investments.index.table_feasibility") %><%= t("admin.budget_investments.index.table_valuation_finished") %><%= t("admin.budget_investments.index.table_evaluation") %><%= t("admin.budget_investments.index.table_selection") %><%= t("admin.budget_investments.index.list.geozone") %><%= t("admin.budget_investments.index.list.feasibility") %><%= t("admin.budget_investments.index.list.valuation_finished") %><%= t("admin.budget_investments.index.list.visible_to_valuators") %><%= t("admin.budget_investments.index.list.selected") %><%= t("admin.budget_investments.index.table_incompatible") %><%= t("admin.budget_investments.index.list.incompatible") %>
    - <%= l(signature_sheet.created_at, format: :short) %> + <%= l(signature_sheet.created_at, format: :long) %>
    + + + + + + + + + <% @system_emails.each do |system_email_title, system_email_actions| %> + + + + + + <% end %> + +
    <%= t("admin.shared.title") %><%= t("admin.shared.description") %><%= t("admin.shared.actions") %>
    + <%= t("admin.system_emails.#{system_email_title}.title") %> + + <%= t("admin.system_emails.#{system_email_title}.description") %> + + <% if system_email_actions.include?('view') %> + <%= link_to t("admin.shared.view"), admin_system_email_view_path(system_email_title), + class: "button hollow" %> + <% end %> + <% if system_email_actions.include?('preview_pending') %> + <%= link_to t("admin.system_emails.preview_pending.action"), + admin_system_email_preview_pending_path(system_email_title), + class: "button" %> + <% end %> +
    diff --git a/app/views/admin/system_emails/preview_pending.html.erb b/app/views/admin/system_emails/preview_pending.html.erb new file mode 100644 index 000000000..6de1c993f --- /dev/null +++ b/app/views/admin/system_emails/preview_pending.html.erb @@ -0,0 +1,16 @@ +<%= back_link_to admin_system_emails_path %> + +<% system_email_name = t("admin.system_emails.#{@system_email}.title") %> +

    <%= t("admin.system_emails.preview_pending.preview_of", name: system_email_name) %>

    + +
    +

    <%= t("admin.system_emails.preview_pending.pending_to_be_sent") %>

    +

    <%= t("admin.system_emails.#{@system_email}.preview_detail") %>

    + + <% @previews.each do |preview| %> + <%= render partial: "admin/system_emails/preview_pending/#{@system_email}", + locals: { preview: preview } %> + <% end %> +
    + +<%= paginate @previews %> diff --git a/app/views/admin/system_emails/preview_pending/_proposal_notification_digest.html.erb b/app/views/admin/system_emails/preview_pending/_proposal_notification_digest.html.erb new file mode 100644 index 000000000..7cdd4d66d --- /dev/null +++ b/app/views/admin/system_emails/preview_pending/_proposal_notification_digest.html.erb @@ -0,0 +1,33 @@ +<% if preview.proposal.present? %> +
    +
    +
    + <%= t("admin.shared.proposal") %>
    + <%= link_to preview.proposal.title, proposal_url(preview.proposal) %> +
    +
    + <%= t("admin.shared.title") %>
    + <%= link_to preview.title, proposal_url(preview.proposal, anchor: "tab-notifications") %> +
    +
    +
    +
    + <%= t("admin.shared.author") %>
    + <%= preview.proposal.author&.username || '-' %> +
    +
    + <%= t("admin.shared.created_at") %>
    + <%= l(preview.created_at, format: :datetime) %> +
    +
    +
    + +
    +
    + <%= t("admin.shared.content") %> +

    + <%= preview.body %> +

    +
    +
    +<% end %> diff --git a/app/views/admin/system_emails/view.html.erb b/app/views/admin/system_emails/view.html.erb new file mode 100644 index 000000000..9aaeb5226 --- /dev/null +++ b/app/views/admin/system_emails/view.html.erb @@ -0,0 +1,36 @@ +<%= back_link_to admin_system_emails_path %> + +

    <%= t("admin.system_emails.#{@system_email}.title") %>

    + +
    +
    +
    +
    + <%= t("admin.newsletters.show.from") %>
    + <%= Setting['mailer_from_address'] %> +
    +
    + <%= t("admin.newsletters.show.subject") %>
    + <%= @subject %> +
    +
    +
    + + <%= t("admin.newsletters.show.body") %> +

    + <%= t("admin.newsletters.show.body_help_text") %> +

    + +
    diff --git a/app/views/budgets/show.html.erb b/app/views/budgets/show.html.erb index 1719624aa..18c4e5e2f 100644 --- a/app/views/budgets/show.html.erb +++ b/app/views/budgets/show.html.erb @@ -36,7 +36,7 @@ <% end %> <% end %> - <% if @budget.finished? || (@budget.balloting? && can?(:read_results, @budget)) %> + <% if @budget.finished? %> <%= link_to t("budgets.show.see_results"), budget_results_path(@budget, heading_id: @budget.headings.first), class: "button margin-top expanded" %> diff --git a/app/views/mailer/proposal_notification_digest.html.erb b/app/views/mailer/proposal_notification_digest.html.erb index 1a0ef1468..8f80c3e0c 100644 --- a/app/views/mailer/proposal_notification_digest.html.erb +++ b/app/views/mailer/proposal_notification_digest.html.erb @@ -22,7 +22,7 @@

    - <%= link_to notification.notifiable.title, notification_url(notification), style: "color: #2895F1; text-decoration: none;" %> + <%= link_to notification.notifiable.title, proposal_url(notification.notifiable.proposal, anchor: "tab-notifications"), style: "color: #2895F1; text-decoration: none;" %>

    <%= notification.notifiable.proposal.title %> •  diff --git a/app/views/notifications/_notification.html.erb b/app/views/notifications/_notification.html.erb index 2d8b115e5..5034d29a2 100644 --- a/app/views/notifications/_notification.html.erb +++ b/app/views/notifications/_notification.html.erb @@ -1,21 +1,11 @@ -

  • "> - - <% if notification.notifiable_available? %> - <%= link_to notification do %> -

    - - <%= t("notifications.notification.action.#{notification.notifiable_action}", - count: notification.counter) %> - - - <%= notification.notifiable_title %> - -

    - -

    - <%= l notification.timestamp, format: :datetime %> -

    - <% end %> +
  • + <% if notification.notifiable.try(:notifiable_available?) %> + <% locals = { notification: notification, + timestamp: notification.timestamp, + title: notification.notifiable_title, + body: notification.notifiable.try(:body) } %> + <% link_text = render partial: '/notifications/notification_body', locals: locals %> + <%= link_to_if notification.link.present?, link_text, notification.link %> <% else %>

    diff --git a/app/views/notifications/_notification_body.html.erb b/app/views/notifications/_notification_body.html.erb new file mode 100644 index 000000000..d6ed86673 --- /dev/null +++ b/app/views/notifications/_notification_body.html.erb @@ -0,0 +1,17 @@ +

    + <% if notification && notification.notifiable_action %> + + <%= t("notifications.notification.action.#{notification.notifiable_action}", + count: notification.counter) %> + + <% end %> + + <%= title %> + + <% if body %> +

    <%= body %>

    + <% end %> +

    +

    + <%= l(timestamp, format: :datetime) %> +

    diff --git a/app/views/notifications/index.html.erb b/app/views/notifications/index.html.erb index eccd3ec6a..e005aad88 100644 --- a/app/views/notifications/index.html.erb +++ b/app/views/notifications/index.html.erb @@ -24,7 +24,9 @@ <% else %> <% end %> diff --git a/config/locales/en/activerecord.yml b/config/locales/en/activerecord.yml index 8e44f6ea9..d3d19f1ed 100644 --- a/config/locales/en/activerecord.yml +++ b/config/locales/en/activerecord.yml @@ -283,6 +283,10 @@ en: attributes: segment_recipient: invalid: "The user recipients segment is invalid" + admin_notification: + attributes: + segment_recipient: + invalid: "The user recipients segment is invalid" map_location: attributes: map: diff --git a/config/locales/en/admin.yml b/config/locales/en/admin.yml index aab916b53..7871b377b 100644 --- a/config/locales/en/admin.yml +++ b/config/locales/en/admin.yml @@ -186,18 +186,20 @@ en: undecided: "Undecided" selected: "Selected" select: "Select" - table_id: "ID" - table_title: "Title" - table_supports: "Supports" - table_admin: "Administrator" - table_valuator: "Valuator" - table_valuation_group: "Valuation Group" - table_geozone: "Scope of operation" - table_feasibility: "Feasibility" - table_valuation_finished: "Val. Fin." - table_selection: "Selected" - table_evaluation: "Show to valuators" - table_incompatible: "Incompatible" + list: + id: ID + title: Title + supports: Supports + admin: Administrator + valuator: Valuator + valuation_group: Valuation Group + geozone: Scope of operation + feasibility: Feasibility + valuation_finished: Val. Fin. + selected: Selected + visible_to_valuators: Show to valuators + author_username: Author username + incompatible: Incompatible cannot_calculate_winners: The budget has to stay on phase "Balloting projects", "Reviewing Ballots" or "Finished budget" in order to calculate winners projects see_results: "See results" show: @@ -516,8 +518,10 @@ en: administrators: Administrators managers: Managers moderators: Moderators - emails: Sending of emails + messaging_users: Messages to users newsletters: Newsletters + admin_notifications: Notifications + system_emails: System Emails emails_download: Emails download valuators: Valuators poll_officers: Poll officers @@ -611,6 +615,50 @@ en: body: Email content body_help_text: This is how the users will see the email send_alert: Are you sure you want to send this newsletter to %{n} users? + admin_notifications: + create_success: Notification created successfully + update_success: Notification updated successfully + send_success: Notification sent successfully + delete_success: Notification deleted successfully + index: + section_title: Notifications + new_notification: New notification + title: Title + segment_recipient: Recipients + sent: Sent + actions: Actions + draft: Draft + edit: Edit + delete: Delete + preview: Preview + view: View + empty_notifications: There are no notifications to show + new: + section_title: New notification + edit: + section_title: Edit notification + show: + section_title: Notification preview + send: Send + will_get_notified: (%{n} users will be notified) + got_notified: (%{n} users got notified) + sent_at: Sent at + title: Title + body: Text + link: Link + segment_recipient: Recipients + preview_guide: "This is how the users will see the notification:" + sent_guide: "This is how the users see the notification:" + send_alert: Are you sure you want to send this notification to %{n} users? + system_emails: + preview_pending: + action: Preview Pending + preview_of: Preview of %{name} + pending_to_be_sent: This is the content pending to be sent + proposal_notification_digest: + title: Proposal Notification Digest + description: Gathers all proposal notifications for an user in a single message, to avoid too much emails. + preview_detail: Users will only recieve notifications from the proposals they are following emails_download: index: title: Emails download @@ -1007,6 +1055,11 @@ en: image: Image show_image: Show image moderated_content: "Check the content moderated by the moderators, and confirm if the moderation has been done correctly." + view: View + proposal: Proposal + author: Author + content: Content + created_at: Created at spending_proposals: index: geozone_filter_all: All zones diff --git a/config/locales/es/activerecord.yml b/config/locales/es/activerecord.yml index 2da027031..52bb51e9f 100644 --- a/config/locales/es/activerecord.yml +++ b/config/locales/es/activerecord.yml @@ -283,6 +283,10 @@ es: attributes: segment_recipient: invalid: "El segmento de usuarios es inválido" + admin_notification: + attributes: + segment_recipient: + invalid: "El segmento de usuarios es inválido" map_location: attributes: map: diff --git a/config/locales/es/admin.yml b/config/locales/es/admin.yml index d01c38e79..c6761b568 100644 --- a/config/locales/es/admin.yml +++ b/config/locales/es/admin.yml @@ -187,18 +187,20 @@ es: undecided: "Sin decidir" selected: "Seleccionada" select: "Seleccionar" - table_id: "ID" - table_title: "Título" - table_supports: "Apoyos" - table_admin: "Administrador" - table_valuator: "Evaluador" - table_valuation_group: "Grupos evaluadores" - table_geozone: "Ámbito de actuación" - table_feasibility: "Viabilidad" - table_valuation_finished: "Ev. Fin." - table_selection: "Seleccionado" - table_evaluation: "Mostrar a evaluadores" - table_incompatible: "Incompatible" + list: + id: ID + title: Título + supports: Apoyos + admin: Administrador + valuator: Evaluador + valuation_group: Grupos evaluadores + geozone: Ámbito de actuación + feasibility: Viabilidad + valuation_finished: Ev. Fin. + selected: Seleccionado + visible_to_valuators: Mostrar a evaluadores + author_username: Usuario autor + incompatible: Incompatible cannot_calculate_winners: El presupuesto debe estar en las fases "Votación final", "Votación finalizada" o "Resultados" para poder calcular las propuestas ganadoras see_results: "Ver resultados" show: @@ -517,8 +519,10 @@ es: administrators: Administradores managers: Gestores moderators: Moderadores - emails: Envío de emails + messaging_users: Mensajes a usuarios newsletters: Newsletters + admin_notifications: Notificaciones + system_emails: Emails del sistema emails_download: Descarga de emails valuators: Evaluadores poll_officers: Presidentes de mesa @@ -612,6 +616,50 @@ es: body: Contenido del email body_help_text: Así es como verán el email los usuarios send_alert: ¿Estás seguro/a de que quieres enviar esta newsletter a %{n} usuarios? + admin_notifications: + create_success: Notificación creada correctamente + update_success: Notificación actualizada correctamente + send_success: Notificación enviada correctamente + delete_success: Notificación borrada correctamente + index: + section_title: Envío de notificaciones + new_notification: Crear notificación + title: Título + segment_recipient: Destinatarios + sent: Enviado + actions: Acciones + draft: Borrador + edit: Editar + delete: Borrar + preview: Previsualizar + view: Visualizar + empty_notifications: No hay notificaciones para mostrar + new: + section_title: Nueva notificación + edit: + section_title: Editar notificación + show: + section_title: Vista previa de notificación + send: Enviar + will_get_notified: (%{n} usuarios serán notificados) + got_notified: (%{n} usuarios fueron notificados) + sent_at: Enviado + title: Título + body: Texto + link: Enlace + segment_recipient: Destinatarios + preview_guide: "Así es como los usuarios verán la notificación:" + sent_guide: "Así es como los usuarios ven la notificación:" + send_alert: ¿Estás seguro/a de que quieres enviar esta notificación a %{n} usuarios? + system_emails: + preview_pending: + action: Previsualizar Pendientes + preview_of: Vista previa de %{name} + pending_to_be_sent: Este es todo el contenido pendiente de enviar + proposal_notification_digest: + title: Resumen de Notificationes de Propuestas + description: Reune todas las notificaciones de propuestas en un único mensaje, para evitar demasiados emails. + preview_detail: Los usuarios sólo recibirán las notificaciones de aquellas propuestas que siguen. emails_download: index: title: Descarga de emails @@ -1008,6 +1056,11 @@ es: image: Imagen show_image: Mostrar imagen moderated_content: "Revisa el contenido moderado por los moderadores, y confirma si la moderación se ha realizado correctamente." + view: Ver + proposal: Propuesta + author: Autor + content: Contenido + created_at: Fecha de creación spending_proposals: index: geozone_filter_all: Todos los ámbitos de actuación diff --git a/config/routes/admin.rb b/config/routes/admin.rb index 001456f6e..150d1b28a 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -159,6 +159,17 @@ namespace :admin do get :users, on: :collection end + resources :admin_notifications do + member do + post :deliver + end + end + + resources :system_emails, only: [:index] do + get :view + get :preview_pending + end + resources :emails_download, only: :index do get :generate_csv, on: :collection end diff --git a/db/dev_seeds.rb b/db/dev_seeds.rb index 966307e10..7b7af9342 100644 --- a/db/dev_seeds.rb +++ b/db/dev_seeds.rb @@ -34,5 +34,6 @@ require_relative 'dev_seeds/legislation_processes' require_relative 'dev_seeds/newsletters' require_relative 'dev_seeds/notifications' require_relative 'dev_seeds/widgets' +require_relative 'dev_seeds/admin_notifications' log "All dev seeds created successfuly 👍" diff --git a/db/dev_seeds/admin_notifications.rb b/db/dev_seeds/admin_notifications.rb new file mode 100644 index 000000000..a07077790 --- /dev/null +++ b/db/dev_seeds/admin_notifications.rb @@ -0,0 +1,28 @@ +section "Creating Admin Notifications & Templates" do + AdminNotification.create!( + title: I18n.t('seeds.admin_notification.internal_link.title'), + body: I18n.t('seeds.admin_notification.internal_link.body'), + link: Setting['url'] + I18n.t('seeds.admin_notification.internal_link.link'), + segment_recipient: 'administrators' + ).deliver + + AdminNotification.create!( + title: I18n.t('seeds.admin_notification.external_link.title'), + body: I18n.t('seeds.admin_notification.external_link.body'), + link: I18n.t('seeds.admin_notification.external_link.link'), + segment_recipient: 'administrators' + ).deliver + + AdminNotification.create!( + title: I18n.t('seeds.admin_notification.without_link.title'), + body: I18n.t('seeds.admin_notification.without_link.body'), + segment_recipient: 'administrators' + ).deliver + + AdminNotification.create!( + title: I18n.t('seeds.admin_notification.not_sent.title'), + body: I18n.t('seeds.admin_notification.not_sent.body'), + segment_recipient: 'administrators', + sent_at: nil + ) +end diff --git a/db/migrate/20180221002503_create_admin_notifications.rb b/db/migrate/20180221002503_create_admin_notifications.rb new file mode 100644 index 000000000..041931495 --- /dev/null +++ b/db/migrate/20180221002503_create_admin_notifications.rb @@ -0,0 +1,14 @@ +class CreateAdminNotifications < ActiveRecord::Migration + def change + create_table :admin_notifications do |t| + t.string :title + t.text :body + t.string :link + t.string :segment_recipient + t.integer :recipients_count + t.date :sent_at, default: nil + + t.timestamps null: false + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 45871b884..878c16869 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -30,6 +30,17 @@ ActiveRecord::Schema.define(version: 20180711224810) do add_index "activities", ["actionable_id", "actionable_type"], name: "index_activities_on_actionable_id_and_actionable_type", using: :btree add_index "activities", ["user_id"], name: "index_activities_on_user_id", using: :btree + create_table "admin_notifications", force: :cascade do |t| + t.string "title" + t.text "body" + t.string "link" + t.string "segment_recipient" + t.integer "recipients_count" + t.date "sent_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "administrators", force: :cascade do |t| t.integer "user_id" end diff --git a/spec/factories.rb b/spec/factories.rb index 77deec79a..beb64aa2d 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -1003,6 +1003,20 @@ LOREM_IPSUM sequence(:body) { |n| "Body #{n}" } end + factory :admin_notification do + title { |n| "Admin Notification title #{n}" } + body { |n| "Admin Notification body #{n}" } + link nil + segment_recipient UserSegments::SEGMENTS.sample + recipients_count nil + sent_at nil + + trait :sent do + recipients_count 1 + sent_at Time.current + end + end + factory :widget_card, class: 'Widget::Card' do sequence(:title) { |n| "Title #{n}" } sequence(:description) { |n| "Description #{n}" } diff --git a/spec/features/admin/admin_notifications_spec.rb b/spec/features/admin/admin_notifications_spec.rb new file mode 100644 index 000000000..a97170e7c --- /dev/null +++ b/spec/features/admin/admin_notifications_spec.rb @@ -0,0 +1,236 @@ +require 'rails_helper' + +feature "Admin Notifications" do + + background do + admin = create(:administrator) + login_as(admin.user) + create(:budget) + end + + context "Show" do + scenario "Valid Admin Notification" do + notification = create(:admin_notification, title: 'Notification title', + body: 'Notification body', + link: 'https://www.decide.madrid.es/vota', + segment_recipient: :all_users) + + visit admin_admin_notification_path(notification) + + expect(page).to have_content('Notification title') + expect(page).to have_content('Notification body') + expect(page).to have_content('https://www.decide.madrid.es/vota') + expect(page).to have_content('All users') + end + + scenario "Notification with invalid segment recipient" do + invalid_notification = create(:admin_notification) + invalid_notification.update_attribute(:segment_recipient, 'invalid_segment') + + visit admin_admin_notification_path(invalid_notification) + + expect(page).to have_content("Recipients user segment is invalid") + end + end + + context "Index" do + scenario "Valid Admin Notifications" do + draft = create(:admin_notification, segment_recipient: :all_users, title: 'Not yet sent') + sent = create(:admin_notification, :sent, segment_recipient: :administrators, + title: 'Sent one') + + visit admin_admin_notifications_path + + expect(page).to have_css(".admin_notification", count: 2) + + within("#admin_notification_#{draft.id}") do + expect(page).to have_content('Not yet sent') + expect(page).to have_content('All users') + expect(page).to have_content('Draft') + end + + within("#admin_notification_#{sent.id}") do + expect(page).to have_content('Sent one') + expect(page).to have_content('Administrators') + expect(page).to have_content(I18n.l(Date.current)) + end + end + + scenario "Notifications with invalid segment recipient" do + invalid_notification = create(:admin_notification) + invalid_notification.update_attribute(:segment_recipient, 'invalid_segment') + + visit admin_admin_notifications_path + + expect(page).to have_content("Recipients user segment is invalid") + end + end + + scenario "Create" do + visit admin_admin_notifications_path + click_link "New notification" + + fill_in_admin_notification_form(segment_recipient: 'Proposal authors', + title: 'This is a title', + body: 'This is a body', + link: 'http://www.dummylink.dev') + + click_button "Create Admin notification" + + expect(page).to have_content "Notification created successfully" + expect(page).to have_content "Proposal authors" + expect(page).to have_content "This is a title" + expect(page).to have_content "This is a body" + expect(page).to have_content "http://www.dummylink.dev" + end + + context "Update" do + scenario "A draft notification can be updated" do + notification = create(:admin_notification) + + visit admin_admin_notifications_path + within("#admin_notification_#{notification.id}") do + click_link "Edit" + end + + + fill_in_admin_notification_form(segment_recipient: 'All users', + title: 'Other title', + body: 'Other body', + link: '') + + click_button "Update Admin notification" + + expect(page).to have_content "Notification updated successfully" + expect(page).to have_content "All users" + expect(page).to have_content "Other title" + expect(page).to have_content "Other body" + expect(page).not_to have_content "http://www.dummylink.dev" + end + + scenario "Sent notification can not be updated" do + notification = create(:admin_notification, :sent) + + visit admin_admin_notifications_path + within("#admin_notification_#{notification.id}") do + expect(page).not_to have_link("Edit") + end + end + end + + context "Destroy" do + scenario "A draft notification can be destroyed" do + notification = create(:admin_notification) + + visit admin_admin_notifications_path + within("#admin_notification_#{notification.id}") do + click_link "Delete" + end + + expect(page).to have_content "Notification deleted successfully" + expect(page).to have_css(".notification", count: 0) + end + + scenario "Sent notification can not be destroyed" do + notification = create(:admin_notification, :sent) + + visit admin_admin_notifications_path + within("#admin_notification_#{notification.id}") do + expect(page).not_to have_link("Delete") + end + end + end + + context "Visualize" do + scenario "A draft notification can be previewed" do + notification = create(:admin_notification, segment_recipient: :administrators) + + visit admin_admin_notifications_path + within("#admin_notification_#{notification.id}") do + click_link "Preview" + end + + expect(page).to have_content "This is how the users will see the notification:" + expect(page).to have_content "Administrators (1 users will be notified)" + end + + scenario "A sent notification can be viewed" do + notification = create(:admin_notification, :sent, recipients_count: 7, + segment_recipient: :administrators) + + visit admin_admin_notifications_path + within("#admin_notification_#{notification.id}") do + click_link "View" + end + + expect(page).to have_content "This is how the users see the notification:" + expect(page).to have_content "Administrators (7 users got notified)" + end + end + + scenario 'Errors on create' do + visit new_admin_admin_notification_path + + click_button "Create Admin notification" + + expect(page).to have_content error_message + end + + scenario "Errors on update" do + notification = create(:admin_notification) + visit edit_admin_admin_notification_path(notification) + + fill_in :admin_notification_title, with: '' + click_button "Update Admin notification" + + expect(page).to have_content error_message + end + + context "Send notification", :js do + scenario "A draft Admin notification can be sent", :js do + 2.times { create(:user) } + notification = create(:admin_notification, segment_recipient: :all_users) + total_users = notification.list_of_recipients.count + confirm_message = "Are you sure you want to send this notification to #{total_users} users?" + + visit admin_admin_notification_path(notification) + + accept_confirm { click_link "Send" } + + expect(page).to have_content "Notification sent successfully" + + User.all.each do |user| + expect(user.notifications.count).to eq(1) + end + end + + scenario "A sent Admin notification can not be sent", :js do + notification = create(:admin_notification, :sent) + + visit admin_admin_notification_path(notification) + + expect(page).not_to have_link("Send") + end + + scenario "Admin notification with invalid segment recipient cannot be sent", :js do + invalid_notification = create(:admin_notification) + invalid_notification.update_attribute(:segment_recipient, 'invalid_segment') + visit admin_admin_notification_path(invalid_notification) + + expect(page).not_to have_link("Send") + end + end + + scenario "Select list of users to send notification" do + UserSegments::SEGMENTS.each do |user_segment| + segment_recipient = I18n.t("admin.segment_recipient.#{user_segment}") + + visit new_admin_admin_notification_path + + fill_in_admin_notification_form(segment_recipient: segment_recipient) + click_button "Create Admin notification" + + expect(page).to have_content(I18n.t("admin.segment_recipient.#{user_segment}")) + end + end +end diff --git a/spec/features/admin/budget_investments_spec.rb b/spec/features/admin/budget_investments_spec.rb index 874615475..8ea9ea5cc 100644 --- a/spec/features/admin/budget_investments_spec.rb +++ b/spec/features/admin/budget_investments_spec.rb @@ -589,8 +589,8 @@ feature 'Admin budget investments' do scenario 'Sort by ID' do visit admin_budget_budget_investments_path(budget, sort_by: 'id') - expect('B First Investment').to appear_before('A Second Investment') - expect('A Second Investment').to appear_before('C Third Investment') + expect('C Third Investment').to appear_before('A Second Investment') + expect('A Second Investment').to appear_before('B First Investment') end scenario 'Sort by title' do @@ -603,8 +603,8 @@ feature 'Admin budget investments' do scenario 'Sort by supports' do visit admin_budget_budget_investments_path(budget, sort_by: 'supports') - expect('C Third Investment').to appear_before('A Second Investment') - expect('A Second Investment').to appear_before('B First Investment') + expect('B First Investment').to appear_before('A Second Investment') + expect('A Second Investment').to appear_before('C Third Investment') end end @@ -1192,13 +1192,15 @@ feature 'Admin budget investments' do cached_votes_up: 88, price: 99, valuators: [], valuator_groups: [valuator_group], - administrator: admin) + administrator: admin, + visible_to_valuators: true) second_investment = create(:budget_investment, :unfeasible, title: "Alt Investment", budget: budget, group: budget_group, heading: second_budget_heading, cached_votes_up: 66, price: 88, valuators: [valuator], - valuator_groups: []) + valuator_groups: [], + visible_to_valuators: false) visit admin_budget_budget_investments_path(budget) @@ -1209,10 +1211,11 @@ feature 'Admin budget investments' do expect(header).to match(/filename="budget_investments.csv"$/) csv_contents = "ID,Title,Supports,Administrator,Valuator,Valuation Group,Scope of operation,"\ - "Feasibility,Val. Fin.,Selected\n#{first_investment.id},Le Investment,88,"\ - "Admin,-,Valuator Group,Budget Heading,Feasible (€99),Yes,Yes\n"\ - "#{second_investment.id},Alt Investment,66,No admin assigned,Valuator,-,"\ - "Other Heading,Unfeasible,No,No\n" + "Feasibility,Val. Fin.,Selected,Show to valuators,Author username\n"\ + "#{first_investment.id},Le Investment,88,Admin,-,Valuator Group,"\ + "Budget Heading,Feasible (€99),Yes,Yes,Yes,#{first_investment.author.username}\n#{second_investment.id},"\ + "Alt Investment,66,No admin assigned,Valuator,-,Other Heading,"\ + "Unfeasible,No,No,No,#{second_investment.author.username}\n" expect(page.body).to eq(csv_contents) end diff --git a/spec/features/admin/signature_sheets_spec.rb b/spec/features/admin/signature_sheets_spec.rb index bcde7bfc0..2417fb176 100644 --- a/spec/features/admin/signature_sheets_spec.rb +++ b/spec/features/admin/signature_sheets_spec.rb @@ -7,15 +7,28 @@ feature 'Signature sheets' do login_as(admin.user) end - scenario "Index" do - 3.times { create(:signature_sheet) } + context "Index" do + scenario 'Lists all signature_sheets' do + 3.times { create(:signature_sheet) } - visit admin_signature_sheets_path + visit admin_signature_sheets_path - expect(page).to have_css(".signature_sheet", count: 3) + expect(page).to have_css(".signature_sheet", count: 3) - SignatureSheet.all.each do |signature_sheet| - expect(page).to have_content signature_sheet.name + SignatureSheet.all.each do |signature_sheet| + expect(page).to have_content signature_sheet.name + end + end + + scenario 'Orders signature_sheets by created_at DESC' do + signature_sheet1 = create(:signature_sheet) + signature_sheet2 = create(:signature_sheet) + signature_sheet3 = create(:signature_sheet) + + visit admin_signature_sheets_path + + expect(signature_sheet3.name).to appear_before(signature_sheet2.name) + expect(signature_sheet2.name).to appear_before(signature_sheet1.name) end end @@ -78,7 +91,7 @@ feature 'Signature sheets' do expect(page).to have_content "Citizen proposal #{proposal.id}" expect(page).to have_content "12345678Z, 123A, 123B" - expect(page).to have_content signature_sheet.created_at.strftime("%d %b %H:%M") + expect(page).to have_content signature_sheet.created_at.strftime("%B %d, %Y %H:%M") expect(page).to have_content user.name within("#document_count") do diff --git a/spec/features/admin/system_emails_spec.rb b/spec/features/admin/system_emails_spec.rb new file mode 100644 index 000000000..dd14e87bc --- /dev/null +++ b/spec/features/admin/system_emails_spec.rb @@ -0,0 +1,70 @@ +require 'rails_helper' + +feature "System Emails" do + + background do + admin = create(:administrator) + login_as(admin.user) + end + + context "Index" do + scenario "Lists all system emails with correct actions" do + visit admin_system_emails_path + + within('#proposal_notification_digest') do + expect(page).to have_link('View') + end + end + end + + context "View" do + scenario "#proposal_notification_digest" do + proposal_a = create(:proposal, title: 'Proposal A') + proposal_b = create(:proposal, title: 'Proposal B') + proposal_notification_a = create(:proposal_notification, proposal: proposal_a, + title: 'Proposal A Title', + body: 'Proposal A Notification Body') + proposal_notification_b = create(:proposal_notification, proposal: proposal_b, + title: 'Proposal B Title', + body: 'Proposal B Notification Body') + create(:notification, notifiable: proposal_notification_a) + create(:notification, notifiable: proposal_notification_b) + + visit admin_system_email_view_path('proposal_notification_digest') + + expect(page).to have_content('Proposal notifications in') + expect(page).to have_link('Proposal A Title', href: proposal_url(proposal_a, + anchor: 'tab-notifications')) + expect(page).to have_link('Proposal B Title', href: proposal_url(proposal_b, + anchor: 'tab-notifications')) + expect(page).to have_content('Proposal A Notification Body') + expect(page).to have_content('Proposal B Notification Body') + end + end + + context "Preview Pending" do + scenario "#proposal_notification_digest" do + proposal_a = create(:proposal, title: 'Proposal A') + proposal_b = create(:proposal, title: 'Proposal B') + proposal_notification_a = create(:proposal_notification, proposal: proposal_a, + title: 'Proposal A Title', + body: 'Proposal A Notification Body') + proposal_notification_b = create(:proposal_notification, proposal: proposal_b, + title: 'Proposal B Title', + body: 'Proposal B Notification Body') + create(:notification, notifiable: proposal_notification_a, emailed_at: nil) + create(:notification, notifiable: proposal_notification_b, emailed_at: nil) + + visit admin_system_email_preview_pending_path('proposal_notification_digest') + + expect(page).to have_content('This is the content pending to be sent') + expect(page).to have_link('Proposal A', href: proposal_url(proposal_a)) + expect(page).to have_link('Proposal B', href: proposal_url(proposal_b)) + expect(page).to have_link('Proposal A Title', href: proposal_url(proposal_a, + anchor: 'tab-notifications')) + expect(page).to have_link('Proposal B Title', href: proposal_url(proposal_b, + anchor: 'tab-notifications')) + end + end + +end diff --git a/spec/features/budgets/budgets_spec.rb b/spec/features/budgets/budgets_spec.rb index 2400a8c6a..db0e6b806 100644 --- a/spec/features/budgets/budgets_spec.rb +++ b/spec/features/budgets/budgets_spec.rb @@ -379,6 +379,58 @@ feature 'Budgets' do expect(page).to have_link "See investments not selected for balloting phase" end + scenario "Take into account headings with the same name from a different budget" do + group1 = create(:budget_group, budget: budget, name: "New York") + heading1 = create(:budget_heading, group: group1, name: "Brooklyn") + heading2 = create(:budget_heading, group: group1, name: "Queens") + + budget2 = create(:budget) + group2 = create(:budget_group, budget: budget2, name: "New York") + heading3 = create(:budget_heading, group: group2, name: "Brooklyn") + heading4 = create(:budget_heading, group: group2, name: "Queens") + + visit budget_path(budget) + click_link "New York" + + expect(page).to have_css("#budget_heading_#{heading1.id}") + expect(page).to have_css("#budget_heading_#{heading2.id}") + + expect(page).to_not have_css("#budget_heading_#{heading3.id}") + expect(page).to_not have_css("#budget_heading_#{heading4.id}") + end + + scenario "See results button is showed if the budget has finished for all users" do + user = create(:user) + admin = create(:administrator) + budget = create(:budget, :finished) + + login_as(user) + visit budget_path(budget) + expect(page).to have_link "See results" + + logout + + login_as(admin.user) + visit budget_path(budget) + expect(page).to have_link "See results" + end + + scenario "See results button isn't showed if the budget hasn't finished for all users" do + user = create(:user) + admin = create(:administrator) + budget = create(:budget, :balloting) + + login_as(user) + visit budget_path(budget) + expect(page).not_to have_link "See results" + + logout + + login_as(admin.user) + visit budget_path(budget) + expect(page).not_to have_link "See results" + end + end context "In Drafting phase" do diff --git a/spec/features/budgets/investments_spec.rb b/spec/features/budgets/investments_spec.rb index aed73ffeb..ce97af1fc 100644 --- a/spec/features/budgets/investments_spec.rb +++ b/spec/features/budgets/investments_spec.rb @@ -1081,6 +1081,24 @@ feature 'Budget Investments' do expect(page).not_to have_content("Local government is not competent in this matter") end + scenario "Show (unfeasible budget investment with valuation not finished)" do + user = create(:user) + login_as(user) + + investment = create(:budget_investment, + :unfeasible, + valuation_finished: false, + budget: budget, + group: group, + heading: heading, + unfeasibility_explanation: 'Local government is not competent in this matter') + + visit budget_investment_path(budget_id: budget.id, id: investment.id) + + expect(page).not_to have_content("Unfeasibility explanation") + expect(page).not_to have_content("Local government is not competent in this matter") + end + scenario "Show milestones", :js do user = create(:user) investment = create(:budget_investment) diff --git a/spec/features/emails_spec.rb b/spec/features/emails_spec.rb index 4d009bfbc..91f8b31b6 100644 --- a/spec/features/emails_spec.rb +++ b/spec/features/emails_spec.rb @@ -306,14 +306,14 @@ feature 'Emails' do expect(email).to have_body_text(notification1.notifiable.body) expect(email).to have_body_text(proposal1.author.name) - expect(email).to have_body_text(/#{notification_path(notification1)}/) + expect(email).to have_body_text(/#{proposal_path(proposal1, anchor: 'tab-notifications')}/) expect(email).to have_body_text(/#{proposal_path(proposal1, anchor: 'comments')}/) expect(email).to have_body_text(/#{proposal_path(proposal1, anchor: 'social-share')}/) expect(email).to have_body_text(proposal2.title) expect(email).to have_body_text(notification2.notifiable.title) expect(email).to have_body_text(notification2.notifiable.body) - expect(email).to have_body_text(/#{notification_path(notification2)}/) + expect(email).to have_body_text(/#{proposal_path(proposal2, anchor: 'tab-notifications')}/) expect(email).to have_body_text(/#{proposal_path(proposal2, anchor: 'comments')}/) expect(email).to have_body_text(/#{proposal_path(proposal2, anchor: 'social-share')}/) expect(email).to have_body_text(proposal2.author.name) diff --git a/spec/features/moderation/proposal_notifications_spec.rb b/spec/features/moderation/proposal_notifications_spec.rb index 4af3a815d..396173190 100644 --- a/spec/features/moderation/proposal_notifications_spec.rb +++ b/spec/features/moderation/proposal_notifications_spec.rb @@ -16,7 +16,7 @@ feature 'Moderate proposal notifications' do accept_confirm { click_link 'Hide' } end - expect(page).to have_css("#proposal_notification_#{proposal.id}.faded") + expect(page).to have_css("#proposal_notification_#{proposal_notification.id}.faded") logout login_as(citizen) diff --git a/spec/features/notifications_spec.rb b/spec/features/notifications_spec.rb index 58aebb472..482d38b5a 100644 --- a/spec/features/notifications_spec.rb +++ b/spec/features/notifications_spec.rb @@ -128,4 +128,58 @@ feature "Notifications" do expect(page).to_not have_css("#notifications") end + scenario "Notification's notifiable model no longer includes Notifiable module" do + create(:notification, notifiable: create(:spending_proposal), user: user) + create(:notification, notifiable: create(:poll_question), user: user) + + click_notifications_icon + expect(page).to have_content('This resource is not available anymore.', count: 2) + end + + context "Admin Notifications" do + let(:admin_notification) do + create(:admin_notification, title: 'Notification title', + body: 'Notification body', + link: 'https://www.external.link.dev/', + segment_recipient: 'all_users') + end + + let!(:notification) do + create(:notification, user: user, notifiable: admin_notification) + end + + before do + login_as user + end + + scenario "With external link" do + visit notifications_path + expect(page).to have_content('Notification title') + expect(page).to have_content('Notification body') + + first("#notification_#{notification.id} a").click + expect(page.current_url).to eq('https://www.external.link.dev/') + end + + scenario "With internal link" do + admin_notification.update_attributes(link: '/stats') + + visit notifications_path + expect(page).to have_content('Notification title') + expect(page).to have_content('Notification body') + + first("#notification_#{notification.id} a").click + expect(page).to have_current_path('/stats') + end + + scenario "Without a link" do + admin_notification.update_attributes(link: '/stats') + + visit notifications_path + expect(page).to have_content('Notification title') + expect(page).to have_content('Notification body') + expect(page).not_to have_link(notification_path(notification), visible: false) + end + end + end diff --git a/spec/models/admin_notification_spec.rb b/spec/models/admin_notification_spec.rb new file mode 100644 index 000000000..eeb974e83 --- /dev/null +++ b/spec/models/admin_notification_spec.rb @@ -0,0 +1,91 @@ +require 'rails_helper' + +describe AdminNotification do + let(:admin_notification) { build(:admin_notification) } + + it "is valid" do + expect(admin_notification).to be_valid + end + + it 'is not valid without a title' do + admin_notification.title = nil + expect(admin_notification).not_to be_valid + end + + it 'is not valid without a body' do + admin_notification.body = nil + expect(admin_notification).not_to be_valid + end + + it 'is not valid without a segment_recipient' do + admin_notification.segment_recipient = nil + expect(admin_notification).not_to be_valid + end + + describe '#complete_link_url' do + it 'does not change link if there is no value' do + expect(admin_notification.link).to be_nil + end + + it 'fixes a link without http://' do + admin_notification.link = 'lol.consul.dev' + + expect(admin_notification).to be_valid + expect(admin_notification.link).to eq('http://lol.consul.dev') + end + + it 'fixes a link with wwww. but without http://' do + admin_notification.link = 'www.lol.consul.dev' + + expect(admin_notification).to be_valid + expect(admin_notification.link).to eq('http://www.lol.consul.dev') + end + + it 'does not modify a link with http://' do + admin_notification.link = 'http://lol.consul.dev' + + expect(admin_notification).to be_valid + expect(admin_notification.link).to eq('http://lol.consul.dev') + end + + it 'does not modify a link with https://' do + admin_notification.link = 'https://lol.consul.dev' + + expect(admin_notification).to be_valid + expect(admin_notification.link).to eq('https://lol.consul.dev') + end + + it 'does not modify a link with http://wwww.' do + admin_notification.link = 'http://www.lol.consul.dev' + + expect(admin_notification).to be_valid + expect(admin_notification.link).to eq('http://www.lol.consul.dev') + end + end + + describe '#valid_segment_recipient?' do + it 'is false when segment_recipient value is invalid' do + admin_notification.update(segment_recipient: 'invalid_segment_name') + error = 'The user recipients segment is invalid' + + expect(admin_notification).not_to be_valid + expect(admin_notification.errors.messages[:segment_recipient]).to include(error) + end + end + + describe '#list_of_recipients' do + let(:erased_user) { create(:user, username: 'erased_user') } + + before do + 2.times { create(:user) } + erased_user.erase + admin_notification.update(segment_recipient: 'all_users') + end + + it 'returns list of all active users' do + expect(admin_notification.list_of_recipients.count).to eq(2) + expect(admin_notification.list_of_recipients).not_to include(erased_user) + end + end + +end diff --git a/spec/models/budget/investment_spec.rb b/spec/models/budget/investment_spec.rb index a4e1d246d..c81ada18a 100644 --- a/spec/models/budget/investment_spec.rb +++ b/spec/models/budget/investment_spec.rb @@ -308,6 +308,30 @@ describe Budget::Investment do end end + describe "#by_budget" do + + it "returns investments scoped by budget" do + budget1 = create(:budget) + budget2 = create(:budget) + + group1 = create(:budget_group, budget: budget1) + group2 = create(:budget_group, budget: budget2) + + heading1 = create(:budget_heading, group: group1) + heading2 = create(:budget_heading, group: group2) + + investment1 = create(:budget_investment, heading: heading1) + investment2 = create(:budget_investment, heading: heading1) + investment3 = create(:budget_investment, heading: heading2) + + investments_by_budget = Budget::Investment.by_budget(budget1) + + expect(investments_by_budget).to include investment1 + expect(investments_by_budget).to include investment2 + expect(investments_by_budget).to_not include investment3 + end + end + describe "#by_admin" do it "returns investments assigned to specific administrator" do investment1 = create(:budget_investment, administrator_id: 33) diff --git a/spec/support/common_actions/notifications.rb b/spec/support/common_actions/notifications.rb index 4ae98c3d0..e387ce1fd 100644 --- a/spec/support/common_actions/notifications.rb +++ b/spec/support/common_actions/notifications.rb @@ -53,4 +53,11 @@ module Notifications field_check_message = 'Please check the marked fields to know how to correct them:' /\d errors? prevented this #{resource_model} from being saved. #{field_check_message}/ end + + def fill_in_admin_notification_form(options = {}) + select (options[:segment_recipient] || 'All users'), from: :admin_notification_segment_recipient + fill_in :admin_notification_title, with: (options[:title] || 'This is the notification title') + fill_in :admin_notification_body, with: (options[:body] || 'This is the notification body') + fill_in :admin_notification_link, with: (options[:link] || 'https://www.decide.madrid.es/vota') + end end