diff --git a/.codeclimate.yml b/.codeclimate.yml index 719b2807..a187069d 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -20,6 +20,8 @@ engines: enabled: true rubocop: enabled: true + exclude_fingerprints: + - 74f18007b920e8d81148d2f6a2756534 ratings: paths: - 'Gemfile.lock' diff --git a/Gemfile b/Gemfile index 9fd59b62..0d8e8d7a 100644 --- a/Gemfile +++ b/Gemfile @@ -17,6 +17,7 @@ gem 'exception_notification' gem 'httparty' gem 'json' gem 'kaminari' +gem 'mailboxer' gem 'paperclip' gem 'pg' gem 'pundit' diff --git a/Gemfile.lock b/Gemfile.lock index 92215068..d104cb51 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -65,6 +65,12 @@ GEM brakeman (3.4.0) builder (3.2.2) byebug (9.0.5) + carrierwave (0.11.2) + activemodel (>= 3.2.0) + activesupport (>= 3.2.0) + json (>= 1.7) + mime-types (>= 1.16) + mimemagic (>= 0.3.0) climate_control (0.0.3) activesupport (>= 3.0) cocaine (0.5.8) @@ -125,6 +131,9 @@ GEM nokogiri (>= 1.5.9) mail (2.6.4) mime-types (>= 1.16, < 4) + mailboxer (0.14.0) + carrierwave (>= 0.5.8) + rails (>= 4.2.0) method_source (0.8.2) mime-types (3.1) mime-types-data (~> 3.2015) @@ -284,6 +293,7 @@ DEPENDENCIES json json-schema kaminari + mailboxer paperclip pg pry-byebug diff --git a/app/assets/images/.DS_Store b/app/assets/images/.DS_Store deleted file mode 100644 index 2bbe5f6a..00000000 Binary files a/app/assets/images/.DS_Store and /dev/null differ diff --git a/app/assets/images/topright_sprite.png b/app/assets/images/topright_sprite.png index 163dd6f7..4c969887 100644 Binary files a/app/assets/images/topright_sprite.png and b/app/assets/images/topright_sprite.png differ diff --git a/app/assets/images/user_sprite.png b/app/assets/images/user_sprite.png old mode 100755 new mode 100644 index 72bcafac..045d48fd Binary files a/app/assets/images/user_sprite.png and b/app/assets/images/user_sprite.png differ diff --git a/app/assets/stylesheets/application.scss.erb b/app/assets/stylesheets/application.scss.erb index a0a915b7..988b8241 100644 --- a/app/assets/stylesheets/application.scss.erb +++ b/app/assets/stylesheets/application.scss.erb @@ -826,7 +826,7 @@ label { position:absolute; pointer-events:none; background-repeat:no-repeat; - background-image: url(<%= asset_data_uri('user_sprite.png') %>); + background-image: url(<%= asset_path('user_sprite.png') %>); } .accountSettings .accountIcon { background-position: 0 0; @@ -3076,3 +3076,7 @@ script.data-gratipay-username { display: inline; float: left; } + +.inline { + display: inline-block; +} diff --git a/app/assets/stylesheets/apps.css.erb b/app/assets/stylesheets/apps.css.erb index e6d75dd7..a88199af 100644 --- a/app/assets/stylesheets/apps.css.erb +++ b/app/assets/stylesheets/apps.css.erb @@ -129,3 +129,11 @@ box-sizing: border-box; border-radius: 2px; } + +.back-to-mapping { + margin: 1em; + width: auto; + max-width: 100%; + box-sizing: border-box; +} + diff --git a/app/assets/stylesheets/clean.css.erb b/app/assets/stylesheets/clean.css.erb index e4da394b..b25816f0 100644 --- a/app/assets/stylesheets/clean.css.erb +++ b/app/assets/stylesheets/clean.css.erb @@ -210,7 +210,10 @@ } .addMap { background-position: -96px 0; - margin-right:10px; +} +.notificationsIcon { + background-position: -128px 0; + margin-right: 10px; // make it look more natural next to the account menu icon } .importDialog:hover { background-position: 0 -32px; @@ -758,7 +761,7 @@ } .exploreMapsCenter .authedApps .exploreMapsIcon { - background-image: url(<%= asset_data_uri('user_sprite.png') %>); + background-image: url(<%= asset_path('user_sprite.png') %>); background-position: 0 -32px; } .exploreMapsCenter .myMaps .exploreMapsIcon { @@ -781,6 +784,10 @@ background-image: url(<%= asset_path 'exploremaps_sprite.png' %>); background-position: -96px 0; } +.exploreMapsCenter .notificationsLink .exploreMapsIcon { + background-image: url(<%= asset_path 'user_sprite.png' %>); + background-position: 0 -128px; +} .authedApps:hover .exploreMapsIcon, .authedApps.active .exploreMapsIcon { background-position-x: -32px; } @@ -799,6 +806,9 @@ .sharedMaps:hover .exploreMapsIcon, .sharedMaps.active .exploreMapsIcon { background-position: -128px -32px; } +.notificationsLink:hover .exploreMapsIcon, .notificationsLink.active .exploreMapsIcon { + background-position-x: -32px; +} .mapsWrapper { /*overflow-y: auto; */ diff --git a/app/assets/stylesheets/mobile.scss.erb b/app/assets/stylesheets/mobile.scss.erb index 01fa5f61..e7eb9a7d 100644 --- a/app/assets/stylesheets/mobile.scss.erb +++ b/app/assets/stylesheets/mobile.scss.erb @@ -213,8 +213,17 @@ line-height: 50px; } +#mobile_header #menu_icon .unread-notifications-dot { + top: 5px; + left: 29px; + width: 12px; + height: 12px; + border: 3px solid #eee; + border-radius: 9px; +} + #mobile_menu { - display: none; + display: none; background: #EEE; position: fixed; top: 50px; @@ -222,11 +231,20 @@ padding: 10px; width: 200px; box-shadow: 3px 3px 3px rgba(0,0,0,0.23), 3px 3px 3px rgba(0,0,0,0.16); -} -#mobile_menu li { - padding: 10px; - list-style: none; + li { + padding: 10px; + list-style: none; + + &.notifications { + position: relative; + + .unread-notifications-dot { + top: 17px; + left: 0px; + } + } + } } /* diff --git a/app/assets/stylesheets/notifications.scss.erb b/app/assets/stylesheets/notifications.scss.erb new file mode 100644 index 00000000..b25bf4e8 --- /dev/null +++ b/app/assets/stylesheets/notifications.scss.erb @@ -0,0 +1,71 @@ +$unread_notifications_dot_size: 8px; +.unread-notifications-dot { + width: $unread_notifications_dot_size; + height: $unread_notifications_dot_size; + background-color: #e22; + border-radius: $unread_notifications_dot_size / 2; + position: absolute; + top: 0; + right: 0; +} + +.upperRightUI { + .notificationsIcon { + position: relative; + } +} + +.controller-notifications { + ul.notifications { + list-style: none; + } + + $menu_bar_height: 6em; + .notificationPage, + .notificationsPage { + width: auto; + max-width: 100%; + box-sizing: border-box; + margin: 1em; + margin-top: 1em + $menu_bar_height; + + & > .title { + border-bottom: 1px solid #eee; + padding-bottom: 0.25em; + margin-bottom: 0.5em; + } + + .back { + margin-top: 1em; + } + } + + + .notification { + .notification-subject { + width: 25%; + } + .notification-body { + width: 50%; + } + .notification-read-unread { + width: 10%; + } + + .notification-body, + .notification-subject, + .notification-read-unread { + display: inline-block; + vertical-align: top; + font-weight: 300; + } + + &.unread { + .notification-body, + .notification-subject, + .notification-read-unread { + font-weight: bold; + } + } + } +} diff --git a/app/controllers/access_controller.rb b/app/controllers/access_controller.rb index c48ac418..a0271981 100644 --- a/app/controllers/access_controller.rb +++ b/app/controllers/access_controller.rb @@ -21,7 +21,8 @@ class AccessController < ApplicationController def access_request request = AccessRequest.create(user: current_user, map: @map) # what about push notification to map owner? - MapMailer.access_request_email(request, @map).deliver_later + mail = MapMailer.access_request_email(request, @map) + @map.user.notify(mail.subject, mail.body) respond_to do |format| format.json do @@ -37,7 +38,9 @@ class AccessController < ApplicationController @map.add_new_collaborators(user_ids).each do |user_id| # add_new_collaborators returns array of added users, # who we then send an email to - MapMailer.invite_to_edit_email(@map, current_user, User.find(user_id)).deliver_later + user = User.find(user_id) + mail = MapMailer.invite_to_edit_email(@map, current_user, User.find(user_id)) + user.notify(mail.subject, mail.body) end @map.remove_old_collaborators(user_ids) @@ -51,7 +54,7 @@ class AccessController < ApplicationController # GET maps/:id/approve_access/:request_id def approve_access request = AccessRequest.find(params[:request_id]) - request.approve() + request.approve respond_to do |format| format.html { redirect_to map_path(@map), notice: 'Request was approved' } end @@ -60,7 +63,7 @@ class AccessController < ApplicationController # GET maps/:id/deny_access/:request_id def deny_access request = AccessRequest.find(params[:request_id]) - request.deny() + request.deny respond_to do |format| format.html { redirect_to map_path(@map), notice: 'Request was turned down' } end @@ -69,7 +72,7 @@ class AccessController < ApplicationController # POST maps/:id/approve_access/:request_id def approve_access_post request = AccessRequest.find(params[:request_id]) - request.approve() + request.approve respond_to do |format| format.json do head :ok @@ -80,7 +83,7 @@ class AccessController < ApplicationController # POST maps/:id/deny_access/:request_id def deny_access_post request = AccessRequest.find(params[:request_id]) - request.deny() + request.deny respond_to do |format| format.json do head :ok @@ -94,5 +97,4 @@ class AccessController < ApplicationController @map = Map.find(params[:id]) authorize @map end - end diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb new file mode 100644 index 00000000..4759ef20 --- /dev/null +++ b/app/controllers/notifications_controller.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true +class NotificationsController < ApplicationController + before_action :set_receipts, only: [:index, :show, :mark_read, :mark_unread] + before_action :set_notification, only: [:show, :mark_read, :mark_unread] + before_action :set_receipt, only: [:show, :mark_read, :mark_unread] + + def index + @notifications = current_user.mailbox.notifications + + respond_to do |format| + format.html + format.json do + render json: @notifications.map do |notification| + receipt = @receipts.find_by(notification_id: notification.id) + notification.as_json.merge(is_read: receipt.is_read) + end + end + end + end + + def show + @receipt.update(is_read: true) + respond_to do |format| + format.html + format.json do + render json: @notification.as_json.merge( + is_read: @receipt.is_read + ) + end + end + end + + def mark_read + @receipt.update(is_read: true) + respond_to do |format| + format.js + format.json do + render json: @notification.as_json.merge( + is_read: @receipt.is_read + ) + end + end + end + + def mark_unread + @receipt.update(is_read: false) + respond_to do |format| + format.js + format.json do + render json: @notification.as_json.merge( + is_read: @receipt.is_read + ) + end + end + end + + def unsubscribe + unsubscribe_redirect_if_logged_out! + check_if_already_unsubscribed! + return if performed? # if one of these checks already redirected, we're done + + if current_user.update(emails_allowed: false) + redirect_to edit_user_path(current_user), + notice: 'You will no longer receive emails from Metamaps.' + else + flash[:alert] = 'Sorry, something went wrong. You have not been unsubscribed from emails.' + redirect_to edit_user_path(current_user) + end + end + + private + + def unsubscribe_redirect_if_logged_out! + return if current_user.present? + + flash[:notice] = 'Continue to unsubscribe from emails by logging in.' + redirect_to "#{sign_in_path}?redirect_to=#{unsubscribe_notifications_path}" + end + + def check_if_already_unsubscribed! + return if current_user.emails_allowed + + redirect_to edit_user_path(current_user), notice: 'You were already unsubscribed from emails.' + end + + def set_receipts + @receipts = current_user.mailboxer_notification_receipts + end + + def set_notification + @notification = current_user.mailbox.notifications.find_by(id: params[:id]) + end + + def set_receipt + @receipt = @receipts.find_by(notification_id: params[:id]) + end +end diff --git a/app/controllers/users/sessions_controller.rb b/app/controllers/users/sessions_controller.rb index fed670ae..1c9c0a1e 100644 --- a/app/controllers/users/sessions_controller.rb +++ b/app/controllers/users/sessions_controller.rb @@ -1,14 +1,25 @@ -class Users::SessionsController < Devise::SessionsController - protected +# frozen_string_literal: true +module Users + class SessionsController < Devise::SessionsController + after_action :store_location, only: [:new] - def after_sign_in_path_for(resource) - stored = stored_location_for(User) - return stored if stored + protected - if request.referer&.match(sign_in_url) || request.referer&.match(sign_up_url) - super - else - request.referer || root_path + def after_sign_in_path_for(resource) + stored = stored_location_for(User) + return stored if stored + + if request.referer&.match(sign_in_url) || request.referer&.match(sign_up_url) + super + else + request.referer || root_path + end + end + + private + + def store_location + store_location_for(User, params[:redirect_to]) if params[:redirect_to] end end end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index a9fff9de..6b771d6f 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -13,13 +13,12 @@ class UsersController < ApplicationController # GET /users/:id/edit def edit - @user = current_user - respond_with(@user) + @user = User.find(current_user.id) end # PUT /users/:id def update - @user = current_user + @user = User.find(current_user.id) if user_params[:password] == '' && user_params[:password_confirmation] == '' # not trying to change the password @@ -96,6 +95,6 @@ class UsersController < ApplicationController private def user_params - params.require(:user).permit(:name, :email, :image, :password, :password_confirmation) + params.require(:user).permit(:name, :email, :image, :password, :password_confirmation, :emails_allowed) end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 3221aa34..cc121cbe 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -37,4 +37,20 @@ module ApplicationHelper def invite_link "#{request.base_url}/join" + (current_user ? "?code=#{current_user.code}" : '') end + + def user_has_unread_notifications? + return @user_has_unread_notifications unless @user_has_unread_notifications.nil? + return (@user_has_unread_notifications = false) if current_user.nil? + current_user.mailboxer_notification_receipts.each do |receipt| + return (@user_has_unread_notifications = true) if receipt.is_read == false + end + @user_has_unread_notifications = false + end + + def user_unread_notification_count + return 0 if current_user.nil? + current_user.mailboxer_notification_receipts.reduce(0) do |total, receipt| + receipt.is_read ? total : total + 1 + end + end end diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb index 59a2175a..10961836 100644 --- a/app/mailers/application_mailer.rb +++ b/app/mailers/application_mailer.rb @@ -2,4 +2,8 @@ class ApplicationMailer < ActionMailer::Base default from: 'team@metamaps.cc' layout 'mailer' + + def deliver + raise NotImplementedError('Please use Mailboxer to send your emails.') + end end diff --git a/app/models/user.rb b/app/models/user.rb index 23ef6440..f6fcb60e 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -2,6 +2,8 @@ require 'open-uri' class User < ApplicationRecord + acts_as_messageable # mailboxer notifications + has_many :topics has_many :synapses has_many :maps @@ -108,4 +110,19 @@ class User < ApplicationRecord def settings=(val) self[:settings] = val end + + # Mailboxer hooks and helper functions + + def mailboxer_email(_message) + return email if emails_allowed + # else return nil, which sends no email + end + + def mailboxer_notifications + mailbox.notifications + end + + def mailboxer_notification_receipts + mailbox.receipts.includes(:notification).where(mailbox_type: nil) + end end diff --git a/app/views/layouts/_account.html.erb b/app/views/layouts/_account.html.erb index 748e5f1b..3d66f687 100644 --- a/app/views/layouts/_account.html.erb +++ b/app/views/layouts/_account.html.erb @@ -18,14 +18,14 @@ <%= link_to "Admin", metacodes_path %> <% end %> -
  • -
    - Share Invite -
  • <%= link_to "Apps", oauth_authorized_applications_path %>
  • +
  • +
    + Share Invite +
  • <%= link_to "Sign Out", "/logout", id: "Logout" %> diff --git a/app/views/layouts/_mobilemenu.html.erb b/app/views/layouts/_mobilemenu.html.erb index e012a808..c557f516 100644 --- a/app/views/layouts/_mobilemenu.html.erb +++ b/app/views/layouts/_mobilemenu.html.erb @@ -2,7 +2,11 @@
    <%= yield(:mobile_title) %>
    - +
    <% end %> diff --git a/app/views/mailboxer/message_mailer/new_message_email.html.erb b/app/views/mailboxer/message_mailer/new_message_email.html.erb new file mode 100644 index 00000000..c779e888 --- /dev/null +++ b/app/views/mailboxer/message_mailer/new_message_email.html.erb @@ -0,0 +1,20 @@ + + + + + + +

    You have a new message: <%= @subject %>

    +

    + You have received a new message: +

    +
    +

    + <%= raw @message.body %> +

    +
    +

    + Visit <%= link_to root_url, root_url %> and go to your inbox for more info. +

    + + diff --git a/app/views/mailboxer/message_mailer/new_message_email.text.erb b/app/views/mailboxer/message_mailer/new_message_email.text.erb new file mode 100644 index 00000000..228ca58a --- /dev/null +++ b/app/views/mailboxer/message_mailer/new_message_email.text.erb @@ -0,0 +1,10 @@ +You have a new message: <%= @subject %> +=============================================== + +You have received a new message: + +----------------------------------------------- +<%= @message.body.html_safe? ? @message.body : strip_tags(@message.body) %> +----------------------------------------------- + +Visit <%= root_url %> and go to your inbox for more info. diff --git a/app/views/mailboxer/message_mailer/reply_message_email.html.erb b/app/views/mailboxer/message_mailer/reply_message_email.html.erb new file mode 100644 index 00000000..fd1286c5 --- /dev/null +++ b/app/views/mailboxer/message_mailer/reply_message_email.html.erb @@ -0,0 +1,20 @@ + + + + + + +

    You have a new reply: <%= @subject %>

    +

    + You have received a new reply: +

    +
    +

    + <%= raw @message.body %> +

    +
    +

    + Visit <%= link_to root_url, root_url %> and go to your inbox for more info. +

    + + diff --git a/app/views/mailboxer/message_mailer/reply_message_email.text.erb b/app/views/mailboxer/message_mailer/reply_message_email.text.erb new file mode 100644 index 00000000..c56bfb5e --- /dev/null +++ b/app/views/mailboxer/message_mailer/reply_message_email.text.erb @@ -0,0 +1,10 @@ +You have a new reply: <%= @subject %> +=============================================== + +You have received a new reply: + +----------------------------------------------- +<%= @message.body.html_safe? ? @message.body : strip_tags(@message.body) %> +----------------------------------------------- + +Visit <%= root_url %> and go to your inbox for more info. diff --git a/app/views/mailboxer/notification_mailer/new_notification_email.html.erb b/app/views/mailboxer/notification_mailer/new_notification_email.html.erb new file mode 100644 index 00000000..23ee4087 --- /dev/null +++ b/app/views/mailboxer/notification_mailer/new_notification_email.html.erb @@ -0,0 +1,10 @@ + + + + + + + <% binding.pry %> + <%= raw @notification.body.parts[1].encoded %> + + diff --git a/app/views/mailboxer/notification_mailer/new_notification_email.text.erb b/app/views/mailboxer/notification_mailer/new_notification_email.text.erb new file mode 100644 index 00000000..fa39d477 --- /dev/null +++ b/app/views/mailboxer/notification_mailer/new_notification_email.text.erb @@ -0,0 +1,2 @@ +<% body = @notification.body.parts[0].encoded %> +<%= body.html_safe? ? body : strip_tags(body) %> diff --git a/app/views/map_mailer/access_request_email.html.erb b/app/views/map_mailer/access_request_email.html.erb index ae4f4018..2e89eb6e 100644 --- a/app/views/map_mailer/access_request_email.html.erb +++ b/app/views/map_mailer/access_request_email.html.erb @@ -1,23 +1,16 @@ - - - - - - +
    + <% button_style = "background-color:#4fc059;border-radius:2px;color:white;display:inline-block;font-family:Roboto,Arial,Helvetica,sans-serif;font-size:12px;font-weight:bold;min-height:29px;line-height:29px;min-width:54px;outline:0px;padding:0 8px;text-align:center;text-decoration:none" %> -
    - <% button_style = "background-color:#4fc059;border-radius:2px;color:white;display:inline-block;font-family:Roboto,Arial,Helvetica,sans-serif;font-size:12px;font-weight:bold;min-height:29px;line-height:29px;min-width:54px;outline:0px;padding:0 8px;text-align:center;text-decoration:none" %> +

    <%= @request.user.name %> is requesting access to collaboratively edit the following map:

    -

    <%= @request.user.name %> is requesting access to collaboratively edit the following map:

    +

    <%= @map.name %>

    -

    <%= @map.name %>

    +

    <%= link_to "Allow", approve_access_map_url(id: @map.id, request_id: @request.id), target: "_blank", style: "font-size: 18px; text-decoration: none; color: #4fc059;" %> +

    <%= link_to "Decline", deny_access_map_url(id: @map.id, request_id: @request.id), target: "_blank", style: "font-size: 18px; text-decoration: none; color: #DB5D5D;" %>

    -

    <%= link_to "Allow", approve_access_map_url(id: @map.id, request_id: @request.id), target: "_blank", style: "font-size: 18px; text-decoration: none; color: #4fc059;" %> -

    <%= link_to "Decline", deny_access_map_url(id: @map.id, request_id: @request.id), target: "_blank", style: "font-size: 18px; text-decoration: none; color: #DB5D5D;" %>

    + <%= link_to 'Open in Metamaps', map_url(@map), target: "_blank", style: button_style %> - <%= link_to 'Open in Metamaps', map_url(@map), target: "_blank", style: button_style %> +

    Make sense with Metamaps

    -

    Make sense with Metamaps

    -
    - - + <%= render partial: 'shared/mailer_unsubscribe_link' %> +
    diff --git a/app/views/map_mailer/access_request_email.text.erb b/app/views/map_mailer/access_request_email.text.erb index ad698d9e..25ccb2bd 100644 --- a/app/views/map_mailer/access_request_email.text.erb +++ b/app/views/map_mailer/access_request_email.text.erb @@ -7,4 +7,4 @@ Decline [<%= deny_access_map_url(id: @map.id, request_id: @request.id) %>] Make sense with Metamaps - +<%= render partial: 'shared/mailer_unsubscribe_link' %> diff --git a/app/views/map_mailer/invite_to_edit_email.html.erb b/app/views/map_mailer/invite_to_edit_email.html.erb index 73067c48..cd2b7b2e 100644 --- a/app/views/map_mailer/invite_to_edit_email.html.erb +++ b/app/views/map_mailer/invite_to_edit_email.html.erb @@ -1,22 +1,15 @@ - - - - - - +
    + <% button_style = "background-color:#4fc059;border-radius:2px;color:white;display:inline-block;font-family:Roboto,Arial,Helvetica,sans-serif;font-size:12px;font-weight:bold;min-height:29px;line-height:29px;min-width:54px;outline:0px;padding:0 8px;text-align:center;text-decoration:none" %> -
    - <% button_style = "background-color:#4fc059;border-radius:2px;color:white;display:inline-block;font-family:Roboto,Arial,Helvetica,sans-serif;font-size:12px;font-weight:bold;min-height:29px;line-height:29px;min-width:54px;outline:0px;padding:0 8px;text-align:center;text-decoration:none" %> +

    <%= @inviter.name %> has invited you to collaboratively edit the following map:

    +

    <%= link_to @map.name, map_url(@map), target: "_blank", style: "font-size: 18px; text-decoration: none; color: #4fc059;" %>

    + <% if @map.desc %> +

    <%= @map.desc %>

    + <% end %> -

    <%= @inviter.name %> has invited you to collaboratively edit the following map:

    -

    <%= link_to @map.name, map_url(@map), target: "_blank", style: "font-size: 18px; text-decoration: none; color: #4fc059;" %>

    - <% if @map.desc %> -

    <%= @map.desc %>

    - <% end %> + <%= link_to 'Open in Metamaps', map_url(@map), target: "_blank", style: button_style %> - <%= link_to 'Open in Metamaps', map_url(@map), target: "_blank", style: button_style %> +

    Make sense with Metamaps

    -

    Make sense with Metamaps

    -
    - - + <%= render partial: 'shared/mailer_unsubscribe_link' %> +
    diff --git a/app/views/map_mailer/invite_to_edit_email.text.erb b/app/views/map_mailer/invite_to_edit_email.text.erb index 80eecfed..7d3bf397 100644 --- a/app/views/map_mailer/invite_to_edit_email.text.erb +++ b/app/views/map_mailer/invite_to_edit_email.text.erb @@ -4,4 +4,4 @@ Make sense with Metamaps - +<%= render partial: 'shared/mailer_unsubscribe_link' %> diff --git a/app/views/notifications/_header.html.erb b/app/views/notifications/_header.html.erb new file mode 100644 index 00000000..f93f46a6 --- /dev/null +++ b/app/views/notifications/_header.html.erb @@ -0,0 +1,18 @@ +
    + +
    +

    + <% if devise_error_messages? %> + <%= devise_error_messages! %> + <% elsif notice %> + <%= notice %> + <% end %> +

    diff --git a/app/views/notifications/index.html.erb b/app/views/notifications/index.html.erb new file mode 100644 index 00000000..59ac08a3 --- /dev/null +++ b/app/views/notifications/index.html.erb @@ -0,0 +1,33 @@ +<% content_for :title, 'Notifications | Metamaps' %> +<% content_for :mobile_title, 'Notifications' %> + +
    +
    +

    My Notifications

    +
      + <% @notifications.each do |notification| %> + <% receipt = @receipts.find_by(notification_id: notification.id) %> +
    • + <%= link_to notification_path(notification.id) do %> +
      + <%= notification.subject %> +
      +
      + <%= notification.body.truncate(70) %> +
      + <% end %> +
      + <% if receipt.is_read? %> + <%= link_to 'mark as unread', mark_unread_notification_path(notification.id), remote: true, method: :put %> + <% else %> + <%= link_to 'mark as read', mark_read_notification_path(notification.id), remote: true, method: :put %> + <% end %> +
      +
    • + <% end %> +
    +
    + <%= render partial: 'shared/back_to_mapping' %> +
    + +<%= render partial: 'notifications/header' %> diff --git a/app/views/notifications/mark_read.js.erb b/app/views/notifications/mark_read.js.erb new file mode 100644 index 00000000..5b28f453 --- /dev/null +++ b/app/views/notifications/mark_read.js.erb @@ -0,0 +1,6 @@ +$('#notification-<%= @notification.id %> .notification-read-unread > a') + .text('mark as unread') + .attr('href', '<%= mark_unread_notification_path(@notification.id) %>') +$('#notification-<%= @notification.id %>') + .removeClass('unread') + .addClass('read') diff --git a/app/views/notifications/mark_unread.js.erb b/app/views/notifications/mark_unread.js.erb new file mode 100644 index 00000000..46744388 --- /dev/null +++ b/app/views/notifications/mark_unread.js.erb @@ -0,0 +1,6 @@ +$('#notification-<%= @notification.id %> .notification-read-unread > a') + .text('mark as read') + .attr('href', '<%= mark_read_notification_path(@notification.id) %>') +$('#notification-<%= @notification.id %>') + .removeClass('read') + .addClass('unread') diff --git a/app/views/notifications/show.html.erb b/app/views/notifications/show.html.erb new file mode 100644 index 00000000..be641f66 --- /dev/null +++ b/app/views/notifications/show.html.erb @@ -0,0 +1,15 @@ +<% content_for :title, 'Notifications | Metamaps' %> +<% content_for :mobile_title, 'Notifications' %> + +
    +
    +

    <%= @notification.subject %>

    + <%= @notification.body %> +
    + <%= link_to 'Back', notifications_path %> +
    +
    + <%= render partial: 'shared/back_to_mapping' %> +
    + +<%= render partial: 'notifications/header' %> diff --git a/app/views/shared/_back_to_mapping.html.erb b/app/views/shared/_back_to_mapping.html.erb new file mode 100644 index 00000000..682a71e6 --- /dev/null +++ b/app/views/shared/_back_to_mapping.html.erb @@ -0,0 +1,3 @@ +
    + Back to mapping +
    diff --git a/app/views/shared/_mailer_unsubscribe_link.html.erb b/app/views/shared/_mailer_unsubscribe_link.html.erb new file mode 100644 index 00000000..2bbe7a89 --- /dev/null +++ b/app/views/shared/_mailer_unsubscribe_link.html.erb @@ -0,0 +1,3 @@ + diff --git a/app/views/shared/_mailer_unsubscribe_link.text.erb b/app/views/shared/_mailer_unsubscribe_link.text.erb new file mode 100644 index 00000000..3a2f7d0d --- /dev/null +++ b/app/views/shared/_mailer_unsubscribe_link.text.erb @@ -0,0 +1,5 @@ + + +You can unsubscribe from all Metamaps emails by visiting the following link: + +<%= unsubscribe_notifications_url %> diff --git a/app/views/users/edit.html.erb b/app/views/users/edit.html.erb index 92890a92..8427582a 100644 --- a/app/views/users/edit.html.erb +++ b/app/views/users/edit.html.erb @@ -33,23 +33,35 @@
    <%= @user.name %>
    - <%= form.label :name, "Name:", :class => "firstFieldText" %> - <%= form.text_field :name %> + <%= form.label :name, "Name:", class: 'firstFieldText' %> + <%= form.text_field :name %> +
    +
    + <%= form.label :email, "Email:", class: 'firstFieldText' %> + <%= form.email_field :email %> +
    +
    + <%= form.label :emails_allowed, class: 'firstFieldText' do %> + <%= form.check_box :emails_allowed, class: 'inline' %> + Send Metamaps notifications to my email. + <% end %>
    -
    <%= form.label :email, "Email:", :class => "firstFieldText" %> - <%= form.email_field :email %>
    Change Password
    -
    - <%= form.label :current_password, "Current Password:", :class => "firstFieldText" %> - <%= password_field_tag :current_password, params[:current_password] %> +
    + <%= form.label :current_password, "Current Password:", :class => "firstFieldText" %> + <%= password_field_tag :current_password, params[:current_password] %> +
    +
    + <%= form.label :password, "New Password:", :class => "firstFieldText" %> + <%= form.password_field :password, :autocomplete => :off%> +
    +
    + <%= form.label :password_confirmation, "Confirm New Password:", :class => "firstFieldText" %> + <%= form.password_field :password_confirmation, :autocomplete => :off%> +
    +
    Oops, don't change password
    -
    <%= form.label :password, "New Password:", :class => "firstFieldText" %> - <%= form.password_field :password, :autocomplete => :off%>
    -
    <%= form.label :password_confirmation, "Confirm New Password:", :class => "firstFieldText" %> - <%= form.password_field :password_confirmation, :autocomplete => :off%>
    -
    Oops, don't change password
    -
    <%= form.submit "Update", class: "update", onclick: "Metamaps.Account.showLoading()" %>
    diff --git a/config/application.rb b/config/application.rb index 0b98bfe8..ff5a621c 100644 --- a/config/application.rb +++ b/config/application.rb @@ -8,14 +8,15 @@ Bundler.require(*Rails.groups) module Metamaps class Application < Rails::Application - config.active_job.queue_adapter = :delayed_job - if ENV['ACTIVE_JOB_FRAMEWORK'] == 'sucker_punch' - config.active_job.queue_adapter = :sucker_punch - end - # Settings in config/environments/* take precedence over those specified here. # Application configuration should go into files in config/initializers # -- all .rb files in that directory are automatically loaded. + # + config.active_job.queue_adapter = if ENV['ACTIVE_JOB_FRAMEWORK'] == 'sucker_punch' + :sucker_punch + else + :delayed_job + end # Custom directories with classes and modules you want to be autoloadable. config.autoload_paths << Rails.root.join('app', 'services') diff --git a/config/environments/development.rb b/config/environments/development.rb index 38741a18..8fef2145 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -14,19 +14,11 @@ Rails.application.configure do config.consider_all_requests_local = true config.action_controller.perform_caching = false - config.action_mailer.delivery_method = :smtp - config.action_mailer.smtp_settings = { - address: ENV['SMTP_SERVER'], - port: ENV['SMTP_PORT'], - user_name: ENV['SMTP_USERNAME'], - password: ENV['SMTP_PASSWORD'], - domain: ENV['SMTP_DOMAIN'], - authentication: 'plain', - enable_starttls_auto: true, - openssl_verify_mode: 'none' + config.action_mailer.delivery_method = :file + config.action_mailer.file_settings = { + location: 'tmp/mails' } config.action_mailer.default_url_options = { host: 'localhost:3000' } - # Don't care if the mailer can't send config.action_mailer.raise_delivery_errors = true # Print deprecation notices to the Rails logger diff --git a/config/initializers/mailboxer.rb b/config/initializers/mailboxer.rb new file mode 100644 index 00000000..9e1efe66 --- /dev/null +++ b/config/initializers/mailboxer.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true +Mailboxer.setup do |config| + # Configures if your application uses or not email sending for Notifications and Messages + config.uses_emails = true + + # Configures the default from for emails sent for Messages and Notifications + config.default_from = 'no-reply@metamaps.cc' + + # Configures the methods needed by mailboxer + config.email_method = :mailboxer_email + config.name_method = :name + + # Configures if you use or not a search engine and which one you are using + # Supported engines: [:solr,:sphinx] + config.search_enabled = false + config.search_engine = :solr + + # Configures maximum length of the message subject and body + config.subject_max_length = 255 + config.body_max_length = 32_000 +end diff --git a/config/routes.rb b/config/routes.rb index 8ba116a1..000784f6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -20,12 +20,25 @@ Metamaps::Application.routes.draw do post 'events/:event', action: :events get :contains - get :request_access, to: 'access#request_access' - get 'approve_access/:request_id', to: 'access#approve_access', as: :approve_access - get 'deny_access/:request_id', to: 'access#deny_access', as: :deny_access - post :access_request, to: 'access#access_request', default: { format: :json } - post 'approve_access/:request_id', to: 'access#approve_access_post', default: { format: :json } - post 'deny_access/:request_id', to: 'access#deny_access_post', default: { format: :json } + get :request_access, + to: 'access#request_access' + get 'approve_access/:request_id', + to: 'access#approve_access', + as: :approve_access + get 'deny_access/:request_id', + to: 'access#deny_access', + as: :deny_access + + post :access_request, + to: 'access#access_request', + default: { format: :json } + post 'approve_access/:request_id', + to: 'access#approve_access_post', + default: { format: :json } + post 'deny_access/:request_id', + to: 'access#deny_access_post', + default: { format: :json } + post :access, to: 'access#access', default: { format: :json } post :star, to: 'stars#create', default: { format: :json } @@ -36,6 +49,15 @@ Metamaps::Application.routes.draw do resources :mappings, except: [:index, :new, :edit] resources :messages, only: [:show, :create, :update, :destroy] + resources :notifications, only: [:index, :show] do + collection do + get :unsubscribe + end + member do + put :mark_read + put :mark_unread + end + end resources :metacode_sets, except: [:show] @@ -109,3 +131,4 @@ Metamaps::Application.routes.draw do get 'load_url_title' end end +# rubocop:enable Rubocop/Metrics/BlockLength diff --git a/db/migrate/20161101031231_create_mailboxer.mailboxer_engine.rb b/db/migrate/20161101031231_create_mailboxer.mailboxer_engine.rb new file mode 100644 index 00000000..99ed59b1 --- /dev/null +++ b/db/migrate/20161101031231_create_mailboxer.mailboxer_engine.rb @@ -0,0 +1,65 @@ +# This migration comes from mailboxer_engine (originally 20110511145103) +class CreateMailboxer < ActiveRecord::Migration + def self.up + #Tables + #Conversations + create_table :mailboxer_conversations do |t| + t.column :subject, :string, :default => "" + t.column :created_at, :datetime, :null => false + t.column :updated_at, :datetime, :null => false + end + #Receipts + create_table :mailboxer_receipts do |t| + t.references :receiver, :polymorphic => true + t.column :notification_id, :integer, :null => false + t.column :is_read, :boolean, :default => false + t.column :trashed, :boolean, :default => false + t.column :deleted, :boolean, :default => false + t.column :mailbox_type, :string, :limit => 25 + t.column :created_at, :datetime, :null => false + t.column :updated_at, :datetime, :null => false + end + #Notifications and Messages + create_table :mailboxer_notifications do |t| + t.column :type, :string + t.column :body, :text + t.column :subject, :string, :default => "" + t.references :sender, :polymorphic => true + t.column :conversation_id, :integer + t.column :draft, :boolean, :default => false + t.string :notification_code, :default => nil + t.references :notified_object, :polymorphic => true + t.column :attachment, :string + t.column :updated_at, :datetime, :null => false + t.column :created_at, :datetime, :null => false + t.boolean :global, default: false + t.datetime :expires + end + + #Indexes + #Conversations + #Receipts + add_index "mailboxer_receipts","notification_id" + + #Messages + add_index "mailboxer_notifications","conversation_id" + + #Foreign keys + #Conversations + #Receipts + add_foreign_key "mailboxer_receipts", "mailboxer_notifications", :name => "receipts_on_notification_id", :column => "notification_id" + #Messages + add_foreign_key "mailboxer_notifications", "mailboxer_conversations", :name => "notifications_on_conversation_id", :column => "conversation_id" + end + + def self.down + #Tables + remove_foreign_key "mailboxer_receipts", :name => "receipts_on_notification_id" + remove_foreign_key "mailboxer_notifications", :name => "notifications_on_conversation_id" + + #Indexes + drop_table :mailboxer_receipts + drop_table :mailboxer_conversations + drop_table :mailboxer_notifications + end +end diff --git a/db/migrate/20161101031232_add_conversation_optout.mailboxer_engine.rb b/db/migrate/20161101031232_add_conversation_optout.mailboxer_engine.rb new file mode 100644 index 00000000..c4f4555a --- /dev/null +++ b/db/migrate/20161101031232_add_conversation_optout.mailboxer_engine.rb @@ -0,0 +1,15 @@ +# This migration comes from mailboxer_engine (originally 20131206080416) +class AddConversationOptout < ActiveRecord::Migration + def self.up + create_table :mailboxer_conversation_opt_outs do |t| + t.references :unsubscriber, :polymorphic => true + t.references :conversation + end + add_foreign_key "mailboxer_conversation_opt_outs", "mailboxer_conversations", :name => "mb_opt_outs_on_conversations_id", :column => "conversation_id" + end + + def self.down + remove_foreign_key "mailboxer_conversation_opt_outs", :name => "mb_opt_outs_on_conversations_id" + drop_table :mailboxer_conversation_opt_outs + end +end diff --git a/db/migrate/20161101031233_add_missing_indices.mailboxer_engine.rb b/db/migrate/20161101031233_add_missing_indices.mailboxer_engine.rb new file mode 100644 index 00000000..fde96718 --- /dev/null +++ b/db/migrate/20161101031233_add_missing_indices.mailboxer_engine.rb @@ -0,0 +1,20 @@ +# This migration comes from mailboxer_engine (originally 20131206080417) +class AddMissingIndices < ActiveRecord::Migration + def change + # We'll explicitly specify its name, as the auto-generated name is too long and exceeds 63 + # characters limitation. + add_index :mailboxer_conversation_opt_outs, [:unsubscriber_id, :unsubscriber_type], + name: 'index_mailboxer_conversation_opt_outs_on_unsubscriber_id_type' + add_index :mailboxer_conversation_opt_outs, :conversation_id + + add_index :mailboxer_notifications, :type + add_index :mailboxer_notifications, [:sender_id, :sender_type] + + # We'll explicitly specify its name, as the auto-generated name is too long and exceeds 63 + # characters limitation. + add_index :mailboxer_notifications, [:notified_object_id, :notified_object_type], + name: 'index_mailboxer_notifications_on_notified_object_id_and_type' + + add_index :mailboxer_receipts, [:receiver_id, :receiver_type] + end +end diff --git a/db/migrate/20161101031234_add_delivery_tracking_info_to_mailboxer_receipts.mailboxer_engine.rb b/db/migrate/20161101031234_add_delivery_tracking_info_to_mailboxer_receipts.mailboxer_engine.rb new file mode 100644 index 00000000..a820919e --- /dev/null +++ b/db/migrate/20161101031234_add_delivery_tracking_info_to_mailboxer_receipts.mailboxer_engine.rb @@ -0,0 +1,8 @@ +# This migration comes from mailboxer_engine (originally 20151103080417) +class AddDeliveryTrackingInfoToMailboxerReceipts < ActiveRecord::Migration + def change + add_column :mailboxer_receipts, :is_delivered, :boolean, default: false + add_column :mailboxer_receipts, :delivery_method, :string + add_column :mailboxer_receipts, :message_id, :string + end +end diff --git a/db/migrate/20161125175229_add_emails_allowed_to_users.rb b/db/migrate/20161125175229_add_emails_allowed_to_users.rb new file mode 100644 index 00000000..609e4309 --- /dev/null +++ b/db/migrate/20161125175229_add_emails_allowed_to_users.rb @@ -0,0 +1,5 @@ +class AddEmailsAllowedToUsers < ActiveRecord::Migration[5.0] + def change + add_column :users, :emails_allowed, :boolean, default: true + end +end diff --git a/db/schema.rb b/db/schema.rb index d16d4fb9..5839929c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20161105160340) do +ActiveRecord::Schema.define(version: 20161125175229) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -63,6 +63,59 @@ ActiveRecord::Schema.define(version: 20161105160340) do t.index ["metacode_set_id"], name: "index_in_metacode_sets_on_metacode_set_id", using: :btree end + create_table "mailboxer_conversation_opt_outs", force: :cascade do |t| + t.string "unsubscriber_type" + t.integer "unsubscriber_id" + t.integer "conversation_id" + t.index ["conversation_id"], name: "index_mailboxer_conversation_opt_outs_on_conversation_id", using: :btree + t.index ["unsubscriber_id", "unsubscriber_type"], name: "index_mailboxer_conversation_opt_outs_on_unsubscriber_id_type", using: :btree + end + + create_table "mailboxer_conversations", force: :cascade do |t| + t.string "subject", default: "" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + create_table "mailboxer_notifications", force: :cascade do |t| + t.string "type" + t.text "body" + t.string "subject", default: "" + t.string "sender_type" + t.integer "sender_id" + t.integer "conversation_id" + t.boolean "draft", default: false + t.string "notification_code" + t.string "notified_object_type" + t.integer "notified_object_id" + t.string "attachment" + t.datetime "updated_at", null: false + t.datetime "created_at", null: false + t.boolean "global", default: false + t.datetime "expires" + t.index ["conversation_id"], name: "index_mailboxer_notifications_on_conversation_id", using: :btree + t.index ["notified_object_id", "notified_object_type"], name: "index_mailboxer_notifications_on_notified_object_id_and_type", using: :btree + t.index ["sender_id", "sender_type"], name: "index_mailboxer_notifications_on_sender_id_and_sender_type", using: :btree + t.index ["type"], name: "index_mailboxer_notifications_on_type", using: :btree + end + + create_table "mailboxer_receipts", force: :cascade do |t| + t.string "receiver_type" + t.integer "receiver_id" + t.integer "notification_id", null: false + t.boolean "is_read", default: false + t.boolean "trashed", default: false + t.boolean "deleted", default: false + t.string "mailbox_type", limit: 25 + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.boolean "is_delivered", default: false + t.string "delivery_method" + t.string "message_id" + t.index ["notification_id"], name: "index_mailboxer_receipts_on_notification_id", using: :btree + t.index ["receiver_id", "receiver_type"], name: "index_mailboxer_receipts_on_receiver_id_and_receiver_type", using: :btree + end + create_table "mappings", force: :cascade do |t| t.text "category" t.integer "xloc" @@ -243,8 +296,8 @@ ActiveRecord::Schema.define(version: 20161105160340) do t.string "password_salt", limit: 255 t.string "persistence_token", limit: 255 t.string "perishable_token", limit: 255 - 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 "code", limit: 8 t.string "joinedwithcode", limit: 8 t.text "settings" @@ -264,6 +317,7 @@ ActiveRecord::Schema.define(version: 20161105160340) do t.integer "image_file_size" t.datetime "image_updated_at" t.integer "generation" + t.boolean "emails_allowed", default: true t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree end @@ -279,5 +333,8 @@ ActiveRecord::Schema.define(version: 20161105160340) do add_foreign_key "access_requests", "maps" add_foreign_key "access_requests", "users" + add_foreign_key "mailboxer_conversation_opt_outs", "mailboxer_conversations", column: "conversation_id", name: "mb_opt_outs_on_conversations_id" + add_foreign_key "mailboxer_notifications", "mailboxer_conversations", column: "conversation_id", name: "notifications_on_conversation_id" + add_foreign_key "mailboxer_receipts", "mailboxer_notifications", column: "notification_id", name: "receipts_on_notification_id" add_foreign_key "tokens", "users" end diff --git a/doc/metamaps-qa-steps.md b/doc/metamaps-qa-steps.md index a3f136ca..7c5c8480 100644 --- a/doc/metamaps-qa-steps.md +++ b/doc/metamaps-qa-steps.md @@ -32,6 +32,13 @@ Run these tests to be reasonably sure that your code changes haven't broken anyt - Add a number of synapses to one of your maps. Reload to see if they are still there. - Rearrange one of your maps and save the layout. Reload to see if the layout is preserved. +### Unsubscribing from Notifications + + - Log out + - Visit /notifications/unsubscribe. It should redirect you to the login page. + - Log in. + - It should redirect you to the user edit page, and you should be unsubscribed. + ### Misc - Login as admin. Change metacode sets.