Merge branch 'develop'

This commit is contained in:
Connor Turland 2017-03-09 14:55:59 -05:00
commit 44753dbfe1
27 changed files with 186 additions and 81 deletions

View file

@ -455,19 +455,6 @@
z-index: 4;
}
.takeScreenshot {
margin-bottom: 5px;
border-radius: 2px;
background-image: url(<%= asset_path 'screenshot_sprite.png' %>);
display: none;
}
.takeScreenshot:hover {
background-position: -32px 0;
}
.canEditMap .takeScreenshot {
display: block;
}
.zoomExtents {
margin-bottom:5px;
border-radius: 2px;
@ -478,7 +465,7 @@
background-position: -32px 0;
}
.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,
.zoomExtents:hover .tooltips, .zoomIn:hover .tooltips, .zoomOut:hover .tooltips, .sidebarFilterIcon:hover .tooltipsUnder, .sidebarForkIcon:hover .tooltipsUnder, .notificationsIcon:hover .tooltipsUnder, .addMap:hover .tooltipsUnder, .authenticated .sidebarAccountIcon:hover .tooltipsUnder,
.mapInfoIcon:hover .tooltipsAbove, .openCheatsheet:hover .tooltipsAbove, .chat-button:hover .tooltips, .importDialog:hover .tooltipsUnder, .starMap:hover .tooltipsAbove, .openMetacodeSwitcher:hover .tooltipsAbove, .pinCarousel:not(.isPinned):hover .tooltipsAbove.helpPin, .pinCarousel.isPinned:hover .tooltipsAbove.helpUnpin {
display: block;
}
@ -609,7 +596,7 @@
margin-top: 40px;
}
.zoomExtents div::after, .zoomIn div::after, .zoomOut div::after, .takeScreenshot div:after, .chat-button div.tooltips::after {
.zoomExtents div::after, .zoomIn div::after, .zoomOut div::after, .chat-button div.tooltips::after {
content: '';
position: absolute;
top: 57%;

View file

@ -3,6 +3,7 @@ module Api
module V2
class UsersController < RestfulController
def current
raise Pundit::NotAuthorizedError if current_user.nil?
@user = current_user
authorize @user
show # delegate to the normal show function

View file

@ -23,11 +23,12 @@ class UsersController < ApplicationController
if user_params[:password] == '' && user_params[:password_confirmation] == ''
# not trying to change the password
if @user.update_attributes(user_params.except(:password, :password_confirmation))
update_follow_settings(@user, params[:settings])
@user.image = nil if params[:remove_image] == '1'
@user.save
sign_in(@user, bypass: true)
respond_to do |format|
format.html { redirect_to root_url, notice: 'Account updated!' }
format.html { redirect_to root_url, notice: 'Settings updated' }
end
else
sign_in(@user, bypass: true)
@ -40,11 +41,12 @@ class UsersController < ApplicationController
correct_pass = @user.valid_password?(params[:current_password])
if correct_pass && @user.update_attributes(user_params)
update_follow_settings(@user, params[:settings])
@user.image = nil if params[:remove_image] == '1'
@user.save
sign_in(@user, bypass: true)
respond_to do |format|
format.html { redirect_to root_url, notice: 'Account updated!' }
format.html { redirect_to root_url, notice: 'Settings updated' }
end
else
respond_to do |format|
@ -104,9 +106,16 @@ class UsersController < ApplicationController
private
def update_follow_settings(user, settings)
user.settings.follow_topic_on_created = settings[:follow_topic_on_created]
user.settings.follow_topic_on_contributed = settings[:follow_topic_on_contributed]
user.settings.follow_map_on_created = settings[:follow_map_on_created]
user.settings.follow_map_on_contributed = settings[:follow_map_on_contributed]
end
def user_params
params.require(:user).permit(
:name, :email, :image, :password, :password_confirmation, :emails_allowed
:name, :email, :image, :password, :password_confirmation, :emails_allowed, :settings
)
end
end

View file

@ -39,7 +39,7 @@ class Map < ApplicationRecord
# Validate the attached image is image/jpg, image/png, etc
validates_attachment_content_type :screenshot, content_type: %r{\Aimage/.*\Z}
after_create :after_created_async
after_create :after_created
after_update :after_updated
after_save :update_deferring_topics_and_synapses, if: :permission_changed?
@ -140,11 +140,10 @@ class Map < ApplicationRecord
protected
def after_created_async
def after_created
FollowService.follow(self, self.user, 'created')
# notify users following the map creator
end
handle_asynchronously :after_created_async
def after_updated
return unless ATTRS_TO_WATCH.any? { |k| changed_attributes.key?(k) }

View file

@ -62,6 +62,12 @@ class User < ApplicationRecord
maps: following.where(followed_type: 'Map').to_a.map(&:followed_id)
}
end
if (_options[:follow_settings])
json['follow_topic_on_created'] = settings.follow_topic_on_created == "1"
json['follow_topic_on_contributed'] = settings.follow_topic_on_contributed == "1"
json['follow_map_on_created'] = settings.follow_map_on_created == "1"
json['follow_map_on_contributed'] = settings.follow_map_on_contributed == "1"
end
if (_options[:email])
json['email'] = email
end
@ -126,9 +132,23 @@ class User < ApplicationRecord
stars.where(map_id: map.id).exists?
end
def has_map_open(map)
latestEvent = Event.where(map: map, user: self)
.where(kind: ['user_present_on_map', 'user_not_present_on_map'])
.order(:created_at)
.last
latestEvent && latestEvent.kind == 'user_present_on_map'
end
def has_map_with_synapse_open(synapse)
synapse.maps.any?{|map| has_map_open(map)}
end
def settings
# make sure we always return a UserPreference instance
self[:settings] = UserPreference.new if self[:settings].nil?
if not self[:settings].respond_to?(:follow_topic_on_created)
self[:settings].initialize_follow_settings
end
self[:settings]
end

View file

@ -1,6 +1,7 @@
# frozen_string_literal: true
class UserPreference
attr_accessor :metacodes, :metacode_focus
attr_accessor :metacodes, :metacode_focus, :follow_topic_on_created, :follow_topic_on_contributed,
:follow_map_on_created, :follow_map_on_contributed
def initialize
array = []
@ -16,5 +17,13 @@ class UserPreference
end
@metacodes = array
@metacode_focus = array[0]
initialize_follow_settings
end
def initialize_follow_settings
@follow_topic_on_created = false
@follow_topic_on_contributed = false
@follow_map_on_created = false
@follow_map_on_contributed = false
end
end

View file

@ -13,50 +13,49 @@ module Api
@embeds ||= (scope[:embeds] || []).select { |e| self.class.embeddable.keys.include?(e) }
end
# self.embeddable might look like this:
# creator: { attr: :first_creator, serializer: UserSerializer }
# contributors: { serializer: UserSerializer}
# This method will remove the :attr key if the underlying attribute name
# is different than the name provided in the final json output. All other keys
# in the hash will be passed to the ActiveModel::Serializer `attribute` method
# directly (e.g. serializer in the examples will be passed).
#
# This setup means if you passed this self.embeddable config and sent no
# ?embed= query param with your API request, you would get the regular attributes
# plus creator_id and contributor_ids. If you passed ?embed=creator,contributors
# then instead of an id and an array of ids, you would get a serialized user
# (the first_creator) and an array of serialized users (the contributors).
# Here's an example object that could be passed in self.embeddable: {
# creator: {
# serializer: UserSerializer,
# },
# collaborators: {
# serializer: UserSerializer
# },
# topic: {},
# synapses: {}
# }
# The key has to be in embeddable or it won't show in the response, and the serializer is
# only needed if the key doesn't match a serializer
def self.embed_dat
embeddable.each_pair do |key, opts|
attr = opts.delete(:attr) || key
if attr.to_s.pluralize == attr.to_s
attribute("#{attr.to_s.singularize}_ids".to_sym,
opts.merge(unless: -> { embeds.include?(key) })) do
Pundit.policy_scope(scope[:current_user], object.send(attr))&.map(&:id) || []
is_plural = key.to_s.pluralize == key.to_s
id_key = key.to_s.singularize + (is_plural ? '_ids' : '_id')
serializer = opts.delete(:serializer) || "Api::V2::#{key.to_s.singularize.camelize}Serializer".constantize
if is_plural
attribute(id_key.to_sym, opts.merge(unless: -> { embeds.include?(key) })) do
Pundit.policy_scope(scope[:current_user], object.send(key))&.map(&:id) || []
end
has_many(attr, opts.merge(if: -> { embeds.include?(key) })) do
list = Pundit.policy_scope(scope[:current_user], object.send(attr)) || []
child_serializer = "Api::V2::#{attr.to_s.singularize.camelize}Serializer".constantize
has_many(key, opts.merge(if: -> { embeds.include?(key) })) do
list = Pundit.policy_scope(scope[:current_user], object.send(key)) || []
resource = ActiveModelSerializers::SerializableResource.new(
list,
each_serializer: child_serializer,
each_serializer: serializer,
scope: scope.merge(embeds: [])
)
resource.as_json
# resource.as_json will return e.g. { users: [ ... ] } for collaborators
# since we can't get the :users key, convert to an array and use .first.second to get the needed values
resource&.as_json&.to_a&.first&.second
end
else
id_opts = opts.merge(key: "#{key}_id")
attribute("#{attr}_id".to_sym,
id_opts.merge(unless: -> { embeds.include?(key) }))
attribute(key, opts.merge(if: -> { embeds.include?(key) })) do |serializer|
object = serializer.object.send(key)
child_serializer = "Api::V2::#{object.class.name}Serializer".constantize
attribute(id_key.to_sym, opts.merge(unless: -> { embeds.include?(key) }))
attribute(key, opts.merge(if: -> { embeds.include?(key) })) do |parent_serializer|
object = parent_serializer.object.send(key)
next nil if object.nil?
resource = ActiveModelSerializers::SerializableResource.new(
object,
serializer: child_serializer,
serializer: serializer,
scope: scope.merge(embeds: [])
)
resource.as_json
resource&.as_json&.to_a&.first&.second
end
end
end

View file

@ -18,7 +18,7 @@ module Api
def self.embeddable
{
user: {},
source: {},
source: { serializer: MapSerializer },
topics: {},
synapses: {},
mappings: {},

View file

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

View file

@ -3,7 +3,9 @@ class FollowService
class << self
def follow(entity, user, reason)
return unless is_tester(user)
return unless user && is_tester(user)
return if (reason == 'created' || reason == 'contributed') && !should_auto_follow(entity, user, reason)
follow = Follow.where(followed: entity, user: user).first_or_create
if FollowReason::REASONS.include?(reason) && !follow.follow_reason.read_attribute(reason)
@ -28,8 +30,20 @@ class FollowService
protected
def is_tester(user)
%w(connorturland@gmail.com devin@callysto.com chessscholar@gmail.com solaureum@gmail.com ishanshapiro@gmail.com).include?(user.email)
def should_auto_follow(entity, user, reason)
if entity.class == Topic
if reason == 'created'
return user.settings.follow_topic_on_created == '1'
elsif reason == 'contributed'
return user.settings.follow_topic_on_contributed == '1'
end
elsif entity.class == Map
if reason == 'created'
return user.settings.follow_map_on_created == '1'
elsif reason == 'contributed'
return user.settings.follow_map_on_contributed == '1'
end
end
end
end
end

View file

@ -34,6 +34,14 @@ class NotificationService
# we'll prbly want to put the body into the actual loop so we can pass the current user in as a local
body = renderer.render(template: settings[:template], locals: { entity: entity, event: event }, layout: false)
follows.each{|follow|
case event_type
when TOPIC_ADDED_TO_MAP
next unless TopicPolicy.new(follow.user, entity).show? && MapPolicy.new(follow.user, event.map).show?
next if follow.user.has_map_open(event.map)
when TOPIC_CONNECTED_1, TOPIC_CONNECTED_2
next unless SynapsePolicy.new(follow.user, event).show?
next if follow.user.has_map_with_synapse_open(event)
end
# this handles email and in-app notifications, in the future, include push
follow.user.notify(settings[:subject], body, event, false, event_type, follow.user.emails_allowed, event.user)
# push could be handled with Actioncable to send transient notifications to the UI

View file

@ -10,7 +10,7 @@
<ul>
<li class="accountListItem accountSettings">
<div class="accountIcon"></div>
<%= link_to "Account", edit_user_url(account) %>
<%= link_to "Settings", edit_user_url(account) %>
</li>
<% if account.admin %>
<li class="accountListItem accountAdmin">

View file

@ -3,7 +3,7 @@
<%= render :partial => 'shared/metacodeBgColors' %>
<script type="text/javascript" charset="utf-8">
<% if current_user %>
Metamaps.ServerData.ActiveMapper = <%= current_user.to_json({follows: true, email: true}).html_safe %>
Metamaps.ServerData.ActiveMapper = <%= current_user.to_json({follows: true, email: true, follow_settings: true}).html_safe %>
<% else %>
Metamaps.ServerData.ActiveMapper = null
<% end %>

View file

@ -1,5 +1,4 @@
<div class="mapControls mapElement">
<div class="takeScreenshot mapControl"><div class="tooltips">Capture Screenshot</div></div>
<div class="zoomExtents mapControl"><div class="tooltips">Center View</div></div>
<div class="zoomIn mapControl"><div class="tooltips">Zoom In</div></div>
<div class="zoomOut mapControl"><div class="tooltips">Zoom Out</div></div>

View file

@ -8,7 +8,7 @@
<% content_for :mobile_title, "Account Settings" %>
<div id="yield">
<%= form_for @user, url: user_url, :html =>{ :multipart => true, :class => "edit_user centerGreyForm"} do |form| %>
<h3>Edit Account</h3>
<h3>Edit Settings</h3>
<div class="userImage">
<div class="userImageDiv" onclick="Metamaps.Account.toggleChangePicture()">
<%= image_tag @user.image.url(:ninetysix), :size => "84x84" %>
@ -45,6 +45,26 @@
<%= form.check_box :emails_allowed, class: 'inline' %>
Send Metamaps notifications to my email.
<% end %>
<% if is_tester(@user) %>
<%= fields_for :settings, @user.settings do |settings| %>
<%= settings.label :follow_topic_on_created, class: 'firstFieldText' do %>
<%= settings.check_box :follow_topic_on_created, class: 'inline' %>
Auto-follow topics you create.
<% end %>
<%= settings.label :follow_topic_on_contributed, class: 'firstFieldText' do %>
<%= settings.check_box :follow_topic_on_contributed, class: 'inline' %>
Auto-follow topics you edit.
<% end %>
<%= settings.label :follow_map_on_created, class: 'firstFieldText' do %>
<%= settings.check_box :follow_map_on_created, class: 'inline' %>
Auto-follow maps you create.
<% end %>
<%= settings.label :follow_map_on_contributed, class: 'firstFieldText' do %>
<%= settings.check_box :follow_map_on_contributed, class: 'inline' %>
Auto-follow maps you edit.
<% end %>
<% end %>
<% end %>
</div>
<div class="changePass" onclick="Metamaps.Account.showPass()">Change Password</div>
<div class="toHide">

View file

@ -0,0 +1,3 @@
def is_tester(user)
user && %w(connorturland@gmail.com devin@callysto.com chessscholar@gmail.com solaureum@gmail.com ishanshapiro@gmail.com).include?(user.email)
end

View file

@ -123,10 +123,14 @@ const Control = {
const authorized = Active.Map.authorizeToEdit(Active.Mapper)
if (!authorized) {
GlobalUI.notifyUser('Cannot edit Public map.')
GlobalUI.notifyUser('Cannot edit this map.')
return
}
if (Active.Mapper.get('follow_map_on_contributed')) {
Active.Mapper.followMap(Active.Map.id)
}
for (let i = l - 1; i >= 0; i -= 1) {
const node = Selected.Nodes[i]
Control.removeNode(node.id)
@ -139,10 +143,14 @@ const Control = {
var node = Visualize.mGraph.graph.getNode(nodeid)
if (!authorized) {
GlobalUI.notifyUser('Cannot edit Public map.')
GlobalUI.notifyUser('Cannot edit this map.')
return
}
if (Active.Mapper.get('follow_map_on_contributed')) {
Active.Mapper.followMap(Active.Map.id)
}
var topic = node.getData('topic')
var mapping = node.getData('mapping')
mapping.destroy()
@ -284,10 +292,14 @@ const Control = {
var authorized = Active.Map.authorizeToEdit(Active.Mapper)
if (!authorized) {
GlobalUI.notifyUser('Cannot edit Public map.')
GlobalUI.notifyUser('Cannot edit this map.')
return
}
if (Active.Mapper.get('follow_map_on_contributed')) {
Active.Mapper.followMap(Active.Map.id)
}
for (let i = l - 1; i >= 0; i -= 1) {
const edge = Selected.Edges[i]
Control.removeEdge(edge)
@ -300,10 +312,14 @@ const Control = {
var authorized = Active.Map.authorizeToEdit(Active.Mapper)
if (!authorized) {
GlobalUI.notifyUser('Cannot edit Public map.')
GlobalUI.notifyUser('Cannot edit this map.')
return
}
if (Active.Mapper.get('follow_map_on_contributed')) {
Active.Mapper.followMap(Active.Map.id)
}
if (edge.getData('mappings').length - 1 === 0) {
Control.hideEdge(edge)
}

View file

@ -17,14 +17,16 @@ const Mapper = Backbone.Model.extend({
</li>`
},
followMap: function(id) {
this.get('follows').maps.push(id)
const idIndex = this.get('follows').maps.indexOf(id)
if (idIndex < 0) this.get('follows').maps.push(id)
},
unfollowMap: function(id) {
const idIndex = this.get('follows').maps.indexOf(id)
if (idIndex > -1) this.get('follows').maps.splice(idIndex, 1)
},
followTopic: function(id) {
this.get('follows').topics.push(id)
const idIndex = this.get('follows').topics.indexOf(id)
if (idIndex < 0) this.get('follows').topics.push(id)
},
unfollowTopic: function(id) {
const idIndex = this.get('follows').topics.indexOf(id)

View file

@ -99,6 +99,9 @@ const CreateMap = {
success: function(model) {
// push the new map onto the collection of 'my maps'
DataModel.Maps.Mine.add(model)
if (Active.Mapper.get('follow_map_on_created')) {
Active.Mapper.followMap(model.id)
}
GlobalUI.clearNotify()
$('#wrapper').append(outdent`

View file

@ -27,7 +27,7 @@ const ImportDialog = {
onFileAdded: PasteInput.handleFile,
exampleImageUrl: serverData['import-example.png'],
downloadScreenshot: ImportDialog.downloadScreenshot,
onExport: format => {
onExport: format => () => {
window.open(`${window.location.pathname}/export.${format}`, '_blank')
}
}), $('.importDialogWrapper').get(0))

View file

@ -59,8 +59,6 @@ const JIT = {
}
$('.zoomExtents').click(zoomExtents)
$('.takeScreenshot').click(Map.exportImage)
self.topicDescImage = new Image()
self.topicDescImage.src = serverData['topic_description_signifier.png']
@ -979,6 +977,9 @@ const JIT = {
}
if (checkWhetherToSave()) {
if (Active.Mapper.get('follow_map_on_contributed')) {
Active.Mapper.followMap(Active.Map.id)
}
mapping = node.getData('mapping')
mapping.save({
xloc: node.getPos().x,

View file

@ -253,11 +253,6 @@ const Map = {
DataModel.Mappers.add(Active.Mapper)
}
},
exportImage: function() {
Map.uploadMapScreenshot()
Map.offerScreenshotDownload()
GlobalUI.notifyUser('Note: this button is going away. Check the map card or the import box for setting the map thumbnail or downloading a screenshot.')
},
offerScreenshotDownload: () => {
const canvas = Map.getMapCanvasForScreenshots()
const filename = Map.getMapScreenshotFilename(Active.Map)

View file

@ -42,6 +42,11 @@ const Synapse = {
var synapseSuccessCallback = function(synapseModel, response) {
if (Active.Map) {
mapping.save({ mappable_id: synapseModel.id }, {
success: function(model, response) {
if (Active.Mapper.get('follow_map_on_contributed')) {
Active.Mapper.followMap(Active.Map.id)
}
},
error: function(model, response) {
console.log('error saving mapping to database')
}
@ -59,6 +64,11 @@ const Synapse = {
})
} else if (!synapse.isNew() && Active.Map) {
mapping.save(null, {
success: function(model, response) {
if (Active.Mapper.get('follow_map_on_contributed')) {
Active.Mapper.followMap(Active.Map.id)
}
},
error: function(model, response) {
console.log('error saving mapping to database')
}

View file

@ -242,12 +242,18 @@ const Topic = {
}
var mappingSuccessCallback = function(mappingModel, response, topicModel) {
if (Active.Mapper.get('follow_map_on_contributed')) {
Active.Mapper.followMap(Active.Map.id)
}
// call a success callback if provided
if (opts.success) {
opts.success(topicModel)
}
}
var topicSuccessCallback = function(topicModel, response) {
if (Active.Mapper.get('follow_topic_on_created')) {
Active.Mapper.followTopic(topicModel.id)
}
if (Active.Map) {
mapping.save({ mappable_id: topicModel.id }, {
success: function(model, response) {

View file

@ -9,7 +9,11 @@ class Attachments extends Component {
return (
<div className="attachments">
<EmbedlyLink link={link} authorizedToEdit={authorizedToEdit} updateTopic={updateTopic} />
<EmbedlyLink topicId={topic.id}
link={link}
authorizedToEdit={authorizedToEdit}
updateTopic={updateTopic}
/>
</div>
)
}

View file

@ -34,7 +34,7 @@ class EmbedlyLink extends Component {
}
render = () => {
const { link, authorizedToEdit } = this.props
const { link, authorizedToEdit, topicId } = this.props
const { linkEdit } = this.state
const hasAttachment = !!link
@ -55,7 +55,7 @@ class EmbedlyLink extends Component {
{linkEdit && <div id="addLinkReset" onClick={this.resetLink}></div>}
</div>
</div>
{link && <Card link={link} />}
{link && <Card key={topicId} link={link} />}
{authorizedToEdit && (
<div id="linkremove"
style={{ display: hasAttachment ? 'block' : 'none' }}
@ -68,6 +68,7 @@ class EmbedlyLink extends Component {
}
EmbedlyLink.propTypes = {
topicId: PropTypes.number,
link: PropTypes.string,
authorizedToEdit: PropTypes.bool,
updateTopic: PropTypes.func

View file

@ -12,7 +12,6 @@ const plugins = [
const externals = ["bindings"] // work around bindings.js error
if (NODE_ENV === 'production') {
plugins.push(new webpack.optimize.DedupePlugin())
plugins.push(new webpack.optimize.UglifyJsPlugin({
compress: { warnings: false }
}))