Setting Action Cable (#956)

* Configurations update for rails 4.0

* ActiveSupport::Testing::Performance extracted to a gem

* ruby prof required as a dependency

* disable rubocop on bin folder

* http patch

* Comment out to allow testing

* no longer supports plugin loading

* lock to sprockets 2.12

* Active record patches

* remove deprecated test syntax

* fix failing tests

* change new super class

* replace right_aws with right_aws_api

right_aws is no longer maintained, was throwing an error

* lock to rails 4.2.11.1

* change rails version in install script

* remove deprecation warnings

* Change test lib to minitest, add  minitest reporters

* make app work

* active record find patches

* root_in_json include defaulted to false

* confirm option removed in link helper

* cookies serializer changed to hybrid

* Change render :text to :plain

render :text will be deprecated and poses a security risk

* console for dev web

* Check and fix interface functionality

* fix export functionality

* add protected attributes for the warpable model

* fix image upload

* Fix comments and images failing tests

* include mass assignment security in annotations

* render html for update images

* clear mail array before every test

* Fix codeclimate issues

* skip failing test

The test is failing because of different names in model(warpable) and
controller(images) skipping this for now until we decide if we want to
standardize the names

* replace unprotected redirects

* Fix codeclimate issues

* Autofixing rubocop offenses and Problematic test for #578 (#667)

* Autofixing rubocop offenses

* Adding Rubocop to Travis pipelines and development branch

* Fixing maps controller test

* Fixing remaining offenses

* change post test to create since action new is a get action

* remove unprotected redirects

* Fix codeclimate issues

* Add yarn

* Remove error log

* Fix oauth icons

* Modify package.json

* Add yarn install to start.sh

* Add leaflet google

* Remove leaflet-google from package json

* remove passenger error logs

* Fix install script

* Remove flag

* Fix gemfile.lock

* Fix gemfile.lock

* Fix login

* Upgrade Gemfile to Rails 5.0

* require rake'

* Change config files

* Add application record

* Bump mysql

* Local builds for Travis runners (#672)

* Using local mysql for travis

* Fix codeclimate issues

* Refactoring yamls

* Autofixing rubocop offenses and Problematic test for #578 (#667)

* Autofixing rubocop offenses

* Adding Rubocop to Travis pipelines and development branch

* Fixing maps controller test

* Fixing remaining offenses

* Fixing unit tests

* Adding docker build to travis pipelines

* Adding docker build to travis pipelines

* Staging builds in travis

* fix travis.yml

* Upgrading sintax of assets and using required gems

* Using updated version of GDAL and installing required dependencies

* Enabling cache in between builds

* Test yarn for travis

* Conditionalize rake db:setup for travis

* Add semicolon

* Remove comment

* modify database.yml

* Migrate seperately

* Run create only for production

* Locking newer Rails v

* Running update task

* Adding missing bootsnap gem

* Fixing missing database

* Adding missing listen gem

* Fixing schema example version

* Fixing migration versions

* Updating dependencies for Rails 5

* Adding ApplicationMailer abstraction

* Adding required initializers

* Prefer require_relative instead of full path

* Making associations not required by default

* Regenerating schema file

* Hotfixing MassAssigment

* Upgrade web-console, remove mysql adapter override

* Remove attr_accessible

* Add rails-controller-testing gem

* Follow new syntax for tests

* Remove extra web-console from gemfile

* Regenerating lock file

* Fixing rubocop offenses and bundler version

* Using correct Paperclip class as in https://github.com/rails/rails/issues/26404#issuecomment-502129936

* Fix images functional tests

* images controller test typecast to string

* Fix rails logger

* Fix map tests

* Bumping rubocop version

* Autofixing rubocop offenses

* Including performance cop to rubocop

* Refactoring deprecated routing and secret_token

* Fixing routes for feed controller

* Using correct routes for RSS builder

* Fixing missing routes

* Fixing travis bundler and yarn cache

* Fixing bundle path

* Splitting bundle and yarn verifications

* Autofixing rubocop offenses

* Fixing prod host for travis, private class usage and rubocop offenses

* Upgrade to Rails 5.2 (#685)

* Upgrade Gemfile to Rails 5.0

* require rake'

* Change config files

* Add application record

* Bump mysql

* Locking newer Rails v

* Running update task

* Adding missing bootsnap gem

* Fixing missing database

* Adding missing listen gem

* Fixing schema example version

* Fixing migration versions

* Updating dependencies for Rails 5

* Adding ApplicationMailer abstraction

* Adding required initializers

* Prefer require_relative instead of full path

* Making associations not required by default

* Regenerating schema file

* Hotfixing MassAssigment

* Upgrade web-console, remove mysql adapter override

* Remove attr_accessible

* Add rails-controller-testing gem

* Follow new syntax for tests

* Remove extra web-console from gemfile

* Regenerating lock file

* Fixing rubocop offenses and bundler version

* Using correct Paperclip class as in https://github.com/rails/rails/issues/26404#issuecomment-502129936

* Fix images functional tests

* images controller test typecast to string

* Fix rails logger

* Fix map tests

* Bumping rubocop version

* Autofixing rubocop offenses

* Including performance cop to rubocop

* Refactoring deprecated routing and secret_token

* Fixing routes for feed controller

* Using correct routes for RSS builder

* Fixing missing routes

* Fixing travis bundler and yarn cache

* Fixing bundle path

* Splitting bundle and yarn verifications

* Fixing prod host for travis, private class usage and rubocop offenses

* Enforcing params usage on get method

* Using correct folder names to Rails >5 conventions

* Enforcing params wrapping and adding missing front_ui route

* Precompiling assets before serving

* Improving Jenkins startup script

* WIP action cable setup

* basic action cable setup complete

* minor change

* minor changes

* few changes

* Using supported docker yaml version by Jenkins

* Adding task to check database existance

* Improving start script

* Improving Makefile's recipes and target

* Adding task to check database existance

* Improving start script

* Improving Makefile's recipes and target

* Improving Makefile's recipes and target

* Patching https://github.com/publiclab/mapknitter/pull/803

* Improving Jenkins setup

* initial working functionality complete

* Fix map loading

* h

* Fixing Leaflet-Environmental-Layers map loading

* h

* leaflet

* fix

* change

* updates

* stop precompiling assets

* precompile

* Using correct Yarn, NPM and Node version, avoiding mismatch

* Removing unwanted tags.js invocation

* Improving Makefile recipe

* Using node_modules/ as dependencies folder, since https://github.com/sass/node-sass/issues/2050#issuecomment-317233552

* Upgrading Yarn dependencies

* Removing duplicate rubocop directive

* Removing test/ from codeclimate checks

* Removing fixed FIXME comments

* Removing fixed FIXME comments

* Updating docs in README

* Refactoring code

* Bumping recaptcha and include methods

* 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

* Fixing migration version

* Using strong params in requests

* Using strong params in requests

* Use Rack::Test::UploadedFile instead of ActionDispatch::Http::UploadedFile

* added documentation

* added more docs

* added tests

* Fix minor asset issue

* Remove manual asset references and add them to application js

* Fix asset ordering in application.js

* Configure System tests  (#936)

* Add new system tests and fix minor asset loading

* modify test

* Add chromedriver to travis

* Add sudo

* Add dependencies to dockerfile

* Properly installing chrome and chromedriver

* Using puma as dependency and correct image controller

* added a few tests

* a few changes

* remove unnecessary render

* few test fixes

* action cable setup (#805)

* 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

* Fixing CodeClimate issues

* Synch editing add ons (#957)

* few bug fixes

* separate editing channels for different maps

* test fixes

* rubocop fixes

* Undoing unwanted pattern set by Rubocop
This commit is contained in:
Álax de Carvalho Alves
2019-08-30 17:19:26 -03:00
committed by Jeffrey Warren
parent 1ac63abb67
commit cee7479cf4
27 changed files with 397 additions and 23 deletions

View File

@@ -28,6 +28,7 @@ AllCops:
- 'public/**/*' - 'public/**/*'
- 'Dangerfile' - 'Dangerfile'
- 'app/views/**/*' - 'app/views/**/*'
- 'app/assets/javascripts/application.js'
TargetRubyVersion: '2.4' TargetRubyVersion: '2.4'
Layout/MultilineMethodCallIndentation: Layout/MultilineMethodCallIndentation:

View File

@@ -26,6 +26,7 @@ group :dependencies do
gem 'bootsnap', '~> 1.4.4' gem 'bootsnap', '~> 1.4.4'
gem 'turbolinks', '~> 5' gem 'turbolinks', '~> 5'
gem 'mini_magick', '~> 4.8' gem 'mini_magick', '~> 4.8'
gem 'puma', '~> 4.1.0'
# if you use amazon s3 for warpable image storage # if you use amazon s3 for warpable image storage
gem 'aws-sdk', '~> 1.5.7' gem 'aws-sdk', '~> 1.5.7'
@@ -37,12 +38,15 @@ group :dependencies do
# compiling markdown to html # compiling markdown to html
gem 'rdiscount', '2.2.0.1' gem 'rdiscount', '2.2.0.1'
# Process manager for applications with multiple components
gem 'foreman', '~> 0.85.0'
# asset pipelining # asset pipelining
gem 'bootstrap-sass' gem 'bootstrap-sass'
gem 'sassc-rails' gem 'sassc-rails'
gem 'jquery-rails' gem 'jquery-rails'
gem 'sprockets', '3.7.2' gem 'sprockets', '3.7.2'
gem "sprockets-rails" gem 'sprockets-rails'
gem 'sass', require: 'sass' gem 'sass', require: 'sass'
gem 'autoprefixer-rails', '~> 9.6.1' gem 'autoprefixer-rails', '~> 9.6.1'
gem 'uglifier', '~> 4.1.20' gem 'uglifier', '~> 4.1.20'
@@ -65,11 +69,11 @@ end
group :development, :test do group :development, :test do
gem 'capybara' gem 'capybara'
gem 'puma'
gem 'selenium-webdriver' gem 'selenium-webdriver'
gem 'byebug', '~> 11.0.1', platforms: [:mri, :mingw, :x64_mingw] gem 'byebug', '~> 11.0.1', platforms: [:mri, :mingw, :x64_mingw]
gem 'faker', '~> 2.1.2' gem 'faker', '~> 2.1.2'
gem 'pry-rails', '~> 0.3.9' gem 'pry-rails', '~> 0.3.9'
gem 'action-cable-testing'
end end
group :development do group :development do

View File

@@ -4,6 +4,8 @@ GEM
RubyInline (3.12.4) RubyInline (3.12.4)
ZenTest (~> 4.3) ZenTest (~> 4.3)
ZenTest (4.11.2) ZenTest (4.11.2)
action-cable-testing (0.6.0)
actioncable (>= 5.0)
actioncable (5.2.3) actioncable (5.2.3)
actionpack (= 5.2.3) actionpack (= 5.2.3)
nio4r (~> 2.0) nio4r (~> 2.0)
@@ -89,6 +91,8 @@ GEM
faker (2.1.2) faker (2.1.2)
i18n (>= 0.8) i18n (>= 0.8)
ffi (1.11.1) ffi (1.11.1)
foreman (0.85.0)
thor (~> 0.19.1)
friendly_id (5.2.5) friendly_id (5.2.5)
activerecord (>= 4.0.0) activerecord (>= 4.0.0)
geokit (1.13.1) geokit (1.13.1)
@@ -210,7 +214,7 @@ GEM
rails-dom-testing (2.0.3) rails-dom-testing (2.0.3)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
nokogiri (>= 1.6) nokogiri (>= 1.6)
rails-html-sanitizer (1.1.0) rails-html-sanitizer (1.2.0)
loofah (~> 2.2, >= 2.2.2) loofah (~> 2.2, >= 2.2.2)
rails-perftest (0.0.7) rails-perftest (0.0.7)
railties (5.2.3) railties (5.2.3)
@@ -261,9 +265,8 @@ GEM
sass-listen (4.0.0) sass-listen (4.0.0)
rb-fsevent (~> 0.9, >= 0.9.4) rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7) rb-inotify (~> 0.9, >= 0.9.7)
sassc (2.0.1) sassc (2.1.0)
ffi (~> 1.9) ffi (~> 1.9)
rake
sassc-rails (2.1.2) sassc-rails (2.1.2)
railties (>= 4.0.0) railties (>= 4.0.0)
sassc (>= 2.0) sassc (>= 2.0)
@@ -296,7 +299,7 @@ GEM
sqlite3 (1.4.1) sqlite3 (1.4.1)
terrapin (0.6.0) terrapin (0.6.0)
climate_control (>= 0.0.3, < 1.0) climate_control (>= 0.0.3, < 1.0)
thor (0.20.3) thor (0.19.4)
thread_safe (0.3.6) thread_safe (0.3.6)
tilt (2.0.9) tilt (2.0.9)
turbolinks (5.2.0) turbolinks (5.2.0)
@@ -330,7 +333,8 @@ PLATFORMS
DEPENDENCIES DEPENDENCIES
RubyInline (~> 3.12.4) RubyInline (~> 3.12.4)
autoprefixer-rails (~> 9.6.1) action-cable-testing
autoprefixer-rails (~> 9.5.1.1)
aws-sdk (~> 1.5.7) aws-sdk (~> 1.5.7)
bootsnap (~> 1.4.4) bootsnap (~> 1.4.4)
bootstrap-sass bootstrap-sass
@@ -338,6 +342,7 @@ DEPENDENCIES
capybara capybara
codecov codecov
faker (~> 2.1.2) faker (~> 2.1.2)
foreman (~> 0.85.0)
friendly_id friendly_id
geokit-rails (= 1.1.4) geokit-rails (= 1.1.4)
httparty httparty
@@ -357,7 +362,7 @@ DEPENDENCIES
passenger passenger
popper_js (~> 1.11, >= 1.11.1) popper_js (~> 1.11, >= 1.11.1)
pry-rails (~> 0.3.9) pry-rails (~> 0.3.9)
puma puma (~> 4.1.0)
rack_session_access rack_session_access
rails (~> 5.2.3) rails (~> 5.2.3)
rails-controller-testing rails-controller-testing

2
Procfile Normal file
View File

@@ -0,0 +1,2 @@
passenger: passenger start
puma: puma -C config/puma.rb

30
SYNCHRONOUS_EDITING.md Normal file
View File

@@ -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

View File

@@ -17,7 +17,7 @@
//= require jquery //= require jquery
//= require jquery-ujs //= require jquery-ujs
//= require jquery/dist/jquery.js //= require jquery/dist/jquery.js
//= require jquery-ujs/src/rails.js //= require jquery-ujs/src/rails.js
//= require jquery-ui/jquery-ui.min.js //= require jquery-ui/jquery-ui.min.js
//= require blueimp-tmpl/js/tmpl.js //= require blueimp-tmpl/js/tmpl.js
@@ -44,9 +44,10 @@
//= require image-sequencer/dist/image-sequencer.js //= require image-sequencer/dist/image-sequencer.js
//= require leaflet-toolbar/dist/leaflet.toolbar.js //= require leaflet-toolbar/dist/leaflet.toolbar.js
//= require leaflet-draw/dist/leaflet.draw-src.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-illustrate/dist/Leaflet.Illustrate.js
//= require leaflet-distortableimage/dist/leaflet.distortableimage.js
//= require leaflet-distortableimage/src/edit/tools/EditAction.js //= require leaflet-distortableimage/src/edit/tools/EditAction.js
//= require mapknitter/Map.js //= require mapknitter/Map.js
//= require cable.js
//= require_tree . //= require_tree .

View File

@@ -0,0 +1,11 @@
//
//= require action_cable
//= require_self
//= require_tree ./channels
(function() {
this.App || (this.App = {});
App.cable = ActionCable.createConsumer();
}).call(this);

View File

@@ -0,0 +1,31 @@
/* Handles all the frontend interactions with action cable and the server. */
App.concurrent_editing = App.cable.subscriptions.create(
{
channel: "ConcurrentEditingChannel",
mapSlug: window.location.href.split("/").pop()
}, {
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,
map_slug: window.location.href.split("/").pop()
});
}
});

View File

@@ -386,6 +386,144 @@ MapKnitter.Map = MapKnitter.Class.extend({
if (this.editing._mode !== "lock") { e.stopPropagation(); } 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;
layer = layers.filter(l => l._url==warpable.srcmedium)[0];
if(layer == null || layer == undefined) {
window.mapknitter.synchronizeNewAddedImage(warpable);
} else {
layer.setCorners(corners);
var index = layers.indexOf(layer);
if (index > -1) {
layers.splice(index, 1);
}
}
});
// remove images if deleted from any user's browser
layers.forEach(function(layer) {
edit = layer.editing
edit._removeToolbar();
edit.disable();
// remove from Leaflet map:
map.removeLayer(layer);
// remove from sidebar too:
$('#warpable-' + layer.warpable_id).remove();
});
},
synchronizeNewAddedImage: function(warpable) {
var wn = warpable.nodes;
bounds = [];
// only already-placed images:
if (wn.length > 0) {
var downloadEl = $('.img-download-' + warpable.id),
imgEl = $('#full-img-' + warpable.id);
downloadEl.click(function () {
downloadEl.html('<i class="fa fa-circle-o-notch fa-spin"></i>');
imgEl[0].onload = function () {
var height = imgEl.height(),
width = imgEl.width(),
nw = map.latLngToContainerPoint(wn[0]),
ne = map.latLngToContainerPoint(wn[1]),
se = map.latLngToContainerPoint(wn[2]),
sw = map.latLngToContainerPoint(wn[3]),
offsetX = nw.x,
offsetY = nw.y,
displayedWidth = $('#warpable-img-' + warpable.id).width(),
ratio = width / displayedWidth;
nw.x -= offsetX;
ne.x -= offsetX;
se.x -= offsetX;
sw.x -= offsetX;
nw.y -= offsetY;
ne.y -= offsetY;
se.y -= offsetY;
sw.y -= offsetY;
warpWebGl(
'full-img-' + warpable.id,
[0, 0, width, 0, width, height, 0, height],
[nw.x, nw.y, ne.x, ne.y, se.x, se.y, sw.x, sw.y],
true // trigger download
)
downloadEl.html('<i class="fa fa-download"></i>');
}
imgEl[0].src = $('.img-download-' + warpable.id).attr('data-image');
});
var corners = [
L.latLng(wn[0].lat, wn[0].lon),
L.latLng(wn[1].lat, wn[1].lon),
L.latLng(wn[3].lat, wn[3].lon),
L.latLng(wn[2].lat, wn[2].lon)
];
var img = L.distortableImageOverlay(warpable.srcmedium, {
corners: corners,
mode: 'lock'
}).addTo(map);
var customExports = mapknitter.customExportAction();
var imgGroup = L.distortableCollection({
actions: [customExports]
}).addTo(map);
imgGroup.addLayer(img);
/**
* TODO: toolbar may still appear outside of frame. Create a getter for toolbar corners in LDI and then include them in this calculation
*/
bounds = bounds.concat(corners);
var newImgBounds = L.latLngBounds(corners);
if (!map._initialBounds.contains(newImgBounds) && !map._initialBounds.equals(newImgBounds)) {
map._initialBounds.extend(newImgBounds);
mapknitter._map.flyToBounds(map._initialBounds);
}
images.push(img);
img.warpable_id = warpable.id;
if (!mapknitter.readOnly) {
L.DomEvent.on(img._image, {
click: mapknitter.selectImage,
dblclick: mapknitter.dblClickImage,
load: mapknitter.setupToolbar
}, img);
L.DomEvent.on(imgGroup, 'layeradd', mapknitter.setupEvents, img);
}
img.editing.disable()
}
},
saveImageIfChanged: function () { saveImageIfChanged: function () {
var img = this, var img = this,
edit = img.editing; edit = img.editing;
@@ -407,10 +545,8 @@ MapKnitter.Map = MapKnitter.Class.extend({
saveImage: function () { saveImage: function () {
var img = this; var img = this;
// reset change state string: img._corner_state = JSON.stringify(img._corners); // reset change state string:
img._corner_state = JSON.stringify(img._corners); $.ajax('/images/'+img.warpable_id, { // send save request
// send save request
$.ajax('/images', {
type: 'PATCH', type: 'PATCH',
data: { data: {
warpable_id: img.warpable_id, warpable_id: img.warpable_id,
@@ -424,6 +560,9 @@ MapKnitter.Map = MapKnitter.Class.extend({
beforeSend: function (e) { beforeSend: function (e) {
$('.mk-save').removeClass('fa-check-circle fa-times-circle fa-green fa-red').addClass('fa-spinner fa-spin') $('.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) { complete: function (e) {
$('.mk-save').removeClass('fa-spinner fa-spin').addClass('fa-check-circle fa-green') $('.mk-save').removeClass('fa-spinner fa-spin').addClass('fa-check-circle fa-green')
}, },
@@ -447,6 +586,9 @@ MapKnitter.Map = MapKnitter.Class.extend({
beforeSend: function (e) { beforeSend: function (e) {
$('.mk-save').removeClass('fa-check-circle fa-times-circle fa-green fa-red').addClass('fa-spinner fa-spin') $('.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) { complete: function (e) {
$('.mk-save').removeClass('fa-spinner fa-spin').addClass('fa-check-circle fa-green') $('.mk-save').removeClass('fa-spinner fa-spin').addClass('fa-check-circle fa-green')
// disable interactivity: // disable interactivity:

View File

@@ -0,0 +1,4 @@
module ApplicationCable
class Channel < ActionCable::Channel::Base
end
end

View File

@@ -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

View File

@@ -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:#{params[:mapSlug]}"
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['map_slug']}", changes
end
end

View File

@@ -20,7 +20,9 @@ class ApplicationController < ActionController::Base
user_id = session[:user_id] user_id = session[:user_id]
if user_id if user_id
begin begin
@user = User.find(user_id) u = User.find(user_id)
cookies.signed["user_id"] = u.id
@user = u
rescue StandardError rescue StandardError
@user = nil @user = nil
end end

View File

@@ -72,8 +72,8 @@ class ImagesController < ApplicationController
def update def update
@warpable = Warpable.find params[:warpable_id] @warpable = Warpable.find params[:warpable_id]
map = Map.find(@warpable.map_id)
if map.anonymous? || logged_in? if Map.find(@warpable.map_id).anonymous? || logged_in?
nodes = [] nodes = []
author = @warpable.map.author author = @warpable.map.author
# is it really necessary to make new points each time? # is it really necessary to make new points each time?
@@ -92,7 +92,11 @@ class ImagesController < ApplicationController
@warpable.locked = params[:locked] @warpable.locked = params[:locked]
@warpable.cm_per_pixel = @warpable.get_cm_per_pixel @warpable.cm_per_pixel = @warpable.get_cm_per_pixel
@warpable.save @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 else
render plain: 'You must be logged in to update the image, unless the map is anonymous.' render plain: 'You must be logged in to update the image, unless the map is anonymous.'
end end
@@ -111,7 +115,7 @@ class ImagesController < ApplicationController
@warpable.destroy @warpable.destroy
respond_to do |format| respond_to do |format|
format.html { redirect_to @warpable.map } format.html { redirect_to @warpable.map }
format.json { render json: @warpable } format.json { render json: @warpable.map.fetch_map_data }
end end
else else
flash[:error] = 'You must be logged in to delete images.' flash[:error] = 'You must be logged in to delete images.'

View File

@@ -273,4 +273,10 @@ class Map < ApplicationRecord
tagname = tagname.downcase tagname = tagname.downcase
tags.create(name: tagname, user_id: user.id, map_id: id) unless has_tag(tagname) tags.create(name: tagname, user_id: user.id, map_id: id) unless has_tag(tagname)
end 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 end

View File

@@ -13,6 +13,7 @@
<link rel="shortcut icon" href="/images/mapknitter-255.png"> <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"> <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' %> <%= stylesheet_link_tag 'application' %>
<%= action_cable_meta_tag %>
<%= javascript_include_tag 'application' %> <%= javascript_include_tag 'application' %>
</head> </head>
<body> <body>

View File

@@ -74,4 +74,7 @@ Rails.application.configure do
# Use an evented file watcher to asynchronously detect changes in source code, # Use an evented file watcher to asynchronously detect changes in source code,
# routes, locales, etc. This feature depends on the listen gem. # routes, locales, etc. This feature depends on the listen gem.
config.file_watcher = ActiveSupport::EventedFileUpdateChecker config.file_watcher = ActiveSupport::EventedFileUpdateChecker
config.action_cable.url = "ws://localhost:3000/cable"
config.action_cable.allowed_request_origins = [/http:\/\/*/, /https:\/\/*/]
end end

View File

@@ -103,4 +103,6 @@ Rails.application.configure do
# Do not dump schema after migrations. # Do not dump schema after migrations.
config.active_record.dump_schema_after_migration = false config.active_record.dump_schema_after_migration = false
config.action_cable.allowed_request_origins = [/http:\/\/*/, /https:\/\/*/]
end end

View File

@@ -11,7 +11,8 @@ Rails.application.configure do
config.assets.paths << Rails.root.join('node_modules') config.assets.paths << Rails.root.join('node_modules')
config.assets.precompile << /\.(?:svg|eot|woff|ttf)\z/ config.assets.precompile << /\.(?:svg|eot|woff|ttf)\z/
config.assets.precompile += ['uploads.js', config.assets.precompile += [
'uploads.js',
'knitter.js', 'knitter.js',
'annotations.js', 'annotations.js',
'maps.js', 'maps.js',

5
config/puma.rb Normal file
View File

@@ -0,0 +1,5 @@
#!/usr/bin/env puma
environment ENV.fetch("RAILS_ENV") { "production" }
pidfile '/app/tmp/pids/puma.pid'

View File

@@ -2,6 +2,8 @@ Mapknitter::Application.routes.draw do
root to: 'front_ui#index' root to: 'front_ui#index'
mount ActionCable.server => '/cable'
get 'front-page', to: 'front_ui#index' get 'front-page', to: 'front_ui#index'
get 'mappers', to: 'front_ui#nearby_mappers' get 'mappers', to: 'front_ui#nearby_mappers'
get 'about', to: 'front_ui#about' get 'about', to: 'front_ui#about'

View File

@@ -24,4 +24,4 @@ if [ -f $pidfile ] ; then
rm $pidfile; rm $pidfile;
fi fi
bundle exec passenger start bundle exec foreman start

View File

@@ -0,0 +1,23 @@
require 'test_helper'
module ApplicationCable
class ConcurrentEditingChannelTest < ActionCable::Channel::TestCase
def test_synch_editing_broadcast_count
channel_name = "concurrent_editing_channel"
assert_broadcasts channel_name, 0
ActionCable.server.broadcast channel_name, data: {}
assert_broadcasts channel_name, 1
end
def test_synch_editing_broadcast_message
channel_name = "concurrent_editing_channel"
changes = { :image_change => "test" }
ActionCable.server.broadcast channel_name, data: changes
assert_broadcast_on(channel_name, data: changes) do
ActionCable.server.broadcast channel_name, data: changes
end
end
end
end

View File

@@ -0,0 +1,31 @@
require 'test_helper'
module ApplicationCable
class ConnectionTest < ActionCable::Connection::TestCase
def test_connection_with_user
cookies.signed["user_id"] = users(:chris)
#this simulates the connection
connect
# assert connected user
assert_equal "chris", connection.current_user.login
end
def test_does_not_connect_without_user
# user not logged in
begin
# trying to connect but fails
connect
rescue Exception=>e
#compare the error class
assert_equal e.class, ActionCable::Connection::Authorization::UnauthorizedError
end
#check is connection is nil(which it should be)
assert_equal nil, connection
end
end
end

View File

@@ -34,7 +34,7 @@ nairobi:
lat: -1.2920659 lat: -1.2920659
lon: 36.8219462 lon: 36.8219462
description: Capital of Kenya description: Capital of Kenya
created_at: <% Time.now %> created_at: <%= Time.now %>
license: publicdomain license: publicdomain
user_id: 2 user_id: 2
author: aaron author: aaron
@@ -47,7 +47,7 @@ village:
lat: -0.023559 lat: -0.023559
lon: 37.90619300000003 lon: 37.90619300000003
description: A mall in Nairobi description: A mall in Nairobi
created_at: <% Time.now %> created_at: <%= Time.now %>
license: publicdomain license: publicdomain
user_id: 2 user_id: 2
author: aaron author: aaron

View File

@@ -56,3 +56,16 @@ four:
way_order: 0 way_order: 0
body: nil body: nil
five:
id: 5
color: "black"
author: "anonymous"
lat: 42.8377535388083
lon: -75.3981708900972
way_id: 0
order: 0
name: ""
description: ""
map_id: 1
way_order: 0
body: nil

View File

@@ -0,0 +1,16 @@
require 'application_system_test_case'
class SynchronousTest < ApplicationSystemTestCase
setup do
Capybara.current_driver = Capybara.javascript_driver
Capybara.asset_host = "http://localhost:3000"
end
test 'warpables change flow' do
map = maps(:saugus)
original_data = map.fetch_map_data
map.warpables.first.update_column(:nodes, "2,5,1,3")
updated_data = map.fetch_map_data
assert_not_equal updated_data, original_data
end
end