mirror of
https://github.com/publiclab/mapknitter.git
synced 2025-12-05 16:00:00 +01:00
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:
committed by
Jeffrey Warren
parent
1ac63abb67
commit
cee7479cf4
@@ -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:
|
||||||
|
|||||||
8
Gemfile
8
Gemfile
@@ -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
|
||||||
|
|||||||
17
Gemfile.lock
17
Gemfile.lock
@@ -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
2
Procfile
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
passenger: passenger start
|
||||||
|
puma: puma -C config/puma.rb
|
||||||
30
SYNCHRONOUS_EDITING.md
Normal file
30
SYNCHRONOUS_EDITING.md
Normal 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
|
||||||
@@ -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 .
|
||||||
|
|||||||
11
app/assets/javascripts/cable.js
Normal file
11
app/assets/javascripts/cable.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
//
|
||||||
|
//= require action_cable
|
||||||
|
//= require_self
|
||||||
|
//= require_tree ./channels
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
this.App || (this.App = {});
|
||||||
|
|
||||||
|
App.cable = ActionCable.createConsumer();
|
||||||
|
|
||||||
|
}).call(this);
|
||||||
31
app/assets/javascripts/channels/concurrent_editing.js
Normal file
31
app/assets/javascripts/channels/concurrent_editing.js
Normal 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()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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:
|
||||||
|
|||||||
4
app/channels/application_cable/channel.rb
Normal file
4
app/channels/application_cable/channel.rb
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
module ApplicationCable
|
||||||
|
class Channel < ActionCable::Channel::Base
|
||||||
|
end
|
||||||
|
end
|
||||||
17
app/channels/application_cable/connection.rb
Normal file
17
app/channels/application_cable/connection.rb
Normal 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
|
||||||
17
app/channels/concurrent_editing_channel.rb
Normal file
17
app/channels/concurrent_editing_channel.rb
Normal 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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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.'
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
5
config/puma.rb
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
#!/usr/bin/env puma
|
||||||
|
|
||||||
|
environment ENV.fetch("RAILS_ENV") { "production" }
|
||||||
|
|
||||||
|
pidfile '/app/tmp/pids/puma.pid'
|
||||||
@@ -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'
|
||||||
|
|||||||
2
start.sh
2
start.sh
@@ -24,4 +24,4 @@ if [ -f $pidfile ] ; then
|
|||||||
rm $pidfile;
|
rm $pidfile;
|
||||||
fi
|
fi
|
||||||
|
|
||||||
bundle exec passenger start
|
bundle exec foreman start
|
||||||
|
|||||||
23
test/channels/concurrent_editing_channel_test.rb
Normal file
23
test/channels/concurrent_editing_channel_test.rb
Normal 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
|
||||||
31
test/channels/connection_test.rb
Normal file
31
test/channels/connection_test.rb
Normal 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
|
||||||
4
test/fixtures/maps.yml
vendored
4
test/fixtures/maps.yml
vendored
@@ -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
|
||||||
|
|||||||
13
test/fixtures/nodes.yml
vendored
13
test/fixtures/nodes.yml
vendored
@@ -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
|
||||||
|
|||||||
16
test/system/synchronous_editing_test.rb
Normal file
16
test/system/synchronous_editing_test.rb
Normal 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
|
||||||
Reference in New Issue
Block a user