diff --git a/.codeclimate.yml b/.codeclimate.yml index fbd96af2..a187069d 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -8,6 +8,7 @@ engines: enabled: true config: languages: + count_threshold: 3 # rule of three ruby: mass_threshold: 36 # default: 18 javascript: @@ -19,6 +20,8 @@ engines: enabled: true rubocop: enabled: true + exclude_fingerprints: + - 74f18007b920e8d81148d2f6a2756534 ratings: paths: - 'Gemfile.lock' diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..dddeed14 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,5 @@ + + + +============ +100BD/C = (100)(__)(__)/(__)=__ diff --git a/.travis.yml b/.travis.yml index 99b9a655..3dca7316 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,3 +22,4 @@ script: addons: code_climate: repo_token: 479d3bf56798fbc7fff3fc8151a5ed09e8ac368fd5af332c437b9e07dbebb44e + postgresql: "9.4" 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..5771e366 100644 --- a/app/assets/stylesheets/apps.css.erb +++ b/app/assets/stylesheets/apps.css.erb @@ -1,17 +1,14 @@ .centerContent { position: relative; - margin: 92px auto 0 auto; - padding: 20px 0 60px 20px; - width: 760px; + margin: 0 auto; + width: auto; + max-width: 800px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,.12),0 1px 2px rgba(0,0,0,.24); background: #fff; - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - border-radius: 3px; - border: 1px solid #dcdcdc; - margin-bottom: 10px; + box-sizing: border-box; padding: 15px; + font-family: 'din-regular', sans-serif; } .centerContent .page-header { @@ -129,3 +126,9 @@ box-sizing: border-box; border-radius: 2px; } + +.centerContent.withPadding { + margin-top: 1em; + margin-bottom: 1em; +} + diff --git a/app/assets/stylesheets/clean.css.erb b/app/assets/stylesheets/clean.css.erb index e4da394b..3970f877 100644 --- a/app/assets/stylesheets/clean.css.erb +++ b/app/assets/stylesheets/clean.css.erb @@ -28,6 +28,8 @@ position: absolute; width: 100%; height: 100%; + box-sizing: border-box; + padding-top: 92px; } /*.animations { @@ -210,7 +212,13 @@ } .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 +} +.notificationsIcon:hover { + background-position: -128px -32px; } .importDialog:hover { background-position: 0 -32px; @@ -223,7 +231,6 @@ } .addMap:hover { background-position: -96px -32px; - margin-right:10px; } @@ -471,7 +478,7 @@ background-position: -32px 0; } -.zoomExtents:hover .tooltips, .zoomIn:hover .tooltips, .zoomOut:hover .tooltips, .takeScreenshot:hover .tooltips, .sidebarFilterIcon:hover .tooltipsUnder, .sidebarForkIcon:hover .tooltipsUnder, .addMap:hover .tooltipsUnder, .authenticated .sidebarAccountIcon:hover .tooltipsUnder, +.zoomExtents:hover .tooltips, .zoomIn:hover .tooltips, .zoomOut:hover .tooltips, .takeScreenshot:hover .tooltips, .sidebarFilterIcon:hover .tooltipsUnder, .sidebarForkIcon:hover .tooltipsUnder, .notificationsIcon:hover .tooltipsUnder, .addMap:hover .tooltipsUnder, .authenticated .sidebarAccountIcon:hover .tooltipsUnder, .mapInfoIcon:hover .tooltipsAbove, .openCheatsheet:hover .tooltipsAbove, .chat-button:hover .tooltips, .importDialog:hover .tooltipsUnder, .starMap:hover .tooltipsAbove, .openMetacodeSwitcher:hover .tooltipsAbove, .pinCarousel:not(.isPinned):hover .tooltipsAbove.helpPin, .pinCarousel.isPinned:hover .tooltipsAbove.helpUnpin { display: block; } @@ -535,6 +542,9 @@ .sidebarFilterIcon .tooltipsUnder { margin-left: -4px; } +.notificationsIcon .tooltipsUnder { + left: -20px; +} .sidebarForkIcon .tooltipsUnder { margin-left: -34px; @@ -612,7 +622,12 @@ border-bottom: 5px solid transparent; } -.importDialog div:after, .sidebarFilterIcon div:after, .sidebarForkIcon div:after, .addMap div:after, .sidebarAccountIcon .tooltipsUnder:after { +.addMap div:after, +.importDialog div:after, +.sidebarForkIcon div:after, +.sidebarFilterIcon div:after, +.notificationsIcon div:after, +.sidebarAccountIcon .tooltipsUnder:after { content: ''; position: absolute; right: 40%; @@ -623,9 +638,15 @@ border-left: 5px solid transparent; border-right: 5px solid transparent; } +.notificationsIcon .unread-notifications-dot:after { + content: none; +} .sidebarFilterIcon div:after { right: 37% !important; } +.notificationsIcon div:after { + right: 46% !important; +} .mapInfoIcon div:after, .openCheatsheet div:after, .starMap div:after, .openMetacodeSwitcher div:after, .pinCarousel div:after { content: ''; @@ -758,7 +779,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 +802,10 @@ background-image: url(<%= asset_path 'exploremaps_sprite.png' %>); background-position: -96px 0; } +.exploreMapsCenter .notificationsLink .exploreMapsIcon { + background-image: url(<%= asset_path 'topright_sprite.png' %>); + background-position: -128px 0; +} .authedApps:hover .exploreMapsIcon, .authedApps.active .exploreMapsIcon { background-position-x: -32px; } @@ -799,6 +824,9 @@ .sharedMaps:hover .exploreMapsIcon, .sharedMaps.active .exploreMapsIcon { background-position: -128px -32px; } +.notificationsLink:hover .exploreMapsIcon, .notificationsLink.active .exploreMapsIcon { + background-position-y: -32px; +} .mapsWrapper { /*overflow-y: auto; */ diff --git a/app/assets/stylesheets/mobile.scss.erb b/app/assets/stylesheets/mobile.scss.erb index 01fa5f61..fc34168d 100644 --- a/app/assets/stylesheets/mobile.scss.erb +++ b/app/assets/stylesheets/mobile.scss.erb @@ -2,12 +2,19 @@ display: none; } -@media only screen and (max-width : 720px) and (min-width : 504px) { +@media only screen and (max-width : 752px) and (min-width : 504px) { .sidebarSearch .tt-hint, .sidebarSearch .sidebarSearchField { width: 160px !important; } } +/* when this switches to two lines */ +@media only screen and (max-width : 728px) { + .controller-notifications .notificationsPage .notification .notification-read-unread a { + margin-top: -20px !important; + } +} + @media only screen and (max-width : 390px) { .map .mapCard .mobileMetadata { width: 190px; @@ -18,6 +25,14 @@ width: 390px; } } +/* 800 is the max-width for centerContent */ +@media only screen and (max-width : 800px) { + .centerContent.withPadding { + margin-top: 0; + margin-bottom: 0; + } +} + /* Smartphones (portrait and landscape) ----------- the minimum space that two map cards can fit side by side */ @media only screen and (max-width : 504px) { @@ -25,6 +40,17 @@ display: none !important; } + .notificationsPage .page-header { + display: none; + } + + .controller-notifications .notificationsPage .notification .notification-read-unread { + display: block !important; + } + .controller-notifications .notificationsPage .notification .notification-date { + display: none; + } + #mobile_header { display: block; } @@ -57,7 +83,7 @@ } #yield { - height: 100%; + padding-top: 50px; } .new_session, .new_user, .edit_user, .login, .forgotPassword { @@ -66,7 +92,7 @@ left: auto; width: 78%; padding: 16px 10%; - margin: 50px auto 0 auto; + margin: 0 auto; } .centerGreyForm input[type="text"], .centerGreyForm input[type="email"], .centerGreyForm input[type="password"] { @@ -213,8 +239,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 +257,21 @@ 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: 50%; + left: 0px; + margin-top: -4px; + } + } + } } /* diff --git a/app/assets/stylesheets/notifications.scss.erb b/app/assets/stylesheets/notifications.scss.erb new file mode 100644 index 00000000..16f96407 --- /dev/null +++ b/app/assets/stylesheets/notifications.scss.erb @@ -0,0 +1,138 @@ +$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; + } + + .notificationPage, + .notificationsPage { + font-family: 'din-regular', Sans-Serif; + + & a:hover { + text-decoration: none; + } + + & > .notification-title { + border-bottom: 1px solid #eee; + padding-bottom: 0.25em; + margin-bottom: 0.5em; + } + + .back { + margin-top: 1em; + } + } + + .notificationsPage { + header { + margin-bottom: 0; + } + + .emptyInbox { + padding-top: 15px; + } + + .notification { + padding: 10px; + position: relative; + + &:hover { + background: #F6F6F6; + + .notification-read-unread { + display:block; + } + + .notification-date { + display: none; + } + } + + & > a { + float: left; + width: 85%; + box-sizing: border-box; + padding-right: 10px; + } + + .notification-actor { + float: left; + + img { + width: 32px; + height: 32px; + border-radius: 16px; + } + } + + .notification-body { + margin-left: 50px; + + .in-bold { + font-family: 'din-medium', Sans-Serif; + } + + .action { + background: #4fb5c0; + color: #FFF; + padding: 2px 6px; + border-radius: 3px; + display: inline-block; + margin: 5px 0; + } + } + + .notification-date { + position: absolute; + top: 50%; + right: 10px; + color: #607d8b; + font-size: 13px; + line-height: 13px; + margin-top: -6px; + } + + .notification-read-unread { + display: none; + float: left; + width: 15%; + + a { + position: absolute; + top: 50%; + margin-top: -10px; + text-align: center; + } + } + + &.unread { + background: #EEE; + } + } + + } + + + + .notificationPage .notification-body { + p, div { + margin: 1em auto; + } + } +} diff --git a/app/controllers/access_controller.rb b/app/controllers/access_controller.rb index c48ac418..2441aa62 100644 --- a/app/controllers/access_controller.rb +++ b/app/controllers/access_controller.rb @@ -6,7 +6,6 @@ class AccessController < ApplicationController :deny_access, :deny_access_post, :request_access] after_action :verify_authorized - # GET maps/:id/request_access def request_access @map = nil @@ -20,13 +19,10 @@ class AccessController < ApplicationController # POST maps/:id/access_request 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 + NotificationService.access_request(request) respond_to do |format| - format.json do - head :ok - end + format.json { head :ok } end end @@ -36,22 +32,21 @@ 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 + # who we then send a notification to + user = User.find(user_id) + NotificationService.invite_to_edit(@map, current_user, user) end @map.remove_old_collaborators(user_ids) respond_to do |format| - format.json do - head :ok - end + format.json { head :ok } end end # GET maps/:id/approve_access/:request_id def approve_access request = AccessRequest.find(params[:request_id]) - request.approve() + request.approve # also marks mailboxer notification as read respond_to do |format| format.html { redirect_to map_path(@map), notice: 'Request was approved' } end @@ -60,7 +55,7 @@ class AccessController < ApplicationController # GET maps/:id/deny_access/:request_id def deny_access request = AccessRequest.find(params[:request_id]) - request.deny() + request.deny # also marks mailboxer notification as read respond_to do |format| format.html { redirect_to map_path(@map), notice: 'Request was turned down' } end @@ -69,7 +64,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 +75,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 +89,4 @@ class AccessController < ApplicationController @map = Map.find(params[:id]) authorize @map end - end diff --git a/app/controllers/api/v2/mappings_controller.rb b/app/controllers/api/v2/mappings_controller.rb index 4490e4af..186d6891 100644 --- a/app/controllers/api/v2/mappings_controller.rb +++ b/app/controllers/api/v2/mappings_controller.rb @@ -5,6 +5,27 @@ module Api def searchable_columns [] end + + def create + instantiate_resource + resource.user = current_user if current_user.present? + resource.updated_by = current_user if current_user.present? + authorize resource + create_action + respond_with_resource + end + + def update + resource.updated_by = current_user if current_user.present? + update_action + respond_with_resource + end + + def destroy + resource.updated_by = current_user if current_user.present? + destroy_action + head :no_content + end end end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 4285682e..4bb5be10 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -22,7 +22,7 @@ class ApplicationController < ActionController::Base helper_method :admin? def handle_unauthorized - if authenticated? and params[:controller] == 'maps' and params[:action] == 'show' + if authenticated? && (params[:controller] == 'maps') && (params[:action] == 'show') redirect_to request_access_map_path(params[:id]) elsif authenticated? redirect_to root_path, notice: "You don't have permission to see that page." @@ -41,13 +41,13 @@ class ApplicationController < ActionController::Base def require_no_user return true unless authenticated? redirect_to edit_user_path(user), notice: 'You must be logged out.' - return false + false end def require_user return true if authenticated? redirect_to sign_in_path, notice: 'You must be logged in.' - return false + false end def require_admin diff --git a/app/controllers/mappings_controller.rb b/app/controllers/mappings_controller.rb index de2c8ea1..86db023e 100644 --- a/app/controllers/mappings_controller.rb +++ b/app/controllers/mappings_controller.rb @@ -19,10 +19,10 @@ class MappingsController < ApplicationController @mapping = Mapping.new(mapping_params) authorize @mapping @mapping.user = current_user + @mapping.updated_by = current_user if @mapping.save render json: @mapping, status: :created - Events::NewMapping.publish!(@mapping, current_user) else render json: @mapping.errors, status: :unprocessable_entity end @@ -32,8 +32,10 @@ class MappingsController < ApplicationController def update @mapping = Mapping.find(params[:id]) authorize @mapping + @mapping.updated_by = current_user + @mapping.assign_attributes(mapping_params) - if @mapping.update_attributes(mapping_params) + if @mapping.save head :no_content else render json: @mapping.errors, status: :unprocessable_entity @@ -44,14 +46,7 @@ class MappingsController < ApplicationController def destroy @mapping = Mapping.find(params[:id]) authorize @mapping - - mappable = @mapping.mappable - if mappable.defer_to_map - mappable.permission = mappable.defer_to_map.permission - mappable.defer_to_map_id = nil - mappable.save - end - + @mapping.updated_by = current_user @mapping.destroy head :no_content diff --git a/app/controllers/maps_controller.rb b/app/controllers/maps_controller.rb index d66456b8..6e1e0d77 100644 --- a/app/controllers/maps_controller.rb +++ b/app/controllers/maps_controller.rb @@ -8,6 +8,7 @@ class MapsController < ApplicationController def show respond_to do |format| format.html do + UserMap.where(map: @map, user: current_user).map(&:mark_invite_notifications_as_read) @allmappers = @map.contributors @allcollaborators = @map.editors @alltopics = policy_scope(@map.topics) diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb new file mode 100644 index 00000000..16049fc0 --- /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.page(params[:page]).per(25) + + 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/search_controller.rb b/app/controllers/search_controller.rb index c9fcc7db..c488c556 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -105,6 +105,7 @@ class SearchController < ApplicationController builder = builder.where(user: user) if user @maps = builder.order(:name) else + skip_policy_scope @maps = [] end @@ -120,10 +121,10 @@ class SearchController < ApplicationController term = term[7..-1] if term.downcase[0..6] == 'mapper:' search = term.downcase.strip + '%' - skip_policy_scope # TODO: builder = policy_scope(User) - builder = User.where('LOWER("name") like ?', search) + builder = policy_scope(User).where('LOWER("name") like ?', search) @mappers = builder.order(:name) else + skip_policy_scope @mappers = [] end render json: autocomplete_user_array_json(@mappers).to_json @@ -146,6 +147,7 @@ class SearchController < ApplicationController @synapses = @one + @two @synapses.sort! { |s1, s2| s1.desc <=> s2.desc }.to_a else + skip_policy_scope @synapses = [] end diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb index ea56059b..b54cc4f5 100644 --- a/app/controllers/topics_controller.rb +++ b/app/controllers/topics_controller.rb @@ -14,14 +14,14 @@ class TopicsController < ApplicationController @topics = policy_scope(Topic).where('LOWER("name") like ?', term.downcase + '%').order('"name"') @mapTopics = @topics.select { |t| t&.metacode&.name == 'Metamap' } # prioritize topics which point to maps, over maps - @exclude = @mapTopics.length > 0 ? @mapTopics.map(&:name) : [''] + @exclude = @mapTopics.length.positive? ? @mapTopics.map(&:name) : [''] @maps = policy_scope(Map).where('LOWER("name") like ? AND name NOT IN (?)', term.downcase + '%', @exclude).order('"name"') else @topics = [] @maps = [] end - @all= @topics.to_a.concat(@maps.to_a).sort { |a, b| a.name <=> b.name } - + @all = @topics.to_a.concat(@maps.to_a).sort_by(&:name) + render json: autocomplete_array_json(@all).to_json end diff --git a/app/controllers/users/registrations_controller.rb b/app/controllers/users/registrations_controller.rb index 7c211f26..44bcb2de 100644 --- a/app/controllers/users/registrations_controller.rb +++ b/app/controllers/users/registrations_controller.rb @@ -21,13 +21,10 @@ class Users::RegistrationsController < Devise::RegistrationsController end end - private def store_location - if params[:redirect_to] - store_location_for(User, params[:redirect_to]) - end + store_location_for(User, params[:redirect_to]) if params[:redirect_to] end def configure_sign_up_params 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..1defb323 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,8 @@ 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..96b5a2b2 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -37,4 +37,11 @@ module ApplicationHelper def invite_link "#{request.base_url}/join" + (current_user ? "?code=#{current_user.code}" : '') end + + def user_unread_notification_count + return 0 if current_user.nil? + @uunc ||= current_user.mailboxer_notification_receipts.reduce(0) do |total, receipt| + receipt.is_read ? total : total + 1 + end + end end diff --git a/app/helpers/topics_helper.rb b/app/helpers/topics_helper.rb index 58f53a6e..fa26095d 100644 --- a/app/helpers/topics_helper.rb +++ b/app/helpers/topics_helper.rb @@ -20,7 +20,7 @@ module TopicsHelper type: is_map ? metamapMetacode.name : t.metacode.name, typeImageURL: is_map ? metamapMetacode.icon : t.metacode.icon, mapCount: is_map ? 0 : t.maps.count, - synapseCount: is_map ? 0 : t.synapses.count, + synapseCount: is_map ? 0 : t.synapses.count } end end diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb index 59a2175a..ebffb2df 100644 --- a/app/mailers/application_mailer.rb +++ b/app/mailers/application_mailer.rb @@ -2,4 +2,23 @@ class ApplicationMailer < ActionMailer::Base default from: 'team@metamaps.cc' layout 'mailer' + + def deliver + raise NotImplementedError('Please use Mailboxer to send your emails.') + end + + class << self + def mail_for_notification(notification) + if notification.notification_code == MAILBOXER_CODE_ACCESS_REQUEST + request = notification.notified_object + MapMailer.access_request_email(request) + elsif notification.notification_code == MAILBOXER_CODE_ACCESS_APPROVED + request = notification.notified_object + MapMailer.access_approved_email(request) + elsif notification.notification_code == MAILBOXER_CODE_INVITE_TO_EDIT + user_map = notification.notified_object + MapMailer.invite_to_edit_email(user_map.map, user_map.map.user, user_map.user) + end + end + end end diff --git a/app/mailers/map_mailer.rb b/app/mailers/map_mailer.rb index f6865ecd..bf0cec7b 100644 --- a/app/mailers/map_mailer.rb +++ b/app/mailers/map_mailer.rb @@ -2,17 +2,21 @@ class MapMailer < ApplicationMailer default from: 'team@metamaps.cc' - def access_request_email(request, map) + def access_request_email(request) @request = request - @map = map - subject = @map.name + ' - request to edit' - mail(to: @map.user.email, subject: subject) + @map = request.map + mail(to: @map.user.email, subject: request.requested_text) + end + + def access_approved_email(request) + @request = request + @map = request.map + mail(to: request.user, subject: request.approved_text) end def invite_to_edit_email(map, inviter, invitee) @inviter = inviter @map = map - subject = @map.name + ' - invitation to edit' - mail(to: invitee.email, subject: subject) + mail(to: invitee.email, subject: map.invited_text) end end diff --git a/app/models/access_request.rb b/app/models/access_request.rb index 185a04f0..fe68ce8f 100644 --- a/app/models/access_request.rb +++ b/app/models/access_request.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class AccessRequest < ApplicationRecord belongs_to :user belongs_to :map @@ -5,14 +6,31 @@ class AccessRequest < ApplicationRecord def approve self.approved = true self.answered = true - self.save - UserMap.create(user: self.user, map: self.map) - MapMailer.invite_to_edit_email(self.map, self.map.user, self.user).deliver_later + save + + Mailboxer::Notification.where(notified_object: self).find_each do |notification| + Mailboxer::Receipt.where(notification: notification).update_all(is_read: true) + end + + user_map = UserMap.create(user: user, map: map) + NotificationService.access_approved(self) end def deny self.approved = false self.answered = true - self.save + save + + Mailboxer::Notification.where(notified_object: self).find_each do |notification| + Mailboxer::Receipt.where(notification: notification).update_all(is_read: true) + end + end + + def requested_text + map.name + ' - request to edit' + end + + def approved_text + map.name + ' - access approved' end end diff --git a/app/models/event.rb b/app/models/event.rb index 02c6d698..cf974664 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -1,8 +1,10 @@ # frozen_string_literal: true class Event < ApplicationRecord - KINDS = %w(user_present_on_map conversation_started_on_map topic_added_to_map synapse_added_to_map).freeze + KINDS = %w(user_present_on_map conversation_started_on_map + topic_added_to_map topic_moved_on_map topic_removed_from_map + synapse_added_to_map synapse_removed_from_map + topic_updated synapse_updated).freeze - # has_many :notifications, dependent: :destroy belongs_to :eventable, polymorphic: true belongs_to :map belongs_to :user @@ -14,18 +16,12 @@ class Event < ApplicationRecord validates :kind, inclusion: { in: KINDS } validates :eventable, presence: true - # def notify!(user) - # notifications.create!(user: user) - # end - def belongs_to?(this_user) user_id == this_user.id end def notify_webhooks! - # group = self.discussion.group map.webhooks.each { |webhook| WebhookService.publish! webhook: webhook, event: self } - # group.webhooks.each { |webhook| WebhookService.publish! webhook: webhook, event: self } end handle_asynchronously :notify_webhooks! end diff --git a/app/models/events/new_mapping.rb b/app/models/events/new_mapping.rb deleted file mode 100644 index 889c69bc..00000000 --- a/app/models/events/new_mapping.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true -class Events::NewMapping < Event - # after_create :notify_users! - - def self.publish!(mapping, user) - create!(kind: mapping.mappable_type == 'Topic' ? 'topic_added_to_map' : 'synapse_added_to_map', - eventable: mapping, - map: mapping.map, - user: user) - end -end diff --git a/app/models/events/synapse_added_to_map.rb b/app/models/events/synapse_added_to_map.rb new file mode 100644 index 00000000..5afa885d --- /dev/null +++ b/app/models/events/synapse_added_to_map.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true +class Events::SynapseAddedToMap < Event + # after_create :notify_users! + + def self.publish!(synapse, map, user, meta) + create!(kind: 'synapse_added_to_map', + eventable: synapse, + map: map, + user: user, + meta: meta) + end +end diff --git a/app/models/events/synapse_removed_from_map.rb b/app/models/events/synapse_removed_from_map.rb new file mode 100644 index 00000000..b64035dd --- /dev/null +++ b/app/models/events/synapse_removed_from_map.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true +class Events::SynapseRemovedFromMap < Event + # after_create :notify_users! + + def self.publish!(synapse, map, user, meta) + create!(kind: 'synapse_removed_from_map', + eventable: synapse, + map: map, + user: user, + meta: meta) + end +end diff --git a/app/models/events/synapse_updated.rb b/app/models/events/synapse_updated.rb new file mode 100644 index 00000000..0d85cbe8 --- /dev/null +++ b/app/models/events/synapse_updated.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true +class Events::SynapseUpdated < Event + # after_create :notify_users! + + def self.publish!(synapse, user, meta) + create!(kind: 'synapse_updated', + eventable: synapse, + user: user, + meta: meta) + end +end diff --git a/app/models/events/topic_added_to_map.rb b/app/models/events/topic_added_to_map.rb new file mode 100644 index 00000000..a3fa62cf --- /dev/null +++ b/app/models/events/topic_added_to_map.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true +class Events::TopicAddedToMap < Event + # after_create :notify_users! + + def self.publish!(topic, map, user, meta) + create!(kind: 'topic_added_to_map', + eventable: topic, + map: map, + user: user, + meta: meta) + end +end diff --git a/app/models/events/topic_moved_on_map.rb b/app/models/events/topic_moved_on_map.rb new file mode 100644 index 00000000..08d01277 --- /dev/null +++ b/app/models/events/topic_moved_on_map.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true +class Events::TopicMovedOnMap < Event + # after_create :notify_users! + + def self.publish!(topic, map, user, meta) + create!(kind: 'topic_moved_on_map', + eventable: topic, + map: map, + user: user, + meta: meta) + end +end diff --git a/app/models/events/topic_removed_from_map.rb b/app/models/events/topic_removed_from_map.rb new file mode 100644 index 00000000..2f03ec26 --- /dev/null +++ b/app/models/events/topic_removed_from_map.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true +class Events::TopicRemovedFromMap < Event + # after_create :notify_users! + + def self.publish!(topic, map, user, meta) + create!(kind: 'topic_removed_from_map', + eventable: topic, + map: map, + user: user, + meta: meta) + end +end diff --git a/app/models/events/topic_updated.rb b/app/models/events/topic_updated.rb new file mode 100644 index 00000000..fd41a4d6 --- /dev/null +++ b/app/models/events/topic_updated.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true +class Events::TopicUpdated < Event + # after_create :notify_users! + + def self.publish!(topic, user, meta) + create!(kind: 'topic_updated', + eventable: topic, + user: user, + meta: meta) + end +end diff --git a/app/models/map.rb b/app/models/map.rb index 86a89a24..899992c6 100644 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -18,11 +18,11 @@ class Map < ApplicationRecord # This method associates the attribute ":image" with a file attachment has_attached_file :screenshot, - styles: { - thumb: ['220x220#', :png] - #:full => ['940x630#', :png] - }, - default_url: 'https://s3.amazonaws.com/metamaps-assets/site/missing-map-square.png' + styles: { + thumb: ['220x220#', :png] + #:full => ['940x630#', :png] + }, + default_url: 'https://s3.amazonaws.com/metamaps-assets/site/missing-map-square.png' validates :name, presence: true validates :arranged, inclusion: { in: [true, false] } @@ -32,14 +32,19 @@ class Map < ApplicationRecord # Validate the attached image is image/jpg, image/png, etc validates_attachment_content_type :screenshot, content_type: /\Aimage\/.*\Z/ + after_save :update_deferring_topics_and_synapses, if: :permission_changed? + + delegate :count, to: :topics, prefix: :topic # same as `def topic_count; topics.count; end` + delegate :count, to: :synapses, prefix: :synapse + delegate :count, to: :contributors, prefix: :contributor + delegate :count, to: :stars, prefix: :star + + delegate :name, to: :user, prefix: true + def mappings topicmappings.or(synapsemappings) end - def mk_permission - Perm.short(permission) - end - def contributors User.where(id: mappings.map(&:user_id).uniq) end @@ -48,28 +53,10 @@ class Map < ApplicationRecord User.where(id: user_id).or(User.where(id: collaborators)) end - def topic_count - topics.length - end - - def synapse_count - synapses.length - end - - delegate :name, to: :user, prefix: true - def user_image user.image.url(:thirtytwo) end - def contributor_count - contributors.length - end - - def star_count - stars.length - end - def collaborator_ids collaborators.map(&:id) end @@ -131,4 +118,13 @@ class Map < ApplicationRecord end removed.compact end + + def update_deferring_topics_and_synapses + Topic.where(defer_to_map_id: id).update_all(permission: permission) + Synapse.where(defer_to_map_id: id).update_all(permission: permission) + end + + def invited_text + name + ' - invited to edit' + end end diff --git a/app/models/mapping.rb b/app/models/mapping.rb index f7219008..99d23db0 100644 --- a/app/models/mapping.rb +++ b/app/models/mapping.rb @@ -6,6 +6,7 @@ class Mapping < ApplicationRecord belongs_to :mappable, polymorphic: true belongs_to :map, class_name: 'Map', foreign_key: 'map_id', touch: true belongs_to :user + belongs_to :updated_by, class_name: 'User' validates :xloc, presence: true, unless: proc { |m| m.mappable_type == 'Synapse' } @@ -16,6 +17,10 @@ class Mapping < ApplicationRecord delegate :name, to: :user, prefix: true + after_create :after_created + after_update :after_updated + before_destroy :before_destroyed + def user_image user.image.url end @@ -23,4 +28,35 @@ class Mapping < ApplicationRecord def as_json(_options = {}) super(methods: [:user_name, :user_image]) end + + def after_created + if mappable_type == 'Topic' + meta = {'x': xloc, 'y': yloc, 'mapping_id': id} + Events::TopicAddedToMap.publish!(mappable, map, user, meta) + elsif mappable_type == 'Synapse' + Events::SynapseAddedToMap.publish!(mappable, map, user, meta) + end + end + + def after_updated + if mappable_type == 'Topic' and (xloc_changed? or yloc_changed?) + meta = {'x': xloc, 'y': yloc, 'mapping_id': id} + Events::TopicMovedOnMap.publish!(mappable, map, updated_by, meta) + end + end + + def before_destroyed + if mappable.defer_to_map + mappable.permission = mappable.defer_to_map.permission + mappable.defer_to_map_id = nil + mappable.save + end + + meta = {'mapping_id': id} + if mappable_type == 'Topic' + Events::TopicRemovedFromMap.publish!(mappable, map, updated_by, meta) + elsif mappable_type == 'Synapse' + Events::SynapseRemovedFromMap.publish!(mappable, map, updated_by, meta) + end + end end diff --git a/app/models/synapse.rb b/app/models/synapse.rb index 37c9c72d..d14a18f4 100644 --- a/app/models/synapse.rb +++ b/app/models/synapse.rb @@ -22,6 +22,8 @@ class Synapse < ApplicationRecord where(topic1_id: topic_id).or(where(topic2_id: topic_id)) } + after_update :after_updated + delegate :name, to: :user, prefix: true def user_image @@ -36,11 +38,18 @@ class Synapse < ApplicationRecord end end - def calculated_permission - defer_to_map&.permission || permission + def as_json(_options = {}) + super(methods: [:user_name, :user_image, :collaborator_ids]) end - def as_json(_options = {}) - super(methods: [:user_name, :user_image, :calculated_permission, :collaborator_ids]) + def after_updated + attrs = ['desc', 'category', 'permission', 'defer_to_map_id'] + if attrs.any? {|k| changed_attributes.key?(k)} + new = self.attributes.select {|k| attrs.include?(k) } + old = changed_attributes.select {|k| attrs.include?(k) } + meta = new.merge(old) # we are prioritizing the old values, keeping them + meta['changed'] = changed_attributes.keys.select {|k| attrs.include?(k) } + Events::SynapseUpdated.publish!(self, user, meta) + end end end diff --git a/app/models/topic.rb b/app/models/topic.rb index 85f670c3..e5ea90ee 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -16,6 +16,7 @@ class Topic < ApplicationRecord belongs_to :metacode before_create :create_metamap? + after_update :after_updated validates :permission, presence: true validates :permission, inclusion: { in: Perm::ISSIONS.map(&:to_s) } @@ -75,12 +76,8 @@ class Topic < ApplicationRecord Pundit.policy_scope(user, maps).map(&:id) end - def calculated_permission - defer_to_map&.permission || permission - end - def as_json(options = {}) - super(methods: [:user_name, :user_image, :calculated_permission, :collaborator_ids]) + super(methods: [:user_name, :user_image, :collaborator_ids]) .merge(inmaps: inmaps(options[:user]), inmapsLinks: inmapsLinks(options[:user]), map_count: map_count(options[:user]), synapse_count: synapse_count(options[:user])) end @@ -129,15 +126,25 @@ class Topic < ApplicationRecord "Get: #{name}" end - def mk_permission - Perm.short(permission) + protected + + def create_metamap? + return unless (link == '') && (metacode.name == 'Metamap') + + @map = Map.create(name: name, permission: permission, desc: '', + arranged: true, user_id: user_id) + self.link = Rails.application.routes.url_helpers + .map_url(host: ENV['MAILER_DEFAULT_URL'], id: @map.id) end - protected - def create_metamap? - if link == '' and metacode.name == 'Metamap' - @map = Map.create({ name: name, permission: permission, desc: '', arranged: true, user_id: user_id }) - self.link = Rails.application.routes.url_helpers.map_url(:host => ENV['MAILER_DEFAULT_URL'], :id => @map.id) - end + def after_updated + attrs = ['name', 'desc', 'link', 'metacode_id', 'permission', 'defer_to_map_id'] + if attrs.any? {|k| changed_attributes.key?(k)} + new = self.attributes.select {|k| attrs.include?(k) } + old = changed_attributes.select {|k| attrs.include?(k) } + meta = new.merge(old) # we are prioritizing the old values, keeping them + meta['changed'] = changed_attributes.keys.select {|k| attrs.include?(k) } + Events::TopicUpdated.publish!(self, user, meta) end + 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/models/user_map.rb b/app/models/user_map.rb index dc268047..c1cdc58e 100644 --- a/app/models/user_map.rb +++ b/app/models/user_map.rb @@ -2,4 +2,10 @@ class UserMap < ApplicationRecord belongs_to :map belongs_to :user + + def mark_invite_notifications_as_read + Mailboxer::Notification.where(notified_object: self).find_each do |notification| + Mailboxer::Receipt.where(notification: notification).update_all(is_read: true) + end + end end diff --git a/app/models/webhooks/slack/base.rb b/app/models/webhooks/slack/base.rb index 2274e32c..27f95861 100644 --- a/app/models/webhooks/slack/base.rb +++ b/app/models/webhooks/slack/base.rb @@ -14,49 +14,16 @@ Webhooks::Slack::Base = Struct.new(:webhook, :event) do 'something' end - def channel - webhook.channel - end - - def attachments - [{ - title: attachment_title, - text: attachment_text, - fields: attachment_fields, - fallback: attachment_fallback - }] - end + delegate :channel, to: :webhook alias_method :read_attribute_for_serialization, :send private - # def motion_vote_field - # { - # title: "Vote on this proposal", - # value: "#{proposal_link(eventable, "yes")} · " + - # "#{proposal_link(eventable, "abstain")} · " + - # "#{proposal_link(eventable, "no")} · " + - # "#{proposal_link(eventable, "block")}" - # } - # end - def view_map_on_metamaps(text = nil) "<#{map_url(event.map)}|#{text || event.map.name}>" end - # def view_discussion_on_loomio(params = {}) - # { value: discussion_link(I18n.t(:"webhooks.slack.view_it_on_loomio"), params) } - # end - - # def proposal_link(proposal, position = nil) - # discussion_link position || proposal.name, { proposal: proposal.key, position: position } - # end - - # def discussion_link(text = nil, params = {}) - # "<#{discussion_url(eventable.map, params)}|#{text || eventable.discussion.title}>" - # end - def eventable @eventable ||= event.eventable end @@ -65,12 +32,3 @@ Webhooks::Slack::Base = Struct.new(:webhook, :event) do @author ||= eventable.author end end - -# webhooks: -# slack: -# motion_closed: "*%{name}* has closed" -# motion_closing_soon: "*%{name}* has a proposal closing in 24 hours" -# motion_outcome_created: "*%{author}* published an outcome in *%{name}*" -# motion_outcome_updated: "*%{author}* updated the outcome for *%{name}*" -# new_motion: "*%{author}* started a new proposal in *%{name}*" -# view_it_on_loomio: "View it on Loomio" diff --git a/app/models/webhooks/slack/conversation_started_on_map.rb b/app/models/webhooks/slack/conversation_started_on_map.rb index daf2270e..6b6595ce 100644 --- a/app/models/webhooks/slack/conversation_started_on_map.rb +++ b/app/models/webhooks/slack/conversation_started_on_map.rb @@ -3,24 +3,4 @@ class Webhooks::Slack::ConversationStartedOnMap < Webhooks::Slack::Base def text "There is a live conversation starting on map *#{event.map.name}*. #{view_map_on_metamaps('Join in!')}" end - # TODO: it would be sweet if it sends it with the metacode as the icon_url - - def attachment_fallback - '' # {}"*#{eventable.name}*\n#{eventable.description}\n" - end - - def attachment_title - '' # proposal_link(eventable) - end - - def attachment_text - '' # "#{eventable.description}\n" - end - - def attachment_fields - [{ - title: 'nothing', - value: 'nothing' - }] # [motion_vote_field] - end end diff --git a/app/models/webhooks/slack/synapse_added_to_map.rb b/app/models/webhooks/slack/synapse_added_to_map.rb index 5157afa7..3d944878 100644 --- a/app/models/webhooks/slack/synapse_added_to_map.rb +++ b/app/models/webhooks/slack/synapse_added_to_map.rb @@ -1,25 +1,7 @@ # frozen_string_literal: true class Webhooks::Slack::SynapseAddedToMap < Webhooks::Slack::Base def text - "\"*#{eventable.mappable.topic1.name}* #{eventable.mappable.desc || '->'} *#{eventable.mappable.topic2.name}*\" was added as a connection to the map *#{view_map_on_metamaps}*" - end - - def attachment_fallback - '' # {}"*#{eventable.name}*\n#{eventable.description}\n" - end - - def attachment_title - '' # proposal_link(eventable) - end - - def attachment_text - '' # "#{eventable.description}\n" - end - - def attachment_fields - [{ - title: 'nothing', - value: 'nothing' - }] # [motion_vote_field] + connector = eventable.desc.empty? ? '->' : eventable.desc + "\"*#{eventable.topic1.name}* #{connector} *#{eventable.topic2.name}*\" was added as a connection by *#{event.user.name}* to the map *#{view_map_on_metamaps}*" end end diff --git a/app/models/webhooks/slack/synapse_removed_from_map.rb b/app/models/webhooks/slack/synapse_removed_from_map.rb new file mode 100644 index 00000000..06d31206 --- /dev/null +++ b/app/models/webhooks/slack/synapse_removed_from_map.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true +class Webhooks::Slack::SynapseRemovedFromMap < Webhooks::Slack::Base + def text + connector = eventable.desc.empty? ? '->' : eventable.desc + # todo express correct directionality of arrows when desc is empty + "\"*#{eventable.topic1.name}* #{connector} *#{eventable.topic2.name}*\" was removed by *#{event.user.name}* as a connection from the map *#{view_map_on_metamaps}*" + end +end diff --git a/app/models/webhooks/slack/topic_added_to_map.rb b/app/models/webhooks/slack/topic_added_to_map.rb index d3a19760..4f726069 100644 --- a/app/models/webhooks/slack/topic_added_to_map.rb +++ b/app/models/webhooks/slack/topic_added_to_map.rb @@ -1,26 +1,7 @@ # frozen_string_literal: true class Webhooks::Slack::TopicAddedToMap < Webhooks::Slack::Base def text - "New #{eventable.mappable.metacode.name} topic *#{eventable.mappable.name}* was added to the map *#{view_map_on_metamaps}*" + "*#{eventable.name}* was added by *#{event.user.name}* to the map *#{view_map_on_metamaps}*" end # TODO: it would be sweet if it sends it with the metacode as the icon_url - - def attachment_fallback - '' # {}"*#{eventable.name}*\n#{eventable.description}\n" - end - - def attachment_title - '' # proposal_link(eventable) - end - - def attachment_text - '' # "#{eventable.description}\n" - end - - def attachment_fields - [{ - title: 'nothing', - value: 'nothing' - }] # [motion_vote_field] - end end diff --git a/app/models/webhooks/slack/topic_moved_on_map.rb b/app/models/webhooks/slack/topic_moved_on_map.rb new file mode 100644 index 00000000..dfe088ed --- /dev/null +++ b/app/models/webhooks/slack/topic_moved_on_map.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true +class Webhooks::Slack::TopicMovedOnMap < Webhooks::Slack::Base + def text + "*#{eventable.name}* was moved by *#{event.user.name}* on the map *#{view_map_on_metamaps}*" + end +end diff --git a/app/models/webhooks/slack/topic_removed_from_map.rb b/app/models/webhooks/slack/topic_removed_from_map.rb new file mode 100644 index 00000000..05a79c3b --- /dev/null +++ b/app/models/webhooks/slack/topic_removed_from_map.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true +class Webhooks::Slack::TopicRemovedFromMap < Webhooks::Slack::Base + def text + "*#{eventable.name}* was removed by *#{event.user.name}* from the map *#{view_map_on_metamaps}*" + end +end diff --git a/app/models/webhooks/slack/user_present_on_map.rb b/app/models/webhooks/slack/user_present_on_map.rb index c3185e48..4cee2992 100644 --- a/app/models/webhooks/slack/user_present_on_map.rb +++ b/app/models/webhooks/slack/user_present_on_map.rb @@ -3,24 +3,4 @@ class Webhooks::Slack::UserPresentOnMap < Webhooks::Slack::Base def text "Mapper *#{event.user.name}* has joined the map *#{event.map.name}*. #{view_map_on_metamaps('Map with them')}" end - # TODO: it would be sweet if it sends it with the metacode as the icon_url - - def attachment_fallback - '' # {}"*#{eventable.name}*\n#{eventable.description}\n" - end - - def attachment_title - '' # proposal_link(eventable) - end - - def attachment_text - '' # "#{eventable.description}\n" - end - - def attachment_fields - [{ - title: 'nothing', - value: 'nothing' - }] # [motion_vote_field] - end end diff --git a/app/policies/explore_policy.rb b/app/policies/explore_policy.rb index b4d52fe5..ce17d4f4 100644 --- a/app/policies/explore_policy.rb +++ b/app/policies/explore_policy.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class ExplorePolicy < ApplicationPolicy def active? true diff --git a/app/policies/hack_policy.rb b/app/policies/hack_policy.rb index b6fbf6ce..bdc9eaab 100644 --- a/app/policies/hack_policy.rb +++ b/app/policies/hack_policy.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class HackPolicy < ApplicationPolicy def load_url_title? true diff --git a/app/policies/map_policy.rb b/app/policies/map_policy.rb index f670f59e..937d564c 100644 --- a/app/policies/map_policy.rb +++ b/app/policies/map_policy.rb @@ -16,7 +16,7 @@ class MapPolicy < ApplicationPolicy end def show? - record.permission.in?(['commons', 'public']) || + record.permission.in?(%w(commons public)) || record.collaborators.include?(user) || record.user == user end diff --git a/app/policies/message_policy.rb b/app/policies/message_policy.rb index c32e29ed..8a140788 100644 --- a/app/policies/message_policy.rb +++ b/app/policies/message_policy.rb @@ -17,7 +17,8 @@ class MessagePolicy < ApplicationPolicy delegate :show?, to: :resource_policy def create? - record.resource.present? && resource_policy.update? + # we have currently decided to let any map that is visible to someone be commented on by them + record.resource.present? && resource_policy.show? end def update? diff --git a/app/policies/synapse_policy.rb b/app/policies/synapse_policy.rb index 145f7432..e3190c18 100644 --- a/app/policies/synapse_policy.rb +++ b/app/policies/synapse_policy.rb @@ -2,11 +2,10 @@ class SynapsePolicy < ApplicationPolicy class Scope < Scope def resolve - visible = %w(public commons) - return scope.where(permission: visible) unless user + return scope.where(permission: %w(public commons)) unless user - scope.where(permission: visible) - .or(scope.where.not(defer_to_map_id: nil).where(defer_to_map_id: user.all_accessible_maps.map(&:id))) + scope.where(permission: %w(public commons)) + .or(scope.where(defer_to_map_id: user.all_accessible_maps.map(&:id))) .or(scope.where(user_id: user.id)) end end diff --git a/app/policies/topic_policy.rb b/app/policies/topic_policy.rb index b29d9b44..bc80f657 100644 --- a/app/policies/topic_policy.rb +++ b/app/policies/topic_policy.rb @@ -2,11 +2,10 @@ class TopicPolicy < ApplicationPolicy class Scope < Scope def resolve - visible = %w(public commons) - return scope.where(permission: visible) unless user + return scope.where(permission: %w(public commons)) unless user - scope.where(permission: visible) - .or(scope.where.not(defer_to_map_id: nil).where(defer_to_map_id: user.all_accessible_maps.map(&:id))) + scope.where(permission: %w(public commons)) + .or(scope.where(defer_to_map_id: user.all_accessible_maps.map(&:id))) .or(scope.where(user_id: user.id)) end end @@ -23,7 +22,7 @@ class TopicPolicy < ApplicationPolicy if record.defer_to_map.present? map_policy.show? else - record.permission.in?(['commons', 'public']) || record.user == user + record.permission.in?(%w(commons public)) || record.user == user end end diff --git a/app/serializers/api/v2/mapping_serializer.rb b/app/serializers/api/v2/mapping_serializer.rb index 19e7318e..30c9bd7f 100644 --- a/app/serializers/api/v2/mapping_serializer.rb +++ b/app/serializers/api/v2/mapping_serializer.rb @@ -14,6 +14,7 @@ module Api def self.embeddable { user: {}, + updated_by: {}, map: {} } end diff --git a/app/serializers/webhook_serializer.rb b/app/serializers/webhook_serializer.rb index a2acf869..c1f0e266 100644 --- a/app/serializers/webhook_serializer.rb +++ b/app/serializers/webhook_serializer.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true class WebhookSerializer < ActiveModel::Serializer - attributes :text, :username, :icon_url # , :attachments + attributes :text, :username, :icon_url attribute :channel, if: :has_channel? def has_channel? diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb new file mode 100644 index 00000000..aa919edb --- /dev/null +++ b/app/services/notification_service.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true +class NotificationService + def self.renderer + renderer ||= ApplicationController.renderer.new( + http_host: ENV['MAILER_DEFAULT_URL'], + https: Rails.env.production? ? true : false + ) + end + + def self.access_request(request) + body = renderer.render(template: 'map_mailer/access_request_email', locals: { map: request.map, request: request }, layout: false) + request.map.user.notify(request.requested_text, body, request, false, MAILBOXER_CODE_ACCESS_REQUEST, true, request.user) + end + + def self.access_approved(request) + body = renderer.render(template: 'map_mailer/access_approved_email', locals: { map: request.map }, layout: false) + receipt = request.user.notify(request.approved_text, body, request, false, MAILBOXER_CODE_ACCESS_APPROVED, true, request.map.user) + end + + def self.invite_to_edit(map, inviter, invited) + user_map = UserMap.find_by(user: invited, map: map) + body = renderer.render(template: 'map_mailer/invite_to_edit_email', locals: { map: map, inviter: inviter }, layout: false) + invited.notify(map.invited_text, body, user_map, false, MAILBOXER_CODE_INVITE_TO_EDIT, true, inviter) + end + + def self.text_for_notification(notification) + if notification.notification_code == MAILBOXER_CODE_ACCESS_REQUEST + map = notification.notified_object.map + 'wants permission to map with you on ' + map.name + '