From 2fe99363127d3e4029354b13d6e4f9d0f3084698 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Mon, 19 Dec 2016 09:55:24 -0500 Subject: [PATCH 1/3] start streaming --- Gemfile | 1 + Gemfile.lock | 2 + app/assets/javascripts/application.js | 1 + app/channels/application_cable/channel.rb | 4 ++ app/channels/application_cable/connection.rb | 20 ++++++++ app/channels/topic_channel.rb | 7 +++ app/models/synapse.rb | 30 ++++++++++++ config/initializers/warden_hooks.rb | 10 ++++ config/routes.rb | 1 + frontend/src/Metamaps/Cable.js | 50 ++++++++++++++++++++ frontend/src/Metamaps/DataModel/Synapse.js | 4 ++ frontend/src/Metamaps/DataModel/Topic.js | 4 ++ frontend/src/Metamaps/JIT.js | 4 +- frontend/src/Metamaps/Topic.js | 4 ++ frontend/src/Metamaps/index.js | 8 +++- 15 files changed, 146 insertions(+), 4 deletions(-) create mode 100644 app/channels/application_cable/channel.rb create mode 100644 app/channels/application_cable/connection.rb create mode 100644 app/channels/topic_channel.rb create mode 100644 config/initializers/warden_hooks.rb create mode 100644 frontend/src/Metamaps/Cable.js 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..f0eab9c6 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,35 @@ class Synapse < ApplicationRecord super(methods: [:user_name, :user_image, :collaborator_ids]) end + def after_created + filteredSynapse = { + id: id, + permission: permission, + user_id: user_id, + collaborator_ids: collaborator_ids + } + filteredTopic1 = { + id: topic1_id, + permission: topic1.permission, + user_id: topic1.user_id, + collaborator_ids: topic1.collaborator_ids + } + filteredTopic2 = { + id: topic2_id, + permission: topic2.permission, + user_id: topic2.user_id, + collaborator_ids: topic2.collaborator_ids + } + data = { + synapse: filteredSynapse, + topic1: filteredTopic1, + topic2: filteredTopic2 + } + # include the filtered topics here too + 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/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..03ec82d4 --- /dev/null +++ b/frontend/src/Metamaps/Cable.js @@ -0,0 +1,50 @@ +/* global $, ActionCable */ + +import Active from './Active' +import DataModel from './DataModel' + +const Cable = { + topicSubs: {}, + init: () => { + let self = Cable + self.cable = ActionCable.createConsumer() + }, + subAllTopics: () => { + let self = Cable + DataModel.Topics.models.forEach(topic => self.subTopic(topic.id)) + }, + 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 => { + console.log(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)) { + console.log('authorized') + } + } +} + +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..078b8991 100644 --- a/frontend/src/Metamaps/JIT.js +++ b/frontend/src/Metamaps/JIT.js @@ -640,14 +640,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..c3afaf9d 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() + DataModel.Topics.models.forEach(topic => Cable.subTopic(topic.id)) + 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) { 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 From 240d9ddab95beb57592431760326eaa1d3f24f64 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Mon, 19 Dec 2016 23:57:07 -0500 Subject: [PATCH 2/3] its aliiiiive --- app/models/synapse.rb | 26 ++++------- app/models/topic.rb | 9 ++++ frontend/src/Metamaps/Cable.js | 8 +++- frontend/src/Metamaps/JIT.js | 22 ++++++++++ frontend/src/Metamaps/Topic.js | 79 +++++++++++++++++++++++++--------- 5 files changed, 103 insertions(+), 41 deletions(-) diff --git a/app/models/synapse.rb b/app/models/synapse.rb index f0eab9c6..b751a605 100644 --- a/app/models/synapse.rb +++ b/app/models/synapse.rb @@ -43,31 +43,21 @@ class Synapse < ApplicationRecord super(methods: [:user_name, :user_image, :collaborator_ids]) end - def after_created - filteredSynapse = { + def filtered + { id: id, permission: permission, user_id: user_id, collaborator_ids: collaborator_ids } - filteredTopic1 = { - id: topic1_id, - permission: topic1.permission, - user_id: topic1.user_id, - collaborator_ids: topic1.collaborator_ids - } - filteredTopic2 = { - id: topic2_id, - permission: topic2.permission, - user_id: topic2.user_id, - collaborator_ids: topic2.collaborator_ids - } + end + + def after_created data = { - synapse: filteredSynapse, - topic1: filteredTopic1, - topic2: filteredTopic2 + synapse: filtered, + topic1: topic1.filtered, + topic2: topic2.filtered } - # include the filtered topics here too 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 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/frontend/src/Metamaps/Cable.js b/frontend/src/Metamaps/Cable.js index 03ec82d4..cd59b255 100644 --- a/frontend/src/Metamaps/Cable.js +++ b/frontend/src/Metamaps/Cable.js @@ -2,6 +2,7 @@ import Active from './Active' import DataModel from './DataModel' +import Topic from './Topic' const Cable = { topicSubs: {}, @@ -13,6 +14,10 @@ const Cable = { 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({ @@ -36,13 +41,12 @@ const Cable = { }, // begin event functions newSynapse: data => { - console.log(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)) { - console.log('authorized') + Topic.fetchForTopicView(data.synapse.id) } } } diff --git a/frontend/src/Metamaps/JIT.js b/frontend/src/Metamaps/JIT.js index 078b8991..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 */ diff --git a/frontend/src/Metamaps/Topic.js b/frontend/src/Metamaps/Topic.js index c3afaf9d..4a2c8c6d 100644 --- a/frontend/src/Metamaps/Topic.js +++ b/frontend/src/Metamaps/Topic.js @@ -44,7 +44,7 @@ const Topic = { DataModel.Synapses = new DataModel.SynapseCollection(data.synapses) DataModel.attachCollectionEvents() - DataModel.Topics.models.forEach(topic => Cable.subTopic(topic.id)) + Cable.subAllTopics() document.title = Active.Topic.get('name') + ' | Metamaps' @@ -98,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 @@ -132,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) } From 03a6d45822bc493ab9ab06f0bda246893a5e0acf Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Tue, 20 Dec 2016 01:47:41 -0500 Subject: [PATCH 3/3] refactor and kill dupe code --- frontend/src/Metamaps/Visualize.js | 37 +----------------------------- 1 file changed, 1 insertion(+), 36 deletions(-) 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')