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? %>
+
+
+
+ | <%= 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_notifications.order(created_at: :desc).each do |admin_notification| %>
+
+ |
+ <%= 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 %>
+ |
+
+ <% 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 @@
- | <%= 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") %> |
<% if params[:filter] == "selected" %>
- <%= t("admin.budget_investments.index.table_incompatible") %> |
+ <%= t("admin.budget_investments.index.list.incompatible") %> |
<% 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 %>
- <%= l(signature_sheet.created_at, format: :short) %>
+ <%= l(signature_sheet.created_at, format: :long) %>
|
<% 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.shared.title") %> |
+ <%= t("admin.shared.description") %> |
+ <%= t("admin.shared.actions") %> |
+
+
+
+ <% @system_emails.each do |system_email_title, system_email_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 %>
+ |
+
+ <% 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") %>
+
+
+ <%= render file: "app/views/layouts/_mailer_header.html.erb" %>
+
+
+
+
+ <%= render file: "app/views/mailer/#{@system_email}.html.erb" %>
+
+
+
+
+ <%= render file: "app/views/layouts/_mailer_footer.html.erb" %>
+
+
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 %>
- <%= render @notifications %>
+ <% @notifications.each do |notification| %>
+ <%= render partial: '/notifications/notification', locals: {notification: notification} %>
+ <% end %>
<% 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
| |