diff --git a/.gitignore b/.gitignore index 43009ea4..52428f17 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ #assety stuff realtime/node_modules public/assets +public/metamaps_mobile vendor/ #secrets and config diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..58bf9d25 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,9 @@ +sudo: false +language: ruby +rvm: + - 2.1.3 +before_script: + - export RAILS_ENV=test + - cp .example-env .env + - bundle exec rake db:create + - bundle exec rake db:schema:load diff --git a/Gemfile b/Gemfile index 8379a7db..bf5997af 100644 --- a/Gemfile +++ b/Gemfile @@ -6,7 +6,9 @@ gem 'rails', '4.2.4' gem 'devise' gem 'redis' gem 'pg' -gem 'cancancan' +gem 'pundit' +gem 'cancan' +gem 'pundit_extra' gem 'formula' gem 'formtastic' gem 'json' @@ -15,6 +17,12 @@ gem 'best_in_place' #in-place editing gem 'kaminari' # pagination gem 'uservoice-ruby' gem 'dotenv' +gem 'snorlax', '~> 0.1.3' +gem 'httparty' +gem 'sequenced', '~> 2.0.0' +gem 'active_model_serializers', '~> 0.8.1' +gem 'delayed_job', '~> 4.0.2' +gem 'delayed_job_active_record', '~> 4.0.1' gem 'paperclip' gem 'aws-sdk', '< 2.0' diff --git a/Gemfile.lock b/Gemfile.lock index 30817e66..bd124e55 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -20,6 +20,8 @@ GEM erubis (~> 2.7.0) rails-dom-testing (~> 1.0, >= 1.0.5) rails-html-sanitizer (~> 1.0, >= 1.0.2) + active_model_serializers (0.8.3) + activemodel (>= 3.0) activejob (4.2.4) activesupport (= 4.2.4) globalid (>= 0.3.0) @@ -43,7 +45,7 @@ GEM aws-sdk-v1 (1.66.0) json (~> 1.4) nokogiri (>= 1.4.4) - bcrypt (3.1.10) + bcrypt (3.1.11) best_in_place (3.1.0) actionpack (>= 3.2) railties (>= 3.2) @@ -54,24 +56,30 @@ GEM binding_of_caller (0.7.2) debug_inspector (>= 0.0.1) builder (3.2.2) - byebug (5.0.0) - columnize (= 0.9.0) - cancancan (1.13.1) + byebug (8.2.2) + cancan (1.6.10) + cancancan (1.10.1) climate_control (0.0.3) activesupport (>= 3.0) cocaine (0.5.8) climate_control (>= 0.0.3, < 1.0) - coderay (1.1.0) - coffee-rails (4.1.0) + coderay (1.1.1) + coffee-rails (4.1.1) coffee-script (>= 2.2.0) - railties (>= 4.0.0, < 5.0) + railties (>= 4.0.0, < 5.1.x) coffee-script (2.4.1) coffee-script-source execjs coffee-script-source (1.10.0) columnize (0.9.0) + concurrent-ruby (1.0.1) debug_inspector (0.0.2) - devise (3.5.2) + delayed_job (4.0.6) + activesupport (>= 3.0, < 5.0) + delayed_job_active_record (4.0.3) + activerecord (>= 3.0, < 5.0) + delayed_job (>= 3.0, < 4.1) + devise (3.5.6) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 3.2.6, < 5) @@ -80,13 +88,13 @@ GEM warden (~> 1.2.3) diff-lcs (1.2.5) docile (1.1.5) - dotenv (2.0.2) + dotenv (2.1.0) erubis (2.7.0) execjs (2.6.0) ezcrypto (0.7.2) factory_girl (4.5.0) activesupport (>= 3.0.0) - factory_girl_rails (4.5.0) + factory_girl_rails (4.6.0) factory_girl (~> 4.5.0) railties (>= 3.0.0) formtastic (3.1.3) @@ -95,18 +103,21 @@ GEM rails (> 3.0.0) globalid (0.3.6) activesupport (>= 4.1.0) + httparty (0.13.7) + json (~> 1.8) + multi_xml (>= 0.5.2) i18n (0.7.0) - jbuilder (2.3.2) - activesupport (>= 3.0.0, < 5) + jbuilder (2.4.1) + activesupport (>= 3.0.0, < 5.1) multi_json (~> 1.2) - jquery-rails (4.0.5) - rails-dom-testing (~> 1.0) + jquery-rails (4.1.1) + rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) jquery-ui-rails (5.0.5) railties (>= 3.2.16) json (1.8.3) - json-schema (2.6.0) + json-schema (2.6.1) addressable (~> 2.3.8) kaminari (0.16.3) actionpack (>= 3.0.0) @@ -116,31 +127,35 @@ GEM mail (2.6.3) mime-types (>= 1.16, < 3) method_source (0.8.2) - mime-types (2.6.2) + mime-types (2.99.1) mimemagic (0.3.0) mini_portile2 (2.0.0) - minitest (5.8.2) + minitest (5.8.4) multi_json (1.11.2) - nokogiri (1.6.7) + multi_xml (0.5.5) + nokogiri (1.6.7.2) mini_portile2 (~> 2.0.0.rc2) - oauth (0.4.7) + oauth (0.5.1) orm_adapter (0.5.0) - paperclip (4.3.1) + paperclip (4.3.5) activemodel (>= 3.2.0) activesupport (>= 3.2.0) cocaine (~> 0.5.5) mime-types mimemagic (= 0.3.0) - pg (0.18.3) + pg (0.18.4) pry (0.10.3) coderay (~> 1.1.0) method_source (~> 0.8.1) slop (~> 3.4) - pry-byebug (3.2.0) - byebug (~> 5.0) + pry-byebug (3.3.0) + byebug (~> 8.0) pry (~> 0.10) pry-rails (0.3.4) pry (>= 0.9.10) + pundit (1.1.0) + activesupport (>= 3.0.0) + pundit_extra (0.1.1) quiet_assets (1.1.0) railties (>= 3.1, < 5.0) rack (1.6.4) @@ -163,66 +178,73 @@ GEM activesupport (>= 4.2.0.beta, < 5.0) nokogiri (~> 1.6.0) rails-deprecated_sanitizer (>= 1.0.1) - rails-html-sanitizer (1.0.2) + rails-html-sanitizer (1.0.3) loofah (~> 2.0) rails3-jquery-autocomplete (1.0.15) rails (>= 3.2) rails_12factor (0.0.3) rails_serve_static_assets rails_stdout_logging - rails_serve_static_assets (0.0.4) + rails_serve_static_assets (0.0.5) rails_stdout_logging (0.0.4) railties (4.2.4) actionpack (= 4.2.4) activesupport (= 4.2.4) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) - rake (10.4.2) + rake (11.1.0) redis (3.2.2) - responders (2.1.0) - railties (>= 4.2.0, < 5) - rspec-core (3.3.2) - rspec-support (~> 3.3.0) - rspec-expectations (3.3.1) + responders (2.1.1) + railties (>= 4.2.0, < 5.1) + rspec-core (3.4.4) + rspec-support (~> 3.4.0) + rspec-expectations (3.4.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.3.0) - rspec-mocks (3.3.2) + rspec-support (~> 3.4.0) + rspec-mocks (3.4.1) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.3.0) - rspec-rails (3.3.3) + rspec-support (~> 3.4.0) + rspec-rails (3.4.2) actionpack (>= 3.0, < 4.3) activesupport (>= 3.0, < 4.3) railties (>= 3.0, < 4.3) - rspec-core (~> 3.3.0) - rspec-expectations (~> 3.3.0) - rspec-mocks (~> 3.3.0) - rspec-support (~> 3.3.0) - rspec-support (3.3.0) - sass (3.4.19) + rspec-core (~> 3.4.0) + rspec-expectations (~> 3.4.0) + rspec-mocks (~> 3.4.0) + rspec-support (~> 3.4.0) + rspec-support (3.4.1) + sass (3.4.21) sass-rails (5.0.4) railties (>= 4.0.0, < 5.0) sass (~> 3.1) sprockets (>= 2.8, < 4.0) sprockets-rails (>= 2.0, < 4.0) tilt (>= 1.1, < 3) - shoulda-matchers (3.0.1) + sequenced (2.0.0) + activerecord (>= 3.0) + activesupport (>= 3.0) + shoulda-matchers (3.1.1) activesupport (>= 4.0.0) - simplecov (0.11.1) + simplecov (0.11.2) docile (~> 1.1.0) json (~> 1.8) simplecov-html (~> 0.10.0) simplecov-html (0.10.0) slop (3.6.0) - sprockets (3.4.0) + snorlax (0.1.4) + cancancan (~> 1.10.1) + rails (> 4.1) + sprockets (3.5.2) + concurrent-ruby (~> 1.0) rack (> 1, < 3) - sprockets-rails (2.3.3) - actionpack (>= 3.0) - activesupport (>= 3.0) - sprockets (>= 2.8, < 4.0) + sprockets-rails (3.0.4) + actionpack (>= 4.0) + activesupport (>= 4.0) + sprockets (>= 3.0.0) thor (0.19.1) thread_safe (0.3.5) - tilt (2.0.1) - tunemygc (1.0.61) + tilt (2.0.2) + tunemygc (1.0.65) tzinfo (1.2.2) thread_safe (~> 0.1) uglifier (2.7.2) @@ -232,24 +254,28 @@ GEM ezcrypto (>= 0.7.2) json (>= 1.7.5) oauth (>= 0.4.7) - warden (1.2.3) + warden (1.2.6) rack (>= 1.0) PLATFORMS ruby DEPENDENCIES + active_model_serializers (~> 0.8.1) aws-sdk (< 2.0) best_in_place better_errors binding_of_caller - cancancan + cancan coffee-rails + delayed_job (~> 4.0.2) + delayed_job_active_record (~> 4.0.1) devise dotenv factory_girl_rails formtastic formula + httparty jbuilder jquery-rails jquery-ui-rails @@ -260,6 +286,8 @@ DEPENDENCIES pg pry-byebug pry-rails + pundit + pundit_extra quiet_assets rails (= 4.2.4) rails3-jquery-autocomplete @@ -267,8 +295,13 @@ DEPENDENCIES redis rspec-rails sass-rails + sequenced (~> 2.0.0) shoulda-matchers simplecov + snorlax (~> 0.1.3) tunemygc uglifier uservoice-ruby + +BUNDLED WITH + 1.11.2 diff --git a/Procfile b/Procfile index 443f2e35..e00c3019 100644 --- a/Procfile +++ b/Procfile @@ -1 +1,3 @@ web: bundle exec rails server -p $PORT +worker: bundle exec rake jobs:work + diff --git a/README.md b/README.md index 293265ac..cf871b82 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ Metamaps ======= [![Join the chat at https://gitter.im/metamaps/metamaps_gen002](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/metamaps/metamaps_gen002?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -[![Build Status](https://jenkins.devinhoward.ca/job/metamaps_gen002.develop/badge/icon)](https://jenkins.devinhoward.ca/job/metamaps_gen002.develop/) +[![Build Status](https://travis-ci.org/metamaps/metamaps_gen002.svg)](https://travis-ci.org/metamaps/metamaps_gen002) -Welcome to the Metamaps GitHub repo. +Welcome to the Metamaps GitHub repo. ## About @@ -12,7 +12,7 @@ Metamaps is a free and AGPL open source technology for changemakers, innovators, You can find a version of this software running at [metamaps.cc][site-beta], where the technology is being tested in a private beta. -Metamaps is created and maintained by a distributed, nomadic community comprised of technologists, artists and storytellers. You can get in touch with us at team@metamaps.cc or @metamapps on twitter. +Metamaps is created and maintained by a distributed, nomadic community comprised of technologists, artists and storytellers. You can get in touch with us at team@metamaps.cc or @metamapps on twitter. To get connected with the community interested in Metamaps, join our [Google+ community][community]. @@ -52,7 +52,7 @@ We haven't figured out Vagrant for Windows yet, but we have a set of manual inst ## Contributing -Cloning this repository directly is primarily for those wishing to contribute to our codebase. Check out our [contributing instructions][contributing] to get involved. +Cloning this repository directly is primarily for those wishing to contribute to our codebase. Check out our [contributing instructions][contributing] to get involved. ## Community diff --git a/Vagrantfile b/Vagrantfile index dad6c7c7..c9ea9363 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -16,7 +16,6 @@ sudo apt-get install nodejs -y sudo apt-get install npm -y sudo apt-get install postgresql -y sudo apt-get install libpq-dev -y -sudo apt-get install redis-server -y # get imagemagick sudo apt-get install imagemagick --fix-missing diff --git a/app/controllers/api/mappings_controller.rb b/app/controllers/api/mappings_controller.rb new file mode 100644 index 00000000..426c9dbe --- /dev/null +++ b/app/controllers/api/mappings_controller.rb @@ -0,0 +1,3 @@ +class Api::MappingsController < API::RestfulController + +end diff --git a/app/controllers/api/maps_controller.rb b/app/controllers/api/maps_controller.rb new file mode 100644 index 00000000..7b805280 --- /dev/null +++ b/app/controllers/api/maps_controller.rb @@ -0,0 +1,3 @@ +class Api::MapsController < API::RestfulController + +end diff --git a/app/controllers/api/restful_controller.rb b/app/controllers/api/restful_controller.rb new file mode 100644 index 00000000..06396e3d --- /dev/null +++ b/app/controllers/api/restful_controller.rb @@ -0,0 +1,49 @@ +class API::RestfulController < ActionController::Base + include Pundit + include PunditExtra + + snorlax_used_rest! + + rescue_from(Pundit::NotAuthorizedError) { |e| respond_with_standard_error e, 403 } + load_and_authorize_resource only: [:show, :update, :destroy] + + def create + authorize resource_class + instantiate_resouce + resource.user = current_user + create_action + respond_with_resource + end + + private + + def resource_serializer + "new_#{resource_name}_serializer".camelize.constantize + end + + def accessible_records + if current_user + visible_records + else + public_records + end + end + + def current_user + super || token_user || nil + end + + def token_user + authenticate_with_http_token do |token, options| + access_token = Token.find_by_token(token) + if access_token + @token_user ||= access_token.user + end + end + end + + def permitted_params + @permitted_params ||= PermittedParams.new(params) + end + +end diff --git a/app/controllers/api/synapses_controller.rb b/app/controllers/api/synapses_controller.rb new file mode 100644 index 00000000..f133ffd0 --- /dev/null +++ b/app/controllers/api/synapses_controller.rb @@ -0,0 +1,3 @@ +class Api::SynapsesController < API::RestfulController + +end diff --git a/app/controllers/api/tokens_controller.rb b/app/controllers/api/tokens_controller.rb new file mode 100644 index 00000000..3fcca370 --- /dev/null +++ b/app/controllers/api/tokens_controller.rb @@ -0,0 +1,19 @@ +class Api::TokensController < API::RestfulController + + def my_tokens + raise Pundit::NotAuthorizedError.new unless current_user + instantiate_collection page_collection: false, timeframe_collection: false + respond_with_collection + end + + private + + def resource_serializer + "#{resource_name}_serializer".camelize.constantize + end + + def visible_records + current_user.tokens + end + +end diff --git a/app/controllers/api/topics_controller.rb b/app/controllers/api/topics_controller.rb new file mode 100644 index 00000000..f3633544 --- /dev/null +++ b/app/controllers/api/topics_controller.rb @@ -0,0 +1,3 @@ +class Api::TopicsController < API::RestfulController + +end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 5a32742d..6e67ba25 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,9 +1,16 @@ class ApplicationController < ActionController::Base + include Pundit + include PunditExtra + rescue_from Pundit::NotAuthorizedError, with: :handle_unauthorized protect_from_forgery - before_filter :get_invite_link + before_action :get_invite_link after_action :allow_embedding + def default_serializer_options + { root: false } + end + # this is for global login include ContentHelper @@ -12,13 +19,7 @@ class ApplicationController < ActionController::Base helper_method :admin? def after_sign_in_path_for(resource) - unsafe_uri = request.env["REQUEST_URI"] - if unsafe_uri.starts_with?('http') && !unsafe_uri.starts_with?('https') - protocol = 'http' - else - protocol = 'https' - end - sign_in_url = url_for(:action => 'new', :controller => 'sessions', :only_path => false, :protocol => protocol) + sign_in_url = url_for(:action => 'new', :controller => 'sessions', :only_path => false, :protocol => 'https') if request.referer == sign_in_url super @@ -29,6 +30,10 @@ class ApplicationController < ActionController::Base end end + def handle_unauthorized + head :forbidden # TODO make this better + end + private def require_no_user diff --git a/app/controllers/main_controller.rb b/app/controllers/main_controller.rb index 1cd0f577..7bc22c7e 100644 --- a/app/controllers/main_controller.rb +++ b/app/controllers/main_controller.rb @@ -3,20 +3,20 @@ class MainController < ApplicationController include MapsHelper include UsersHelper include SynapsesHelper + + after_action :verify_policy_scoped respond_to :html, :json # home page def home - @current = current_user - + @maps = policy_scope(Map).order("updated_at DESC").page(1).per(20) respond_to do |format| format.html { - if authenticated? - @maps = Map.where("maps.permission != ?", "private").order("updated_at DESC").page(1).per(20) - respond_with(@maps, @current) + if not authenticated? + render 'main/home' else - respond_with(@current) + render 'maps/activemaps' end } end @@ -59,69 +59,35 @@ class MainController < ApplicationController filterByMetacode = m end end + + search = '%' + term.downcase + '%' + builder = policy_scope(Topic) if filterByMetacode if term == "" - @topics = [] + builder = builder.none else - search = term.downcase + '%' - - if user - @topics = Set.new(Topic.where('LOWER("name") like ?', search).where('metacode_id = ? AND user_id = ?', filterByMetacode.id, user).order('"name"')) - @topics2 = Set.new(Topic.where('LOWER("name") like ?', '%' + search).where('metacode_id = ? AND user_id = ?', filterByMetacode.id, user).order('"name"')) - @topics3 = Set.new(Topic.where('LOWER("desc") like ?', '%' + search).where('metacode_id = ? AND user_id = ?', filterByMetacode.id, user).order('"name"')) - @topics4 = Set.new(Topic.where('LOWER("link") like ?', '%' + search).where('metacode_id = ? AND user_id = ?', filterByMetacode.id, user).order('"name"')) - else - @topics = Set.new(Topic.where('LOWER("name") like ?', search).where('metacode_id = ?', filterByMetacode.id).order('"name"')) - @topics2 = Set.new(Topic.where('LOWER("name") like ?', '%' + search).where('metacode_id = ?', filterByMetacode.id).order('"name"')) - @topics3 = Set.new(Topic.where('LOWER("desc") like ?', '%' + search).where('metacode_id = ?', filterByMetacode.id).order('"name"')) - @topics4 = Set.new(Topic.where('LOWER("link") like ?', '%' + search).where('metacode_id = ?', filterByMetacode.id).order('"name"')) - end - - #get unique elements only through the magic of Sets - @topics = (@topics + @topics2 + @topics3 + @topics4).to_a + builder = builder.where('LOWER("name") like ? OR + LOWER("desc") like ? OR + LOWER("link") like ?', search, search, search) + builder = builder.where(metacode_id: filterByMetacode.id) end elsif desc - search = '%' + term.downcase + '%' - if !user - @topics = Topic.where('LOWER("desc") like ?', search).order('"name"') - elsif user - @topics = Topic.where('LOWER("desc") like ?', search).where('user_id = ?', user).order('"name"') - end + builder = builder.where('LOWER("desc") like ?', search) elsif link - search = '%' + term.downcase + '%' - if !user - @topics = Topic.where('LOWER("link") like ?', search).order('"name"') - elsif user - @topics = Topic.where('LOWER("link") like ?', search).where('user_id = ?', user).order('"name"') - end + builder = builder.where('LOWER("link") like ?', search) else #regular case, just search the name - search = term.downcase + '%' - if !user - @topics = Topic.where('LOWER("name") like ?', search).order('"name"') - @topics2 = Topic.where('LOWER("name") like ?', '%' + search).order('"name"') - @topics3 = Topic.where('LOWER("desc") like ?', '%' + search).order('"name"') - @topics4 = Topic.where('LOWER("link") like ?', '%' + search).order('"name"') - @topics = @topics + (@topics2 - @topics) - @topics = @topics + (@topics3 - @topics) - @topics = @topics + (@topics4 - @topics) - elsif user - @topics = Topic.where('LOWER("name") like ?', search).where('user_id = ?', user).order('"name"') - @topics2 = Topic.where('LOWER("name") like ?', '%' + search).where('user_id = ?', user).order('"name"') - @topics3 = Topic.where('LOWER("desc") like ?', '%' + search).where('user_id = ?', user).order('"name"') - @topics4 = Topic.where('LOWER("link") like ?', '%' + search).where('user_id = ?', user).order('"name"') - @topics = @topics + (@topics2 - @topics) - @topics = @topics + (@topics3 - @topics) - @topics = @topics + (@topics4 - @topics) - end + builder = builder.where('LOWER("name") like ? OR + LOWER("desc") like ? OR + LOWER("link") like ?', search, search, search) end + + builder = builder.where(user: user) if user + @topics = builder.order(:name) else @topics = [] end - - #read this next line as 'delete a topic if its private and you're either 1. logged out or 2. logged in but not the topic creator - @topics.to_a.delete_if {|t| t.permission == "private" && (!authenticated? || (authenticated? && current_user.id != t.user_id)) } - + render json: autocomplete_array_json(@topics) end @@ -141,21 +107,21 @@ class MainController < ApplicationController term = term[5..-1] desc = true end + search = '%' + term.downcase + '%' - query = desc ? 'LOWER("desc") like ?' : 'LOWER("name") like ?' - if !user - # !connor why is the limit 5 done here and not above? also, why not limit after sorting alphabetically? - @maps = Map.where(query, search).limit(5).order('"name"') - elsif user - @maps = Map.where(query, search).where('user_id = ?', user).order('"name"') + builder = policy_scope(Map) + + if desc + builder = builder.where('LOWER("desc") like ?', search) + else + builder = builder.where('LOWER("name") like ?', search) end + builder = builder.where(user: user) if user + @maps = builder.order(:name) else @maps = [] end - #read this next line as 'delete a map if its private and you're either 1. logged out or 2. logged in but not the map creator - @maps.to_a.delete_if {|m| m.permission == "private" && (!authenticated? || (authenticated? && current_user.id != m.user_id)) } - render json: autocomplete_map_array_json(@maps) end @@ -166,7 +132,10 @@ class MainController < ApplicationController #remove "mapper:" if appended at beginning term = term[7..-1] if term.downcase[0..6] == "mapper:" - @mappers = User.where('LOWER("name") like ?', term.downcase + '%').order('"name"') + search = term.downcase + '%' + builder = policy_scope(User) # TODO do I need to policy scope? I guess yes to verify_policy_scoped + builder = builder.where('LOWER("name") like ?', search) + @mappers = builder.order(:name) else @mappers = [] end @@ -181,7 +150,7 @@ class MainController < ApplicationController topic2id = params[:topic2id] if term && !term.empty? - @synapses = Synapse.where('LOWER("desc") like ?', '%' + term.downcase + '%').order('"desc"') + @synapses = policy_scope(Synapse).where('LOWER("desc") like ?', '%' + term.downcase + '%').order('"desc"') # remove any duplicate synapse types that just differ by # leading or trailing whitespaces @@ -195,23 +164,18 @@ class MainController < ApplicationController boolean = true end } - - #limit to 5 results - @synapses = @synapses.slice(0,5) elsif topic1id && !topic1id.empty? - @one = Synapse.where('node1_id = ? AND node2_id = ?', topic1id, topic2id) - @two = Synapse.where('node2_id = ? AND node1_id = ?', topic1id, topic2id) + @one = policy_scope(Synapse).where('node1_id = ? AND node2_id = ?', topic1id, topic2id) + @two = policy_scope(Synapse).where('node2_id = ? AND node1_id = ?', topic1id, topic2id) @synapses = @one + @two @synapses.sort! {|s1,s2| s1.desc <=> s2.desc }.to_a - - #permissions - @synapses.delete_if {|s| s.permission == "private" && !authenticated? } - @synapses.delete_if {|s| s.permission == "private" && authenticated? && current_user.id != s.user_id } else @synapses = [] end + #limit to 5 results + @synapses = @synapses.slice(0,5) + render json: autocomplete_synapse_array_json(@synapses) end - end diff --git a/app/controllers/mappings_controller.rb b/app/controllers/mappings_controller.rb index c20b0153..936ffcc2 100644 --- a/app/controllers/mappings_controller.rb +++ b/app/controllers/mappings_controller.rb @@ -1,12 +1,15 @@ class MappingsController < ApplicationController - - before_filter :require_user, only: [:create, :update, :destroy] + + before_action :require_user, only: [:create, :update, :destroy] + after_action :verify_authorized, except: :index + after_action :verify_policy_scoped, only: :index respond_to :json # GET /mappings/1.json def show @mapping = Mapping.find(params[:id]) + authorize @mapping render json: @mapping end @@ -14,9 +17,12 @@ class MappingsController < ApplicationController # POST /mappings.json def create @mapping = Mapping.new(mapping_params) + authorize @mapping + @mapping.user = current_user if @mapping.save render json: @mapping, status: :created + Events::NewMapping.publish!(@mapping, current_user) else render json: @mapping.errors, status: :unprocessable_entity end @@ -25,6 +31,7 @@ class MappingsController < ApplicationController # PUT /mappings/1.json def update @mapping = Mapping.find(params[:id]) + authorize @mapping if @mapping.update_attributes(mapping_params) head :no_content @@ -36,7 +43,7 @@ class MappingsController < ApplicationController # DELETE /mappings/1.json def destroy @mapping = Mapping.find(params[:id]) - @map = @mapping.map + authorize @mapping @mapping.destroy @@ -46,6 +53,6 @@ class MappingsController < ApplicationController private # Never trust parameters from the scary internet, only allow the white list through. def mapping_params - params.require(:mapping).permit(:id, :xloc, :yloc, :mappable_id, :mappable_type, :map_id, :user_id) + params.require(:mapping).permit(:id, :xloc, :yloc, :mappable_id, :mappable_type, :map_id) end end diff --git a/app/controllers/maps_controller.rb b/app/controllers/maps_controller.rb index f2e3d239..fa743d93 100644 --- a/app/controllers/maps_controller.rb +++ b/app/controllers/maps_controller.rb @@ -1,67 +1,84 @@ class MapsController < ApplicationController - before_filter :require_user, only: [:create, :update, :screenshot, :destroy] + + before_action :require_user, only: [:create, :update, :screenshot, :destroy] + after_action :verify_authorized, except: [:activemaps, :featuredmaps, :mymaps, :usermaps] + after_action :verify_policy_scoped, only: [:activemaps, :featuredmaps, :mymaps, :usermaps] respond_to :html, :json, :csv autocomplete :map, :name, :full => true, :extra_data => [:user_id] # GET /explore/active - # GET /explore/featured - # GET /explore/mapper/:id - def index - return redirect_to activemaps_url if request.path == "/explore" - - @current = current_user - @maps = [] + def activemaps page = params[:page].present? ? params[:page] : 1 - - if request.path.index("/explore/active") != nil - @maps = Map.where("maps.permission != ?", "private").order("updated_at DESC").page(page).per(20) - @request = "active" - elsif request.path.index("/explore/featured") != nil - @maps = Map.where("maps.featured = ? AND maps.permission != ?", true, "private").order("updated_at DESC").page(page).per(20) - @request = "featured" - elsif request.path.index('/explore/mine') != nil # looking for maps by me - return redirect_to activemaps_url if !authenticated? - - # don't need to exclude private maps because they all belong to you - @maps = Map.where("maps.user_id = ?", @current.id).order("updated_at DESC").page(page).per(20) - @request = "you" - elsif request.path.index('/explore/mapper/') != nil # looking for maps by a mapper - @user = User.find(params[:id]) - @maps = Map.where("maps.user_id = ? AND maps.permission != ?", @user.id, "private").order("updated_at DESC").page(page).per(20) - @request = "mapper" - end + @maps = policy_scope(Map).order("updated_at DESC") + .page(page).per(20) respond_to do |format| - format.html { - if @request == "active" && authenticated? - redirect_to root_url and return - end - respond_with(@maps, @request, @user) + format.html { + # root url => main/home. main/home renders maps/activemaps view. + redirect_to root_url and return if authenticated? + respond_with(@maps, @user) } format.json { render json: @maps } end end + # GET /explore/featured + def featuredmaps + page = params[:page].present? ? params[:page] : 1 + @maps = policy_scope( + Map.where("maps.featured = ? AND maps.permission != ?", + true, "private") + ).order("updated_at DESC").page(page).per(20) + + respond_to do |format| + format.html { respond_with(@maps, @user) } + format.json { render json: @maps } + end + end + + # GET /explore/mine + def mymaps + return redirect_to activemaps_url if !authenticated? + + page = params[:page].present? ? params[:page] : 1 + @maps = policy_scope( + Map.where("maps.user_id = ?", current_user.id) + ).order("updated_at DESC").page(page).per(20) + + respond_to do |format| + format.html { respond_with(@maps, @user) } + format.json { render json: @maps } + end + end + + # GET /explore/mapper/:id + def usermaps + page = params[:page].present? ? params[:page] : 1 + @user = User.find(params[:id]) + @maps = policy_scope(Map.where(user: @user)) + .order("updated_at DESC").page(page).per(20) + + respond_to do |format| + format.html { respond_with(@maps, @user) } + format.json { render json: @maps } + end + end + # GET maps/:id def show - - @current = current_user - @map = Map.find(params[:id]).authorize_to_show(@current) - - if not @map - redirect_to root_url, notice: "Access denied. That map is private." and return - end + @map = Map.find(params[:id]) + authorize @map respond_to do |format| format.html { @allmappers = @map.contributors - @alltopics = @map.topics.to_a.delete_if {|t| t.permission == "private" && (!authenticated? || (authenticated? && @current.id != t.user_id)) } - @allsynapses = @map.synapses.to_a.delete_if {|s| s.permission == "private" && (!authenticated? || (authenticated? && @current.id != s.user_id)) } - @allmappings = @map.mappings.to_a.delete_if {|m| + @alltopics = @map.topics.to_a.delete_if {|t| t.permission == "private" && (!authenticated? || (authenticated? && current_user.id != t.user_id)) } + @allsynapses = @map.synapses.to_a.delete_if {|s| s.permission == "private" && (!authenticated? || (authenticated? && current_user.id != s.user_id)) } + @allmappings = @map.mappings.to_a.delete_if {|m| object = m.mappable - !object || (object.permission == "private" && (!authenticated? || (authenticated? && @current.id != object.user_id))) + !object || (object.permission == "private" && (!authenticated? || (authenticated? && current_user.id != object.user_id))) } @allmessages = @map.messages.sort_by(&:created_at) @@ -75,20 +92,15 @@ class MapsController < ApplicationController # GET maps/:id/contains def contains - - @current = current_user - @map = Map.find(params[:id]).authorize_to_show(@current) - - if not @map - redirect_to root_url, notice: "Access denied. That map is private." and return - end + @map = Map.find(params[:id]) + authorize @map @allmappers = @map.contributors - @alltopics = @map.topics.to_a.delete_if {|t| t.permission == "private" && (!authenticated? || (authenticated? && @current.id != t.user_id)) } - @allsynapses = @map.synapses.to_a.delete_if {|s| s.permission == "private" && (!authenticated? || (authenticated? && @current.id != s.user_id)) } - @allmappings = @map.mappings.to_a.delete_if {|m| + @alltopics = @map.topics.to_a.delete_if {|t| t.permission == "private" && (!authenticated? || (authenticated? && current_user.id != t.user_id)) } + @allsynapses = @map.synapses.to_a.delete_if {|s| s.permission == "private" && (!authenticated? || (authenticated? && current_user.id != s.user_id)) } + @allmappings = @map.mappings.to_a.delete_if {|m| object = m.mappable - !object || (object.permission == "private" && (!authenticated? || (authenticated? && @current.id != object.user_id))) + !object || (object.permission == "private" && (!authenticated? || (authenticated? && current_user.id != object.user_id))) } @json = Hash.new() @@ -125,6 +137,7 @@ class MapsController < ApplicationController mapping.xloc = topic[1] mapping.yloc = topic[2] @map.topicmappings << mapping + authorize mapping, :create mapping.save end @@ -137,6 +150,7 @@ class MapsController < ApplicationController mapping.map = @map mapping.mappable = Synapse.find(synapse_id) @map.synapsemappings << mapping + authorize mapping, :create mapping.save end end @@ -144,6 +158,8 @@ class MapsController < ApplicationController @map.arranged = true end + authorize @map + if @map.save respond_to do |format| format.json { render :json => @map } @@ -157,13 +173,11 @@ class MapsController < ApplicationController # PUT maps/:id def update - @current = current_user - @map = Map.find(params[:id]).authorize_to_edit(@current) + @map = Map.find(params[:id]) + authorize @map respond_to do |format| - if !@map - format.json { render json: "unauthorized" } - elsif @map.update_attributes(map_params) + if @map.update_attributes(map_params) format.json { head :no_content } else format.json { render json: @map.errors, status: :unprocessable_entity } @@ -173,51 +187,42 @@ class MapsController < ApplicationController # POST maps/:id/upload_screenshot def screenshot - @current = current_user - @map = Map.find(params[:id]).authorize_to_edit(@current) + @map = Map.find(params[:id]) + authorize @map - if @map - png = Base64.decode64(params[:encoded_image]['data:image/png;base64,'.length .. -1]) - StringIO.open(png) do |data| - data.class.class_eval { attr_accessor :original_filename, :content_type } - data.original_filename = "map-" + @map.id.to_s + "-screenshot.png" - data.content_type = "image/png" - @map.screenshot = data - end - - if @map.save - render :json => {:message => "Successfully uploaded the map screenshot."} - else - render :json => {:message => "Failed to upload image."} - end - else - render :json => {:message => "Unauthorized to set map screenshot."} - end + png = Base64.decode64(params[:encoded_image]['data:image/png;base64,'.length .. -1]) + StringIO.open(png) do |data| + data.class.class_eval { attr_accessor :original_filename, :content_type } + data.original_filename = "map-" + @map.id.to_s + "-screenshot.png" + data.content_type = "image/png" + @map.screenshot = data + end + + if @map.save + render :json => {:message => "Successfully uploaded the map screenshot."} + else + render :json => {:message => "Failed to upload image."} + end end # DELETE maps/:id def destroy - @current = current_user + @map = Map.find(params[:id]) + authorize @map - @map = Map.find(params[:id]).authorize_to_delete(@current) + @map.delete - @map.delete if @map - - respond_to do |format| - format.json { - if @map - render json: "success" - else - render json: "unauthorized" - end - } + respond_to do |format| + format.json do + head :no_content end + end end private # Never trust parameters from the scary internet, only allow the white list through. def map_params - params.require(:map).permit(:id, :name, :arranged, :desc, :permission, :user_id) + params.require(:map).permit(:id, :name, :arranged, :desc, :permission) end end diff --git a/app/controllers/metacode_sets_controller.rb b/app/controllers/metacode_sets_controller.rb index 720076c1..e76f4c9a 100644 --- a/app/controllers/metacode_sets_controller.rb +++ b/app/controllers/metacode_sets_controller.rb @@ -1,6 +1,6 @@ class MetacodeSetsController < ApplicationController - before_filter :require_admin + before_action :require_admin # GET /metacode_sets # GET /metacode_sets.json diff --git a/app/controllers/metacodes_controller.rb b/app/controllers/metacodes_controller.rb index 54956c60..77f9ba54 100644 --- a/app/controllers/metacodes_controller.rb +++ b/app/controllers/metacodes_controller.rb @@ -1,5 +1,5 @@ class MetacodesController < ApplicationController - before_filter :require_admin, except: [:index] + before_action :require_admin, except: [:index] # GET /metacodes # GET /metacodes.json diff --git a/app/controllers/synapses_controller.rb b/app/controllers/synapses_controller.rb index f19dc053..4440872f 100644 --- a/app/controllers/synapses_controller.rb +++ b/app/controllers/synapses_controller.rb @@ -1,20 +1,17 @@ class SynapsesController < ApplicationController include TopicsHelper - before_filter :require_user, only: [:create, :update, :destroy] + before_action :require_user, only: [:create, :update, :destroy] + after_action :verify_authorized, except: :index + after_action :verify_policy_scoped, only: :index respond_to :json # GET /synapses/1.json def show @synapse = Synapse.find(params[:id]) + authorize @synapse - #.authorize_to_show(@current) - - #if not @synapse - # redirect_to root_url and return - #end - render json: @synapse end @@ -23,6 +20,7 @@ class SynapsesController < ApplicationController def create @synapse = Synapse.new(synapse_params) @synapse.desc = "" if @synapse.desc.nil? + authorize @synapse respond_to do |format| if @synapse.save @@ -38,6 +36,7 @@ class SynapsesController < ApplicationController def update @synapse = Synapse.find(params[:id]) @synapse.desc = "" if @synapse.desc.nil? + authorize @synapse respond_to do |format| if @synapse.update_attributes(synapse_params) @@ -50,8 +49,9 @@ class SynapsesController < ApplicationController # DELETE synapses/:id def destroy - @synapse = Synapse.find(params[:id]).authorize_to_delete(current_user) - @synapse.delete if @synapse + @synapse = Synapse.find(params[:id]) + authorize @synapse + @synapse.delete respond_to do |format| format.json { head :no_content } diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb index 19062ba9..1b1e9b3c 100644 --- a/app/controllers/topics_controller.rb +++ b/app/controllers/topics_controller.rb @@ -1,21 +1,16 @@ class TopicsController < ApplicationController include TopicsHelper - before_filter :require_user, only: [:create, :update, :destroy] - + before_action :require_user, only: [:create, :update, :destroy] + after_action :verify_authorized, except: :autocomplete_topic + respond_to :html, :js, :json # GET /topics/autocomplete_topic def autocomplete_topic - @current = current_user term = params[:term] if term && !term.empty? - @topics = Topic.where('LOWER("name") like ?', term.downcase + '%').order('"name"') - - #read this next line as 'delete a topic if its private and you're either - #1. logged out or 2. logged in but not the topic creator - @topics.to_a.delete_if {|t| t.permission == "private" && - (!authenticated? || (authenticated? && @current.id != t.user_id)) } + @topics = policy_scope(Topic.where('LOWER("name") like ?', term.downcase + '%')).order('"name"') else @topics = [] end @@ -24,29 +19,16 @@ class TopicsController < ApplicationController # GET topics/:id def show - @current = current_user - @topic = Topic.find(params[:id]).authorize_to_show(@current) - - if not @topic - redirect_to root_url, notice: "Access denied. That topic is private." and return - end + @topic = Topic.find(params[:id]) + authorize @topic respond_to do |format| format.html { - @alltopics = ([@topic] + @topic.relatives).delete_if {|t| t.permission == "private" && (!authenticated? || (authenticated? && @current.id != t.user_id)) } # should limit to topics visible to user - @allsynapses = @topic.synapses.to_a.delete_if {|s| s.permission == "private" && (!authenticated? || (authenticated? && @current.id != s.user_id)) } + @alltopics = ([@topic] + policy_scope(Topic.relatives(@topic.id))) + @allsynapses = policy_scope(Synapse.for_topic(@topic.id)) - @allcreators = [] - @alltopics.each do |t| - if @allcreators.index(t.user) == nil - @allcreators.push(t.user) - end - end - @allsynapses.each do |s| - if @allcreators.index(s.user) == nil - @allcreators.push(s.user) - end - end + @allcreators = @alltopics.map(&:user).uniq + @allcreators += @allsynapses.map(&:user).uniq respond_with(@allsynapses, @alltopics, @allcreators, @topic) } @@ -56,28 +38,15 @@ class TopicsController < ApplicationController # GET topics/:id/network def network - @current = current_user - @topic = Topic.find(params[:id]).authorize_to_show(@current) + @topic = Topic.find(params[:id]) + authorize @topic - if not @topic - redirect_to root_url, notice: "Access denied. That topic is private." and return - end + @alltopics = [@topic] + policy_scope(@topic.relatives) + @allsynapses = policy_scope(@topic.synapses) + + @allcreators = @alltopics.map(&:user).uniq + @allcreators += @allsynapses.map(&:user).uniq - @alltopics = @topic.relatives.to_a.delete_if {|t| t.permission == "private" && (!authenticated? || (authenticated? && @current.id != t.user_id)) } - @allsynapses = @topic.synapses.to_a.delete_if {|s| s.permission == "private" && (!authenticated? || (authenticated? && @current.id != s.user_id)) } - @allcreators = [] - @allcreators.push(@topic.user) - @alltopics.each do |t| - if @allcreators.index(t.user) == nil - @allcreators.push(t.user) - end - end - @allsynapses.each do |s| - if @allcreators.index(s.user) == nil - @allcreators.push(s.user) - end - end - @json = Hash.new() @json['topic'] = @topic @json['creators'] = @allcreators @@ -91,121 +60,99 @@ class TopicsController < ApplicationController # GET topics/:id/relative_numbers def relative_numbers - @current = current_user - @topic = Topic.find(params[:id]).authorize_to_show(@current) + @topic = Topic.find(params[:id]) + authorize @topic - if not @topic - redirect_to root_url, notice: "Access denied. That topic is private." and return + topicsAlreadyHas = params[:network] ? params[:network].split(',').map(&:to_i) : [] + + @alltopics = policy_scope(@topic.relatives).to_a.uniq + @alltopics.delete_if! do |topic| + topicsAlreadyHas.index(topic.id) != nil end - @topicsAlreadyHas = params[:network] ? params[:network].split(',') : [] - - @alltopics = @topic.relatives.to_a.delete_if {|t| - @topicsAlreadyHas.index(t.id.to_s) != nil || - (t.permission == "private" && (!authenticated? || (authenticated? && @current.id != t.user_id))) - } - - @alltopics.uniq! - - @json = Hash.new() + @json = Hash.new(0) @alltopics.each do |t| - if @json[t.metacode.id] - @json[t.metacode.id] += 1 - else - @json[t.metacode.id] = 1 - end + @json[t.metacode.id] += 1 end respond_to do |format| - format.json { render json: @json } + format.json { render json: @json } end end # GET topics/:id/relatives def relatives - @current = current_user - @topic = Topic.find(params[:id]).authorize_to_show(@current) + @topic = Topic.find(params[:id]) + authorize @topic - if not @topic - redirect_to root_url, notice: "Access denied. That topic is private." and return - end + topicsAlreadyHas = params[:network] ? params[:network].split(',').map(&:to_i) : [] - @topicsAlreadyHas = params[:network] ? params[:network].split(',') : [] + alltopics = policy_scope(@topic.relatives).to_a.uniq.delete_if do |topic| + topicsAlreadyHas.index(topic.id.to_s) != nil + end - @alltopics = @topic.relatives.to_a.delete_if {|t| - @topicsAlreadyHas.index(t.id.to_s) != nil || - (params[:metacode] && t.metacode_id.to_s != params[:metacode]) || - (t.permission == "private" && (!authenticated? || (authenticated? && @current.id != t.user_id))) - } + #find synapses between topics in alltopics array + allsynapses = policy_scope(@topic.synapses) + synapse_ids = (allsynapses.map(&:topic1_id) + allsynapses.map(&:topic2_id)).uniq + allsynapses.delete_if! do |synapse| + synapse_ids.index(synapse.id) != nil + end - @alltopics.uniq! + creatorsAlreadyHas = params[:creators] ? params[:creators].split(',').map(&:to_i) : [] + allcreators = (alltopics.map(&:user) + allsynapses.map(&:user)).uniq.delete_if do |user| + creatorsAlreadyHas.index(user.id) != nil + end - @allsynapses = @topic.synapses.to_a.delete_if {|s| - (s.topic1 == @topic && @alltopics.index(s.topic2) == nil) || - (s.topic2 == @topic && @alltopics.index(s.topic1) == nil) - } + @json = Hash.new() + @json['topics'] = alltopics + @json['synapses'] = allsynapses + @json['creators'] = allcreators - @creatorsAlreadyHas = params[:creators] ? params[:creators].split(',') : [] - @allcreators = [] - @alltopics.each do |t| - if @allcreators.index(t.user) == nil && @creatorsAlreadyHas.index(t.user_id.to_s) == nil - @allcreators.push(t.user) - end - end - @allsynapses.each do |s| - if @allcreators.index(s.user) == nil && @creatorsAlreadyHas.index(s.user_id.to_s) == nil - @allcreators.push(s.user) - end - end - - @json = Hash.new() - @json['topics'] = @alltopics - @json['synapses'] = @allsynapses - @json['creators'] = @allcreators - - respond_to do |format| - format.json { render json: @json } - end + respond_to do |format| + format.json { render json: @json } + end end - # POST /topics - # POST /topics.json - def create - @topic = Topic.new(topic_params) + # POST /topics + # POST /topics.json + def create + @topic = Topic.new(topic_params) + authorize @topic - respond_to do |format| - if @topic.save - format.json { render json: @topic, status: :created } - else - format.json { render json: @topic.errors, status: :unprocessable_entity } - end - end + respond_to do |format| + if @topic.save + format.json { render json: @topic, status: :created } + else + format.json { render json: @topic.errors, status: :unprocessable_entity } + end end + end - # PUT /topics/1 - # PUT /topics/1.json - def update - @topic = Topic.find(params[:id]) + # PUT /topics/1 + # PUT /topics/1.json + def update + @topic = Topic.find(params[:id]) + authorize @topic - respond_to do |format| - if @topic.update_attributes(topic_params) - format.json { head :no_content } - else - format.json { render json: @topic.errors, status: :unprocessable_entity } - end - end + respond_to do |format| + if @topic.update_attributes(topic_params) + format.json { head :no_content } + else + format.json { render json: @topic.errors, status: :unprocessable_entity } + end end + end - # DELETE topics/:id - def destroy - @current = current_user - @topic = Topic.find(params[:id]).authorize_to_delete(@current) - @topic.delete if @topic + # DELETE topics/:id + def destroy + @topic = Topic.find(params[:id]) + authorize @topic - respond_to do |format| - format.json { head :no_content } - end + @topic.delete + respond_to do |format| + format.json { head :no_content } end + end private diff --git a/app/controllers/users/registrations_controller.rb b/app/controllers/users/registrations_controller.rb index c77edb50..88474e21 100644 --- a/app/controllers/users/registrations_controller.rb +++ b/app/controllers/users/registrations_controller.rb @@ -1,6 +1,6 @@ class Users::RegistrationsController < Devise::RegistrationsController - before_filter :configure_sign_up_params, only: [:create] - before_filter :configure_account_update_params, only: [:update] + before_action :configure_sign_up_params, only: [:create] + before_action :configure_account_update_params, only: [:update] protected def after_sign_up_path_for(resource) diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 063ab866..f5b0aab9 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1,5 +1,5 @@ class UsersController < ApplicationController - before_filter :require_user, only: [:edit, :update, :updatemetacodes] + before_action :require_user, only: [:edit, :update, :updatemetacodes] respond_to :html, :json diff --git a/app/models/concerns/routing.rb b/app/models/concerns/routing.rb new file mode 100644 index 00000000..2f8467bf --- /dev/null +++ b/app/models/concerns/routing.rb @@ -0,0 +1,10 @@ +module Routing + extend ActiveSupport::Concern + include Rails.application.routes.url_helpers + + included do + def default_url_options + ActionMailer::Base.default_url_options + end + end +end diff --git a/app/models/event.rb b/app/models/event.rb new file mode 100644 index 00000000..b49b4856 --- /dev/null +++ b/app/models/event.rb @@ -0,0 +1,34 @@ +class Event < ActiveRecord::Base + KINDS = %w[topic_added_to_map synapse_added_to_map] + + #has_many :notifications, dependent: :destroy + belongs_to :eventable, polymorphic: true + belongs_to :map + belongs_to :user + + scope :sequenced, -> { where('sequence_id is not null').order('sequence_id asc') } + scope :chronologically, -> { order('created_at asc') } + + after_create :notify_webhooks!, if: :map + + validates_inclusion_of :kind, :in => KINDS + validates_presence_of :eventable + + acts_as_sequenced scope: :map_id, column: :sequence_id, skip: lambda {|e| e.map.nil? || e.map_id.nil? } + + #def notify!(user) + # notifications.create!(user: user) + #end + + def belongs_to?(this_user) + self.user_id == this_user.id + end + + def notify_webhooks! + #group = self.discussion.group + self.map.webhooks.each { |webhook| WebhookService.publish! webhook: webhook, event: self } + #group.webhooks.each { |webhook| WebhookService.publish! webhook: webhook, event: self } + end + handle_asynchronously :notify_webhooks! + +end diff --git a/app/models/events/new_mapping.rb b/app/models/events/new_mapping.rb new file mode 100644 index 00000000..7d6e0696 --- /dev/null +++ b/app/models/events/new_mapping.rb @@ -0,0 +1,18 @@ +class Events::NewMapping < Event + #after_create :notify_users! + + def self.publish!(mapping, user) + create!(kind: mapping.mappable_type == "Topic" ? "topic_added_to_map" : "synapse_added_to_map", + eventable: mapping, + map: mapping.map, + user: user) + end + + private + + #def notify_users! + # unless comment_vote.user == comment_vote.comment_user + # notify!(comment_vote.comment_user) + # end + #end +end diff --git a/app/models/map.rb b/app/models/map.rb index d1ab0cfb..5cd30bbe 100644 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -8,6 +8,9 @@ class Map < ActiveRecord::Base has_many :synapses, through: :synapsemappings, source: :mappable, source_type: "Synapse" has_many :messages, as: :resource, dependent: :destroy + has_many :webhooks, as: :hookable + has_many :events, -> { includes :user }, as: :eventable, dependent: :destroy + # This method associates the attribute ":image" with a file attachment has_attached_file :screenshot, :styles => { :thumb => ['188x126#', :png] @@ -98,36 +101,7 @@ class Map < ActiveRecord::Base end end end - - ##### PERMISSIONS ###### - - def authorize_to_delete(user) - if (self.user != user) - return false - end - return self - end - - # returns false if user not allowed to 'show' Topic, Synapse, or Map - def authorize_to_show(user) - if (self.permission == "private" && self.user != user) - return false - end - return self - end - - # returns false if user not allowed to 'edit' Topic, Synapse, or Map - def authorize_to_edit(user) - if !user - return false - elsif (self.permission == "private" && self.user != user) - return false - elsif (self.permission == "public" && self.user != user) - return false - end - return self - end - + def decode_base64(imgBase64) decoded_data = Base64.decode64(imgBase64) diff --git a/app/models/mapping.rb b/app/models/mapping.rb index d034de1c..1a37f490 100644 --- a/app/models/mapping.rb +++ b/app/models/mapping.rb @@ -25,5 +25,5 @@ class Mapping < ActiveRecord::Base def as_json(options={}) super(:methods =>[:user_name, :user_image]) end - + end diff --git a/app/models/permitted_params.rb b/app/models/permitted_params.rb new file mode 100644 index 00000000..a49ed648 --- /dev/null +++ b/app/models/permitted_params.rb @@ -0,0 +1,33 @@ +class PermittedParams < Struct.new(:params) + + %w[map synapse topic mapping token].each do |kind| + define_method(kind) do + permitted_attributes = self.send("#{kind}_attributes") + params.require(kind).permit(*permitted_attributes) + end + alias_method :"api_#{kind}", kind.to_sym + end + + alias :read_attribute_for_serialization :send + + def token_attributes + [:description] + end + + def map_attributes + [:name, :desc, :permission, :arranged] + end + + def synapse_attributes + [:desc, :category, :weight, :permission, :node1_id, :node2_id] + end + + def topic_attributes + [:name, :desc, :link, :permission, :metacode_id] + end + + def mapping_attributes + [:xloc, :yloc, :map_id, :mappable_type, :mappable_id] + end + +end diff --git a/app/models/synapse.rb b/app/models/synapse.rb index ea5889cc..540376bb 100644 --- a/app/models/synapse.rb +++ b/app/models/synapse.rb @@ -10,10 +10,16 @@ class Synapse < ActiveRecord::Base validates :desc, length: { minimum: 0, allow_nil: false } validates :permission, presence: true + validates :node1_id, presence: true + validates :node2_id, presence: true validates :permission, inclusion: { in: Perm::ISSIONS.map(&:to_s) } validates :category, inclusion: { in: ['from-to', 'both'], allow_nil: true } + scope :for_topic, ->(topic_id = nil) { + where("node1_id = ? OR node2_id = ?", topic_id, topic_id) + } + # :nocov: def user_name user.name @@ -32,30 +38,4 @@ class Synapse < ActiveRecord::Base end # :nocov: - ##### PERMISSIONS ###### - - # returns false if user not allowed to 'show' Topic, Synapse, or Map - def authorize_to_show(user) - if (self.permission == "private" && self.user != user) - return false - end - return self - end - - # returns false if user not allowed to 'edit' Topic, Synapse, or Map - def authorize_to_edit(user) - if (self.permission == "private" && self.user != user) - return false - elsif (self.permission == "public" && self.user != user) - return false - end - return self - end - - def authorize_to_delete(user) - if (self.user == user || user.admin) - return self - end - return false - end end diff --git a/app/models/token.rb b/app/models/token.rb new file mode 100644 index 00000000..714730fc --- /dev/null +++ b/app/models/token.rb @@ -0,0 +1,22 @@ +class Token < ActiveRecord::Base + belongs_to :user + + before_create :assign_token + + CHARS = 32 + + private + def assign_token + self.token = generate_token + end + + def generate_token + loop do + candidate = SecureRandom.base64(CHARS).gsub(/\W/, '') + if candidate.size >= CHARS + return candidate[0...CHARS] + end + end + end + +end diff --git a/app/models/topic.rb b/app/models/topic.rb index 3e66366b..b2e8635b 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -41,6 +41,13 @@ class Topic < ActiveRecord::Base belongs_to :metacode + scope :relatives, ->(topic_id = nil) { + includes(:synapses1) + .includes(:synapses2) + .where('synapses.node1_id = ? OR synapses.node2_id = ?', topic_id, topic_id) + .references(:synapses) + } + def user_name user.name end @@ -119,31 +126,4 @@ class Topic < ActiveRecord::Base end output end - - ##### PERMISSIONS ###### - - # returns false if user not allowed to 'show' Topic, Synapse, or Map - def authorize_to_show(user) - if (self.permission == "private" && self.user != user) - return false - end - return self - end - - # returns false if user not allowed to 'edit' Topic, Synapse, or Map - def authorize_to_edit(user) - if (self.permission == "private" && self.user != user) - return false - elsif (self.permission == "public" && self.user != user) - return false - end - return self - end - - def authorize_to_delete(user) - if (self.user == user || user.admin) - return self - end - return false - end end diff --git a/app/models/user.rb b/app/models/user.rb index 39a5e00f..10a0a71c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -6,6 +6,7 @@ class User < ActiveRecord::Base has_many :synapses has_many :maps has_many :mappings + has_many :tokens after_create :generate_code diff --git a/app/models/webhook.rb b/app/models/webhook.rb new file mode 100644 index 00000000..4e20272c --- /dev/null +++ b/app/models/webhook.rb @@ -0,0 +1,13 @@ +class Webhook < ActiveRecord::Base + belongs_to :hookable, polymorphic: true + + validates :uri, presence: true + validates :hookable, presence: true + validates_inclusion_of :kind, in: %w[slack] + validates :event_types, length: { minimum: 1 } + + def headers + {} + end + +end diff --git a/app/models/webhooks/slack/base.rb b/app/models/webhooks/slack/base.rb new file mode 100644 index 00000000..98503916 --- /dev/null +++ b/app/models/webhooks/slack/base.rb @@ -0,0 +1,72 @@ +Webhooks::Slack::Base = Struct.new(:event) do + include Routing + + def username + "Metamaps Bot" + end + + def icon_url + "https://pbs.twimg.com/profile_images/539300245029392385/dJ1bwnw7.jpeg" + end + + def text + "something" + end + + def attachments + [{ + title: attachment_title, + text: attachment_text, + fields: attachment_fields, + fallback: attachment_fallback + }] + end + + alias :read_attribute_for_serialization :send + + private + + #def motion_vote_field + # { + # title: "Vote on this proposal", + # value: "#{proposal_link(eventable, "yes")} · " + + # "#{proposal_link(eventable, "abstain")} · " + + # "#{proposal_link(eventable, "no")} · " + + # "#{proposal_link(eventable, "block")}" + # } + #end + + def view_map_on_metamaps(text = nil) + "<#{map_url(eventable.map)}|#{text || eventable.map.name}>" + end + + #def view_discussion_on_loomio(params = {}) + # { value: discussion_link(I18n.t(:"webhooks.slack.view_it_on_loomio"), params) } + #end + + #def proposal_link(proposal, position = nil) + # discussion_link position || proposal.name, { proposal: proposal.key, position: position } + #end + + #def discussion_link(text = nil, params = {}) + # "<#{discussion_url(eventable.map, params)}|#{text || eventable.discussion.title}>" + #end + + def eventable + @eventable ||= event.eventable + end + + def author + @author ||= eventable.author + end + +end + +#webhooks: +# slack: +# motion_closed: "*%{name}* has closed" +# motion_closing_soon: "*%{name}* has a proposal closing in 24 hours" +# motion_outcome_created: "*%{author}* published an outcome in *%{name}*" +# motion_outcome_updated: "*%{author}* updated the outcome for *%{name}*" +# new_motion: "*%{author}* started a new proposal in *%{name}*" +# view_it_on_loomio: "View it on Loomio" diff --git a/app/models/webhooks/slack/synapse_added_to_map.rb b/app/models/webhooks/slack/synapse_added_to_map.rb new file mode 100644 index 00000000..3d70450b --- /dev/null +++ b/app/models/webhooks/slack/synapse_added_to_map.rb @@ -0,0 +1,26 @@ +class Webhooks::Slack::SynapseAddedToMap < Webhooks::Slack::Base + + def text + "\"*#{eventable.mappable.topic1.name}* #{eventable.mappable.desc || '->'} *#{eventable.mappable.topic2.name}*\" was added as a connection to the map *#{view_map_on_metamaps()}*" + end + + def attachment_fallback + "" #{}"*#{eventable.name}*\n#{eventable.description}\n" + end + + def attachment_title + "" #proposal_link(eventable) + end + + def attachment_text + "" # "#{eventable.description}\n" + end + + def attachment_fields + [{ + title: "nothing", + value: "nothing" + }] #[motion_vote_field] + end + +end diff --git a/app/models/webhooks/slack/topic_added_to_map.rb b/app/models/webhooks/slack/topic_added_to_map.rb new file mode 100644 index 00000000..3574a464 --- /dev/null +++ b/app/models/webhooks/slack/topic_added_to_map.rb @@ -0,0 +1,27 @@ +class Webhooks::Slack::TopicAddedToMap < Webhooks::Slack::Base + + def text + "New #{eventable.mappable.metacode.name} topic *#{eventable.mappable.name}* was added to the map *#{view_map_on_metamaps()}*" + end + # todo: it would be sweet if it sends it with the metacode as the icon_url + + def attachment_fallback + "" #{}"*#{eventable.name}*\n#{eventable.description}\n" + end + + def attachment_title + "" #proposal_link(eventable) + end + + def attachment_text + "" # "#{eventable.description}\n" + end + + def attachment_fields + [{ + title: "nothing", + value: "nothing" + }] #[motion_vote_field] + end + +end diff --git a/app/policies/application_policy.rb b/app/policies/application_policy.rb new file mode 100644 index 00000000..39b7a961 --- /dev/null +++ b/app/policies/application_policy.rb @@ -0,0 +1,61 @@ +class ApplicationPolicy + attr_reader :user, :record + + def initialize(user, record) + @user = user + @record = record + end + + def index? + false + end + + def show? + scope.where(:id => record.id).exists? + end + + def create? + false + end + + def new? + create? + end + + def update? + false + end + + def edit? + update? + end + + def destroy? + false + end + + # TODO update this function to enable some flag in the interface + # so that admins usually can't do super admin stuff unless they + # explicitly say they want to (E.g. seeing/editing/deleting private + # maps - they should be able to, but not by accident) + def admin_override + user && user.admin + end + + def scope + Pundit.policy_scope!(user, record.class) + end + + class Scope + attr_reader :user, :scope + + def initialize(user, scope) + @user = user + @scope = scope + end + + def resolve + scope + end + end +end diff --git a/app/policies/main_policy.rb b/app/policies/main_policy.rb new file mode 100644 index 00000000..ee7f9fc9 --- /dev/null +++ b/app/policies/main_policy.rb @@ -0,0 +1,26 @@ +class MainPolicy < ApplicationPolicy + def initialize(user, record) + @user = user + @record = nil + end + + def home? + true + end + + def searchtopics? + true + end + + def searchmaps? + true + end + + def searchmappers? + true + end + + def searchsynapses? + true + end +end diff --git a/app/policies/map_policy.rb b/app/policies/map_policy.rb new file mode 100644 index 00000000..65f721bf --- /dev/null +++ b/app/policies/map_policy.rb @@ -0,0 +1,53 @@ +class MapPolicy < ApplicationPolicy + class Scope < Scope + def resolve + visible = ['public', 'commons'] + permission = 'maps.permission IN (?)' + if user + scope.where(permission + ' OR maps.user_id = ?', visible, user.id) + else + scope.where(permission, visible) + end + end + end + + def activemaps? + user.blank? # redirect to root url if authenticated for some reason + end + + def featuredmaps? + true + end + + def mymaps? + user.present? + end + + def usermaps? + true + end + + def show? + record.permission == 'commons' || record.permission == 'public' || record.user == user + end + + def contains? + show? + end + + def create? + user.present? + end + + def update? + user.present? && (record.permission == 'commons' || record.user == user) + end + + def screenshot? + update? + end + + def destroy? + record.user == user || admin_override + end +end diff --git a/app/policies/mapping_policy.rb b/app/policies/mapping_policy.rb new file mode 100644 index 00000000..ed93bc66 --- /dev/null +++ b/app/policies/mapping_policy.rb @@ -0,0 +1,35 @@ +class MappingPolicy < ApplicationPolicy + class Scope < Scope + def resolve + # TODO base this on the map policy + # it would be nice if we could also base this on the mappable, but that + # gets really complicated. Devin thinks it's OK to SHOW a mapping for + # a private topic, since you can't see the private topic anyways + visible = ['public', 'commons'] + permission = 'maps.permission IN (?)' + if user + scope.joins(:maps).where(permission + ' OR maps.user_id = ?', visible, user.id) + else + scope.where(permission, visible) + end + end + end + + def show? + map = Pundit.policy(user, record.map) + mappable = Pundit.policy(user, record.mappable) + map.show? && mappable.show? + end + + def create? + Pundit.policy(user, record.map).update? + end + + def update? + Pundit.policy(user, record.map).update? + end + + def destroy? + record.user == user || admin_override + end +end diff --git a/app/policies/synapse_policy.rb b/app/policies/synapse_policy.rb new file mode 100644 index 00000000..042c9a75 --- /dev/null +++ b/app/policies/synapse_policy.rb @@ -0,0 +1,30 @@ +class SynapsePolicy < ApplicationPolicy + class Scope < Scope + def resolve + visible = ['public', 'commons'] + permission = 'synapses.permission IN (?)' + if user + scope.where(permission + ' OR synapses.user_id = ?', visible, user.id) + else + scope.where(permission, visible) + end + end + end + + def create? + user.present? + # todo add validation against whether you can see both topics + end + + def show? + record.permission == 'commons' || record.permission == 'public' || record.user == user + end + + def update? + user.present? && (record.permission == 'commons' || record.user == user) + end + + def destroy? + record.user == user || admin_override + end +end diff --git a/app/policies/token_policy.rb b/app/policies/token_policy.rb new file mode 100644 index 00000000..393d2441 --- /dev/null +++ b/app/policies/token_policy.rb @@ -0,0 +1,24 @@ +class TokenPolicy < ApplicationPolicy + class Scope < Scope + def resolve + if user + scope.where('tokens.user_id = ?', user.id) + else + where(:id => nil).where("id IS NOT ?", nil) # to just return none + end + end + end + + def create? + user.present? + end + + def my_tokens? + user.present? + end + + def destroy? + user.present? && record.user == user + end + +end diff --git a/app/policies/topic_policy.rb b/app/policies/topic_policy.rb new file mode 100644 index 00000000..335a2ed2 --- /dev/null +++ b/app/policies/topic_policy.rb @@ -0,0 +1,45 @@ +class TopicPolicy < ApplicationPolicy + class Scope < Scope + def resolve + visible = ['public', 'commons'] + permission = 'topics.permission IN (?)' + if user + scope.where(permission + ' OR topics.user_id = ?', visible, user.id) + else + scope.where(permission, visible) + end + end + end + + def create? + user.present? + end + + def show? + record.permission == 'commons' || record.permission == 'public' || record.user == user + end + + def update? + user.present? && (record.permission == 'commons' || record.user == user) + end + + def destroy? + record.user == user || admin_override + end + + def autocomplete_topic? + user.present? + end + + def network? + show? + end + + def relative_numbers? + show? + end + + def relatives? + show? + end +end diff --git a/app/serializers/event_serializer.rb b/app/serializers/event_serializer.rb new file mode 100644 index 00000000..0e87cd44 --- /dev/null +++ b/app/serializers/event_serializer.rb @@ -0,0 +1,15 @@ +class EventSerializer < ActiveModel::Serializer + embed :ids, include: true + attributes :id, :sequence_id, :kind, :map_id, :created_at + + has_one :actor, serializer: NewUserSerializer, root: 'users' + has_one :map, serializer: NewMapSerializer + + def actor + object.user || object.eventable.try(:user) + end + + def map + object.eventable.try(:map) || object.eventable.map + end +end diff --git a/app/serializers/new_map_serializer.rb b/app/serializers/new_map_serializer.rb new file mode 100644 index 00000000..9b2ff400 --- /dev/null +++ b/app/serializers/new_map_serializer.rb @@ -0,0 +1,16 @@ +class NewMapSerializer < ActiveModel::Serializer + embed :ids, include: true + attributes :id, + :name, + :desc, + :permission, + :screenshot, + :created_at, + :updated_at + + has_many :topics, serializer: NewTopicSerializer + has_many :synapses, serializer: NewSynapseSerializer + has_many :mappings, serializer: NewMappingSerializer + has_many :contributors, root: :users, serializer: NewUserSerializer + +end diff --git a/app/serializers/new_mapping_serializer.rb b/app/serializers/new_mapping_serializer.rb new file mode 100644 index 00000000..4e65e161 --- /dev/null +++ b/app/serializers/new_mapping_serializer.rb @@ -0,0 +1,19 @@ +class NewMappingSerializer < ActiveModel::Serializer + embed :ids, include: true + attributes :id, + :xloc, + :yloc, + :created_at, + :updated_at, + :mappable_id, + :mappable_type + + has_one :user, serializer: NewUserSerializer + has_one :map, serializer: NewMapSerializer + + def filter(keys) + keys.delete(:xloc) unless object.mappable_type == "Topic" + keys.delete(:yloc) unless object.mappable_type == "Topic" + keys + end +end diff --git a/app/serializers/new_metacode_serializer.rb b/app/serializers/new_metacode_serializer.rb new file mode 100644 index 00000000..e664e7ea --- /dev/null +++ b/app/serializers/new_metacode_serializer.rb @@ -0,0 +1,7 @@ +class NewMetacodeSerializer < ActiveModel::Serializer + attributes :id, + :name, + :manual_icon, + :color, + :aws_icon +end diff --git a/app/serializers/new_synapse_serializer.rb b/app/serializers/new_synapse_serializer.rb new file mode 100644 index 00000000..b145d605 --- /dev/null +++ b/app/serializers/new_synapse_serializer.rb @@ -0,0 +1,15 @@ +class NewSynapseSerializer < ActiveModel::Serializer + embed :ids, include: true + attributes :id, + :desc, + :category, + :weight, + :permission, + :created_at, + :updated_at + + has_one :topic1, root: :topics, serializer: NewTopicSerializer + has_one :topic2, root: :topics, serializer: NewTopicSerializer + has_one :user, serializer: NewUserSerializer + +end diff --git a/app/serializers/new_topic_serializer.rb b/app/serializers/new_topic_serializer.rb new file mode 100644 index 00000000..cdbbedf9 --- /dev/null +++ b/app/serializers/new_topic_serializer.rb @@ -0,0 +1,14 @@ +class NewTopicSerializer < ActiveModel::Serializer + embed :ids, include: true + attributes :id, + :name, + :desc, + :link, + :permission, + :created_at, + :updated_at + + has_one :user, serializer: NewUserSerializer + has_one :metacode, serializer: NewMetacodeSerializer + +end diff --git a/app/serializers/new_user_serializer.rb b/app/serializers/new_user_serializer.rb new file mode 100644 index 00000000..45be36b0 --- /dev/null +++ b/app/serializers/new_user_serializer.rb @@ -0,0 +1,15 @@ +class NewUserSerializer < ActiveModel::Serializer + attributes :id, + :name, + :avatar, + :is_admin, + :generation + + def avatar + object.image.url(:sixtyfour) + end + + def is_admin + object.admin + end +end diff --git a/app/serializers/token_serializer.rb b/app/serializers/token_serializer.rb new file mode 100644 index 00000000..7abcc3df --- /dev/null +++ b/app/serializers/token_serializer.rb @@ -0,0 +1,8 @@ +class TokenSerializer < ActiveModel::Serializer + attributes :id, + :token, + :description, + :created_at, + :updated_at + +end diff --git a/app/serializers/webhook_serializer.rb b/app/serializers/webhook_serializer.rb new file mode 100644 index 00000000..ed013cae --- /dev/null +++ b/app/serializers/webhook_serializer.rb @@ -0,0 +1,3 @@ +class WebhookSerializer < ActiveModel::Serializer + attributes :text, :username, :icon_url #, :attachments +end diff --git a/app/services/webhook_service.rb b/app/services/webhook_service.rb new file mode 100644 index 00000000..7a4361b4 --- /dev/null +++ b/app/services/webhook_service.rb @@ -0,0 +1,18 @@ +class WebhookService + + def self.publish!(webhook:, event:) + return false unless webhook.event_types.include? event.kind + HTTParty.post webhook.uri, body: payload_for(webhook, event), headers: webhook.headers + end + + private + + def self.payload_for(webhook, event) + WebhookSerializer.new(webhook_object_for(webhook, event), root: false).to_json + end + + def self.webhook_object_for(webhook, event) + "Webhooks::#{webhook.kind.classify}::#{event.kind.classify}".constantize.new(event) + end + +end diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index c09d227e..0480cb39 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -81,7 +81,7 @@ classes += controller_name == "maps" && action_name == "index" ? " explorePage" : "" if controller_name == "maps" && action_name == "show" classes += " mapPage" - if @map.authorize_to_edit(current_user) + if policy(@map).update? classes += " canEditMap" end if @map.permission == "commons" diff --git a/app/views/main/home.html.erb b/app/views/main/home.html.erb index 3c1319e0..6089b7df 100644 --- a/app/views/main/home.html.erb +++ b/app/views/main/home.html.erb @@ -2,52 +2,42 @@ # @file # Located at / # Shows 3 most recently created topics, synapses, and maps. - #%> - -<% if !authenticated? %> - <% content_for :title, "Home | Metamaps" %> -
Designers, inventors, artists, educators, strategists, consultants, facilitators, entrepreneurs, systems thinkers, changemakers, analysts, students, researchers... maybe you!
- - EXPLORE FEATURED MAPS - REQUEST INVITE -Designers, inventors, artists, educators, strategists, consultants, facilitators, entrepreneurs, systems thinkers, changemakers, analysts, students, researchers... maybe you!
+ + EXPLORE FEATURED MAPS + REQUEST INVITE +