Merge branch 'develop'; tag v3.1

This commit is contained in:
Devin Howard 2016-12-16 17:17:24 -05:00
commit b914065bb3
134 changed files with 1563 additions and 496 deletions

View file

@ -8,6 +8,7 @@ engines:
enabled: true enabled: true
config: config:
languages: languages:
count_threshold: 3 # rule of three
ruby: ruby:
mass_threshold: 36 # default: 18 mass_threshold: 36 # default: 18
javascript: javascript:
@ -19,6 +20,8 @@ engines:
enabled: true enabled: true
rubocop: rubocop:
enabled: true enabled: true
exclude_fingerprints:
- 74f18007b920e8d81148d2f6a2756534
ratings: ratings:
paths: paths:
- 'Gemfile.lock' - 'Gemfile.lock'

5
.github/ISSUE_TEMPLATE.md vendored Normal file
View file

@ -0,0 +1,5 @@
============
100BD/C = (100)(__)(__)/(__)=__

View file

@ -22,3 +22,4 @@ script:
addons: addons:
code_climate: code_climate:
repo_token: 479d3bf56798fbc7fff3fc8151a5ed09e8ac368fd5af332c437b9e07dbebb44e repo_token: 479d3bf56798fbc7fff3fc8151a5ed09e8ac368fd5af332c437b9e07dbebb44e
postgresql: "9.4"

View file

@ -17,6 +17,7 @@ gem 'exception_notification'
gem 'httparty' gem 'httparty'
gem 'json' gem 'json'
gem 'kaminari' gem 'kaminari'
gem 'mailboxer'
gem 'paperclip' gem 'paperclip'
gem 'pg' gem 'pg'
gem 'pundit' gem 'pundit'

View file

@ -65,6 +65,12 @@ GEM
brakeman (3.4.0) brakeman (3.4.0)
builder (3.2.2) builder (3.2.2)
byebug (9.0.5) 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) climate_control (0.0.3)
activesupport (>= 3.0) activesupport (>= 3.0)
cocaine (0.5.8) cocaine (0.5.8)
@ -125,6 +131,9 @@ GEM
nokogiri (>= 1.5.9) nokogiri (>= 1.5.9)
mail (2.6.4) mail (2.6.4)
mime-types (>= 1.16, < 4) mime-types (>= 1.16, < 4)
mailboxer (0.14.0)
carrierwave (>= 0.5.8)
rails (>= 4.2.0)
method_source (0.8.2) method_source (0.8.2)
mime-types (3.1) mime-types (3.1)
mime-types-data (~> 3.2015) mime-types-data (~> 3.2015)
@ -284,6 +293,7 @@ DEPENDENCIES
json json
json-schema json-schema
kaminari kaminari
mailboxer
paperclip paperclip
pg pg
pry-byebug pry-byebug

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

BIN
app/assets/images/user_sprite.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View file

@ -826,7 +826,7 @@ label {
position:absolute; position:absolute;
pointer-events:none; pointer-events:none;
background-repeat:no-repeat; background-repeat:no-repeat;
background-image: url(<%= asset_data_uri('user_sprite.png') %>); background-image: url(<%= asset_path('user_sprite.png') %>);
} }
.accountSettings .accountIcon { .accountSettings .accountIcon {
background-position: 0 0; background-position: 0 0;
@ -3076,3 +3076,7 @@ script.data-gratipay-username {
display: inline; display: inline;
float: left; float: left;
} }
.inline {
display: inline-block;
}

View file

@ -1,17 +1,14 @@
.centerContent { .centerContent {
position: relative; position: relative;
margin: 92px auto 0 auto; margin: 0 auto;
padding: 20px 0 60px 20px; width: auto;
width: 760px; max-width: 800px;
overflow: hidden; overflow: hidden;
box-shadow: 0 1px 3px rgba(0,0,0,.12),0 1px 2px rgba(0,0,0,.24); box-shadow: 0 1px 3px rgba(0,0,0,.12),0 1px 2px rgba(0,0,0,.24);
background: #fff; background: #fff;
-webkit-border-radius: 3px; box-sizing: border-box;
-moz-border-radius: 3px;
border-radius: 3px;
border: 1px solid #dcdcdc;
margin-bottom: 10px;
padding: 15px; padding: 15px;
font-family: 'din-regular', sans-serif;
} }
.centerContent .page-header { .centerContent .page-header {
@ -129,3 +126,9 @@
box-sizing: border-box; box-sizing: border-box;
border-radius: 2px; border-radius: 2px;
} }
.centerContent.withPadding {
margin-top: 1em;
margin-bottom: 1em;
}

View file

@ -28,6 +28,8 @@
position: absolute; position: absolute;
width: 100%; width: 100%;
height: 100%; height: 100%;
box-sizing: border-box;
padding-top: 92px;
} }
/*.animations { /*.animations {
@ -210,7 +212,13 @@
} }
.addMap { .addMap {
background-position: -96px 0; 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 { .importDialog:hover {
background-position: 0 -32px; background-position: 0 -32px;
@ -223,7 +231,6 @@
} }
.addMap:hover { .addMap:hover {
background-position: -96px -32px; background-position: -96px -32px;
margin-right:10px;
} }
@ -471,7 +478,7 @@
background-position: -32px 0; 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 { .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; display: block;
} }
@ -535,6 +542,9 @@
.sidebarFilterIcon .tooltipsUnder { .sidebarFilterIcon .tooltipsUnder {
margin-left: -4px; margin-left: -4px;
} }
.notificationsIcon .tooltipsUnder {
left: -20px;
}
.sidebarForkIcon .tooltipsUnder { .sidebarForkIcon .tooltipsUnder {
margin-left: -34px; margin-left: -34px;
@ -612,7 +622,12 @@
border-bottom: 5px solid transparent; 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: ''; content: '';
position: absolute; position: absolute;
right: 40%; right: 40%;
@ -623,9 +638,15 @@
border-left: 5px solid transparent; border-left: 5px solid transparent;
border-right: 5px solid transparent; border-right: 5px solid transparent;
} }
.notificationsIcon .unread-notifications-dot:after {
content: none;
}
.sidebarFilterIcon div:after { .sidebarFilterIcon div:after {
right: 37% !important; right: 37% !important;
} }
.notificationsIcon div:after {
right: 46% !important;
}
.mapInfoIcon div:after, .openCheatsheet div:after, .starMap div:after, .openMetacodeSwitcher div:after, .pinCarousel div:after { .mapInfoIcon div:after, .openCheatsheet div:after, .starMap div:after, .openMetacodeSwitcher div:after, .pinCarousel div:after {
content: ''; content: '';
@ -758,7 +779,7 @@
} }
.exploreMapsCenter .authedApps .exploreMapsIcon { .exploreMapsCenter .authedApps .exploreMapsIcon {
background-image: url(<%= asset_data_uri('user_sprite.png') %>); background-image: url(<%= asset_path('user_sprite.png') %>);
background-position: 0 -32px; background-position: 0 -32px;
} }
.exploreMapsCenter .myMaps .exploreMapsIcon { .exploreMapsCenter .myMaps .exploreMapsIcon {
@ -781,6 +802,10 @@
background-image: url(<%= asset_path 'exploremaps_sprite.png' %>); background-image: url(<%= asset_path 'exploremaps_sprite.png' %>);
background-position: -96px 0; 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 { .authedApps:hover .exploreMapsIcon, .authedApps.active .exploreMapsIcon {
background-position-x: -32px; background-position-x: -32px;
} }
@ -799,6 +824,9 @@
.sharedMaps:hover .exploreMapsIcon, .sharedMaps.active .exploreMapsIcon { .sharedMaps:hover .exploreMapsIcon, .sharedMaps.active .exploreMapsIcon {
background-position: -128px -32px; background-position: -128px -32px;
} }
.notificationsLink:hover .exploreMapsIcon, .notificationsLink.active .exploreMapsIcon {
background-position-y: -32px;
}
.mapsWrapper { .mapsWrapper {
/*overflow-y: auto; */ /*overflow-y: auto; */

View file

@ -2,12 +2,19 @@
display: none; 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 { .sidebarSearch .tt-hint, .sidebarSearch .sidebarSearchField {
width: 160px !important; 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) { @media only screen and (max-width : 390px) {
.map .mapCard .mobileMetadata { .map .mapCard .mobileMetadata {
width: 190px; width: 190px;
@ -18,6 +25,14 @@
width: 390px; 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 */ /* Smartphones (portrait and landscape) ----------- the minimum space that two map cards can fit side by side */
@media only screen and (max-width : 504px) { @media only screen and (max-width : 504px) {
@ -25,6 +40,17 @@
display: none !important; 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 { #mobile_header {
display: block; display: block;
} }
@ -57,7 +83,7 @@
} }
#yield { #yield {
height: 100%; padding-top: 50px;
} }
.new_session, .new_user, .edit_user, .login, .forgotPassword { .new_session, .new_user, .edit_user, .login, .forgotPassword {
@ -66,7 +92,7 @@
left: auto; left: auto;
width: 78%; width: 78%;
padding: 16px 10%; padding: 16px 10%;
margin: 50px auto 0 auto; margin: 0 auto;
} }
.centerGreyForm input[type="text"], .centerGreyForm input[type="email"], .centerGreyForm input[type="password"] { .centerGreyForm input[type="text"], .centerGreyForm input[type="email"], .centerGreyForm input[type="password"] {
@ -213,6 +239,15 @@
line-height: 50px; 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 { #mobile_menu {
display: none; display: none;
background: #EEE; background: #EEE;
@ -222,11 +257,21 @@
padding: 10px; padding: 10px;
width: 200px; width: 200px;
box-shadow: 3px 3px 3px rgba(0,0,0,0.23), 3px 3px 3px rgba(0,0,0,0.16); box-shadow: 3px 3px 3px rgba(0,0,0,0.23), 3px 3px 3px rgba(0,0,0,0.16);
}
#mobile_menu li { li {
padding: 10px; padding: 10px;
list-style: none; list-style: none;
&.notifications {
position: relative;
.unread-notifications-dot {
top: 50%;
left: 0px;
margin-top: -4px;
}
}
}
} }
/* /*

View file

@ -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;
}
}
}

View file

@ -6,7 +6,6 @@ class AccessController < ApplicationController
:deny_access, :deny_access_post, :request_access] :deny_access, :deny_access_post, :request_access]
after_action :verify_authorized after_action :verify_authorized
# GET maps/:id/request_access # GET maps/:id/request_access
def request_access def request_access
@map = nil @map = nil
@ -20,13 +19,10 @@ class AccessController < ApplicationController
# POST maps/:id/access_request # POST maps/:id/access_request
def access_request def access_request
request = AccessRequest.create(user: current_user, map: @map) request = AccessRequest.create(user: current_user, map: @map)
# what about push notification to map owner? NotificationService.access_request(request)
MapMailer.access_request_email(request, @map).deliver_later
respond_to do |format| respond_to do |format|
format.json do format.json { head :ok }
head :ok
end
end end
end end
@ -36,22 +32,21 @@ class AccessController < ApplicationController
@map.add_new_collaborators(user_ids).each do |user_id| @map.add_new_collaborators(user_ids).each do |user_id|
# add_new_collaborators returns array of added users, # add_new_collaborators returns array of added users,
# who we then send an email to # who we then send a notification to
MapMailer.invite_to_edit_email(@map, current_user, User.find(user_id)).deliver_later user = User.find(user_id)
NotificationService.invite_to_edit(@map, current_user, user)
end end
@map.remove_old_collaborators(user_ids) @map.remove_old_collaborators(user_ids)
respond_to do |format| respond_to do |format|
format.json do format.json { head :ok }
head :ok
end
end end
end end
# GET maps/:id/approve_access/:request_id # GET maps/:id/approve_access/:request_id
def approve_access def approve_access
request = AccessRequest.find(params[:request_id]) request = AccessRequest.find(params[:request_id])
request.approve() request.approve # also marks mailboxer notification as read
respond_to do |format| respond_to do |format|
format.html { redirect_to map_path(@map), notice: 'Request was approved' } format.html { redirect_to map_path(@map), notice: 'Request was approved' }
end end
@ -60,7 +55,7 @@ class AccessController < ApplicationController
# GET maps/:id/deny_access/:request_id # GET maps/:id/deny_access/:request_id
def deny_access def deny_access
request = AccessRequest.find(params[:request_id]) request = AccessRequest.find(params[:request_id])
request.deny() request.deny # also marks mailboxer notification as read
respond_to do |format| respond_to do |format|
format.html { redirect_to map_path(@map), notice: 'Request was turned down' } format.html { redirect_to map_path(@map), notice: 'Request was turned down' }
end end
@ -69,7 +64,7 @@ class AccessController < ApplicationController
# POST maps/:id/approve_access/:request_id # POST maps/:id/approve_access/:request_id
def approve_access_post def approve_access_post
request = AccessRequest.find(params[:request_id]) request = AccessRequest.find(params[:request_id])
request.approve() request.approve
respond_to do |format| respond_to do |format|
format.json do format.json do
head :ok head :ok
@ -80,7 +75,7 @@ class AccessController < ApplicationController
# POST maps/:id/deny_access/:request_id # POST maps/:id/deny_access/:request_id
def deny_access_post def deny_access_post
request = AccessRequest.find(params[:request_id]) request = AccessRequest.find(params[:request_id])
request.deny() request.deny
respond_to do |format| respond_to do |format|
format.json do format.json do
head :ok head :ok
@ -94,5 +89,4 @@ class AccessController < ApplicationController
@map = Map.find(params[:id]) @map = Map.find(params[:id])
authorize @map authorize @map
end end
end end

View file

@ -5,6 +5,27 @@ module Api
def searchable_columns def searchable_columns
[] []
end 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 end
end end

View file

@ -22,7 +22,7 @@ class ApplicationController < ActionController::Base
helper_method :admin? helper_method :admin?
def handle_unauthorized 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]) redirect_to request_access_map_path(params[:id])
elsif authenticated? elsif authenticated?
redirect_to root_path, notice: "You don't have permission to see that page." 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 def require_no_user
return true unless authenticated? return true unless authenticated?
redirect_to edit_user_path(user), notice: 'You must be logged out.' redirect_to edit_user_path(user), notice: 'You must be logged out.'
return false false
end end
def require_user def require_user
return true if authenticated? return true if authenticated?
redirect_to sign_in_path, notice: 'You must be logged in.' redirect_to sign_in_path, notice: 'You must be logged in.'
return false false
end end
def require_admin def require_admin

View file

@ -19,10 +19,10 @@ class MappingsController < ApplicationController
@mapping = Mapping.new(mapping_params) @mapping = Mapping.new(mapping_params)
authorize @mapping authorize @mapping
@mapping.user = current_user @mapping.user = current_user
@mapping.updated_by = current_user
if @mapping.save if @mapping.save
render json: @mapping, status: :created render json: @mapping, status: :created
Events::NewMapping.publish!(@mapping, current_user)
else else
render json: @mapping.errors, status: :unprocessable_entity render json: @mapping.errors, status: :unprocessable_entity
end end
@ -32,8 +32,10 @@ class MappingsController < ApplicationController
def update def update
@mapping = Mapping.find(params[:id]) @mapping = Mapping.find(params[:id])
authorize @mapping authorize @mapping
@mapping.updated_by = current_user
@mapping.assign_attributes(mapping_params)
if @mapping.update_attributes(mapping_params) if @mapping.save
head :no_content head :no_content
else else
render json: @mapping.errors, status: :unprocessable_entity render json: @mapping.errors, status: :unprocessable_entity
@ -44,14 +46,7 @@ class MappingsController < ApplicationController
def destroy def destroy
@mapping = Mapping.find(params[:id]) @mapping = Mapping.find(params[:id])
authorize @mapping authorize @mapping
@mapping.updated_by = current_user
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.destroy @mapping.destroy
head :no_content head :no_content

View file

@ -8,6 +8,7 @@ class MapsController < ApplicationController
def show def show
respond_to do |format| respond_to do |format|
format.html do format.html do
UserMap.where(map: @map, user: current_user).map(&:mark_invite_notifications_as_read)
@allmappers = @map.contributors @allmappers = @map.contributors
@allcollaborators = @map.editors @allcollaborators = @map.editors
@alltopics = policy_scope(@map.topics) @alltopics = policy_scope(@map.topics)

View file

@ -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

View file

@ -105,6 +105,7 @@ class SearchController < ApplicationController
builder = builder.where(user: user) if user builder = builder.where(user: user) if user
@maps = builder.order(:name) @maps = builder.order(:name)
else else
skip_policy_scope
@maps = [] @maps = []
end end
@ -120,10 +121,10 @@ class SearchController < ApplicationController
term = term[7..-1] if term.downcase[0..6] == 'mapper:' term = term[7..-1] if term.downcase[0..6] == 'mapper:'
search = term.downcase.strip + '%' search = term.downcase.strip + '%'
skip_policy_scope # TODO: builder = policy_scope(User) builder = policy_scope(User).where('LOWER("name") like ?', search)
builder = User.where('LOWER("name") like ?', search)
@mappers = builder.order(:name) @mappers = builder.order(:name)
else else
skip_policy_scope
@mappers = [] @mappers = []
end end
render json: autocomplete_user_array_json(@mappers).to_json render json: autocomplete_user_array_json(@mappers).to_json
@ -146,6 +147,7 @@ class SearchController < ApplicationController
@synapses = @one + @two @synapses = @one + @two
@synapses.sort! { |s1, s2| s1.desc <=> s2.desc }.to_a @synapses.sort! { |s1, s2| s1.desc <=> s2.desc }.to_a
else else
skip_policy_scope
@synapses = [] @synapses = []
end end

View file

@ -14,13 +14,13 @@ class TopicsController < ApplicationController
@topics = policy_scope(Topic).where('LOWER("name") like ?', term.downcase + '%').order('"name"') @topics = policy_scope(Topic).where('LOWER("name") like ?', term.downcase + '%').order('"name"')
@mapTopics = @topics.select { |t| t&.metacode&.name == 'Metamap' } @mapTopics = @topics.select { |t| t&.metacode&.name == 'Metamap' }
# prioritize topics which point to maps, over maps # 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"') @maps = policy_scope(Map).where('LOWER("name") like ? AND name NOT IN (?)', term.downcase + '%', @exclude).order('"name"')
else else
@topics = [] @topics = []
@maps = [] @maps = []
end 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 render json: autocomplete_array_json(@all).to_json
end end

View file

@ -21,13 +21,10 @@ class Users::RegistrationsController < Devise::RegistrationsController
end end
end end
private private
def store_location def store_location
if params[:redirect_to] store_location_for(User, params[:redirect_to]) if params[:redirect_to]
store_location_for(User, params[:redirect_to])
end
end end
def configure_sign_up_params def configure_sign_up_params

View file

@ -1,4 +1,8 @@
class Users::SessionsController < Devise::SessionsController # frozen_string_literal: true
module Users
class SessionsController < Devise::SessionsController
after_action :store_location, only: [:new]
protected protected
def after_sign_in_path_for(resource) def after_sign_in_path_for(resource)
@ -11,4 +15,11 @@ class Users::SessionsController < Devise::SessionsController
request.referer || root_path request.referer || root_path
end end
end end
private
def store_location
store_location_for(User, params[:redirect_to]) if params[:redirect_to]
end
end
end end

View file

@ -13,13 +13,12 @@ class UsersController < ApplicationController
# GET /users/:id/edit # GET /users/:id/edit
def edit def edit
@user = current_user @user = User.find(current_user.id)
respond_with(@user)
end end
# PUT /users/:id # PUT /users/:id
def update def update
@user = current_user @user = User.find(current_user.id)
if user_params[:password] == '' && user_params[:password_confirmation] == '' if user_params[:password] == '' && user_params[:password_confirmation] == ''
# not trying to change the password # not trying to change the password
@ -96,6 +95,8 @@ class UsersController < ApplicationController
private private
def user_params 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
end end

View file

@ -37,4 +37,11 @@ module ApplicationHelper
def invite_link def invite_link
"#{request.base_url}/join" + (current_user ? "?code=#{current_user.code}" : '') "#{request.base_url}/join" + (current_user ? "?code=#{current_user.code}" : '')
end 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 end

View file

@ -20,7 +20,7 @@ module TopicsHelper
type: is_map ? metamapMetacode.name : t.metacode.name, type: is_map ? metamapMetacode.name : t.metacode.name,
typeImageURL: is_map ? metamapMetacode.icon : t.metacode.icon, typeImageURL: is_map ? metamapMetacode.icon : t.metacode.icon,
mapCount: is_map ? 0 : t.maps.count, mapCount: is_map ? 0 : t.maps.count,
synapseCount: is_map ? 0 : t.synapses.count, synapseCount: is_map ? 0 : t.synapses.count
} }
end end
end end

View file

@ -2,4 +2,23 @@
class ApplicationMailer < ActionMailer::Base class ApplicationMailer < ActionMailer::Base
default from: 'team@metamaps.cc' default from: 'team@metamaps.cc'
layout 'mailer' 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 end

View file

@ -2,17 +2,21 @@
class MapMailer < ApplicationMailer class MapMailer < ApplicationMailer
default from: 'team@metamaps.cc' default from: 'team@metamaps.cc'
def access_request_email(request, map) def access_request_email(request)
@request = request @request = request
@map = map @map = request.map
subject = @map.name + ' - request to edit' mail(to: @map.user.email, subject: request.requested_text)
mail(to: @map.user.email, subject: subject) end
def access_approved_email(request)
@request = request
@map = request.map
mail(to: request.user, subject: request.approved_text)
end end
def invite_to_edit_email(map, inviter, invitee) def invite_to_edit_email(map, inviter, invitee)
@inviter = inviter @inviter = inviter
@map = map @map = map
subject = @map.name + ' - invitation to edit' mail(to: invitee.email, subject: map.invited_text)
mail(to: invitee.email, subject: subject)
end end
end end

View file

@ -1,3 +1,4 @@
# frozen_string_literal: true
class AccessRequest < ApplicationRecord class AccessRequest < ApplicationRecord
belongs_to :user belongs_to :user
belongs_to :map belongs_to :map
@ -5,14 +6,31 @@ class AccessRequest < ApplicationRecord
def approve def approve
self.approved = true self.approved = true
self.answered = true self.answered = true
self.save save
UserMap.create(user: self.user, map: self.map)
MapMailer.invite_to_edit_email(self.map, self.map.user, self.user).deliver_later 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 end
def deny def deny
self.approved = false self.approved = false
self.answered = true 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
end end

View file

@ -1,8 +1,10 @@
# frozen_string_literal: true # frozen_string_literal: true
class Event < ApplicationRecord 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 :eventable, polymorphic: true
belongs_to :map belongs_to :map
belongs_to :user belongs_to :user
@ -14,18 +16,12 @@ class Event < ApplicationRecord
validates :kind, inclusion: { in: KINDS } validates :kind, inclusion: { in: KINDS }
validates :eventable, presence: true validates :eventable, presence: true
# def notify!(user)
# notifications.create!(user: user)
# end
def belongs_to?(this_user) def belongs_to?(this_user)
user_id == this_user.id user_id == this_user.id
end end
def notify_webhooks! def notify_webhooks!
# group = self.discussion.group
map.webhooks.each { |webhook| WebhookService.publish! webhook: webhook, event: self } map.webhooks.each { |webhook| WebhookService.publish! webhook: webhook, event: self }
# group.webhooks.each { |webhook| WebhookService.publish! webhook: webhook, event: self }
end end
handle_asynchronously :notify_webhooks! handle_asynchronously :notify_webhooks!
end end

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -32,14 +32,19 @@ class Map < ApplicationRecord
# Validate the attached image is image/jpg, image/png, etc # Validate the attached image is image/jpg, image/png, etc
validates_attachment_content_type :screenshot, content_type: /\Aimage\/.*\Z/ 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 def mappings
topicmappings.or(synapsemappings) topicmappings.or(synapsemappings)
end end
def mk_permission
Perm.short(permission)
end
def contributors def contributors
User.where(id: mappings.map(&:user_id).uniq) User.where(id: mappings.map(&:user_id).uniq)
end end
@ -48,28 +53,10 @@ class Map < ApplicationRecord
User.where(id: user_id).or(User.where(id: collaborators)) User.where(id: user_id).or(User.where(id: collaborators))
end end
def topic_count
topics.length
end
def synapse_count
synapses.length
end
delegate :name, to: :user, prefix: true
def user_image def user_image
user.image.url(:thirtytwo) user.image.url(:thirtytwo)
end end
def contributor_count
contributors.length
end
def star_count
stars.length
end
def collaborator_ids def collaborator_ids
collaborators.map(&:id) collaborators.map(&:id)
end end
@ -131,4 +118,13 @@ class Map < ApplicationRecord
end end
removed.compact removed.compact
end 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 end

View file

@ -6,6 +6,7 @@ class Mapping < ApplicationRecord
belongs_to :mappable, polymorphic: true belongs_to :mappable, polymorphic: true
belongs_to :map, class_name: 'Map', foreign_key: 'map_id', touch: true belongs_to :map, class_name: 'Map', foreign_key: 'map_id', touch: true
belongs_to :user belongs_to :user
belongs_to :updated_by, class_name: 'User'
validates :xloc, presence: true, validates :xloc, presence: true,
unless: proc { |m| m.mappable_type == 'Synapse' } unless: proc { |m| m.mappable_type == 'Synapse' }
@ -16,6 +17,10 @@ class Mapping < ApplicationRecord
delegate :name, to: :user, prefix: true delegate :name, to: :user, prefix: true
after_create :after_created
after_update :after_updated
before_destroy :before_destroyed
def user_image def user_image
user.image.url user.image.url
end end
@ -23,4 +28,35 @@ class Mapping < ApplicationRecord
def as_json(_options = {}) def as_json(_options = {})
super(methods: [:user_name, :user_image]) super(methods: [:user_name, :user_image])
end 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 end

View file

@ -22,6 +22,8 @@ class Synapse < ApplicationRecord
where(topic1_id: topic_id).or(where(topic2_id: topic_id)) where(topic1_id: topic_id).or(where(topic2_id: topic_id))
} }
after_update :after_updated
delegate :name, to: :user, prefix: true delegate :name, to: :user, prefix: true
def user_image def user_image
@ -36,11 +38,18 @@ class Synapse < ApplicationRecord
end end
end end
def calculated_permission def as_json(_options = {})
defer_to_map&.permission || permission super(methods: [:user_name, :user_image, :collaborator_ids])
end end
def as_json(_options = {}) def after_updated
super(methods: [:user_name, :user_image, :calculated_permission, :collaborator_ids]) 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
end end

View file

@ -16,6 +16,7 @@ class Topic < ApplicationRecord
belongs_to :metacode belongs_to :metacode
before_create :create_metamap? before_create :create_metamap?
after_update :after_updated
validates :permission, presence: true validates :permission, presence: true
validates :permission, inclusion: { in: Perm::ISSIONS.map(&:to_s) } validates :permission, inclusion: { in: Perm::ISSIONS.map(&:to_s) }
@ -75,12 +76,8 @@ class Topic < ApplicationRecord
Pundit.policy_scope(user, maps).map(&:id) Pundit.policy_scope(user, maps).map(&:id)
end end
def calculated_permission
defer_to_map&.permission || permission
end
def as_json(options = {}) 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]), .merge(inmaps: inmaps(options[:user]), inmapsLinks: inmapsLinks(options[:user]),
map_count: map_count(options[:user]), synapse_count: synapse_count(options[:user])) map_count: map_count(options[:user]), synapse_count: synapse_count(options[:user]))
end end
@ -129,15 +126,25 @@ class Topic < ApplicationRecord
"Get: #{name}" "Get: #{name}"
end end
def mk_permission protected
Perm.short(permission)
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 end
protected def after_updated
def create_metamap? attrs = ['name', 'desc', 'link', 'metacode_id', 'permission', 'defer_to_map_id']
if link == '' and metacode.name == 'Metamap' if attrs.any? {|k| changed_attributes.key?(k)}
@map = Map.create({ name: name, permission: permission, desc: '', arranged: true, user_id: user_id }) new = self.attributes.select {|k| attrs.include?(k) }
self.link = Rails.application.routes.url_helpers.map_url(:host => ENV['MAILER_DEFAULT_URL'], :id => @map.id) 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 end
end end

View file

@ -2,6 +2,8 @@
require 'open-uri' require 'open-uri'
class User < ApplicationRecord class User < ApplicationRecord
acts_as_messageable # mailboxer notifications
has_many :topics has_many :topics
has_many :synapses has_many :synapses
has_many :maps has_many :maps
@ -108,4 +110,19 @@ class User < ApplicationRecord
def settings=(val) def settings=(val)
self[:settings] = val self[:settings] = val
end 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 end

View file

@ -2,4 +2,10 @@
class UserMap < ApplicationRecord class UserMap < ApplicationRecord
belongs_to :map belongs_to :map
belongs_to :user 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 end

View file

@ -14,49 +14,16 @@ Webhooks::Slack::Base = Struct.new(:webhook, :event) do
'something' 'something'
end end
def channel delegate :channel, to: :webhook
webhook.channel
end
def attachments
[{
title: attachment_title,
text: attachment_text,
fields: attachment_fields,
fallback: attachment_fallback
}]
end
alias_method :read_attribute_for_serialization, :send alias_method :read_attribute_for_serialization, :send
private 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) def view_map_on_metamaps(text = nil)
"<#{map_url(event.map)}|#{text || event.map.name}>" "<#{map_url(event.map)}|#{text || event.map.name}>"
end 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 def eventable
@eventable ||= event.eventable @eventable ||= event.eventable
end end
@ -65,12 +32,3 @@ Webhooks::Slack::Base = Struct.new(:webhook, :event) do
@author ||= eventable.author @author ||= eventable.author
end end
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"

View file

@ -3,24 +3,4 @@ class Webhooks::Slack::ConversationStartedOnMap < Webhooks::Slack::Base
def text def text
"There is a live conversation starting on map *#{event.map.name}*. #{view_map_on_metamaps('Join in!')}" "There is a live conversation starting on map *#{event.map.name}*. #{view_map_on_metamaps('Join in!')}"
end 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 end

View file

@ -1,25 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class Webhooks::Slack::SynapseAddedToMap < Webhooks::Slack::Base class Webhooks::Slack::SynapseAddedToMap < Webhooks::Slack::Base
def text 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}*" connector = eventable.desc.empty? ? '->' : eventable.desc
end "\"*#{eventable.topic1.name}* #{connector} *#{eventable.topic2.name}*\" was added as a connection by *#{event.user.name}* to the map *#{view_map_on_metamaps}*"
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
end end

View file

@ -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

View file

@ -1,26 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class Webhooks::Slack::TopicAddedToMap < Webhooks::Slack::Base class Webhooks::Slack::TopicAddedToMap < Webhooks::Slack::Base
def text 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 end
# TODO: it would be sweet if it sends it with the metacode as the icon_url # 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 end

View file

@ -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

View file

@ -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

View file

@ -3,24 +3,4 @@ class Webhooks::Slack::UserPresentOnMap < Webhooks::Slack::Base
def text def text
"Mapper *#{event.user.name}* has joined the map *#{event.map.name}*. #{view_map_on_metamaps('Map with them')}" "Mapper *#{event.user.name}* has joined the map *#{event.map.name}*. #{view_map_on_metamaps('Map with them')}"
end 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 end

View file

@ -1,3 +1,4 @@
# frozen_string_literal: true
class ExplorePolicy < ApplicationPolicy class ExplorePolicy < ApplicationPolicy
def active? def active?
true true

View file

@ -1,3 +1,4 @@
# frozen_string_literal: true
class HackPolicy < ApplicationPolicy class HackPolicy < ApplicationPolicy
def load_url_title? def load_url_title?
true true

View file

@ -16,7 +16,7 @@ class MapPolicy < ApplicationPolicy
end end
def show? def show?
record.permission.in?(['commons', 'public']) || record.permission.in?(%w(commons public)) ||
record.collaborators.include?(user) || record.collaborators.include?(user) ||
record.user == user record.user == user
end end

View file

@ -17,7 +17,8 @@ class MessagePolicy < ApplicationPolicy
delegate :show?, to: :resource_policy delegate :show?, to: :resource_policy
def create? 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 end
def update? def update?

View file

@ -2,11 +2,10 @@
class SynapsePolicy < ApplicationPolicy class SynapsePolicy < ApplicationPolicy
class Scope < Scope class Scope < Scope
def resolve def resolve
visible = %w(public commons) return scope.where(permission: %w(public commons)) unless user
return scope.where(permission: visible) unless user
scope.where(permission: visible) scope.where(permission: %w(public commons))
.or(scope.where.not(defer_to_map_id: nil).where(defer_to_map_id: user.all_accessible_maps.map(&:id))) .or(scope.where(defer_to_map_id: user.all_accessible_maps.map(&:id)))
.or(scope.where(user_id: user.id)) .or(scope.where(user_id: user.id))
end end
end end

View file

@ -2,11 +2,10 @@
class TopicPolicy < ApplicationPolicy class TopicPolicy < ApplicationPolicy
class Scope < Scope class Scope < Scope
def resolve def resolve
visible = %w(public commons) return scope.where(permission: %w(public commons)) unless user
return scope.where(permission: visible) unless user
scope.where(permission: visible) scope.where(permission: %w(public commons))
.or(scope.where.not(defer_to_map_id: nil).where(defer_to_map_id: user.all_accessible_maps.map(&:id))) .or(scope.where(defer_to_map_id: user.all_accessible_maps.map(&:id)))
.or(scope.where(user_id: user.id)) .or(scope.where(user_id: user.id))
end end
end end
@ -23,7 +22,7 @@ class TopicPolicy < ApplicationPolicy
if record.defer_to_map.present? if record.defer_to_map.present?
map_policy.show? map_policy.show?
else else
record.permission.in?(['commons', 'public']) || record.user == user record.permission.in?(%w(commons public)) || record.user == user
end end
end end

View file

@ -14,6 +14,7 @@ module Api
def self.embeddable def self.embeddable
{ {
user: {}, user: {},
updated_by: {},
map: {} map: {}
} }
end end

View file

@ -1,6 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
class WebhookSerializer < ActiveModel::Serializer class WebhookSerializer < ActiveModel::Serializer
attributes :text, :username, :icon_url # , :attachments attributes :text, :username, :icon_url
attribute :channel, if: :has_channel? attribute :channel, if: :has_channel?
def has_channel? def has_channel?

View file

@ -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 <span class="in-bold">' + map.name + '</span>&nbsp;&nbsp;<div class="action">Offer a response</div>'
elsif notification.notification_code == MAILBOXER_CODE_ACCESS_APPROVED
map = notification.notified_object.map
'granted your request to edit map <span class="in-bold">' + map.name + '</span>'
elsif notification.notification_code == MAILBOXER_CODE_INVITE_TO_EDIT
map = notification.notified_object.map
'gave you edit access to map <span class="in-bold">' + map.name + '</span>'
end
end
end

View file

@ -18,14 +18,14 @@
<%= link_to "Admin", metacodes_path %> <%= link_to "Admin", metacodes_path %>
</li> </li>
<% end %> <% end %>
<li class="accountListItem accountInvite openLightbox" data-open="invite">
<div class="accountIcon"></div>
<span>Share Invite</span>
</li>
<li class="accountListItem accountApps"> <li class="accountListItem accountApps">
<div class="accountIcon"></div> <div class="accountIcon"></div>
<%= link_to "Apps", oauth_authorized_applications_path %> <%= link_to "Apps", oauth_authorized_applications_path %>
</li> </li>
<li class="accountListItem accountInvite openLightbox" data-open="invite">
<div class="accountIcon"></div>
<span>Share Invite</span>
</li>
<li class="accountListItem accountLogout"> <li class="accountListItem accountLogout">
<div class="accountIcon"></div> <div class="accountIcon"></div>
<%= link_to "Sign Out", "/logout", id: "Logout" %> <%= link_to "Sign Out", "/logout", id: "Logout" %>

View file

@ -2,7 +2,11 @@
<div id="header_content"> <div id="header_content">
<%= yield(:mobile_title) %> <%= yield(:mobile_title) %>
</div> </div>
<div id="menu_icon"></div> <div id="menu_icon">
<% if user_unread_notification_count > 0 %>
<div class="unread-notifications-dot"></div>
<% end %>
</div>
</div> </div>
<div id="mobile_menu"> <div id="mobile_menu">
<ul> <ul>
@ -49,6 +53,12 @@
<li> <li>
<%= link_to "Account", edit_user_url(current_user) %> <%= link_to "Account", edit_user_url(current_user) %>
</li> </li>
<li class="notifications">
<%= link_to "Notifications", notifications_path %>
<% if user_unread_notification_count > 0 %>
<div class="unread-notifications-dot"></div>
<% end %>
</li>
<li> <li>
<%= link_to "Sign Out", "/logout", id: "Logout" %> <%= link_to "Sign Out", "/logout", id: "Logout" %>
</li> </li>

View file

@ -4,7 +4,7 @@
<div class="upperLeftUI"> <div class="upperLeftUI">
<!-- home button --> <!-- home button -->
<div class="homeButton"> <div class="homeButton">
<a href="<%= root_url %>" <% if current_user && !appsPage %><%= 'data-router=true' %><% end %>>METAMAPS</a> <a href="<%= root_url %>" <% if current_user && !noHardHomeLink %><%= 'data-router=true' %><% end %>>METAMAPS</a>
</div> <!-- end homeButton --> </div> <!-- end homeButton -->
<!-- search box --> <!-- search box -->
@ -71,6 +71,22 @@
</a><!-- end addMap --> </a><!-- end addMap -->
<% end %> <% end %>
<script type="text/javascript">
Metamaps.ServerData.unreadNotificationsCount = <%= user_unread_notification_count %>
</script>
<% if current_user.present? %>
<span id="notification_icon">
<%= link_to notifications_path, class: "notificationsIcon upperRightEl upperRightIcon #{user_unread_notification_count > 0 ? 'unread' : 'read'}" do %>
<div class="tooltipsUnder">
Notifications
</div>
<% if user_unread_notification_count > 0 %>
<div class="unread-notifications-dot"></div>
<% end %>
<% end %>
</span>
<% end %>
<!-- Account / Sign in --> <!-- Account / Sign in -->
<% if !(controller_name == "sessions" && action_name == "new") %> <% if !(controller_name == "sessions" && action_name == "new") %>
<div class="sidebarAccount upperRightEl"> <div class="sidebarAccount upperRightEl">

View file

@ -30,7 +30,7 @@
<div class="wrapper <%= classes %>" id="wrapper"> <div class="wrapper <%= classes %>" id="wrapper">
<%= render :partial => 'layouts/upperelements', :locals => { :appsPage => false } %> <%= render :partial => 'layouts/upperelements', :locals => { :noHardHomeLink => controller_name == "notifications" ? true : false } %>
<%= yield %> <%= yield %>
@ -64,9 +64,13 @@
<p id="toast" class="toast"> <p id="toast" class="toast">
<% if devise_error_messages? %> <% if devise_error_messages? %>
<%= devise_error_messages! %> <%= devise_error_messages! %>
<% elsif notice %> <% end %>
<% if notice %>
<%= notice %> <%= notice %>
<% end %> <% end %>
<% if alert %>
<%= alert %>
<% end %>
</p> </p>
<div id="loading"></div> <div id="loading"></div>
</div> </div>

View file

@ -22,7 +22,7 @@
<div class="wrapper <%= classes %>" id="wrapper"> <div class="wrapper <%= classes %>" id="wrapper">
<%= render :partial => 'layouts/upperelements', :locals => {:appsPage => true } %> <%= render :partial => 'layouts/upperelements', :locals => {:noHardHomeLink => true } %>
<%= yield %> <%= yield %>
@ -38,6 +38,9 @@
<a href="<%= oauth_authorized_applications_path %>" class="authedApps exploreMapsButton <%= params[:controller] == 'doorkeeper/authorized_applications' ? 'active' : nil %>"> <a href="<%= oauth_authorized_applications_path %>" class="authedApps exploreMapsButton <%= params[:controller] == 'doorkeeper/authorized_applications' ? 'active' : nil %>">
<div class="exploreMapsIcon"></div>Authorized Apps <div class="exploreMapsIcon"></div>Authorized Apps
</a> </a>
<a href="/" class="myMaps exploreMapsButton">
<div class="exploreMapsIcon"></div>Maps
</a>
</div> </div>
</div> </div>
</div> </div>

View file

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html>
<head>
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
</head>
<body>
<h1>You have a new message: <%= @subject %></h1>
<p>
You have received a new message:
</p>
<blockquote>
<p>
<%= raw @message.body %>
</p>
</blockquote>
<p>
Visit <%= link_to root_url, root_url %> and go to your inbox for more info.
</p>
</body>
</html>

View file

@ -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.

View file

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html>
<head>
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
</head>
<body>
<h1>You have a new reply: <%= @subject %></h1>
<p>
You have received a new reply:
</p>
<blockquote>
<p>
<%= raw @message.body %>
</p>
</blockquote>
<p>
Visit <%= link_to root_url, root_url %> and go to your inbox for more info.
</p>
</body>
</html>

View file

@ -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.

View file

@ -0,0 +1,6 @@
<!DOCTYPE html>
<div style="padding: 16px; background: white; text-align: left;">
<%= raw @notification.body %>
<p style="font-size: 12px;">Make sense with Metamaps</p>
<%= render partial: 'shared/mailer_unsubscribe_link' %>
</div>

View file

@ -0,0 +1,8 @@
<% mail = ApplicationMailer.mail_for_notification(@notification) %>
<% if mail %>
<%= mail.text_part&.body&.decoded %>
<% end %>
Make sense with Metamaps
<%= render partial: 'shared/mailer_unsubscribe_link' %>

View file

@ -0,0 +1,8 @@
<% map = @map || map %>
<% 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" %>
<p><span style="font-weight: bold;"><%= map.user.name %></span> has responded to your access request and invited you to <span style="font-weight: bold">collaboratively edit</span> the following map:</p>
<p><%= link_to map.name, map_url(map), style: "font-size: 18px; text-decoration: none; color: #4fc059;" %></p>
<% if map.desc %>
<p style="font-size: 12px;"><%= map.desc %></p>
<% end %>
<%= link_to 'Go to Map', map_url(map), style: button_style %>

View file

@ -0,0 +1,4 @@
<% map = @map || map %>
<%= map.user.name %> has responded to your access request and invited you to collaboratively edit the following map:
<%= map.name %> [<%= map_url(map) %>]

View file

@ -1,23 +1,8 @@
<!DOCTYPE html> <% map = @map || map %>
<html> <% request = @request || request %>
<head>
<meta content='text/html; charset=UTF-8' http-equiv='Content-Type' />
</head>
<body style="font-family: sans-serif; width: 100%; padding: 24px 16px 16px 16px; background-color: #f5f5f5; text-align: center;">
<div style="padding: 16px; background: white; text-align: left;">
<% 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" %>
<p><span style="font-weight: bold;"><%= request.user.name %></span> is requesting access to <span style="font-weight: bold">collaboratively edit</span> the following map:</p>
<p><span style="font-weight: bold;"><%= @request.user.name %></span> is requesting access to <span style="font-weight: bold">collaboratively edit</span> the following map:</p> <p><%= map.name %></p>
<p><%= link_to "Allow", approve_access_map_url(id: map.id, request_id: request.id), style: "font-size: 18px; text-decoration: none; color: #4fc059;" %>
<p><%= @map.name %></p> <p><%= link_to "Decline", deny_access_map_url(id: map.id, request_id: request.id), style: "font-size: 18px; text-decoration: none; color: #DB5D5D;" %></p>
<%= link_to 'Go to Map', map_url(map), style: button_style %>
<p><%= 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;" %>
<p><%= 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;" %></p>
<%= link_to 'Open in Metamaps', map_url(@map), target: "_blank", style: button_style %>
<p style="font-size: 12px;">Make sense with Metamaps</p>
</div>
</body>
</html>

View file

@ -1,10 +1,10 @@
<%= @request.user.name %> has requested to collaboratively edit the following map: <% map = @map || map %>
<% request = @request || request %>
<%= request.user.name %> has requested to collaboratively edit the following map:
<%= @map.name %> [<%= map_url(@map) %>] <%= map.name %> [<%= map_url(map) %>]
Allow [<%= approve_access_map_url(id: @map.id, request_id: @request.id) %>] Allow [<%= approve_access_map_url(id: map.id, request_id: request.id) %>]
Decline [<%= deny_access_map_url(id: @map.id, request_id: @request.id) %>] Decline [<%= deny_access_map_url(id: map.id, request_id: request.id) %>]
Make sense with Metamaps

View file

@ -1,22 +1,9 @@
<!DOCTYPE html> <% map = @map || map %>
<html> <% inviter = @inviter || inviter %>
<head>
<meta content='text/html; charset=UTF-8' http-equiv='Content-Type' />
</head>
<body style="font-family: sans-serif; width: 100%; padding: 24px 16px 16px 16px; background-color: #f5f5f5; text-align: center;">
<div style="padding: 16px; background: white; text-align: left;">
<% 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" %>
<p><span style="font-weight: bold;"><%= inviter.name %></span> has invited you to <span style="font-weight: bold">collaboratively edit</span> the following map:</p>
<p><span style="font-weight: bold;"><%= @inviter.name %></span> has invited you to <span style="font-weight: bold">collaboratively edit</span> the following map:</p> <p><%= link_to map.name, map_url(map), style: "font-size: 18px; text-decoration: none; color: #4fc059;" %></p>
<p><%= link_to @map.name, map_url(@map), target: "_blank", style: "font-size: 18px; text-decoration: none; color: #4fc059;" %></p> <% if map.desc %>
<% if @map.desc %> <p style="font-size: 12px;"><%= map.desc %></p>
<p style="font-size: 12px;"><%= @map.desc %></p>
<% end %> <% end %>
<%= link_to 'Go to Map', map_url(map), style: button_style %>
<%= link_to 'Open in Metamaps', map_url(@map), target: "_blank", style: button_style %>
<p style="font-size: 12px;">Make sense with Metamaps</p>
</div>
</body>
</html>

View file

@ -1,7 +1,5 @@
<%= @inviter.name %> has invited you to collaboratively edit the following map: <% map = @map || map %>
<% inviter = @inviter || inviter %>
<%= @map.name %> [<%= map_url(@map) %>] <%= inviter.name %> has invited you to collaboratively edit the following map:
Make sense with Metamaps
<%= map.name %> [<%= map_url(map) %>]

View file

@ -0,0 +1,14 @@
<div id="exploreMapsHeader">
<div class="exploreMapsBar exploreElement">
<div class="exploreMapsMenu">
<div class="exploreMapsCenter">
<a href="<%= notifications_path %>" class="notificationsLink exploreMapsButton active">
<div class="exploreMapsIcon"></div>Notifications
</a>
<a href="/" class="exploreMapsButton myMaps">
<div class="exploreMapsIcon"></div>Maps
</a>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,50 @@
<% content_for :title, 'Notifications | Metamaps' %>
<% content_for :mobile_title, 'Notifications' %>
<div id="yield">
<div class="centerContent notificationsPage">
<header class="page-header">
<h2 class="title">Notifications</h4>
</header>
<ul class="notifications">
<% @notifications.each do |notification| %>
<% receipt = @receipts.find_by(notification_id: notification.id) %>
<li class="notification <%= receipt.is_read? ? 'read' : 'unread' %>" id="notification-<%= notification.id %>">
<%= link_to notification_path(notification.id) do %>
<div class="notification-actor">
<%= image_tag notification.sender.image(:thirtytwo) %>
</div>
<div class="notification-body">
<div class="in-bold"><%= notification.sender.name %></div>
<%= raw NotificationService.text_for_notification(notification) %>
</div>
<% end %>
<div class="notification-read-unread">
<% 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 %>
</div>
<div class="notification-date">
<%= notification.created_at.strftime("%b %d") %>
</div>
<div class="clearfloat"></div>
</li>
<% end %>
<% if @notifications.count == 0 %>
<div class="emptyInbox">
You have no notifications. More time for dancing.
</div>
<% end %>
</ul>
</div>
<% if @notifications.total_pages > 1 %>
<div class="centerContent withPadding pagination">
<%= paginate @notifications %>
</div>
<% end %>
</div>
<%= render partial: 'notifications/header' %>

View file

@ -0,0 +1,7 @@
$('#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')
Metamaps.GlobalUI.NotificationIcon.render(Metamaps.GlobalUI.NotificationIcon.unreadNotificationsCount - 1)

View file

@ -0,0 +1,7 @@
$('#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')
Metamaps.GlobalUI.NotificationIcon.render(Metamaps.GlobalUI.NotificationIcon.unreadNotificationsCount + 1)

View file

@ -0,0 +1,16 @@
<% content_for :title, 'Notifications | Metamaps' %>
<% content_for :mobile_title, 'Notifications' %>
<div id="yield">
<div class="centerContent withPadding back">
<%= link_to 'Back to notifications', notifications_path %>
</div>
<div class="centerContent notificationPage">
<h2 class="notification-title"><%= @notification.subject %></h4>
<div class="notification-body">
<%= raw @notification.body %>
</div>
</div>
</div>
<%= render partial: 'notifications/header' %>

View file

@ -0,0 +1,3 @@
<div class="unsubscribe-link">
<%= link_to 'Click here to unsubscribe from all Metamaps emails', unsubscribe_notifications_url(protocol: Rails.env.production? ? :https : :http) %>
</div>

View file

@ -0,0 +1,5 @@
You can unsubscribe from all Metamaps emails by visiting the following link:
<%= unsubscribe_notifications_url(protocol: Rails.env.production? ? :https : :http) %>

View file

@ -33,21 +33,33 @@
<div class="nameEdit"><%= @user.name %></div> <div class="nameEdit"><%= @user.name %></div>
</div> </div>
<div class="changeName"> <div class="changeName">
<%= form.label :name, "Name:", :class => "firstFieldText" %> <%= form.label :name, "Name:", class: 'firstFieldText' %>
<%= form.text_field :name %> <%= form.text_field :name %>
</div> </div>
<div><%= form.label :email, "Email:", :class => "firstFieldText" %> <div>
<%= form.email_field :email %></div> <%= form.label :email, "Email:", class: 'firstFieldText' %>
<%= form.email_field :email %>
</div>
<div>
<%= form.label :emails_allowed, class: 'firstFieldText' do %>
<%= form.check_box :emails_allowed, class: 'inline' %>
Send Metamaps notifications to my email.
<% end %>
</div>
<div class="changePass" onclick="Metamaps.Account.showPass()">Change Password</div> <div class="changePass" onclick="Metamaps.Account.showPass()">Change Password</div>
<div class="toHide"> <div class="toHide">
<div> <div>
<%= form.label :current_password, "Current Password:", :class => "firstFieldText" %> <%= form.label :current_password, "Current Password:", :class => "firstFieldText" %>
<%= password_field_tag :current_password, params[:current_password] %> <%= password_field_tag :current_password, params[:current_password] %>
</div> </div>
<div><%= form.label :password, "New Password:", :class => "firstFieldText" %> <div>
<%= form.password_field :password, :autocomplete => :off%></div> <%= form.label :password, "New Password:", :class => "firstFieldText" %>
<div><%= form.label :password_confirmation, "Confirm New Password:", :class => "firstFieldText" %> <%= form.password_field :password, :autocomplete => :off%>
<%= form.password_field :password_confirmation, :autocomplete => :off%></div> </div>
<div>
<%= form.label :password_confirmation, "Confirm New Password:", :class => "firstFieldText" %>
<%= form.password_field :password_confirmation, :autocomplete => :off%>
</div>
<div class="noChangePass" onclick="Metamaps.Account.hidePass()">Oops, don't change password</div> <div class="noChangePass" onclick="Metamaps.Account.hidePass()">Oops, don't change password</div>
</div> </div>
<div id="accountPageLoading"></div> <div id="accountPageLoading"></div>

View file

@ -8,14 +8,15 @@ Bundler.require(*Rails.groups)
module Metamaps module Metamaps
class Application < Rails::Application 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. # Settings in config/environments/* take precedence over those specified here.
# Application configuration should go into files in config/initializers # Application configuration should go into files in config/initializers
# -- all .rb files in that directory are automatically loaded. # -- 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. # Custom directories with classes and modules you want to be autoloadable.
config.autoload_paths << Rails.root.join('app', 'services') config.autoload_paths << Rails.root.join('app', 'services')

24
config/brakeman.ignore Normal file
View file

@ -0,0 +1,24 @@
{
"ignored_warnings": [
{
"warning_type": "Cross Site Scripting",
"warning_code": 2,
"fingerprint": "88694dca0bcc2226859746f9ed40cc682d6e5eaec1e73f2be557770a854ede0b",
"message": "Unescaped model attribute",
"file": "app/views/notifications/show.html.erb",
"line": 7,
"link": "http://brakemanscanner.org/docs/warning_types/cross_site_scripting",
"code": "current_user.mailbox.notifications.find_by(:id => params[:id]).body",
"render_path": [{"type":"controller","class":"NotificationsController","method":"show","line":24,"file":"app/controllers/notifications_controller.rb"}],
"location": {
"type": "template",
"template": "notifications/show"
},
"user_input": "current_user.mailbox.notifications",
"confidence": "Weak",
"note": ""
}
],
"updated": "2016-11-29 13:01:34 -0500",
"brakeman_version": "3.4.0"
}

View file

@ -14,19 +14,11 @@ Rails.application.configure do
config.consider_all_requests_local = true config.consider_all_requests_local = true
config.action_controller.perform_caching = false config.action_controller.perform_caching = false
config.action_mailer.delivery_method = :smtp config.action_mailer.delivery_method = :file
config.action_mailer.smtp_settings = { config.action_mailer.file_settings = {
address: ENV['SMTP_SERVER'], location: 'tmp/mails'
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.default_url_options = { host: 'localhost:3000' } 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 config.action_mailer.raise_delivery_errors = true
# Print deprecation notices to the Rails logger # Print deprecation notices to the Rails logger

View file

@ -1,16 +1,16 @@
# frozen_string_literal: true # frozen_string_literal: true
Rails.application.configure do Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb # Settings specified here will take precedence over those in config/application.rb
config.log_level = :warn # log to stdout
config.eager_load = true logger = Logger.new(STDOUT)
# 12 factor: log to stdout
logger = ActiveSupport::Logger.new(STDOUT)
logger.formatter = config.log_formatter logger.formatter = config.log_formatter
logger.level = :warn
config.logger = ActiveSupport::TaggedLogging.new(logger) config.logger = ActiveSupport::TaggedLogging.new(logger)
# Code is not reloaded between requests # Code is not reloaded between requests
config.eager_load = true
config.cache_classes = true config.cache_classes = true
# Full error reports are disabled and caching is turned on # Full error reports are disabled and caching is turned on

View file

@ -9,20 +9,20 @@ Doorkeeper.configure do
current_user current_user
else else
store_location_for(User, request.fullpath) store_location_for(User, request.fullpath)
redirect_to(sign_in_url, notice: "Sign In to Connect") redirect_to(sign_in_url, notice: 'Sign In to Connect')
end end
end end
# If you want to restrict access to the web interface for adding oauth authorized applications, # If you want to restrict access to the web interface for adding oauth authorized applications,
# you need to declare the block below. # you need to declare the block below.
admin_authenticator do admin_authenticator do
if current_user && current_user.admin if current_user&.admin
current_user current_user
elsif current_user && !current_user.admin elsif current_user && !current_user.admin
redirect_to(root_url, notice: "Unauthorized") redirect_to(root_url, notice: 'Unauthorized')
else else
store_location_for(User, request.fullpath) store_location_for(User, request.fullpath)
redirect_to(sign_in_url, notice: "Try signing in to do that") redirect_to(sign_in_url, notice: 'Try signing in to do that')
end end
end end

View file

@ -0,0 +1,33 @@
# frozen_string_literal: true
# notification codes to differentiate different types of notifications
# e.g. a notification might have {
# notified_object_type: 'Map',
# notified_object_id: 1,
# notification_code: MAILBOXER_CODE_ACCESS_REQUEST
# },
# which would imply that this is an access request to Map.find(1)
MAILBOXER_CODE_ACCESS_REQUEST = 'ACCESS_REQUEST'
MAILBOXER_CODE_ACCESS_APPROVED = 'ACCESS_APPROVED'
MAILBOXER_CODE_INVITE_TO_EDIT = 'INVITE_TO_EDIT'
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 = 'team@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

View file

@ -1,3 +1,4 @@
# frozen_string_literal: true
class Rack::Attack class Rack::Attack
Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
@ -11,10 +12,8 @@ class Rack::Attack
# Throttle POST requests to /login by IP address # Throttle POST requests to /login by IP address
# #
# Key: "rack::attack:#{Time.now.to_i/:period}:logins/ip:#{req.ip}" # Key: "rack::attack:#{Time.now.to_i/:period}:logins/ip:#{req.ip}"
throttle('logins/ip', :limit => 5, :period => 20.seconds) do |req| throttle('logins/ip', limit: 5, period: 20.seconds) do |req|
if req.path == '/login' && req.post? req.ip if req.path == '/login' && req.post?
req.ip
end
end end
# Throttle POST requests to /login by email param # Throttle POST requests to /login by email param
@ -25,17 +24,17 @@ class Rack::Attack
# throttle logins for another user and force their login requests to be # throttle logins for another user and force their login requests to be
# denied, but that's not very common and shouldn't happen to you. (Knock # denied, but that's not very common and shouldn't happen to you. (Knock
# on wood!) # on wood!)
throttle("logins/email", :limit => 5, :period => 20.seconds) do |req| throttle('logins/email', limit: 5, period: 20.seconds) do |req|
if req.path == '/login' && req.post? if req.path == '/login' && req.post?
# return the email if present, nil otherwise # return the email if present, nil otherwise
req.params['email'].presence req.params['email'].presence
end end
end end
throttle('load_url_title/req/5mins/ip', :limit => 300, :period => 5.minutes) do |req| throttle('load_url_title/req/5mins/ip', limit: 300, period: 5.minutes) do |req|
req.ip if req.path == 'hacks/load_url_title' req.ip if req.path == 'hacks/load_url_title'
end end
throttle('load_url_title/req/1s/ip', :limit => 5, :period => 1.second) do |req| throttle('load_url_title/req/1s/ip', limit: 5, period: 1.second) do |req|
# If the return value is truthy, the cache key for the return value # If the return value is truthy, the cache key for the return value
# is incremented and compared with the limit. In this case: # is incremented and compared with the limit. In this case:
# "rack::attack:#{Time.now.to_i/1.second}:load_url_title/req/ip:#{req.ip}" # "rack::attack:#{Time.now.to_i/1.second}:load_url_title/req/ip:#{req.ip}"

View file

@ -1,8 +1,4 @@
# Sample localization file for English. Add more files in this directory for other locales.
# See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points.
en: en:
activerecord: mailboxer:
attributes: notification_mailer:
user: subject: "%{subject}"
joinedwithcode: "Access code"

View file

@ -20,12 +20,25 @@ Metamaps::Application.routes.draw do
post 'events/:event', action: :events post 'events/:event', action: :events
get :contains get :contains
get :request_access, to: 'access#request_access' get :request_access,
get 'approve_access/:request_id', to: 'access#approve_access', as: :approve_access to: 'access#request_access'
get 'deny_access/:request_id', to: 'access#deny_access', as: :deny_access get 'approve_access/:request_id',
post :access_request, to: 'access#access_request', default: { format: :json } to: 'access#approve_access',
post 'approve_access/:request_id', to: 'access#approve_access_post', default: { format: :json } as: :approve_access
post 'deny_access/:request_id', to: 'access#deny_access_post', default: { format: :json } 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 :access, to: 'access#access', default: { format: :json }
post :star, to: 'stars#create', 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 :mappings, except: [:index, :new, :edit]
resources :messages, only: [:show, :create, :update, :destroy] 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] resources :metacode_sets, except: [:show]
@ -109,3 +131,4 @@ Metamaps::Application.routes.draw do
get 'load_url_title' get 'load_url_title'
end end
end end
# rubocop:enable Rubocop/Metrics/BlockLength

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,5 @@
class AddEmailsAllowedToUsers < ActiveRecord::Migration[5.0]
def change
add_column :users, :emails_allowed, :boolean, default: true
end
end

Some files were not shown because too many files have changed in this diff Show more