yeehaw add collaborators

This commit is contained in:
Connor Turland 2016-04-21 18:54:26 -04:00
parent fa51ee850e
commit 6c7ce56e4f
18 changed files with 277 additions and 16 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 418 B

View file

@ -434,6 +434,8 @@ Metamaps.Map.InfoBox = {
self.attachEventListeners() self.attachEventListeners()
self.generateBoxHTML = Hogan.compile($('#mapInfoBoxTemplate').html()) self.generateBoxHTML = Hogan.compile($('#mapInfoBoxTemplate').html())
}, },
toggleBox: function (event) { toggleBox: function (event) {
@ -474,7 +476,7 @@ Metamaps.Map.InfoBox = {
var map = Metamaps.Active.Map var map = Metamaps.Active.Map
var obj = map.pick('permission', 'contributor_count', 'topic_count', 'synapse_count') var obj = map.pick('permission', 'topic_count', 'synapse_count')
var isCreator = map.authorizePermissionChange(Metamaps.Active.Mapper) var isCreator = map.authorizePermissionChange(Metamaps.Active.Mapper)
var canEdit = map.authorizeToEdit(Metamaps.Active.Mapper) var canEdit = map.authorizeToEdit(Metamaps.Active.Mapper)
@ -485,6 +487,7 @@ Metamaps.Map.InfoBox = {
obj['desc'] = canEdit ? Hogan.compile(self.descHTML).render({id: map.id, desc: map.get('desc')}) : map.get('desc') obj['desc'] = canEdit ? Hogan.compile(self.descHTML).render({id: map.id, desc: map.get('desc')}) : map.get('desc')
obj['map_creator_tip'] = isCreator ? self.changePermissionText : '' obj['map_creator_tip'] = isCreator ? self.changePermissionText : ''
obj['contributor_count'] = relevantPeople.length
obj['contributors_class'] = relevantPeople.length > 1 ? 'multiple' : '' obj['contributors_class'] = relevantPeople.length > 1 ? 'multiple' : ''
obj['contributors_class'] += relevantPeople.length === 2 ? ' mTwo' : '' obj['contributors_class'] += relevantPeople.length === 2 ? ' mTwo' : ''
obj['contributor_image'] = relevantPeople.length > 0 ? relevantPeople.models[0].get('image') : "<%= asset_path('user.png') %>" obj['contributor_image'] = relevantPeople.length > 0 ? relevantPeople.models[0].get('image') : "<%= asset_path('user.png') %>"
@ -558,6 +561,85 @@ Metamaps.Map.InfoBox = {
$('.mapInfoBox').unbind('.hideTip').bind('click.hideTip', function () { $('.mapInfoBox').unbind('.hideTip').bind('click.hideTip', function () {
$('.mapContributors .tip').hide() $('.mapContributors .tip').hide()
}) })
self.addTypeahead()
},
addTypeahead: function () {
var self = Metamaps.Map.InfoBox
// for autocomplete
var collaborators = {
name: 'collaborators',
limit: 9999,
display: function(s) { return s.label; },
templates: {
notFound: function(s) {
return Hogan.compile($('#collaboratorSearchTemplate').html()).render({
value: "No results",
label: "No results",
rtype: "noresult",
profile: "<%= asset_path('user.png') %>",
});
},
suggestion: function(s) {
return Hogan.compile($('#collaboratorSearchTemplate').html()).render(s);
},
},
source: new Bloodhound({
datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'),
queryTokenizer: Bloodhound.tokenizers.whitespace,
remote: {
url: '/search/mappers?term=%QUERY',
wildcard: '%QUERY',
},
})
}
// for adding map collaborators, who will have edit rights
if (Metamaps.Active.Mapper && Metamaps.Active.Mapper.id === Metamaps.Active.Map.get('user_id')) {
$('.collaboratorSearchField').typeahead(
{
highlight: false,
},
[collaborators]
)
$('.collaboratorSearchField').bind('typeahead:select', self.handleResultClick)
$('.mapContributors .removeCollaborator').click(function () {
self.removeCollaborator(parseInt($(this).data('id')))
})
}
},
removeCollaborator: function (collaboratorId) {
var self = Metamaps.Map.InfoBox
Metamaps.Collaborators.remove(Metamaps.Collaborators.get(collaboratorId))
var mapperIds = Metamaps.Collaborators.models.map(function (mapper) { return mapper.id })
$.post('/maps/' + Metamaps.Active.Map.id + '/access', { access: mapperIds })
self.updateNumbers()
},
addCollaborator: function (newCollaboratorId) {
var self = Metamaps.Map.InfoBox
if (Metamaps.Collaborators.get(newCollaboratorId)) {
Metamaps.GlobalUI.notifyUser('That user already has access')
return
}
function callback(mapper) {
Metamaps.Collaborators.add(mapper)
var mapperIds = Metamaps.Collaborators.models.map(function (mapper) { return mapper.id })
$.post('/maps/' + Metamaps.Active.Map.id + '/access', { access: mapperIds })
var name = Metamaps.Collaborators.get(newCollaboratorId).get('name')
Metamaps.GlobalUI.notifyUser(name + ' will be notified by email')
self.updateNumbers()
}
$.getJSON('/users/' + newCollaboratorId + '.json', callback)
},
handleResultClick: function (event, item) {
var self = Metamaps.Map.InfoBox
self.addCollaborator(item.id)
$('.collaboratorSearchField').typeahead('val', '')
}, },
updateNameDescPerm: function (name, desc, perm) { updateNameDescPerm: function (name, desc, perm) {
$('.mapInfoName .best_in_place_name').html(name) $('.mapInfoName .best_in_place_name').html(name)
@ -567,14 +649,24 @@ Metamaps.Map.InfoBox = {
createContributorList: function () { createContributorList: function () {
var self = Metamaps.Map.InfoBox var self = Metamaps.Map.InfoBox
var relevantPeople = Metamaps.Active.Map.get('permission') === 'commons' ? Metamaps.Mappers : Metamaps.Collaborators var relevantPeople = Metamaps.Active.Map.get('permission') === 'commons' ? Metamaps.Mappers : Metamaps.Collaborators
var activeMapperIsCreator = Metamaps.Active.Mapper && Metamaps.Active.Mapper.id === Metamaps.Active.Map.get('user_id')
var string = '' var string = ''
string += '<ul>' string += '<ul>'
relevantPeople.each(function (m) { relevantPeople.each(function (m) {
string += '<li><a href="/explore/mapper/' + m.get('id') + '">' + '<img class="rtUserImage" width="25" height="25" src="' + m.get('image') + '" />' + m.get('name') + '</a></li>' var isCreator = Metamaps.Active.Map.get('user_id') === m.get('id')
string += '<li><a href="/explore/mapper/' + m.get('id') + '">' + '<img class="rtUserImage" width="25" height="25" src="' + m.get('image') + '" />' + m.get('name')
if (isCreator) string += ' (creator)'
string += '</a>'
if (activeMapperIsCreator && !isCreator) string += '<span class="removeCollaborator" data-id="' + m.get('id') + '"></span>'
string += '</li>'
}) })
string += '</ul>' string += '</ul>'
if (activeMapperIsCreator) {
string += '<div class="collabSearchField"><span class="addCollab"></span><input class="collaboratorSearchField" placeholder="Add a collaborator!"></input></div>'
}
return string return string
}, },
updateNumbers: function () { updateNumbers: function () {
@ -594,6 +686,10 @@ Metamaps.Map.InfoBox = {
$('.mapContributors img').attr('src', contributors_image).removeClass('multiple mTwo').addClass(contributors_class) $('.mapContributors img').attr('src', contributors_image).removeClass('multiple mTwo').addClass(contributors_class)
$('.mapContributors span').text(relevantPeople.length) $('.mapContributors span').text(relevantPeople.length)
$('.mapContributors .tip').html(self.createContributorList()) $('.mapContributors .tip').html(self.createContributorList())
self.addTypeahead()
$('.mapContributors .tip').unbind().click(function (event) {
event.stopPropagation()
})
$('.mapTopics').text(Metamaps.Topics.length) $('.mapTopics').text(Metamaps.Topics.length)
$('.mapSynapses').text(Metamaps.Synapses.length) $('.mapSynapses').text(Metamaps.Synapses.length)

View file

@ -1569,6 +1569,11 @@ h3.filterBox {
background-repeat: no-repeat; background-repeat: no-repeat;
text-align: left; text-align: left;
} }
.commonsMap .mapContributors {
visibility: hidden;
}
.mapContributors { .mapContributors {
position: relative; position: relative;
height: 30px; height: 30px;
@ -1576,6 +1581,7 @@ h3.filterBox {
padding: 0; padding: 0;
width: 64px; width: 64px;
} }
#mapContribs { #mapContribs {
float: left; float: left;
border: 2px solid #424242; border: 2px solid #424242;
@ -1591,7 +1597,7 @@ h3.filterBox {
#mapContribs.multiple { #mapContribs.multiple {
box-shadow: 1px 1px 0 0 #B5B5B5,3px 2px 0 0 #424242,4px 3px 0 0 #B5B5B5,5px 4px 0 0 #424242; box-shadow: 1px 1px 0 0 #B5B5B5,3px 2px 0 0 #424242,4px 3px 0 0 #B5B5B5,5px 4px 0 0 #424242;
} }
.mapContributors span { .mapContributors span.count {
height: 20px; height: 20px;
padding-top: 5px; padding-top: 5px;
padding-left: 8px; padding-left: 8px;
@ -1626,6 +1632,7 @@ h3.filterBox {
.mapContributors .tip { .mapContributors .tip {
top: 45px; top: 45px;
left: -10px; left: -10px;
min-width: 200px;
} }
.mapContributors .tip ul { .mapContributors .tip ul {
@ -1652,7 +1659,7 @@ h3.filterBox {
.mapContributors .tip li a { .mapContributors .tip li a {
color: white; color: white;
} }
.mapContributors div:after { .mapContributors div.tip:after {
content: ''; content: '';
position: absolute; position: absolute;
top: -4px; top: -4px;
@ -1672,6 +1679,90 @@ h3.filterBox {
border-radius: 14px; border-radius: 14px;
} }
.mapContributors span.twitter-typeahead {
padding: 0;
}
.collabSearchField {
text-align: left;
}
.collabNameWrapper {
float: left;
}
.collabIconWrapper img.icon {
position: relative;
top: 0;
left: 0;
}
.collabIconWrapper {
position: relative;
float: left;
padding: 0 4px;
}
.mapContributors .collabName {
font-weight: normal;
font-size: 14px;
line-height: 28px;
color: #424242;
padding: 0 4px;
}
span.removeCollaborator {
position: absolute;
top: 11px;
right: 8px;
height: 16px;
width: 16px;
background-image: url(<%= asset_data_uri('removecollab_sprite.png') %>);
cursor: pointer;
}
span.removeCollaborator:hover {
background-position: -16px 0;
}
span.addCollab {
width: 16px;
height: 16px;
background-image: url(<%= asset_data_uri('addcollab_sprite.png') %>);
display: inline-block;
vertical-align: middle;
margin: 0 12px 0 10px;
}
input.collaboratorSearchField {
background: #FFFFFF;
height: 14px;
margin: 0;
padding: 10px 6px;
border: none;
border-radius: 2px;
outline: none;
font-size: 14px;
line-height: 14px;
color: #424242;
font-family: 'din-medium', helvetica, sans-serif;
}
.tt-dataset.tt-dataset-collaborators {
padding: 2px;
background: #E0E0E0;
min-width: 156px;
border-radius: 2px;
}
.tt-dataset.tt-dataset .collabResult {
padding: 4px;
}
.collabResult.tt-suggestion.tt-cursor, .collabResult.tt-suggestion:hover {
background-color: #CCCCCC;
}
.mapInfoBox .mapPermission .tooltips { .mapInfoBox .mapPermission .tooltips {
top: -20px; top: -20px;
right: 36px; right: 36px;

View file

@ -4,7 +4,7 @@ class MainController < ApplicationController
include UsersHelper include UsersHelper
include SynapsesHelper include SynapsesHelper
after_action :verify_policy_scoped, except: :requestinvite after_action :verify_policy_scoped, except: [:requestinvite, :searchmappers]
respond_to :html, :json respond_to :html, :json
@ -133,8 +133,7 @@ class MainController < ApplicationController
#remove "mapper:" if appended at beginning #remove "mapper:" if appended at beginning
term = term[7..-1] if term.downcase[0..6] == "mapper:" term = term[7..-1] if term.downcase[0..6] == "mapper:"
search = term.downcase + '%' search = term.downcase + '%'
builder = policy_scope(User) # TODO do I need to policy scope? I guess yes to verify_policy_scoped builder = User.where('LOWER("name") like ?', search)
builder = builder.where('LOWER("name") like ?', search)
@mappers = builder.order(:name) @mappers = builder.order(:name)
else else
@mappers = [] @mappers = []

View file

@ -241,10 +241,11 @@ class MapsController < ApplicationController
not @map.collaborators.include?(user) not @map.collaborators.include?(user)
end end
} }
removed = @map.collaborators.select { |user| not userIds.include?(user.id) }.map(&:id) removed = @map.collaborators.select { |user| not userIds.include?(user.id.to_s) }.map(&:id)
added.each { |uid| added.each { |uid|
um = UserMap.create({ user_id: uid.to_i, map_id: @map.id }) um = UserMap.create({ user_id: uid.to_i, map_id: @map.id })
# send email here user = User.find(uid.to_i)
MapMailer.invite_to_edit_email(@map, current_user, user).deliver_later
} }
removed.each { |uid| removed.each { |uid|
@map.user_maps.select{ |um| um.user_id == uid }.each{ |um| um.destroy } @map.user_maps.select{ |um| um.user_id == uid }.each{ |um| um.destroy }

View file

@ -0,0 +1,4 @@
class ApplicationMailer < ActionMailer::Base
default from: "team@metamaps.cc"
layout 'mailer'
end

11
app/mailers/map_mailer.rb Normal file
View file

@ -0,0 +1,11 @@
class MapMailer < ApplicationMailer
default from: "team@metamaps.cc"
def invite_to_edit_email(map, inviter, invitee)
@inviter = inviter
@map = map
@url = map_url(@map)
subject = @map.name + ' - Invitation to edit'
mail(to: invitee.email, subject: subject)
end
end

View file

@ -12,7 +12,7 @@
<div class="infoStatIcon mapContributors hoverForTip"> <div class="infoStatIcon mapContributors hoverForTip">
<img id="mapContribs" class="{{contributors_class}}" <img id="mapContribs" class="{{contributors_class}}"
width="25" height="25" src="{{contributor_image}}" /> width="25" height="25" src="{{contributor_image}}" />
<span>{{contributor_count}}</span> <span class="count">{{contributor_count}}</span>
<div class="tip">{{{contributor_list}}}</div> <div class="tip">{{{contributor_list}}}</div>
</div> </div>
<div class="infoStatIcon mapTopics"> <div class="infoStatIcon mapTopics">
@ -181,6 +181,18 @@
</div> </div>
</script> </script>
<script type="text/template" id="collaboratorSearchTemplate">
<div class="collabResult">
<div class="collabIconWrapper">
<img class="icon" width="25" height="25" src="{{profile}}">
</div>
<div class="collabNameWrapper">
<p class="collabName">{{label}}</p>
</div>
<div class="clearfloat"></div>
</div>
</script>
<script type="text/template" id="synapseAutocompleteTemplate"> <script type="text/template" id="synapseAutocompleteTemplate">
<div class="result{{rtype}}"> <div class="result{{rtype}}">
<p class="autocompleteSection synapseDesc">{{label}}</p> <p class="autocompleteSection synapseDesc">{{label}}</p>

View file

@ -0,0 +1,5 @@
<html>
<body>
<%= yield %>
</body>
</html>

View file

@ -0,0 +1 @@
<%= yield %>

View file

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<meta content='text/html; charset=UTF-8' http-equiv='Content-Type' />
</head>
<body>
<p><%= @inviter.name %> has invited you to <span style="font-weight: bold">collaboratively edit</span> the following map on Metamaps:</p>
<p style="font-weight: bold;"><%= link_to @map.name, @url %></p>
<p style="font-size: 11px;">Make sense with Metamaps</p>
</body>
</html>

View file

@ -0,0 +1,7 @@
<%= @inviter.name %> has invited you to collaboratively edit the following map on Metamaps:
<%= @map.name + ' [' + @url + ']' %>
Make sense with Metamaps

View file

@ -22,12 +22,24 @@
<% elsif relevantPeople.count > 2 %> <% elsif relevantPeople.count > 2 %>
<img id="mapContribs" width="25" height="25" src="<%= relevantPeople[0].image.url(:thirtytwo) %>" class="multiple" /> <img id="mapContribs" width="25" height="25" src="<%= relevantPeople[0].image.url(:thirtytwo) %>" class="multiple" />
<% end %> <% end %>
<span><%= relevantPeople.count %></span> <span class="count"><%= relevantPeople.count %></span>
<div class="tip"> <ul><% relevantPeople.each_with_index do |c, index| %> <div class="tip">
<li ><a href="/explore/mapper/<%= c.id %>" > <img class="rtUserImage" width="25" height="25" src="<%= asset_path c.image.url(:thirtytwo) %>" /> <ul><% relevantPeople.each_with_index do |c, index| %>
<%= c.name %></a> <li>
<a href="/explore/mapper/<%= c.id %>" > <img class="rtUserImage" width="25" height="25" src="<%= asset_path c.image.url(:thirtytwo) %>" />
<%= c.name %>
<% if @map.user == c %> (creator)<% end %>
</a>
<% if @map.user != c && @map.user == current_user %>
<span class="removeCollaborator" data-id=" + m.get('id') + "></span>
<% end %>
</li> </li>
<% end %></ul></div> <% end %>
</ul>
<% if @map.user == current_user %>
<div class="collabSearchField"><span class="addCollab"></span><input class="collaboratorSearchField" placeholder="Add a collaborator!"></input></div>
<% end %>
</div>
</div> </div>
<div class="infoStatIcon mapTopics"> <div class="infoStatIcon mapTopics">
<%= @map.topics.count %> <%= @map.topics.count %>

View file

@ -30,7 +30,7 @@ Metamaps::Application.configure do
port: ENV['SMTP_PORT'], port: ENV['SMTP_PORT'],
user_name: ENV['SMTP_USERNAME'], user_name: ENV['SMTP_USERNAME'],
password: ENV['SMTP_PASSWORD'], password: ENV['SMTP_PASSWORD'],
#domain: ENV['SMTP_DOMAIN'] domain: ENV['SMTP_DOMAIN'],
authentication: 'plain', authentication: 'plain',
enable_starttls_auto: true, enable_starttls_auto: true,
openssl_verify_mode: 'none' } openssl_verify_mode: 'none' }

View file

@ -0,0 +1,5 @@
require "rails_helper"
RSpec.describe MapMailer, type: :mailer do
pending "add some examples to (or delete) #{__FILE__}"
end

View file

@ -0,0 +1,4 @@
# Preview all emails at http://localhost:3000/rails/mailers/map_mailer
class MapMailerPreview < ActionMailer::Preview
end