From cee7479cf4f0323a262b931a37cc72215ca48273 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lax=20de=20Carvalho=20Alves?= Date: Fri, 30 Aug 2019 17:19:26 -0300 Subject: [PATCH] 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 --- .rubocop.yml | 1 + Gemfile | 8 +- Gemfile.lock | 17 +- Procfile | 2 + SYNCHRONOUS_EDITING.md | 30 ++++ app/assets/javascripts/application.js | 5 +- app/assets/javascripts/cable.js | 11 ++ .../channels/concurrent_editing.js | 31 ++++ app/assets/javascripts/mapknitter/Map.js | 150 +++++++++++++++++- app/channels/application_cable/channel.rb | 4 + app/channels/application_cable/connection.rb | 17 ++ app/channels/concurrent_editing_channel.rb | 17 ++ app/controllers/application_controller.rb | 4 +- app/controllers/images_controller.rb | 12 +- app/models/map.rb | 6 + app/views/layouts/application.html.erb | 1 + config/environments/development.rb | 3 + config/environments/production.rb | 2 + config/initializers/assets.rb | 3 +- config/puma.rb | 5 + config/routes.rb | 2 + start.sh | 2 +- .../concurrent_editing_channel_test.rb | 23 +++ test/channels/connection_test.rb | 31 ++++ test/fixtures/maps.yml | 4 +- test/fixtures/nodes.yml | 13 ++ test/system/synchronous_editing_test.rb | 16 ++ 27 files changed, 397 insertions(+), 23 deletions(-) create mode 100644 Procfile create mode 100644 SYNCHRONOUS_EDITING.md create mode 100644 app/assets/javascripts/cable.js create mode 100644 app/assets/javascripts/channels/concurrent_editing.js create mode 100644 app/channels/application_cable/channel.rb create mode 100644 app/channels/application_cable/connection.rb create mode 100644 app/channels/concurrent_editing_channel.rb create mode 100644 config/puma.rb create mode 100644 test/channels/concurrent_editing_channel_test.rb create mode 100644 test/channels/connection_test.rb create mode 100644 test/system/synchronous_editing_test.rb diff --git a/.rubocop.yml b/.rubocop.yml index 7312570c..5e2f5d28 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -28,6 +28,7 @@ AllCops: - 'public/**/*' - 'Dangerfile' - 'app/views/**/*' + - 'app/assets/javascripts/application.js' TargetRubyVersion: '2.4' Layout/MultilineMethodCallIndentation: diff --git a/Gemfile b/Gemfile index 85756023..865588ff 100644 --- a/Gemfile +++ b/Gemfile @@ -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' @@ -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.6.1' gem 'uglifier', '~> 4.1.20' @@ -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 diff --git a/Gemfile.lock b/Gemfile.lock index bc71709b..bf517b6d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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) @@ -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) @@ -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) @@ -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) @@ -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) @@ -330,7 +333,8 @@ PLATFORMS DEPENDENCIES RubyInline (~> 3.12.4) - autoprefixer-rails (~> 9.6.1) + action-cable-testing + autoprefixer-rails (~> 9.5.1.1) aws-sdk (~> 1.5.7) bootsnap (~> 1.4.4) bootstrap-sass @@ -338,6 +342,7 @@ DEPENDENCIES capybara codecov faker (~> 2.1.2) + foreman (~> 0.85.0) friendly_id geokit-rails (= 1.1.4) httparty @@ -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 diff --git a/Procfile b/Procfile new file mode 100644 index 00000000..841da413 --- /dev/null +++ b/Procfile @@ -0,0 +1,2 @@ +passenger: passenger start +puma: puma -C config/puma.rb diff --git a/SYNCHRONOUS_EDITING.md b/SYNCHRONOUS_EDITING.md new file mode 100644 index 00000000..66819275 --- /dev/null +++ b/SYNCHRONOUS_EDITING.md @@ -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 \ No newline at end of file diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index dc9abfe1..88e685b6 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -17,7 +17,7 @@ //= require jquery //= require jquery-ujs //= 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 blueimp-tmpl/js/tmpl.js @@ -44,9 +44,10 @@ //= 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/dist/leaflet.distortableimage.js //= require leaflet-distortableimage/src/edit/tools/EditAction.js //= require mapknitter/Map.js +//= require cable.js //= require_tree . diff --git a/app/assets/javascripts/cable.js b/app/assets/javascripts/cable.js new file mode 100644 index 00000000..f613e17f --- /dev/null +++ b/app/assets/javascripts/cable.js @@ -0,0 +1,11 @@ +// +//= require action_cable +//= require_self +//= require_tree ./channels + +(function() { + this.App || (this.App = {}); + + App.cable = ActionCable.createConsumer(); + +}).call(this); diff --git a/app/assets/javascripts/channels/concurrent_editing.js b/app/assets/javascripts/channels/concurrent_editing.js new file mode 100644 index 00000000..87202278 --- /dev/null +++ b/app/assets/javascripts/channels/concurrent_editing.js @@ -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() + }); + } +}); diff --git a/app/assets/javascripts/mapknitter/Map.js b/app/assets/javascripts/mapknitter/Map.js index 0a43a103..a7051a25 100644 --- a/app/assets/javascripts/mapknitter/Map.js +++ b/app/assets/javascripts/mapknitter/Map.js @@ -386,6 +386,144 @@ 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; + + 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(''); + + 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(''); + } + + 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 () { var img = this, edit = img.editing; @@ -407,10 +545,8 @@ MapKnitter.Map = MapKnitter.Class.extend({ saveImage: function () { var img = this; - // reset change state string: - img._corner_state = JSON.stringify(img._corners); - // send save request - $.ajax('/images', { + img._corner_state = JSON.stringify(img._corners); // reset change state string: + $.ajax('/images/'+img.warpable_id, { // send save request type: 'PATCH', data: { warpable_id: img.warpable_id, @@ -424,6 +560,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') }, @@ -447,6 +586,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') // disable interactivity: 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..a76243b2 --- /dev/null +++ b/app/channels/application_cable/connection.rb @@ -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 diff --git a/app/channels/concurrent_editing_channel.rb b/app/channels/concurrent_editing_channel.rb new file mode 100644 index 00000000..35ea0bb7 --- /dev/null +++ b/app/channels/concurrent_editing_channel.rb @@ -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 diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 6760cf9a..782efc64 100755 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -20,7 +20,9 @@ class ApplicationController < ActionController::Base 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 diff --git a/app/controllers/images_controller.rb b/app/controllers/images_controller.rb index 6ef5d486..9646d130 100644 --- a/app/controllers/images_controller.rb +++ b/app/controllers/images_controller.rb @@ -72,8 +72,8 @@ class ImagesController < ApplicationController def update @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 = [] author = @warpable.map.author # is it really necessary to make new points each time? @@ -92,7 +92,11 @@ class ImagesController < ApplicationController @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 @@ -111,7 +115,7 @@ class ImagesController < ApplicationController @warpable.destroy respond_to do |format| format.html { redirect_to @warpable.map } - format.json { render json: @warpable } + format.json { render json: @warpable.map.fetch_map_data } end else flash[:error] = 'You must be logged in to delete images.' diff --git a/app/models/map.rb b/app/models/map.rb index 7d65e3ec..ab001ff1 100755 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -273,4 +273,10 @@ class Map < ApplicationRecord 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 diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 85bdb4cd..2f5c573b 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -13,6 +13,7 @@ <%= stylesheet_link_tag 'application' %> + <%= action_cable_meta_tag %> <%= javascript_include_tag 'application' %> diff --git a/config/environments/development.rb b/config/environments/development.rb index d4ca60dc..2e870ab0 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -74,4 +74,7 @@ Rails.application.configure do # 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 diff --git a/config/environments/production.rb b/config/environments/production.rb index 91ef1928..7bed767d 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -103,4 +103,6 @@ Rails.application.configure do # Do not dump schema after migrations. config.active_record.dump_schema_after_migration = false + + config.action_cable.allowed_request_origins = [/http:\/\/*/, /https:\/\/*/] end diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb index 4a93d752..0ce2627b 100644 --- a/config/initializers/assets.rb +++ b/config/initializers/assets.rb @@ -11,7 +11,8 @@ Rails.application.configure do config.assets.paths << Rails.root.join('node_modules') config.assets.precompile << /\.(?:svg|eot|woff|ttf)\z/ - config.assets.precompile += ['uploads.js', + config.assets.precompile += [ + 'uploads.js', 'knitter.js', 'annotations.js', 'maps.js', diff --git a/config/puma.rb b/config/puma.rb new file mode 100644 index 00000000..a6860886 --- /dev/null +++ b/config/puma.rb @@ -0,0 +1,5 @@ +#!/usr/bin/env puma + +environment ENV.fetch("RAILS_ENV") { "production" } + +pidfile '/app/tmp/pids/puma.pid' \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 8435a5b2..15c1ded2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -2,6 +2,8 @@ Mapknitter::Application.routes.draw do root to: 'front_ui#index' + mount ActionCable.server => '/cable' + get 'front-page', to: 'front_ui#index' get 'mappers', to: 'front_ui#nearby_mappers' get 'about', to: 'front_ui#about' diff --git a/start.sh b/start.sh index 517a983d..674e2b56 100755 --- a/start.sh +++ b/start.sh @@ -24,4 +24,4 @@ if [ -f $pidfile ] ; then rm $pidfile; fi -bundle exec passenger start +bundle exec foreman start diff --git a/test/channels/concurrent_editing_channel_test.rb b/test/channels/concurrent_editing_channel_test.rb new file mode 100644 index 00000000..b458255e --- /dev/null +++ b/test/channels/concurrent_editing_channel_test.rb @@ -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 diff --git a/test/channels/connection_test.rb b/test/channels/connection_test.rb new file mode 100644 index 00000000..30956577 --- /dev/null +++ b/test/channels/connection_test.rb @@ -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 diff --git a/test/fixtures/maps.yml b/test/fixtures/maps.yml index 2e9c1f20..76dab52c 100644 --- a/test/fixtures/maps.yml +++ b/test/fixtures/maps.yml @@ -34,7 +34,7 @@ nairobi: lat: -1.2920659 lon: 36.8219462 description: Capital of Kenya - created_at: <% Time.now %> + created_at: <%= Time.now %> license: publicdomain user_id: 2 author: aaron @@ -47,7 +47,7 @@ village: lat: -0.023559 lon: 37.90619300000003 description: A mall in Nairobi - created_at: <% Time.now %> + created_at: <%= Time.now %> license: publicdomain user_id: 2 author: aaron diff --git a/test/fixtures/nodes.yml b/test/fixtures/nodes.yml index 3759b4ce..d16cde45 100644 --- a/test/fixtures/nodes.yml +++ b/test/fixtures/nodes.yml @@ -56,3 +56,16 @@ four: way_order: 0 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 diff --git a/test/system/synchronous_editing_test.rb b/test/system/synchronous_editing_test.rb new file mode 100644 index 00000000..2dde3353 --- /dev/null +++ b/test/system/synchronous_editing_test.rb @@ -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 \ No newline at end of file