Skip to content

Commit

Permalink
action cable setup (#805)
Browse files Browse the repository at this point in the history
* WIP action cable setup

* basic action cable setup complete

* minor change

* minor changes

* few changes

* initial working functionality complete

* Refactoring code

* Adding Foreman gem

* Scheduling Puma and Passenger servers

* WIP action cable setup

* basic action cable setup complete

* minor change

* minor changes

* few changes

* initial working functionality complete

* Refactoring code

* Adding Foreman gem

* Scheduling Puma and Passenger servers

* few minor fix

* added a few tests

* Refactoring connection module

* Using strong params in requests

* added documentation

* added more docs

* added tests

* Using puma as dependency and correct image controller

* added a few tests

* a few changes

* remove unnecessary render

* few test fixes
  • Loading branch information
ViditChitkara authored and alaxalves committed Aug 21, 2019
1 parent 87e18ee commit 843a719
Show file tree
Hide file tree
Showing 25 changed files with 299 additions and 45 deletions.
8 changes: 6 additions & 2 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ group :dependencies do
gem 'bootsnap', '~> 1.4.4'
gem 'turbolinks', '~> 5'
gem 'mini_magick', '~> 4.8'
gem 'puma', '~> 4.1.0'

# if you use amazon s3 for warpable image storage
gem 'aws-sdk', '~> 1.5.7'
Expand All @@ -37,12 +38,15 @@ group :dependencies do
# compiling markdown to html
gem 'rdiscount', '2.2.0.1'

# Process manager for applications with multiple components
gem 'foreman', '~> 0.85.0'

# asset pipelining
gem 'bootstrap-sass'
gem 'sassc-rails'
gem 'jquery-rails'
gem 'sprockets', '3.7.2'
gem "sprockets-rails"
gem 'sprockets-rails'
gem 'sass', require: 'sass'
gem 'autoprefixer-rails', '~> 9.5.1.1'
gem 'uglifier', '~> 4.1.20'
Expand All @@ -65,11 +69,11 @@ end

group :development, :test do
gem 'capybara'
gem 'puma'
gem 'selenium-webdriver'
gem 'byebug', '~> 11.0.1', platforms: [:mri, :mingw, :x64_mingw]
gem 'faker', '~> 2.1.2'
gem 'pry-rails', '~> 0.3.9'
gem 'action-cable-testing'
end

group :development do
Expand Down
15 changes: 10 additions & 5 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ GEM
RubyInline (3.12.4)
ZenTest (~> 4.3)
ZenTest (4.11.2)
action-cable-testing (0.6.0)
actioncable (>= 5.0)
actioncable (5.2.3)
actionpack (= 5.2.3)
nio4r (~> 2.0)
Expand Down Expand Up @@ -89,6 +91,8 @@ GEM
faker (2.1.2)
i18n (>= 0.8)
ffi (1.11.1)
foreman (0.85.0)
thor (~> 0.19.1)
friendly_id (5.2.5)
activerecord (>= 4.0.0)
geokit (1.13.1)
Expand Down Expand Up @@ -210,7 +214,7 @@ GEM
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
rails-html-sanitizer (1.1.0)
rails-html-sanitizer (1.2.0)
loofah (~> 2.2, >= 2.2.2)
rails-perftest (0.0.7)
railties (5.2.3)
Expand Down Expand Up @@ -261,9 +265,8 @@ GEM
sass-listen (4.0.0)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
sassc (2.0.1)
sassc (2.1.0)
ffi (~> 1.9)
rake
sassc-rails (2.1.2)
railties (>= 4.0.0)
sassc (>= 2.0)
Expand Down Expand Up @@ -296,7 +299,7 @@ GEM
sqlite3 (1.4.1)
terrapin (0.6.0)
climate_control (>= 0.0.3, < 1.0)
thor (0.20.3)
thor (0.19.4)
thread_safe (0.3.6)
tilt (2.0.9)
turbolinks (5.2.0)
Expand Down Expand Up @@ -330,6 +333,7 @@ PLATFORMS

DEPENDENCIES
RubyInline (~> 3.12.4)
action-cable-testing
autoprefixer-rails (~> 9.5.1.1)
aws-sdk (~> 1.5.7)
bootsnap (~> 1.4.4)
Expand All @@ -338,6 +342,7 @@ DEPENDENCIES
capybara
codecov
faker (~> 2.1.2)
foreman (~> 0.85.0)
friendly_id
geokit-rails (= 1.1.4)
httparty
Expand All @@ -357,7 +362,7 @@ DEPENDENCIES
passenger
popper_js (~> 1.11, >= 1.11.1)
pry-rails (~> 0.3.9)
puma
puma (~> 4.1.0)
rack_session_access
rails (~> 5.2.3)
rails-controller-testing
Expand Down
2 changes: 2 additions & 0 deletions Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
passenger: passenger start
puma: puma -C config/puma.rb
30 changes: 30 additions & 0 deletions SYNCHRONOUS_EDITING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
The new synchronous editing feature
===================================

With the introduction of ActionCable to our system, it has been possible
to do perform real-time tasks quite easily. We have used rail's default
action cable to make a _concurrent_editing_channel.rb_ in the _app/channels_ folder,
to handle all the incoming requests and consists of all the business
logic as well. At the frontend we have, _app/javascripts/channels/concurrent_editing.js_ which
handles the logic at the browser or the frontend.

## Flow of the feature:

1. When the map is updated, the _speak_ method of _concurrent_editing.js_ is called which requests
the _sync_ method of _concurrent_editing_channel.rb_ to broadcast the updated data to
the connected users.

2. The broadcasted data is finally caught by the _received_ function of _app/javascripts/channels/concurrent_editing.js_

3. Finally the _received_ function calls the _synchronizeData_ function to update
all the fresh data on the map.


## Testing:

1. The _action-cable-testing_ gem is used for the feature's testing. It has some really
cool testing functionality which was required for our use case.

2. Currently we have separate tests written for connection related features and channel
specific features. The relevant files are test/channels/concurrent_editing_channel_test.rb and
test/channels/connection_test.rb
67 changes: 34 additions & 33 deletions app/assets/javascripts/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,41 +12,42 @@
//


//= require leaflet/dist/leaflet-src.js
// = require leaflet/dist/leaflet-src.js

//= require jquery
//= require jquery-ujs
//= require jquery/dist/jquery.js
//= require jquery-ujs/src/rails.js
//= require jquery-ui/jquery-ui.min.js
// = require jquery
// = require jquery-ujs
// = require jquery/dist/jquery.js
// = require jquery-ujs/src/rails.js
// = require jquery-ui/jquery-ui.min.js

//= require blueimp-tmpl/js/tmpl.js
//= require blueimp-file-upload/js/vendor/jquery.ui.widget
//= require blueimp-file-upload/js/jquery.fileupload
//= require blueimp-file-upload/js/jquery.fileupload-process
//= require blueimp-file-upload/js/jquery.fileupload-ui
// = require blueimp-tmpl/js/tmpl.js
// = require blueimp-file-upload/js/vendor/jquery.ui.widget
// = require blueimp-file-upload/js/jquery.fileupload
// = require blueimp-file-upload/js/jquery.fileupload-process
// = require blueimp-file-upload/js/jquery.fileupload-ui

//= require bootstrap/dist/js/bootstrap.js
// = require bootstrap/dist/js/bootstrap.js

//= require leaflet-fullHash.js
//= require leaflet-providers/leaflet-providers.js
//= require leaflet-environmental-layers/dist/LeafletEnvironmentalLayers.js
//= require leaflet-environmental-layers/src/windRoseLayer.js
//= require leaflet-easybutton/src/easy-button.js
//= require sparklines/source/sparkline.js
//= require glfx-js/dist/glfx.js
//= require ion-rangeslider/js/ion.rangeSlider.js
//= require exif-js/exif.js
//= require webgl-distort/dist/webgl-distort.js
//= require mapknitter/core/Class.js
//= require leaflet-spin/example/spin/dist/spin.min.js
//= require leaflet-spin/example/leaflet.spin.min.js
//= require image-sequencer/dist/image-sequencer.js
//= require leaflet-toolbar/dist/leaflet.toolbar.js
//= require leaflet-draw/dist/leaflet.draw-src.js
//= require leaflet-distortableimage/dist/leaflet.distortableimage.js
//= require leaflet-illustrate/dist/Leaflet.Illustrate.js
//= require leaflet-distortableimage/src/edit/tools/EditAction.js
//= require mapknitter/Map.js
// = require leaflet-fullHash.js
// = require leaflet-providers/leaflet-providers.js
// = require leaflet-environmental-layers/dist/LeafletEnvironmentalLayers.js
// = require leaflet-environmental-layers/src/windRoseLayer.js
// = require leaflet-easybutton/src/easy-button.js
// = require sparklines/source/sparkline.js
// = require glfx-js/dist/glfx.js
// = require ion-rangeslider/js/ion.rangeSlider.js
// = require exif-js/exif.js
// = require webgl-distort/dist/webgl-distort.js
// = require mapknitter/core/Class.js
// = require leaflet-spin/example/spin/dist/spin.min.js
// = require leaflet-spin/example/leaflet.spin.min.js
// = require image-sequencer/dist/image-sequencer.js
// = require leaflet-toolbar/dist/leaflet.toolbar.js
// = require leaflet-draw/dist/leaflet.draw-src.js
// = require leaflet-illustrate/dist/Leaflet.Illustrate.js
// = require leaflet-distortableimage/dist/leaflet.distortableimage.js
// = require leaflet-distortableimage/src/edit/tools/EditAction.js
// = require mapknitter/Map.js
// = require cable.js

//= require_tree .
// = require_tree .
11 changes: 11 additions & 0 deletions app/assets/javascripts/cable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//
//= require action_cable
//= require_self
//= require_tree ./channels

(function() {
this.App || (this.App = {});

App.cable = ActionCable.createConsumer();

}).call(this);
26 changes: 26 additions & 0 deletions app/assets/javascripts/channels/concurrent_editing.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/* Handles all the frontend interactions with action cable and the server. */

App.concurrent_editing = App.cable.subscriptions.create("ConcurrentEditingChannel", {
connected: function() {
// Called when the subscription is ready for use on the server
},

disconnected: function() {
// Called when the subscription has been terminated by the server
},

received: function(data) {
// Called when there's incoming data on the websocket for this channel
window.mapKnitter.synchronizeData(data.changes);
},

speak: function(changes) {
/* Called when an image is updated from Map.js ('saveImage' function).
* This function calls concurrent_editing_channel.rb's 'sync' method
* which is responsible for broadcasting the updated warpables
* to all the user's connected to the concurrent_editing channel. */
return this.perform("sync", {
changes: changes
});
}
});
29 changes: 29 additions & 0 deletions app/assets/javascripts/mapknitter/Map.js
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,32 @@ MapKnitter.Map = MapKnitter.Class.extend({
if (this.editing._mode !== "lock") { e.stopPropagation(); }
},

/* Called by the concurrent_editing.js channel's 'received' function (app/assets/javascripts/channels/concurrent_editing.js).
* It recieves a list of updated warpables,i.e. list of images with updated corner points. The aim of writing this function
* is to reposition the updated images onto the map on every connected browser (via the ActionCable). */

synchronizeData: function(warpables) {
var layers = [];
map.eachLayer(function(l) {layers.push(l)});
layers = layers.filter(image => (image._url!=undefined || image._url!=null));
warpables.forEach(function(warpable) {
corners = [];
warpable.nodes.forEach(function(node) {
corners.push(L.latLng(node.lat, node.lon));
});

x = corners[2];
y = corners [3];
corners [2] = y;
corners [3] = x;

console.log(corners);

layer = layers.filter(l => l._url==warpable.srcmedium)[0];
layer.setCorners(corners);
});
},

saveImageIfChanged: function () {
var img = this,
edit = img.editing;
Expand Down Expand Up @@ -424,6 +450,9 @@ MapKnitter.Map = MapKnitter.Class.extend({
beforeSend: function (e) {
$('.mk-save').removeClass('fa-check-circle fa-times-circle fa-green fa-red').addClass('fa-spinner fa-spin')
},
success: function(data) {
App.concurrent_editing.speak(data);
},
complete: function (e) {
$('.mk-save').removeClass('fa-spinner fa-spin').addClass('fa-check-circle fa-green')
},
Expand Down
4 changes: 4 additions & 0 deletions app/channels/application_cable/channel.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module ApplicationCable
class Channel < ActionCable::Channel::Base
end
end
17 changes: 17 additions & 0 deletions app/channels/application_cable/connection.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user

def connect
self.current_user = find_verified_user
end

private

def find_verified_user
User.find(cookies.signed["user_id"])
rescue ActiveRecord::RecordNotFound
reject_unauthorized_connection
end
end
end
17 changes: 17 additions & 0 deletions app/channels/concurrent_editing_channel.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
class ConcurrentEditingChannel < ApplicationCable::Channel
# This class handles the server side logic of the actioncable communication.

def subscribed
# Called first to connect user to the channel.
stream_from "concurrent_editing_channel"
end

def unsubscribed
# Any cleanup needed when channel is unsubscribed
end

def sync(changes)
# Responsible for broadcasting the updated warpables or simply images to the user's connected on this channel.
ActionCable.server.broadcast 'concurrent_editing_channel', changes
end
end
4 changes: 3 additions & 1 deletion app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ def current_user
user_id = session[:user_id]
if user_id
begin
@user = User.find(user_id)
u = User.find(user_id)
cookies.signed["user_id"] = u.id
@user = u
rescue StandardError
@user = nil
end
Expand Down
6 changes: 5 additions & 1 deletion app/controllers/images_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,11 @@ def update
@warpable.locked = params[:locked]
@warpable.cm_per_pixel = @warpable.get_cm_per_pixel
@warpable.save
render html: 'success'

respond_to do |format|
format.html { render html: 'success' }
format.json { render json: @warpable.map.fetch_map_data }
end
else
render plain: 'You must be logged in to update the image, unless the map is anonymous.'
end
Expand Down
6 changes: 6 additions & 0 deletions app/models/map.rb
Original file line number Diff line number Diff line change
Expand Up @@ -273,4 +273,10 @@ def add_tag(tagname, user)
tagname = tagname.downcase
tags.create(name: tagname, user_id: user.id, map_id: id) unless has_tag(tagname)
end

def fetch_map_data
# fetches a list of updated warpables along with their corners in a json format.
data = warpables
data.to_json
end
end
1 change: 1 addition & 0 deletions app/views/layouts/application.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<link rel="shortcut icon" href="/images/mapknitter-255.png">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.2/css/all.css" integrity="sha384-oS3vJWv+0UjzBfQzYUhtDYW+Pj2yciDJxpsK1OYPAYjqT085Qq/1cq5FLXAZQ7Ay" crossorigin="anonymous">
<%= stylesheet_link_tag 'application' %>
<%= action_cable_meta_tag %>
<%= javascript_include_tag 'application' %>
</head>
<body>
Expand Down
3 changes: 3 additions & 0 deletions config/environments/development.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,7 @@
# Use an evented file watcher to asynchronously detect changes in source code,
# routes, locales, etc. This feature depends on the listen gem.
config.file_watcher = ActiveSupport::EventedFileUpdateChecker

config.action_cable.url = "ws://localhost:3000/cable"
config.action_cable.allowed_request_origins = [/http:\/\/*/, /https:\/\/*/]
end
Loading

0 comments on commit 843a719

Please sign in to comment.