diff --git a/Gemfile b/Gemfile index 0d8e8d7a..58e26f01 100644 --- a/Gemfile +++ b/Gemfile @@ -44,6 +44,7 @@ group :test do end group :development, :test do + gem 'puma' gem 'better_errors' gem 'binding_of_caller' gem 'pry-byebug' diff --git a/Gemfile.lock b/Gemfile.lock index d104cb51..a1aafb05 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -167,6 +167,7 @@ GEM pry (~> 0.10) pry-rails (0.3.4) pry (>= 0.9.10) + puma (2.15.3) pundit (1.1.0) activesupport (>= 3.0.0) pundit_extra (0.3.0) @@ -298,6 +299,7 @@ DEPENDENCIES pg pry-byebug pry-rails + puma pundit pundit_extra rack-attack diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 11633bea..6af52fbd 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -14,6 +14,7 @@ //= require jquery //= require jquery-ui //= require jquery_ujs +//= require action_cable //= require_directory ./lib //= require ./webpacked/metamaps.bundle //= require ./Metamaps.ServerData diff --git a/app/channels/application_cable/channel.rb b/app/channels/application_cable/channel.rb new file mode 100644 index 00000000..d6726972 --- /dev/null +++ b/app/channels/application_cable/channel.rb @@ -0,0 +1,4 @@ +module ApplicationCable + class Channel < ActionCable::Channel::Base + end +end diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb new file mode 100644 index 00000000..8eb318cd --- /dev/null +++ b/app/channels/application_cable/connection.rb @@ -0,0 +1,20 @@ +module ApplicationCable + class Connection < ActionCable::Connection::Base + identified_by :current_user + + def connect + self.current_user = find_verified_user + logger.add_tags 'ActionCable', current_user.name + end + + protected + def find_verified_user + verified_user = User.find_by(id: cookies.signed['user.id']) + if verified_user && cookies.signed['user.expires_at'] > Time.now + verified_user + else + reject_unauthorized_connection + end + end + end +end diff --git a/app/channels/topic_channel.rb b/app/channels/topic_channel.rb new file mode 100644 index 00000000..bc5cd834 --- /dev/null +++ b/app/channels/topic_channel.rb @@ -0,0 +1,7 @@ +class TopicChannel < ApplicationCable::Channel + # Called when the consumer has successfully + # become a subscriber of this channel. + def subscribed + stream_from "topic_#{params[:id]}" + end +end diff --git a/app/models/synapse.rb b/app/models/synapse.rb index d14a18f4..b751a605 100644 --- a/app/models/synapse.rb +++ b/app/models/synapse.rb @@ -22,6 +22,7 @@ class Synapse < ApplicationRecord where(topic1_id: topic_id).or(where(topic2_id: topic_id)) } + after_create :after_created after_update :after_updated delegate :name, to: :user, prefix: true @@ -42,6 +43,25 @@ class Synapse < ApplicationRecord super(methods: [:user_name, :user_image, :collaborator_ids]) end + def filtered + { + id: id, + permission: permission, + user_id: user_id, + collaborator_ids: collaborator_ids + } + end + + def after_created + data = { + synapse: filtered, + topic1: topic1.filtered, + topic2: topic2.filtered + } + ActionCable.server.broadcast 'topic_' + topic1_id.to_s, type: 'newSynapse', data: data + ActionCable.server.broadcast 'topic_' + topic2_id.to_s, type: 'newSynapse', data: data + end + def after_updated attrs = ['desc', 'category', 'permission', 'defer_to_map_id'] if attrs.any? {|k| changed_attributes.key?(k)} diff --git a/app/models/topic.rb b/app/models/topic.rb index e5ea90ee..af88f342 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -90,6 +90,15 @@ class Topic < ApplicationRecord end end + def filtered + { + id: id, + permission: permission, + user_id: user_id, + collaborator_ids: collaborator_ids + } + end + # TODO: move to a decorator? def synapses_csv(output_format = 'array') output = [] diff --git a/config/initializers/warden_hooks.rb b/config/initializers/warden_hooks.rb new file mode 100644 index 00000000..da983955 --- /dev/null +++ b/config/initializers/warden_hooks.rb @@ -0,0 +1,10 @@ +Warden::Manager.after_set_user do |user,auth,opts| + scope = opts[:scope] + auth.cookies.signed["#{scope}.id"] = user.id + auth.cookies.signed["#{scope}.expires_at"] = 30.minutes.from_now +end +Warden::Manager.before_logout do |user, auth, opts| + scope = opts[:scope] + auth.cookies.signed["#{scope}.id"] = nil + auth.cookies.signed["#{scope}.expires_at"] = nil +end diff --git a/config/routes.rb b/config/routes.rb index 000784f6..4dc44c91 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true Metamaps::Application.routes.draw do use_doorkeeper + mount ActionCable.server => '/cable' root to: 'main#home', via: :get get 'request', to: 'main#requestinvite', as: :request diff --git a/frontend/src/Metamaps/Cable.js b/frontend/src/Metamaps/Cable.js new file mode 100644 index 00000000..cd59b255 --- /dev/null +++ b/frontend/src/Metamaps/Cable.js @@ -0,0 +1,54 @@ +/* global $, ActionCable */ + +import Active from './Active' +import DataModel from './DataModel' +import Topic from './Topic' + +const Cable = { + topicSubs: {}, + init: () => { + let self = Cable + self.cable = ActionCable.createConsumer() + }, + subAllTopics: () => { + let self = Cable + DataModel.Topics.models.forEach(topic => self.subTopic(topic.id)) + }, + subUnsubbedTopics: (topic1id, topic2id) => { + if (!Cable.topicSubs[topic1id]) Cable.subTopic(topic1id) + if (!Cable.topicSubs[topic2id]) Cable.subTopic(topic2id) + }, + subTopic: id => { + let self = Cable + self.topicSubs[id] = self.cable.subscriptions.create({ + channel: 'TopicChannel', + id: id + }, { + received: event => self[event.type](event.data) + }) + }, + unsubTopic: id => { + let self = Cable + self.topicSubs[id] && self.topicSubs[id].unsubscribe() + delete self.topicSubs[id] + }, + unsubAllTopics: () => { + let self = Cable + Object.keys(self.topicSubs).forEach(id => { + self.topicSubs[id].unsubscribe() + }) + self.topicSubs = {} + }, + // begin event functions + newSynapse: data => { + const m = Active.Mapper + const s = new DataModel.Synapse(data.synapse) + const t1 = new DataModel.Topic(data.topic1) + const t2 = new DataModel.Topic(data.topic2) + if (t1.authorizeToShow(m) && t2.authorizeToShow(m) && s.authorizeToShow(m)) { + Topic.fetchForTopicView(data.synapse.id) + } + } +} + +export default Cable diff --git a/frontend/src/Metamaps/DataModel/Synapse.js b/frontend/src/Metamaps/DataModel/Synapse.js index e6a7f1c7..be37e095 100644 --- a/frontend/src/Metamaps/DataModel/Synapse.js +++ b/frontend/src/Metamaps/DataModel/Synapse.js @@ -87,6 +87,10 @@ const Synapse = Backbone.Model.extend({ if (mapper && (this.get('permission') === 'commons' || this.get('collaborator_ids').includes(mapper.get('id')) || this.get('user_id') === mapper.get('id'))) return true else return false }, + authorizeToShow: function(mapper) { + if (this.get('permission') !== 'private' || (mapper && this.get('collaborator_ids').includes(mapper.get('id')) || this.get('user_id') === mapper.get('id'))) return true + else return false + }, authorizePermissionChange: function(mapper) { if (mapper && this.get('user_id') === mapper.get('id')) return true else return false diff --git a/frontend/src/Metamaps/DataModel/Topic.js b/frontend/src/Metamaps/DataModel/Topic.js index 0d71c973..aaf0937b 100644 --- a/frontend/src/Metamaps/DataModel/Topic.js +++ b/frontend/src/Metamaps/DataModel/Topic.js @@ -88,6 +88,10 @@ const Topic = Backbone.Model.extend({ return false } }, + authorizeToShow: function(mapper) { + if (this.get('permission') !== 'private' || (mapper && this.get('collaborator_ids').includes(mapper.get('id')) || this.get('user_id') === mapper.get('id'))) return true + else return false + }, authorizePermissionChange: function(mapper) { if (mapper && this.get('user_id') === mapper.get('id')) return true else return false diff --git a/frontend/src/Metamaps/JIT.js b/frontend/src/Metamaps/JIT.js index 903cc11d..ce20b879 100644 --- a/frontend/src/Metamaps/JIT.js +++ b/frontend/src/Metamaps/JIT.js @@ -69,6 +69,28 @@ const JIT = { self.topicLinkImage = new Image() self.topicLinkImage.src = serverData['topic_link_signifier.png'] }, + connectModelsToGraph: function () { + var i, l, t, s + + Visualize.mGraph.graph.eachNode(function(n) { + t = DataModel.Topics.get(n.id) + t.set({ node: n }, { silent: true }) + t.updateNode() + + n.eachAdjacency(function(edge) { + if (!edge.getData('init')) { + edge.setData('init', true) + + l = edge.getData('synapseIDs').length + for (i = 0; i < l; i++) { + s = DataModel.Synapses.get(edge.getData('synapseIDs')[i]) + s.set({ edge: edge }, { silent: true }) + s.updateEdge() + } + } + }) + }) + }, /** * convert our topic JSON into something JIT can use */ @@ -640,14 +662,14 @@ const JIT = { } }, // this will just be used to patch the ForceDirected graphsettings with the few things which actually differ - background: { + /*background: { levelDistance: 200, numberOfCircles: 4, CanvasStyles: { strokeStyle: '#333', lineWidth: 1.5 } - }, + },*/ levelDistance: 200 }, onMouseEnter: function(edge) { diff --git a/frontend/src/Metamaps/Topic.js b/frontend/src/Metamaps/Topic.js index 6b1aa8c1..4a2c8c6d 100644 --- a/frontend/src/Metamaps/Topic.js +++ b/frontend/src/Metamaps/Topic.js @@ -4,6 +4,7 @@ import $jit from '../patched/JIT' import Active from './Active' import AutoLayout from './AutoLayout' +import Cable from './Cable' import Create from './Create' import DataModel from './DataModel' import Filter from './Filter' @@ -43,6 +44,8 @@ const Topic = { DataModel.Synapses = new DataModel.SynapseCollection(data.synapses) DataModel.attachCollectionEvents() + Cable.subAllTopics() + document.title = Active.Topic.get('name') + ' | Metamaps' // set filter mapper H3 text @@ -78,6 +81,7 @@ const Topic = { TopicCard.hideCard() SynapseCard.hideCard() Filter.close() + Cable.unsubAllTopics() } }, centerOn: function(nodeid, callback) { @@ -94,6 +98,62 @@ const Topic = { Active.Topic = DataModel.Topics.get(nodeid) } }, + fetchForTopicView: function(synapseId) { + var self = this + + var successCallback + successCallback = function(data) { + data = data.data + if (Visualize.mGraph.busy) { + // don't clash with centerOn + window.setTimeout(function() { successCallback(data) }, 100) + return + } + // todo: these won't work when the api works as expected + const topic1user = { + id: data.topic1.user_id, + image: data.topic1.user_image, + name: data.topic1.name + } + const topic2user = { + id: data.topic2.user_id, + image: data.topic2.user_image, + name: data.topic2.name + } + let creators = [data.user, topic1user, topic2user] + DataModel.Creators.add(creators) + DataModel.Topics.add(data.topic1) + DataModel.Topics.add(data.topic2) + Cable.subUnsubbedTopics(data.topic1.id, data.topic2.id) + var topicColl = new DataModel.TopicCollection([data.topic1, data.topic2]) + + data.topic1_id = data.topic1.id + data.topic2_id = data.topic2.id + data.user_id = data.user.id + delete data.topic1 + delete data.topic2 + delete data.user + DataModel.Synapses.add(data) + var synapseColl = new DataModel.SynapseCollection(data) + + var graph = JIT.convertModelsToJIT(topicColl, synapseColl)[0] + console.log(graph) + Visualize.mGraph.op.sum(graph, { + type: 'fade', + duration: 500, + hideLabels: false + }) + JIT.connectModelsToGraph() + } + + + $.ajax({ + type: 'GET', + url: '/api/v2/synapses/' + synapseId + '?embed=topic1,topic2,user', + success: successCallback, + error: function() {} + }) + }, fetchRelatives: function(nodes, metacodeId) { var self = this @@ -128,27 +188,8 @@ const Topic = { duration: 500, hideLabels: false }) + JIT.connectModelsToGraph() - var i, l, t, s - - Visualize.mGraph.graph.eachNode(function(n) { - t = DataModel.Topics.get(n.id) - t.set({ node: n }, { silent: true }) - t.updateNode() - - n.eachAdjacency(function(edge) { - if (!edge.getData('init')) { - edge.setData('init', true) - - l = edge.getData('synapseIDs').length - for (i = 0; i < l; i++) { - s = DataModel.Synapses.get(edge.getData('synapseIDs')[i]) - s.set({ edge: edge }, { silent: true }) - s.updateEdge() - } - } - }) - }) if ($.isArray(nodes) && nodes.length > 1) { self.fetchRelatives(nodes.slice(1), metacodeId) } diff --git a/frontend/src/Metamaps/Visualize.js b/frontend/src/Metamaps/Visualize.js index 577c5418..dc02663b 100644 --- a/frontend/src/Metamaps/Visualize.js +++ b/frontend/src/Metamaps/Visualize.js @@ -48,28 +48,9 @@ const Visualize = { computePositions: function() { const self = Visualize + JIT.connectModelsToGraph() if (self.type === 'RGraph') { - let i - let l - self.mGraph.graph.eachNode(function(n) { - const topic = DataModel.Topics.get(n.id) - topic.set({ node: n }, { silent: true }) - topic.updateNode() - - n.eachAdjacency(function(edge) { - if (!edge.getData('init')) { - edge.setData('init', true) - - l = edge.getData('synapseIDs').length - for (i = 0; i < l; i++) { - const synapse = DataModel.Synapses.get(edge.getData('synapseIDs')[i]) - synapse.set({ edge: edge }, { silent: true }) - synapse.updateEdge() - } - } - }) - var pos = n.getPos() pos.setc(-200, -200) }) @@ -77,23 +58,7 @@ const Visualize = { } else if (self.type === 'ForceDirected') { self.mGraph.graph.eachNode(function(n) { const topic = DataModel.Topics.get(n.id) - topic.set({ node: n }, { silent: true }) - topic.updateNode() const mapping = topic.getMapping() - - n.eachAdjacency(function(edge) { - if (!edge.getData('init')) { - edge.setData('init', true) - - const l = edge.getData('synapseIDs').length - for (let i = 0; i < l; i++) { - const synapse = DataModel.Synapses.get(edge.getData('synapseIDs')[i]) - synapse.set({ edge: edge }, { silent: true }) - synapse.updateEdge() - } - } - }) - const startPos = new $jit.Complex(0, 0) const endPos = new $jit.Complex(mapping.get('xloc'), mapping.get('yloc')) n.setPos(startPos, 'start') diff --git a/frontend/src/Metamaps/index.js b/frontend/src/Metamaps/index.js index be218aff..b5604994 100644 --- a/frontend/src/Metamaps/index.js +++ b/frontend/src/Metamaps/index.js @@ -2,9 +2,10 @@ import Account from './Account' import Active from './Active' import Admin from './Admin' import AutoLayout from './AutoLayout' -import DataModel from './DataModel' +import Cable from './Cable' import Control from './Control' import Create from './Create' +import DataModel from './DataModel' import Debug from './Debug' import Filter from './Filter' import GlobalUI, { @@ -38,9 +39,10 @@ Metamaps.Account = Account Metamaps.Active = Active Metamaps.Admin = Admin Metamaps.AutoLayout = AutoLayout -Metamaps.DataModel = DataModel +Metamaps.Cable = Cable Metamaps.Control = Control Metamaps.Create = Create +Metamaps.DataModel = DataModel Metamaps.Debug = Debug Metamaps.Filter = Filter Metamaps.GlobalUI = GlobalUI @@ -106,6 +108,8 @@ document.addEventListener('DOMContentLoaded', function() { JIT.prepareVizData() GlobalUI.showDiv('#infovis') } + + if (Active.Topic) Cable.subAllTopics() }) export default Metamaps