diff --git a/app/assets/images/icon_mailer_comment.png b/app/assets/images/icon_mailer_comment.png new file mode 100644 index 000000000..d3c9bff73 Binary files /dev/null and b/app/assets/images/icon_mailer_comment.png differ diff --git a/app/assets/images/icon_mailer_reply.png b/app/assets/images/icon_mailer_reply.png new file mode 100644 index 000000000..0c9159e84 Binary files /dev/null and b/app/assets/images/icon_mailer_reply.png differ diff --git a/app/assets/images/icon_mailer_share.png b/app/assets/images/icon_mailer_share.png new file mode 100644 index 000000000..f95d44141 Binary files /dev/null and b/app/assets/images/icon_mailer_share.png differ diff --git a/app/assets/stylesheets/_settings.scss b/app/assets/stylesheets/_settings.scss index 22e7ac8de..57a1afb7c 100644 --- a/app/assets/stylesheets/_settings.scss +++ b/app/assets/stylesheets/_settings.scss @@ -3,6 +3,7 @@ // // Table of Contents: // +// 0. Custom variables // 1. Global // 2. Breakpoints // 3. The Grid @@ -43,6 +44,70 @@ @import 'util/util'; +// 0. Custom variables +// -------------------- + +$base-font-size: rem-calc(17); +$base-line-height: rem-calc(26); +$small-font-size: rem-calc(14); +$line-height: rem-calc(24); + +$brand: #004A83; +$body: #E9E9E9; +$background: #EDEFF0; +$border: #DEE0E3; +$dark: darken($brand, 10%); + +$text: #222222; +$text-medium: #999999; +$text-light: #CCCCCC; + +$link: #2895F1; +$link-hover: #2178BF; + +$debates: #008CCF; +$votes-bg: #26AEEE; +$votes-border: #1F94CB; + +$votes-like: #7BD2A8; +$votes-like-act: #5D9E7F; +$votes-unlike: #EF8585; +$votes-unlike-act: #BD6A6A; + +$delete: #F04124; +$check: #46DB91; + +$proposals: #FFA42D; +$proposals-border: #CC8425; + +$budget: #454372; +$budget-hover: #7571BF; + +$highlight: #E7F2FC; +$featured: #FED900; + +$footer-bg: #DEE0E2; +$footer-color: #171819; +$footer-link: #454A4C; +$footer-border: #BFC1C3; + +$success-bg: #DFF0D8; +$success-border: #D6E9C6; +$color-success: #3C763D; + +$info-bg: #D9EDF7; +$info-border: #BCE8F1; +$color-info: #31708F; + +$warning-bg: #FCF8E3; +$warning-border: #FAEBCC; +$color-warning: #8A6D3B; + +$alert-bg: #F2DEDE; +$alert-border: #EBCCD1; +$color-alert: #A94442; + + // 1. Global // --------- @@ -127,7 +192,7 @@ $header-color: inherit; $header-lineheight: 1.4; $header-margin-bottom: 0.5rem; $header-text-rendering: optimizeLegibility; -$small-font-size: 80%; +$small-font-size: rem-calc(14); $header-small-font-color: $medium-gray; $paragraph-lineheight: 1.6; $paragraph-margin-bottom: 1rem; @@ -510,13 +575,13 @@ $show-header-for-stacked: false; $tab-margin: 0; $tab-background: $white; -$tab-background-active: $light-gray; -$tab-item-font-size: rem-calc(12); +$tab-background-active: $white; +$tab-item-font-size: $base-font-size; $tab-item-background-hover: $white; -$tab-item-padding: 1.25rem 1.5rem; +$tab-item-padding: $line-height/2 0; $tab-expand-max: 6; $tab-content-background: $white; -$tab-content-border: $light-gray; +$tab-content-border: $border; $tab-content-color: foreground($tab-background, $primary-color); $tab-content-padding: 1rem; @@ -563,66 +628,3 @@ $topbar-submenu-background: $topbar-background; $topbar-title-spacing: 1rem; $topbar-input-width: 200px; $topbar-unstack-breakpoint: medium; - -// 37. Custom variables -// -------------------- - -$base-font-size: rem-calc(17); -$base-line-height: rem-calc(26); -$small-font-size: rem-calc(14); -$line-height: rem-calc(24); - -$brand: #004A83; -$body: #E9E9E9; -$background: #EDEFF0; -$border: #DEE0E3; -$dark: darken($brand, 10%); - -$text: #222222; -$text-medium: #999999; -$text-light: #CCCCCC; - -$link: #2895F1; -$link-hover: #2178BF; - -$debates: #008CCF; -$votes-bg: #26AEEE; -$votes-border: #1F94CB; - -$votes-like: #7BD2A8; -$votes-like-act: #5D9E7F; -$votes-unlike: #EF8585; -$votes-unlike-act: #BD6A6A; - -$delete: #F04124; -$check: #46DB91; - -$proposals: #FFA42D; -$proposals-border: #CC8425; - -$budget: #454372; -$budget-hover: #7571BF; - -$highlight: #E7F2FC; -$featured: #FED900; - -$footer-bg: #DEE0E2; -$footer-color: #171819; -$footer-link: #454A4C; -$footer-border: #BFC1C3; - -$success-bg: #DFF0D8; -$success-border: #D6E9C6; -$color-success: #3C763D; - -$info-bg: #D9EDF7; -$info-border: #BCE8F1; -$color-info: #31708F; - -$warning-bg: #FCF8E3; -$warning-border: #FAEBCC; -$color-warning: #8A6D3B; - -$alert-bg: #F2DEDE; -$alert-border: #EBCCD1; -$color-alert: #A94442; diff --git a/app/assets/stylesheets/layout.scss b/app/assets/stylesheets/layout.scss index b5a7ff714..9152955dc 100644 --- a/app/assets/stylesheets/layout.scss +++ b/app/assets/stylesheets/layout.scss @@ -219,10 +219,44 @@ a { float: left; } +.tabs-content { + border: 0; +} + +.tabs { + border: { + left: 0; + right: 0; + top: 0; + }; + margin-bottom: $line-height; + + .tabs-title > a { + color: $text-medium; + margin-bottom: rem-calc(-1); + margin-right: $line-height; + + &[aria-selected='true'], + &.is-active { + color: $brand; + border-bottom: 2px solid $brand; + font-weight: bold; + } + } + + h2 { + font-size: $base-font-size; + } +} + .no-max-width { max-width: none; } +.button.float-right ~ .button.float-right { + margin: 0 $line-height/2; +} + // 02. Header // ---------- @@ -1464,7 +1498,6 @@ table { .comments { background: $white; background-repeat: repeat-x; - padding-top: $line-height; padding-bottom: $line-height*4; h2 { @@ -1488,7 +1521,7 @@ table { .comment-votes { color: $text-medium; - font-size: $small-font-size; + font-size: rem-calc(14); line-height: $line-height; a { @@ -1651,7 +1684,7 @@ table { &:first-child { padding-left: $line-height*1.5; - width: 80%; + width: 75%; } &:before { diff --git a/app/assets/stylesheets/participation.scss b/app/assets/stylesheets/participation.scss index 502fe62aa..6e5b4273a 100644 --- a/app/assets/stylesheets/participation.scss +++ b/app/assets/stylesheets/participation.scss @@ -715,6 +715,16 @@ } } +.more-info { + clear: both; + color: $text-medium; + font-size: $small-font-size; + + a { + color: $text-medium; + } +} + .debate { .votes { diff --git a/app/controllers/account_controller.rb b/app/controllers/account_controller.rb index 9a0f89bae..9212eb4fa 100644 --- a/app/controllers/account_controller.rb +++ b/app/controllers/account_controller.rb @@ -25,7 +25,7 @@ class AccountController < ApplicationController if @account.organization? params.require(:account).permit(:phone_number, :email_on_comment, :email_on_comment_reply, :newsletter, organization_attributes: [:name, :responsible_name]) else - params.require(:account).permit(:username, :public_activity, :email_on_comment, :email_on_comment_reply, :newsletter) + params.require(:account).permit(:username, :public_activity, :email_on_comment, :email_on_comment_reply, :email_on_direct_message, :email_digest, :newsletter) end end diff --git a/app/controllers/direct_messages_controller.rb b/app/controllers/direct_messages_controller.rb new file mode 100644 index 000000000..07f39d941 --- /dev/null +++ b/app/controllers/direct_messages_controller.rb @@ -0,0 +1,36 @@ +class DirectMessagesController < ApplicationController + load_and_authorize_resource + + def new + @receiver = User.find(params[:user_id]) + @direct_message = DirectMessage.new(receiver: @receiver) + end + + def create + @sender = current_user + @receiver = User.find(params[:user_id]) + + @direct_message = DirectMessage.new(parsed_params) + if @direct_message.save + Mailer.direct_message_for_receiver(@direct_message).deliver_later + Mailer.direct_message_for_sender(@direct_message).deliver_later + redirect_to [@receiver, @direct_message], notice: I18n.t("flash.actions.create.direct_message") + else + render :new + end + end + + def show + @direct_message = DirectMessage.find(params[:id]) + end + + private + + def direct_message_params + params.require(:direct_message).permit(:title, :body) + end + + def parsed_params + direct_message_params.merge(sender: @sender, receiver: @receiver) + end +end \ No newline at end of file diff --git a/app/controllers/management/proposals_controller.rb b/app/controllers/management/proposals_controller.rb index 4f6ca18c4..2ce74d6d5 100644 --- a/app/controllers/management/proposals_controller.rb +++ b/app/controllers/management/proposals_controller.rb @@ -13,6 +13,7 @@ class Management::ProposalsController < Management::BaseController def show super + @notifications = @proposal.notifications redirect_to management_proposal_path(@proposal), status: :moved_permanently if request.path != management_proposal_path(@proposal) end diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb index a4ec31b50..ab346e802 100644 --- a/app/controllers/notifications_controller.rb +++ b/app/controllers/notifications_controller.rb @@ -9,7 +9,7 @@ class NotificationsController < ApplicationController def show @notification = current_user.notifications.find(params[:id]) - redirect_to url_for(@notification.notifiable) + redirect_to url_for(@notification.linkable_resource) end def mark_all_as_read diff --git a/app/controllers/proposal_notifications_controller.rb b/app/controllers/proposal_notifications_controller.rb new file mode 100644 index 000000000..fc9cdf3d8 --- /dev/null +++ b/app/controllers/proposal_notifications_controller.rb @@ -0,0 +1,33 @@ +class ProposalNotificationsController < ApplicationController + load_and_authorize_resource except: [:new] + + def new + @proposal = Proposal.find(params[:proposal_id]) + @notification = ProposalNotification.new(proposal_id: @proposal.id) + authorize! :new, @notification + end + + def create + @notification = ProposalNotification.new(proposal_notification_params) + @proposal = Proposal.find(proposal_notification_params[:proposal_id]) + if @notification.save + @proposal.voters.each do |voter| + Notification.add(voter.id, @notification) + end + redirect_to @notification, notice: I18n.t("flash.actions.create.proposal_notification") + else + render :new + end + end + + def show + @notification = ProposalNotification.find(params[:id]) + end + + private + + def proposal_notification_params + params.require(:proposal_notification).permit(:title, :body, :proposal_id) + end + +end \ No newline at end of file diff --git a/app/controllers/proposals_controller.rb b/app/controllers/proposals_controller.rb index 0b350f687..578395ab3 100644 --- a/app/controllers/proposals_controller.rb +++ b/app/controllers/proposals_controller.rb @@ -2,7 +2,6 @@ class ProposalsController < ApplicationController include CommentableActions include FlagActions - before_action :parse_search_terms, only: [:index, :suggest] before_action :parse_advanced_search_terms, only: :index before_action :parse_tag_filter, only: :index @@ -22,6 +21,7 @@ class ProposalsController < ApplicationController def show super + @notifications = @proposal.notifications redirect_to proposal_path(@proposal), status: :moved_permanently if request.path != proposal_path(@proposal) end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index af4005e06..fbd77f184 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -3,6 +3,7 @@ class UsersController < ApplicationController load_and_authorize_resource helper_method :authorized_for_filter? + helper_method :author? helper_method :author_or_admin? def show @@ -65,8 +66,12 @@ class UsersController < ApplicationController @user.public_activity || authorized_current_user? end + def author? + @author ||= current_user && (current_user == @user) + end + def author_or_admin? - @author_or_admin ||= current_user && (current_user == @user || current_user.administrator?) + @author_or_admin ||= current_user && (author? || current_user.administrator?) end def authorized_current_user? diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index c5f2e6aca..1f52f0eea 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -35,4 +35,9 @@ module ApplicationHelper } Redcarpet::Markdown.new(renderer, extensions).render(text).html_safe end + + def author_of?(authorable, user) + return false if authorable.blank? || user.blank? + authorable.author_id == user.id + end end diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb index 281163380..2b907535f 100644 --- a/app/helpers/notifications_helper.rb +++ b/app/helpers/notifications_helper.rb @@ -1,6 +1,4 @@ module NotificationsHelper - def notification_action(notification) - notification.notifiable_type == "Comment" ? "replies_to" : "comments_on" - end + end diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb index c1f3e4bb6..8838f522f 100644 --- a/app/mailers/application_mailer.rb +++ b/app/mailers/application_mailer.rb @@ -1,5 +1,5 @@ class ApplicationMailer < ActionMailer::Base helper :settings - default from: "participacion@madrid.es" + default from: "Decide Madrid " layout 'mailer' end diff --git a/app/mailers/mailer.rb b/app/mailers/mailer.rb index 82abbb205..d987e34cc 100644 --- a/app/mailers/mailer.rb +++ b/app/mailers/mailer.rb @@ -42,6 +42,32 @@ class Mailer < ApplicationMailer end end + def direct_message_for_receiver(direct_message) + @direct_message = direct_message + @receiver = @direct_message.receiver + + with_user(@receiver) do + mail(to: @receiver.email, subject: t('mailers.direct_message_for_receiver.subject')) + end + end + + def direct_message_for_sender(direct_message) + @direct_message = direct_message + @sender = @direct_message.sender + + with_user(@sender) do + mail(to: @sender.email, subject: t('mailers.direct_message_for_sender.subject')) + end + end + + def proposal_notification_digest(user) + @notifications = user.notifications.where(notifiable_type: "ProposalNotification") + + with_user(user) do + mail(to: user.email, subject: t('mailers.proposal_notification_digest.title', org_name: Setting['org_name'])) + end + end + private def with_user(user, &block) diff --git a/app/models/abilities/common.rb b/app/models/abilities/common.rb index 6dd36d5b0..f82e78b58 100644 --- a/app/models/abilities/common.rb +++ b/app/models/abilities/common.rb @@ -46,11 +46,14 @@ module Abilities can :vote_featured, Proposal can :vote, SpendingProposal can :create, SpendingProposal + can :create, DirectMessage + can :show, DirectMessage, sender_id: user.id end + can [:create, :show], ProposalNotification, proposal: { author_id: user.id } + can :create, Annotation can [:update, :destroy], Annotation, user_id: user.id - end end end diff --git a/app/models/abilities/everyone.rb b/app/models/abilities/everyone.rb index 21e142c05..39a7f69f5 100644 --- a/app/models/abilities/everyone.rb +++ b/app/models/abilities/everyone.rb @@ -10,6 +10,7 @@ module Abilities can :read, Legislation can :read, User can [:search, :read], Annotation + can :new, DirectMessage end end end diff --git a/app/models/direct_message.rb b/app/models/direct_message.rb new file mode 100644 index 000000000..14083f2c7 --- /dev/null +++ b/app/models/direct_message.rb @@ -0,0 +1,22 @@ +class DirectMessage < ActiveRecord::Base + belongs_to :sender, class_name: 'User', foreign_key: 'sender_id' + belongs_to :receiver, class_name: 'User', foreign_key: 'receiver_id' + + validates :title, presence: true + validates :body, presence: true + validates :sender, presence: true + validates :receiver, presence: true + validate :max_per_day + + scope :today, lambda { where('DATE(created_at) = ?', Date.today) } + + def max_per_day + return if errors.any? + max = Setting[:direct_message_max_per_day] + + if sender.direct_messages_sent.today.count >= max.to_i + errors.add(:title, I18n.t('activerecord.errors.models.direct_message.attributes.max_per_day.invalid', max: max)) + end + end + +end diff --git a/app/models/notification.rb b/app/models/notification.rb index 1cb500ccf..9695c1b01 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -21,4 +21,30 @@ class Notification < ActiveRecord::Base Notification.create!(user_id: user_id, notifiable: notifiable) end end + + def notifiable_title + case notifiable.class.name + when "ProposalNotification" + notifiable.proposal.title + when "Comment" + notifiable.commentable.title + else + notifiable.title + end + end + + def notifiable_action + case notifiable_type + when "ProposalNotification" + "proposal_notification" + when "Comment" + "replies_to" + else + "comments_on" + end + end + + def linkable_resource + notifiable.is_a?(ProposalNotification) ? notifiable.proposal : notifiable + end end \ No newline at end of file diff --git a/app/models/proposal.rb b/app/models/proposal.rb index d2b97f1c5..7d2bddb30 100644 --- a/app/models/proposal.rb +++ b/app/models/proposal.rb @@ -16,6 +16,7 @@ class Proposal < ActiveRecord::Base belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id' belongs_to :geozone has_many :comments, as: :commentable + has_many :proposal_notifications validates :title, presence: true validates :question, presence: true @@ -97,6 +98,10 @@ class Proposal < ActiveRecord::Base cached_votes_up + physical_votes end + def voters + votes_for.voters + end + def editable? total_votes <= Setting["max_votes_for_proposal_edit"].to_i end @@ -150,6 +155,10 @@ class Proposal < ActiveRecord::Base Setting['votes_for_proposal_success'].to_i end + def notifications + proposal_notifications + end + protected def set_responsible_name diff --git a/app/models/proposal_notification.rb b/app/models/proposal_notification.rb new file mode 100644 index 000000000..f73264318 --- /dev/null +++ b/app/models/proposal_notification.rb @@ -0,0 +1,17 @@ +class ProposalNotification < ActiveRecord::Base + belongs_to :author, class_name: 'User', foreign_key: 'author_id' + belongs_to :proposal + + validates :title, presence: true + validates :body, presence: true + validates :proposal, presence: true + validate :minimum_interval + + def minimum_interval + return true if proposal.try(:notifications).blank? + if proposal.notifications.last.created_at > (Time.now - Setting[:proposal_notification_minimum_interval_in_days].to_i.days).to_datetime + errors.add(:title, I18n.t('activerecord.errors.models.proposal_notification.attributes.minimum_interval.invalid', interval: Setting[:proposal_notification_minimum_interval_in_days])) + end + end + +end \ No newline at end of file diff --git a/app/models/user.rb b/app/models/user.rb index 4839d3ced..7240c8929 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -23,6 +23,8 @@ class User < ActiveRecord::Base has_many :spending_proposals, foreign_key: :author_id has_many :failed_census_calls has_many :notifications + has_many :direct_messages_sent, class_name: 'DirectMessage', foreign_key: :sender_id + has_many :direct_messages_received, class_name: 'DirectMessage', foreign_key: :receiver_id belongs_to :geozone validates :username, presence: true, if: :username_required? @@ -50,6 +52,7 @@ class User < ActiveRecord::Base scope :officials, -> { where("official_level > 0") } scope :for_render, -> { includes(:organization) } scope :by_document, -> (document_type, document_number) { where(document_type: document_type, document_number: document_number) } + scope :email_digest, -> { where(email_digest: true) } before_validation :clean_document_number diff --git a/app/views/account/show.html.erb b/app/views/account/show.html.erb index f0d2b6abc..98987626d 100644 --- a/app/views/account/show.html.erb +++ b/app/views/account/show.html.erb @@ -34,7 +34,9 @@
<%= f.label :public_activity do %> <%= f.check_box :public_activity, title: t('account.show.public_activity_label'), label: false %> - <%= t("account.show.public_activity_label") %> + + <%= t("account.show.public_activity_label") %> + <% end %>
@@ -43,21 +45,45 @@
<%= f.label :email_on_comment do %> <%= f.check_box :email_on_comment, title: t('account.show.email_on_comment_label'), label: false %> - <%= t("account.show.email_on_comment_label") %> + + <%= t("account.show.email_on_comment_label") %> + <% end %>
<%= f.label :email_on_comment_reply do %> <%= f.check_box :email_on_comment_reply, title: t('account.show.email_on_comment_reply_label'), label: false %> - <%= t("account.show.email_on_comment_reply_label") %> + + <%= t("account.show.email_on_comment_reply_label") %> + <% end %>
<%= f.label :email_newsletter_subscribed do %> <%= f.check_box :newsletter, title: t('account.show.subscription_to_website_newsletter_label'), label: false %> - <%= t("account.show.subscription_to_website_newsletter_label") %> + + <%= t("account.show.subscription_to_website_newsletter_label") %> + + <% end %> +
+ +
+ <%= f.label :email_digest do %> + <%= f.check_box :email_digest, title: t('account.show.email_digest_label'), label: false %> + + <%= t("account.show.email_digest_label") %> + + <% end %> +
+ +
+ <%= f.label :email_on_direct_message do %> + <%= f.check_box :email_on_direct_message, title: t('account.show.email_on_direct_message_label'), label: false %> + + <%= t("account.show.email_on_direct_message_label") %> + <% end %>
diff --git a/app/views/direct_messages/new.html.erb b/app/views/direct_messages/new.html.erb new file mode 100644 index 000000000..3413f26f0 --- /dev/null +++ b/app/views/direct_messages/new.html.erb @@ -0,0 +1,47 @@ +
+
+ <%= render 'shared/back_link' %> + +

+ <%= t("users.direct_messages.new.title", receiver: @receiver.name) %> +

+ + <% if not current_user %> +
+

+ <%= t("users.direct_messages.new.authenticate", + signin: link_to(t("users.direct_messages.new.signin"), new_user_session_path), + signup: link_to(t("users.direct_messages.new.signup"), new_user_registration_path)).html_safe %> +

+
+ <% elsif not @receiver.email_on_direct_message? %> +
+

+ <%= t("users.direct_messages.new.direct_messages_bloqued") %> +

+
+ <% elsif can? :create, @direct_message %> + <%= form_for [@receiver, @direct_message] do |f| %> + <%= render "shared/errors", resource: @direct_message %> + + <%= f.label :title, t("users.direct_messages.new.title_label") %> + <%= f.text_field :title, label: false %> + + <%= f.label :body, t("users.direct_messages.new.body_label") %> + <%= f.text_area :body, label: false, rows: "3" %> + +
+ <%= f.submit t("users.direct_messages.new.submit_button"), class: "button expanded" %> +
+ <% end %> + <% else %> +
+

+ <%= t("users.direct_messages.new.verified_only", + verify_account: link_to( t("users.direct_messages.new.verify_account"), + verification_path )).html_safe %> +

+
+ <% end %> +
+
diff --git a/app/views/direct_messages/show.html.erb b/app/views/direct_messages/show.html.erb new file mode 100644 index 000000000..30a771a39 --- /dev/null +++ b/app/views/direct_messages/show.html.erb @@ -0,0 +1,18 @@ +
+
+ <%= link_to user_path(@direct_message.receiver), class: "back" do %> + + <%= t("shared.back") %> + <% end %> + +
+ +
+ <%= t("users.direct_messages.show.receiver", + receiver: @direct_message.receiver.name) %> +
+ +

<%= @direct_message.title %>

+

<%= @direct_message.body %>

+
+
diff --git a/app/views/layouts/mailer.html.erb b/app/views/layouts/mailer.html.erb index e0533ca50..f90b8e39d 100644 --- a/app/views/layouts/mailer.html.erb +++ b/app/views/layouts/mailer.html.erb @@ -32,6 +32,10 @@

<%= setting['org_name'] %>

+ +

+ <%= t('mailers.no_reply') %>

+ diff --git a/app/views/mailer/direct_message_for_receiver.html.erb b/app/views/mailer/direct_message_for_receiver.html.erb new file mode 100644 index 000000000..2523f3502 --- /dev/null +++ b/app/views/mailer/direct_message_for_receiver.html.erb @@ -0,0 +1,37 @@ + +

+ <%= @direct_message.title %> +

+ +

+ <%= @direct_message.body %> +

+ + + + + + + +
+ <%= link_to user_url(@direct_message.sender), style: "font-family: 'Open Sans','Helvetica Neue',arial,sans-serif; background: #f7f5f2; border-radius: 6px; color: #3d3d66!important; font-weight: bold; margin: 0px; padding: 10px 15px; text-align: center; text-decoration: none; min-width: 200px; display: inline-block;", target: "_blank" do %> + <%= image_tag('icon_mailer_reply.png', style: "border: 0; display: inline-block; width: 100%; max-width: 12px; vertical-align: sub;", alt: "") %> + <%= t('mailers.direct_message_for_receiver.reply', + sender: @direct_message.sender.name) %> + <% end %> +
+ + + + + + + +
+

+ <%= t('mailers.direct_message_for_receiver.unsubscribe', + account: link_to(t('mailers.direct_message_for_receiver.unsubscribe_account'), + account_url, style: "color: #2895F1; text-decoration: none;")).html_safe %> +

+
+ diff --git a/app/views/mailer/direct_message_for_sender.html.erb b/app/views/mailer/direct_message_for_sender.html.erb new file mode 100644 index 000000000..4cf832414 --- /dev/null +++ b/app/views/mailer/direct_message_for_sender.html.erb @@ -0,0 +1,15 @@ + + +

+ <%= t('mailers.direct_message_for_sender.title_html', + receiver: @direct_message.receiver.name ) %> +

+ +

+ <%= @direct_message.title %> +

+ +

+ <%= @direct_message.body %> +

+ diff --git a/app/views/mailer/proposal_notification_digest.html.erb b/app/views/mailer/proposal_notification_digest.html.erb new file mode 100644 index 000000000..0b839150f --- /dev/null +++ b/app/views/mailer/proposal_notification_digest.html.erb @@ -0,0 +1,68 @@ + + + + + + + +
+

+ <%= t('mailers.proposal_notification_digest.title', + org_name: Setting['org_name']) %> +

+
+ + <% @notifications.each do |notification| %> + + + + + + +
+

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

+

+ <%= notification.notifiable.proposal.title %> •  + <%= notification.notifiable.proposal.created_at.to_date %> •  + <%= notification.notifiable.proposal.author.name %> +

+

+ <%= notification.notifiable.body %> +

+ + + + + + + +
+ <%= link_to proposal_url(notification.notifiable.proposal, anchor: "social-share"), style: "font-family: 'Open Sans','Helvetica Neue',arial,sans-serif; background: #f7f5f2; border-radius: 6px; color: #3d3d66!important; font-weight: bold; margin: 0px; padding: 10px 15px; text-align: center; text-decoration: none; min-width: 160px; display: inline-block;" do %> + <%= image_tag('icon_mailer_share.png', style: "border: 0; display: inline-block; width: 100%; max-width: 16px", alt: "") %> + <%= t('mailers.proposal_notification_digest.share') %> + <% end %> + + <%= link_to proposal_url(notification.notifiable.proposal, anchor: "comments"), style: "font-family: 'Open Sans','Helvetica Neue',arial,sans-serif; background: #f7f5f2; border-radius: 6px; color: #3d3d66!important; font-weight: bold; margin: 0px; padding: 10px 15px; text-align: center; text-decoration: none; min-width: 160px; display: inline-block; margin-left: 12px;" do %> + <%= image_tag('icon_mailer_comment.png', style: "border: 0; display: inline-block; width: 100%; max-width: 16px; vertical-align: middle;", alt: "") %> + <%= t('mailers.proposal_notification_digest.comment') %> + <% end %> +
+
+ <% end %> + + + + + + + +
+

+ <%= t('mailers.proposal_notification_digest.unsubscribe', + account: link_to(t('mailers.proposal_notification_digest.unsubscribe_account'), + account_url, style: "color: #2895F1; text-decoration: none;")).html_safe %> +

+
+ diff --git a/app/views/notifications/_notification.html.erb b/app/views/notifications/_notification.html.erb index 28def42f7..99b4b7def 100644 --- a/app/views/notifications/_notification.html.erb +++ b/app/views/notifications/_notification.html.erb @@ -1,9 +1,13 @@
  • <%= link_to notification do %>

    - <%= t("notifications.index.#{notification_action(notification)}", count: notification.counter) %> - <%= notification.notifiable.is_a?(Comment) ? notification.notifiable.commentable.title : notification.notifiable.title %> + + <%= t("notifications.index.#{notification.notifiable_action}", + count: notification.counter) %> + + <%= notification.notifiable_title %>

    +

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

    <% end %>
  • \ No newline at end of file diff --git a/app/views/proposal_notifications/new.html.erb b/app/views/proposal_notifications/new.html.erb new file mode 100644 index 000000000..678e00088 --- /dev/null +++ b/app/views/proposal_notifications/new.html.erb @@ -0,0 +1,36 @@ +
    +
    + <%= render 'shared/back_link' %> + +

    <%= t("proposal_notifications.new.title") %>

    + +
    +

    + <%= t("proposal_notifications.new.info_about_receivers_html", + count: @proposal.voters.count, + proposal_page: link_to(t("proposal_notifications.new.proposal_page"), + proposal_path(@proposal, anchor: "comments"))).html_safe %> +

    +
    +
    +
    + +
    +
    + <%= form_for @notification do |f| %> + <%= render "shared/errors", resource: @notification %> + + <%= f.label :title, t("proposal_notifications.new.title_label") %> + <%= f.text_field :title, label: false %> + + <%= f.label :body, t("proposal_notifications.new.body_label") %> + <%= f.text_area :body, label: false, rows: "3" %> + + <%= f.hidden_field :proposal_id, value: @proposal.id %> + +
    + <%= f.submit t("proposal_notifications.new.submit_button"), class: "button expanded" %> +
    + <% end %> +
    +
    diff --git a/app/views/proposal_notifications/show.html.erb b/app/views/proposal_notifications/show.html.erb new file mode 100644 index 000000000..379b361fb --- /dev/null +++ b/app/views/proposal_notifications/show.html.erb @@ -0,0 +1,11 @@ +
    +
    + <%= link_to user_path(current_user), class: "back" do %> + + <%= t("proposal_notifications.show.back") %> + <% end %> + +

    <%= @notification.title %>

    +

    <%= @notification.body %>

    +
    +
    diff --git a/app/views/proposals/_comments.html.erb b/app/views/proposals/_comments.html.erb index 31f1cb8ba..ed52b1b66 100644 --- a/app/views/proposals/_comments.html.erb +++ b/app/views/proposals/_comments.html.erb @@ -1,12 +1,7 @@ <% cache [locale_and_user_status, @current_order, commentable_cache_key(@proposal), @comment_tree.comments, @comment_tree.comment_authors, @proposal.comments_count, @comment_flags] do %> -
    +
    -

    - <%= t("proposals.show.comments_title") %> - (<%= @proposal.comments_count %>) -

    - <%= render 'shared/wide_order_selector', i18n_namespace: "comments" %> <% if user_signed_in? %> diff --git a/app/views/proposals/_filter_subnav.html.erb b/app/views/proposals/_filter_subnav.html.erb new file mode 100644 index 000000000..b7bc85dd6 --- /dev/null +++ b/app/views/proposals/_filter_subnav.html.erb @@ -0,0 +1,22 @@ +
    +
    +
      +
    • + <%= link_to "#tab-comments" do %> +

      + <%= t("proposals.show.comments_tab") %> + (<%= @proposal.comments_count %>) +

      + <% end %> +
    • +
    • + <%= link_to "#tab-notifications" do %> +

      + <%= t("proposals.show.notifications_tab") %> + (<%= @notifications.count %>) +

      + <% end %> +
    • +
    +
    +
    diff --git a/app/views/proposals/_notifications.html.erb b/app/views/proposals/_notifications.html.erb new file mode 100644 index 000000000..69389b2c5 --- /dev/null +++ b/app/views/proposals/_notifications.html.erb @@ -0,0 +1,17 @@ +
    +
    +
    + <% if @notifications.blank? %> +
    + <%= t('proposals.show.no_notifications') %> +
    + <% end %> + + <% @notifications.each do |notification| %> +

    <%= notification.title %>

    +

    <%= notification.created_at.to_date %>

    +

    <%= notification.body %>

    + <% end %> +
    +
    +
    diff --git a/app/views/proposals/show.html.erb b/app/views/proposals/show.html.erb index cb2773c65..3a9869d83 100644 --- a/app/views/proposals/show.html.erb +++ b/app/views/proposals/show.html.erb @@ -12,9 +12,13 @@
    <%= render "shared/back_link" %> + <% if author_of?(@proposal, current_user) %> + <%= link_to t("proposals.show.send_notification"), new_proposal_notification_path(proposal_id: @proposal.id), + class: 'button hollow float-right' %> + <% end %> + <% if current_user && @proposal.editable_by?(current_user) %> - <%= link_to edit_proposal_path(@proposal), class: 'edit-proposal button success small float-right' do %> - + <%= link_to edit_proposal_path(@proposal), class: 'edit-proposal button hollow float-right' do %> <%= t("proposals.show.edit_proposal_link") %> <% end %> <% end %> @@ -104,7 +108,7 @@ { proposal: @proposal, vote_url: vote_proposal_path(@proposal, value: 'yes') } %>
    - +

    <%= t("proposals.show.share") %>

    <% end %> -<%= render "proposals/comments" %> + +
    + <%= render "proposals/filter_subnav" %> + <%= render "proposals/notifications" %> + +
    + <%= render "proposals/comments" %> +
    +
    diff --git a/app/views/users/_proposals.html.erb b/app/views/users/_proposals.html.erb index e0b54dcc4..f50045635 100644 --- a/app/views/users/_proposals.html.erb +++ b/app/views/users/_proposals.html.erb @@ -7,17 +7,25 @@ <%= proposal.summary %> + <% if author? %> + + <%= link_to t("users.proposals.send_notification"), new_proposal_notification_path(proposal_id: proposal.id), + class: 'button hollow' %> + + <% end %> + <% if author_or_admin? %> <% if proposal.retired? %> - <%= t('users.show.retired') %> + <%= t('users.proposals.retired') %> <% else %> - <%= link_to t('users.show.retire'), + <%= link_to t('users.proposals.retire'), retire_form_proposal_path(proposal), - class: 'delete' %> + class: 'button hollow alert' %> <% end %> <% end %> + <% end %> diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb index 563b145dd..8b4428a09 100644 --- a/app/views/users/show.html.erb +++ b/app/views/users/show.html.erb @@ -2,7 +2,17 @@
    -

    + <% if @user != current_user && @user.email_on_direct_message? %> + <%= link_to t("users.show.send_private_message"), + new_user_direct_message_path(@user), + class: "button hollow float-right" %> + <% else %> +
    + <%= t("users.show.no_private_messages") %> +
    + <% end %> + +

    <%= avatar_image(@user, seed: @user.id, size: 60) %> <%= @user.name %> <% if current_administrator? %> diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml index c849b469e..250ed18f9 100644 --- a/config/i18n-tasks.yml +++ b/config/i18n-tasks.yml @@ -95,6 +95,8 @@ search: # - '{devise,simple_form}.*' ignore_missing: - 'unauthorized.*' + - 'activerecord.errors.models.proposal_notification.*' + - 'activerecord.errors.models.direct_message.*' - 'errors.messages.blank' - 'errors.messages.taken' - 'devise.failure.invalid' @@ -134,6 +136,7 @@ ignore_unused: - 'spending_proposals.index.search_form.*' - 'notifications.index.comments_on*' - 'notifications.index.replies_to*' + - 'notifications.index.proposal_notification*' - 'helpers.page_entries_info.*' # kaminari - 'views.pagination.*' # kaminari - 'shared.suggest.*' diff --git a/config/locales/activerecord.en.yml b/config/locales/activerecord.en.yml index 91c035dfb..9b48c1fc2 100644 --- a/config/locales/activerecord.en.yml +++ b/config/locales/activerecord.en.yml @@ -1,11 +1,5 @@ en: activerecord: - errors: - models: - user: - attributes: - email: - password_already_set: "This user already has a password" models: activity: one: "activity" @@ -76,11 +70,23 @@ en: title: "Title" errors: models: + user: + attributes: + email: + password_already_set: "This user already has a password" debate: attributes: tag_list: less_than_or_equal_to: "tags must be less than or equal to %{count}" + direct_message: + attributes: + max_per_day: + invalid: "You can only send a maximum of %{max} direct messages per day" proposal: attributes: tag_list: less_than_or_equal_to: "tags must be less than or equal to %{count}" + proposal_notification: + attributes: + minimum_interval: + invalid: "You have to wait a minium of %{interval} days between notifications" \ No newline at end of file diff --git a/config/locales/activerecord.es.yml b/config/locales/activerecord.es.yml index a7644f02e..f14022255 100644 --- a/config/locales/activerecord.es.yml +++ b/config/locales/activerecord.es.yml @@ -1,11 +1,5 @@ es: activerecord: - errors: - models: - user: - attributes: - email: - password_already_set: "Este usuario ya tiene una clave asociada" models: activity: one: "actividad" @@ -76,11 +70,23 @@ es: title: "Título" errors: models: + user: + attributes: + email: + password_already_set: "Este usuario ya tiene una clave asociada" debate: attributes: tag_list: less_than_or_equal_to: "los temas deben ser menor o igual que %{count}" + direct_message: + attributes: + max_per_day: + invalid: "Sólo puedes enviar %{max} mensajes directos por día" proposal: attributes: tag_list: less_than_or_equal_to: "los temas deben ser menor o igual que %{count}" + proposal_notification: + attributes: + minimum_interval: + invalid: "Debes esperar un mínimo de %{interval} días entre notificaciones" \ No newline at end of file diff --git a/config/locales/en.yml b/config/locales/en.yml index 62fc521c5..b16f1092a 100755 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -15,6 +15,8 @@ en: public_activity_label: Keep my list of activities public save_changes_submit: Save changes subscription_to_website_newsletter_label: Receive by email website relevant information + email_on_direct_message_label: Receive emails about direct messages + email_digest_label: Receive a summary of proposal notifications title: My account user_permission_debates: Participate on debates user_permission_info: With your account you can... @@ -141,11 +143,13 @@ en: accept_terms_title: I agree to the Privacy Policy and the Terms and conditions of use conditions: Terms and conditions of use debate: Debate + direct_message: private message error: error errors: errors not_saved: 'prevented this %{resource} from being saved:' policy: Privacy Policy proposal: Proposal + proposal_notification: "Notification" spending_proposal: Spending proposal user: Account verification/sms: phone @@ -221,6 +225,9 @@ en: other: There are %{count} new comments on empty_notifications: You don't have new notifications. mark_all_as_read: Mark all as read + proposal_notification: + one: There is one new notification on + other: There are %{count} new notifications on replies_to: one: Someone replied to your comment on other: There are %{count} new replies to your comment on @@ -349,17 +356,30 @@ en: one: 1 comment other: "%{count} comments" zero: No comments - comments_title: Comments + comments_tab: Comments edit_proposal_link: Edit flag: This proposal has been flagged as inappropriate by several users. login_to_comment: You must %{signin} or %{signup} to leave a comment. + notifications_tab: Notifications retired_warning: "The author considers this proposal should not receive more supports." retired_warning_link_to_explanation: Read the explanation before voting for it. retired: Proposal retired by the author share: Share + send_notification: Send notification + no_notifications: "This proposal has any notifications." update: form: submit_button: Save changes + proposal_notifications: + new: + title: "Send message" + title_label: "Title" + body_label: "Message" + submit_button: "Send message" + info_about_receivers_html: "This message will be send to %{count} people and it will be visible in %{proposal_page}.
    Message are not sent immediately, users will receive periodically an email with all proposal notifications." + proposal_page: "the proposal's page" + show: + back: "Go back to my activity" shared: advanced_search: author_type: 'By author category' @@ -483,6 +503,20 @@ en: manage: all: "You do not have permission to carry out the action '%{action}' on %{subject}." users: + direct_messages: + new: + body_label: Message + direct_messages_bloqued: "This user has decided not to receive direct messages" + submit_button: Send message + title: Send private message to %{receiver} + title_label: Title + verified_only: To send a private message %{verify_account} + verify_account: verify your account + authenticate: You must %{signin} or %{signup} to continue. + signin: sign in + signup: sign up + show: + receiver: Message sent to %{receiver} show: deleted: Deleted deleted_debate: This debate has been deleted @@ -503,9 +537,13 @@ en: one: 1 Spending proposal other: "%{count} Spending proposals" no_activity: User has no public activity + no_private_messages: "This user doesn't accept private messages." private_activity: This user decided to keep the activity list private - retire: Retire - retired: Retired + send_private_message: "Send private message" + proposals: + send_notification: "Send notification" + retire: "Retire" + retired: "Retired" votes: agree: I agree anonymous: Too many anonymous votes to admit vote %{verify_account}. diff --git a/config/locales/es.yml b/config/locales/es.yml index 05e53fc2b..a3922ef5f 100755 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -15,6 +15,8 @@ es: public_activity_label: Mostrar públicamente mi lista de actividades save_changes_submit: Guardar cambios subscription_to_website_newsletter_label: Recibir emails con información interesante sobre la web + email_on_direct_message_label: Recibir emails con mensajes directos + email_digest_label: Recibir resumen de notificaciones sobre propuestas title: Mi cuenta user_permission_debates: Participar en debates user_permission_info: Con tu cuenta ya puedes... @@ -141,11 +143,13 @@ es: accept_terms_title: Acepto la Política de privacidad y las Condiciones de uso conditions: Condiciones de uso debate: el debate + direct_message: el mensaje privado error: error errors: errores not_saved: 'impidieron guardar %{resource}:' policy: Política de privacidad proposal: la propuesta + proposal_notification: "la notificación" spending_proposal: la propuesta de gasto user: la cuenta verification/sms: el teléfono @@ -221,6 +225,9 @@ es: other: Hay %{count} comentarios nuevos en empty_notifications: No tienes notificaciones nuevas. mark_all_as_read: Marcar todas como leídas + proposal_notification: + one: Hay una nueva notificación en + other: Hay %{count} nuevas notificaciones en replies_to: one: Hay una respuesta nueva a tu comentario en other: Hay %{count} nuevas respuestas a tu comentario en @@ -349,17 +356,30 @@ es: one: 1 Comentario other: "%{count} Comentarios" zero: Sin comentarios - comments_title: Comentarios + comments_tab: Comentarios edit_proposal_link: Editar propuesta flag: Esta propuesta ha sido marcada como inapropiada por varios usuarios. login_to_comment: Necesitas %{signin} o %{signup} para comentar. + notifications_tab: Notificaciones retired_warning: "El autor de esta propuesta considera que ya no debe seguir recogiendo apoyos." retired_warning_link_to_explanation: Revisa su explicación antes de apoyarla. retired: Propuesta retirada por el autor + send_notification: Enviar notificación share: Compartir + no_notifications: "Esta propuesta no tiene notificaciones." update: form: submit_button: Guardar cambios + proposal_notifications: + new: + title: "Enviar mensaje" + title_label: "Título" + body_label: "Mensaje" + submit_button: "Enviar mensaje" + info_about_receivers_html: "Este mensaje se enviará a %{count} usuarios y se publicará en %{proposal_page}.
    El mensaje no se enviará inmediatamente, los usuarios recibirán periódicamente un email con todas las notificaciones de propuestas." + proposal_page: "la página de la propuesta" + show: + back: "Volver a mi actividad" shared: advanced_search: author_type: 'Por categoría de autor' @@ -483,6 +503,20 @@ es: manage: all: "No tienes permiso para realizar la acción '%{action}' sobre %{subject}." users: + direct_messages: + new: + body_label: "Mensaje" + direct_messages_bloqued: "Este usuarios ha decidido no recibir mensajes directos" + submit_button: "Enviar mensaje" + title: Enviar mensaje privado a %{receiver} + title_label: "Título" + verified_only: Para enviar un mensaje privado %{verify_account} + verify_account: verifica tu cuenta + authenticate: Necesitas %{signin} o %{signup}. + signin: iniciar sesión + signup: registrarte + show: + receiver: Mensaje enviado a %{receiver} show: deleted: Eliminado deleted_debate: Este debate ha sido eliminado @@ -503,9 +537,13 @@ es: one: 1 Propuesta de inversión other: "%{count} Propuestas de inversión" no_activity: Usuario sin actividad pública + no_private_messages: "Este usuario no acepta mensajes privados." private_activity: Este usuario ha decidido mantener en privado su lista de actividades - retire: Retirar - retired: Retirada + send_private_message: "Enviar un mensaje privado" + proposals: + send_notification: "Enviar notificación" + retire: "Retirar" + retired: "Retirada" votes: agree: Estoy de acuerdo anonymous: Demasiados votos anónimos, para poder votar %{verify_account}. diff --git a/config/locales/mailers.en.yml b/config/locales/mailers.en.yml index 16cc762d6..c2e2b5d85 100755 --- a/config/locales/mailers.en.yml +++ b/config/locales/mailers.en.yml @@ -1,6 +1,7 @@ --- en: mailers: + no_reply: "This message was sent from an email address that does not accept replies." comment: hi: Hi new_comment_by_html: There is a new comment from %{commenter} @@ -29,4 +30,18 @@ en: signatory: "DEPARTMENT OF PUBLIC PARTICIPATION" sorry: "Sorry for the inconvenience and we again thank you for your invaluable participation." subject: "Your investment project '%{code}' has been marked as unfeasible" - unfeasible_html: "From the Madrid City Council we want to thank you for your participation in the participatory budgets of the city of Madrid. We regret to inform you that your proposal '%{title}' will be excluded from this participatory process for the following reason:" \ No newline at end of file + unfeasible_html: "From the Madrid City Council we want to thank you for your participation in the participatory budgets of the city of Madrid. We regret to inform you that your proposal '%{title}' will be excluded from this participatory process for the following reason:" + proposal_notification_digest: + title: "Proposal notifications in %{org_name}" + share: Share proposal + comment: Comment proposal + unsubscribe: "If you don't want receive proposal's notification, visit %{account} and unckeck 'Receive a summary of proposal notifications'." + unsubscribe_account: My account + direct_message_for_receiver: + subject: "You have received a new private message" + reply: Reply to %{sender} + unsubscribe: "If you don't want receive direct messages, visit %{account} and unckeck 'Receive emails about direct messages'." + unsubscribe_account: My account + direct_message_for_sender: + subject: "You have send a new private message" + title_html: "You have send a new private message to %{receiver} with the content:" \ No newline at end of file diff --git a/config/locales/mailers.es.yml b/config/locales/mailers.es.yml index 8a16c00ec..bde81ec86 100644 --- a/config/locales/mailers.es.yml +++ b/config/locales/mailers.es.yml @@ -1,6 +1,7 @@ --- es: mailers: + no_reply: "Este mensaje se ha enviado desde una dirección de correo electrónico que no admite respuestas." comment: hi: Hola new_comment_by_html: Hay un nuevo comentario de %{commenter} en @@ -30,3 +31,17 @@ es: sorry: "Sentimos las molestias ocasionadas y volvemos a darte las gracias por tu inestimable participación." subject: "Tu propuesta de inversión '%{code}' ha sido marcada como inviable" unfeasible_html: "Desde el Ayuntamiento de Madrid queremos agradecer tu participación en los Presupuestos Participativos de la ciudad de Madrid. Lamentamos informarte de que tu propuesta '%{title}' quedará excluida de este proceso participativo por el siguiente motivo:" + proposal_notification_digest: + title: "Notificaciones de propuestas en %{org_name}" + share: Compartir propuesta + comment: Comentar propuesta + unsubscribe: "Si no quieres recibir notificaciones de propuestas, puedes entrar en %{account} y desmarcar la opción 'Recibir resumen de notificaciones sobre propuestas'." + unsubscribe_account: Mi cuenta + direct_message_for_receiver: + subject: "Has recibido un nuevo mensaje privado" + reply: Responder a %{sender} + unsubscribe: "Si no quieres recibir mensajes privados, puedes entrar en %{account} y desmarcar la opción 'Recibir emails con mensajes directos'." + unsubscribe_account: Mi cuenta + direct_message_for_sender: + subject: "Has enviado un nuevo mensaje privado" + title_html: "Has enviado un nuevo mensaje privado a %{receiver} con el siguiente contenido:" \ No newline at end of file diff --git a/config/locales/responders.en.yml b/config/locales/responders.en.yml index 3f83f746a..ab1641799 100755 --- a/config/locales/responders.en.yml +++ b/config/locales/responders.en.yml @@ -5,8 +5,11 @@ en: create: notice: "%{resource_name} created successfully." debate: "Debate created successfully." + direct_message: "You message has been sent successfully." proposal: "Proposal created successfully." + proposal_notification: "Your message has been sent correctly." spending_proposal: "Spending proposal created successfully. You can access it from %{activity}" + save_changes: notice: Changes saved update: diff --git a/config/locales/responders.es.yml b/config/locales/responders.es.yml index e57dacd31..387085d69 100644 --- a/config/locales/responders.es.yml +++ b/config/locales/responders.es.yml @@ -5,7 +5,9 @@ es: create: notice: "%{resource_name} creado correctamente." debate: "Debate creado correctamente." + direct_message: "Tu mensaje ha sido enviado correctamente." proposal: "Propuesta creada correctamente." + proposal_notification: "Tu message ha sido enviado correctamente." spending_proposal: "Propuesta de inversión creada correctamente. Puedes acceder a ella desde %{activity}" save_changes: notice: Cambios guardados diff --git a/config/routes.rb b/config/routes.rb index 7d0a3a996..d805683d3 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -83,7 +83,9 @@ Rails.application.routes.draw do get :search, on: :collection end - resources :users, only: [:show] + resources :users, only: [:show] do + resources :direct_messages, only: [:new, :create, :show] + end resource :account, controller: "account", only: [:show, :update, :delete] do get :erase, on: :collection @@ -93,6 +95,8 @@ Rails.application.routes.draw do put :mark_all_as_read, on: :collection end + resources :proposal_notifications, only: [:new, :create, :show] + resource :verification, controller: "verification", only: [:show] scope module: :verification do diff --git a/db/migrate/20160601103338_create_proposal_notifications.rb b/db/migrate/20160601103338_create_proposal_notifications.rb new file mode 100644 index 000000000..97760da6b --- /dev/null +++ b/db/migrate/20160601103338_create_proposal_notifications.rb @@ -0,0 +1,12 @@ +class CreateProposalNotifications < ActiveRecord::Migration + def change + create_table :proposal_notifications do |t| + t.string :title + t.text :body + t.integer :author_id + t.integer :proposal_id + + t.timestamps null: false + end + end +end diff --git a/db/migrate/20160606102427_add_email_on_proposal_notification_to_users.rb b/db/migrate/20160606102427_add_email_on_proposal_notification_to_users.rb new file mode 100644 index 000000000..84753eb50 --- /dev/null +++ b/db/migrate/20160606102427_add_email_on_proposal_notification_to_users.rb @@ -0,0 +1,5 @@ +class AddEmailOnProposalNotificationToUsers < ActiveRecord::Migration + def change + add_column :users, :email_on_proposal_notification, :boolean, default: true + end +end diff --git a/db/migrate/20160608174104_create_direct_messages.rb b/db/migrate/20160608174104_create_direct_messages.rb new file mode 100644 index 000000000..0cdcd6659 --- /dev/null +++ b/db/migrate/20160608174104_create_direct_messages.rb @@ -0,0 +1,12 @@ +class CreateDirectMessages < ActiveRecord::Migration + def change + create_table :direct_messages do |t| + t.integer :sender_id + t.integer :receiver_id + t.string :title + t.text :body + + t.timestamps null: false + end + end +end diff --git a/db/migrate/20160613150659_add_email_digest_to_users.rb b/db/migrate/20160613150659_add_email_digest_to_users.rb new file mode 100644 index 000000000..d127af78e --- /dev/null +++ b/db/migrate/20160613150659_add_email_digest_to_users.rb @@ -0,0 +1,5 @@ +class AddEmailDigestToUsers < ActiveRecord::Migration + def change + add_column :users, :email_digest, :boolean, default: true + end +end diff --git a/db/migrate/20160614160949_add_email_on_direct_messages_to_users.rb b/db/migrate/20160614160949_add_email_on_direct_messages_to_users.rb new file mode 100644 index 000000000..db18e3f27 --- /dev/null +++ b/db/migrate/20160614160949_add_email_on_direct_messages_to_users.rb @@ -0,0 +1,5 @@ +class AddEmailOnDirectMessagesToUsers < ActiveRecord::Migration + def change + add_column :users, :email_on_direct_message, :boolean, default: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 10f10c7a8..f5671b3a3 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20160518141543) do +ActiveRecord::Schema.define(version: 20160614160949) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -168,6 +168,15 @@ ActiveRecord::Schema.define(version: 20160518141543) do add_index "delayed_jobs", ["priority", "run_at"], name: "delayed_jobs_priority", using: :btree + create_table "direct_messages", force: :cascade do |t| + t.integer "sender_id" + t.integer "receiver_id" + t.string "title" + t.text "body" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "failed_census_calls", force: :cascade do |t| t.integer "user_id" t.string "document_number" @@ -260,6 +269,15 @@ ActiveRecord::Schema.define(version: 20160518141543) do add_index "organizations", ["user_id"], name: "index_organizations_on_user_id", using: :btree + create_table "proposal_notifications", force: :cascade do |t| + t.string "title" + t.text "body" + t.integer "author_id" + t.integer "proposal_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "proposals", force: :cascade do |t| t.string "title", limit: 80 t.text "description" @@ -393,30 +411,30 @@ ActiveRecord::Schema.define(version: 20160518141543) do add_index "tolk_translations", ["phrase_id", "locale_id"], name: "index_tolk_translations_on_phrase_id_and_locale_id", unique: true, using: :btree create_table "users", force: :cascade do |t| - t.string "email", default: "" - t.string "encrypted_password", default: "", null: false + t.string "email", default: "" + t.string "encrypted_password", default: "", null: false t.string "reset_password_token" t.datetime "reset_password_sent_at" t.datetime "remember_created_at" - t.integer "sign_in_count", default: 0, null: false + t.integer "sign_in_count", default: 0, null: false t.datetime "current_sign_in_at" t.datetime "last_sign_in_at" t.string "current_sign_in_ip" t.string "last_sign_in_ip" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.string "confirmation_token" t.datetime "confirmed_at" t.datetime "confirmation_sent_at" t.string "unconfirmed_email" - t.boolean "email_on_comment", default: false - t.boolean "email_on_comment_reply", default: false - t.string "phone_number", limit: 30 + t.boolean "email_on_comment", default: false + t.boolean "email_on_comment_reply", default: false + t.string "phone_number", limit: 30 t.string "official_position" - t.integer "official_level", default: 0 + t.integer "official_level", default: 0 t.datetime "hidden_at" t.string "sms_confirmation_code" - t.string "username", limit: 60 + t.string "username", limit: 60 t.string "document_number" t.string "document_type" t.datetime "residence_verified_at" @@ -427,20 +445,23 @@ ActiveRecord::Schema.define(version: 20160518141543) do t.datetime "letter_requested_at" t.datetime "confirmed_hide_at" t.string "letter_verification_code" - t.integer "failed_census_calls_count", default: 0 + t.integer "failed_census_calls_count", default: 0 t.datetime "level_two_verified_at" t.string "erase_reason" t.datetime "erased_at" - t.boolean "public_activity", default: true - t.boolean "newsletter", default: true - t.integer "notifications_count", default: 0 - t.boolean "registering_with_oauth", default: false + t.boolean "public_activity", default: true + t.boolean "newsletter", default: true + t.integer "notifications_count", default: 0 + t.boolean "registering_with_oauth", default: false t.string "locale" t.string "oauth_email" t.integer "geozone_id" t.string "redeemable_code" - t.string "gender", limit: 10 + t.string "gender", limit: 10 t.datetime "date_of_birth" + t.boolean "email_on_proposal_notification", default: true + t.boolean "email_digest", default: true + t.boolean "email_on_direct_message", default: true end add_index "users", ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree diff --git a/db/seeds.rb b/db/seeds.rb index 7555f6999..4a4741a07 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -74,3 +74,7 @@ Setting['banner-style.banner-style-three'] = "Banner style 3" Setting['banner-img.banner-img-one'] = "Banner image 1" Setting['banner-img.banner-img-two'] = "Banner image 2" Setting['banner-img.banner-img-three'] = "Banner image 3" + +# Proposal notifications +Setting['proposal_notification_minimum_interval_in_days '] = 3 +Setting['direct_message_max_per_day'] = 3 \ No newline at end of file diff --git a/lib/email_digest.rb b/lib/email_digest.rb new file mode 100644 index 000000000..90838f78f --- /dev/null +++ b/lib/email_digest.rb @@ -0,0 +1,14 @@ +class EmailDigest + + def initialize + end + + def create + User.email_digest.each do |user| + if user.notifications.where(notifiable_type: "ProposalNotification").any? + Mailer.proposal_notification_digest(user).deliver_later + end + end + end + +end \ No newline at end of file diff --git a/lib/tasks/emails.rake b/lib/tasks/emails.rake new file mode 100644 index 000000000..6670264a5 --- /dev/null +++ b/lib/tasks/emails.rake @@ -0,0 +1,9 @@ +namespace :emails do + + desc "Sends email digest of proposal notifications to each user" + task digest: :environment do + email_digest = EmailDigest.new + email_digest.create + end + +end diff --git a/spec/factories.rb b/spec/factories.rb index 62bea738f..4ead8ecac 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -1,5 +1,4 @@ FactoryGirl.define do - sequence(:document_number) { |n| "#{n.to_s.rjust(8, '0')}X" } factory :user do @@ -137,7 +136,7 @@ FactoryGirl.define do factory :proposal do sequence(:title) { |n| "Proposal #{n} title" } - summary 'In summary, what we want is...' + sequence(:summary) { |n| "In summary, what we want is... #{n}" } description 'Proposal description' question 'Proposal question' external_url 'http://external_documention.es' @@ -323,7 +322,20 @@ FactoryGirl.define do style {["banner-style-one", "banner-style-two", "banner-style-three"].sample} image {["banner.banner-img-one", "banner.banner-img-two", "banner.banner-img-three"].sample} target_url {["/proposals", "/debates" ].sample} - post_started_at Time.now - 7.days - post_ended_at Time.now + 7.days + post_started_at Time.now - 7.days + post_ended_at Time.now + 7.days + end + + factory :proposal_notification do + title "Thank you for supporting my proposal" + body "Please let others know so we can make it happen" + proposal + end + + factory :direct_message do + title "Hey" + body "How are You doing?" + association :sender, factory: :user + association :receiver, factory: :user end end diff --git a/spec/features/account_spec.rb b/spec/features/account_spec.rb index bc9c38dae..0d5da38b2 100644 --- a/spec/features/account_spec.rb +++ b/spec/features/account_spec.rb @@ -35,6 +35,8 @@ feature 'Account' do fill_in 'account_username', with: 'Larry Bird' check 'account_email_on_comment' check 'account_email_on_comment_reply' + uncheck 'account_email_digest' + uncheck 'account_email_on_direct_message' click_button 'Save changes' expect(page).to have_content "Changes saved" @@ -42,8 +44,10 @@ feature 'Account' do visit account_path expect(page).to have_selector("input[value='Larry Bird']") - expect(page).to have_selector("input[id='account_email_on_comment'][value='1']") - expect(page).to have_selector("input[id='account_email_on_comment_reply'][value='1']") + expect(find("#account_email_on_comment")).to be_checked + expect(find("#account_email_on_comment_reply")).to be_checked + expect(find("#account_email_digest")).to_not be_checked + expect(find("#account_email_on_direct_message")).to_not be_checked end scenario 'Edit Organization' do @@ -53,6 +57,7 @@ feature 'Account' do fill_in 'account_organization_attributes_name', with: 'Google' check 'account_email_on_comment' check 'account_email_on_comment_reply' + click_button 'Save changes' expect(page).to have_content "Changes saved" @@ -60,8 +65,8 @@ feature 'Account' do visit account_path expect(page).to have_selector("input[value='Google']") - expect(page).to have_selector("input[id='account_email_on_comment'][value='1']") - expect(page).to have_selector("input[id='account_email_on_comment_reply'][value='1']") + expect(find("#account_email_on_comment")).to be_checked + expect(find("#account_email_on_comment_reply")).to be_checked end scenario "Errors on edit" do diff --git a/spec/features/comments/proposals_spec.rb b/spec/features/comments/proposals_spec.rb index 53b1006c3..8312176cd 100644 --- a/spec/features/comments/proposals_spec.rb +++ b/spec/features/comments/proposals_spec.rb @@ -168,7 +168,10 @@ feature 'Commenting proposals' do within "#comments" do expect(page).to have_content 'Have you thought about...?' - expect(page).to have_content '(1)' + end + + within "#tab-comments-label" do + expect(page).to have_content 'Comments (1)' end end diff --git a/spec/features/direct_messages_spec.rb b/spec/features/direct_messages_spec.rb new file mode 100644 index 000000000..8ec949e9a --- /dev/null +++ b/spec/features/direct_messages_spec.rb @@ -0,0 +1,122 @@ +require 'rails_helper' + +feature 'Direct messages' do + + background do + Setting[:direct_message_max_per_day] = 3 + end + + scenario "Create" do + sender = create(:user, :level_two) + receiver = create(:user, :level_two) + + login_as(sender) + visit user_path(receiver) + + click_link "Send private message" + + expect(page).to have_content "Send private message to #{receiver.name}" + + fill_in 'direct_message_title', with: "Hey!" + fill_in 'direct_message_body', with: "How are you doing?" + click_button "Send message" + + expect(page).to have_content "You message has been sent successfully." + expect(page).to have_content "Hey!" + expect(page).to have_content "How are you doing?" + end + + context "Permissions" do + + scenario "Do not display link to send message to myself" do + sender = create(:user, :level_two) + + login_as(sender) + visit user_path(sender) + + expect(page).to_not have_link "Send private message" + end + + scenario "Do not display link if direct message for user not allowed" do + sender = create(:user, :level_two) + receiver = create(:user, :level_two, email_on_direct_message: false) + + login_as(sender) + visit user_path(receiver) + + expect(page).to have_content "This user doesn't accept private messages." + expect(page).to_not have_link "Send private message" + end + + scenario "Unverified user" do + sender = create(:user) + receiver = create(:user) + + login_as(sender) + visit new_user_direct_message_path(receiver) + + expect(page).to have_content "To send a private message verify your account" + expect(page).to_not have_link "Send private message" + end + + scenario "User not logged in" do + sender = create(:user) + receiver = create(:user) + + visit new_user_direct_message_path(receiver) + + expect(page).to have_content "You must sign in or sign up to continue." + expect(page).to_not have_link "Send private message" + end + + scenario "Accessing form directly" do + sender = create(:user, :level_two) + receiver = create(:user, :level_two, email_on_direct_message: false) + + login_as(sender) + visit new_user_direct_message_path(receiver) + + expect(page).to have_content("This user has decided not to receive direct messages") + expect(page).to_not have_css("#direct_message_title") + end + + end + + scenario "Error messages" do + author = create(:user) + proposal = create(:proposal, author: author) + + login_as(author) + + visit new_proposal_notification_path(proposal_id: proposal.id) + click_button "Send message" + + expect(page).to have_content error_message + end + + context "Limits" do + + scenario "Can only send a maximum number of direct messages per day" do + sender = create(:user, :level_two) + receiver = create(:user, :level_two) + + 3.times { create(:direct_message, sender: sender) } + + login_as(sender) + visit user_path(receiver) + + click_link "Send private message" + + expect(page).to have_content "Send private message to #{receiver.name}" + + fill_in 'direct_message_title', with: "Hey!" + fill_in 'direct_message_body', with: "How are you doing?" + click_button "Send message" + + expect(page).to have_content "You can only send a maximum of 3 direct messages per day" + expect(page).to_not have_content "You message has been sent successfully." + end + + end + +end \ No newline at end of file diff --git a/spec/features/emails_spec.rb b/spec/features/emails_spec.rb index c6f0d3708..f793b5bb7 100644 --- a/spec/features/emails_spec.rb +++ b/spec/features/emails_spec.rb @@ -148,4 +148,87 @@ feature 'Emails' do expect(email).to have_body_text(spending_proposal.feasible_explanation) end + context "Direct Message" do + + scenario "Receiver email" do + sender = create(:user, :level_two) + receiver = create(:user, :level_two) + + direct_message = create_direct_message(sender, receiver) + + email = unread_emails_for(receiver.email).first + + expect(email).to have_subject("You have received a new private message") + expect(email).to have_body_text(direct_message.title) + expect(email).to have_body_text(direct_message.body) + expect(email).to have_body_text(direct_message.sender.name) + expect(email).to have_body_text(/#{user_path(direct_message.sender_id)}/) + end + + scenario "Sender email" do + sender = create(:user, :level_two) + receiver = create(:user, :level_two) + + direct_message = create_direct_message(sender, receiver) + + email = unread_emails_for(sender.email).first + + expect(email).to have_subject("You have send a new private message") + expect(email).to have_body_text(direct_message.title) + expect(email).to have_body_text(direct_message.body) + expect(email).to have_body_text(direct_message.receiver.name) + end + + pending "In the copy sent to the sender, display the receiver's name" + + end + + context "Proposal notification digest" do + + scenario "notifications for proposals that I have supported" do + user = create(:user, email_digest: true) + + proposal1 = create(:proposal) + proposal2 = create(:proposal) + proposal3 = create(:proposal) + + create(:vote, votable: proposal1, voter: user) + create(:vote, votable: proposal2, voter: user) + + reset_mailer + + notification1 = create_proposal_notification(proposal1) + notification2 = create_proposal_notification(proposal2) + notification3 = create_proposal_notification(proposal3) + + email_digest = EmailDigest.new + email_digest.create + + email = open_last_email + expect(email).to have_subject("Proposal notifications in Consul") + expect(email).to deliver_to(user.email) + + expect(email).to have_body_text(proposal1.title) + expect(email).to have_body_text(notification1.notifiable.title) + 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: '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: 'comments')}/) + expect(email).to have_body_text(/#{proposal_path(proposal2, anchor: 'social-share')}/) + expect(email).to have_body_text(proposal2.author.name) + + expect(email).to_not have_body_text(proposal3.title) + expect(email).to have_body_text(/#{account_path}/) + end + + end + end diff --git a/spec/features/notifications_spec.rb b/spec/features/notifications_spec.rb index 1ffc6a854..e16dedf37 100644 --- a/spec/features/notifications_spec.rb +++ b/spec/features/notifications_spec.rb @@ -149,6 +149,64 @@ feature "Notifications" do expect(page).to have_css ".notification", count: 0 end + context "Proposal notification" do + + scenario "Voters should receive a notification", :js do + author = create(:user) + + user1 = create(:user) + user2 = create(:user) + user3 = create(:user) + + proposal = create(:proposal, author: author) + + create(:vote, voter: user1, votable: proposal, vote_flag: true) + create(:vote, voter: user2, votable: proposal, vote_flag: true) + + login_as(author) + visit root_path + + visit new_proposal_notification_path(proposal_id: proposal.id) + + fill_in 'proposal_notification_title', with: "Thank you for supporting my proposal" + fill_in 'proposal_notification_body', with: "Please share it with others so we can make it happen!" + click_button "Send message" + + expect(page).to have_content "Your message has been sent correctly." + + logout + login_as user1 + visit root_path + + find(".icon-notification").click + + expect(page).to have_css ".notification", count: 1 + expect(page).to have_content "There is one new notification on #{proposal.title}" + expect(page).to have_xpath "//a[@href='#{notification_path(Notification.last)}']" + + logout + login_as user2 + visit root_path + + find(".icon-notification").click + + expect(page).to have_css ".notification", count: 1 + expect(page).to have_content "There is one new notification on #{proposal.title}" + expect(page).to have_xpath "//a[@href='#{notification_path(Notification.first)}']" + + logout + login_as user3 + visit root_path + + find(".icon-no-notification").click + + expect(page).to have_css ".notification", count: 0 + end + + pending "group notifications for the same proposal" + + end + context "mark as read" do scenario "mark a single notification as read" do diff --git a/spec/features/proposal_notifications_spec.rb b/spec/features/proposal_notifications_spec.rb new file mode 100644 index 000000000..6092289f0 --- /dev/null +++ b/spec/features/proposal_notifications_spec.rb @@ -0,0 +1,109 @@ +require 'rails_helper' + +feature 'Proposal Notifications' do + + scenario "Send a notification" do + author = create(:user) + proposal = create(:proposal, author: author) + + login_as(author) + visit root_path + + click_link "My activity" + + within("#proposal_#{proposal.id}") do + click_link "Send notification" + end + + fill_in 'proposal_notification_title', with: "Thank you for supporting my proposal" + fill_in 'proposal_notification_body', with: "Please share it with others so we can make it happen!" + click_button "Send message" + + expect(page).to have_content "Your message has been sent correctly." + expect(page).to have_content "Thank you for supporting my proposal" + expect(page).to have_content "Please share it with others so we can make it happen!" + end + + scenario "Show notifications" do + proposal = create(:proposal) + notification1 = create(:proposal_notification, proposal: proposal, title: "Hey guys", body: "Just wanted to let you know that...") + notification2 = create(:proposal_notification, proposal: proposal, title: "Another update", body: "We are almost there please share with your peoples!") + + visit proposal_path(proposal) + + expect(page).to have_content "Hey guys" + expect(page).to have_content "Just wanted to let you know that..." + + expect(page).to have_content "Another update" + expect(page).to have_content "We are almost there please share with your peoples!" + end + + scenario "Message about receivers" do + author = create(:user) + proposal = create(:proposal, author: author) + + 7.times { create(:vote, votable: proposal, vote_flag: true) } + + login_as(author) + visit new_proposal_notification_path(proposal_id: proposal.id) + + expect(page).to have_content "This message will be send to 7 people and it will be visible in the proposal's page" + expect(page).to have_link("the proposal's page", href: proposal_path(proposal, anchor: 'comments')) + end + + context "Permissions" do + + scenario "Link to send the message" do + user = create(:user) + author = create(:user) + proposal = create(:proposal, author: author) + + login_as(author) + visit user_path(author) + + within("#proposal_#{proposal.id}") do + expect(page).to have_link "Send notification" + end + + login_as(user) + visit user_path(author) + + within("#proposal_#{proposal.id}") do + expect(page).to_not have_link "Send message" + end + end + + scenario "Accessing form directly" do + user = create(:user) + author = create(:user) + proposal = create(:proposal, author: author) + + login_as(user) + visit new_proposal_notification_path(proposal_id: proposal.id) + + expect(current_path).to eq(proposals_path) + expect(page).to have_content("You do not have permission to carry out the action") + end + + end + + scenario "Error messages" do + author = create(:user) + proposal = create(:proposal, author: author) + + login_as(author) + + visit new_proposal_notification_path(proposal_id: proposal.id) + click_button "Send message" + + expect(page).to have_content error_message + end + + context "Limits" do + + pending "Cannot send more than one notification within established interval" + pending "use timecop to make sure notifications can be sent after time interval" + + end + +end \ No newline at end of file diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb new file mode 100644 index 000000000..61137912d --- /dev/null +++ b/spec/helpers/application_helper_spec.rb @@ -0,0 +1,27 @@ +require 'rails_helper' + +describe ApplicationHelper do + + describe "#author_of?" do + it "should be true if user is the author" do + user = create(:user) + proposal = create(:proposal, author: user) + expect(author_of?(proposal, user)).to eq true + end + + it "should be false if user is not the author" do + user = create(:user) + proposal = create(:proposal) + expect(author_of?(proposal, user)).to eq false + end + + it "should be false if user or authorable is nil" do + user = create(:user) + proposal = create(:proposal) + + expect(author_of?(nil, user)).to eq false + expect(author_of?(proposal, nil)).to eq false + end + end + +end \ No newline at end of file diff --git a/spec/helpers/notifications_helper_spec.rb b/spec/helpers/notifications_helper_spec.rb deleted file mode 100644 index ccb3b3a9d..000000000 --- a/spec/helpers/notifications_helper_spec.rb +++ /dev/null @@ -1,25 +0,0 @@ -require 'rails_helper' - -describe NotificationsHelper do - - describe "#notification_action" do - let(:debate) { create :debate } - let(:debate_comment) { create :comment, commentable: debate } - - context "when action was comment on a debate" do - it "returns correct text when someone comments on your debate" do - notification = create :notification, notifiable: debate - expect(notification_action(notification)).to eq "comments_on" - end - end - - context "when action was comment on a debate" do - it "returns correct text when someone replies to your comment" do - notification = create :notification, notifiable: debate_comment - expect(notification_action(notification)).to eq "replies_to" - end - end - end - - -end diff --git a/spec/lib/email_digests_spec.rb b/spec/lib/email_digests_spec.rb new file mode 100644 index 000000000..6fc6eef53 --- /dev/null +++ b/spec/lib/email_digests_spec.rb @@ -0,0 +1,9 @@ +require 'rails_helper' + +describe EmailDigest do + + describe "create" do + pending "only send unread notifications" + end + +end \ No newline at end of file diff --git a/spec/models/abilities/common_spec.rb b/spec/models/abilities/common_spec.rb index 468173797..ab14c01bd 100644 --- a/spec/models/abilities/common_spec.rb +++ b/spec/models/abilities/common_spec.rb @@ -37,6 +37,10 @@ describe "Abilities::Common" do it { should_not be_able_to(:comment_as_administrator, proposal) } it { should_not be_able_to(:comment_as_moderator, proposal) } + it { should be_able_to(:new, DirectMessage) } + it { should_not be_able_to(:create, DirectMessage) } + it { should_not be_able_to(:show, DirectMessage) } + describe 'flagging content' do it { should be_able_to(:flag, debate) } it { should be_able_to(:unflag, debate) } @@ -85,6 +89,7 @@ describe "Abilities::Common" do describe "when level 2 verified" do let(:own_spending_proposal) { create(:spending_proposal, author: user) } + let(:own_direct_message) { create(:direct_message, sender: user) } before{ user.update(residence_verified_at: Time.now, confirmed_phone: "1") } it { should be_able_to(:vote, Proposal) } @@ -93,10 +98,16 @@ describe "Abilities::Common" do it { should be_able_to(:create, SpendingProposal) } it { should_not be_able_to(:destroy, create(:spending_proposal)) } it { should_not be_able_to(:destroy, own_spending_proposal) } + + it { should be_able_to(:new, DirectMessage) } + it { should be_able_to(:create, DirectMessage) } + it { should be_able_to(:show, own_direct_message) } + it { should_not be_able_to(:show, create(:direct_message)) } end describe "when level 3 verified" do let(:own_spending_proposal) { create(:spending_proposal, author: user) } + let(:own_direct_message) { create(:direct_message, sender: user) } before{ user.update(verified_at: Time.now) } it { should be_able_to(:vote, Proposal) } @@ -105,5 +116,10 @@ describe "Abilities::Common" do it { should be_able_to(:create, SpendingProposal) } it { should_not be_able_to(:destroy, create(:spending_proposal)) } it { should_not be_able_to(:destroy, own_spending_proposal) } + + it { should be_able_to(:new, DirectMessage) } + it { should be_able_to(:create, DirectMessage) } + it { should be_able_to(:show, own_direct_message) } + it { should_not be_able_to(:show, create(:direct_message)) } end end diff --git a/spec/models/abilities/everyone_spec.rb b/spec/models/abilities/everyone_spec.rb index 3f1e57278..30ce5c3a6 100644 --- a/spec/models/abilities/everyone_spec.rb +++ b/spec/models/abilities/everyone_spec.rb @@ -26,4 +26,6 @@ describe "Abilities::Everyone" do it { should be_able_to(:index, SpendingProposal) } it { should_not be_able_to(:create, SpendingProposal) } -end + + pending "only authors can access new and create for ProposalNotifications" +end \ No newline at end of file diff --git a/spec/models/direct_message_spec.rb b/spec/models/direct_message_spec.rb new file mode 100644 index 000000000..e982f9e56 --- /dev/null +++ b/spec/models/direct_message_spec.rb @@ -0,0 +1,85 @@ +require 'rails_helper' + +describe DirectMessage do + + let(:direct_message) { build(:direct_message) } + + before(:each) do + Setting[:direct_message_max_per_day] = 3 + end + + it "should be valid" do + expect(direct_message).to be_valid + end + + it "should not be valid without a title" do + direct_message.title = nil + expect(direct_message).to_not be_valid + end + + it "should not be valid without a body" do + direct_message.body = nil + expect(direct_message).to_not be_valid + end + + it "should not be valid without an associated sender" do + direct_message.sender = nil + expect(direct_message).to_not be_valid + end + + it "should not be valid without an associated receiver" do + direct_message.receiver = nil + expect(direct_message).to_not be_valid + end + + describe "maximum number of direct messages per day" do + + it "should not be valid if above maximum" do + sender = create(:user) + direct_message1 = create(:direct_message, sender: sender) + direct_message2 = create(:direct_message, sender: sender) + direct_message3 = create(:direct_message, sender: sender) + + direct_message4 = build(:direct_message, sender: sender) + expect(direct_message4).to_not be_valid + end + + it "should be valid if below maximum" do + sender = create(:user) + direct_message1 = create(:direct_message, sender: sender) + direct_message2 = create(:direct_message, sender: sender) + + direct_message3 = build(:direct_message) + expect(direct_message).to be_valid + end + + it "should be valid if no direct_messages sent" do + direct_message = build(:direct_message) + + expect(direct_message).to be_valid + end + + end + + describe "scopes" do + + describe "today" do + it "should return direct messages created today" do + direct_message1 = create(:direct_message, created_at: Time.zone.now.beginning_of_day + 3.hours) + direct_message2 = create(:direct_message, created_at: Time.zone.now) + direct_message3 = create(:direct_message, created_at: Time.zone.now.end_of_day) + + expect(DirectMessage.today.count).to eq 3 + end + + it "should not return direct messages created another day" do + direct_message1 = create(:direct_message, created_at: 1.day.ago) + direct_message2 = create(:direct_message, created_at: 1.day.from_now) + + expect(DirectMessage.today.count).to eq 0 + end + end + + end + +end \ No newline at end of file diff --git a/spec/models/notification_spec.rb b/spec/models/notification_spec.rb index 361211a51..fcf7cab4b 100644 --- a/spec/models/notification_spec.rb +++ b/spec/models/notification_spec.rb @@ -47,4 +47,35 @@ describe Notification do end end + describe "#notification_action" do + + context "when action was comment on a debate" do + it "returns correct text when someone comments on your debate" do + debate = create(:debate) + notification = create :notification, notifiable: debate + + expect(notification.notifiable_action).to eq "comments_on" + end + end + + context "when action was comment on a debate" do + it "returns correct text when someone replies to your comment" do + debate = create(:debate) + debate_comment = create :comment, commentable: debate + notification = create :notification, notifiable: debate_comment + + expect(notification.notifiable_action).to eq "replies_to" + end + end + + context "when action was proposal notification" do + it "returns correct text when the author created a proposal notification" do + proposal_notification = create(:proposal_notification) + notification = create :notification, notifiable: proposal_notification + + expect(notification.notifiable_action).to eq "proposal_notification" + end + end + end + end diff --git a/spec/models/proposal_notification_spec.rb b/spec/models/proposal_notification_spec.rb new file mode 100644 index 000000000..f66b254b6 --- /dev/null +++ b/spec/models/proposal_notification_spec.rb @@ -0,0 +1,59 @@ +require 'rails_helper' + +describe ProposalNotification do + let(:notification) { build(:proposal_notification) } + + it "should be valid" do + expect(notification).to be_valid + end + + it "should not be valid without a title" do + notification.title = nil + expect(notification).to_not be_valid + end + + it "should not be valid without a body" do + notification.body = nil + expect(notification).to_not be_valid + end + + it "should not be valid without an associated proposal" do + notification.proposal = nil + expect(notification).to_not be_valid + end + + describe "minimum interval between notifications" do + + before(:each) do + Setting[:proposal_notification_minimum_interval_in_days] = 3 + end + + it "should not be valid if below minium interval" do + proposal = create(:proposal) + + notification1 = create(:proposal_notification, proposal: proposal) + notification2 = build(:proposal_notification, proposal: proposal) + + proposal.reload + expect(notification2).to_not be_valid + end + + it "should be valid if notifications above minium interval" do + proposal = create(:proposal) + + notification1 = create(:proposal_notification, proposal: proposal, created_at: 4.days.ago) + notification2 = build(:proposal_notification, proposal: proposal) + + proposal.reload + expect(notification2).to be_valid + end + + it "should be valid if no notifications sent" do + notification1 = build(:proposal_notification) + + expect(notification1).to be_valid + end + + end + +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 054e043f9..143053c77 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -83,6 +83,18 @@ describe User do expect(subject.newsletter).to eq(true) end end + + describe 'email_digest' do + it 'should be true by default' do + expect(subject.email_digest).to eq(true) + end + end + + describe 'email_on_direct_message' do + it 'should be true by default' do + expect(subject.email_on_direct_message).to eq(true) + end + end end describe "administrator?" do diff --git a/spec/support/common_actions.rb b/spec/support/common_actions.rb index 18a3f9499..80bdde907 100644 --- a/spec/support/common_actions.rb +++ b/spec/support/common_actions.rb @@ -196,4 +196,39 @@ module CommonActions tag_cloud.tags.map(&:name) end + def create_proposal_notification(proposal) + login_as(proposal.author) + visit root_path + + click_link "My activity" + + within("#proposal_#{proposal.id}") do + click_link "Send notification" + end + + fill_in 'proposal_notification_title', with: "Thank you for supporting my proposal #{proposal.title}" + fill_in 'proposal_notification_body', with: "Please share it with others so we can make it happen! #{proposal.summary}" + click_button "Send message" + + expect(page).to have_content "Your message has been sent correctly." + Notification.last + end + + def create_direct_message(sender, receiver) + login_as(sender) + visit user_path(receiver) + + click_link "Send private message" + + expect(page).to have_content "Send private message to #{receiver.name}" + + fill_in 'direct_message_title', with: "Hey #{receiver.name}!" + fill_in 'direct_message_body', with: "How are you doing? This is #{sender.name}" + + click_button "Send message" + + expect(page).to have_content "You message has been sent successfully." + DirectMessage.last + end + end