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/**/*'
|
||||
- 'Dangerfile'
|
||||
- 'app/views/**/*'
|
||||
- 'app/assets/javascripts/application.js'
|
||||
TargetRubyVersion: '2.4'
|
||||
|
||||
Layout/MultilineMethodCallIndentation:
|
||||
|
||||
8
Gemfile
8
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
|
||||
|
||||
17
Gemfile.lock
17
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
|
||||
|
||||
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-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 .
|
||||
|
||||
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(); }
|
||||
},
|
||||
|
||||
/* 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 () {
|
||||
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:
|
||||
|
||||
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]
|
||||
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
|
||||
|
||||
@@ -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.'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<link rel="shortcut icon" href="/images/mapknitter-255.png">
|
||||
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.2/css/all.css" integrity="sha384-oS3vJWv+0UjzBfQzYUhtDYW+Pj2yciDJxpsK1OYPAYjqT085Qq/1cq5FLXAZQ7Ay" crossorigin="anonymous">
|
||||
<%= stylesheet_link_tag 'application' %>
|
||||
<%= action_cable_meta_tag %>
|
||||
<%= javascript_include_tag 'application' %>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
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'
|
||||
|
||||
mount ActionCable.server => '/cable'
|
||||
|
||||
get 'front-page', to: 'front_ui#index'
|
||||
get 'mappers', to: 'front_ui#nearby_mappers'
|
||||
get 'about', to: 'front_ui#about'
|
||||
|
||||
2
start.sh
2
start.sh
@@ -24,4 +24,4 @@ if [ -f $pidfile ] ; then
|
||||
rm $pidfile;
|
||||
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
|
||||
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
|
||||
|
||||
13
test/fixtures/nodes.yml
vendored
13
test/fixtures/nodes.yml
vendored
@@ -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
|
||||
|
||||
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